From 05a7a0d67f7fb920c4bdc22da598e0e6a4c5d4f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Wed, 13 May 2026 00:22:00 -0400 Subject: [PATCH] Present Servo BGRA hardware surfaces --- Cargo.lock | 2 - Cargo.toml | 4 + README.md | 4 +- .../ely_app/src/services/iosurface_metal.rs | 42 +- crates/ely_app/src/services/servo_live.rs | 67 +- .../src/services/servo_sidecar_command.rs | 57 +- crates/ely_app/src/shell/web_surface.rs | 8 +- crates/ely_app/src/shell/web_surface_frame.rs | 61 +- crates/ely_app/src/shell/web_surface_tests.rs | 35 +- crates/ely_app/src/shell/web_surface_view.rs | 27 +- .../tests/hardware_rendering_context.rs | 9 +- scripts/create_macos_app_bundle.sh | 2 +- third_party/gpui/Cargo.toml | 624 ++ third_party/gpui/LICENSE-APACHE | 222 + third_party/gpui/README.md | 66 + third_party/gpui/build.rs | 457 ++ .../gpui/resources/windows/gpui.manifest.xml | 16 + third_party/gpui/resources/windows/gpui.rc | 2 + .../gpui/src/_ownership_and_data_flow.rs | 140 + third_party/gpui/src/action.rs | 440 ++ third_party/gpui/src/app.rs | 2460 ++++++++ third_party/gpui/src/app/async_context.rs | 488 ++ third_party/gpui/src/app/context.rs | 824 +++ third_party/gpui/src/app/entity_map.rs | 889 +++ third_party/gpui/src/app/test_context.rs | 1047 ++++ third_party/gpui/src/arena.rs | 289 + third_party/gpui/src/asset_cache.rs | 84 + third_party/gpui/src/assets.rs | 107 + third_party/gpui/src/bounds_tree.rs | 337 ++ third_party/gpui/src/color.rs | 934 +++ third_party/gpui/src/colors.rs | 122 + third_party/gpui/src/element.rs | 769 +++ third_party/gpui/src/elements/anchored.rs | 292 + third_party/gpui/src/elements/animation.rs | 263 + third_party/gpui/src/elements/canvas.rs | 95 + third_party/gpui/src/elements/deferred.rs | 96 + third_party/gpui/src/elements/div.rs | 3250 +++++++++++ third_party/gpui/src/elements/image_cache.rs | 353 ++ third_party/gpui/src/elements/img.rs | 767 +++ third_party/gpui/src/elements/list.rs | 1287 +++++ third_party/gpui/src/elements/mod.rs | 25 + third_party/gpui/src/elements/surface.rs | 120 + third_party/gpui/src/elements/svg.rs | 221 + third_party/gpui/src/elements/text.rs | 914 +++ third_party/gpui/src/elements/uniform_list.rs | 710 +++ third_party/gpui/src/executor.rs | 611 ++ third_party/gpui/src/geometry.rs | 3912 +++++++++++++ third_party/gpui/src/global.rs | 75 + third_party/gpui/src/gpui.rs | 311 + third_party/gpui/src/input.rs | 180 + third_party/gpui/src/inspector.rs | 254 + third_party/gpui/src/interactive.rs | 670 +++ third_party/gpui/src/key_dispatch.rs | 843 +++ third_party/gpui/src/keymap.rs | 713 +++ third_party/gpui/src/keymap/binding.rs | 143 + third_party/gpui/src/keymap/context.rs | 760 +++ third_party/gpui/src/path_builder.rs | 347 ++ third_party/gpui/src/platform.rs | 1862 ++++++ third_party/gpui/src/platform/app_menu.rs | 252 + third_party/gpui/src/platform/blade.rs | 11 + .../gpui/src/platform/blade/apple_compat.rs | 60 + .../gpui/src/platform/blade/blade_atlas.rs | 384 ++ .../gpui/src/platform/blade/blade_context.rs | 80 + .../gpui/src/platform/blade/blade_renderer.rs | 1072 ++++ .../gpui/src/platform/blade/shaders.wgsl | 1296 +++++ third_party/gpui/src/platform/keyboard.rs | 41 + third_party/gpui/src/platform/keystroke.rs | 767 +++ third_party/gpui/src/platform/linux.rs | 29 + .../gpui/src/platform/linux/dispatcher.rs | 129 + .../gpui/src/platform/linux/headless.rs | 3 + .../src/platform/linux/headless/client.rs | 134 + .../gpui/src/platform/linux/keyboard.rs | 22 + .../gpui/src/platform/linux/platform.rs | 1039 ++++ .../gpui/src/platform/linux/text_system.rs | 581 ++ .../gpui/src/platform/linux/wayland.rs | 46 + .../gpui/src/platform/linux/wayland/client.rs | 2159 +++++++ .../src/platform/linux/wayland/clipboard.rs | 262 + .../gpui/src/platform/linux/wayland/cursor.rs | 152 + .../src/platform/linux/wayland/display.rs | 42 + .../gpui/src/platform/linux/wayland/serial.rs | 49 + .../gpui/src/platform/linux/wayland/window.rs | 1219 ++++ third_party/gpui/src/platform/linux/x11.rs | 12 + .../gpui/src/platform/linux/x11/client.rs | 2491 ++++++++ .../gpui/src/platform/linux/x11/clipboard.rs | 1270 ++++ .../gpui/src/platform/linux/x11/display.rs | 51 + .../gpui/src/platform/linux/x11/event.rs | 154 + .../gpui/src/platform/linux/x11/window.rs | 1670 ++++++ .../src/platform/linux/x11/xim_handler.rs | 133 + .../src/platform/linux/xdg_desktop_portal.rs | 171 + third_party/gpui/src/platform/mac.rs | 163 + .../src/platform/mac/attributed_string.rs | 119 + third_party/gpui/src/platform/mac/dispatch.h | 2 + .../gpui/src/platform/mac/dispatcher.rs | 75 + third_party/gpui/src/platform/mac/display.rs | 117 + .../gpui/src/platform/mac/display_link.rs | 283 + third_party/gpui/src/platform/mac/events.rs | 533 ++ third_party/gpui/src/platform/mac/keyboard.rs | 1500 +++++ .../gpui/src/platform/mac/metal_atlas.rs | 281 + .../gpui/src/platform/mac/metal_renderer.rs | 1390 +++++ .../gpui/src/platform/mac/open_type.rs | 147 + third_party/gpui/src/platform/mac/platform.rs | 1709 ++++++ .../gpui/src/platform/mac/screen_capture.rs | 334 ++ .../gpui/src/platform/mac/shaders.metal | 1246 ++++ .../gpui/src/platform/mac/status_item.rs | 388 ++ .../gpui/src/platform/mac/text_system.rs | 846 +++ third_party/gpui/src/platform/mac/window.rs | 2647 +++++++++ .../src/platform/mac/window_appearance.rs | 37 + .../gpui/src/platform/scap_screen_capture.rs | 325 ++ third_party/gpui/src/platform/test.rs | 11 + .../gpui/src/platform/test/dispatcher.rs | 314 + third_party/gpui/src/platform/test/display.rs | 33 + .../gpui/src/platform/test/platform.rs | 463 ++ third_party/gpui/src/platform/test/window.rs | 362 ++ third_party/gpui/src/platform/windows.rs | 40 + .../platform/windows/alpha_correction.hlsl | 28 + .../gpui/src/platform/windows/clipboard.rs | 388 ++ .../platform/windows/color_text_raster.hlsl | 44 + .../src/platform/windows/destination_list.rs | 201 + .../gpui/src/platform/windows/direct_write.rs | 1923 +++++++ .../src/platform/windows/directx_atlas.rs | 308 + .../src/platform/windows/directx_devices.rs | 197 + .../src/platform/windows/directx_renderer.rs | 1758 ++++++ .../gpui/src/platform/windows/dispatcher.rs | 110 + .../gpui/src/platform/windows/display.rs | 255 + .../gpui/src/platform/windows/events.rs | 1563 +++++ .../gpui/src/platform/windows/keyboard.rs | 404 ++ .../gpui/src/platform/windows/platform.rs | 1169 ++++ .../gpui/src/platform/windows/shaders.hlsl | 1182 ++++ .../src/platform/windows/system_settings.rs | 197 + third_party/gpui/src/platform/windows/util.rs | 219 + .../gpui/src/platform/windows/vsync.rs | 81 + .../gpui/src/platform/windows/window.rs | 1413 +++++ .../gpui/src/platform/windows/wrapper.rs | 53 + third_party/gpui/src/prelude.rs | 9 + third_party/gpui/src/scene.rs | 833 +++ third_party/gpui/src/shared_string.rs | 145 + third_party/gpui/src/shared_uri.rs | 25 + third_party/gpui/src/style.rs | 1472 +++++ third_party/gpui/src/styled.rs | 766 +++ third_party/gpui/src/subscription.rs | 209 + third_party/gpui/src/svg_renderer.rs | 104 + third_party/gpui/src/tab_stop.rs | 611 ++ third_party/gpui/src/taffy.rs | 608 ++ third_party/gpui/src/test.rs | 161 + third_party/gpui/src/text_system.rs | 918 +++ .../gpui/src/text_system/font_fallbacks.rs | 21 + .../gpui/src/text_system/font_features.rs | 154 + third_party/gpui/src/text_system/line.rs | 591 ++ .../gpui/src/text_system/line_layout.rs | 672 +++ .../gpui/src/text_system/line_wrapper.rs | 743 +++ third_party/gpui/src/util.rs | 175 + third_party/gpui/src/view.rs | 373 ++ third_party/gpui/src/window.rs | 5103 +++++++++++++++++ third_party/gpui/src/window/prompts.rs | 231 + 154 files changed, 84157 insertions(+), 115 deletions(-) create mode 100644 third_party/gpui/Cargo.toml create mode 100644 third_party/gpui/LICENSE-APACHE create mode 100644 third_party/gpui/README.md create mode 100644 third_party/gpui/build.rs create mode 100644 third_party/gpui/resources/windows/gpui.manifest.xml create mode 100644 third_party/gpui/resources/windows/gpui.rc create mode 100644 third_party/gpui/src/_ownership_and_data_flow.rs create mode 100644 third_party/gpui/src/action.rs create mode 100644 third_party/gpui/src/app.rs create mode 100644 third_party/gpui/src/app/async_context.rs create mode 100644 third_party/gpui/src/app/context.rs create mode 100644 third_party/gpui/src/app/entity_map.rs create mode 100644 third_party/gpui/src/app/test_context.rs create mode 100644 third_party/gpui/src/arena.rs create mode 100644 third_party/gpui/src/asset_cache.rs create mode 100644 third_party/gpui/src/assets.rs create mode 100644 third_party/gpui/src/bounds_tree.rs create mode 100644 third_party/gpui/src/color.rs create mode 100644 third_party/gpui/src/colors.rs create mode 100644 third_party/gpui/src/element.rs create mode 100644 third_party/gpui/src/elements/anchored.rs create mode 100644 third_party/gpui/src/elements/animation.rs create mode 100644 third_party/gpui/src/elements/canvas.rs create mode 100644 third_party/gpui/src/elements/deferred.rs create mode 100644 third_party/gpui/src/elements/div.rs create mode 100644 third_party/gpui/src/elements/image_cache.rs create mode 100644 third_party/gpui/src/elements/img.rs create mode 100644 third_party/gpui/src/elements/list.rs create mode 100644 third_party/gpui/src/elements/mod.rs create mode 100644 third_party/gpui/src/elements/surface.rs create mode 100644 third_party/gpui/src/elements/svg.rs create mode 100644 third_party/gpui/src/elements/text.rs create mode 100644 third_party/gpui/src/elements/uniform_list.rs create mode 100644 third_party/gpui/src/executor.rs create mode 100644 third_party/gpui/src/geometry.rs create mode 100644 third_party/gpui/src/global.rs create mode 100644 third_party/gpui/src/gpui.rs create mode 100644 third_party/gpui/src/input.rs create mode 100644 third_party/gpui/src/inspector.rs create mode 100644 third_party/gpui/src/interactive.rs create mode 100644 third_party/gpui/src/key_dispatch.rs create mode 100644 third_party/gpui/src/keymap.rs create mode 100644 third_party/gpui/src/keymap/binding.rs create mode 100644 third_party/gpui/src/keymap/context.rs create mode 100644 third_party/gpui/src/path_builder.rs create mode 100644 third_party/gpui/src/platform.rs create mode 100644 third_party/gpui/src/platform/app_menu.rs create mode 100644 third_party/gpui/src/platform/blade.rs create mode 100644 third_party/gpui/src/platform/blade/apple_compat.rs create mode 100644 third_party/gpui/src/platform/blade/blade_atlas.rs create mode 100644 third_party/gpui/src/platform/blade/blade_context.rs create mode 100644 third_party/gpui/src/platform/blade/blade_renderer.rs create mode 100644 third_party/gpui/src/platform/blade/shaders.wgsl create mode 100644 third_party/gpui/src/platform/keyboard.rs create mode 100644 third_party/gpui/src/platform/keystroke.rs create mode 100644 third_party/gpui/src/platform/linux.rs create mode 100644 third_party/gpui/src/platform/linux/dispatcher.rs create mode 100644 third_party/gpui/src/platform/linux/headless.rs create mode 100644 third_party/gpui/src/platform/linux/headless/client.rs create mode 100644 third_party/gpui/src/platform/linux/keyboard.rs create mode 100644 third_party/gpui/src/platform/linux/platform.rs create mode 100644 third_party/gpui/src/platform/linux/text_system.rs create mode 100644 third_party/gpui/src/platform/linux/wayland.rs create mode 100644 third_party/gpui/src/platform/linux/wayland/client.rs create mode 100644 third_party/gpui/src/platform/linux/wayland/clipboard.rs create mode 100644 third_party/gpui/src/platform/linux/wayland/cursor.rs create mode 100644 third_party/gpui/src/platform/linux/wayland/display.rs create mode 100644 third_party/gpui/src/platform/linux/wayland/serial.rs create mode 100644 third_party/gpui/src/platform/linux/wayland/window.rs create mode 100644 third_party/gpui/src/platform/linux/x11.rs create mode 100644 third_party/gpui/src/platform/linux/x11/client.rs create mode 100644 third_party/gpui/src/platform/linux/x11/clipboard.rs create mode 100644 third_party/gpui/src/platform/linux/x11/display.rs create mode 100644 third_party/gpui/src/platform/linux/x11/event.rs create mode 100644 third_party/gpui/src/platform/linux/x11/window.rs create mode 100644 third_party/gpui/src/platform/linux/x11/xim_handler.rs create mode 100644 third_party/gpui/src/platform/linux/xdg_desktop_portal.rs create mode 100644 third_party/gpui/src/platform/mac.rs create mode 100644 third_party/gpui/src/platform/mac/attributed_string.rs create mode 100644 third_party/gpui/src/platform/mac/dispatch.h create mode 100644 third_party/gpui/src/platform/mac/dispatcher.rs create mode 100644 third_party/gpui/src/platform/mac/display.rs create mode 100644 third_party/gpui/src/platform/mac/display_link.rs create mode 100644 third_party/gpui/src/platform/mac/events.rs create mode 100644 third_party/gpui/src/platform/mac/keyboard.rs create mode 100644 third_party/gpui/src/platform/mac/metal_atlas.rs create mode 100644 third_party/gpui/src/platform/mac/metal_renderer.rs create mode 100644 third_party/gpui/src/platform/mac/open_type.rs create mode 100644 third_party/gpui/src/platform/mac/platform.rs create mode 100644 third_party/gpui/src/platform/mac/screen_capture.rs create mode 100644 third_party/gpui/src/platform/mac/shaders.metal create mode 100644 third_party/gpui/src/platform/mac/status_item.rs create mode 100644 third_party/gpui/src/platform/mac/text_system.rs create mode 100644 third_party/gpui/src/platform/mac/window.rs create mode 100644 third_party/gpui/src/platform/mac/window_appearance.rs create mode 100644 third_party/gpui/src/platform/scap_screen_capture.rs create mode 100644 third_party/gpui/src/platform/test.rs create mode 100644 third_party/gpui/src/platform/test/dispatcher.rs create mode 100644 third_party/gpui/src/platform/test/display.rs create mode 100644 third_party/gpui/src/platform/test/platform.rs create mode 100644 third_party/gpui/src/platform/test/window.rs create mode 100644 third_party/gpui/src/platform/windows.rs create mode 100644 third_party/gpui/src/platform/windows/alpha_correction.hlsl create mode 100644 third_party/gpui/src/platform/windows/clipboard.rs create mode 100644 third_party/gpui/src/platform/windows/color_text_raster.hlsl create mode 100644 third_party/gpui/src/platform/windows/destination_list.rs create mode 100644 third_party/gpui/src/platform/windows/direct_write.rs create mode 100644 third_party/gpui/src/platform/windows/directx_atlas.rs create mode 100644 third_party/gpui/src/platform/windows/directx_devices.rs create mode 100644 third_party/gpui/src/platform/windows/directx_renderer.rs create mode 100644 third_party/gpui/src/platform/windows/dispatcher.rs create mode 100644 third_party/gpui/src/platform/windows/display.rs create mode 100644 third_party/gpui/src/platform/windows/events.rs create mode 100644 third_party/gpui/src/platform/windows/keyboard.rs create mode 100644 third_party/gpui/src/platform/windows/platform.rs create mode 100644 third_party/gpui/src/platform/windows/shaders.hlsl create mode 100644 third_party/gpui/src/platform/windows/system_settings.rs create mode 100644 third_party/gpui/src/platform/windows/util.rs create mode 100644 third_party/gpui/src/platform/windows/vsync.rs create mode 100644 third_party/gpui/src/platform/windows/window.rs create mode 100644 third_party/gpui/src/platform/windows/wrapper.rs create mode 100644 third_party/gpui/src/prelude.rs create mode 100644 third_party/gpui/src/scene.rs create mode 100644 third_party/gpui/src/shared_string.rs create mode 100644 third_party/gpui/src/shared_uri.rs create mode 100644 third_party/gpui/src/style.rs create mode 100644 third_party/gpui/src/styled.rs create mode 100644 third_party/gpui/src/subscription.rs create mode 100644 third_party/gpui/src/svg_renderer.rs create mode 100644 third_party/gpui/src/tab_stop.rs create mode 100644 third_party/gpui/src/taffy.rs create mode 100644 third_party/gpui/src/test.rs create mode 100644 third_party/gpui/src/text_system.rs create mode 100644 third_party/gpui/src/text_system/font_fallbacks.rs create mode 100644 third_party/gpui/src/text_system/font_features.rs create mode 100644 third_party/gpui/src/text_system/line.rs create mode 100644 third_party/gpui/src/text_system/line_layout.rs create mode 100644 third_party/gpui/src/text_system/line_wrapper.rs create mode 100644 third_party/gpui/src/util.rs create mode 100644 third_party/gpui/src/view.rs create mode 100644 third_party/gpui/src/window.rs create mode 100644 third_party/gpui/src/window/prompts.rs diff --git a/Cargo.lock b/Cargo.lock index 75096be..c7257ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3312,8 +3312,6 @@ dependencies = [ [[package]] name = "gpui" version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "979b45cfa6ec723b6f42330915a1b3769b930d02b2d505f9697f8ca602bee707" dependencies = [ "anyhow", "as-raw-xcb-connection", diff --git a/Cargo.toml b/Cargo.toml index 6da0a6c..a4e682b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,10 @@ ureq = "2.12.1" url = "2.5.4" uuid = { version = "1.12.1", features = ["v7"] } +[patch.crates-io] +# Local GPUI 0.2.2 patch: macOS `surface(CVPixelBuffer)` accepts Servo's BGRA IOSurfaces. +gpui = { path = "third_party/gpui" } + [workspace.lints.rust] unsafe_code = "deny" diff --git a/README.md b/README.md index 70dc57d..73b147d 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ cargo fmt --all --check cargo check --workspace --all-targets cargo clippy --workspace --all-targets -- -D warnings cargo test --workspace --all-targets -cargo check -p ely_servo_host --features servo-engine --all-targets -cargo clippy -p ely_servo_host --features servo-engine --all-targets -- -D warnings +cargo check -p ely_servo_host --features servo-engine,hardware-render --all-targets +cargo clippy -p ely_servo_host --features servo-engine,hardware-render --all-targets -- -D warnings cargo test -p ely_servo_host --features servo-engine --test software_host scripts/verify_prd_site_rendering.sh scripts/verify_windows_app_manifest.sh diff --git a/crates/ely_app/src/services/iosurface_metal.rs b/crates/ely_app/src/services/iosurface_metal.rs index cc65b6e..73858c7 100644 --- a/crates/ely_app/src/services/iosurface_metal.rs +++ b/crates/ely_app/src/services/iosurface_metal.rs @@ -3,10 +3,9 @@ //! //! `T10.4` originally imported the IOSurface into an `MTLTexture` //! directly. GPUI 0.2.2 exposes `Window::paint_surface` / -//! `elements::surface::Surface` for `CVPixelBuffer`, and that public -//! path is wired for NV12 video frames. Servo's hardware renderer -//! publishes BGRA IOSurfaces, so this cache stays as verified -//! cross-process plumbing until the presenter accepts BGRA surfaces. +//! `elements::surface::Surface` for `CVPixelBuffer`; the local GPUI +//! patch adds a BGRA fragment pipeline for Servo's hardware +//! IOSurfaces, so this cache is the renderer-side handoff point. //! //! Lifetime contract: //! @@ -161,11 +160,14 @@ mod tests { const TEST_HEIGHT: u32 = 48; /// Build a CPU-backed IOSurface from scratch, the same way - /// surfman's macOS backend does — BGRA8 (four-cc '32BGRA'), width - /// + height + bytes_per_element + bytes_per_row in a Core - /// Foundation properties dictionary. The pointer-casts mirror + /// surfman's macOS backend does. + /// + /// BGRA8 (four-cc '32BGRA'), width + height + bytes_per_element + /// + bytes_per_row live in a Core Foundation properties dictionary. + /// + /// The pointer-casts mirror /// `surfman::platform::macos::system::surface::create_io_surface`. - fn build_local_iosurface() -> CFRetained { + fn build_local_iosurface() -> Result, String> { let pixel_format: i32 = i32::from_be_bytes(*b"BGRA"); let bytes_per_element: i32 = 4; let bytes_per_row: i32 = (TEST_WIDTH as i32) * bytes_per_element; @@ -196,27 +198,25 @@ mod tests { &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks, ) - .expect("CFDictionaryCreate must succeed for the properties dict"); + .ok_or_else(|| "CFDictionaryCreate returned null".to_string())?; IOSurfaceRef::new(&properties) - .expect("IOSurfaceCreate must succeed for a well-formed properties dict") + .ok_or_else(|| "IOSurfaceCreate returned null".to_string()) } } #[test] - fn imports_local_iosurface_into_pixel_buffer() { + fn imports_local_iosurface_into_pixel_buffer() -> Result<(), String> { let mut cache = IOSurfaceCache::new(); - let iosurface = build_local_iosurface(); + let iosurface = build_local_iosurface()?; let mach_port = iosurface.create_mach_port(); assert!(mach_port != 0, "IOSurfaceCreateMachPort must yield a real port"); let surface_id: u64 = 0xDEAD_BEEFu64; - cache - .import(mach_port, surface_id) - .expect("local IOSurface must round-trip into a CVPixelBuffer"); + cache.import(mach_port, surface_id).map_err(|error| error.to_string())?; let pixel_buffer = cache .pixel_buffer_for(surface_id) - .expect("imported pixel buffer must be retrievable by surface_id"); + .ok_or_else(|| "imported pixel buffer was missing".to_string())?; assert_eq!( pixel_buffer.get_width() as u32, TEST_WIDTH, @@ -228,20 +228,22 @@ mod tests { "CVPixelBuffer height must match the source IOSurface", ); assert_eq!(cache.cached_surface_count(), 1); + Ok(()) } #[test] - fn second_import_with_same_surface_id_is_idempotent() { + fn second_import_with_same_surface_id_is_idempotent() -> Result<(), String> { let mut cache = IOSurfaceCache::new(); - let iosurface = build_local_iosurface(); + let iosurface = build_local_iosurface()?; let port_a = iosurface.create_mach_port(); let port_b = iosurface.create_mach_port(); assert!(port_a != 0 && port_b != 0 && port_a != port_b); - cache.import(port_a, 0xAAAA_AAAA).expect("first import"); + cache.import(port_a, 0xAAAA_AAAA).map_err(|error| error.to_string())?; // Same surface_id → defensive dedup path; port_b is deallocated // without minting a duplicate CVPixelBuffer. - cache.import(port_b, 0xAAAA_AAAA).expect("duplicate import is idempotent"); + cache.import(port_b, 0xAAAA_AAAA).map_err(|error| error.to_string())?; assert_eq!(cache.cached_surface_count(), 1); + Ok(()) } } diff --git a/crates/ely_app/src/services/servo_live.rs b/crates/ely_app/src/services/servo_live.rs index 94b857c..bcdae98 100644 --- a/crates/ely_app/src/services/servo_live.rs +++ b/crates/ely_app/src/services/servo_live.rs @@ -10,7 +10,7 @@ use std::{ /// (default — bit-identical to pre-flag builds) and `hardware` (real /// GPU adapter via the vendored `HardwareOffscreenContext`; requires /// the sidecar binary to be compiled with the `hardware-render` -/// feature and a GPUI BGRA surface presenter). Anything else is +/// feature and the local GPUI BGRA surface presenter). Anything else is /// silently dropped and the sidecar defaults to software so a typo'd /// value never blocks the browser from starting; the sidecar's own /// arg parser still errors loudly on an unrecognised value when set @@ -143,8 +143,8 @@ impl ServoLiveClient { // header so a buggy or hostile sidecar can't park us on // `read_exact` for an arbitrarily-sized buffer. The honest // upper limit is `width * height * 4` (RGBA8); `0` is the - // explicit "hardware path active, sample the IOSurface - // instead" signal — anything else is a protocol violation. + // explicit "hardware path active, sample the IOSurface" + // signal; any other byte count is a protocol violation. let pixel_byte_count = (report.width as u64).saturating_mul(report.height as u64).saturating_mul(4); let advertised = report.rgba_byte_count as u64; @@ -157,12 +157,10 @@ impl ServoLiveClient { }); } - // Raw frame bytes follow the JSON header on the same pipe - // ONLY when the sidecar didn't drop the payload for the - // hardware path. `read_exact` drains BufReader's buffer first + // Raw frame bytes follow the JSON header on the same pipe for + // software frames. `read_exact` drains BufReader's buffer first // (the line read never crosses the `\n` boundary) and then - // pulls the rest straight from the child's stdout — no - // fs::read, no temp file. + // pulls the rest straight from the child's stdout. let mut rgba_bytes = vec![0u8; report.rgba_byte_count]; if report.rgba_byte_count > 0 { self.stdout.read_exact(&mut rgba_bytes).map_err(ServoLiveError::FrameRead)?; @@ -189,10 +187,8 @@ impl Drop for ServoLiveClient { #[cfg(target_os = "macos")] impl ServoLiveClient { /// Convert the sidecar's `surface_handle` into a `CVPixelBuffer` - /// in the local cache. Failures are logged but don't error the - /// request — the renderer falls back to the existing software - /// `Arc` path when no pixel buffer is available, so - /// the user always sees a frame. + /// in the local cache. A later frame with a missing pixel buffer + /// becomes a web-surface error instead of a blank ready frame. fn import_iosurface_handle(&mut self, handle: &LiveSurfaceHandle) { match self.iosurface_cache.import(handle.mach_port_name, handle.surface_id) { Ok(()) => tracing::info!( @@ -266,10 +262,8 @@ pub(crate) struct ServoLiveFrame { #[cfg(all(test, feature = "live-site-smoke"))] sample_hash: u64, rgba_bytes: Vec, - /// Hardware-path companion: the imported IOSurface published by - /// the sidecar. GPUI 0.2.2 presents `surface(...)` through its - /// NV12 video path, so the current BGRA Servo surface stays as - /// observability plumbing until a BGRA presenter lands. + /// Hardware-path surface: the imported IOSurface published by the + /// sidecar, wrapped as a CVPixelBuffer for GPUI's `surface(...)`. #[cfg(target_os = "macos")] pixel_buffer: Option, } @@ -295,9 +289,7 @@ impl ServoLiveFrame { } /// Returns the imported `CVPixelBuffer` matching the frame's - /// current hardware surface, if any. The renderer keeps this as - /// wire-path evidence while GPUI's public `surface(...)` element - /// remains NV12-only. + /// current hardware surface. #[cfg(target_os = "macos")] #[must_use] pub fn pixel_buffer(&self) -> Option<&CVPixelBuffer> { @@ -371,6 +363,29 @@ impl ServoLiveFrame { pixel_buffer: None, } } + + #[cfg(all(test, target_os = "macos"))] + pub(crate) fn for_test_with_pixel_buffer( + width: u32, + height: u32, + pixel_buffer: CVPixelBuffer, + ) -> Self { + Self { + loaded_url: Some("https://example.com/".to_string()), + title: Some("Example".to_string()), + render_state: "complete".to_string(), + width, + height, + #[cfg(all(test, feature = "live-site-smoke"))] + non_white_pixel_count: 0, + #[cfg(all(test, feature = "live-site-smoke"))] + content_pixel_count: 0, + #[cfg(all(test, feature = "live-site-smoke"))] + sample_hash: 0, + rgba_bytes: Vec::new(), + pixel_buffer: Some(pixel_buffer), + } + } } #[derive(Debug, Error)] @@ -417,13 +432,6 @@ fn rendering_context_from_env() -> Option<&'static str> { let raw = env::var(RENDERING_CONTEXT_ENV).ok()?; match rendering_context_selection(raw.as_str()) { RenderingContextSelection::Forward(value) => Some(value), - RenderingContextSelection::HoldHardware => { - tracing::warn!( - target: "ely::servo::iosurface", - "hardware rendering context requested; GPUI 0.2.2 surface presenter accepts NV12 CVPixelBuffers; Servo publishes BGRA IOSurfaces; using software rendering context", - ); - None - } RenderingContextSelection::Ignore => None, } } @@ -431,14 +439,13 @@ fn rendering_context_from_env() -> Option<&'static str> { #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum RenderingContextSelection { Forward(&'static str), - HoldHardware, Ignore, } fn rendering_context_selection(raw: &str) -> RenderingContextSelection { match raw.to_lowercase().as_str() { "software" => RenderingContextSelection::Forward("software"), - "hardware" => RenderingContextSelection::HoldHardware, + "hardware" => RenderingContextSelection::Forward("hardware"), _ => RenderingContextSelection::Ignore, } } @@ -448,10 +455,10 @@ mod tests { use super::{RenderingContextSelection, rendering_context_selection}; #[test] - fn hardware_env_is_held_until_gpui_can_present_bgra_surfaces() { + fn hardware_env_forwards_to_the_sidecar() { assert_eq!( rendering_context_selection("hardware"), - RenderingContextSelection::HoldHardware + RenderingContextSelection::Forward("hardware") ); } diff --git a/crates/ely_app/src/services/servo_sidecar_command.rs b/crates/ely_app/src/services/servo_sidecar_command.rs index 79dd687..d65e694 100644 --- a/crates/ely_app/src/services/servo_sidecar_command.rs +++ b/crates/ely_app/src/services/servo_sidecar_command.rs @@ -7,6 +7,9 @@ use std::{ use thiserror::Error; const SIDECAR_PATH_ENV: &str = "ELY_SERVO_SIDECAR"; +const RENDERING_CONTEXT_ENV: &str = "ELY_SERVO_RENDERING_CONTEXT"; +const SOFTWARE_SIDECAR_FEATURES: &str = "servo-engine"; +const HARDWARE_SIDECAR_FEATURES: &str = "servo-engine,hardware-render"; #[derive(Clone, Debug)] pub(super) enum SidecarCommandTarget { @@ -28,7 +31,7 @@ impl SidecarCommandTarget { .arg("-p") .arg("ely_servo_host") .arg("--features") - .arg("servo-engine") + .arg(sidecar_features_from_env()) .arg("--bin") .arg("ely_servo_sidecar") .arg("--"); @@ -64,13 +67,17 @@ pub(super) fn default_sidecar_command() -> Result Option { option_env!("ELY_WORKSPACE_MANIFEST").map(PathBuf::from) } +fn hardware_rendering_context_requested() -> bool { + env::var(RENDERING_CONTEXT_ENV).ok().as_deref().is_some_and(rendering_context_requests_hardware) +} + +fn sidecar_features_from_env() -> &'static str { + env::var(RENDERING_CONTEXT_ENV) + .ok() + .as_deref() + .map(sidecar_features_for_rendering_context) + .unwrap_or(SOFTWARE_SIDECAR_FEATURES) +} + +fn sidecar_features_for_rendering_context(raw: &str) -> &'static str { + if rendering_context_requests_hardware(raw) { + HARDWARE_SIDECAR_FEATURES + } else { + SOFTWARE_SIDECAR_FEATURES + } +} + +fn rendering_context_requests_hardware(raw: &str) -> bool { + raw.eq_ignore_ascii_case("hardware") +} + fn workspace_target_sidecar_path(manifest_path: &Path) -> Option { let profile = if cfg!(debug_assertions) { "debug" } else { "release" }; Some(manifest_path.parent()?.join("target").join(profile).join(sidecar_binary_name())) @@ -94,3 +125,23 @@ fn workspace_target_sidecar_path(manifest_path: &Path) -> Option { fn sidecar_binary_name() -> String { format!("ely_servo_sidecar{}", env::consts::EXE_SUFFIX) } + +#[cfg(test)] +mod tests { + use super::{ + HARDWARE_SIDECAR_FEATURES, SOFTWARE_SIDECAR_FEATURES, + sidecar_features_for_rendering_context, + }; + + #[test] + fn hardware_rendering_context_enables_hardware_sidecar_feature() { + assert_eq!(sidecar_features_for_rendering_context("hardware"), HARDWARE_SIDECAR_FEATURES); + assert_eq!(sidecar_features_for_rendering_context("HARDWARE"), HARDWARE_SIDECAR_FEATURES); + } + + #[test] + fn software_and_unknown_contexts_use_software_sidecar_feature() { + assert_eq!(sidecar_features_for_rendering_context("software"), SOFTWARE_SIDECAR_FEATURES); + assert_eq!(sidecar_features_for_rendering_context("garbage"), SOFTWARE_SIDECAR_FEATURES); + } +} diff --git a/crates/ely_app/src/shell/web_surface.rs b/crates/ely_app/src/shell/web_surface.rs index a0e9c79..18144c5 100644 --- a/crates/ely_app/src/shell/web_surface.rs +++ b/crates/ely_app/src/shell/web_surface.rs @@ -177,12 +177,12 @@ impl WebSurfaceStore { position: Point, scale_factor: f32, ) -> WebSurfaceInputOutcome { - let surface = - self.surfaces.get_mut(tab_id).filter(|surface| surface.viewport_bounds.is_some()); - let Some(surface) = surface else { + let Some(surface) = self.surfaces.get_mut(tab_id) else { + return WebSurfaceInputOutcome::DroppedNoViewportBounds; + }; + let Some(bounds) = surface.viewport_bounds else { return WebSurfaceInputOutcome::DroppedNoViewportBounds; }; - let bounds = surface.viewport_bounds.expect("viewport_bounds checked above"); let Some(point) = WebSurfaceClickPoint::from_window_position(bounds, position, scale_factor) else { diff --git a/crates/ely_app/src/shell/web_surface_frame.rs b/crates/ely_app/src/shell/web_surface_frame.rs index e284fa1..f1b489c 100644 --- a/crates/ely_app/src/shell/web_surface_frame.rs +++ b/crates/ely_app/src/shell/web_surface_frame.rs @@ -57,14 +57,12 @@ pub(super) struct WebSurfaceFrame { content_pixel_count: u64, #[cfg(all(test, feature = "live-site-smoke"))] sample_hash: u64, - /// Software-path image. Current GPUI builds require this for every - /// ready web frame because BGRA IOSurface presentation is still - /// held at the protocol boundary. + /// Software-path image built from RGBA bytes when the sidecar runs + /// without hardware surface publication. pub(super) image: Option>, - /// Hardware-path companion imported from the sidecar. GPUI 0.2.2's - /// public `surface(...)` presenter accepts NV12 video buffers, and - /// Servo publishes BGRA IOSurfaces; this remains observability - /// state until a BGRA presenter is available. + /// Hardware-path surface imported from the sidecar's IOSurface. + /// GPUI is patched locally to present BGRA CVPixelBuffers through + /// `surface(...)`, so hardware frames can skip the RGBA pipe. #[cfg(target_os = "macos")] pub(super) pixel_buffer: Option, } @@ -102,23 +100,32 @@ impl WebSurfaceFrame { } fn from_parts(parts: WebSurfaceFrameParts) -> Result { - if parts.rgba_bytes.is_empty() { + #[cfg(target_os = "macos")] + let has_pixel_buffer = parts.pixel_buffer.is_some(); + #[cfg(not(target_os = "macos"))] + let has_pixel_buffer = false; + + if parts.rgba_bytes.is_empty() && !has_pixel_buffer { return Err(WebSurfaceError::MissingRenderablePayload); } - // Servo's `read_pixels(gl::RGBA, gl::UNSIGNED_BYTE)` writes - // R-G-B-A in memory order. GPUI's `RenderImage` is documented - // as "in BGRA format" and uploads via - // `MTLPixelFormat::BGRA8Unorm`, which reads B-G-R-A. Hand the bytes across - // unchanged and the Metal sampler treats R as B (and vice - // versa) — every coloured pixel renders with R and B swapped. - // Swap once here so the rest of the pipeline (dedup hash, - // image buffer, GPU upload) all operate on the same BGRA - // representation. - let mut bytes = parts.rgba_bytes; - swap_red_blue_in_place(&mut bytes); - let bytes_hash = rgba_hash(&bytes); - let image = Some(resolve_render_image(parts.width, parts.height, bytes, bytes_hash)?); + let image = if parts.rgba_bytes.is_empty() { + None + } else { + // Servo's `read_pixels(gl::RGBA, gl::UNSIGNED_BYTE)` writes + // R-G-B-A in memory order. GPUI's `RenderImage` is documented + // as "in BGRA format" and uploads via + // `MTLPixelFormat::BGRA8Unorm`, which reads B-G-R-A. Hand the bytes across + // unchanged and the Metal sampler treats R as B (and vice + // versa) — every coloured pixel renders with R and B swapped. + // Swap once here so the rest of the pipeline (dedup hash, + // image buffer, GPU upload) all operate on the same BGRA + // representation. + let mut bytes = parts.rgba_bytes; + swap_red_blue_in_place(&mut bytes); + let bytes_hash = rgba_hash(&bytes); + Some(resolve_render_image(parts.width, parts.height, bytes, bytes_hash)?) + }; Ok(Self { requested_url: parts.requested_url, @@ -240,9 +247,7 @@ struct WebSurfaceFrameParts { pub(super) enum WebSurfaceError { #[error("invalid servo frame buffer for {width}x{height}")] InvalidFrameBuffer { width: u32, height: u32 }, - #[error( - "servo live frame did not include renderable pixels; BGRA IOSurface presentation is unavailable in GPUI 0.2.2" - )] + #[error("servo live frame did not include a software image or hardware IOSurface")] MissingRenderablePayload, } @@ -269,10 +274,10 @@ fn resolve_render_image( ) -> Result, WebSurfaceError> { LAST_FRAME_IMAGE.with(|cache| -> Result, WebSurfaceError> { let mut cache = cache.borrow_mut(); - if let Some((cached_hash, cached_image)) = cache.as_ref() { - if *cached_hash == bytes_hash { - return Ok(cached_image.clone()); - } + if let Some((cached_hash, cached_image)) = cache.as_ref() + && *cached_hash == bytes_hash + { + return Ok(cached_image.clone()); } let image_buffer = ImageBuffer::, _>::from_raw(width, height, rgba_bytes) diff --git a/crates/ely_app/src/shell/web_surface_tests.rs b/crates/ely_app/src/shell/web_surface_tests.rs index cabc32a..2198d2f 100644 --- a/crates/ely_app/src/shell/web_surface_tests.rs +++ b/crates/ely_app/src/shell/web_surface_tests.rs @@ -384,7 +384,7 @@ fn live_frame_swaps_red_and_blue_bytes_for_gpui_bgra() -> Result<(), Box Result<(), String> { use crate::services::servo_live::ServoLiveFrame; use crate::shell::web_surface_frame::WebSurfaceFrame; use crate::shell::web_surface_geometry::WebSurfaceScrollOffset; @@ -396,13 +396,40 @@ fn empty_live_frame_payload_is_rejected() { ServoLiveFrame::for_test(1, 1, Vec::new()), ); - let Err(error) = result else { - panic!("empty Servo frame payload must be rejected before it reaches Ready state"); + let error = match result { + Ok(_) => return Err("empty Servo frame payload reached Ready state".to_string()), + Err(error) => error, }; assert_eq!( error.to_string(), - "servo live frame did not include renderable pixels; BGRA IOSurface presentation is unavailable in GPUI 0.2.2", + "servo live frame did not include a software image or hardware IOSurface", ); + Ok(()) +} + +#[cfg(target_os = "macos")] +#[test] +fn hardware_live_frame_with_pixel_buffer_skips_software_image() -> Result<(), String> { + use core_video::pixel_buffer::{CVPixelBuffer, kCVPixelFormatType_32BGRA}; + + use crate::services::servo_live::ServoLiveFrame; + use crate::shell::web_surface_frame::WebSurfaceFrame; + use crate::shell::web_surface_geometry::WebSurfaceScrollOffset; + + let pixel_buffer = CVPixelBuffer::new(kCVPixelFormatType_32BGRA, 1, 1, None) + .map_err(|status| format!("CVPixelBufferCreate returned status {status}"))?; + let live = ServoLiveFrame::for_test_with_pixel_buffer(1, 1, pixel_buffer); + let frame = WebSurfaceFrame::from_live_frame( + "https://example.com/".to_string(), + WebSurfaceScrollOffset::default(), + 100, + live, + ) + .map_err(|error| error.to_string())?; + + assert!(frame.image.is_none(), "hardware frame should use the CVPixelBuffer surface path"); + assert!(frame.pixel_buffer.is_some(), "hardware frame should carry the imported CVPixelBuffer"); + Ok(()) } fn web_bounds() -> Bounds { diff --git a/crates/ely_app/src/shell/web_surface_view.rs b/crates/ely_app/src/shell/web_surface_view.rs index ffca44b..39310ba 100644 --- a/crates/ely_app/src/shell/web_surface_view.rs +++ b/crates/ely_app/src/shell/web_surface_view.rs @@ -1,7 +1,7 @@ use ely_domain::{BrowserTab, TabId}; use gpui::{ AnyElement, App, Entity, ImageSource, InteractiveElement, IntoElement, MouseButton, ObjectFit, - ParentElement, Styled, StyledImage, Window, canvas, div, img, px, rgb, + ParentElement, Styled, StyledImage, Window, canvas, div, img, px, rgb, surface, }; use super::{ElyShell, web_surface_frame::WebSurfaceFrame}; @@ -12,22 +12,15 @@ pub(super) fn render_ready_web_surface( tab: &BrowserTab, state_entity: Entity, ) -> AnyElement { - // T14: the `gpui::surface(...)` hardware path is held. - // - // GPUI 0.2.2's Blade Metal renderer hard-asserts that any - // CVPixelBuffer handed to `surface(...)` is NV12 YUV - // (kCVPixelFormatType_420YpCbCr8BiPlanarFullRange). See - // ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ - // gpui-0.2.2/src/platform/blade/blade_renderer.rs:832 - // for the assert. Our sidecar produces BGRA IOSurfaces, so - // calling `surface()` with that buffer panics the renderer on - // the first frame. - // - // We keep `services::iosurface_metal` and - // `WebSurfaceFrame::pixel_buffer` intact so the wire-side - // IOSurfaceHandle import path stays exercised; once GPUI gains a - // BGRA-capable Surface element this branch can come back. Until - // then every frame must go through `img()` below. + #[cfg(target_os = "macos")] + if let Some(pixel_buffer) = frame.pixel_buffer.as_ref() { + return render_web_surface( + tab, + state_entity, + surface(pixel_buffer.clone()).size_full().object_fit(ObjectFit::Fill), + ); + } + if let Some(image) = frame.image.as_ref() { return render_web_surface( tab, diff --git a/crates/ely_servo_host/tests/hardware_rendering_context.rs b/crates/ely_servo_host/tests/hardware_rendering_context.rs index 548b42b..dc95b75 100644 --- a/crates/ely_servo_host/tests/hardware_rendering_context.rs +++ b/crates/ely_servo_host/tests/hardware_rendering_context.rs @@ -38,7 +38,7 @@ fn constructs_or_explains_why_not() { #[cfg(target_os = "macos")] #[test] -fn extracts_iosurface_mach_port_from_current_surface() { +fn extracts_iosurface_mach_port_from_current_surface() -> Result<(), String> { let width = 256; let height = 192; let context = match HardwareOffscreenContext::new(PhysicalSize::new(width, height)) { @@ -48,13 +48,13 @@ fn extracts_iosurface_mach_port_from_current_surface() { "hardware GL adapter not available on this host \ (acceptable in headless / no-GPU environments): {error:?}" ); - return; + return Ok(()); } }; let first = context .current_iosurface_mach_port() - .expect("first IOSurface mach port extraction must succeed"); + .map_err(|error| format!("first IOSurface mach port extraction failed: {error:?}"))?; assert!( first.mach_port_name != 0, "IOSurfaceCreateMachPort must return a non-null mach_port_t (got 0)" @@ -67,8 +67,9 @@ fn extracts_iosurface_mach_port_from_current_surface() { // a stale `Framebuffer::None`. let second = context .current_iosurface_mach_port() - .expect("repeated mach port extraction must succeed after rebind"); + .map_err(|error| format!("repeated mach port extraction failed: {error:?}"))?; assert!(second.mach_port_name != 0); assert_eq!(second.width, width); assert_eq!(second.height, height); + Ok(()) } diff --git a/scripts/create_macos_app_bundle.sh b/scripts/create_macos_app_bundle.sh index 3235426..8e40915 100755 --- a/scripts/create_macos_app_bundle.sh +++ b/scripts/create_macos_app_bundle.sh @@ -10,7 +10,7 @@ macos_dir="${contents_dir}/MacOS" resources_dir="${contents_dir}/Resources" cargo build -p ely_app -cargo build -p ely_servo_host --features servo-engine --bin ely_servo_sidecar +cargo build -p ely_servo_host --features servo-engine,hardware-render --bin ely_servo_sidecar rm -rf "${bundle_root}" mkdir -p "${macos_dir}" "${resources_dir}" diff --git a/third_party/gpui/Cargo.toml b/third_party/gpui/Cargo.toml new file mode 100644 index 0000000..9f20281 --- /dev/null +++ b/third_party/gpui/Cargo.toml @@ -0,0 +1,624 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# Local copy trims registry examples/tests/docs; runtime dependencies +# and library/build targets stay aligned with the published crate. + +[package] +edition = "2024" +name = "gpui" +version = "0.2.2" +authors = ["Nathan Sobo "] +build = "build.rs" +publish = true +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Zed's GPU-accelerated UI framework" +homepage = "https://gpui.rs" +readme = "README.md" +keywords = [ + "desktop", + "gui", + "immediate", +] +categories = ["gui"] +license = "Apache-2.0" +repository = "https://github.com/zed-industries/zed" +resolver = "2" + +[features] +default = [ + "font-kit", + "wayland", + "x11", + "windows-manifest", +] +inspector = ["gpui_macros/inspector"] +leak-detection = ["backtrace"] +macos-blade = [ + "blade-graphics", + "blade-macros", + "blade-util", + "bytemuck", + "objc2", + "objc2-metal", +] +runtime_shaders = [] +screen-capture = ["scap"] +test-support = [ + "leak-detection", + "collections/test-support", + "rand", + "util/test-support", + "http_client/test-support", + "wayland", + "x11", +] +wayland = [ + "blade-graphics", + "blade-macros", + "blade-util", + "bytemuck", + "ashpd/wayland", + "cosmic-text", + "font-kit", + "calloop-wayland-source", + "wayland-backend", + "wayland-client", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-plasma", + "filedescriptor", + "xkbcommon", + "open", +] +windows-manifest = [] +x11 = [ + "blade-graphics", + "blade-macros", + "blade-util", + "bytemuck", + "ashpd", + "cosmic-text", + "font-kit", + "as-raw-xcb-connection", + "x11rb", + "xkbcommon", + "xim", + "x11-clipboard", + "filedescriptor", + "open", + "scap?/x11", +] + +[lib] +name = "gpui" +path = "src/gpui.rs" +doctest = false + +[dependencies.anyhow] +version = "1.0.86" + +[dependencies.async-task] +version = "4.7" + +[dependencies.backtrace] +version = "0.3" +optional = true + +[dependencies.blade-graphics] +version = "0.7.0" +optional = true + +[dependencies.blade-macros] +version = "0.3.0" +optional = true + +[dependencies.blade-util] +version = "0.3.0" +optional = true + +[dependencies.bytemuck] +version = "1" +optional = true + +[dependencies.collections] +version = "0.2.2" +package = "gpui_collections" + +[dependencies.ctor] +version = "0.4.0" + +[dependencies.derive_more] +version = "0.99.17" + +[dependencies.etagere] +version = "0.2" + +[dependencies.futures] +version = "0.3" + +[dependencies.gpui_macros] +version = "0.2.2" +package = "gpui-macros" + +[dependencies.http_client] +version = "0.2.2" +package = "gpui_http_client" + +[dependencies.image] +version = "0.25.1" + +[dependencies.inventory] +version = "0.3.19" + +[dependencies.itertools] +version = "0.14.0" + +[dependencies.libc] +version = "0.2" + +[dependencies.log] +version = "0.4.16" +features = [ + "kv_unstable_serde", + "serde", +] + +[dependencies.lyon] +version = "1.0" + +[dependencies.num_cpus] +version = "1.13" + +[dependencies.parking] +version = "2.0.0" + +[dependencies.parking_lot] +version = "0.12.1" + +[dependencies.pin-project] +version = "1.1.10" + +[dependencies.postage] +version = "0.5" +features = ["futures-traits"] + +[dependencies.profiling] +version = "1" + +[dependencies.rand] +version = "0.9" +optional = true + +[dependencies.raw-window-handle] +version = "0.6" + +[dependencies.refineable] +version = "0.2.2" +package = "gpui_refineable" + +[dependencies.resvg] +version = "0.45.0" +features = [ + "text", + "system-fonts", + "memmap-fonts", +] +default-features = false + +[dependencies.schemars] +version = "1.0" +features = ["indexmap2"] + +[dependencies.seahash] +version = "4.1" + +[dependencies.semantic_version] +version = "0.2.2" +package = "gpui_semantic_version" + +[dependencies.serde] +version = "1.0.221" +features = [ + "derive", + "rc", +] + +[dependencies.serde_json] +version = "1.0.144" +features = [ + "preserve_order", + "raw_value", +] + +[dependencies.slotmap] +version = "1.0.6" + +[dependencies.smallvec] +version = "1.6" +features = ["union"] + +[dependencies.smol] +version = "2.0" + +[dependencies.stacksafe] +version = "0.1" + +[dependencies.strum] +version = "0.27.2" +features = ["derive"] + +[dependencies.sum_tree] +version = "0.2.2" +package = "gpui_sum_tree" + +[dependencies.taffy] +version = "=0.9.0" + +[dependencies.thiserror] +version = "2.0.12" + +[dependencies.usvg] +version = "0.45.0" +default-features = false + +[dependencies.util] +version = "0.2.2" +package = "gpui_util" + +[dependencies.util_macros] +version = "0.2.2" +package = "gpui_util_macros" + +[dependencies.uuid] +version = "1.1.2" +features = [ + "v4", + "v5", + "v7", + "serde", +] + +[dependencies.waker-fn] +version = "1.2.0" + +[dev-dependencies.backtrace] +version = "0.3" + +[dev-dependencies.collections] +version = "0.2.2" +features = ["test-support"] +package = "gpui_collections" + +[dev-dependencies.env_logger] +version = "0.11" + +[dev-dependencies.http_client] +version = "0.2.2" +features = ["test-support"] +package = "gpui_http_client" + +[dev-dependencies.lyon] +version = "1.0" +features = ["extra"] + +[dev-dependencies.pretty_assertions] +version = "1.3.0" +features = ["unstable"] + +[dev-dependencies.rand] +version = "0.9" + +[dev-dependencies.unicode-segmentation] +version = "1.10" + +[dev-dependencies.util] +version = "0.2.2" +features = ["test-support"] +package = "gpui_util" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.as-raw-xcb-connection] +version = "1" +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.ashpd] +version = "0.11" +features = ["async-std"] +optional = true +default-features = false + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.blade-graphics] +version = "0.7.0" +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.blade-macros] +version = "0.3.0" +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.blade-util] +version = "0.3.0" +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.bytemuck] +version = "1" +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.calloop] +version = "0.13.0" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.calloop-wayland-source] +version = "0.3.0" +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.cosmic-text] +version = "0.14.0" +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.filedescriptor] +version = "0.8.2" +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.flume] +version = "0.11" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.font-kit] +version = "0.14.1-zed" +features = ["source-fontconfig-dlopen"] +optional = true +package = "zed-font-kit" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.oo7] +version = "0.5.0" +features = [ + "async-std", + "native_crypto", +] +default-features = false + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.open] +version = "5.2.0" +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.wayland-backend] +version = "0.3.3" +features = [ + "client_system", + "dlopen", +] +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.wayland-client] +version = "0.31.2" +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.wayland-cursor] +version = "0.31.1" +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.wayland-protocols] +version = "0.31.2" +features = [ + "client", + "staging", + "unstable", +] +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.wayland-protocols-plasma] +version = "0.2.0" +features = ["client"] +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.x11-clipboard] +version = "0.9.3" +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.x11rb] +version = "0.13.1" +features = [ + "allow-unsafe-code", + "xkb", + "randr", + "xinput", + "cursor", + "resource_manager", + "sync", +] +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.xim] +version = "0.4.0-zed" +features = [ + "x11rb-xcb", + "x11rb-client", +] +optional = true +package = "zed-xim" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.xkbcommon] +version = "0.8.0" +features = [ + "wayland", + "x11", +] +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.build-dependencies.naga] +version = "25.0" +features = ["wgsl-in"] + +[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))'.dependencies.pathfinder_geometry] +version = "0.5" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))'.dependencies.scap] +version = "0.0.8-zed" +optional = true +default-features = false +package = "zed-scap" + +[target.'cfg(target_os = "macos")'.dependencies.block] +version = "0.1" + +[target.'cfg(target_os = "macos")'.dependencies.cocoa] +version = "=0.26.0" + +[target.'cfg(target_os = "macos")'.dependencies.cocoa-foundation] +version = "=0.2.0" + +[target.'cfg(target_os = "macos")'.dependencies.core-foundation] +version = "=0.10.0" + +[target.'cfg(target_os = "macos")'.dependencies.core-foundation-sys] +version = "0.8.6" + +[target.'cfg(target_os = "macos")'.dependencies.core-graphics] +version = "0.24" + +[target.'cfg(target_os = "macos")'.dependencies.core-text] +version = "21" + +[target.'cfg(target_os = "macos")'.dependencies.core-video] +version = "0.4.3" +features = ["metal"] + +[target.'cfg(target_os = "macos")'.dependencies.font-kit] +version = "0.14.1-zed" +optional = true +package = "zed-font-kit" + +[target.'cfg(target_os = "macos")'.dependencies.foreign-types] +version = "0.5" + +[target.'cfg(target_os = "macos")'.dependencies.log] +version = "0.4.16" +features = [ + "kv_unstable_serde", + "serde", +] + +[target.'cfg(target_os = "macos")'.dependencies.media] +version = "0.2.2" +package = "gpui_media" + +[target.'cfg(target_os = "macos")'.dependencies.metal] +version = "0.29" + +[target.'cfg(target_os = "macos")'.dependencies.objc] +version = "0.2" + +[target.'cfg(target_os = "macos")'.dependencies.objc2] +version = "0.6" +optional = true + +[target.'cfg(target_os = "macos")'.dependencies.objc2-metal] +version = "0.3" +optional = true + +[target.'cfg(target_os = "macos")'.build-dependencies.bindgen] +version = "0.71" + +[target.'cfg(target_os = "macos")'.build-dependencies.cbindgen] +version = "0.28.0" +default-features = false + +[target.'cfg(target_os = "macos")'.build-dependencies.naga] +version = "25.0" +features = ["wgsl-in"] + +[target.'cfg(target_os = "windows")'.dependencies.flume] +version = "0.11" + +[target.'cfg(target_os = "windows")'.dependencies.rand] +version = "0.9" + +[target.'cfg(target_os = "windows")'.dependencies.windows] +version = "0.61" +features = [ + "Foundation_Numerics", + "Storage_Search", + "Storage_Streams", + "System_Threading", + "UI_ViewManagement", + "Wdk_System_SystemServices", + "Win32_Globalization", + "Win32_Graphics_Direct3D", + "Win32_Graphics_Direct3D11", + "Win32_Graphics_Direct3D_Fxc", + "Win32_Graphics_DirectComposition", + "Win32_Graphics_DirectWrite", + "Win32_Graphics_Dwm", + "Win32_Graphics_Dxgi", + "Win32_Graphics_Dxgi_Common", + "Win32_Graphics_Gdi", + "Win32_Graphics_Imaging", + "Win32_Graphics_Hlsl", + "Win32_Networking_WinSock", + "Win32_Security", + "Win32_Security_Credentials", + "Win32_Security_Cryptography", + "Win32_Storage_FileSystem", + "Win32_System_Com", + "Win32_System_Com_StructuredStorage", + "Win32_System_Console", + "Win32_System_DataExchange", + "Win32_System_IO", + "Win32_System_LibraryLoader", + "Win32_System_Memory", + "Win32_System_Ole", + "Win32_System_Performance", + "Win32_System_Pipes", + "Win32_System_SystemInformation", + "Win32_System_SystemServices", + "Win32_System_Threading", + "Win32_System_Variant", + "Win32_System_WinRT", + "Win32_UI_Controls", + "Win32_UI_HiDpi", + "Win32_UI_Input_Ime", + "Win32_UI_Input_KeyboardAndMouse", + "Win32_UI_Shell", + "Win32_UI_Shell_Common", + "Win32_UI_Shell_PropertiesSystem", + "Win32_UI_WindowsAndMessaging", +] + +[target.'cfg(target_os = "windows")'.dependencies.windows-core] +version = "0.61" + +[target.'cfg(target_os = "windows")'.dependencies.windows-numerics] +version = "0.2" + +[target.'cfg(target_os = "windows")'.dependencies.windows-registry] +version = "0.5" + +[target.'cfg(target_os = "windows")'.build-dependencies.embed-resource] +version = "3.0" + +[lints.clippy] +dbg_macro = "deny" +declare_interior_mutable_const = "deny" +disallowed_methods = "deny" +large_enum_variant = "allow" +let_underscore_future = "allow" +nonminimal_bool = "allow" +redundant_clone = "deny" +single_range_in_vec_init = "allow" +todo = "deny" +too_many_arguments = "allow" +type_complexity = "allow" + +[lints.clippy.style] +level = "allow" +priority = -1 + +[lints.rust.unexpected_cfgs] +level = "allow" +priority = 0 diff --git a/third_party/gpui/LICENSE-APACHE b/third_party/gpui/LICENSE-APACHE new file mode 100644 index 0000000..461a0fe --- /dev/null +++ b/third_party/gpui/LICENSE-APACHE @@ -0,0 +1,222 @@ +Copyright 2022 - 2025 Zed Industries, Inc. + + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + + http://www.apache.org/licenses/LICENSE-2.0 + + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + + +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + + 1. Definitions. + + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + + END OF TERMS AND CONDITIONS diff --git a/third_party/gpui/README.md b/third_party/gpui/README.md new file mode 100644 index 0000000..2c411f7 --- /dev/null +++ b/third_party/gpui/README.md @@ -0,0 +1,66 @@ +# Welcome to GPUI! + +GPUI is a hybrid immediate and retained mode, GPU accelerated, UI framework +for Rust, designed to support a wide variety of applications. + +## Getting Started + +GPUI is still in active development as we work on the Zed code editor, and is still pre-1.0. There will often be breaking changes between versions. You'll also need to use the latest version of stable Rust and be on macOS or Linux. Add the following to your `Cargo.toml`: + +```toml +gpui = { version = "*" } +``` + + - [Ownership and data flow](src/_ownership_and_data_flow.rs) + +Everything in GPUI starts with an `Application`. You can create one with `Application::new()`, and kick off your application by passing a callback to `Application::run()`. Inside this callback, you can create a new window with `App::open_window()`, and register your first root view. See [gpui.rs](https://www.gpui.rs/) for a complete example. + +### Dependencies + +GPUI has various system dependencies that it needs in order to work. + +#### macOS + +On macOS, GPUI uses Metal for rendering. In order to use Metal, you need to do the following: + +- Install [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) from the macOS App Store, or from the [Apple Developer](https://developer.apple.com/download/all/) website. Note this requires a developer account. + +> Ensure you launch Xcode after installing, and install the macOS components, which is the default option. + +- Install [Xcode command line tools](https://developer.apple.com/xcode/resources/) + + ```sh + xcode-select --install + ``` + +- Ensure that the Xcode command line tools are using your newly installed copy of Xcode: + + ```sh + sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer + ``` + +## The Big Picture + +GPUI offers three different [registers]() depending on your needs: + +- State management and communication with `Entity`'s. Whenever you need to store application state that communicates between different parts of your application, you'll want to use GPUI's entities. Entities are owned by GPUI and are only accessible through an owned smart pointer similar to an `Rc`. See the `app::context` module for more information. + +- High level, declarative UI with views. All UI in GPUI starts with a view. A view is simply an `Entity` that can be rendered, by implementing the `Render` trait. At the start of each frame, GPUI will call this render method on the root view of a given window. Views build a tree of `elements`, lay them out and style them with a tailwind-style API, and then give them to GPUI to turn into pixels. See the `div` element for an all purpose swiss-army knife of rendering. + +- Low level, imperative UI with Elements. Elements are the building blocks of UI in GPUI, and they provide a nice wrapper around an imperative API that provides as much flexibility and control as you need. Elements have total control over how they and their child elements are rendered and can be used for making efficient views into large lists, implement custom layouting for a code editor, and anything else you can think of. See the `element` module for more information. + +Each of these registers has one or more corresponding contexts that can be accessed from all GPUI services. This context is your main interface to GPUI, and is used extensively throughout the framework. + +## Other Resources + +In addition to the systems above, GPUI provides a range of smaller services that are useful for building complex applications: + +- Actions are user-defined structs that are used for converting keystrokes into logical operations in your UI. Use this for implementing keyboard shortcuts, such as cmd-q. See the `action` module for more information. + +- Platform services, such as `quit the app` or `open a URL` are available as methods on the `app::App`. + +- An async executor that is integrated with the platform's event loop. See the `executor` module for more information., + +- The `[gpui::test]` macro provides a convenient way to write tests for your GPUI applications. Tests also have their own kind of context, a `TestAppContext` which provides ways of simulating common platform input. See `app::test_context` and `test` modules for more details. + +Currently, the best way to learn about these APIs is to read the Zed source code, ask us about it at a fireside hack, or drop a question in the [Zed Discord](https://zed.dev/community-links). We're working on improving the documentation, creating more examples, and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog). diff --git a/third_party/gpui/build.rs b/third_party/gpui/build.rs new file mode 100644 index 0000000..83aea8a --- /dev/null +++ b/third_party/gpui/build.rs @@ -0,0 +1,457 @@ +#![allow(clippy::disallowed_methods, reason = "build scripts are exempt")] +#![cfg_attr(any(not(target_os = "macos"), feature = "macos-blade"), allow(unused))] + +//TODO: consider generating shader code for WGSL +//TODO: deprecate "runtime-shaders" and "macos-blade" + +use std::env; + +fn main() { + let target = env::var("CARGO_CFG_TARGET_OS"); + println!("cargo::rustc-check-cfg=cfg(gles)"); + + #[cfg(any( + not(any(target_os = "macos", target_os = "windows")), + all(target_os = "macos", feature = "macos-blade") + ))] + check_wgsl_shaders(); + + match target.as_deref() { + Ok("macos") => { + #[cfg(target_os = "macos")] + macos::build(); + } + Ok("windows") => { + #[cfg(target_os = "windows")] + windows::build(); + } + _ => (), + }; +} + +#[cfg(any( + not(any(target_os = "macos", target_os = "windows")), + all(target_os = "macos", feature = "macos-blade") +))] +fn check_wgsl_shaders() { + use std::path::PathBuf; + use std::process; + use std::str::FromStr; + + let shader_source_path = "./src/platform/blade/shaders.wgsl"; + let shader_path = PathBuf::from_str(shader_source_path).unwrap(); + println!("cargo:rerun-if-changed={}", &shader_path.display()); + + let shader_source = std::fs::read_to_string(&shader_path).unwrap(); + + match naga::front::wgsl::parse_str(&shader_source) { + Ok(_) => { + // All clear + } + Err(e) => { + println!("cargo::error=WGSL shader compilation failed:\n{}", e); + process::exit(1); + } + } +} +#[cfg(target_os = "macos")] +mod macos { + use std::{ + env, + path::{Path, PathBuf}, + }; + + use cbindgen::Config; + + pub(super) fn build() { + generate_dispatch_bindings(); + #[cfg(not(feature = "macos-blade"))] + { + let header_path = generate_shader_bindings(); + + #[cfg(feature = "runtime_shaders")] + emit_stitched_shaders(&header_path); + #[cfg(not(feature = "runtime_shaders"))] + compile_metal_shaders(&header_path); + } + } + + fn generate_dispatch_bindings() { + println!("cargo:rustc-link-lib=framework=System"); + + let bindings = bindgen::Builder::default() + .header("src/platform/mac/dispatch.h") + .allowlist_var("_dispatch_main_q") + .allowlist_var("_dispatch_source_type_data_add") + .allowlist_var("DISPATCH_QUEUE_PRIORITY_HIGH") + .allowlist_var("DISPATCH_TIME_NOW") + .allowlist_function("dispatch_get_global_queue") + .allowlist_function("dispatch_async_f") + .allowlist_function("dispatch_after_f") + .allowlist_function("dispatch_time") + .allowlist_function("dispatch_source_merge_data") + .allowlist_function("dispatch_source_create") + .allowlist_function("dispatch_source_set_event_handler_f") + .allowlist_function("dispatch_resume") + .allowlist_function("dispatch_suspend") + .allowlist_function("dispatch_source_cancel") + .allowlist_function("dispatch_set_context") + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + .layout_tests(false) + .generate() + .expect("unable to generate bindings"); + + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("dispatch_sys.rs")) + .expect("couldn't write dispatch bindings"); + } + + fn generate_shader_bindings() -> PathBuf { + let output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("scene.h"); + let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let mut config = Config { + include_guard: Some("SCENE_H".into()), + language: cbindgen::Language::C, + no_includes: true, + ..Default::default() + }; + config.export.include.extend([ + "Bounds".into(), + "Corners".into(), + "Edges".into(), + "Size".into(), + "Pixels".into(), + "PointF".into(), + "Hsla".into(), + "ContentMask".into(), + "Uniforms".into(), + "AtlasTile".into(), + "PathRasterizationInputIndex".into(), + "PathVertex_ScaledPixels".into(), + "PathRasterizationVertex".into(), + "ShadowInputIndex".into(), + "Shadow".into(), + "QuadInputIndex".into(), + "Underline".into(), + "UnderlineInputIndex".into(), + "Quad".into(), + "BorderStyle".into(), + "SpriteInputIndex".into(), + "MonochromeSprite".into(), + "PolychromeSprite".into(), + "PathSprite".into(), + "SurfaceInputIndex".into(), + "SurfaceBounds".into(), + "TransformationMatrix".into(), + ]); + config.no_includes = true; + config.enumeration.prefix_with_name = true; + + let mut builder = cbindgen::Builder::new(); + + let src_paths = [ + crate_dir.join("src/scene.rs"), + crate_dir.join("src/geometry.rs"), + crate_dir.join("src/color.rs"), + crate_dir.join("src/window.rs"), + crate_dir.join("src/platform.rs"), + crate_dir.join("src/platform/mac/metal_renderer.rs"), + ]; + for src_path in src_paths { + println!("cargo:rerun-if-changed={}", src_path.display()); + builder = builder.with_src(src_path); + } + + builder + .with_config(config) + .generate() + .expect("Unable to generate bindings") + .write_to_file(&output_path); + + output_path + } + + /// To enable runtime compilation, we need to "stitch" the shaders file with the generated header + /// so that it is self-contained. + #[cfg(feature = "runtime_shaders")] + fn emit_stitched_shaders(header_path: &Path) { + use std::str::FromStr; + fn stitch_header(header: &Path, shader_path: &Path) -> std::io::Result { + let header_contents = std::fs::read_to_string(header)?; + let shader_contents = std::fs::read_to_string(shader_path)?; + let stitched_contents = format!("{header_contents}\n{shader_contents}"); + let out_path = + PathBuf::from(env::var("OUT_DIR").unwrap()).join("stitched_shaders.metal"); + std::fs::write(&out_path, stitched_contents)?; + Ok(out_path) + } + let shader_source_path = "./src/platform/mac/shaders.metal"; + let shader_path = PathBuf::from_str(shader_source_path).unwrap(); + stitch_header(header_path, &shader_path).unwrap(); + println!("cargo:rerun-if-changed={}", &shader_source_path); + } + + #[cfg(not(feature = "runtime_shaders"))] + fn compile_metal_shaders(header_path: &Path) { + use std::process::{self, Command}; + let shader_path = "./src/platform/mac/shaders.metal"; + let air_output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.air"); + let metallib_output_path = + PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.metallib"); + println!("cargo:rerun-if-changed={}", shader_path); + + let output = Command::new("xcrun") + .args([ + "-sdk", + "macosx", + "metal", + "-gline-tables-only", + "-mmacosx-version-min=10.15.7", + "-MO", + "-c", + shader_path, + "-include", + (header_path.to_str().unwrap()), + "-o", + ]) + .arg(&air_output_path) + .output() + .unwrap(); + + if !output.status.success() { + println!( + "cargo::error=metal shader compilation failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + process::exit(1); + } + + let output = Command::new("xcrun") + .args(["-sdk", "macosx", "metallib"]) + .arg(air_output_path) + .arg("-o") + .arg(metallib_output_path) + .output() + .unwrap(); + + if !output.status.success() { + println!( + "cargo::error=metallib compilation failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + process::exit(1); + } + } +} + +#[cfg(target_os = "windows")] +mod windows { + use std::{ + fs, + io::Write, + path::{Path, PathBuf}, + process::{self, Command}, + }; + + pub(super) fn build() { + // Compile HLSL shaders + #[cfg(not(debug_assertions))] + compile_shaders(); + + // Embed the Windows manifest and resource file + #[cfg(feature = "windows-manifest")] + embed_resource(); + } + + #[cfg(feature = "windows-manifest")] + fn embed_resource() { + let manifest = std::path::Path::new("resources/windows/gpui.manifest.xml"); + let rc_file = std::path::Path::new("resources/windows/gpui.rc"); + println!("cargo:rerun-if-changed={}", manifest.display()); + println!("cargo:rerun-if-changed={}", rc_file.display()); + embed_resource::compile(rc_file, embed_resource::NONE) + .manifest_required() + .unwrap(); + } + + /// You can set the `GPUI_FXC_PATH` environment variable to specify the path to the fxc.exe compiler. + fn compile_shaders() { + let shader_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()) + .join("src/platform/windows/shaders.hlsl"); + let out_dir = std::env::var("OUT_DIR").unwrap(); + + println!("cargo:rerun-if-changed={}", shader_path.display()); + + // Check if fxc.exe is available + let fxc_path = find_fxc_compiler(); + + // Define all modules + let modules = [ + "quad", + "shadow", + "path_rasterization", + "path_sprite", + "underline", + "monochrome_sprite", + "polychrome_sprite", + ]; + + let rust_binding_path = format!("{}/shaders_bytes.rs", out_dir); + if Path::new(&rust_binding_path).exists() { + fs::remove_file(&rust_binding_path) + .expect("Failed to remove existing Rust binding file"); + } + for module in modules { + compile_shader_for_module( + module, + &out_dir, + &fxc_path, + shader_path.to_str().unwrap(), + &rust_binding_path, + ); + } + + { + let shader_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()) + .join("src/platform/windows/color_text_raster.hlsl"); + compile_shader_for_module( + "emoji_rasterization", + &out_dir, + &fxc_path, + shader_path.to_str().unwrap(), + &rust_binding_path, + ); + } + } + + /// You can set the `GPUI_FXC_PATH` environment variable to specify the path to the fxc.exe compiler. + fn find_fxc_compiler() -> String { + // Check environment variable + if let Ok(path) = std::env::var("GPUI_FXC_PATH") + && Path::new(&path).exists() + { + return path; + } + + // Try to find in PATH + // NOTE: This has to be `where.exe` on Windows, not `where`, it must be ended with `.exe` + if let Ok(output) = std::process::Command::new("where.exe") + .arg("fxc.exe") + .output() + && output.status.success() + { + let path = String::from_utf8_lossy(&output.stdout); + return path.trim().to_string(); + } + + // Check the default path + if Path::new(r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\fxc.exe") + .exists() + { + return r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\fxc.exe" + .to_string(); + } + + panic!("Failed to find fxc.exe"); + } + + fn compile_shader_for_module( + module: &str, + out_dir: &str, + fxc_path: &str, + shader_path: &str, + rust_binding_path: &str, + ) { + // Compile vertex shader + let output_file = format!("{}/{}_vs.h", out_dir, module); + let const_name = format!("{}_VERTEX_BYTES", module.to_uppercase()); + compile_shader_impl( + fxc_path, + &format!("{module}_vertex"), + &output_file, + &const_name, + shader_path, + "vs_4_1", + ); + generate_rust_binding(&const_name, &output_file, rust_binding_path); + + // Compile fragment shader + let output_file = format!("{}/{}_ps.h", out_dir, module); + let const_name = format!("{}_FRAGMENT_BYTES", module.to_uppercase()); + compile_shader_impl( + fxc_path, + &format!("{module}_fragment"), + &output_file, + &const_name, + shader_path, + "ps_4_1", + ); + generate_rust_binding(&const_name, &output_file, rust_binding_path); + } + + fn compile_shader_impl( + fxc_path: &str, + entry_point: &str, + output_path: &str, + var_name: &str, + shader_path: &str, + target: &str, + ) { + let output = Command::new(fxc_path) + .args([ + "/T", + target, + "/E", + entry_point, + "/Fh", + output_path, + "/Vn", + var_name, + "/O3", + shader_path, + ]) + .output(); + + match output { + Ok(result) => { + if result.status.success() { + return; + } + println!( + "cargo::error=Shader compilation failed for {}:\n{}", + entry_point, + String::from_utf8_lossy(&result.stderr) + ); + process::exit(1); + } + Err(e) => { + println!("cargo::error=Failed to run fxc for {}: {}", entry_point, e); + process::exit(1); + } + } + } + + fn generate_rust_binding(const_name: &str, head_file: &str, output_path: &str) { + let header_content = fs::read_to_string(head_file).expect("Failed to read header file"); + let const_definition = { + let global_var_start = header_content.find("const BYTE").unwrap(); + let global_var = &header_content[global_var_start..]; + let equal = global_var.find('=').unwrap(); + global_var[equal + 1..].trim() + }; + let rust_binding = format!( + "const {}: &[u8] = &{}\n", + const_name, + const_definition.replace('{', "[").replace('}', "]") + ); + let mut options = fs::OpenOptions::new() + .create(true) + .append(true) + .open(output_path) + .expect("Failed to open Rust binding file"); + options + .write_all(rust_binding.as_bytes()) + .expect("Failed to write Rust binding file"); + } +} diff --git a/third_party/gpui/resources/windows/gpui.manifest.xml b/third_party/gpui/resources/windows/gpui.manifest.xml new file mode 100644 index 0000000..5a69b43 --- /dev/null +++ b/third_party/gpui/resources/windows/gpui.manifest.xml @@ -0,0 +1,16 @@ + + + + true + PerMonitorV2 + + + + + + + + diff --git a/third_party/gpui/resources/windows/gpui.rc b/third_party/gpui/resources/windows/gpui.rc new file mode 100644 index 0000000..a6f3787 --- /dev/null +++ b/third_party/gpui/resources/windows/gpui.rc @@ -0,0 +1,2 @@ +#define RT_MANIFEST 24 +1 RT_MANIFEST "resources/windows/gpui.manifest.xml" \ No newline at end of file diff --git a/third_party/gpui/src/_ownership_and_data_flow.rs b/third_party/gpui/src/_ownership_and_data_flow.rs new file mode 100644 index 0000000..9bb8bf6 --- /dev/null +++ b/third_party/gpui/src/_ownership_and_data_flow.rs @@ -0,0 +1,140 @@ +//! In GPUI, every model or view in the application is actually owned by a single top-level object called the `App`. When a new entity or view is created (referred to collectively as _entities_), the application is given ownership of their state to enable their participation in a variety of app services and interaction with other entities. +//! +//! To illustrate, consider the trivial app below. We start the app by calling `run` with a callback, which is passed a reference to the `App` that owns all the state for the application. This `App` is our gateway to all application-level services, such as opening windows, presenting dialogs, etc. It also has an `insert_entity` method, which is called below to create an entity and give ownership of it to the application. +//! +//! ```no_run +//! # use gpui::{App, AppContext, Application, Entity}; +//! # struct Counter { +//! # count: usize, +//! # } +//! Application::new().run(|cx: &mut App| { +//! let _counter: Entity = cx.new(|_cx| Counter { count: 0 }); +//! // ... +//! }); +//! ``` +//! +//! The call to `new_entity` returns an _entity handle_, which carries a type parameter based on the type of object it references. By itself, this `Entity` handle doesn't provide access to the entity's state. It's merely an inert identifier plus a compile-time type tag, and it maintains a reference counted pointer to the underlying `Counter` object that is owned by the app. +//! +//! Much like an `Rc` from the Rust standard library, this reference count is incremented when the handle is cloned and decremented when it is dropped to enable shared ownership over the underlying model, but unlike an `Rc` it only provides access to the model's state when a reference to an `App` is available. The handle doesn't truly _own_ the state, but it can be used to access the state from its true owner, the `App`. Stripping away some of the setup code for brevity: +//! +//! ```no_run +//! # use gpui::{App, AppContext, Application, Context, Entity}; +//! # struct Counter { +//! # count: usize, +//! # } +//! Application::new().run(|cx: &mut App| { +//! let counter: Entity = cx.new(|_cx| Counter { count: 0 }); +//! // Call `update` to access the model's state. +//! counter.update(cx, |counter: &mut Counter, _cx: &mut Context| { +//! counter.count += 1; +//! }); +//! }); +//! ``` +//! +//! To update the counter, we call `update` on the handle, passing the context reference and a callback. The callback is yielded a mutable reference to the counter, which can be used to manipulate state. +//! +//! The callback is also provided a second `Context` reference. This reference is similar to the `App` reference provided to the `run` callback. A `Context` is actually a wrapper around the `App`, including some additional data to indicate which particular entity it is tied to; in this case the counter. +//! +//! In addition to the application-level services provided by `App`, a `Context` provides access to entity-level services. For example, it can be used it to inform observers of this entity that its state has changed. Let's add that to our example, by calling `cx.notify()`. +//! +//! ```no_run +//! # use gpui::{App, AppContext, Application, Entity}; +//! # struct Counter { +//! # count: usize, +//! # } +//! Application::new().run(|cx: &mut App| { +//! let counter: Entity = cx.new(|_cx| Counter { count: 0 }); +//! counter.update(cx, |counter, cx| { +//! counter.count += 1; +//! cx.notify(); // Notify observers +//! }); +//! }); +//! ``` +//! +//! Next, these notifications need to be observed and reacted to. Before updating the counter, we'll construct a second counter that observes it. Whenever the first counter changes, twice its count is assigned to the second counter. Note how `observe` is called on the `Context` belonging to our second counter to arrange for it to be notified whenever the first counter notifies. The call to `observe` returns a `Subscription`, which is `detach`ed to preserve this behavior for as long as both counters exist. We could also store this subscription and drop it at a time of our choosing to cancel this behavior. +//! +//! The `observe` callback is passed a mutable reference to the observer and a _handle_ to the observed counter, whose state we access with the `read` method. +//! +//! ```no_run +//! # use gpui::{App, AppContext, Application, Entity, prelude::*}; +//! # struct Counter { +//! # count: usize, +//! # } +//! Application::new().run(|cx: &mut App| { +//! let first_counter: Entity = cx.new(|_cx| Counter { count: 0 }); +//! +//! let second_counter = cx.new(|cx: &mut Context| { +//! // Note we can set up the callback before the Counter is even created! +//! cx.observe( +//! &first_counter, +//! |second: &mut Counter, first: Entity, cx| { +//! second.count = first.read(cx).count * 2; +//! }, +//! ) +//! .detach(); +//! +//! Counter { count: 0 } +//! }); +//! +//! first_counter.update(cx, |counter, cx| { +//! counter.count += 1; +//! cx.notify(); +//! }); +//! +//! assert_eq!(second_counter.read(cx).count, 2); +//! }); +//! ``` +//! +//! After updating the first counter, it can be noted that the observing counter's state is maintained according to our subscription. +//! +//! In addition to `observe` and `notify`, which indicate that an entity's state has changed, GPUI also offers `subscribe` and `emit`, which enables entities to emit typed events. To opt into this system, the emitting object must implement the `EventEmitter` trait. +//! +//! Let's introduce a new event type called `CounterChangeEvent`, then indicate that `Counter` can emit this type of event: +//! +//! ```no_run +//! use gpui::EventEmitter; +//! # struct Counter { +//! # count: usize, +//! # } +//! struct CounterChangeEvent { +//! increment: usize, +//! } +//! +//! impl EventEmitter for Counter {} +//! ``` +//! +//! Next, the example should be updated, replacing the observation with a subscription. Whenever the counter is incremented, a `Change` event is emitted to indicate the magnitude of the increase. +//! +//! ```no_run +//! # use gpui::{App, AppContext, Application, Context, Entity, EventEmitter}; +//! # struct Counter { +//! # count: usize, +//! # } +//! # struct CounterChangeEvent { +//! # increment: usize, +//! # } +//! # impl EventEmitter for Counter {} +//! Application::new().run(|cx: &mut App| { +//! let first_counter: Entity = cx.new(|_cx| Counter { count: 0 }); +//! +//! let second_counter = cx.new(|cx: &mut Context| { +//! // Note we can set up the callback before the Counter is even created! +//! cx.subscribe(&first_counter, |second: &mut Counter, _first: Entity, event, _cx| { +//! second.count += event.increment * 2; +//! }) +//! .detach(); +//! +//! Counter { +//! count: first_counter.read(cx).count * 2, +//! } +//! }); +//! +//! first_counter.update(cx, |first, cx| { +//! first.count += 2; +//! cx.emit(CounterChangeEvent { increment: 2 }); +//! cx.notify(); +//! }); +//! +//! assert_eq!(second_counter.read(cx).count, 4); +//! }); +//! ``` diff --git a/third_party/gpui/src/action.rs b/third_party/gpui/src/action.rs new file mode 100644 index 0000000..38e94aa --- /dev/null +++ b/third_party/gpui/src/action.rs @@ -0,0 +1,440 @@ +use anyhow::{Context as _, Result}; +use collections::HashMap; +pub use gpui_macros::Action; +pub use no_action::{NoAction, is_no_action}; +use serde_json::json; +use std::{ + any::{Any, TypeId}, + fmt::Display, +}; + +/// Defines and registers unit structs that can be used as actions. For more complex data types, derive `Action`. +/// +/// For example: +/// +/// ``` +/// use gpui::actions; +/// actions!(editor, [MoveUp, MoveDown, MoveLeft, MoveRight, Newline]); +/// ``` +/// +/// This will create actions with names like `editor::MoveUp`, `editor::MoveDown`, etc. +/// +/// The namespace argument `editor` can also be omitted, though it is required for Zed actions. +#[macro_export] +macro_rules! actions { + ($namespace:path, [ $( $(#[$attr:meta])* $name:ident),* $(,)? ]) => { + $( + #[derive(::std::clone::Clone, ::std::cmp::PartialEq, ::std::default::Default, ::std::fmt::Debug, gpui::Action)] + #[action(namespace = $namespace)] + $(#[$attr])* + pub struct $name; + )* + }; + ([ $( $(#[$attr:meta])* $name:ident),* $(,)? ]) => { + $( + #[derive(::std::clone::Clone, ::std::cmp::PartialEq, ::std::default::Default, ::std::fmt::Debug, gpui::Action)] + $(#[$attr])* + pub struct $name; + )* + }; +} + +/// Actions are used to implement keyboard-driven UI. When you declare an action, you can bind keys +/// to the action in the keymap and listeners for that action in the element tree. +/// +/// To declare a list of simple actions, you can use the actions! macro, which defines a simple unit +/// struct action for each listed action name in the given namespace. +/// +/// ``` +/// use gpui::actions; +/// actions!(editor, [MoveUp, MoveDown, MoveLeft, MoveRight, Newline]); +/// ``` +/// +/// Registering the actions with the same name will result in a panic during `App` creation. +/// +/// # Derive Macro +/// +/// More complex data types can also be actions, by using the derive macro for `Action`: +/// +/// ``` +/// use gpui::Action; +/// #[derive(Clone, PartialEq, serde::Deserialize, schemars::JsonSchema, Action)] +/// #[action(namespace = editor)] +/// pub struct SelectNext { +/// pub replace_newest: bool, +/// } +/// ``` +/// +/// The derive macro for `Action` requires that the type implement `Clone` and `PartialEq`. It also +/// requires `serde::Deserialize` and `schemars::JsonSchema` unless `#[action(no_json)]` is +/// specified. In Zed these trait impls are used to load keymaps from JSON. +/// +/// Multiple arguments separated by commas may be specified in `#[action(...)]`: +/// +/// - `namespace = some_namespace` sets the namespace. In Zed this is required. +/// +/// - `name = "ActionName"` overrides the action's name. This must not contain `::`. +/// +/// - `no_json` causes the `build` method to always error and `action_json_schema` to return `None`, +/// and allows actions not implement `serde::Serialize` and `schemars::JsonSchema`. +/// +/// - `no_register` skips registering the action. This is useful for implementing the `Action` trait +/// while not supporting invocation by name or JSON deserialization. +/// +/// - `deprecated_aliases = ["editor::SomeAction"]` specifies deprecated old names for the action. +/// These action names should *not* correspond to any actions that are registered. These old names +/// can then still be used to refer to invoke this action. In Zed, the keymap JSON schema will +/// accept these old names and provide warnings. +/// +/// - `deprecated = "Message about why this action is deprecation"` specifies a deprecation message. +/// In Zed, the keymap JSON schema will cause this to be displayed as a warning. +/// +/// # Manual Implementation +/// +/// If you want to control the behavior of the action trait manually, you can use the lower-level +/// `#[register_action]` macro, which only generates the code needed to register your action before +/// `main`. +/// +/// ``` +/// use gpui::{SharedString, register_action}; +/// #[derive(Clone, PartialEq, Eq, serde::Deserialize, schemars::JsonSchema)] +/// pub struct Paste { +/// pub content: SharedString, +/// } +/// +/// impl gpui::Action for Paste { +/// # fn boxed_clone(&self) -> Box { unimplemented!()} +/// # fn partial_eq(&self, other: &dyn gpui::Action) -> bool { unimplemented!() } +/// # fn name(&self) -> &'static str { "Paste" } +/// # fn name_for_type() -> &'static str { "Paste" } +/// # fn build(value: serde_json::Value) -> anyhow::Result> { +/// # unimplemented!() +/// # } +/// } +/// +/// register_action!(Paste); +/// ``` +pub trait Action: Any + Send { + /// Clone the action into a new box + fn boxed_clone(&self) -> Box; + + /// Do a partial equality check on this action and the other + fn partial_eq(&self, action: &dyn Action) -> bool; + + /// Get the name of this action, for displaying in UI + fn name(&self) -> &'static str; + + /// Get the name of this action type (static) + fn name_for_type() -> &'static str + where + Self: Sized; + + /// Build this action from a JSON value. This is used to construct actions from the keymap. + /// A value of `{}` will be passed for actions that don't have any parameters. + fn build(value: serde_json::Value) -> Result> + where + Self: Sized; + + /// Optional JSON schema for the action's input data. + fn action_json_schema(_: &mut schemars::SchemaGenerator) -> Option + where + Self: Sized, + { + None + } + + /// A list of alternate, deprecated names for this action. These names can still be used to + /// invoke the action. In Zed, the keymap JSON schema will accept these old names and provide + /// warnings. + fn deprecated_aliases() -> &'static [&'static str] + where + Self: Sized, + { + &[] + } + + /// Returns the deprecation message for this action, if any. In Zed, the keymap JSON schema will + /// cause this to be displayed as a warning. + fn deprecation_message() -> Option<&'static str> + where + Self: Sized, + { + None + } + + /// The documentation for this action, if any. When using the derive macro for actions + /// this will be automatically generated from the doc comments on the action struct. + fn documentation() -> Option<&'static str> + where + Self: Sized, + { + None + } +} + +impl std::fmt::Debug for dyn Action { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("dyn Action") + .field("name", &self.name()) + .finish() + } +} + +impl dyn Action { + /// Type-erase Action type. + pub fn as_any(&self) -> &dyn Any { + self as &dyn Any + } +} + +/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use +/// markdown to display it. +#[derive(Debug)] +pub enum ActionBuildError { + /// Indicates that an action with this name has not been registered. + NotFound { + /// Name of the action that was not found. + name: String, + }, + /// Indicates that an error occurred while building the action, typically a JSON deserialization + /// error. + BuildError { + /// Name of the action that was attempting to be built. + name: String, + /// Error that occurred while building the action. + error: anyhow::Error, + }, +} + +impl std::error::Error for ActionBuildError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + ActionBuildError::NotFound { .. } => None, + ActionBuildError::BuildError { error, .. } => error.source(), + } + } +} + +impl Display for ActionBuildError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ActionBuildError::NotFound { name } => { + write!(f, "Didn't find an action named \"{name}\"") + } + ActionBuildError::BuildError { name, error } => { + write!(f, "Error while building action \"{name}\": {error}") + } + } + } +} + +type ActionBuilder = fn(json: serde_json::Value) -> anyhow::Result>; + +pub(crate) struct ActionRegistry { + by_name: HashMap<&'static str, ActionData>, + names_by_type_id: HashMap, + all_names: Vec<&'static str>, // So we can return a static slice. + deprecated_aliases: HashMap<&'static str, &'static str>, // deprecated name -> preferred name + deprecation_messages: HashMap<&'static str, &'static str>, // action name -> deprecation message + documentation: HashMap<&'static str, &'static str>, // action name -> documentation +} + +impl Default for ActionRegistry { + fn default() -> Self { + let mut this = ActionRegistry { + by_name: Default::default(), + names_by_type_id: Default::default(), + documentation: Default::default(), + all_names: Default::default(), + deprecated_aliases: Default::default(), + deprecation_messages: Default::default(), + }; + + this.load_actions(); + + this + } +} + +struct ActionData { + pub build: ActionBuilder, + pub json_schema: fn(&mut schemars::SchemaGenerator) -> Option, +} + +/// This type must be public so that our macros can build it in other crates. +/// But this is an implementation detail and should not be used directly. +#[doc(hidden)] +pub struct MacroActionBuilder(pub fn() -> MacroActionData); + +/// This type must be public so that our macros can build it in other crates. +/// But this is an implementation detail and should not be used directly. +#[doc(hidden)] +pub struct MacroActionData { + pub name: &'static str, + pub type_id: TypeId, + pub build: ActionBuilder, + pub json_schema: fn(&mut schemars::SchemaGenerator) -> Option, + pub deprecated_aliases: &'static [&'static str], + pub deprecation_message: Option<&'static str>, + pub documentation: Option<&'static str>, +} + +inventory::collect!(MacroActionBuilder); + +impl ActionRegistry { + /// Load all registered actions into the registry. + pub(crate) fn load_actions(&mut self) { + for builder in inventory::iter:: { + let action = builder.0(); + self.insert_action(action); + } + } + + #[cfg(test)] + pub(crate) fn load_action(&mut self) { + self.insert_action(MacroActionData { + name: A::name_for_type(), + type_id: TypeId::of::(), + build: A::build, + json_schema: A::action_json_schema, + deprecated_aliases: A::deprecated_aliases(), + deprecation_message: A::deprecation_message(), + documentation: A::documentation(), + }); + } + + fn insert_action(&mut self, action: MacroActionData) { + let name = action.name; + if self.by_name.contains_key(name) { + panic!( + "Action with name `{name}` already registered \ + (might be registered in `#[action(deprecated_aliases = [...])]`." + ); + } + self.by_name.insert( + name, + ActionData { + build: action.build, + json_schema: action.json_schema, + }, + ); + for &alias in action.deprecated_aliases { + if self.by_name.contains_key(alias) { + panic!( + "Action with name `{alias}` already registered. \ + `{alias}` is specified in `#[action(deprecated_aliases = [...])]` for action `{name}`." + ); + } + self.by_name.insert( + alias, + ActionData { + build: action.build, + json_schema: action.json_schema, + }, + ); + self.deprecated_aliases.insert(alias, name); + self.all_names.push(alias); + } + self.names_by_type_id.insert(action.type_id, name); + self.all_names.push(name); + if let Some(deprecation_msg) = action.deprecation_message { + self.deprecation_messages.insert(name, deprecation_msg); + } + if let Some(documentation) = action.documentation { + self.documentation.insert(name, documentation); + } + } + + /// Construct an action based on its name and optional JSON parameters sourced from the keymap. + pub fn build_action_type(&self, type_id: &TypeId) -> Result> { + let name = self + .names_by_type_id + .get(type_id) + .with_context(|| format!("no action type registered for {type_id:?}"))?; + + Ok(self.build_action(name, None)?) + } + + /// Construct an action based on its name and optional JSON parameters sourced from the keymap. + pub fn build_action( + &self, + name: &str, + params: Option, + ) -> std::result::Result, ActionBuildError> { + let build_action = self + .by_name + .get(name) + .ok_or_else(|| ActionBuildError::NotFound { + name: name.to_owned(), + })? + .build; + (build_action)(params.unwrap_or_else(|| json!({}))).map_err(|e| { + ActionBuildError::BuildError { + name: name.to_owned(), + error: e, + } + }) + } + + pub fn all_action_names(&self) -> &[&'static str] { + self.all_names.as_slice() + } + + pub fn action_schemas( + &self, + generator: &mut schemars::SchemaGenerator, + ) -> Vec<(&'static str, Option)> { + // Use the order from all_names so that the resulting schema has sensible order. + self.all_names + .iter() + .map(|name| { + let action_data = self + .by_name + .get(name) + .expect("All actions in all_names should be registered"); + (*name, (action_data.json_schema)(generator)) + }) + .collect::>() + } + + pub fn deprecated_aliases(&self) -> &HashMap<&'static str, &'static str> { + &self.deprecated_aliases + } + + pub fn deprecation_messages(&self) -> &HashMap<&'static str, &'static str> { + &self.deprecation_messages + } + + pub fn documentation(&self) -> &HashMap<&'static str, &'static str> { + &self.documentation + } +} + +/// Generate a list of all the registered actions. +/// Useful for transforming the list of available actions into a +/// format suited for static analysis such as in validating keymaps, or +/// generating documentation. +pub fn generate_list_of_all_registered_actions() -> impl Iterator { + inventory::iter:: + .into_iter() + .map(|builder| builder.0()) +} + +mod no_action { + use crate as gpui; + use std::any::Any as _; + + actions!( + zed, + [ + /// Action with special handling which unbinds the keybinding this is associated with, + /// if it is the highest precedence match. + NoAction + ] + ); + + /// Returns whether or not this action represents a removed key binding. + pub fn is_no_action(action: &dyn gpui::Action) -> bool { + action.as_any().type_id() == (NoAction {}).type_id() + } +} diff --git a/third_party/gpui/src/app.rs b/third_party/gpui/src/app.rs new file mode 100644 index 0000000..d4bd779 --- /dev/null +++ b/third_party/gpui/src/app.rs @@ -0,0 +1,2460 @@ +use std::{ + any::{TypeId, type_name}, + cell::{BorrowMutError, Ref, RefCell, RefMut}, + marker::PhantomData, + mem, + ops::{Deref, DerefMut}, + path::{Path, PathBuf}, + rc::{Rc, Weak}, + sync::{Arc, atomic::Ordering::SeqCst}, + time::{Duration, Instant}, +}; + +use anyhow::{Context as _, Result, anyhow}; +use derive_more::{Deref, DerefMut}; +use futures::{ + Future, FutureExt, + channel::oneshot, + future::{LocalBoxFuture, Shared}, +}; +use itertools::Itertools; +use parking_lot::RwLock; +use slotmap::SlotMap; + +pub use async_context::*; +use collections::{FxHashMap, FxHashSet, HashMap, VecDeque}; +pub use context::*; +pub use entity_map::*; +use http_client::{HttpClient, Url}; +use smallvec::SmallVec; +#[cfg(any(test, feature = "test-support"))] +pub use test_context::*; +use util::{ResultExt, debug_panic}; + +#[cfg(any(feature = "inspector", debug_assertions))] +use crate::InspectorElementRegistry; +use crate::{ + Action, ActionBuildError, ActionRegistry, Any, AnyView, AnyWindowHandle, AppContext, Asset, + AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId, + EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext, + Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform, + PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, PromptBuilder, + PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle, + Reservation, ScreenCaptureSource, SharedString, SubscriberSet, Subscription, SvgRenderer, Task, + TextSystem, Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator, + colors::{Colors, GlobalColors}, + current_platform, hash, init_app_menus, +}; + +mod async_context; +mod context; +mod entity_map; +#[cfg(any(test, feature = "test-support"))] +mod test_context; + +/// The duration for which futures returned from [Context::on_app_quit] can run before the application fully quits. +pub const SHUTDOWN_TIMEOUT: Duration = Duration::from_millis(100); + +/// Temporary(?) wrapper around [`RefCell`] to help us debug any double borrows. +/// Strongly consider removing after stabilization. +#[doc(hidden)] +pub struct AppCell { + app: RefCell, +} + +impl AppCell { + #[doc(hidden)] + #[track_caller] + pub fn borrow(&self) -> AppRef<'_> { + if option_env!("TRACK_THREAD_BORROWS").is_some() { + let thread_id = std::thread::current().id(); + eprintln!("borrowed {thread_id:?}"); + } + AppRef(self.app.borrow()) + } + + #[doc(hidden)] + #[track_caller] + pub fn borrow_mut(&self) -> AppRefMut<'_> { + if option_env!("TRACK_THREAD_BORROWS").is_some() { + let thread_id = std::thread::current().id(); + eprintln!("borrowed {thread_id:?}"); + } + AppRefMut(self.app.borrow_mut()) + } + + #[doc(hidden)] + #[track_caller] + pub fn try_borrow_mut(&self) -> Result, BorrowMutError> { + if option_env!("TRACK_THREAD_BORROWS").is_some() { + let thread_id = std::thread::current().id(); + eprintln!("borrowed {thread_id:?}"); + } + Ok(AppRefMut(self.app.try_borrow_mut()?)) + } +} + +#[doc(hidden)] +#[derive(Deref, DerefMut)] +pub struct AppRef<'a>(Ref<'a, App>); + +impl Drop for AppRef<'_> { + fn drop(&mut self) { + if option_env!("TRACK_THREAD_BORROWS").is_some() { + let thread_id = std::thread::current().id(); + eprintln!("dropped borrow from {thread_id:?}"); + } + } +} + +#[doc(hidden)] +#[derive(Deref, DerefMut)] +pub struct AppRefMut<'a>(RefMut<'a, App>); + +impl Drop for AppRefMut<'_> { + fn drop(&mut self) { + if option_env!("TRACK_THREAD_BORROWS").is_some() { + let thread_id = std::thread::current().id(); + eprintln!("dropped {thread_id:?}"); + } + } +} + +/// A reference to a GPUI application, typically constructed in the `main` function of your app. +/// You won't interact with this type much outside of initial configuration and startup. +pub struct Application(Rc); + +/// Represents an application before it is fully launched. Once your app is +/// configured, you'll start the app with `App::run`. +impl Application { + /// Builds an app with the given asset source. + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + #[cfg(any(test, feature = "test-support"))] + log::info!("GPUI was compiled in test mode"); + + Self(App::new_app( + current_platform(false), + Arc::new(()), + Arc::new(NullHttpClient), + )) + } + + /// Build an app in headless mode. This prevents opening windows, + /// but makes it possible to run an application in an context like + /// SSH, where GUI applications are not allowed. + pub fn headless() -> Self { + Self(App::new_app( + current_platform(true), + Arc::new(()), + Arc::new(NullHttpClient), + )) + } + + /// Assign + pub fn with_assets(self, asset_source: impl AssetSource) -> Self { + let mut context_lock = self.0.borrow_mut(); + let asset_source = Arc::new(asset_source); + context_lock.asset_source = asset_source.clone(); + context_lock.svg_renderer = SvgRenderer::new(asset_source); + drop(context_lock); + self + } + + /// Sets the HTTP client for the application. + pub fn with_http_client(self, http_client: Arc) -> Self { + let mut context_lock = self.0.borrow_mut(); + context_lock.http_client = http_client; + drop(context_lock); + self + } + + /// Start the application. The provided callback will be called once the + /// app is fully launched. + pub fn run(self, on_finish_launching: F) + where + F: 'static + FnOnce(&mut App), + { + let this = self.0.clone(); + let platform = self.0.borrow().platform.clone(); + platform.run(Box::new(move || { + let cx = &mut *this.borrow_mut(); + on_finish_launching(cx); + })); + } + + /// Register a handler to be invoked when the platform instructs the application + /// to open one or more URLs. + pub fn on_open_urls(&self, mut callback: F) -> &Self + where + F: 'static + FnMut(Vec), + { + self.0.borrow().platform.on_open_urls(Box::new(callback)); + self + } + + /// Invokes a handler when an already-running application is launched. + /// On macOS, this can occur when the application icon is double-clicked or the app is launched via the dock. + pub fn on_reopen(&self, mut callback: F) -> &Self + where + F: 'static + FnMut(&mut App), + { + let this = Rc::downgrade(&self.0); + self.0.borrow_mut().platform.on_reopen(Box::new(move || { + if let Some(app) = this.upgrade() { + callback(&mut app.borrow_mut()); + } + })); + self + } + + /// Returns a handle to the [`BackgroundExecutor`] associated with this app, which can be used to spawn futures in the background. + pub fn background_executor(&self) -> BackgroundExecutor { + self.0.borrow().background_executor.clone() + } + + /// Returns a handle to the [`ForegroundExecutor`] associated with this app, which can be used to spawn futures in the foreground. + pub fn foreground_executor(&self) -> ForegroundExecutor { + self.0.borrow().foreground_executor.clone() + } + + /// Returns a reference to the [`TextSystem`] associated with this app. + pub fn text_system(&self) -> Arc { + self.0.borrow().text_system.clone() + } + + /// Returns the file URL of the executable with the specified name in the application bundle + pub fn path_for_auxiliary_executable(&self, name: &str) -> Result { + self.0.borrow().path_for_auxiliary_executable(name) + } +} + +type Handler = Box bool + 'static>; +type Listener = Box bool + 'static>; +pub(crate) type KeystrokeObserver = + Box bool + 'static>; +type QuitHandler = Box LocalBoxFuture<'static, ()> + 'static>; +type WindowClosedHandler = Box; +type ReleaseListener = Box; +type NewEntityListener = Box, &mut App) + 'static>; + +#[doc(hidden)] +#[derive(Clone, PartialEq, Eq)] +pub struct SystemWindowTab { + pub id: WindowId, + pub title: SharedString, + pub handle: AnyWindowHandle, + pub last_active_at: Instant, +} + +impl SystemWindowTab { + /// Create a new instance of the window tab. + pub fn new(title: SharedString, handle: AnyWindowHandle) -> Self { + Self { + id: handle.id, + title, + handle, + last_active_at: Instant::now(), + } + } +} + +/// A controller for managing window tabs. +#[derive(Default)] +pub struct SystemWindowTabController { + visible: Option, + tab_groups: FxHashMap>, +} + +impl Global for SystemWindowTabController {} + +impl SystemWindowTabController { + /// Create a new instance of the window tab controller. + pub fn new() -> Self { + Self { + visible: None, + tab_groups: FxHashMap::default(), + } + } + + /// Initialize the global window tab controller. + pub fn init(cx: &mut App) { + cx.set_global(SystemWindowTabController::new()); + } + + /// Get all tab groups. + pub fn tab_groups(&self) -> &FxHashMap> { + &self.tab_groups + } + + /// Get the next tab group window handle. + pub fn get_next_tab_group_window(cx: &mut App, id: WindowId) -> Option<&AnyWindowHandle> { + let controller = cx.global::(); + let current_group = controller + .tab_groups + .iter() + .find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group)); + + let current_group = current_group?; + let mut group_ids: Vec<_> = controller.tab_groups.keys().collect(); + let idx = group_ids.iter().position(|g| *g == current_group)?; + let next_idx = (idx + 1) % group_ids.len(); + + controller + .tab_groups + .get(group_ids[next_idx]) + .and_then(|tabs| { + tabs.iter() + .max_by_key(|tab| tab.last_active_at) + .or_else(|| tabs.first()) + .map(|tab| &tab.handle) + }) + } + + /// Get the previous tab group window handle. + pub fn get_prev_tab_group_window(cx: &mut App, id: WindowId) -> Option<&AnyWindowHandle> { + let controller = cx.global::(); + let current_group = controller + .tab_groups + .iter() + .find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group)); + + let current_group = current_group?; + let mut group_ids: Vec<_> = controller.tab_groups.keys().collect(); + let idx = group_ids.iter().position(|g| *g == current_group)?; + let prev_idx = if idx == 0 { + group_ids.len() - 1 + } else { + idx - 1 + }; + + controller + .tab_groups + .get(group_ids[prev_idx]) + .and_then(|tabs| { + tabs.iter() + .max_by_key(|tab| tab.last_active_at) + .or_else(|| tabs.first()) + .map(|tab| &tab.handle) + }) + } + + /// Get all tabs in the same window. + pub fn tabs(&self, id: WindowId) -> Option<&Vec> { + let tab_group = self + .tab_groups + .iter() + .find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| *group))?; + + self.tab_groups.get(&tab_group) + } + + /// Initialize the visibility of the system window tab controller. + pub fn init_visible(cx: &mut App, visible: bool) { + let mut controller = cx.global_mut::(); + if controller.visible.is_none() { + controller.visible = Some(visible); + } + } + + /// Get the visibility of the system window tab controller. + pub fn is_visible(&self) -> bool { + self.visible.unwrap_or(false) + } + + /// Set the visibility of the system window tab controller. + pub fn set_visible(cx: &mut App, visible: bool) { + let mut controller = cx.global_mut::(); + controller.visible = Some(visible); + } + + /// Update the last active of a window. + pub fn update_last_active(cx: &mut App, id: WindowId) { + let mut controller = cx.global_mut::(); + for windows in controller.tab_groups.values_mut() { + for tab in windows.iter_mut() { + if tab.id == id { + tab.last_active_at = Instant::now(); + } + } + } + } + + /// Update the position of a tab within its group. + pub fn update_tab_position(cx: &mut App, id: WindowId, ix: usize) { + let mut controller = cx.global_mut::(); + for (_, windows) in controller.tab_groups.iter_mut() { + if let Some(current_pos) = windows.iter().position(|tab| tab.id == id) { + if ix < windows.len() && current_pos != ix { + let window_tab = windows.remove(current_pos); + windows.insert(ix, window_tab); + } + break; + } + } + } + + /// Update the title of a tab. + pub fn update_tab_title(cx: &mut App, id: WindowId, title: SharedString) { + let controller = cx.global::(); + let tab = controller + .tab_groups + .values() + .flat_map(|windows| windows.iter()) + .find(|tab| tab.id == id); + + if tab.map_or(true, |t| t.title == title) { + return; + } + + let mut controller = cx.global_mut::(); + for windows in controller.tab_groups.values_mut() { + for tab in windows.iter_mut() { + if tab.id == id { + tab.title = title; + return; + } + } + } + } + + /// Insert a tab into a tab group. + pub fn add_tab(cx: &mut App, id: WindowId, tabs: Vec) { + let mut controller = cx.global_mut::(); + let Some(tab) = tabs.clone().into_iter().find(|tab| tab.id == id) else { + return; + }; + + let mut expected_tab_ids: Vec<_> = tabs + .iter() + .filter(|tab| tab.id != id) + .map(|tab| tab.id) + .sorted() + .collect(); + + let mut tab_group_id = None; + for (group_id, group_tabs) in &controller.tab_groups { + let tab_ids: Vec<_> = group_tabs.iter().map(|tab| tab.id).sorted().collect(); + if tab_ids == expected_tab_ids { + tab_group_id = Some(*group_id); + break; + } + } + + if let Some(tab_group_id) = tab_group_id { + if let Some(tabs) = controller.tab_groups.get_mut(&tab_group_id) { + tabs.push(tab); + } + } else { + let new_group_id = controller.tab_groups.len(); + controller.tab_groups.insert(new_group_id, tabs); + } + } + + /// Remove a tab from a tab group. + pub fn remove_tab(cx: &mut App, id: WindowId) -> Option { + let mut controller = cx.global_mut::(); + let mut removed_tab = None; + + controller.tab_groups.retain(|_, tabs| { + if let Some(pos) = tabs.iter().position(|tab| tab.id == id) { + removed_tab = Some(tabs.remove(pos)); + } + !tabs.is_empty() + }); + + removed_tab + } + + /// Move a tab to a new tab group. + pub fn move_tab_to_new_window(cx: &mut App, id: WindowId) { + let mut removed_tab = Self::remove_tab(cx, id); + let mut controller = cx.global_mut::(); + + if let Some(tab) = removed_tab { + let new_group_id = controller.tab_groups.keys().max().map_or(0, |k| k + 1); + controller.tab_groups.insert(new_group_id, vec![tab]); + } + } + + /// Merge all tab groups into a single group. + pub fn merge_all_windows(cx: &mut App, id: WindowId) { + let mut controller = cx.global_mut::(); + let Some(initial_tabs) = controller.tabs(id) else { + return; + }; + + let mut all_tabs = initial_tabs.clone(); + for tabs in controller.tab_groups.values() { + all_tabs.extend( + tabs.iter() + .filter(|tab| !initial_tabs.contains(tab)) + .cloned(), + ); + } + + controller.tab_groups.clear(); + controller.tab_groups.insert(0, all_tabs); + } + + /// Selects the next tab in the tab group in the trailing direction. + pub fn select_next_tab(cx: &mut App, id: WindowId) { + let mut controller = cx.global_mut::(); + let Some(tabs) = controller.tabs(id) else { + return; + }; + + let current_index = tabs.iter().position(|tab| tab.id == id).unwrap(); + let next_index = (current_index + 1) % tabs.len(); + + let _ = &tabs[next_index].handle.update(cx, |_, window, _| { + window.activate_window(); + }); + } + + /// Selects the previous tab in the tab group in the leading direction. + pub fn select_previous_tab(cx: &mut App, id: WindowId) { + let mut controller = cx.global_mut::(); + let Some(tabs) = controller.tabs(id) else { + return; + }; + + let current_index = tabs.iter().position(|tab| tab.id == id).unwrap(); + let previous_index = if current_index == 0 { + tabs.len() - 1 + } else { + current_index - 1 + }; + + let _ = &tabs[previous_index].handle.update(cx, |_, window, _| { + window.activate_window(); + }); + } +} + +/// Contains the state of the full application, and passed as a reference to a variety of callbacks. +/// Other [Context] derefs to this type. +/// You need a reference to an `App` to access the state of a [Entity]. +pub struct App { + pub(crate) this: Weak, + pub(crate) platform: Rc, + text_system: Arc, + flushing_effects: bool, + pending_updates: usize, + pub(crate) actions: Rc, + pub(crate) active_drag: Option, + pub(crate) background_executor: BackgroundExecutor, + pub(crate) foreground_executor: ForegroundExecutor, + pub(crate) loading_assets: FxHashMap<(TypeId, u64), Box>, + asset_source: Arc, + pub(crate) svg_renderer: SvgRenderer, + http_client: Arc, + pub(crate) globals_by_type: FxHashMap>, + pub(crate) entities: EntityMap, + pub(crate) window_update_stack: Vec, + pub(crate) new_entity_observers: SubscriberSet, + pub(crate) windows: SlotMap>>, + pub(crate) window_handles: FxHashMap, + pub(crate) focus_handles: Arc, + pub(crate) keymap: Rc>, + pub(crate) keyboard_layout: Box, + pub(crate) keyboard_mapper: Rc, + pub(crate) global_action_listeners: + FxHashMap>>, + pending_effects: VecDeque, + pub(crate) pending_notifications: FxHashSet, + pub(crate) pending_global_notifications: FxHashSet, + pub(crate) observers: SubscriberSet, + // TypeId is the type of the event that the listener callback expects + pub(crate) event_listeners: SubscriberSet, + pub(crate) keystroke_observers: SubscriberSet<(), KeystrokeObserver>, + pub(crate) keystroke_interceptors: SubscriberSet<(), KeystrokeObserver>, + pub(crate) keyboard_layout_observers: SubscriberSet<(), Handler>, + pub(crate) release_listeners: SubscriberSet, + pub(crate) global_observers: SubscriberSet, + pub(crate) quit_observers: SubscriberSet<(), QuitHandler>, + pub(crate) restart_observers: SubscriberSet<(), Handler>, + pub(crate) restart_path: Option, + pub(crate) window_closed_observers: SubscriberSet<(), WindowClosedHandler>, + pub(crate) layout_id_buffer: Vec, // We recycle this memory across layout requests. + pub(crate) propagate_event: bool, + pub(crate) prompt_builder: Option, + pub(crate) window_invalidators_by_entity: + FxHashMap>, + pub(crate) tracked_entities: FxHashMap>, + #[cfg(any(feature = "inspector", debug_assertions))] + pub(crate) inspector_renderer: Option, + #[cfg(any(feature = "inspector", debug_assertions))] + pub(crate) inspector_element_registry: InspectorElementRegistry, + #[cfg(any(test, feature = "test-support", debug_assertions))] + pub(crate) name: Option<&'static str>, + quitting: bool, +} + +impl App { + #[allow(clippy::new_ret_no_self)] + pub(crate) fn new_app( + platform: Rc, + asset_source: Arc, + http_client: Arc, + ) -> Rc { + let executor = platform.background_executor(); + let foreground_executor = platform.foreground_executor(); + assert!( + executor.is_main_thread(), + "must construct App on main thread" + ); + + let text_system = Arc::new(TextSystem::new(platform.text_system())); + let entities = EntityMap::new(); + let keyboard_layout = platform.keyboard_layout(); + let keyboard_mapper = platform.keyboard_mapper(); + + let app = Rc::new_cyclic(|this| AppCell { + app: RefCell::new(App { + this: this.clone(), + platform: platform.clone(), + text_system, + actions: Rc::new(ActionRegistry::default()), + flushing_effects: false, + pending_updates: 0, + active_drag: None, + background_executor: executor, + foreground_executor, + svg_renderer: SvgRenderer::new(asset_source.clone()), + loading_assets: Default::default(), + asset_source, + http_client, + globals_by_type: FxHashMap::default(), + entities, + new_entity_observers: SubscriberSet::new(), + windows: SlotMap::with_key(), + window_update_stack: Vec::new(), + window_handles: FxHashMap::default(), + focus_handles: Arc::new(RwLock::new(SlotMap::with_key())), + keymap: Rc::new(RefCell::new(Keymap::default())), + keyboard_layout, + keyboard_mapper, + global_action_listeners: FxHashMap::default(), + pending_effects: VecDeque::new(), + pending_notifications: FxHashSet::default(), + pending_global_notifications: FxHashSet::default(), + observers: SubscriberSet::new(), + tracked_entities: FxHashMap::default(), + window_invalidators_by_entity: FxHashMap::default(), + event_listeners: SubscriberSet::new(), + release_listeners: SubscriberSet::new(), + keystroke_observers: SubscriberSet::new(), + keystroke_interceptors: SubscriberSet::new(), + keyboard_layout_observers: SubscriberSet::new(), + global_observers: SubscriberSet::new(), + quit_observers: SubscriberSet::new(), + restart_observers: SubscriberSet::new(), + restart_path: None, + window_closed_observers: SubscriberSet::new(), + layout_id_buffer: Default::default(), + propagate_event: true, + prompt_builder: Some(PromptBuilder::Default), + #[cfg(any(feature = "inspector", debug_assertions))] + inspector_renderer: None, + #[cfg(any(feature = "inspector", debug_assertions))] + inspector_element_registry: InspectorElementRegistry::default(), + quitting: false, + + #[cfg(any(test, feature = "test-support", debug_assertions))] + name: None, + }), + }); + + init_app_menus(platform.as_ref(), &app.borrow()); + SystemWindowTabController::init(&mut app.borrow_mut()); + + platform.on_keyboard_layout_change(Box::new({ + let app = Rc::downgrade(&app); + move || { + if let Some(app) = app.upgrade() { + let cx = &mut app.borrow_mut(); + cx.keyboard_layout = cx.platform.keyboard_layout(); + cx.keyboard_mapper = cx.platform.keyboard_mapper(); + cx.keyboard_layout_observers + .clone() + .retain(&(), move |callback| (callback)(cx)); + } + } + })); + + platform.on_quit(Box::new({ + let cx = app.clone(); + move || { + cx.borrow_mut().shutdown(); + } + })); + + app + } + + /// Quit the application gracefully. Handlers registered with [`Context::on_app_quit`] + /// will be given 100ms to complete before exiting. + pub fn shutdown(&mut self) { + let mut futures = Vec::new(); + + for observer in self.quit_observers.remove(&()) { + futures.push(observer(self)); + } + + self.windows.clear(); + self.window_handles.clear(); + self.flush_effects(); + self.quitting = true; + + let futures = futures::future::join_all(futures); + if self + .background_executor + .block_with_timeout(SHUTDOWN_TIMEOUT, futures) + .is_err() + { + log::error!("timed out waiting on app_will_quit"); + } + + self.quitting = false; + } + + /// Get the id of the current keyboard layout + pub fn keyboard_layout(&self) -> &dyn PlatformKeyboardLayout { + self.keyboard_layout.as_ref() + } + + /// Get the current keyboard mapper. + pub fn keyboard_mapper(&self) -> &Rc { + &self.keyboard_mapper + } + + /// Invokes a handler when the current keyboard layout changes + pub fn on_keyboard_layout_change(&self, mut callback: F) -> Subscription + where + F: 'static + FnMut(&mut App), + { + let (subscription, activate) = self.keyboard_layout_observers.insert( + (), + Box::new(move |cx| { + callback(cx); + true + }), + ); + activate(); + subscription + } + + /// Gracefully quit the application via the platform's standard routine. + pub fn quit(&self) { + self.platform.quit(); + } + + /// Schedules all windows in the application to be redrawn. This can be called + /// multiple times in an update cycle and still result in a single redraw. + pub fn refresh_windows(&mut self) { + self.pending_effects.push_back(Effect::RefreshWindows); + } + + pub(crate) fn update(&mut self, update: impl FnOnce(&mut Self) -> R) -> R { + self.start_update(); + let result = update(self); + self.finish_update(); + result + } + + pub(crate) fn start_update(&mut self) { + self.pending_updates += 1; + } + + pub(crate) fn finish_update(&mut self) { + if !self.flushing_effects && self.pending_updates == 1 { + self.flushing_effects = true; + self.flush_effects(); + self.flushing_effects = false; + } + self.pending_updates -= 1; + } + + /// Arrange a callback to be invoked when the given entity calls `notify` on its respective context. + pub fn observe( + &mut self, + entity: &Entity, + mut on_notify: impl FnMut(Entity, &mut App) + 'static, + ) -> Subscription + where + W: 'static, + { + self.observe_internal(entity, move |e, cx| { + on_notify(e, cx); + true + }) + } + + pub(crate) fn detect_accessed_entities( + &mut self, + callback: impl FnOnce(&mut App) -> R, + ) -> (R, FxHashSet) { + let accessed_entities_start = self.entities.accessed_entities.borrow().clone(); + let result = callback(self); + let accessed_entities_end = self.entities.accessed_entities.borrow().clone(); + let entities_accessed_in_callback = accessed_entities_end + .difference(&accessed_entities_start) + .copied() + .collect::>(); + (result, entities_accessed_in_callback) + } + + pub(crate) fn record_entities_accessed( + &mut self, + window_handle: AnyWindowHandle, + invalidator: WindowInvalidator, + entities: &FxHashSet, + ) { + let mut tracked_entities = + std::mem::take(self.tracked_entities.entry(window_handle.id).or_default()); + for entity in tracked_entities.iter() { + self.window_invalidators_by_entity + .entry(*entity) + .and_modify(|windows| { + windows.remove(&window_handle.id); + }); + } + for entity in entities.iter() { + self.window_invalidators_by_entity + .entry(*entity) + .or_default() + .insert(window_handle.id, invalidator.clone()); + } + tracked_entities.clear(); + tracked_entities.extend(entities.iter().copied()); + self.tracked_entities + .insert(window_handle.id, tracked_entities); + } + + pub(crate) fn new_observer(&mut self, key: EntityId, value: Handler) -> Subscription { + let (subscription, activate) = self.observers.insert(key, value); + self.defer(move |_| activate()); + subscription + } + + pub(crate) fn observe_internal( + &mut self, + entity: &Entity, + mut on_notify: impl FnMut(Entity, &mut App) -> bool + 'static, + ) -> Subscription + where + W: 'static, + { + let entity_id = entity.entity_id(); + let handle = entity.downgrade(); + self.new_observer( + entity_id, + Box::new(move |cx| { + if let Some(entity) = handle.upgrade() { + on_notify(entity, cx) + } else { + false + } + }), + ) + } + + /// Arrange for the given callback to be invoked whenever the given entity emits an event of a given type. + /// The callback is provided a handle to the emitting entity and a reference to the emitted event. + pub fn subscribe( + &mut self, + entity: &Entity, + mut on_event: impl FnMut(Entity, &Event, &mut App) + 'static, + ) -> Subscription + where + T: 'static + EventEmitter, + Event: 'static, + { + self.subscribe_internal(entity, move |entity, event, cx| { + on_event(entity, event, cx); + true + }) + } + + pub(crate) fn new_subscription( + &mut self, + key: EntityId, + value: (TypeId, Listener), + ) -> Subscription { + let (subscription, activate) = self.event_listeners.insert(key, value); + self.defer(move |_| activate()); + subscription + } + pub(crate) fn subscribe_internal( + &mut self, + entity: &Entity, + mut on_event: impl FnMut(Entity, &Evt, &mut App) -> bool + 'static, + ) -> Subscription + where + T: 'static + EventEmitter, + Evt: 'static, + { + let entity_id = entity.entity_id(); + let handle = entity.downgrade(); + self.new_subscription( + entity_id, + ( + TypeId::of::(), + Box::new(move |event, cx| { + let event: &Evt = event.downcast_ref().expect("invalid event type"); + if let Some(entity) = handle.upgrade() { + on_event(entity, event, cx) + } else { + false + } + }), + ), + ) + } + + /// Returns handles to all open windows in the application. + /// Each handle could be downcast to a handle typed for the root view of that window. + /// To find all windows of a given type, you could filter on + pub fn windows(&self) -> Vec { + self.windows + .keys() + .flat_map(|window_id| self.window_handles.get(&window_id).copied()) + .collect() + } + + /// Returns the window handles ordered by their appearance on screen, front to back. + /// + /// The first window in the returned list is the active/topmost window of the application. + /// + /// This method returns None if the platform doesn't implement the method yet. + pub fn window_stack(&self) -> Option> { + self.platform.window_stack() + } + + /// Returns a handle to the window that is currently focused at the platform level, if one exists. + pub fn active_window(&self) -> Option { + self.platform.active_window() + } + + /// Opens a new window with the given option and the root view returned by the given function. + /// The function is invoked with a `Window`, which can be used to interact with window-specific + /// functionality. + pub fn open_window( + &mut self, + options: crate::WindowOptions, + build_root_view: impl FnOnce(&mut Window, &mut App) -> Entity, + ) -> anyhow::Result> { + self.update(|cx| { + let id = cx.windows.insert(None); + let handle = WindowHandle::new(id); + match Window::new(handle.into(), options, cx) { + Ok(mut window) => { + cx.window_update_stack.push(id); + let root_view = build_root_view(&mut window, cx); + cx.window_update_stack.pop(); + window.root.replace(root_view.into()); + window.defer(cx, |window: &mut Window, cx| window.appearance_changed(cx)); + + // allow a window to draw at least once before returning + // this didn't cause any issues on non windows platforms as it seems we always won the race to on_request_frame + // on windows we quite frequently lose the race and return a window that has never rendered, which leads to a crash + // where DispatchTree::root_node_id asserts on empty nodes + let clear = window.draw(cx); + clear.clear(); + + cx.window_handles.insert(id, window.handle); + cx.windows.get_mut(id).unwrap().replace(Box::new(window)); + Ok(handle) + } + Err(e) => { + cx.windows.remove(id); + Err(e) + } + } + }) + } + + /// Instructs the platform to activate the application by bringing it to the foreground. + pub fn activate(&self, ignoring_other_apps: bool) { + self.platform.activate(ignoring_other_apps); + } + + /// Hide the application at the platform level. + pub fn hide(&self) { + self.platform.hide(); + } + + /// Hide other applications at the platform level. + pub fn hide_other_apps(&self) { + self.platform.hide_other_apps(); + } + + /// Unhide other applications at the platform level. + pub fn unhide_other_apps(&self) { + self.platform.unhide_other_apps(); + } + + /// Returns the list of currently active displays. + pub fn displays(&self) -> Vec> { + self.platform.displays() + } + + /// Returns the primary display that will be used for new windows. + pub fn primary_display(&self) -> Option> { + self.platform.primary_display() + } + + /// Returns whether `screen_capture_sources` may work. + pub fn is_screen_capture_supported(&self) -> bool { + self.platform.is_screen_capture_supported() + } + + /// Returns a list of available screen capture sources. + pub fn screen_capture_sources( + &self, + ) -> oneshot::Receiver>>> { + self.platform.screen_capture_sources() + } + + /// Returns the display with the given ID, if one exists. + pub fn find_display(&self, id: DisplayId) -> Option> { + self.displays() + .iter() + .find(|display| display.id() == id) + .cloned() + } + + /// Returns the appearance of the application's windows. + pub fn window_appearance(&self) -> WindowAppearance { + self.platform.window_appearance() + } + + /// Writes data to the primary selection buffer. + /// Only available on Linux. + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + pub fn write_to_primary(&self, item: ClipboardItem) { + self.platform.write_to_primary(item) + } + + /// Writes data to the platform clipboard. + pub fn write_to_clipboard(&self, item: ClipboardItem) { + self.platform.write_to_clipboard(item) + } + + /// Reads data from the primary selection buffer. + /// Only available on Linux. + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + pub fn read_from_primary(&self) -> Option { + self.platform.read_from_primary() + } + + /// Reads data from the platform clipboard. + pub fn read_from_clipboard(&self) -> Option { + self.platform.read_from_clipboard() + } + + /// Writes credentials to the platform keychain. + pub fn write_credentials( + &self, + url: &str, + username: &str, + password: &[u8], + ) -> Task> { + self.platform.write_credentials(url, username, password) + } + + /// Reads credentials from the platform keychain. + pub fn read_credentials(&self, url: &str) -> Task)>>> { + self.platform.read_credentials(url) + } + + /// Deletes credentials from the platform keychain. + pub fn delete_credentials(&self, url: &str) -> Task> { + self.platform.delete_credentials(url) + } + + /// Directs the platform's default browser to open the given URL. + pub fn open_url(&self, url: &str) { + self.platform.open_url(url); + } + + /// Registers the given URL scheme (e.g. `zed` for `zed://` urls) to be + /// opened by the current app. + /// + /// On some platforms (e.g. macOS) you may be able to register URL schemes + /// as part of app distribution, but this method exists to let you register + /// schemes at runtime. + pub fn register_url_scheme(&self, scheme: &str) -> Task> { + self.platform.register_url_scheme(scheme) + } + + /// Returns the full pathname of the current app bundle. + /// + /// Returns an error if the app is not being run from a bundle. + pub fn app_path(&self) -> Result { + self.platform.app_path() + } + + /// On Linux, returns the name of the compositor in use. + /// + /// Returns an empty string on other platforms. + pub fn compositor_name(&self) -> &'static str { + self.platform.compositor_name() + } + + /// Returns the file URL of the executable with the specified name in the application bundle + pub fn path_for_auxiliary_executable(&self, name: &str) -> Result { + self.platform.path_for_auxiliary_executable(name) + } + + /// Displays a platform modal for selecting paths. + /// + /// When one or more paths are selected, they'll be relayed asynchronously via the returned oneshot channel. + /// If cancelled, a `None` will be relayed instead. + /// May return an error on Linux if the file picker couldn't be opened. + pub fn prompt_for_paths( + &self, + options: PathPromptOptions, + ) -> oneshot::Receiver>>> { + self.platform.prompt_for_paths(options) + } + + /// Displays a platform modal for selecting a new path where a file can be saved. + /// + /// The provided directory will be used to set the initial location. + /// When a path is selected, it is relayed asynchronously via the returned oneshot channel. + /// If cancelled, a `None` will be relayed instead. + /// May return an error on Linux if the file picker couldn't be opened. + pub fn prompt_for_new_path( + &self, + directory: &Path, + suggested_name: Option<&str>, + ) -> oneshot::Receiver>> { + self.platform.prompt_for_new_path(directory, suggested_name) + } + + /// Reveals the specified path at the platform level, such as in Finder on macOS. + pub fn reveal_path(&self, path: &Path) { + self.platform.reveal_path(path) + } + + /// Opens the specified path with the system's default application. + pub fn open_with_system(&self, path: &Path) { + self.platform.open_with_system(path) + } + + /// Returns whether the user has configured scrollbars to auto-hide at the platform level. + pub fn should_auto_hide_scrollbars(&self) -> bool { + self.platform.should_auto_hide_scrollbars() + } + + /// Restarts the application. + pub fn restart(&mut self) { + self.restart_observers + .clone() + .retain(&(), |observer| observer(self)); + self.platform.restart(self.restart_path.take()) + } + + /// Sets the path to use when restarting the application. + pub fn set_restart_path(&mut self, path: PathBuf) { + self.restart_path = Some(path); + } + + /// Returns the HTTP client for the application. + pub fn http_client(&self) -> Arc { + self.http_client.clone() + } + + /// Sets the HTTP client for the application. + pub fn set_http_client(&mut self, new_client: Arc) { + self.http_client = new_client; + } + + /// Returns the SVG renderer used by the application. + pub fn svg_renderer(&self) -> SvgRenderer { + self.svg_renderer.clone() + } + + pub(crate) fn push_effect(&mut self, effect: Effect) { + match &effect { + Effect::Notify { emitter } => { + if !self.pending_notifications.insert(*emitter) { + return; + } + } + Effect::NotifyGlobalObservers { global_type } => { + if !self.pending_global_notifications.insert(*global_type) { + return; + } + } + _ => {} + }; + + self.pending_effects.push_back(effect); + } + + /// Called at the end of [`App::update`] to complete any side effects + /// such as notifying observers, emitting events, etc. Effects can themselves + /// cause effects, so we continue looping until all effects are processed. + fn flush_effects(&mut self) { + loop { + self.release_dropped_entities(); + self.release_dropped_focus_handles(); + if let Some(effect) = self.pending_effects.pop_front() { + match effect { + Effect::Notify { emitter } => { + self.apply_notify_effect(emitter); + } + + Effect::Emit { + emitter, + event_type, + event, + } => self.apply_emit_effect(emitter, event_type, event), + + Effect::RefreshWindows => { + self.apply_refresh_effect(); + } + + Effect::NotifyGlobalObservers { global_type } => { + self.apply_notify_global_observers_effect(global_type); + } + + Effect::Defer { callback } => { + self.apply_defer_effect(callback); + } + Effect::EntityCreated { + entity, + tid, + window, + } => { + self.apply_entity_created_effect(entity, tid, window); + } + } + } else { + #[cfg(any(test, feature = "test-support"))] + for window in self + .windows + .values() + .filter_map(|window| { + let window = window.as_deref()?; + window.invalidator.is_dirty().then_some(window.handle) + }) + .collect::>() + { + self.update_window(window, |_, window, cx| window.draw(cx).clear()) + .unwrap(); + } + + if self.pending_effects.is_empty() { + break; + } + } + } + } + + /// Repeatedly called during `flush_effects` to release any entities whose + /// reference count has become zero. We invoke any release observers before dropping + /// each entity. + fn release_dropped_entities(&mut self) { + loop { + let dropped = self.entities.take_dropped(); + if dropped.is_empty() { + break; + } + + for (entity_id, mut entity) in dropped { + self.observers.remove(&entity_id); + self.event_listeners.remove(&entity_id); + for release_callback in self.release_listeners.remove(&entity_id) { + release_callback(entity.as_mut(), self); + } + } + } + } + + /// Repeatedly called during `flush_effects` to handle a focused handle being dropped. + fn release_dropped_focus_handles(&mut self) { + self.focus_handles + .clone() + .write() + .retain(|handle_id, focus| { + if focus.ref_count.load(SeqCst) == 0 { + for window_handle in self.windows() { + window_handle + .update(self, |_, window, _| { + if window.focus == Some(handle_id) { + window.blur(); + } + }) + .unwrap(); + } + false + } else { + true + } + }); + } + + fn apply_notify_effect(&mut self, emitter: EntityId) { + self.pending_notifications.remove(&emitter); + + self.observers + .clone() + .retain(&emitter, |handler| handler(self)); + } + + fn apply_emit_effect(&mut self, emitter: EntityId, event_type: TypeId, event: Box) { + self.event_listeners + .clone() + .retain(&emitter, |(stored_type, handler)| { + if *stored_type == event_type { + handler(event.as_ref(), self) + } else { + true + } + }); + } + + fn apply_refresh_effect(&mut self) { + for window in self.windows.values_mut() { + if let Some(window) = window.as_deref_mut() { + window.refreshing = true; + window.invalidator.set_dirty(true); + } + } + } + + fn apply_notify_global_observers_effect(&mut self, type_id: TypeId) { + self.pending_global_notifications.remove(&type_id); + self.global_observers + .clone() + .retain(&type_id, |observer| observer(self)); + } + + fn apply_defer_effect(&mut self, callback: Box) { + callback(self); + } + + fn apply_entity_created_effect( + &mut self, + entity: AnyEntity, + tid: TypeId, + window: Option, + ) { + self.new_entity_observers.clone().retain(&tid, |observer| { + if let Some(id) = window { + self.update_window_id(id, { + let entity = entity.clone(); + |_, window, cx| (observer)(entity, &mut Some(window), cx) + }) + .expect("All windows should be off the stack when flushing effects"); + } else { + (observer)(entity.clone(), &mut None, self) + } + true + }); + } + + fn update_window_id(&mut self, id: WindowId, update: F) -> Result + where + F: FnOnce(AnyView, &mut Window, &mut App) -> T, + { + self.update(|cx| { + let mut window = cx.windows.get_mut(id)?.take()?; + + let root_view = window.root.clone().unwrap(); + + cx.window_update_stack.push(window.handle.id); + let result = update(root_view, &mut window, cx); + cx.window_update_stack.pop(); + + if window.removed { + cx.window_handles.remove(&id); + cx.windows.remove(id); + + cx.window_closed_observers.clone().retain(&(), |callback| { + callback(cx); + true + }); + } else { + cx.windows.get_mut(id)?.replace(window); + } + + Some(result) + }) + .context("window not found") + } + + /// Creates an `AsyncApp`, which can be cloned and has a static lifetime + /// so it can be held across `await` points. + pub fn to_async(&self) -> AsyncApp { + AsyncApp { + app: self.this.clone(), + background_executor: self.background_executor.clone(), + foreground_executor: self.foreground_executor.clone(), + } + } + + /// Obtains a reference to the executor, which can be used to spawn futures. + pub fn background_executor(&self) -> &BackgroundExecutor { + &self.background_executor + } + + /// Obtains a reference to the executor, which can be used to spawn futures. + pub fn foreground_executor(&self) -> &ForegroundExecutor { + if self.quitting { + panic!("Can't spawn on main thread after on_app_quit") + }; + &self.foreground_executor + } + + /// Spawns the future returned by the given function on the main thread. The closure will be invoked + /// with [AsyncApp], which allows the application state to be accessed across await points. + #[track_caller] + pub fn spawn(&self, f: AsyncFn) -> Task + where + AsyncFn: AsyncFnOnce(&mut AsyncApp) -> R + 'static, + R: 'static, + { + if self.quitting { + debug_panic!("Can't spawn on main thread after on_app_quit") + }; + + let mut cx = self.to_async(); + + self.foreground_executor + .spawn(async move { f(&mut cx).await }) + } + + /// Schedules the given function to be run at the end of the current effect cycle, allowing entities + /// that are currently on the stack to be returned to the app. + pub fn defer(&mut self, f: impl FnOnce(&mut App) + 'static) { + self.push_effect(Effect::Defer { + callback: Box::new(f), + }); + } + + /// Accessor for the application's asset source, which is provided when constructing the `App`. + pub fn asset_source(&self) -> &Arc { + &self.asset_source + } + + /// Accessor for the text system. + pub fn text_system(&self) -> &Arc { + &self.text_system + } + + /// Check whether a global of the given type has been assigned. + pub fn has_global(&self) -> bool { + self.globals_by_type.contains_key(&TypeId::of::()) + } + + /// Access the global of the given type. Panics if a global for that type has not been assigned. + #[track_caller] + pub fn global(&self) -> &G { + self.globals_by_type + .get(&TypeId::of::()) + .map(|any_state| any_state.downcast_ref::().unwrap()) + .with_context(|| format!("no state of type {} exists", type_name::())) + .unwrap() + } + + /// Access the global of the given type if a value has been assigned. + pub fn try_global(&self) -> Option<&G> { + self.globals_by_type + .get(&TypeId::of::()) + .map(|any_state| any_state.downcast_ref::().unwrap()) + } + + /// Access the global of the given type mutably. Panics if a global for that type has not been assigned. + #[track_caller] + pub fn global_mut(&mut self) -> &mut G { + let global_type = TypeId::of::(); + self.push_effect(Effect::NotifyGlobalObservers { global_type }); + self.globals_by_type + .get_mut(&global_type) + .and_then(|any_state| any_state.downcast_mut::()) + .with_context(|| format!("no state of type {} exists", type_name::())) + .unwrap() + } + + /// Access the global of the given type mutably. A default value is assigned if a global of this type has not + /// yet been assigned. + pub fn default_global(&mut self) -> &mut G { + let global_type = TypeId::of::(); + self.push_effect(Effect::NotifyGlobalObservers { global_type }); + self.globals_by_type + .entry(global_type) + .or_insert_with(|| Box::::default()) + .downcast_mut::() + .unwrap() + } + + /// Sets the value of the global of the given type. + pub fn set_global(&mut self, global: G) { + let global_type = TypeId::of::(); + self.push_effect(Effect::NotifyGlobalObservers { global_type }); + self.globals_by_type.insert(global_type, Box::new(global)); + } + + /// Clear all stored globals. Does not notify global observers. + #[cfg(any(test, feature = "test-support"))] + pub fn clear_globals(&mut self) { + self.globals_by_type.drain(); + } + + /// Remove the global of the given type from the app context. Does not notify global observers. + pub fn remove_global(&mut self) -> G { + let global_type = TypeId::of::(); + self.push_effect(Effect::NotifyGlobalObservers { global_type }); + *self + .globals_by_type + .remove(&global_type) + .unwrap_or_else(|| panic!("no global added for {}", std::any::type_name::())) + .downcast() + .unwrap() + } + + /// Register a callback to be invoked when a global of the given type is updated. + pub fn observe_global( + &mut self, + mut f: impl FnMut(&mut Self) + 'static, + ) -> Subscription { + let (subscription, activate) = self.global_observers.insert( + TypeId::of::(), + Box::new(move |cx| { + f(cx); + true + }), + ); + self.defer(move |_| activate()); + subscription + } + + /// Move the global of the given type to the stack. + #[track_caller] + pub(crate) fn lease_global(&mut self) -> GlobalLease { + GlobalLease::new( + self.globals_by_type + .remove(&TypeId::of::()) + .with_context(|| format!("no global registered of type {}", type_name::())) + .unwrap(), + ) + } + + /// Restore the global of the given type after it is moved to the stack. + pub(crate) fn end_global_lease(&mut self, lease: GlobalLease) { + let global_type = TypeId::of::(); + + self.push_effect(Effect::NotifyGlobalObservers { global_type }); + self.globals_by_type.insert(global_type, lease.global); + } + + pub(crate) fn new_entity_observer( + &self, + key: TypeId, + value: NewEntityListener, + ) -> Subscription { + let (subscription, activate) = self.new_entity_observers.insert(key, value); + activate(); + subscription + } + + /// Arrange for the given function to be invoked whenever a view of the specified type is created. + /// The function will be passed a mutable reference to the view along with an appropriate context. + pub fn observe_new( + &self, + on_new: impl 'static + Fn(&mut T, Option<&mut Window>, &mut Context), + ) -> Subscription { + self.new_entity_observer( + TypeId::of::(), + Box::new( + move |any_entity: AnyEntity, window: &mut Option<&mut Window>, cx: &mut App| { + any_entity + .downcast::() + .unwrap() + .update(cx, |entity_state, cx| { + on_new(entity_state, window.as_deref_mut(), cx) + }) + }, + ), + ) + } + + /// Observe the release of a entity. The callback is invoked after the entity + /// has no more strong references but before it has been dropped. + pub fn observe_release( + &self, + handle: &Entity, + on_release: impl FnOnce(&mut T, &mut App) + 'static, + ) -> Subscription + where + T: 'static, + { + let (subscription, activate) = self.release_listeners.insert( + handle.entity_id(), + Box::new(move |entity, cx| { + let entity = entity.downcast_mut().expect("invalid entity type"); + on_release(entity, cx) + }), + ); + activate(); + subscription + } + + /// Observe the release of a entity. The callback is invoked after the entity + /// has no more strong references but before it has been dropped. + pub fn observe_release_in( + &self, + handle: &Entity, + window: &Window, + on_release: impl FnOnce(&mut T, &mut Window, &mut App) + 'static, + ) -> Subscription + where + T: 'static, + { + let window_handle = window.handle; + self.observe_release(handle, move |entity, cx| { + let _ = window_handle.update(cx, |_, window, cx| on_release(entity, window, cx)); + }) + } + + /// Register a callback to be invoked when a keystroke is received by the application + /// in any window. Note that this fires after all other action and event mechanisms have resolved + /// and that this API will not be invoked if the event's propagation is stopped. + pub fn observe_keystrokes( + &mut self, + mut f: impl FnMut(&KeystrokeEvent, &mut Window, &mut App) + 'static, + ) -> Subscription { + fn inner( + keystroke_observers: &SubscriberSet<(), KeystrokeObserver>, + handler: KeystrokeObserver, + ) -> Subscription { + let (subscription, activate) = keystroke_observers.insert((), handler); + activate(); + subscription + } + + inner( + &self.keystroke_observers, + Box::new(move |event, window, cx| { + f(event, window, cx); + true + }), + ) + } + + /// Register a callback to be invoked when a keystroke is received by the application + /// in any window. Note that this fires _before_ all other action and event mechanisms have resolved + /// unlike [`App::observe_keystrokes`] which fires after. This means that `cx.stop_propagation` calls + /// within interceptors will prevent action dispatch + pub fn intercept_keystrokes( + &mut self, + mut f: impl FnMut(&KeystrokeEvent, &mut Window, &mut App) + 'static, + ) -> Subscription { + fn inner( + keystroke_interceptors: &SubscriberSet<(), KeystrokeObserver>, + handler: KeystrokeObserver, + ) -> Subscription { + let (subscription, activate) = keystroke_interceptors.insert((), handler); + activate(); + subscription + } + + inner( + &self.keystroke_interceptors, + Box::new(move |event, window, cx| { + f(event, window, cx); + true + }), + ) + } + + /// Register key bindings. + pub fn bind_keys(&mut self, bindings: impl IntoIterator) { + self.keymap.borrow_mut().add_bindings(bindings); + self.pending_effects.push_back(Effect::RefreshWindows); + } + + /// Clear all key bindings in the app. + pub fn clear_key_bindings(&mut self) { + self.keymap.borrow_mut().clear(); + self.pending_effects.push_back(Effect::RefreshWindows); + } + + /// Get all key bindings in the app. + pub fn key_bindings(&self) -> Rc> { + self.keymap.clone() + } + + /// Register a global handler for actions invoked via the keyboard. These handlers are run at + /// the end of the bubble phase for actions, and so will only be invoked if there are no other + /// handlers or if they called `cx.propagate()`. + pub fn on_action(&mut self, listener: impl Fn(&A, &mut Self) + 'static) { + self.global_action_listeners + .entry(TypeId::of::()) + .or_default() + .push(Rc::new(move |action, phase, cx| { + if phase == DispatchPhase::Bubble { + let action = action.downcast_ref().unwrap(); + listener(action, cx) + } + })); + } + + /// Event handlers propagate events by default. Call this method to stop dispatching to + /// event handlers with a lower z-index (mouse) or higher in the tree (keyboard). This is + /// the opposite of [`Self::propagate`]. It's also possible to cancel a call to [`Self::propagate`] by + /// calling this method before effects are flushed. + pub fn stop_propagation(&mut self) { + self.propagate_event = false; + } + + /// Action handlers stop propagation by default during the bubble phase of action dispatch + /// dispatching to action handlers higher in the element tree. This is the opposite of + /// [`Self::stop_propagation`]. It's also possible to cancel a call to [`Self::stop_propagation`] by calling + /// this method before effects are flushed. + pub fn propagate(&mut self) { + self.propagate_event = true; + } + + /// Build an action from some arbitrary data, typically a keymap entry. + pub fn build_action( + &self, + name: &str, + data: Option, + ) -> std::result::Result, ActionBuildError> { + self.actions.build_action(name, data) + } + + /// Get all action names that have been registered. Note that registration only allows for + /// actions to be built dynamically, and is unrelated to binding actions in the element tree. + pub fn all_action_names(&self) -> &[&'static str] { + self.actions.all_action_names() + } + + /// Returns key bindings that invoke the given action on the currently focused element, without + /// checking context. Bindings are returned in the order they were added. For display, the last + /// binding should take precedence. + pub fn all_bindings_for_input(&self, input: &[Keystroke]) -> Vec { + RefCell::borrow(&self.keymap).all_bindings_for_input(input) + } + + /// Get all non-internal actions that have been registered, along with their schemas. + pub fn action_schemas( + &self, + generator: &mut schemars::SchemaGenerator, + ) -> Vec<(&'static str, Option)> { + self.actions.action_schemas(generator) + } + + /// Get a map from a deprecated action name to the canonical name. + pub fn deprecated_actions_to_preferred_actions(&self) -> &HashMap<&'static str, &'static str> { + self.actions.deprecated_aliases() + } + + /// Get a map from an action name to the deprecation messages. + pub fn action_deprecation_messages(&self) -> &HashMap<&'static str, &'static str> { + self.actions.deprecation_messages() + } + + /// Get a map from an action name to the documentation. + pub fn action_documentation(&self) -> &HashMap<&'static str, &'static str> { + self.actions.documentation() + } + + /// Register a callback to be invoked when the application is about to quit. + /// It is not possible to cancel the quit event at this point. + pub fn on_app_quit( + &self, + mut on_quit: impl FnMut(&mut App) -> Fut + 'static, + ) -> Subscription + where + Fut: 'static + Future, + { + let (subscription, activate) = self.quit_observers.insert( + (), + Box::new(move |cx| { + let future = on_quit(cx); + future.boxed_local() + }), + ); + activate(); + subscription + } + + /// Register a callback to be invoked when the application is about to restart. + /// + /// These callbacks are called before any `on_app_quit` callbacks. + pub fn on_app_restart(&self, mut on_restart: impl 'static + FnMut(&mut App)) -> Subscription { + let (subscription, activate) = self.restart_observers.insert( + (), + Box::new(move |cx| { + on_restart(cx); + true + }), + ); + activate(); + subscription + } + + /// Register a callback to be invoked when a window is closed + /// The window is no longer accessible at the point this callback is invoked. + pub fn on_window_closed(&self, mut on_closed: impl FnMut(&mut App) + 'static) -> Subscription { + let (subscription, activate) = self.window_closed_observers.insert((), Box::new(on_closed)); + activate(); + subscription + } + + pub(crate) fn clear_pending_keystrokes(&mut self) { + for window in self.windows() { + window + .update(self, |_, window, _| { + window.clear_pending_keystrokes(); + }) + .ok(); + } + } + + /// Checks if the given action is bound in the current context, as defined by the app's current focus, + /// the bindings in the element tree, and any global action listeners. + pub fn is_action_available(&mut self, action: &dyn Action) -> bool { + let mut action_available = false; + if let Some(window) = self.active_window() + && let Ok(window_action_available) = + window.update(self, |_, window, cx| window.is_action_available(action, cx)) + { + action_available = window_action_available; + } + + action_available + || self + .global_action_listeners + .contains_key(&action.as_any().type_id()) + } + + /// Sets the menu bar for this application. This will replace any existing menu bar. + pub fn set_menus(&self, menus: Vec) { + self.platform.set_menus(menus, &self.keymap.borrow()); + } + + /// Gets the menu bar for this application. + pub fn get_menus(&self) -> Option> { + self.platform.get_menus() + } + + /// Sets the right click menu for the app icon in the dock + pub fn set_dock_menu(&self, menus: Vec) { + self.platform.set_dock_menu(menus, &self.keymap.borrow()) + } + + /// Performs the action associated with the given dock menu item, only used on Windows for now. + pub fn perform_dock_menu_action(&self, action: usize) { + self.platform.perform_dock_menu_action(action); + } + + /// Adds given path to the bottom of the list of recent paths for the application. + /// The list is usually shown on the application icon's context menu in the dock, + /// and allows to open the recent files via that context menu. + /// If the path is already in the list, it will be moved to the bottom of the list. + pub fn add_recent_document(&self, path: &Path) { + self.platform.add_recent_document(path); + } + + /// Updates the jump list with the updated list of recent paths for the application, only used on Windows for now. + /// Note that this also sets the dock menu on Windows. + pub fn update_jump_list( + &self, + menus: Vec, + entries: Vec>, + ) -> Vec> { + self.platform.update_jump_list(menus, entries) + } + + /// Dispatch an action to the currently active window or global action handler + /// See [`crate::Action`] for more information on how actions work + pub fn dispatch_action(&mut self, action: &dyn Action) { + if let Some(active_window) = self.active_window() { + active_window + .update(self, |_, window, cx| { + window.dispatch_action(action.boxed_clone(), cx) + }) + .log_err(); + } else { + self.dispatch_global_action(action); + } + } + + fn dispatch_global_action(&mut self, action: &dyn Action) { + self.propagate_event = true; + + if let Some(mut global_listeners) = self + .global_action_listeners + .remove(&action.as_any().type_id()) + { + for listener in &global_listeners { + listener(action.as_any(), DispatchPhase::Capture, self); + if !self.propagate_event { + break; + } + } + + global_listeners.extend( + self.global_action_listeners + .remove(&action.as_any().type_id()) + .unwrap_or_default(), + ); + + self.global_action_listeners + .insert(action.as_any().type_id(), global_listeners); + } + + if self.propagate_event + && let Some(mut global_listeners) = self + .global_action_listeners + .remove(&action.as_any().type_id()) + { + for listener in global_listeners.iter().rev() { + listener(action.as_any(), DispatchPhase::Bubble, self); + if !self.propagate_event { + break; + } + } + + global_listeners.extend( + self.global_action_listeners + .remove(&action.as_any().type_id()) + .unwrap_or_default(), + ); + + self.global_action_listeners + .insert(action.as_any().type_id(), global_listeners); + } + } + + /// Is there currently something being dragged? + pub fn has_active_drag(&self) -> bool { + self.active_drag.is_some() + } + + /// Gets the cursor style of the currently active drag operation. + pub fn active_drag_cursor_style(&self) -> Option { + self.active_drag.as_ref().and_then(|drag| drag.cursor_style) + } + + /// Stops active drag and clears any related effects. + pub fn stop_active_drag(&mut self, window: &mut Window) -> bool { + if self.active_drag.is_some() { + self.active_drag = None; + window.refresh(); + true + } else { + false + } + } + + /// Sets the cursor style for the currently active drag operation. + pub fn set_active_drag_cursor_style( + &mut self, + cursor_style: CursorStyle, + window: &mut Window, + ) -> bool { + if let Some(ref mut drag) = self.active_drag { + drag.cursor_style = Some(cursor_style); + window.refresh(); + true + } else { + false + } + } + + /// Set the prompt renderer for GPUI. This will replace the default or platform specific + /// prompts with this custom implementation. + pub fn set_prompt_builder( + &mut self, + renderer: impl Fn( + PromptLevel, + &str, + Option<&str>, + &[PromptButton], + PromptHandle, + &mut Window, + &mut App, + ) -> RenderablePromptHandle + + 'static, + ) { + self.prompt_builder = Some(PromptBuilder::Custom(Box::new(renderer))); + } + + /// Reset the prompt builder to the default implementation. + pub fn reset_prompt_builder(&mut self) { + self.prompt_builder = Some(PromptBuilder::Default); + } + + /// Remove an asset from GPUI's cache + pub fn remove_asset(&mut self, source: &A::Source) { + let asset_id = (TypeId::of::(), hash(source)); + self.loading_assets.remove(&asset_id); + } + + /// Asynchronously load an asset, if the asset hasn't finished loading this will return None. + /// + /// Note that the multiple calls to this method will only result in one `Asset::load` call at a + /// time, and the results of this call will be cached + pub fn fetch_asset(&mut self, source: &A::Source) -> (Shared>, bool) { + let asset_id = (TypeId::of::(), hash(source)); + let mut is_first = false; + let task = self + .loading_assets + .remove(&asset_id) + .map(|boxed_task| *boxed_task.downcast::>>().unwrap()) + .unwrap_or_else(|| { + is_first = true; + let future = A::load(source.clone(), self); + + self.background_executor().spawn(future).shared() + }); + + self.loading_assets.insert(asset_id, Box::new(task.clone())); + + (task, is_first) + } + + /// Obtain a new [`FocusHandle`], which allows you to track and manipulate the keyboard focus + /// for elements rendered within this window. + #[track_caller] + pub fn focus_handle(&self) -> FocusHandle { + FocusHandle::new(&self.focus_handles) + } + + /// Tell GPUI that an entity has changed and observers of it should be notified. + pub fn notify(&mut self, entity_id: EntityId) { + let window_invalidators = mem::take( + self.window_invalidators_by_entity + .entry(entity_id) + .or_default(), + ); + + if window_invalidators.is_empty() { + if self.pending_notifications.insert(entity_id) { + self.pending_effects + .push_back(Effect::Notify { emitter: entity_id }); + } + } else { + for invalidator in window_invalidators.values() { + invalidator.invalidate_view(entity_id, self); + } + } + + self.window_invalidators_by_entity + .insert(entity_id, window_invalidators); + } + + /// Returns the name for this [`App`]. + #[cfg(any(test, feature = "test-support", debug_assertions))] + pub fn get_name(&self) -> Option<&'static str> { + self.name + } + + /// Returns `true` if the platform file picker supports selecting a mix of files and directories. + pub fn can_select_mixed_files_and_dirs(&self) -> bool { + self.platform.can_select_mixed_files_and_dirs() + } + + /// Removes an image from the sprite atlas on all windows. + /// + /// If the current window is being updated, it will be removed from `App.windows`, you can use `current_window` to specify the current window. + /// This is a no-op if the image is not in the sprite atlas. + pub fn drop_image(&mut self, image: Arc, current_window: Option<&mut Window>) { + // remove the texture from all other windows + for window in self.windows.values_mut().flatten() { + _ = window.drop_image(image.clone()); + } + + // remove the texture from the current window + if let Some(window) = current_window { + _ = window.drop_image(image); + } + } + + /// Sets the renderer for the inspector. + #[cfg(any(feature = "inspector", debug_assertions))] + pub fn set_inspector_renderer(&mut self, f: crate::InspectorRenderer) { + self.inspector_renderer = Some(f); + } + + /// Registers a renderer specific to an inspector state. + #[cfg(any(feature = "inspector", debug_assertions))] + pub fn register_inspector_element( + &mut self, + f: impl 'static + Fn(crate::InspectorElementId, &T, &mut Window, &mut App) -> R, + ) { + self.inspector_element_registry.register(f); + } + + /// Initializes gpui's default colors for the application. + /// + /// These colors can be accessed through `cx.default_colors()`. + pub fn init_colors(&mut self) { + self.set_global(GlobalColors(Arc::new(Colors::default()))); + } +} + +impl AppContext for App { + type Result = T; + + /// Builds an entity that is owned by the application. + /// + /// The given function will be invoked with a [`Context`] and must return an object representing the entity. An + /// [`Entity`] handle will be returned, which can be used to access the entity in a context. + fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity { + self.update(|cx| { + let slot = cx.entities.reserve(); + let handle = slot.clone(); + let entity = build_entity(&mut Context::new_context(cx, slot.downgrade())); + + cx.push_effect(Effect::EntityCreated { + entity: handle.clone().into_any(), + tid: TypeId::of::(), + window: cx.window_update_stack.last().cloned(), + }); + + cx.entities.insert(slot, entity); + handle + }) + } + + fn reserve_entity(&mut self) -> Self::Result> { + Reservation(self.entities.reserve()) + } + + fn insert_entity( + &mut self, + reservation: Reservation, + build_entity: impl FnOnce(&mut Context) -> T, + ) -> Self::Result> { + self.update(|cx| { + let slot = reservation.0; + let entity = build_entity(&mut Context::new_context(cx, slot.downgrade())); + cx.entities.insert(slot, entity) + }) + } + + /// Updates the entity referenced by the given handle. The function is passed a mutable reference to the + /// entity along with a `Context` for the entity. + fn update_entity( + &mut self, + handle: &Entity, + update: impl FnOnce(&mut T, &mut Context) -> R, + ) -> R { + self.update(|cx| { + let mut entity = cx.entities.lease(handle); + let result = update( + &mut entity, + &mut Context::new_context(cx, handle.downgrade()), + ); + cx.entities.end_lease(entity); + result + }) + } + + fn as_mut<'a, T>(&'a mut self, handle: &Entity) -> GpuiBorrow<'a, T> + where + T: 'static, + { + GpuiBorrow::new(handle.clone(), self) + } + + fn read_entity( + &self, + handle: &Entity, + read: impl FnOnce(&T, &App) -> R, + ) -> Self::Result + where + T: 'static, + { + let entity = self.entities.read(handle); + read(entity, self) + } + + fn update_window(&mut self, handle: AnyWindowHandle, update: F) -> Result + where + F: FnOnce(AnyView, &mut Window, &mut App) -> T, + { + self.update_window_id(handle.id, update) + } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(Entity, &App) -> R, + ) -> Result + where + T: 'static, + { + let window = self + .windows + .get(window.id) + .context("window not found")? + .as_deref() + .expect("attempted to read a window that is already on the stack"); + + let root_view = window.root.clone().unwrap(); + let view = root_view + .downcast::() + .map_err(|_| anyhow!("root view's type has changed"))?; + + Ok(read(view, self)) + } + + fn background_spawn(&self, future: impl Future + Send + 'static) -> Task + where + R: Send + 'static, + { + self.background_executor.spawn(future) + } + + fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> Self::Result + where + G: Global, + { + let mut g = self.global::(); + callback(g, self) + } +} + +/// These effects are processed at the end of each application update cycle. +pub(crate) enum Effect { + Notify { + emitter: EntityId, + }, + Emit { + emitter: EntityId, + event_type: TypeId, + event: Box, + }, + RefreshWindows, + NotifyGlobalObservers { + global_type: TypeId, + }, + Defer { + callback: Box, + }, + EntityCreated { + entity: AnyEntity, + tid: TypeId, + window: Option, + }, +} + +impl std::fmt::Debug for Effect { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Effect::Notify { emitter } => write!(f, "Notify({})", emitter), + Effect::Emit { emitter, .. } => write!(f, "Emit({:?})", emitter), + Effect::RefreshWindows => write!(f, "RefreshWindows"), + Effect::NotifyGlobalObservers { global_type } => { + write!(f, "NotifyGlobalObservers({:?})", global_type) + } + Effect::Defer { .. } => write!(f, "Defer(..)"), + Effect::EntityCreated { entity, .. } => write!(f, "EntityCreated({:?})", entity), + } + } +} + +/// Wraps a global variable value during `update_global` while the value has been moved to the stack. +pub(crate) struct GlobalLease { + global: Box, + global_type: PhantomData, +} + +impl GlobalLease { + fn new(global: Box) -> Self { + GlobalLease { + global, + global_type: PhantomData, + } + } +} + +impl Deref for GlobalLease { + type Target = G; + + fn deref(&self) -> &Self::Target { + self.global.downcast_ref().unwrap() + } +} + +impl DerefMut for GlobalLease { + fn deref_mut(&mut self) -> &mut Self::Target { + self.global.downcast_mut().unwrap() + } +} + +/// Contains state associated with an active drag operation, started by dragging an element +/// within the window or by dragging into the app from the underlying platform. +pub struct AnyDrag { + /// The view used to render this drag + pub view: AnyView, + + /// The value of the dragged item, to be dropped + pub value: Arc, + + /// This is used to render the dragged item in the same place + /// on the original element that the drag was initiated + pub cursor_offset: Point, + + /// The cursor style to use while dragging + pub cursor_style: Option, +} + +/// Contains state associated with a tooltip. You'll only need this struct if you're implementing +/// tooltip behavior on a custom element. Otherwise, use [Div::tooltip](crate::Interactivity::tooltip). +#[derive(Clone)] +pub struct AnyTooltip { + /// The view used to display the tooltip + pub view: AnyView, + + /// The absolute position of the mouse when the tooltip was deployed. + pub mouse_position: Point, + + /// Given the bounds of the tooltip, checks whether the tooltip should still be visible and + /// updates its state accordingly. This is needed atop the hovered element's mouse move handler + /// to handle the case where the element is not painted (e.g. via use of `visible_on_hover`). + pub check_visible_and_update: Rc, &mut Window, &mut App) -> bool>, +} + +/// A keystroke event, and potentially the associated action +#[derive(Debug)] +pub struct KeystrokeEvent { + /// The keystroke that occurred + pub keystroke: Keystroke, + + /// The action that was resolved for the keystroke, if any + pub action: Option>, + + /// The context stack at the time + pub context_stack: Vec, +} + +struct NullHttpClient; + +impl HttpClient for NullHttpClient { + fn send( + &self, + _req: http_client::Request, + ) -> futures::future::BoxFuture< + 'static, + anyhow::Result>, + > { + async move { + anyhow::bail!("No HttpClient available"); + } + .boxed() + } + + fn user_agent(&self) -> Option<&http_client::http::HeaderValue> { + None + } + + fn proxy(&self) -> Option<&Url> { + None + } + + fn type_name(&self) -> &'static str { + type_name::() + } +} + +/// A mutable reference to an entity owned by GPUI +pub struct GpuiBorrow<'a, T> { + inner: Option>, + app: &'a mut App, +} + +impl<'a, T: 'static> GpuiBorrow<'a, T> { + fn new(inner: Entity, app: &'a mut App) -> Self { + app.start_update(); + let lease = app.entities.lease(&inner); + Self { + inner: Some(lease), + app, + } + } +} + +impl<'a, T: 'static> std::borrow::Borrow for GpuiBorrow<'a, T> { + fn borrow(&self) -> &T { + self.inner.as_ref().unwrap().borrow() + } +} + +impl<'a, T: 'static> std::borrow::BorrowMut for GpuiBorrow<'a, T> { + fn borrow_mut(&mut self) -> &mut T { + self.inner.as_mut().unwrap().borrow_mut() + } +} + +impl<'a, T: 'static> std::ops::Deref for GpuiBorrow<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.inner.as_ref().unwrap() + } +} + +impl<'a, T: 'static> std::ops::DerefMut for GpuiBorrow<'a, T> { + fn deref_mut(&mut self) -> &mut T { + self.inner.as_mut().unwrap() + } +} + +impl<'a, T> Drop for GpuiBorrow<'a, T> { + fn drop(&mut self) { + let lease = self.inner.take().unwrap(); + self.app.notify(lease.id); + self.app.entities.end_lease(lease); + self.app.finish_update(); + } +} + +#[cfg(test)] +mod test { + use std::{cell::RefCell, rc::Rc}; + + use crate::{AppContext, TestAppContext}; + + #[test] + fn test_gpui_borrow() { + let cx = TestAppContext::single(); + let observation_count = Rc::new(RefCell::new(0)); + + let state = cx.update(|cx| { + let state = cx.new(|_| false); + cx.observe(&state, { + let observation_count = observation_count.clone(); + move |_, _| { + let mut count = observation_count.borrow_mut(); + *count += 1; + } + }) + .detach(); + + state + }); + + cx.update(|cx| { + // Calling this like this so that we don't clobber the borrow_mut above + *std::borrow::BorrowMut::borrow_mut(&mut state.as_mut(cx)) = true; + }); + + cx.update(|cx| { + state.write(cx, false); + }); + + assert_eq!(*observation_count.borrow(), 2); + } +} diff --git a/third_party/gpui/src/app/async_context.rs b/third_party/gpui/src/app/async_context.rs new file mode 100644 index 0000000..cfe7a5a --- /dev/null +++ b/third_party/gpui/src/app/async_context.rs @@ -0,0 +1,488 @@ +use crate::{ + AnyView, AnyWindowHandle, App, AppCell, AppContext, BackgroundExecutor, BorrowAppContext, + Entity, EventEmitter, Focusable, ForegroundExecutor, Global, PromptButton, PromptLevel, Render, + Reservation, Result, Subscription, Task, VisualContext, Window, WindowHandle, +}; +use anyhow::{Context as _, anyhow}; +use derive_more::{Deref, DerefMut}; +use futures::channel::oneshot; +use std::{future::Future, rc::Weak}; + +use super::{Context, WeakEntity}; + +/// An async-friendly version of [App] with a static lifetime so it can be held across `await` points in async code. +/// You're provided with an instance when calling [App::spawn], and you can also create one with [App::to_async]. +/// Internally, this holds a weak reference to an `App`, so its methods are fallible to protect against cases where the [App] is dropped. +#[derive(Clone)] +pub struct AsyncApp { + pub(crate) app: Weak, + pub(crate) background_executor: BackgroundExecutor, + pub(crate) foreground_executor: ForegroundExecutor, +} + +impl AppContext for AsyncApp { + type Result = Result; + + fn new( + &mut self, + build_entity: impl FnOnce(&mut Context) -> T, + ) -> Self::Result> { + let app = self.app.upgrade().context("app was released")?; + let mut app = app.borrow_mut(); + Ok(app.new(build_entity)) + } + + fn reserve_entity(&mut self) -> Result> { + let app = self.app.upgrade().context("app was released")?; + let mut app = app.borrow_mut(); + Ok(app.reserve_entity()) + } + + fn insert_entity( + &mut self, + reservation: Reservation, + build_entity: impl FnOnce(&mut Context) -> T, + ) -> Result> { + let app = self.app.upgrade().context("app was released")?; + let mut app = app.borrow_mut(); + Ok(app.insert_entity(reservation, build_entity)) + } + + fn update_entity( + &mut self, + handle: &Entity, + update: impl FnOnce(&mut T, &mut Context) -> R, + ) -> Self::Result { + let app = self.app.upgrade().context("app was released")?; + let mut app = app.borrow_mut(); + Ok(app.update_entity(handle, update)) + } + + fn as_mut<'a, T>(&'a mut self, _handle: &Entity) -> Self::Result> + where + T: 'static, + { + Err(anyhow!( + "Cannot as_mut with an async context. Try calling update() first" + )) + } + + fn read_entity( + &self, + handle: &Entity, + callback: impl FnOnce(&T, &App) -> R, + ) -> Self::Result + where + T: 'static, + { + let app = self.app.upgrade().context("app was released")?; + let lock = app.borrow(); + Ok(lock.read_entity(handle, callback)) + } + + fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result + where + F: FnOnce(AnyView, &mut Window, &mut App) -> T, + { + let app = self.app.upgrade().context("app was released")?; + let mut lock = app.try_borrow_mut()?; + lock.update_window(window, f) + } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(Entity, &App) -> R, + ) -> Result + where + T: 'static, + { + let app = self.app.upgrade().context("app was released")?; + let lock = app.borrow(); + lock.read_window(window, read) + } + + fn background_spawn(&self, future: impl Future + Send + 'static) -> Task + where + R: Send + 'static, + { + self.background_executor.spawn(future) + } + + fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> Self::Result + where + G: Global, + { + let app = self.app.upgrade().context("app was released")?; + let mut lock = app.borrow_mut(); + Ok(lock.update(|this| this.read_global(callback))) + } +} + +impl AsyncApp { + /// Schedules all windows in the application to be redrawn. + pub fn refresh(&self) -> Result<()> { + let app = self.app.upgrade().context("app was released")?; + let mut lock = app.borrow_mut(); + lock.refresh_windows(); + Ok(()) + } + + /// Get an executor which can be used to spawn futures in the background. + pub fn background_executor(&self) -> &BackgroundExecutor { + &self.background_executor + } + + /// Get an executor which can be used to spawn futures in the foreground. + pub fn foreground_executor(&self) -> &ForegroundExecutor { + &self.foreground_executor + } + + /// Invoke the given function in the context of the app, then flush any effects produced during its invocation. + pub fn update(&self, f: impl FnOnce(&mut App) -> R) -> Result { + let app = self.app.upgrade().context("app was released")?; + let mut lock = app.borrow_mut(); + Ok(lock.update(f)) + } + + /// Arrange for the given callback to be invoked whenever the given entity emits an event of a given type. + /// The callback is provided a handle to the emitting entity and a reference to the emitted event. + pub fn subscribe( + &mut self, + entity: &Entity, + mut on_event: impl FnMut(Entity, &Event, &mut App) + 'static, + ) -> Result + where + T: 'static + EventEmitter, + Event: 'static, + { + let app = self.app.upgrade().context("app was released")?; + let mut lock = app.borrow_mut(); + let subscription = lock.subscribe(entity, on_event); + Ok(subscription) + } + + /// Open a window with the given options based on the root view returned by the given function. + pub fn open_window( + &self, + options: crate::WindowOptions, + build_root_view: impl FnOnce(&mut Window, &mut App) -> Entity, + ) -> Result> + where + V: 'static + Render, + { + let app = self.app.upgrade().context("app was released")?; + let mut lock = app.borrow_mut(); + lock.open_window(options, build_root_view) + } + + /// Schedule a future to be polled in the background. + #[track_caller] + pub fn spawn(&self, f: AsyncFn) -> Task + where + AsyncFn: AsyncFnOnce(&mut AsyncApp) -> R + 'static, + R: 'static, + { + let mut cx = self.clone(); + self.foreground_executor + .spawn(async move { f(&mut cx).await }) + } + + /// Determine whether global state of the specified type has been assigned. + /// Returns an error if the `App` has been dropped. + pub fn has_global(&self) -> Result { + let app = self.app.upgrade().context("app was released")?; + let app = app.borrow_mut(); + Ok(app.has_global::()) + } + + /// Reads the global state of the specified type, passing it to the given callback. + /// + /// Panics if no global state of the specified type has been assigned. + /// Returns an error if the `App` has been dropped. + pub fn read_global(&self, read: impl FnOnce(&G, &App) -> R) -> Result { + let app = self.app.upgrade().context("app was released")?; + let app = app.borrow_mut(); + Ok(read(app.global(), &app)) + } + + /// Reads the global state of the specified type, passing it to the given callback. + /// + /// Similar to [`AsyncApp::read_global`], but returns an error instead of panicking + /// if no state of the specified type has been assigned. + /// + /// Returns an error if no state of the specified type has been assigned the `App` has been dropped. + pub fn try_read_global(&self, read: impl FnOnce(&G, &App) -> R) -> Option { + let app = self.app.upgrade()?; + let app = app.borrow_mut(); + Some(read(app.try_global()?, &app)) + } + + /// Reads the global state of the specified type, passing it to the given callback. + /// A default value is assigned if a global of this type has not yet been assigned. + /// + /// # Errors + /// If the app has ben dropped this returns an error. + pub fn try_read_default_global( + &self, + read: impl FnOnce(&G, &App) -> R, + ) -> Result { + let app = self.app.upgrade().context("app was released")?; + let mut app = app.borrow_mut(); + app.update(|cx| { + cx.default_global::(); + }); + Ok(read(app.try_global().context("app was released")?, &app)) + } + + /// A convenience method for [`App::update_global`](BorrowAppContext::update_global) + /// for updating the global state of the specified type. + pub fn update_global( + &self, + update: impl FnOnce(&mut G, &mut App) -> R, + ) -> Result { + let app = self.app.upgrade().context("app was released")?; + let mut app = app.borrow_mut(); + Ok(app.update(|cx| cx.update_global(update))) + } + + /// Run something using this entity and cx, when the returned struct is dropped + pub fn on_drop) + 'static>( + &self, + entity: &WeakEntity, + f: Callback, + ) -> util::Deferred> { + let entity = entity.clone(); + let mut cx = self.clone(); + util::defer(move || { + entity.update(&mut cx, f).ok(); + }) + } +} + +/// A cloneable, owned handle to the application context, +/// composed with the window associated with the current task. +#[derive(Clone, Deref, DerefMut)] +pub struct AsyncWindowContext { + #[deref] + #[deref_mut] + app: AsyncApp, + window: AnyWindowHandle, +} + +impl AsyncWindowContext { + pub(crate) fn new_context(app: AsyncApp, window: AnyWindowHandle) -> Self { + Self { app, window } + } + + /// Get the handle of the window this context is associated with. + pub fn window_handle(&self) -> AnyWindowHandle { + self.window + } + + /// A convenience method for [`App::update_window`]. + pub fn update(&mut self, update: impl FnOnce(&mut Window, &mut App) -> R) -> Result { + self.app + .update_window(self.window, |_, window, cx| update(window, cx)) + } + + /// A convenience method for [`App::update_window`]. + pub fn update_root( + &mut self, + update: impl FnOnce(AnyView, &mut Window, &mut App) -> R, + ) -> Result { + self.app.update_window(self.window, update) + } + + /// A convenience method for [`Window::on_next_frame`]. + pub fn on_next_frame(&mut self, f: impl FnOnce(&mut Window, &mut App) + 'static) { + self.window + .update(self, |_, window, _| window.on_next_frame(f)) + .ok(); + } + + /// A convenience method for [`App::global`]. + pub fn read_global( + &mut self, + read: impl FnOnce(&G, &Window, &App) -> R, + ) -> Result { + self.window + .update(self, |_, window, cx| read(cx.global(), window, cx)) + } + + /// A convenience method for [`App::update_global`](BorrowAppContext::update_global). + /// for updating the global state of the specified type. + pub fn update_global( + &mut self, + update: impl FnOnce(&mut G, &mut Window, &mut App) -> R, + ) -> Result + where + G: Global, + { + self.window.update(self, |_, window, cx| { + cx.update_global(|global, cx| update(global, window, cx)) + }) + } + + /// Schedule a future to be executed on the main thread. This is used for collecting + /// the results of background tasks and updating the UI. + #[track_caller] + pub fn spawn(&self, f: AsyncFn) -> Task + where + AsyncFn: AsyncFnOnce(&mut AsyncWindowContext) -> R + 'static, + R: 'static, + { + let mut cx = self.clone(); + self.foreground_executor + .spawn(async move { f(&mut cx).await }) + } + + /// Present a platform dialog. + /// The provided message will be presented, along with buttons for each answer. + /// When a button is clicked, the returned Receiver will receive the index of the clicked button. + pub fn prompt( + &mut self, + level: PromptLevel, + message: &str, + detail: Option<&str>, + answers: &[T], + ) -> oneshot::Receiver + where + T: Clone + Into, + { + self.window + .update(self, |_, window, cx| { + window.prompt(level, message, detail, answers, cx) + }) + .unwrap_or_else(|_| oneshot::channel().1) + } +} + +impl AppContext for AsyncWindowContext { + type Result = Result; + + fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Result> + where + T: 'static, + { + self.window.update(self, |_, _, cx| cx.new(build_entity)) + } + + fn reserve_entity(&mut self) -> Result> { + self.window.update(self, |_, _, cx| cx.reserve_entity()) + } + + fn insert_entity( + &mut self, + reservation: Reservation, + build_entity: impl FnOnce(&mut Context) -> T, + ) -> Self::Result> { + self.window + .update(self, |_, _, cx| cx.insert_entity(reservation, build_entity)) + } + + fn update_entity( + &mut self, + handle: &Entity, + update: impl FnOnce(&mut T, &mut Context) -> R, + ) -> Result { + self.window + .update(self, |_, _, cx| cx.update_entity(handle, update)) + } + + fn as_mut<'a, T>(&'a mut self, _: &Entity) -> Self::Result> + where + T: 'static, + { + Err(anyhow!( + "Cannot use as_mut() from an async context, call `update`" + )) + } + + fn read_entity( + &self, + handle: &Entity, + read: impl FnOnce(&T, &App) -> R, + ) -> Self::Result + where + T: 'static, + { + self.app.read_entity(handle, read) + } + + fn update_window(&mut self, window: AnyWindowHandle, update: F) -> Result + where + F: FnOnce(AnyView, &mut Window, &mut App) -> T, + { + self.app.update_window(window, update) + } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(Entity, &App) -> R, + ) -> Result + where + T: 'static, + { + self.app.read_window(window, read) + } + + fn background_spawn(&self, future: impl Future + Send + 'static) -> Task + where + R: Send + 'static, + { + self.app.background_executor.spawn(future) + } + + fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> Result + where + G: Global, + { + self.app.read_global(callback) + } +} + +impl VisualContext for AsyncWindowContext { + fn window_handle(&self) -> AnyWindowHandle { + self.window + } + + fn new_window_entity( + &mut self, + build_entity: impl FnOnce(&mut Window, &mut Context) -> T, + ) -> Self::Result> { + self.window + .update(self, |_, window, cx| cx.new(|cx| build_entity(window, cx))) + } + + fn update_window_entity( + &mut self, + view: &Entity, + update: impl FnOnce(&mut T, &mut Window, &mut Context) -> R, + ) -> Self::Result { + self.window.update(self, |_, window, cx| { + view.update(cx, |entity, cx| update(entity, window, cx)) + }) + } + + fn replace_root_view( + &mut self, + build_view: impl FnOnce(&mut Window, &mut Context) -> V, + ) -> Self::Result> + where + V: 'static + Render, + { + self.window + .update(self, |_, window, cx| window.replace_root(cx, build_view)) + } + + fn focus(&mut self, view: &Entity) -> Self::Result<()> + where + V: Focusable, + { + self.window.update(self, |_, window, cx| { + view.read(cx).focus_handle(cx).focus(window); + }) + } +} diff --git a/third_party/gpui/src/app/context.rs b/third_party/gpui/src/app/context.rs new file mode 100644 index 0000000..41d6cac --- /dev/null +++ b/third_party/gpui/src/app/context.rs @@ -0,0 +1,824 @@ +use crate::{ + AnyView, AnyWindowHandle, AppContext, AsyncApp, DispatchPhase, Effect, EntityId, EventEmitter, + FocusHandle, FocusOutEvent, Focusable, Global, KeystrokeObserver, Reservation, SubscriberSet, + Subscription, Task, WeakEntity, WeakFocusHandle, Window, WindowHandle, +}; +use anyhow::Result; +use futures::FutureExt; +use std::{ + any::{Any, TypeId}, + borrow::{Borrow, BorrowMut}, + future::Future, + ops, + sync::Arc, +}; +use util::Deferred; + +use super::{App, AsyncWindowContext, Entity, KeystrokeEvent}; + +/// The app context, with specialized behavior for the given entity. +pub struct Context<'a, T> { + app: &'a mut App, + entity_state: WeakEntity, +} + +impl<'a, T> ops::Deref for Context<'a, T> { + type Target = App; + + fn deref(&self) -> &Self::Target { + self.app + } +} + +impl<'a, T> ops::DerefMut for Context<'a, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.app + } +} + +impl<'a, T: 'static> Context<'a, T> { + pub(crate) fn new_context(app: &'a mut App, entity_state: WeakEntity) -> Self { + Self { app, entity_state } + } + + /// The entity id of the entity backing this context. + pub fn entity_id(&self) -> EntityId { + self.entity_state.entity_id + } + + /// Returns a handle to the entity belonging to this context. + pub fn entity(&self) -> Entity { + self.weak_entity() + .upgrade() + .expect("The entity must be alive if we have a entity context") + } + + /// Returns a weak handle to the entity belonging to this context. + pub fn weak_entity(&self) -> WeakEntity { + self.entity_state.clone() + } + + /// Arranges for the given function to be called whenever [`Context::notify`] is + /// called with the given entity. + pub fn observe( + &mut self, + entity: &Entity, + mut on_notify: impl FnMut(&mut T, Entity, &mut Context) + 'static, + ) -> Subscription + where + T: 'static, + W: 'static, + { + let this = self.weak_entity(); + self.app.observe_internal(entity, move |e, cx| { + if let Some(this) = this.upgrade() { + this.update(cx, |this, cx| on_notify(this, e, cx)); + true + } else { + false + } + }) + } + + /// Observe changes to ourselves + pub fn observe_self( + &mut self, + mut on_event: impl FnMut(&mut T, &mut Context) + 'static, + ) -> Subscription + where + T: 'static, + { + let this = self.entity(); + self.app.observe(&this, move |this, cx| { + this.update(cx, |this, cx| on_event(this, cx)) + }) + } + + /// Subscribe to an event type from another entity + pub fn subscribe( + &mut self, + entity: &Entity, + mut on_event: impl FnMut(&mut T, Entity, &Evt, &mut Context) + 'static, + ) -> Subscription + where + T: 'static, + T2: 'static + EventEmitter, + Evt: 'static, + { + let this = self.weak_entity(); + self.app.subscribe_internal(entity, move |e, event, cx| { + if let Some(this) = this.upgrade() { + this.update(cx, |this, cx| on_event(this, e, event, cx)); + true + } else { + false + } + }) + } + + /// Subscribe to an event type from ourself + pub fn subscribe_self( + &mut self, + mut on_event: impl FnMut(&mut T, &Evt, &mut Context) + 'static, + ) -> Subscription + where + T: 'static + EventEmitter, + Evt: 'static, + { + let this = self.entity(); + self.app.subscribe(&this, move |this, evt, cx| { + this.update(cx, |this, cx| on_event(this, evt, cx)) + }) + } + + /// Register a callback to be invoked when GPUI releases this entity. + pub fn on_release(&self, on_release: impl FnOnce(&mut T, &mut App) + 'static) -> Subscription + where + T: 'static, + { + let (subscription, activate) = self.app.release_listeners.insert( + self.entity_state.entity_id, + Box::new(move |this, cx| { + let this = this.downcast_mut().expect("invalid entity type"); + on_release(this, cx); + }), + ); + activate(); + subscription + } + + /// Register a callback to be run on the release of another entity + pub fn observe_release( + &self, + entity: &Entity, + on_release: impl FnOnce(&mut T, &mut T2, &mut Context) + 'static, + ) -> Subscription + where + T: Any, + T2: 'static, + { + let entity_id = entity.entity_id(); + let this = self.weak_entity(); + let (subscription, activate) = self.app.release_listeners.insert( + entity_id, + Box::new(move |entity, cx| { + let entity = entity.downcast_mut().expect("invalid entity type"); + if let Some(this) = this.upgrade() { + this.update(cx, |this, cx| on_release(this, entity, cx)); + } + }), + ); + activate(); + subscription + } + + /// Register a callback to for updates to the given global + pub fn observe_global( + &mut self, + mut f: impl FnMut(&mut T, &mut Context) + 'static, + ) -> Subscription + where + T: 'static, + { + let handle = self.weak_entity(); + let (subscription, activate) = self.global_observers.insert( + TypeId::of::(), + Box::new(move |cx| handle.update(cx, |view, cx| f(view, cx)).is_ok()), + ); + self.defer(move |_| activate()); + subscription + } + + /// Register a callback to be invoked when the application is about to restart. + pub fn on_app_restart( + &self, + mut on_restart: impl FnMut(&mut T, &mut App) + 'static, + ) -> Subscription + where + T: 'static, + { + let handle = self.weak_entity(); + self.app.on_app_restart(move |cx| { + handle.update(cx, |entity, cx| on_restart(entity, cx)).ok(); + }) + } + + /// Arrange for the given function to be invoked whenever the application is quit. + /// The future returned from this callback will be polled for up to [crate::SHUTDOWN_TIMEOUT] until the app fully quits. + pub fn on_app_quit( + &self, + mut on_quit: impl FnMut(&mut T, &mut Context) -> Fut + 'static, + ) -> Subscription + where + Fut: 'static + Future, + T: 'static, + { + let handle = self.weak_entity(); + self.app.on_app_quit(move |cx| { + let future = handle.update(cx, |entity, cx| on_quit(entity, cx)).ok(); + async move { + if let Some(future) = future { + future.await; + } + } + .boxed_local() + }) + } + + /// Tell GPUI that this entity has changed and observers of it should be notified. + pub fn notify(&mut self) { + self.app.notify(self.entity_state.entity_id); + } + + /// Spawn the future returned by the given function. + /// The function is provided a weak handle to the entity owned by this context and a context that can be held across await points. + /// The returned task must be held or detached. + #[track_caller] + pub fn spawn(&self, f: AsyncFn) -> Task + where + T: 'static, + AsyncFn: AsyncFnOnce(WeakEntity, &mut AsyncApp) -> R + 'static, + R: 'static, + { + let this = self.weak_entity(); + self.app.spawn(async move |cx| f(this, cx).await) + } + + /// Convenience method for accessing view state in an event callback. + /// + /// Many GPUI callbacks take the form of `Fn(&E, &mut Window, &mut App)`, + /// but it's often useful to be able to access view state in these + /// callbacks. This method provides a convenient way to do so. + pub fn listener( + &self, + f: impl Fn(&mut T, &E, &mut Window, &mut Context) + 'static, + ) -> impl Fn(&E, &mut Window, &mut App) + 'static { + let view = self.entity().downgrade(); + move |e: &E, window: &mut Window, cx: &mut App| { + view.update(cx, |view, cx| f(view, e, window, cx)).ok(); + } + } + + /// Convenience method for producing view state in a closure. + /// See `listener` for more details. + pub fn processor( + &self, + f: impl Fn(&mut T, E, &mut Window, &mut Context) -> R + 'static, + ) -> impl Fn(E, &mut Window, &mut App) -> R + 'static { + let view = self.entity(); + move |e: E, window: &mut Window, cx: &mut App| { + view.update(cx, |view, cx| f(view, e, window, cx)) + } + } + + /// Run something using this entity and cx, when the returned struct is dropped + pub fn on_drop( + &self, + f: impl FnOnce(&mut T, &mut Context) + 'static, + ) -> Deferred { + let this = self.weak_entity(); + let mut cx = self.to_async(); + util::defer(move || { + this.update(&mut cx, f).ok(); + }) + } + + /// Focus the given view in the given window. View type is required to implement Focusable. + pub fn focus_view(&mut self, view: &Entity, window: &mut Window) { + window.focus(&view.focus_handle(self)); + } + + /// Sets a given callback to be run on the next frame. + pub fn on_next_frame( + &self, + window: &mut Window, + f: impl FnOnce(&mut T, &mut Window, &mut Context) + 'static, + ) where + T: 'static, + { + let view = self.entity(); + window.on_next_frame(move |window, cx| view.update(cx, |view, cx| f(view, window, cx))); + } + + /// Schedules the given function to be run at the end of the current effect cycle, allowing entities + /// that are currently on the stack to be returned to the app. + pub fn defer_in( + &mut self, + window: &Window, + f: impl FnOnce(&mut T, &mut Window, &mut Context) + 'static, + ) { + let view = self.entity(); + window.defer(self, move |window, cx| { + view.update(cx, |view, cx| f(view, window, cx)) + }); + } + + /// Observe another entity for changes to its state, as tracked by [`Context::notify`]. + pub fn observe_in( + &mut self, + observed: &Entity, + window: &mut Window, + mut on_notify: impl FnMut(&mut T, Entity, &mut Window, &mut Context) + 'static, + ) -> Subscription + where + V2: 'static, + T: 'static, + { + let observed_id = observed.entity_id(); + let observed = observed.downgrade(); + let window_handle = window.handle; + let observer = self.weak_entity(); + self.new_observer( + observed_id, + Box::new(move |cx| { + window_handle + .update(cx, |_, window, cx| { + if let Some((observer, observed)) = + observer.upgrade().zip(observed.upgrade()) + { + observer.update(cx, |observer, cx| { + on_notify(observer, observed, window, cx); + }); + true + } else { + false + } + }) + .unwrap_or(false) + }), + ) + } + + /// Subscribe to events emitted by another entity. + /// The entity to which you're subscribing must implement the [`EventEmitter`] trait. + /// The callback will be invoked with a reference to the current view, a handle to the emitting `Entity`, the event, a mutable reference to the `Window`, and the context for the entity. + pub fn subscribe_in( + &mut self, + emitter: &Entity, + window: &Window, + mut on_event: impl FnMut(&mut T, &Entity, &Evt, &mut Window, &mut Context) + 'static, + ) -> Subscription + where + Emitter: EventEmitter, + Evt: 'static, + { + let emitter = emitter.downgrade(); + let window_handle = window.handle; + let subscriber = self.weak_entity(); + self.new_subscription( + emitter.entity_id(), + ( + TypeId::of::(), + Box::new(move |event, cx| { + window_handle + .update(cx, |_, window, cx| { + if let Some((subscriber, emitter)) = + subscriber.upgrade().zip(emitter.upgrade()) + { + let event = event.downcast_ref().expect("invalid event type"); + subscriber.update(cx, |subscriber, cx| { + on_event(subscriber, &emitter, event, window, cx); + }); + true + } else { + false + } + }) + .unwrap_or(false) + }), + ), + ) + } + + /// Register a callback to be invoked when the view is released. + /// + /// The callback receives a handle to the view's window. This handle may be + /// invalid, if the window was closed before the view was released. + pub fn on_release_in( + &mut self, + window: &Window, + on_release: impl FnOnce(&mut T, &mut Window, &mut App) + 'static, + ) -> Subscription { + let entity = self.entity(); + self.app.observe_release_in(&entity, window, on_release) + } + + /// Register a callback to be invoked when the given Entity is released. + pub fn observe_release_in( + &self, + observed: &Entity, + window: &Window, + mut on_release: impl FnMut(&mut T, &mut T2, &mut Window, &mut Context) + 'static, + ) -> Subscription + where + T: 'static, + T2: 'static, + { + let observer = self.weak_entity(); + self.app + .observe_release_in(observed, window, move |observed, window, cx| { + observer + .update(cx, |observer, cx| { + on_release(observer, observed, window, cx) + }) + .ok(); + }) + } + + /// Register a callback to be invoked when the window is resized. + pub fn observe_window_bounds( + &self, + window: &mut Window, + mut callback: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, + ) -> Subscription { + let view = self.weak_entity(); + let (subscription, activate) = window.bounds_observers.insert( + (), + Box::new(move |window, cx| { + view.update(cx, |view, cx| callback(view, window, cx)) + .is_ok() + }), + ); + activate(); + subscription + } + + /// Register a callback to be invoked when the window is activated or deactivated. + pub fn observe_window_activation( + &self, + window: &mut Window, + mut callback: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, + ) -> Subscription { + let view = self.weak_entity(); + let (subscription, activate) = window.activation_observers.insert( + (), + Box::new(move |window, cx| { + view.update(cx, |view, cx| callback(view, window, cx)) + .is_ok() + }), + ); + activate(); + subscription + } + + /// Registers a callback to be invoked when the window appearance changes. + pub fn observe_window_appearance( + &self, + window: &mut Window, + mut callback: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, + ) -> Subscription { + let view = self.weak_entity(); + let (subscription, activate) = window.appearance_observers.insert( + (), + Box::new(move |window, cx| { + view.update(cx, |view, cx| callback(view, window, cx)) + .is_ok() + }), + ); + activate(); + subscription + } + + /// Register a callback to be invoked when a keystroke is received by the application + /// in any window. Note that this fires after all other action and event mechanisms have resolved + /// and that this API will not be invoked if the event's propagation is stopped. + pub fn observe_keystrokes( + &mut self, + mut f: impl FnMut(&mut T, &KeystrokeEvent, &mut Window, &mut Context) + 'static, + ) -> Subscription { + fn inner( + keystroke_observers: &SubscriberSet<(), KeystrokeObserver>, + handler: KeystrokeObserver, + ) -> Subscription { + let (subscription, activate) = keystroke_observers.insert((), handler); + activate(); + subscription + } + + let view = self.weak_entity(); + inner( + &self.keystroke_observers, + Box::new(move |event, window, cx| { + if let Some(view) = view.upgrade() { + view.update(cx, |view, cx| f(view, event, window, cx)); + true + } else { + false + } + }), + ) + } + + /// Register a callback to be invoked when the window's pending input changes. + pub fn observe_pending_input( + &self, + window: &mut Window, + mut callback: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, + ) -> Subscription { + let view = self.weak_entity(); + let (subscription, activate) = window.pending_input_observers.insert( + (), + Box::new(move |window, cx| { + view.update(cx, |view, cx| callback(view, window, cx)) + .is_ok() + }), + ); + activate(); + subscription + } + + /// Register a listener to be called when the given focus handle receives focus. + /// Returns a subscription and persists until the subscription is dropped. + pub fn on_focus( + &mut self, + handle: &FocusHandle, + window: &mut Window, + mut listener: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, + ) -> Subscription { + let view = self.weak_entity(); + let focus_id = handle.id; + let (subscription, activate) = + window.new_focus_listener(Box::new(move |event, window, cx| { + view.update(cx, |view, cx| { + if event.previous_focus_path.last() != Some(&focus_id) + && event.current_focus_path.last() == Some(&focus_id) + { + listener(view, window, cx) + } + }) + .is_ok() + })); + self.defer(|_| activate()); + subscription + } + + /// Register a listener to be called when the given focus handle or one of its descendants receives focus. + /// This does not fire if the given focus handle - or one of its descendants - was previously focused. + /// Returns a subscription and persists until the subscription is dropped. + pub fn on_focus_in( + &mut self, + handle: &FocusHandle, + window: &mut Window, + mut listener: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, + ) -> Subscription { + let view = self.weak_entity(); + let focus_id = handle.id; + let (subscription, activate) = + window.new_focus_listener(Box::new(move |event, window, cx| { + view.update(cx, |view, cx| { + if event.is_focus_in(focus_id) { + listener(view, window, cx) + } + }) + .is_ok() + })); + self.defer(|_| activate()); + subscription + } + + /// Register a listener to be called when the given focus handle loses focus. + /// Returns a subscription and persists until the subscription is dropped. + pub fn on_blur( + &mut self, + handle: &FocusHandle, + window: &mut Window, + mut listener: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, + ) -> Subscription { + let view = self.weak_entity(); + let focus_id = handle.id; + let (subscription, activate) = + window.new_focus_listener(Box::new(move |event, window, cx| { + view.update(cx, |view, cx| { + if event.previous_focus_path.last() == Some(&focus_id) + && event.current_focus_path.last() != Some(&focus_id) + { + listener(view, window, cx) + } + }) + .is_ok() + })); + self.defer(|_| activate()); + subscription + } + + /// Register a listener to be called when nothing in the window has focus. + /// This typically happens when the node that was focused is removed from the tree, + /// and this callback lets you chose a default place to restore the users focus. + /// Returns a subscription and persists until the subscription is dropped. + pub fn on_focus_lost( + &mut self, + window: &mut Window, + mut listener: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, + ) -> Subscription { + let view = self.weak_entity(); + let (subscription, activate) = window.focus_lost_listeners.insert( + (), + Box::new(move |window, cx| { + view.update(cx, |view, cx| listener(view, window, cx)) + .is_ok() + }), + ); + self.defer(|_| activate()); + subscription + } + + /// Register a listener to be called when the given focus handle or one of its descendants loses focus. + /// Returns a subscription and persists until the subscription is dropped. + pub fn on_focus_out( + &mut self, + handle: &FocusHandle, + window: &mut Window, + mut listener: impl FnMut(&mut T, FocusOutEvent, &mut Window, &mut Context) + 'static, + ) -> Subscription { + let view = self.weak_entity(); + let focus_id = handle.id; + let (subscription, activate) = + window.new_focus_listener(Box::new(move |event, window, cx| { + view.update(cx, |view, cx| { + if let Some(blurred_id) = event.previous_focus_path.last().copied() + && event.is_focus_out(focus_id) + { + let event = FocusOutEvent { + blurred: WeakFocusHandle { + id: blurred_id, + handles: Arc::downgrade(&cx.focus_handles), + }, + }; + listener(view, event, window, cx) + } + }) + .is_ok() + })); + self.defer(|_| activate()); + subscription + } + + /// Schedule a future to be run asynchronously. + /// The given callback is invoked with a [`WeakEntity`] to avoid leaking the entity for a long-running process. + /// It's also given an [`AsyncWindowContext`], which can be used to access the state of the entity across await points. + /// The returned future will be polled on the main thread. + #[track_caller] + pub fn spawn_in(&self, window: &Window, f: AsyncFn) -> Task + where + R: 'static, + AsyncFn: AsyncFnOnce(WeakEntity, &mut AsyncWindowContext) -> R + 'static, + { + let view = self.weak_entity(); + window.spawn(self, async move |cx| f(view, cx).await) + } + + /// Register a callback to be invoked when the given global state changes. + pub fn observe_global_in( + &mut self, + window: &Window, + mut f: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, + ) -> Subscription { + let window_handle = window.handle; + let view = self.weak_entity(); + let (subscription, activate) = self.global_observers.insert( + TypeId::of::(), + Box::new(move |cx| { + window_handle + .update(cx, |_, window, cx| { + view.update(cx, |view, cx| f(view, window, cx)).is_ok() + }) + .unwrap_or(false) + }), + ); + self.defer(move |_| activate()); + subscription + } + + /// Register a callback to be invoked when the given Action type is dispatched to the window. + pub fn on_action( + &mut self, + action_type: TypeId, + window: &mut Window, + listener: impl Fn(&mut T, &dyn Any, DispatchPhase, &mut Window, &mut Context) + 'static, + ) { + let handle = self.weak_entity(); + window.on_action(action_type, move |action, phase, window, cx| { + handle + .update(cx, |view, cx| { + listener(view, action, phase, window, cx); + }) + .ok(); + }); + } + + /// Move focus to the current view, assuming it implements [`Focusable`]. + pub fn focus_self(&mut self, window: &mut Window) + where + T: Focusable, + { + let view = self.entity(); + window.defer(self, move |window, cx| { + view.read(cx).focus_handle(cx).focus(window) + }) + } +} + +impl Context<'_, T> { + /// Emit an event of the specified type, which can be handled by other entities that have subscribed via `subscribe` methods on their respective contexts. + pub fn emit(&mut self, event: Evt) + where + T: EventEmitter, + Evt: 'static, + { + self.app.pending_effects.push_back(Effect::Emit { + emitter: self.entity_state.entity_id, + event_type: TypeId::of::(), + event: Box::new(event), + }); + } +} + +impl AppContext for Context<'_, T> { + type Result = U; + + fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> U) -> Entity { + self.app.new(build_entity) + } + + fn reserve_entity(&mut self) -> Reservation { + self.app.reserve_entity() + } + + fn insert_entity( + &mut self, + reservation: Reservation, + build_entity: impl FnOnce(&mut Context) -> U, + ) -> Self::Result> { + self.app.insert_entity(reservation, build_entity) + } + + fn update_entity( + &mut self, + handle: &Entity, + update: impl FnOnce(&mut U, &mut Context) -> R, + ) -> R { + self.app.update_entity(handle, update) + } + + fn as_mut<'a, E>(&'a mut self, handle: &Entity) -> Self::Result> + where + E: 'static, + { + self.app.as_mut(handle) + } + + fn read_entity( + &self, + handle: &Entity, + read: impl FnOnce(&U, &App) -> R, + ) -> Self::Result + where + U: 'static, + { + self.app.read_entity(handle, read) + } + + fn update_window(&mut self, window: AnyWindowHandle, update: F) -> Result + where + F: FnOnce(AnyView, &mut Window, &mut App) -> R, + { + self.app.update_window(window, update) + } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(Entity, &App) -> R, + ) -> Result + where + U: 'static, + { + self.app.read_window(window, read) + } + + fn background_spawn(&self, future: impl Future + Send + 'static) -> Task + where + R: Send + 'static, + { + self.app.background_executor.spawn(future) + } + + fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> Self::Result + where + G: Global, + { + self.app.read_global(callback) + } +} + +impl Borrow for Context<'_, T> { + fn borrow(&self) -> &App { + self.app + } +} + +impl BorrowMut for Context<'_, T> { + fn borrow_mut(&mut self) -> &mut App { + self.app + } +} diff --git a/third_party/gpui/src/app/entity_map.rs b/third_party/gpui/src/app/entity_map.rs new file mode 100644 index 0000000..bea98cb --- /dev/null +++ b/third_party/gpui/src/app/entity_map.rs @@ -0,0 +1,889 @@ +use crate::{App, AppContext, GpuiBorrow, VisualContext, Window, seal::Sealed}; +use anyhow::{Context as _, Result}; +use collections::FxHashSet; +use derive_more::{Deref, DerefMut}; +use parking_lot::{RwLock, RwLockUpgradableReadGuard}; +use slotmap::{KeyData, SecondaryMap, SlotMap}; +use std::{ + any::{Any, TypeId, type_name}, + cell::RefCell, + cmp::Ordering, + fmt::{self, Display}, + hash::{Hash, Hasher}, + marker::PhantomData, + mem, + num::NonZeroU64, + sync::{ + Arc, Weak, + atomic::{AtomicU64, AtomicUsize, Ordering::SeqCst}, + }, + thread::panicking, +}; + +use super::Context; +use crate::util::atomic_incr_if_not_zero; +#[cfg(any(test, feature = "leak-detection"))] +use collections::HashMap; + +slotmap::new_key_type! { + /// A unique identifier for a entity across the application. + pub struct EntityId; +} + +impl From for EntityId { + fn from(value: u64) -> Self { + Self(KeyData::from_ffi(value)) + } +} + +impl EntityId { + /// Converts this entity id to a [NonZeroU64] + pub fn as_non_zero_u64(self) -> NonZeroU64 { + NonZeroU64::new(self.0.as_ffi()).unwrap() + } + + /// Converts this entity id to a [u64] + pub fn as_u64(self) -> u64 { + self.0.as_ffi() + } +} + +impl Display for EntityId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_u64()) + } +} + +pub(crate) struct EntityMap { + entities: SecondaryMap>, + pub accessed_entities: RefCell>, + ref_counts: Arc>, +} + +struct EntityRefCounts { + counts: SlotMap, + dropped_entity_ids: Vec, + #[cfg(any(test, feature = "leak-detection"))] + leak_detector: LeakDetector, +} + +impl EntityMap { + pub fn new() -> Self { + Self { + entities: SecondaryMap::new(), + accessed_entities: RefCell::new(FxHashSet::default()), + ref_counts: Arc::new(RwLock::new(EntityRefCounts { + counts: SlotMap::with_key(), + dropped_entity_ids: Vec::new(), + #[cfg(any(test, feature = "leak-detection"))] + leak_detector: LeakDetector { + next_handle_id: 0, + entity_handles: HashMap::default(), + }, + })), + } + } + + /// Reserve a slot for an entity, which you can subsequently use with `insert`. + pub fn reserve(&self) -> Slot { + let id = self.ref_counts.write().counts.insert(1.into()); + Slot(Entity::new(id, Arc::downgrade(&self.ref_counts))) + } + + /// Insert an entity into a slot obtained by calling `reserve`. + pub fn insert(&mut self, slot: Slot, entity: T) -> Entity + where + T: 'static, + { + let mut accessed_entities = self.accessed_entities.borrow_mut(); + accessed_entities.insert(slot.entity_id); + + let handle = slot.0; + self.entities.insert(handle.entity_id, Box::new(entity)); + handle + } + + /// Move an entity to the stack. + #[track_caller] + pub fn lease(&mut self, pointer: &Entity) -> Lease { + self.assert_valid_context(pointer); + let mut accessed_entities = self.accessed_entities.borrow_mut(); + accessed_entities.insert(pointer.entity_id); + + let entity = Some( + self.entities + .remove(pointer.entity_id) + .unwrap_or_else(|| double_lease_panic::("update")), + ); + Lease { + entity, + id: pointer.entity_id, + entity_type: PhantomData, + } + } + + /// Returns an entity after moving it to the stack. + pub fn end_lease(&mut self, mut lease: Lease) { + self.entities.insert(lease.id, lease.entity.take().unwrap()); + } + + pub fn read(&self, entity: &Entity) -> &T { + self.assert_valid_context(entity); + let mut accessed_entities = self.accessed_entities.borrow_mut(); + accessed_entities.insert(entity.entity_id); + + self.entities + .get(entity.entity_id) + .and_then(|entity| entity.downcast_ref()) + .unwrap_or_else(|| double_lease_panic::("read")) + } + + fn assert_valid_context(&self, entity: &AnyEntity) { + debug_assert!( + Weak::ptr_eq(&entity.entity_map, &Arc::downgrade(&self.ref_counts)), + "used a entity with the wrong context" + ); + } + + pub fn extend_accessed(&mut self, entities: &FxHashSet) { + self.accessed_entities + .borrow_mut() + .extend(entities.iter().copied()); + } + + pub fn clear_accessed(&mut self) { + self.accessed_entities.borrow_mut().clear(); + } + + pub fn take_dropped(&mut self) -> Vec<(EntityId, Box)> { + let mut ref_counts = self.ref_counts.write(); + let dropped_entity_ids = mem::take(&mut ref_counts.dropped_entity_ids); + let mut accessed_entities = self.accessed_entities.borrow_mut(); + + dropped_entity_ids + .into_iter() + .filter_map(|entity_id| { + let count = ref_counts.counts.remove(entity_id).unwrap(); + debug_assert_eq!( + count.load(SeqCst), + 0, + "dropped an entity that was referenced" + ); + accessed_entities.remove(&entity_id); + // If the EntityId was allocated with `Context::reserve`, + // the entity may not have been inserted. + Some((entity_id, self.entities.remove(entity_id)?)) + }) + .collect() + } +} + +#[track_caller] +fn double_lease_panic(operation: &str) -> ! { + panic!( + "cannot {operation} {} while it is already being updated", + std::any::type_name::() + ) +} + +pub(crate) struct Lease { + entity: Option>, + pub id: EntityId, + entity_type: PhantomData, +} + +impl core::ops::Deref for Lease { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.entity.as_ref().unwrap().downcast_ref().unwrap() + } +} + +impl core::ops::DerefMut for Lease { + fn deref_mut(&mut self) -> &mut Self::Target { + self.entity.as_mut().unwrap().downcast_mut().unwrap() + } +} + +impl Drop for Lease { + fn drop(&mut self) { + if self.entity.is_some() && !panicking() { + panic!("Leases must be ended with EntityMap::end_lease") + } + } +} + +#[derive(Deref, DerefMut)] +pub(crate) struct Slot(Entity); + +/// A dynamically typed reference to a entity, which can be downcast into a `Entity`. +pub struct AnyEntity { + pub(crate) entity_id: EntityId, + pub(crate) entity_type: TypeId, + entity_map: Weak>, + #[cfg(any(test, feature = "leak-detection"))] + handle_id: HandleId, +} + +impl AnyEntity { + fn new(id: EntityId, entity_type: TypeId, entity_map: Weak>) -> Self { + Self { + entity_id: id, + entity_type, + #[cfg(any(test, feature = "leak-detection"))] + handle_id: entity_map + .clone() + .upgrade() + .unwrap() + .write() + .leak_detector + .handle_created(id), + entity_map, + } + } + + /// Returns the id associated with this entity. + pub fn entity_id(&self) -> EntityId { + self.entity_id + } + + /// Returns the [TypeId] associated with this entity. + pub fn entity_type(&self) -> TypeId { + self.entity_type + } + + /// Converts this entity handle into a weak variant, which does not prevent it from being released. + pub fn downgrade(&self) -> AnyWeakEntity { + AnyWeakEntity { + entity_id: self.entity_id, + entity_type: self.entity_type, + entity_ref_counts: self.entity_map.clone(), + } + } + + /// Converts this entity handle into a strongly-typed entity handle of the given type. + /// If this entity handle is not of the specified type, returns itself as an error variant. + pub fn downcast(self) -> Result, AnyEntity> { + if TypeId::of::() == self.entity_type { + Ok(Entity { + any_entity: self, + entity_type: PhantomData, + }) + } else { + Err(self) + } + } +} + +impl Clone for AnyEntity { + fn clone(&self) -> Self { + if let Some(entity_map) = self.entity_map.upgrade() { + let entity_map = entity_map.read(); + let count = entity_map + .counts + .get(self.entity_id) + .expect("detected over-release of a entity"); + let prev_count = count.fetch_add(1, SeqCst); + assert_ne!(prev_count, 0, "Detected over-release of a entity."); + } + + Self { + entity_id: self.entity_id, + entity_type: self.entity_type, + entity_map: self.entity_map.clone(), + #[cfg(any(test, feature = "leak-detection"))] + handle_id: self + .entity_map + .upgrade() + .unwrap() + .write() + .leak_detector + .handle_created(self.entity_id), + } + } +} + +impl Drop for AnyEntity { + fn drop(&mut self) { + if let Some(entity_map) = self.entity_map.upgrade() { + let entity_map = entity_map.upgradable_read(); + let count = entity_map + .counts + .get(self.entity_id) + .expect("detected over-release of a handle."); + let prev_count = count.fetch_sub(1, SeqCst); + assert_ne!(prev_count, 0, "Detected over-release of a entity."); + if prev_count == 1 { + // We were the last reference to this entity, so we can remove it. + let mut entity_map = RwLockUpgradableReadGuard::upgrade(entity_map); + entity_map.dropped_entity_ids.push(self.entity_id); + } + } + + #[cfg(any(test, feature = "leak-detection"))] + if let Some(entity_map) = self.entity_map.upgrade() { + entity_map + .write() + .leak_detector + .handle_released(self.entity_id, self.handle_id) + } + } +} + +impl From> for AnyEntity { + fn from(entity: Entity) -> Self { + entity.any_entity + } +} + +impl Hash for AnyEntity { + fn hash(&self, state: &mut H) { + self.entity_id.hash(state); + } +} + +impl PartialEq for AnyEntity { + fn eq(&self, other: &Self) -> bool { + self.entity_id == other.entity_id + } +} + +impl Eq for AnyEntity {} + +impl Ord for AnyEntity { + fn cmp(&self, other: &Self) -> Ordering { + self.entity_id.cmp(&other.entity_id) + } +} + +impl PartialOrd for AnyEntity { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl std::fmt::Debug for AnyEntity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("AnyEntity") + .field("entity_id", &self.entity_id.as_u64()) + .finish() + } +} + +/// A strong, well-typed reference to a struct which is managed +/// by GPUI +#[derive(Deref, DerefMut)] +pub struct Entity { + #[deref] + #[deref_mut] + pub(crate) any_entity: AnyEntity, + pub(crate) entity_type: PhantomData T>, +} + +impl Sealed for Entity {} + +impl Entity { + fn new(id: EntityId, entity_map: Weak>) -> Self + where + T: 'static, + { + Self { + any_entity: AnyEntity::new(id, TypeId::of::(), entity_map), + entity_type: PhantomData, + } + } + + /// Get the entity ID associated with this entity + pub fn entity_id(&self) -> EntityId { + self.any_entity.entity_id + } + + /// Downgrade this entity pointer to a non-retaining weak pointer + pub fn downgrade(&self) -> WeakEntity { + WeakEntity { + any_entity: self.any_entity.downgrade(), + entity_type: self.entity_type, + } + } + + /// Convert this into a dynamically typed entity. + pub fn into_any(self) -> AnyEntity { + self.any_entity + } + + /// Grab a reference to this entity from the context. + pub fn read<'a>(&self, cx: &'a App) -> &'a T { + cx.entities.read(self) + } + + /// Read the entity referenced by this handle with the given function. + pub fn read_with( + &self, + cx: &C, + f: impl FnOnce(&T, &App) -> R, + ) -> C::Result { + cx.read_entity(self, f) + } + + /// Updates the entity referenced by this handle with the given function. + pub fn update( + &self, + cx: &mut C, + update: impl FnOnce(&mut T, &mut Context) -> R, + ) -> C::Result { + cx.update_entity(self, update) + } + + /// Updates the entity referenced by this handle with the given function. + pub fn as_mut<'a, C: AppContext>(&self, cx: &'a mut C) -> C::Result> { + cx.as_mut(self) + } + + /// Updates the entity referenced by this handle with the given function. + pub fn write(&self, cx: &mut C, value: T) -> C::Result<()> { + self.update(cx, |entity, cx| { + *entity = value; + cx.notify(); + }) + } + + /// Updates the entity referenced by this handle with the given function if + /// the referenced entity still exists, within a visual context that has a window. + /// Returns an error if the entity has been released. + pub fn update_in( + &self, + cx: &mut C, + update: impl FnOnce(&mut T, &mut Window, &mut Context) -> R, + ) -> C::Result { + cx.update_window_entity(self, update) + } +} + +impl Clone for Entity { + fn clone(&self) -> Self { + Self { + any_entity: self.any_entity.clone(), + entity_type: self.entity_type, + } + } +} + +impl std::fmt::Debug for Entity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Entity") + .field("entity_id", &self.any_entity.entity_id) + .field("entity_type", &type_name::()) + .finish() + } +} + +impl Hash for Entity { + fn hash(&self, state: &mut H) { + self.any_entity.hash(state); + } +} + +impl PartialEq for Entity { + fn eq(&self, other: &Self) -> bool { + self.any_entity == other.any_entity + } +} + +impl Eq for Entity {} + +impl PartialEq> for Entity { + fn eq(&self, other: &WeakEntity) -> bool { + self.any_entity.entity_id() == other.entity_id() + } +} + +impl Ord for Entity { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.entity_id().cmp(&other.entity_id()) + } +} + +impl PartialOrd for Entity { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +/// A type erased, weak reference to a entity. +#[derive(Clone)] +pub struct AnyWeakEntity { + pub(crate) entity_id: EntityId, + entity_type: TypeId, + entity_ref_counts: Weak>, +} + +impl AnyWeakEntity { + /// Get the entity ID associated with this weak reference. + pub fn entity_id(&self) -> EntityId { + self.entity_id + } + + /// Check if this weak handle can be upgraded, or if the entity has already been dropped + pub fn is_upgradable(&self) -> bool { + let ref_count = self + .entity_ref_counts + .upgrade() + .and_then(|ref_counts| Some(ref_counts.read().counts.get(self.entity_id)?.load(SeqCst))) + .unwrap_or(0); + ref_count > 0 + } + + /// Upgrade this weak entity reference to a strong reference. + pub fn upgrade(&self) -> Option { + let ref_counts = &self.entity_ref_counts.upgrade()?; + let ref_counts = ref_counts.read(); + let ref_count = ref_counts.counts.get(self.entity_id)?; + + if atomic_incr_if_not_zero(ref_count) == 0 { + // entity_id is in dropped_entity_ids + return None; + } + drop(ref_counts); + + Some(AnyEntity { + entity_id: self.entity_id, + entity_type: self.entity_type, + entity_map: self.entity_ref_counts.clone(), + #[cfg(any(test, feature = "leak-detection"))] + handle_id: self + .entity_ref_counts + .upgrade() + .unwrap() + .write() + .leak_detector + .handle_created(self.entity_id), + }) + } + + /// Assert that entity referenced by this weak handle has been released. + #[cfg(any(test, feature = "leak-detection"))] + pub fn assert_released(&self) { + self.entity_ref_counts + .upgrade() + .unwrap() + .write() + .leak_detector + .assert_released(self.entity_id); + + if self + .entity_ref_counts + .upgrade() + .and_then(|ref_counts| Some(ref_counts.read().counts.get(self.entity_id)?.load(SeqCst))) + .is_some() + { + panic!( + "entity was recently dropped but resources are retained until the end of the effect cycle." + ) + } + } + + /// Creates a weak entity that can never be upgraded. + pub fn new_invalid() -> Self { + /// To hold the invariant that all ids are unique, and considering that slotmap + /// increases their IDs from `0`, we can decrease ours from `u64::MAX` so these + /// two will never conflict (u64 is way too large). + static UNIQUE_NON_CONFLICTING_ID_GENERATOR: AtomicU64 = AtomicU64::new(u64::MAX); + let entity_id = UNIQUE_NON_CONFLICTING_ID_GENERATOR.fetch_sub(1, SeqCst); + + Self { + // Safety: + // Docs say this is safe but can be unspecified if slotmap changes the representation + // after `1.0.7`, that said, providing a valid entity_id here is not necessary as long + // as we guarantee that `entity_id` is never used if `entity_ref_counts` equals + // to `Weak::new()` (that is, it's unable to upgrade), that is the invariant that + // actually needs to be hold true. + // + // And there is no sane reason to read an entity slot if `entity_ref_counts` can't be + // read in the first place, so we're good! + entity_id: entity_id.into(), + entity_type: TypeId::of::<()>(), + entity_ref_counts: Weak::new(), + } + } +} + +impl std::fmt::Debug for AnyWeakEntity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct(type_name::()) + .field("entity_id", &self.entity_id) + .field("entity_type", &self.entity_type) + .finish() + } +} + +impl From> for AnyWeakEntity { + fn from(entity: WeakEntity) -> Self { + entity.any_entity + } +} + +impl Hash for AnyWeakEntity { + fn hash(&self, state: &mut H) { + self.entity_id.hash(state); + } +} + +impl PartialEq for AnyWeakEntity { + fn eq(&self, other: &Self) -> bool { + self.entity_id == other.entity_id + } +} + +impl Eq for AnyWeakEntity {} + +impl Ord for AnyWeakEntity { + fn cmp(&self, other: &Self) -> Ordering { + self.entity_id.cmp(&other.entity_id) + } +} + +impl PartialOrd for AnyWeakEntity { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +/// A weak reference to a entity of the given type. +#[derive(Deref, DerefMut)] +pub struct WeakEntity { + #[deref] + #[deref_mut] + any_entity: AnyWeakEntity, + entity_type: PhantomData T>, +} + +impl std::fmt::Debug for WeakEntity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct(type_name::()) + .field("entity_id", &self.any_entity.entity_id) + .field("entity_type", &type_name::()) + .finish() + } +} + +impl Clone for WeakEntity { + fn clone(&self) -> Self { + Self { + any_entity: self.any_entity.clone(), + entity_type: self.entity_type, + } + } +} + +impl WeakEntity { + /// Upgrade this weak entity reference into a strong entity reference + pub fn upgrade(&self) -> Option> { + Some(Entity { + any_entity: self.any_entity.upgrade()?, + entity_type: self.entity_type, + }) + } + + /// Updates the entity referenced by this handle with the given function if + /// the referenced entity still exists. Returns an error if the entity has + /// been released. + pub fn update( + &self, + cx: &mut C, + update: impl FnOnce(&mut T, &mut Context) -> R, + ) -> Result + where + C: AppContext, + Result>: crate::Flatten, + { + crate::Flatten::flatten( + self.upgrade() + .context("entity released") + .map(|this| cx.update_entity(&this, update)), + ) + } + + /// Updates the entity referenced by this handle with the given function if + /// the referenced entity still exists, within a visual context that has a window. + /// Returns an error if the entity has been released. + pub fn update_in( + &self, + cx: &mut C, + update: impl FnOnce(&mut T, &mut Window, &mut Context) -> R, + ) -> Result + where + C: VisualContext, + Result>: crate::Flatten, + { + let window = cx.window_handle(); + let this = self.upgrade().context("entity released")?; + + crate::Flatten::flatten(window.update(cx, |_, window, cx| { + this.update(cx, |entity, cx| update(entity, window, cx)) + })) + } + + /// Reads the entity referenced by this handle with the given function if + /// the referenced entity still exists. Returns an error if the entity has + /// been released. + pub fn read_with(&self, cx: &C, read: impl FnOnce(&T, &App) -> R) -> Result + where + C: AppContext, + Result>: crate::Flatten, + { + crate::Flatten::flatten( + self.upgrade() + .context("entity released") + .map(|this| cx.read_entity(&this, read)), + ) + } + + /// Create a new weak entity that can never be upgraded. + pub fn new_invalid() -> Self { + Self { + any_entity: AnyWeakEntity::new_invalid(), + entity_type: PhantomData, + } + } +} + +impl Hash for WeakEntity { + fn hash(&self, state: &mut H) { + self.any_entity.hash(state); + } +} + +impl PartialEq for WeakEntity { + fn eq(&self, other: &Self) -> bool { + self.any_entity == other.any_entity + } +} + +impl Eq for WeakEntity {} + +impl PartialEq> for WeakEntity { + fn eq(&self, other: &Entity) -> bool { + self.entity_id() == other.any_entity.entity_id() + } +} + +impl Ord for WeakEntity { + fn cmp(&self, other: &Self) -> Ordering { + self.entity_id().cmp(&other.entity_id()) + } +} + +impl PartialOrd for WeakEntity { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[cfg(any(test, feature = "leak-detection"))] +static LEAK_BACKTRACE: std::sync::LazyLock = + std::sync::LazyLock::new(|| std::env::var("LEAK_BACKTRACE").is_ok_and(|b| !b.is_empty())); + +#[cfg(any(test, feature = "leak-detection"))] +#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)] +pub(crate) struct HandleId { + id: u64, // id of the handle itself, not the pointed at object +} + +#[cfg(any(test, feature = "leak-detection"))] +pub(crate) struct LeakDetector { + next_handle_id: u64, + entity_handles: HashMap>>, +} + +#[cfg(any(test, feature = "leak-detection"))] +impl LeakDetector { + #[track_caller] + pub fn handle_created(&mut self, entity_id: EntityId) -> HandleId { + let id = util::post_inc(&mut self.next_handle_id); + let handle_id = HandleId { id }; + let handles = self.entity_handles.entry(entity_id).or_default(); + handles.insert( + handle_id, + LEAK_BACKTRACE.then(backtrace::Backtrace::new_unresolved), + ); + handle_id + } + + pub fn handle_released(&mut self, entity_id: EntityId, handle_id: HandleId) { + let handles = self.entity_handles.entry(entity_id).or_default(); + handles.remove(&handle_id); + } + + pub fn assert_released(&mut self, entity_id: EntityId) { + let handles = self.entity_handles.entry(entity_id).or_default(); + if !handles.is_empty() { + for backtrace in handles.values_mut() { + if let Some(mut backtrace) = backtrace.take() { + backtrace.resolve(); + eprintln!("Leaked handle: {:#?}", backtrace); + } else { + eprintln!("Leaked handle: export LEAK_BACKTRACE to find allocation site"); + } + } + panic!(); + } + } +} + +#[cfg(test)] +mod test { + use crate::EntityMap; + + struct TestEntity { + pub i: i32, + } + + #[test] + fn test_entity_map_slot_assignment_before_cleanup() { + // Tests that slots are not re-used before take_dropped. + let mut entity_map = EntityMap::new(); + + let slot = entity_map.reserve::(); + entity_map.insert(slot, TestEntity { i: 1 }); + + let slot = entity_map.reserve::(); + entity_map.insert(slot, TestEntity { i: 2 }); + + let dropped = entity_map.take_dropped(); + assert_eq!(dropped.len(), 2); + + assert_eq!( + dropped + .into_iter() + .map(|(_, entity)| entity.downcast::().unwrap().i) + .collect::>(), + vec![1, 2], + ); + } + + #[test] + fn test_entity_map_weak_upgrade_before_cleanup() { + // Tests that weak handles are not upgraded before take_dropped + let mut entity_map = EntityMap::new(); + + let slot = entity_map.reserve::(); + let handle = entity_map.insert(slot, TestEntity { i: 1 }); + let weak = handle.downgrade(); + drop(handle); + + let strong = weak.upgrade(); + assert_eq!(strong, None); + + let dropped = entity_map.take_dropped(); + assert_eq!(dropped.len(), 1); + + assert_eq!( + dropped + .into_iter() + .map(|(_, entity)| entity.downcast::().unwrap().i) + .collect::>(), + vec![1], + ); + } +} diff --git a/third_party/gpui/src/app/test_context.rs b/third_party/gpui/src/app/test_context.rs new file mode 100644 index 0000000..d974823 --- /dev/null +++ b/third_party/gpui/src/app/test_context.rs @@ -0,0 +1,1047 @@ +use crate::{ + Action, AnyView, AnyWindowHandle, App, AppCell, AppContext, AsyncApp, AvailableSpace, + BackgroundExecutor, BorrowAppContext, Bounds, Capslock, ClipboardItem, DrawPhase, Drawable, + Element, Empty, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Modifiers, + ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, + Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, + TestScreenCaptureSource, TestWindow, TextSystem, VisualContext, Window, WindowBounds, + WindowHandle, WindowOptions, +}; +use anyhow::{anyhow, bail}; +use futures::{Stream, StreamExt, channel::oneshot}; +use rand::{SeedableRng, rngs::StdRng}; +use std::{cell::RefCell, future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration}; + +/// A TestAppContext is provided to tests created with `#[gpui::test]`, it provides +/// an implementation of `Context` with additional methods that are useful in tests. +#[derive(Clone)] +pub struct TestAppContext { + #[doc(hidden)] + pub app: Rc, + #[doc(hidden)] + pub background_executor: BackgroundExecutor, + #[doc(hidden)] + pub foreground_executor: ForegroundExecutor, + #[doc(hidden)] + pub dispatcher: TestDispatcher, + test_platform: Rc, + text_system: Arc, + fn_name: Option<&'static str>, + on_quit: Rc>>>, +} + +impl AppContext for TestAppContext { + type Result = T; + + fn new( + &mut self, + build_entity: impl FnOnce(&mut Context) -> T, + ) -> Self::Result> { + let mut app = self.app.borrow_mut(); + app.new(build_entity) + } + + fn reserve_entity(&mut self) -> Self::Result> { + let mut app = self.app.borrow_mut(); + app.reserve_entity() + } + + fn insert_entity( + &mut self, + reservation: crate::Reservation, + build_entity: impl FnOnce(&mut Context) -> T, + ) -> Self::Result> { + let mut app = self.app.borrow_mut(); + app.insert_entity(reservation, build_entity) + } + + fn update_entity( + &mut self, + handle: &Entity, + update: impl FnOnce(&mut T, &mut Context) -> R, + ) -> Self::Result { + let mut app = self.app.borrow_mut(); + app.update_entity(handle, update) + } + + fn as_mut<'a, T>(&'a mut self, _: &Entity) -> Self::Result> + where + T: 'static, + { + panic!("Cannot use as_mut with a test app context. Try calling update() first") + } + + fn read_entity( + &self, + handle: &Entity, + read: impl FnOnce(&T, &App) -> R, + ) -> Self::Result + where + T: 'static, + { + let app = self.app.borrow(); + app.read_entity(handle, read) + } + + fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result + where + F: FnOnce(AnyView, &mut Window, &mut App) -> T, + { + let mut lock = self.app.borrow_mut(); + lock.update_window(window, f) + } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(Entity, &App) -> R, + ) -> Result + where + T: 'static, + { + let app = self.app.borrow(); + app.read_window(window, read) + } + + fn background_spawn(&self, future: impl Future + Send + 'static) -> Task + where + R: Send + 'static, + { + self.background_executor.spawn(future) + } + + fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> Self::Result + where + G: Global, + { + let app = self.app.borrow(); + app.read_global(callback) + } +} + +impl TestAppContext { + /// Creates a new `TestAppContext`. Usually you can rely on `#[gpui::test]` to do this for you. + pub fn build(dispatcher: TestDispatcher, fn_name: Option<&'static str>) -> Self { + let arc_dispatcher = Arc::new(dispatcher.clone()); + let background_executor = BackgroundExecutor::new(arc_dispatcher.clone()); + let foreground_executor = ForegroundExecutor::new(arc_dispatcher); + let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone()); + let asset_source = Arc::new(()); + let http_client = http_client::FakeHttpClient::with_404_response(); + let text_system = Arc::new(TextSystem::new(platform.text_system())); + + Self { + app: App::new_app(platform.clone(), asset_source, http_client), + background_executor, + foreground_executor, + dispatcher, + test_platform: platform, + text_system, + fn_name, + on_quit: Rc::new(RefCell::new(Vec::default())), + } + } + + /// Create a single TestAppContext, for non-multi-client tests + pub fn single() -> Self { + let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0)); + Self::build(dispatcher, None) + } + + /// The name of the test function that created this `TestAppContext` + pub fn test_function_name(&self) -> Option<&'static str> { + self.fn_name + } + + /// Checks whether there have been any new path prompts received by the platform. + pub fn did_prompt_for_new_path(&self) -> bool { + self.test_platform.did_prompt_for_new_path() + } + + /// returns a new `TestAppContext` re-using the same executors to interleave tasks. + pub fn new_app(&self) -> TestAppContext { + Self::build(self.dispatcher.clone(), self.fn_name) + } + + /// Called by the test helper to end the test. + /// public so the macro can call it. + pub fn quit(&self) { + self.on_quit.borrow_mut().drain(..).for_each(|f| f()); + self.app.borrow_mut().shutdown(); + } + + /// Register cleanup to run when the test ends. + pub fn on_quit(&mut self, f: impl FnOnce() + 'static) { + self.on_quit.borrow_mut().push(Box::new(f)); + } + + /// Schedules all windows to be redrawn on the next effect cycle. + pub fn refresh(&mut self) -> Result<()> { + let mut app = self.app.borrow_mut(); + app.refresh_windows(); + Ok(()) + } + + /// Returns an executor (for running tasks in the background) + pub fn executor(&self) -> BackgroundExecutor { + self.background_executor.clone() + } + + /// Returns an executor (for running tasks on the main thread) + pub fn foreground_executor(&self) -> &ForegroundExecutor { + &self.foreground_executor + } + + #[expect(clippy::wrong_self_convention)] + fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity { + let mut cx = self.app.borrow_mut(); + cx.new(build_entity) + } + + /// Gives you an `&mut App` for the duration of the closure + pub fn update(&self, f: impl FnOnce(&mut App) -> R) -> R { + let mut cx = self.app.borrow_mut(); + cx.update(f) + } + + /// Gives you an `&App` for the duration of the closure + pub fn read(&self, f: impl FnOnce(&App) -> R) -> R { + let cx = self.app.borrow(); + f(&cx) + } + + /// Adds a new window. The Window will always be backed by a `TestWindow` which + /// can be retrieved with `self.test_window(handle)` + pub fn add_window(&mut self, build_window: F) -> WindowHandle + where + F: FnOnce(&mut Window, &mut Context) -> V, + V: 'static + Render, + { + let mut cx = self.app.borrow_mut(); + + // Some tests rely on the window size matching the bounds of the test display + let bounds = Bounds::maximized(None, &cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| cx.new(|cx| build_window(window, cx)), + ) + .unwrap() + } + + /// Adds a new window with no content. + pub fn add_empty_window(&mut self) -> &mut VisualTestContext { + let mut cx = self.app.borrow_mut(); + let bounds = Bounds::maximized(None, &cx); + let window = cx + .open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| Empty), + ) + .unwrap(); + drop(cx); + let cx = VisualTestContext::from_window(*window.deref(), self).into_mut(); + cx.run_until_parked(); + cx + } + + /// Adds a new window, and returns its root view and a `VisualTestContext` which can be used + /// as a `Window` and `App` for the rest of the test. Typically you would shadow this context with + /// the returned one. `let (view, cx) = cx.add_window_view(...);` + pub fn add_window_view( + &mut self, + build_root_view: F, + ) -> (Entity, &mut VisualTestContext) + where + F: FnOnce(&mut Window, &mut Context) -> V, + V: 'static + Render, + { + let mut cx = self.app.borrow_mut(); + let bounds = Bounds::maximized(None, &cx); + let window = cx + .open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| cx.new(|cx| build_root_view(window, cx)), + ) + .unwrap(); + drop(cx); + let view = window.root(self).unwrap(); + let cx = VisualTestContext::from_window(*window.deref(), self).into_mut(); + cx.run_until_parked(); + + // it might be nice to try and cleanup these at the end of each test. + (view, cx) + } + + /// returns the TextSystem + pub fn text_system(&self) -> &Arc { + &self.text_system + } + + /// Simulates writing to the platform clipboard + pub fn write_to_clipboard(&self, item: ClipboardItem) { + self.test_platform.write_to_clipboard(item) + } + + /// Simulates reading from the platform clipboard. + /// This will return the most recent value from `write_to_clipboard`. + pub fn read_from_clipboard(&self) -> Option { + self.test_platform.read_from_clipboard() + } + + /// Simulates choosing a File in the platform's "Open" dialog. + pub fn simulate_new_path_selection( + &self, + select_path: impl FnOnce(&std::path::Path) -> Option, + ) { + self.test_platform.simulate_new_path_selection(select_path); + } + + /// Simulates clicking a button in an platform-level alert dialog. + #[track_caller] + pub fn simulate_prompt_answer(&self, button: &str) { + self.test_platform.simulate_prompt_answer(button); + } + + /// Returns true if there's an alert dialog open. + pub fn has_pending_prompt(&self) -> bool { + self.test_platform.has_pending_prompt() + } + + /// Returns true if there's an alert dialog open. + pub fn pending_prompt(&self) -> Option<(String, String)> { + self.test_platform.pending_prompt() + } + + /// All the urls that have been opened with cx.open_url() during this test. + pub fn opened_url(&self) -> Option { + self.test_platform.opened_url.borrow().clone() + } + + /// Simulates the user resizing the window to the new size. + pub fn simulate_window_resize(&self, window_handle: AnyWindowHandle, size: Size) { + self.test_window(window_handle).simulate_resize(size); + } + + /// Causes the given sources to be returned if the application queries for screen + /// capture sources. + pub fn set_screen_capture_sources(&self, sources: Vec) { + self.test_platform.set_screen_capture_sources(sources); + } + + /// Returns all windows open in the test. + pub fn windows(&self) -> Vec { + self.app.borrow().windows() + } + + /// Run the given task on the main thread. + #[track_caller] + pub fn spawn(&self, f: impl FnOnce(AsyncApp) -> Fut) -> Task + where + Fut: Future + 'static, + R: 'static, + { + self.foreground_executor.spawn(f(self.to_async())) + } + + /// true if the given global is defined + pub fn has_global(&self) -> bool { + let app = self.app.borrow(); + app.has_global::() + } + + /// runs the given closure with a reference to the global + /// panics if `has_global` would return false. + pub fn read_global(&self, read: impl FnOnce(&G, &App) -> R) -> R { + let app = self.app.borrow(); + read(app.global(), &app) + } + + /// runs the given closure with a reference to the global (if set) + pub fn try_read_global(&self, read: impl FnOnce(&G, &App) -> R) -> Option { + let lock = self.app.borrow(); + Some(read(lock.try_global()?, &lock)) + } + + /// sets the global in this context. + pub fn set_global(&mut self, global: G) { + let mut lock = self.app.borrow_mut(); + lock.update(|cx| cx.set_global(global)) + } + + /// updates the global in this context. (panics if `has_global` would return false) + pub fn update_global(&mut self, update: impl FnOnce(&mut G, &mut App) -> R) -> R { + let mut lock = self.app.borrow_mut(); + lock.update(|cx| cx.update_global(update)) + } + + /// Returns an `AsyncApp` which can be used to run tasks that expect to be on a background + /// thread on the current thread in tests. + pub fn to_async(&self) -> AsyncApp { + AsyncApp { + app: Rc::downgrade(&self.app), + background_executor: self.background_executor.clone(), + foreground_executor: self.foreground_executor.clone(), + } + } + + /// Wait until there are no more pending tasks. + pub fn run_until_parked(&mut self) { + self.background_executor.run_until_parked() + } + + /// Simulate dispatching an action to the currently focused node in the window. + pub fn dispatch_action(&mut self, window: AnyWindowHandle, action: A) + where + A: Action, + { + window + .update(self, |_, window, cx| { + window.dispatch_action(action.boxed_clone(), cx) + }) + .unwrap(); + + self.background_executor.run_until_parked() + } + + /// simulate_keystrokes takes a space-separated list of keys to type. + /// cx.simulate_keystrokes("cmd-shift-p b k s p enter") + /// in Zed, this will run backspace on the current editor through the command palette. + /// This will also run the background executor until it's parked. + pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) { + for keystroke in keystrokes + .split(' ') + .map(Keystroke::parse) + .map(Result::unwrap) + { + self.dispatch_keystroke(window, keystroke); + } + + self.background_executor.run_until_parked() + } + + /// simulate_input takes a string of text to type. + /// cx.simulate_input("abc") + /// will type abc into your current editor + /// This will also run the background executor until it's parked. + pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) { + for keystroke in input.split("").map(Keystroke::parse).map(Result::unwrap) { + self.dispatch_keystroke(window, keystroke); + } + + self.background_executor.run_until_parked() + } + + /// dispatches a single Keystroke (see also `simulate_keystrokes` and `simulate_input`) + pub fn dispatch_keystroke(&mut self, window: AnyWindowHandle, keystroke: Keystroke) { + self.update_window(window, |_, window, cx| { + window.dispatch_keystroke(keystroke, cx) + }) + .unwrap(); + } + + /// Returns the `TestWindow` backing the given handle. + pub(crate) fn test_window(&self, window: AnyWindowHandle) -> TestWindow { + self.app + .borrow_mut() + .windows + .get_mut(window.id) + .unwrap() + .as_deref_mut() + .unwrap() + .platform_window + .as_test() + .unwrap() + .clone() + } + + /// Returns a stream of notifications whenever the Entity is updated. + pub fn notifications( + &mut self, + entity: &Entity, + ) -> impl Stream + use { + let (tx, rx) = futures::channel::mpsc::unbounded(); + self.update(|cx| { + cx.observe(entity, { + let tx = tx.clone(); + move |_, _| { + let _ = tx.unbounded_send(()); + } + }) + .detach(); + cx.observe_release(entity, move |_, _| tx.close_channel()) + .detach() + }); + rx + } + + /// Returns a stream of events emitted by the given Entity. + pub fn events>( + &mut self, + entity: &Entity, + ) -> futures::channel::mpsc::UnboundedReceiver + where + Evt: 'static + Clone, + { + let (tx, rx) = futures::channel::mpsc::unbounded(); + entity + .update(self, |_, cx: &mut Context| { + cx.subscribe(entity, move |_entity, _handle, event, _cx| { + let _ = tx.unbounded_send(event.clone()); + }) + }) + .detach(); + rx + } + + /// Runs until the given condition becomes true. (Prefer `run_until_parked` if you + /// don't need to jump in at a specific time). + pub async fn condition( + &mut self, + entity: &Entity, + mut predicate: impl FnMut(&mut T, &mut Context) -> bool, + ) { + let timer = self.executor().timer(Duration::from_secs(3)); + let mut notifications = self.notifications(entity); + + use futures::FutureExt as _; + use smol::future::FutureExt as _; + + async { + loop { + if entity.update(self, &mut predicate) { + return Ok(()); + } + + if notifications.next().await.is_none() { + bail!("entity dropped") + } + } + } + .race(timer.map(|_| Err(anyhow!("condition timed out")))) + .await + .unwrap(); + } + + /// Set a name for this App. + #[cfg(any(test, feature = "test-support"))] + pub fn set_name(&mut self, name: &'static str) { + self.update(|cx| cx.name = Some(name)) + } +} + +impl Entity { + /// Block until the next event is emitted by the entity, then return it. + pub fn next_event(&self, cx: &mut TestAppContext) -> impl Future + where + Event: Send + Clone + 'static, + T: EventEmitter, + { + let (tx, mut rx) = oneshot::channel(); + let mut tx = Some(tx); + let subscription = self.update(cx, |_, cx| { + cx.subscribe(self, move |_, _, event, _| { + if let Some(tx) = tx.take() { + _ = tx.send(event.clone()); + } + }) + }); + + async move { + let event = rx.await.expect("no event emitted"); + drop(subscription); + event + } + } +} + +impl Entity { + /// Returns a future that resolves when the view is next updated. + pub fn next_notification( + &self, + advance_clock_by: Duration, + cx: &TestAppContext, + ) -> impl Future { + use postage::prelude::{Sink as _, Stream as _}; + + let (mut tx, mut rx) = postage::mpsc::channel(1); + let subscription = cx.app.borrow_mut().observe(self, move |_, _| { + tx.try_send(()).ok(); + }); + + let duration = if std::env::var("CI").is_ok() { + Duration::from_secs(5) + } else { + Duration::from_secs(1) + }; + + cx.executor().advance_clock(advance_clock_by); + + async move { + let notification = crate::util::smol_timeout(duration, rx.recv()) + .await + .expect("next notification timed out"); + drop(subscription); + notification.expect("entity dropped while test was waiting for its next notification") + } + } +} + +impl Entity { + /// Returns a future that resolves when the condition becomes true. + pub fn condition( + &self, + cx: &TestAppContext, + mut predicate: impl FnMut(&V, &App) -> bool, + ) -> impl Future + where + Evt: 'static, + V: EventEmitter, + { + use postage::prelude::{Sink as _, Stream as _}; + + let (tx, mut rx) = postage::mpsc::channel(1024); + + let mut cx = cx.app.borrow_mut(); + let subscriptions = ( + cx.observe(self, { + let mut tx = tx.clone(); + move |_, _| { + tx.blocking_send(()).ok(); + } + }), + cx.subscribe(self, { + let mut tx = tx; + move |_, _: &Evt, _| { + tx.blocking_send(()).ok(); + } + }), + ); + + let cx = cx.this.upgrade().unwrap(); + let handle = self.downgrade(); + + async move { + crate::util::smol_timeout(Duration::from_secs(1), async move { + loop { + { + let cx = cx.borrow(); + let cx = &*cx; + if predicate( + handle + .upgrade() + .expect("view dropped with pending condition") + .read(cx), + cx, + ) { + break; + } + } + + cx.borrow().background_executor().start_waiting(); + rx.recv() + .await + .expect("view dropped with pending condition"); + cx.borrow().background_executor().finish_waiting(); + } + }) + .await + .expect("condition timed out"); + drop(subscriptions); + } + } +} + +use derive_more::{Deref, DerefMut}; + +use super::{Context, Entity}; +#[derive(Deref, DerefMut, Clone)] +/// A VisualTestContext is the test-equivalent of a `Window` and `App`. It allows you to +/// run window-specific test code. It can be dereferenced to a `TextAppContext`. +pub struct VisualTestContext { + #[deref] + #[deref_mut] + /// cx is the original TestAppContext (you can more easily access this using Deref) + pub cx: TestAppContext, + window: AnyWindowHandle, +} + +impl VisualTestContext { + /// Provides a `Window` and `App` for the duration of the closure. + pub fn update(&mut self, f: impl FnOnce(&mut Window, &mut App) -> R) -> R { + self.cx + .update_window(self.window, |_, window, cx| f(window, cx)) + .unwrap() + } + + /// Creates a new VisualTestContext. You would typically shadow the passed in + /// TestAppContext with this, as this is typically more useful. + /// `let cx = VisualTestContext::from_window(window, cx);` + pub fn from_window(window: AnyWindowHandle, cx: &TestAppContext) -> Self { + Self { + cx: cx.clone(), + window, + } + } + + /// Wait until there are no more pending tasks. + pub fn run_until_parked(&self) { + self.cx.background_executor.run_until_parked(); + } + + /// Dispatch the action to the currently focused node. + pub fn dispatch_action(&mut self, action: A) + where + A: Action, + { + self.cx.dispatch_action(self.window, action) + } + + /// Read the title off the window (set by `Window#set_window_title`) + pub fn window_title(&mut self) -> Option { + self.cx.test_window(self.window).0.lock().title.clone() + } + + /// Simulate a sequence of keystrokes `cx.simulate_keystrokes("cmd-p escape")` + /// Automatically runs until parked. + pub fn simulate_keystrokes(&mut self, keystrokes: &str) { + self.cx.simulate_keystrokes(self.window, keystrokes) + } + + /// Simulate typing text `cx.simulate_input("hello")` + /// Automatically runs until parked. + pub fn simulate_input(&mut self, input: &str) { + self.cx.simulate_input(self.window, input) + } + + /// Simulate a mouse move event to the given point + pub fn simulate_mouse_move( + &mut self, + position: Point, + button: impl Into>, + modifiers: Modifiers, + ) { + self.simulate_event(MouseMoveEvent { + position, + modifiers, + pressed_button: button.into(), + }) + } + + /// Simulate a mouse down event to the given point + pub fn simulate_mouse_down( + &mut self, + position: Point, + button: MouseButton, + modifiers: Modifiers, + ) { + self.simulate_event(MouseDownEvent { + position, + modifiers, + button, + click_count: 1, + first_mouse: false, + }) + } + + /// Simulate a mouse up event to the given point + pub fn simulate_mouse_up( + &mut self, + position: Point, + button: MouseButton, + modifiers: Modifiers, + ) { + self.simulate_event(MouseUpEvent { + position, + modifiers, + button, + click_count: 1, + }) + } + + /// Simulate a primary mouse click at the given point + pub fn simulate_click(&mut self, position: Point, modifiers: Modifiers) { + self.simulate_event(MouseDownEvent { + position, + modifiers, + button: MouseButton::Left, + click_count: 1, + first_mouse: false, + }); + self.simulate_event(MouseUpEvent { + position, + modifiers, + button: MouseButton::Left, + click_count: 1, + }); + } + + /// Simulate a modifiers changed event + pub fn simulate_modifiers_change(&mut self, modifiers: Modifiers) { + self.simulate_event(ModifiersChangedEvent { + modifiers, + capslock: Capslock { on: false }, + }) + } + + /// Simulate a capslock changed event + pub fn simulate_capslock_change(&mut self, on: bool) { + self.simulate_event(ModifiersChangedEvent { + modifiers: Modifiers::none(), + capslock: Capslock { on }, + }) + } + + /// Simulates the user resizing the window to the new size. + pub fn simulate_resize(&self, size: Size) { + self.simulate_window_resize(self.window, size) + } + + /// debug_bounds returns the bounds of the element with the given selector. + pub fn debug_bounds(&mut self, selector: &'static str) -> Option> { + self.update(|window, _| window.rendered_frame.debug_bounds.get(selector).copied()) + } + + /// Draw an element to the window. Useful for simulating events or actions + pub fn draw( + &mut self, + origin: Point, + space: impl Into>, + f: impl FnOnce(&mut Window, &mut App) -> E, + ) -> (E::RequestLayoutState, E::PrepaintState) + where + E: Element, + { + self.update(|window, cx| { + window.invalidator.set_phase(DrawPhase::Prepaint); + let mut element = Drawable::new(f(window, cx)); + element.layout_as_root(space.into(), window, cx); + window.with_absolute_element_offset(origin, |window| element.prepaint(window, cx)); + + window.invalidator.set_phase(DrawPhase::Paint); + let (request_layout_state, prepaint_state) = element.paint(window, cx); + + window.invalidator.set_phase(DrawPhase::None); + window.refresh(); + + (request_layout_state, prepaint_state) + }) + } + + /// Simulate an event from the platform, e.g. a ScrollWheelEvent + /// Make sure you've called [VisualTestContext::draw] first! + pub fn simulate_event(&mut self, event: E) { + self.test_window(self.window) + .simulate_input(event.to_platform_input()); + self.background_executor.run_until_parked(); + } + + /// Simulates the user blurring the window. + pub fn deactivate_window(&mut self) { + if Some(self.window) == self.test_platform.active_window() { + self.test_platform.set_active_window(None) + } + self.background_executor.run_until_parked(); + } + + /// Simulates the user closing the window. + /// Returns true if the window was closed. + pub fn simulate_close(&mut self) -> bool { + let handler = self + .cx + .update_window(self.window, |_, window, _| { + window + .platform_window + .as_test() + .unwrap() + .0 + .lock() + .should_close_handler + .take() + }) + .unwrap(); + if let Some(mut handler) = handler { + let should_close = handler(); + self.cx + .update_window(self.window, |_, window, _| { + window.platform_window.on_should_close(handler); + }) + .unwrap(); + should_close + } else { + false + } + } + + /// Get an &mut VisualTestContext (which is mostly what you need to pass to other methods). + /// This method internally retains the VisualTestContext until the end of the test. + pub fn into_mut(self) -> &'static mut Self { + let ptr = Box::into_raw(Box::new(self)); + // safety: on_quit will be called after the test has finished. + // the executor will ensure that all tasks related to the test have stopped. + // so there is no way for cx to be accessed after on_quit is called. + // todo: This is unsound under stacked borrows (also tree borrows probably?) + // the mutable reference invalidates `ptr` which is later used in the closure + let cx = unsafe { &mut *ptr }; + cx.on_quit(move || unsafe { + drop(Box::from_raw(ptr)); + }); + cx + } +} + +impl AppContext for VisualTestContext { + type Result = ::Result; + + fn new( + &mut self, + build_entity: impl FnOnce(&mut Context) -> T, + ) -> Self::Result> { + self.cx.new(build_entity) + } + + fn reserve_entity(&mut self) -> Self::Result> { + self.cx.reserve_entity() + } + + fn insert_entity( + &mut self, + reservation: crate::Reservation, + build_entity: impl FnOnce(&mut Context) -> T, + ) -> Self::Result> { + self.cx.insert_entity(reservation, build_entity) + } + + fn update_entity( + &mut self, + handle: &Entity, + update: impl FnOnce(&mut T, &mut Context) -> R, + ) -> Self::Result + where + T: 'static, + { + self.cx.update_entity(handle, update) + } + + fn as_mut<'a, T>(&'a mut self, handle: &Entity) -> Self::Result> + where + T: 'static, + { + self.cx.as_mut(handle) + } + + fn read_entity( + &self, + handle: &Entity, + read: impl FnOnce(&T, &App) -> R, + ) -> Self::Result + where + T: 'static, + { + self.cx.read_entity(handle, read) + } + + fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result + where + F: FnOnce(AnyView, &mut Window, &mut App) -> T, + { + self.cx.update_window(window, f) + } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(Entity, &App) -> R, + ) -> Result + where + T: 'static, + { + self.cx.read_window(window, read) + } + + fn background_spawn(&self, future: impl Future + Send + 'static) -> Task + where + R: Send + 'static, + { + self.cx.background_spawn(future) + } + + fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> Self::Result + where + G: Global, + { + self.cx.read_global(callback) + } +} + +impl VisualContext for VisualTestContext { + /// Get the underlying window handle underlying this context. + fn window_handle(&self) -> AnyWindowHandle { + self.window + } + + fn new_window_entity( + &mut self, + build_entity: impl FnOnce(&mut Window, &mut Context) -> T, + ) -> Self::Result> { + self.window + .update(&mut self.cx, |_, window, cx| { + cx.new(|cx| build_entity(window, cx)) + }) + .unwrap() + } + + fn update_window_entity( + &mut self, + view: &Entity, + update: impl FnOnce(&mut V, &mut Window, &mut Context) -> R, + ) -> Self::Result { + self.window + .update(&mut self.cx, |_, window, cx| { + view.update(cx, |v, cx| update(v, window, cx)) + }) + .unwrap() + } + + fn replace_root_view( + &mut self, + build_view: impl FnOnce(&mut Window, &mut Context) -> V, + ) -> Self::Result> + where + V: 'static + Render, + { + self.window + .update(&mut self.cx, |_, window, cx| { + window.replace_root(cx, build_view) + }) + .unwrap() + } + + fn focus(&mut self, view: &Entity) -> Self::Result<()> { + self.window + .update(&mut self.cx, |_, window, cx| { + view.read(cx).focus_handle(cx).focus(window) + }) + .unwrap() + } +} + +impl AnyWindowHandle { + /// Creates the given view in this window. + pub fn build_entity( + &self, + cx: &mut TestAppContext, + build_view: impl FnOnce(&mut Window, &mut Context) -> V, + ) -> Entity { + self.update(cx, |_, window, cx| cx.new(|cx| build_view(window, cx))) + .unwrap() + } +} diff --git a/third_party/gpui/src/arena.rs b/third_party/gpui/src/arena.rs new file mode 100644 index 0000000..9898c80 --- /dev/null +++ b/third_party/gpui/src/arena.rs @@ -0,0 +1,289 @@ +use std::{ + alloc::{self, handle_alloc_error}, + cell::Cell, + num::NonZeroUsize, + ops::{Deref, DerefMut}, + ptr::{self, NonNull}, + rc::Rc, +}; + +struct ArenaElement { + value: *mut u8, + drop: unsafe fn(*mut u8), +} + +impl Drop for ArenaElement { + #[inline(always)] + fn drop(&mut self) { + unsafe { (self.drop)(self.value) }; + } +} + +struct Chunk { + start: *mut u8, + end: *mut u8, + offset: *mut u8, +} + +impl Drop for Chunk { + fn drop(&mut self) { + unsafe { + let chunk_size = self.end.offset_from_unsigned(self.start); + // SAFETY: This succeeded during allocation. + let layout = alloc::Layout::from_size_align_unchecked(chunk_size, 1); + alloc::dealloc(self.start, layout); + } + } +} + +impl Chunk { + fn new(chunk_size: NonZeroUsize) -> Self { + // this only fails if chunk_size is unreasonably huge + let layout = alloc::Layout::from_size_align(chunk_size.get(), 1).unwrap(); + let start = unsafe { alloc::alloc(layout) }; + if start.is_null() { + handle_alloc_error(layout); + } + let end = unsafe { start.add(chunk_size.get()) }; + Self { + start, + end, + offset: start, + } + } + + fn allocate(&mut self, layout: alloc::Layout) -> Option> { + let aligned = unsafe { self.offset.add(self.offset.align_offset(layout.align())) }; + let next = unsafe { aligned.add(layout.size()) }; + + if next <= self.end { + self.offset = next; + NonNull::new(aligned) + } else { + None + } + } + + fn reset(&mut self) { + self.offset = self.start; + } +} + +pub struct Arena { + chunks: Vec, + elements: Vec, + valid: Rc>, + current_chunk_index: usize, + chunk_size: NonZeroUsize, +} + +impl Drop for Arena { + fn drop(&mut self) { + self.clear(); + } +} + +impl Arena { + pub fn new(chunk_size: usize) -> Self { + let chunk_size = NonZeroUsize::try_from(chunk_size).unwrap(); + Self { + chunks: vec![Chunk::new(chunk_size)], + elements: Vec::new(), + valid: Rc::new(Cell::new(true)), + current_chunk_index: 0, + chunk_size, + } + } + + pub fn capacity(&self) -> usize { + self.chunks.len() * self.chunk_size.get() + } + + pub fn clear(&mut self) { + self.valid.set(false); + self.valid = Rc::new(Cell::new(true)); + self.elements.clear(); + for chunk_index in 0..=self.current_chunk_index { + self.chunks[chunk_index].reset(); + } + self.current_chunk_index = 0; + } + + #[inline(always)] + pub fn alloc(&mut self, f: impl FnOnce() -> T) -> ArenaBox { + #[inline(always)] + unsafe fn inner_writer(ptr: *mut T, f: F) + where + F: FnOnce() -> T, + { + unsafe { ptr::write(ptr, f()) }; + } + + unsafe fn drop(ptr: *mut u8) { + unsafe { std::ptr::drop_in_place(ptr.cast::()) }; + } + + let layout = alloc::Layout::new::(); + let mut current_chunk = &mut self.chunks[self.current_chunk_index]; + let ptr = if let Some(ptr) = current_chunk.allocate(layout) { + ptr.as_ptr() + } else { + self.current_chunk_index += 1; + if self.current_chunk_index >= self.chunks.len() { + self.chunks.push(Chunk::new(self.chunk_size)); + assert_eq!(self.current_chunk_index, self.chunks.len() - 1); + log::trace!( + "increased element arena capacity to {}kb", + self.capacity() / 1024, + ); + } + current_chunk = &mut self.chunks[self.current_chunk_index]; + if let Some(ptr) = current_chunk.allocate(layout) { + ptr.as_ptr() + } else { + panic!( + "Arena chunk_size of {} is too small to allocate {} bytes", + self.chunk_size, + layout.size() + ); + } + }; + + unsafe { inner_writer(ptr.cast(), f) }; + self.elements.push(ArenaElement { + value: ptr, + drop: drop::, + }); + + ArenaBox { + ptr: ptr.cast(), + valid: self.valid.clone(), + } + } +} + +pub struct ArenaBox { + ptr: *mut T, + valid: Rc>, +} + +impl ArenaBox { + #[inline(always)] + pub fn map(mut self, f: impl FnOnce(&mut T) -> &mut U) -> ArenaBox { + ArenaBox { + ptr: f(&mut self), + valid: self.valid, + } + } + + #[track_caller] + fn validate(&self) { + assert!( + self.valid.get(), + "attempted to dereference an ArenaRef after its Arena was cleared" + ); + } +} + +impl Deref for ArenaBox { + type Target = T; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + self.validate(); + unsafe { &*self.ptr } + } +} + +impl DerefMut for ArenaBox { + #[inline(always)] + fn deref_mut(&mut self) -> &mut Self::Target { + self.validate(); + unsafe { &mut *self.ptr } + } +} + +#[cfg(test)] +mod tests { + use std::{cell::Cell, rc::Rc}; + + use super::*; + + #[test] + fn test_arena() { + let mut arena = Arena::new(1024); + let a = arena.alloc(|| 1u64); + let b = arena.alloc(|| 2u32); + let c = arena.alloc(|| 3u16); + let d = arena.alloc(|| 4u8); + assert_eq!(*a, 1); + assert_eq!(*b, 2); + assert_eq!(*c, 3); + assert_eq!(*d, 4); + + arena.clear(); + let a = arena.alloc(|| 5u64); + let b = arena.alloc(|| 6u32); + let c = arena.alloc(|| 7u16); + let d = arena.alloc(|| 8u8); + assert_eq!(*a, 5); + assert_eq!(*b, 6); + assert_eq!(*c, 7); + assert_eq!(*d, 8); + + // Ensure drop gets called. + let dropped = Rc::new(Cell::new(false)); + struct DropGuard(Rc>); + impl Drop for DropGuard { + fn drop(&mut self) { + self.0.set(true); + } + } + arena.alloc(|| DropGuard(dropped.clone())); + arena.clear(); + assert!(dropped.get()); + } + + #[test] + fn test_arena_grow() { + let mut arena = Arena::new(8); + arena.alloc(|| 1u64); + arena.alloc(|| 2u64); + + assert_eq!(arena.capacity(), 16); + + arena.alloc(|| 3u32); + arena.alloc(|| 4u32); + + assert_eq!(arena.capacity(), 24); + } + + #[test] + fn test_arena_alignment() { + let mut arena = Arena::new(256); + let x1 = arena.alloc(|| 1u8); + let x2 = arena.alloc(|| 2u16); + let x3 = arena.alloc(|| 3u32); + let x4 = arena.alloc(|| 4u64); + let x5 = arena.alloc(|| 5u64); + + assert_eq!(*x1, 1); + assert_eq!(*x2, 2); + assert_eq!(*x3, 3); + assert_eq!(*x4, 4); + assert_eq!(*x5, 5); + + assert_eq!(x1.ptr.align_offset(std::mem::align_of_val(&*x1)), 0); + assert_eq!(x2.ptr.align_offset(std::mem::align_of_val(&*x2)), 0); + } + + #[test] + #[should_panic(expected = "attempted to dereference an ArenaRef after its Arena was cleared")] + fn test_arena_use_after_clear() { + let mut arena = Arena::new(16); + let value = arena.alloc(|| 1u64); + + arena.clear(); + let _read_value = *value; + } +} diff --git a/third_party/gpui/src/asset_cache.rs b/third_party/gpui/src/asset_cache.rs new file mode 100644 index 0000000..9afbba8 --- /dev/null +++ b/third_party/gpui/src/asset_cache.rs @@ -0,0 +1,84 @@ +use crate::{App, SharedString, SharedUri}; +use futures::{Future, TryFutureExt}; + +use std::fmt::Debug; +use std::hash::{Hash, Hasher}; +use std::marker::PhantomData; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +/// An enum representing +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub enum Resource { + /// This resource is at a given URI + Uri(SharedUri), + /// This resource is at a given path in the file system + Path(Arc), + /// This resource is embedded in the application binary + Embedded(SharedString), +} + +impl From for Resource { + fn from(value: SharedUri) -> Self { + Self::Uri(value) + } +} + +impl From for Resource { + fn from(value: PathBuf) -> Self { + Self::Path(value.into()) + } +} + +impl From> for Resource { + fn from(value: Arc) -> Self { + Self::Path(value) + } +} + +/// A trait for asynchronous asset loading. +pub trait Asset: 'static { + /// The source of the asset. + type Source: Clone + Hash + Send; + + /// The loaded asset + type Output: Clone + Send; + + /// Load the asset asynchronously + fn load( + source: Self::Source, + cx: &mut App, + ) -> impl Future + Send + 'static; +} + +/// An asset Loader which logs the [`Err`] variant of a [`Result`] during loading +pub enum AssetLogger { + #[doc(hidden)] + _Phantom(PhantomData, &'static dyn crate::seal::Sealed), +} + +impl Asset for AssetLogger +where + T: Asset>, + R: Clone + Send, + E: Clone + Send + std::fmt::Display, +{ + type Source = T::Source; + + type Output = T::Output; + + fn load( + source: Self::Source, + cx: &mut App, + ) -> impl Future + Send + 'static { + let load = T::load(source, cx); + load.inspect_err(|e| log::error!("Failed to load asset: {}", e)) + } +} + +/// Use a quick, non-cryptographically secure hash function to get an identifier from data +pub fn hash(data: &T) -> u64 { + let mut hasher = collections::FxHasher::default(); + data.hash(&mut hasher); + hasher.finish() +} diff --git a/third_party/gpui/src/assets.rs b/third_party/gpui/src/assets.rs new file mode 100644 index 0000000..8930b58 --- /dev/null +++ b/third_party/gpui/src/assets.rs @@ -0,0 +1,107 @@ +use crate::{DevicePixels, Pixels, Result, SharedString, Size, size}; +use smallvec::SmallVec; + +use image::{Delay, Frame}; +use std::{ + borrow::Cow, + fmt, + hash::Hash, + sync::atomic::{AtomicUsize, Ordering::SeqCst}, +}; + +/// A source of assets for this app to use. +pub trait AssetSource: 'static + Send + Sync { + /// Load the given asset from the source path. + fn load(&self, path: &str) -> Result>>; + + /// List the assets at the given path. + fn list(&self, path: &str) -> Result>; +} + +impl AssetSource for () { + fn load(&self, _path: &str) -> Result>> { + Ok(None) + } + + fn list(&self, _path: &str) -> Result> { + Ok(vec![]) + } +} + +/// A unique identifier for the image cache +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct ImageId(pub usize); + +#[derive(PartialEq, Eq, Hash, Clone)] +pub(crate) struct RenderImageParams { + pub(crate) image_id: ImageId, + pub(crate) frame_index: usize, +} + +/// A cached and processed image, in BGRA format +pub struct RenderImage { + /// The ID associated with this image + pub id: ImageId, + /// The scale factor of this image on render. + pub(crate) scale_factor: f32, + data: SmallVec<[Frame; 1]>, +} + +impl PartialEq for RenderImage { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for RenderImage {} + +impl RenderImage { + /// Create a new image from the given data. + pub fn new(data: impl Into>) -> Self { + static NEXT_ID: AtomicUsize = AtomicUsize::new(0); + + Self { + id: ImageId(NEXT_ID.fetch_add(1, SeqCst)), + scale_factor: 1.0, + data: data.into(), + } + } + + /// Convert this image into a byte slice. + pub fn as_bytes(&self, frame_index: usize) -> Option<&[u8]> { + self.data + .get(frame_index) + .map(|frame| frame.buffer().as_raw().as_slice()) + } + + /// Get the size of this image, in pixels. + pub fn size(&self, frame_index: usize) -> Size { + let (width, height) = self.data[frame_index].buffer().dimensions(); + size(width.into(), height.into()) + } + + /// Get the size of this image, in pixels for display, adjusted for the scale factor. + pub(crate) fn render_size(&self, frame_index: usize) -> Size { + self.size(frame_index) + .map(|v| (v.0 as f32 / self.scale_factor).into()) + } + + /// Get the delay of this frame from the previous + pub fn delay(&self, frame_index: usize) -> Delay { + self.data[frame_index].delay() + } + + /// Get the number of frames for this image. + pub fn frame_count(&self) -> usize { + self.data.len() + } +} + +impl fmt::Debug for RenderImage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ImageData") + .field("id", &self.id) + .field("size", &self.size(0)) + .finish() + } +} diff --git a/third_party/gpui/src/bounds_tree.rs b/third_party/gpui/src/bounds_tree.rs new file mode 100644 index 0000000..a96bfe5 --- /dev/null +++ b/third_party/gpui/src/bounds_tree.rs @@ -0,0 +1,337 @@ +use crate::{Bounds, Half}; +use std::{ + cmp, + fmt::Debug, + ops::{Add, Sub}, +}; + +#[derive(Debug)] +pub(crate) struct BoundsTree +where + U: Clone + Debug + Default + PartialEq, +{ + root: Option, + nodes: Vec>, + stack: Vec, +} + +impl BoundsTree +where + U: Clone + + Debug + + PartialEq + + PartialOrd + + Add + + Sub + + Half + + Default, +{ + pub fn clear(&mut self) { + self.root = None; + self.nodes.clear(); + self.stack.clear(); + } + + pub fn insert(&mut self, new_bounds: Bounds) -> u32 { + // If the tree is empty, make the root the new leaf. + if self.root.is_none() { + let new_node = self.push_leaf(new_bounds, 1); + self.root = Some(new_node); + return 1; + } + + // Search for the best place to add the new leaf based on heuristics. + let mut max_intersecting_ordering = 0; + let mut index = self.root.unwrap(); + while let Node::Internal { + left, + right, + bounds: node_bounds, + .. + } = &mut self.nodes[index] + { + let left = *left; + let right = *right; + *node_bounds = node_bounds.union(&new_bounds); + self.stack.push(index); + + // Descend to the best-fit child, based on which one would increase + // the surface area the least. This attempts to keep the tree balanced + // in terms of surface area. If there is an intersection with the other child, + // add its keys to the intersections vector. + let left_cost = new_bounds.union(self.nodes[left].bounds()).half_perimeter(); + let right_cost = new_bounds + .union(self.nodes[right].bounds()) + .half_perimeter(); + if left_cost < right_cost { + max_intersecting_ordering = + self.find_max_ordering(right, &new_bounds, max_intersecting_ordering); + index = left; + } else { + max_intersecting_ordering = + self.find_max_ordering(left, &new_bounds, max_intersecting_ordering); + index = right; + } + } + + // We've found a leaf ('index' now refers to a leaf node). + // We'll insert a new parent node above the leaf and attach our new leaf to it. + let sibling = index; + + // Check for collision with the located leaf node + let Node::Leaf { + bounds: sibling_bounds, + order: sibling_ordering, + .. + } = &self.nodes[index] + else { + unreachable!(); + }; + if sibling_bounds.intersects(&new_bounds) { + max_intersecting_ordering = cmp::max(max_intersecting_ordering, *sibling_ordering); + } + + let ordering = max_intersecting_ordering + 1; + let new_node = self.push_leaf(new_bounds, ordering); + let new_parent = self.push_internal(sibling, new_node); + + // If there was an old parent, we need to update its children indices. + if let Some(old_parent) = self.stack.last().copied() { + let Node::Internal { left, right, .. } = &mut self.nodes[old_parent] else { + unreachable!(); + }; + + if *left == sibling { + *left = new_parent; + } else { + *right = new_parent; + } + } else { + // If the old parent was the root, the new parent is the new root. + self.root = Some(new_parent); + } + + for node_index in self.stack.drain(..).rev() { + let Node::Internal { + max_order: max_ordering, + .. + } = &mut self.nodes[node_index] + else { + unreachable!() + }; + if *max_ordering >= ordering { + break; + } + *max_ordering = ordering; + } + + ordering + } + + fn find_max_ordering(&self, index: usize, bounds: &Bounds, mut max_ordering: u32) -> u32 { + match &self.nodes[index] { + Node::Leaf { + bounds: node_bounds, + order: ordering, + .. + } => { + if bounds.intersects(node_bounds) { + max_ordering = cmp::max(*ordering, max_ordering); + } + } + Node::Internal { + left, + right, + bounds: node_bounds, + max_order: node_max_ordering, + .. + } => { + if bounds.intersects(node_bounds) && max_ordering < *node_max_ordering { + let left_max_ordering = self.nodes[*left].max_ordering(); + let right_max_ordering = self.nodes[*right].max_ordering(); + if left_max_ordering > right_max_ordering { + max_ordering = self.find_max_ordering(*left, bounds, max_ordering); + max_ordering = self.find_max_ordering(*right, bounds, max_ordering); + } else { + max_ordering = self.find_max_ordering(*right, bounds, max_ordering); + max_ordering = self.find_max_ordering(*left, bounds, max_ordering); + } + } + } + } + max_ordering + } + + fn push_leaf(&mut self, bounds: Bounds, order: u32) -> usize { + self.nodes.push(Node::Leaf { bounds, order }); + self.nodes.len() - 1 + } + + fn push_internal(&mut self, left: usize, right: usize) -> usize { + let left_node = &self.nodes[left]; + let right_node = &self.nodes[right]; + let new_bounds = left_node.bounds().union(right_node.bounds()); + let max_ordering = cmp::max(left_node.max_ordering(), right_node.max_ordering()); + self.nodes.push(Node::Internal { + bounds: new_bounds, + left, + right, + max_order: max_ordering, + }); + self.nodes.len() - 1 + } +} + +impl Default for BoundsTree +where + U: Clone + Debug + Default + PartialEq, +{ + fn default() -> Self { + BoundsTree { + root: None, + nodes: Vec::new(), + stack: Vec::new(), + } + } +} + +#[derive(Debug, Clone)] +enum Node +where + U: Clone + Debug + Default + PartialEq, +{ + Leaf { + bounds: Bounds, + order: u32, + }, + Internal { + left: usize, + right: usize, + bounds: Bounds, + max_order: u32, + }, +} + +impl Node +where + U: Clone + Debug + Default + PartialEq, +{ + fn bounds(&self) -> &Bounds { + match self { + Node::Leaf { bounds, .. } => bounds, + Node::Internal { bounds, .. } => bounds, + } + } + + fn max_ordering(&self) -> u32 { + match self { + Node::Leaf { + order: ordering, .. + } => *ordering, + Node::Internal { + max_order: max_ordering, + .. + } => *max_ordering, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Bounds, Point, Size}; + use rand::{Rng, SeedableRng}; + + #[test] + fn test_insert() { + let mut tree = BoundsTree::::default(); + let bounds1 = Bounds { + origin: Point { x: 0.0, y: 0.0 }, + size: Size { + width: 10.0, + height: 10.0, + }, + }; + let bounds2 = Bounds { + origin: Point { x: 5.0, y: 5.0 }, + size: Size { + width: 10.0, + height: 10.0, + }, + }; + let bounds3 = Bounds { + origin: Point { x: 10.0, y: 10.0 }, + size: Size { + width: 10.0, + height: 10.0, + }, + }; + + // Insert the bounds into the tree and verify the order is correct + assert_eq!(tree.insert(bounds1), 1); + assert_eq!(tree.insert(bounds2), 2); + assert_eq!(tree.insert(bounds3), 3); + + // Insert non-overlapping bounds and verify they can reuse orders + let bounds4 = Bounds { + origin: Point { x: 20.0, y: 20.0 }, + size: Size { + width: 10.0, + height: 10.0, + }, + }; + let bounds5 = Bounds { + origin: Point { x: 40.0, y: 40.0 }, + size: Size { + width: 10.0, + height: 10.0, + }, + }; + let bounds6 = Bounds { + origin: Point { x: 25.0, y: 25.0 }, + size: Size { + width: 10.0, + height: 10.0, + }, + }; + assert_eq!(tree.insert(bounds4), 1); // bounds4 does not overlap with bounds1, bounds2, or bounds3 + assert_eq!(tree.insert(bounds5), 1); // bounds5 does not overlap with any other bounds + assert_eq!(tree.insert(bounds6), 2); // bounds6 overlaps with bounds4, so it should have a different order + } + + #[test] + fn test_random_iterations() { + let max_bounds = 100; + for seed in 1..=1000 { + // let seed = 44; + let mut tree = BoundsTree::default(); + let mut rng = rand::rngs::StdRng::seed_from_u64(seed as u64); + let mut expected_quads: Vec<(Bounds, u32)> = Vec::new(); + + // Insert a random number of random AABBs into the tree. + let num_bounds = rng.random_range(1..=max_bounds); + for _ in 0..num_bounds { + let min_x: f32 = rng.random_range(-100.0..100.0); + let min_y: f32 = rng.random_range(-100.0..100.0); + let width: f32 = rng.random_range(0.0..50.0); + let height: f32 = rng.random_range(0.0..50.0); + let bounds = Bounds { + origin: Point { x: min_x, y: min_y }, + size: Size { width, height }, + }; + + let expected_ordering = expected_quads + .iter() + .filter_map(|quad| quad.0.intersects(&bounds).then_some(quad.1)) + .max() + .unwrap_or(0) + + 1; + expected_quads.push((bounds, expected_ordering)); + + // Insert the AABB into the tree and collect intersections. + let actual_ordering = tree.insert(bounds); + assert_eq!(actual_ordering, expected_ordering); + } + } + } +} diff --git a/third_party/gpui/src/color.rs b/third_party/gpui/src/color.rs new file mode 100644 index 0000000..3af5731 --- /dev/null +++ b/third_party/gpui/src/color.rs @@ -0,0 +1,934 @@ +use anyhow::{Context as _, bail}; +use schemars::{JsonSchema, json_schema}; +use serde::{ + Deserialize, Deserializer, Serialize, Serializer, + de::{self, Visitor}, +}; +use std::borrow::Cow; +use std::{ + fmt::{self, Display, Formatter}, + hash::{Hash, Hasher}, +}; + +/// Convert an RGB hex color code number to a color type +pub fn rgb(hex: u32) -> Rgba { + let [_, r, g, b] = hex.to_be_bytes().map(|b| (b as f32) / 255.0); + Rgba { r, g, b, a: 1.0 } +} + +/// Convert an RGBA hex color code number to [`Rgba`] +pub fn rgba(hex: u32) -> Rgba { + let [r, g, b, a] = hex.to_be_bytes().map(|b| (b as f32) / 255.0); + Rgba { r, g, b, a } +} + +/// Swap from RGBA with premultiplied alpha to BGRA +pub(crate) fn swap_rgba_pa_to_bgra(color: &mut [u8]) { + color.swap(0, 2); + if color[3] > 0 { + let a = color[3] as f32 / 255.; + color[0] = (color[0] as f32 / a) as u8; + color[1] = (color[1] as f32 / a) as u8; + color[2] = (color[2] as f32 / a) as u8; + } +} + +/// An RGBA color +#[derive(PartialEq, Clone, Copy, Default)] +#[repr(C)] +pub struct Rgba { + /// The red component of the color, in the range 0.0 to 1.0 + pub r: f32, + /// The green component of the color, in the range 0.0 to 1.0 + pub g: f32, + /// The blue component of the color, in the range 0.0 to 1.0 + pub b: f32, + /// The alpha component of the color, in the range 0.0 to 1.0 + pub a: f32, +} + +impl fmt::Debug for Rgba { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "rgba({:#010x})", u32::from(*self)) + } +} + +impl Rgba { + /// Create a new [`Rgba`] color by blending this and another color together + pub fn blend(&self, other: Rgba) -> Self { + if other.a >= 1.0 { + other + } else if other.a <= 0.0 { + *self + } else { + Rgba { + r: (self.r * (1.0 - other.a)) + (other.r * other.a), + g: (self.g * (1.0 - other.a)) + (other.g * other.a), + b: (self.b * (1.0 - other.a)) + (other.b * other.a), + a: self.a, + } + } + } +} + +impl From for u32 { + fn from(rgba: Rgba) -> Self { + let r = (rgba.r * 255.0) as u32; + let g = (rgba.g * 255.0) as u32; + let b = (rgba.b * 255.0) as u32; + let a = (rgba.a * 255.0) as u32; + (r << 24) | (g << 16) | (b << 8) | a + } +} + +struct RgbaVisitor; + +impl Visitor<'_> for RgbaVisitor { + type Value = Rgba; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string in the format #rrggbb or #rrggbbaa") + } + + fn visit_str(self, value: &str) -> Result { + Rgba::try_from(value).map_err(E::custom) + } +} + +impl JsonSchema for Rgba { + fn schema_name() -> Cow<'static, str> { + "Rgba".into() + } + + fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + json_schema!({ + "type": "string", + "pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$" + }) + } +} + +impl<'de> Deserialize<'de> for Rgba { + fn deserialize>(deserializer: D) -> Result { + deserializer.deserialize_str(RgbaVisitor) + } +} + +impl Serialize for Rgba { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let r = (self.r * 255.0).round() as u8; + let g = (self.g * 255.0).round() as u8; + let b = (self.b * 255.0).round() as u8; + let a = (self.a * 255.0).round() as u8; + + let s = format!("#{r:02x}{g:02x}{b:02x}{a:02x}"); + serializer.serialize_str(&s) + } +} + +impl From for Rgba { + fn from(color: Hsla) -> Self { + let h = color.h; + let s = color.s; + let l = color.l; + + let c = (1.0 - (2.0 * l - 1.0).abs()) * s; + let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs()); + let m = l - c / 2.0; + let cm = c + m; + let xm = x + m; + + let (r, g, b) = match (h * 6.0).floor() as i32 { + 0 | 6 => (cm, xm, m), + 1 => (xm, cm, m), + 2 => (m, cm, xm), + 3 => (m, xm, cm), + 4 => (xm, m, cm), + _ => (cm, m, xm), + }; + + Rgba { + r: r.clamp(0., 1.), + g: g.clamp(0., 1.), + b: b.clamp(0., 1.), + a: color.a, + } + } +} + +impl TryFrom<&'_ str> for Rgba { + type Error = anyhow::Error; + + fn try_from(value: &'_ str) -> Result { + const RGB: usize = "rgb".len(); + const RGBA: usize = "rgba".len(); + const RRGGBB: usize = "rrggbb".len(); + const RRGGBBAA: usize = "rrggbbaa".len(); + + const EXPECTED_FORMATS: &str = "Expected #rgb, #rgba, #rrggbb, or #rrggbbaa"; + const INVALID_UNICODE: &str = "invalid unicode characters in color"; + + let Some(("", hex)) = value.trim().split_once('#') else { + bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}"); + }; + + let (r, g, b, a) = match hex.len() { + RGB | RGBA => { + let r = u8::from_str_radix( + hex.get(0..1).with_context(|| { + format!("{INVALID_UNICODE}: r component of #rgb/#rgba for value: '{value}'") + })?, + 16, + )?; + let g = u8::from_str_radix( + hex.get(1..2).with_context(|| { + format!("{INVALID_UNICODE}: g component of #rgb/#rgba for value: '{value}'") + })?, + 16, + )?; + let b = u8::from_str_radix( + hex.get(2..3).with_context(|| { + format!("{INVALID_UNICODE}: b component of #rgb/#rgba for value: '{value}'") + })?, + 16, + )?; + let a = if hex.len() == RGBA { + u8::from_str_radix( + hex.get(3..4).with_context(|| { + format!("{INVALID_UNICODE}: a component of #rgba for value: '{value}'") + })?, + 16, + )? + } else { + 0xf + }; + + /// Duplicates a given hex digit. + /// E.g., `0xf` -> `0xff`. + const fn duplicate(value: u8) -> u8 { + (value << 4) | value + } + + (duplicate(r), duplicate(g), duplicate(b), duplicate(a)) + } + RRGGBB | RRGGBBAA => { + let r = u8::from_str_radix( + hex.get(0..2).with_context(|| { + format!( + "{}: r component of #rrggbb/#rrggbbaa for value: '{}'", + INVALID_UNICODE, value + ) + })?, + 16, + )?; + let g = u8::from_str_radix( + hex.get(2..4).with_context(|| { + format!( + "{INVALID_UNICODE}: g component of #rrggbb/#rrggbbaa for value: '{value}'" + ) + })?, + 16, + )?; + let b = u8::from_str_radix( + hex.get(4..6).with_context(|| { + format!( + "{INVALID_UNICODE}: b component of #rrggbb/#rrggbbaa for value: '{value}'" + ) + })?, + 16, + )?; + let a = if hex.len() == RRGGBBAA { + u8::from_str_radix( + hex.get(6..8).with_context(|| { + format!( + "{INVALID_UNICODE}: a component of #rrggbbaa for value: '{value}'" + ) + })?, + 16, + )? + } else { + 0xff + }; + (r, g, b, a) + } + _ => bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}"), + }; + + Ok(Rgba { + r: r as f32 / 255., + g: g as f32 / 255., + b: b as f32 / 255., + a: a as f32 / 255., + }) + } +} + +/// An HSLA color +#[derive(Default, Copy, Clone, Debug)] +#[repr(C)] +pub struct Hsla { + /// Hue, in a range from 0 to 1 + pub h: f32, + + /// Saturation, in a range from 0 to 1 + pub s: f32, + + /// Lightness, in a range from 0 to 1 + pub l: f32, + + /// Alpha, in a range from 0 to 1 + pub a: f32, +} + +impl PartialEq for Hsla { + fn eq(&self, other: &Self) -> bool { + self.h + .total_cmp(&other.h) + .then(self.s.total_cmp(&other.s)) + .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a))) + .is_eq() + } +} + +impl PartialOrd for Hsla { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Hsla { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.h + .total_cmp(&other.h) + .then(self.s.total_cmp(&other.s)) + .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a))) + } +} + +impl Eq for Hsla {} + +impl Hash for Hsla { + fn hash(&self, state: &mut H) { + state.write_u32(u32::from_be_bytes(self.h.to_be_bytes())); + state.write_u32(u32::from_be_bytes(self.s.to_be_bytes())); + state.write_u32(u32::from_be_bytes(self.l.to_be_bytes())); + state.write_u32(u32::from_be_bytes(self.a.to_be_bytes())); + } +} + +impl Display for Hsla { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "hsla({:.2}, {:.2}%, {:.2}%, {:.2})", + self.h * 360., + self.s * 100., + self.l * 100., + self.a + ) + } +} + +/// Construct an [`Hsla`] object from plain values +pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla { + Hsla { + h: h.clamp(0., 1.), + s: s.clamp(0., 1.), + l: l.clamp(0., 1.), + a: a.clamp(0., 1.), + } +} + +/// Pure black in [`Hsla`] +pub const fn black() -> Hsla { + Hsla { + h: 0., + s: 0., + l: 0., + a: 1., + } +} + +/// Transparent black in [`Hsla`] +pub const fn transparent_black() -> Hsla { + Hsla { + h: 0., + s: 0., + l: 0., + a: 0., + } +} + +/// Transparent white in [`Hsla`] +pub const fn transparent_white() -> Hsla { + Hsla { + h: 0., + s: 0., + l: 1., + a: 0., + } +} + +/// Opaque grey in [`Hsla`], values will be clamped to the range [0, 1] +pub fn opaque_grey(lightness: f32, opacity: f32) -> Hsla { + Hsla { + h: 0., + s: 0., + l: lightness.clamp(0., 1.), + a: opacity.clamp(0., 1.), + } +} + +/// Pure white in [`Hsla`] +pub const fn white() -> Hsla { + Hsla { + h: 0., + s: 0., + l: 1., + a: 1., + } +} + +/// The color red in [`Hsla`] +pub const fn red() -> Hsla { + Hsla { + h: 0., + s: 1., + l: 0.5, + a: 1., + } +} + +/// The color blue in [`Hsla`] +pub const fn blue() -> Hsla { + Hsla { + h: 0.6666666667, + s: 1., + l: 0.5, + a: 1., + } +} + +/// The color green in [`Hsla`] +pub const fn green() -> Hsla { + Hsla { + h: 0.3333333333, + s: 1., + l: 0.25, + a: 1., + } +} + +/// The color yellow in [`Hsla`] +pub const fn yellow() -> Hsla { + Hsla { + h: 0.1666666667, + s: 1., + l: 0.5, + a: 1., + } +} + +impl Hsla { + /// Converts this HSLA color to an RGBA color. + pub fn to_rgb(self) -> Rgba { + self.into() + } + + /// The color red + pub const fn red() -> Self { + red() + } + + /// The color green + pub const fn green() -> Self { + green() + } + + /// The color blue + pub const fn blue() -> Self { + blue() + } + + /// The color black + pub const fn black() -> Self { + black() + } + + /// The color white + pub const fn white() -> Self { + white() + } + + /// The color transparent black + pub const fn transparent_black() -> Self { + transparent_black() + } + + /// Returns true if the HSLA color is fully transparent, false otherwise. + pub fn is_transparent(&self) -> bool { + self.a == 0.0 + } + + /// Returns true if the HSLA color is fully opaque, false otherwise. + pub fn is_opaque(&self) -> bool { + self.a == 1.0 + } + + /// Blends `other` on top of `self` based on `other`'s alpha value. The resulting color is a combination of `self`'s and `other`'s colors. + /// + /// If `other`'s alpha value is 1.0 or greater, `other` color is fully opaque, thus `other` is returned as the output color. + /// If `other`'s alpha value is 0.0 or less, `other` color is fully transparent, thus `self` is returned as the output color. + /// Else, the output color is calculated as a blend of `self` and `other` based on their weighted alpha values. + /// + /// Assumptions: + /// - Alpha values are contained in the range [0, 1], with 1 as fully opaque and 0 as fully transparent. + /// - The relative contributions of `self` and `other` is based on `self`'s alpha value (`self.a`) and `other`'s alpha value (`other.a`), `self` contributing `self.a * (1.0 - other.a)` and `other` contributing its own alpha value. + /// - RGB color components are contained in the range [0, 1]. + /// - If `self` and `other` colors are out of the valid range, the blend operation's output and behavior is undefined. + pub fn blend(self, other: Hsla) -> Hsla { + let alpha = other.a; + + if alpha >= 1.0 { + other + } else if alpha <= 0.0 { + self + } else { + let converted_self = Rgba::from(self); + let converted_other = Rgba::from(other); + let blended_rgb = converted_self.blend(converted_other); + Hsla::from(blended_rgb) + } + } + + /// Returns a new HSLA color with the same hue, and lightness, but with no saturation. + pub fn grayscale(&self) -> Self { + Hsla { + h: self.h, + s: 0., + l: self.l, + a: self.a, + } + } + + /// Fade out the color by a given factor. This factor should be between 0.0 and 1.0. + /// Where 0.0 will leave the color unchanged, and 1.0 will completely fade out the color. + pub fn fade_out(&mut self, factor: f32) { + self.a *= 1.0 - factor.clamp(0., 1.); + } + + /// Multiplies the alpha value of the color by a given factor + /// and returns a new HSLA color. + /// + /// Useful for transforming colors with dynamic opacity, + /// like a color from an external source. + /// + /// Example: + /// ``` + /// let color = gpui::red(); + /// let faded_color = color.opacity(0.5); + /// assert_eq!(faded_color.a, 0.5); + /// ``` + /// + /// This will return a red color with half the opacity. + /// + /// Example: + /// ``` + /// use gpui::hsla; + /// let color = hsla(0.7, 1.0, 0.5, 0.7); // A saturated blue + /// let faded_color = color.opacity(0.16); + /// assert!((faded_color.a - 0.112).abs() < 1e-6); + /// ``` + /// + /// This will return a blue color with around ~10% opacity, + /// suitable for an element's hover or selected state. + /// + pub fn opacity(&self, factor: f32) -> Self { + Hsla { + h: self.h, + s: self.s, + l: self.l, + a: self.a * factor.clamp(0., 1.), + } + } + + /// Returns a new HSLA color with the same hue, saturation, + /// and lightness, but with a new alpha value. + /// + /// Example: + /// ``` + /// let color = gpui::red(); + /// let red_color = color.alpha(0.25); + /// assert_eq!(red_color.a, 0.25); + /// ``` + /// + /// This will return a red color with half the opacity. + /// + /// Example: + /// ``` + /// use gpui::hsla; + /// let color = hsla(0.7, 1.0, 0.5, 0.7); // A saturated blue + /// let faded_color = color.alpha(0.25); + /// assert_eq!(faded_color.a, 0.25); + /// ``` + /// + /// This will return a blue color with 25% opacity. + pub fn alpha(&self, a: f32) -> Self { + Hsla { + h: self.h, + s: self.s, + l: self.l, + a: a.clamp(0., 1.), + } + } +} + +impl From for Hsla { + fn from(color: Rgba) -> Self { + let r = color.r; + let g = color.g; + let b = color.b; + + let max = r.max(g.max(b)); + let min = r.min(g.min(b)); + let delta = max - min; + + let l = (max + min) / 2.0; + let s = if l == 0.0 || l == 1.0 { + 0.0 + } else if l < 0.5 { + delta / (2.0 * l) + } else { + delta / (2.0 - 2.0 * l) + }; + + let h = if delta == 0.0 { + 0.0 + } else if max == r { + ((g - b) / delta).rem_euclid(6.0) / 6.0 + } else if max == g { + ((b - r) / delta + 2.0) / 6.0 + } else { + ((r - g) / delta + 4.0) / 6.0 + }; + + Hsla { + h, + s, + l, + a: color.a, + } + } +} + +impl JsonSchema for Hsla { + fn schema_name() -> Cow<'static, str> { + Rgba::schema_name() + } + + fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + Rgba::json_schema(generator) + } +} + +impl Serialize for Hsla { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + Rgba::from(*self).serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Hsla { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ok(Rgba::deserialize(deserializer)?.into()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)] +#[repr(C)] +pub(crate) enum BackgroundTag { + Solid = 0, + LinearGradient = 1, + PatternSlash = 2, +} + +/// A color space for color interpolation. +/// +/// References: +/// - +/// - +#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize, JsonSchema)] +#[repr(C)] +pub enum ColorSpace { + #[default] + /// The sRGB color space. + Srgb = 0, + /// The Oklab color space. + Oklab = 1, +} + +impl Display for ColorSpace { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + ColorSpace::Srgb => write!(f, "sRGB"), + ColorSpace::Oklab => write!(f, "Oklab"), + } + } +} + +/// A background color, which can be either a solid color or a linear gradient. +#[derive(Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)] +#[repr(C)] +pub struct Background { + pub(crate) tag: BackgroundTag, + pub(crate) color_space: ColorSpace, + pub(crate) solid: Hsla, + pub(crate) gradient_angle_or_pattern_height: f32, + pub(crate) colors: [LinearColorStop; 2], + /// Padding for alignment for repr(C) layout. + pad: u32, +} + +impl std::fmt::Debug for Background { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self.tag { + BackgroundTag::Solid => write!(f, "Solid({:?})", self.solid), + BackgroundTag::LinearGradient => { + write!( + f, + "LinearGradient({}, {:?}, {:?})", + self.gradient_angle_or_pattern_height, self.colors[0], self.colors[1] + ) + } + BackgroundTag::PatternSlash => { + write!( + f, + "PatternSlash({:?}, {})", + self.solid, self.gradient_angle_or_pattern_height + ) + } + } + } +} + +impl Eq for Background {} +impl Default for Background { + fn default() -> Self { + Self { + tag: BackgroundTag::Solid, + solid: Hsla::default(), + color_space: ColorSpace::default(), + gradient_angle_or_pattern_height: 0.0, + colors: [LinearColorStop::default(), LinearColorStop::default()], + pad: 0, + } + } +} + +/// Creates a hash pattern background +pub fn pattern_slash(color: Hsla, width: f32, interval: f32) -> Background { + let width_scaled = (width * 255.0) as u32; + let interval_scaled = (interval * 255.0) as u32; + let height = ((width_scaled * 0xFFFF) + interval_scaled) as f32; + + Background { + tag: BackgroundTag::PatternSlash, + solid: color, + gradient_angle_or_pattern_height: height, + ..Default::default() + } +} + +/// Creates a solid background color. +pub fn solid_background(color: impl Into) -> Background { + Background { + solid: color.into(), + ..Default::default() + } +} + +/// Creates a LinearGradient background color. +/// +/// The gradient line's angle of direction. A value of `0.` is equivalent to top; increasing values rotate clockwise from there. +/// +/// The `angle` is in degrees value in the range 0.0 to 360.0. +/// +/// +pub fn linear_gradient( + angle: f32, + from: impl Into, + to: impl Into, +) -> Background { + Background { + tag: BackgroundTag::LinearGradient, + gradient_angle_or_pattern_height: angle, + colors: [from.into(), to.into()], + ..Default::default() + } +} + +/// A color stop in a linear gradient. +/// +/// +#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +#[repr(C)] +pub struct LinearColorStop { + /// The color of the color stop. + pub color: Hsla, + /// The percentage of the gradient, in the range 0.0 to 1.0. + pub percentage: f32, +} + +/// Creates a new linear color stop. +/// +/// The percentage of the gradient, in the range 0.0 to 1.0. +pub fn linear_color_stop(color: impl Into, percentage: f32) -> LinearColorStop { + LinearColorStop { + color: color.into(), + percentage, + } +} + +impl LinearColorStop { + /// Returns a new color stop with the same color, but with a modified alpha value. + pub fn opacity(&self, factor: f32) -> Self { + Self { + percentage: self.percentage, + color: self.color.opacity(factor), + } + } +} + +impl Background { + /// Use specified color space for color interpolation. + /// + /// + pub fn color_space(mut self, color_space: ColorSpace) -> Self { + self.color_space = color_space; + self + } + + /// Returns a new background color with the same hue, saturation, and lightness, but with a modified alpha value. + pub fn opacity(&self, factor: f32) -> Self { + let mut background = *self; + background.solid = background.solid.opacity(factor); + background.colors = [ + self.colors[0].opacity(factor), + self.colors[1].opacity(factor), + ]; + background + } + + /// Returns whether the background color is transparent. + pub fn is_transparent(&self) -> bool { + match self.tag { + BackgroundTag::Solid => self.solid.is_transparent(), + BackgroundTag::LinearGradient => self.colors.iter().all(|c| c.color.is_transparent()), + BackgroundTag::PatternSlash => self.solid.is_transparent(), + } + } +} + +impl From for Background { + fn from(value: Hsla) -> Self { + Background { + tag: BackgroundTag::Solid, + solid: value, + ..Default::default() + } + } +} +impl From for Background { + fn from(value: Rgba) -> Self { + Background { + tag: BackgroundTag::Solid, + solid: Hsla::from(value), + ..Default::default() + } + } +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + + #[test] + fn test_deserialize_three_value_hex_to_rgba() { + let actual: Rgba = serde_json::from_value(json!("#f09")).unwrap(); + + assert_eq!(actual, rgba(0xff0099ff)) + } + + #[test] + fn test_deserialize_four_value_hex_to_rgba() { + let actual: Rgba = serde_json::from_value(json!("#f09f")).unwrap(); + + assert_eq!(actual, rgba(0xff0099ff)) + } + + #[test] + fn test_deserialize_six_value_hex_to_rgba() { + let actual: Rgba = serde_json::from_value(json!("#ff0099")).unwrap(); + + assert_eq!(actual, rgba(0xff0099ff)) + } + + #[test] + fn test_deserialize_eight_value_hex_to_rgba() { + let actual: Rgba = serde_json::from_value(json!("#ff0099ff")).unwrap(); + + assert_eq!(actual, rgba(0xff0099ff)) + } + + #[test] + fn test_deserialize_eight_value_hex_with_padding_to_rgba() { + let actual: Rgba = serde_json::from_value(json!(" #f5f5f5ff ")).unwrap(); + + assert_eq!(actual, rgba(0xf5f5f5ff)) + } + + #[test] + fn test_deserialize_eight_value_hex_with_mixed_case_to_rgba() { + let actual: Rgba = serde_json::from_value(json!("#DeAdbEeF")).unwrap(); + + assert_eq!(actual, rgba(0xdeadbeef)) + } + + #[test] + fn test_background_solid() { + let color = Hsla::from(rgba(0xff0099ff)); + let mut background = Background::from(color); + assert_eq!(background.tag, BackgroundTag::Solid); + assert_eq!(background.solid, color); + + assert_eq!(background.opacity(0.5).solid, color.opacity(0.5)); + assert!(!background.is_transparent()); + background.solid = hsla(0.0, 0.0, 0.0, 0.0); + assert!(background.is_transparent()); + } + + #[test] + fn test_background_linear_gradient() { + let from = linear_color_stop(rgba(0xff0099ff), 0.0); + let to = linear_color_stop(rgba(0x00ff99ff), 1.0); + let background = linear_gradient(90.0, from, to); + assert_eq!(background.tag, BackgroundTag::LinearGradient); + assert_eq!(background.colors[0], from); + assert_eq!(background.colors[1], to); + + assert_eq!(background.opacity(0.5).colors[0], from.opacity(0.5)); + assert_eq!(background.opacity(0.5).colors[1], to.opacity(0.5)); + assert!(!background.is_transparent()); + assert!(background.opacity(0.0).is_transparent()); + } +} diff --git a/third_party/gpui/src/colors.rs b/third_party/gpui/src/colors.rs new file mode 100644 index 0000000..ef11ef5 --- /dev/null +++ b/third_party/gpui/src/colors.rs @@ -0,0 +1,122 @@ +use crate::{App, Global, Rgba, Window, WindowAppearance, rgb}; +use std::ops::Deref; +use std::sync::Arc; + +/// The default set of colors for gpui. +/// +/// These are used for styling base components, examples and more. +#[derive(Clone, Debug)] +pub struct Colors { + /// Text color + pub text: Rgba, + /// Selected text color + pub selected_text: Rgba, + /// Background color + pub background: Rgba, + /// Disabled color + pub disabled: Rgba, + /// Selected color + pub selected: Rgba, + /// Border color + pub border: Rgba, + /// Separator color + pub separator: Rgba, + /// Container color + pub container: Rgba, +} + +impl Default for Colors { + fn default() -> Self { + Self::light() + } +} + +impl Colors { + /// Returns the default colors for the given window appearance. + pub fn for_appearance(window: &Window) -> Self { + match window.appearance() { + WindowAppearance::Light | WindowAppearance::VibrantLight => Self::light(), + WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::dark(), + } + } + + /// Returns the default dark colors. + pub fn dark() -> Self { + Self { + text: rgb(0xffffff), + selected_text: rgb(0xffffff), + disabled: rgb(0x565656), + selected: rgb(0x2457ca), + background: rgb(0x222222), + border: rgb(0x000000), + separator: rgb(0xd9d9d9), + container: rgb(0x262626), + } + } + + /// Returns the default light colors. + pub fn light() -> Self { + Self { + text: rgb(0x252525), + selected_text: rgb(0xffffff), + background: rgb(0xffffff), + disabled: rgb(0xb0b0b0), + selected: rgb(0x2a63d9), + border: rgb(0xd9d9d9), + separator: rgb(0xe6e6e6), + container: rgb(0xf4f5f5), + } + } + + /// Get [Colors] from the global state + pub fn get_global(cx: &App) -> &Arc { + &cx.global::().0 + } +} + +/// Get [Colors] from the global state +#[derive(Clone, Debug)] +pub struct GlobalColors(pub Arc); + +impl Deref for GlobalColors { + type Target = Arc; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Global for GlobalColors {} + +/// Implement this trait to allow global [Colors] access via `cx.default_colors()`. +pub trait DefaultColors { + /// Returns the default [`Colors`] + fn default_colors(&self) -> &Arc; +} + +impl DefaultColors for App { + fn default_colors(&self) -> &Arc { + &self.global::().0 + } +} + +/// The appearance of the base GPUI colors, used to style GPUI elements +/// +/// Varies based on the system's current [`WindowAppearance`]. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum DefaultAppearance { + /// Use the set of colors for light appearances. + #[default] + Light, + /// Use the set of colors for dark appearances. + Dark, +} + +impl From for DefaultAppearance { + fn from(appearance: WindowAppearance) -> Self { + match appearance { + WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light, + WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark, + } + } +} diff --git a/third_party/gpui/src/element.rs b/third_party/gpui/src/element.rs new file mode 100644 index 0000000..a3fc626 --- /dev/null +++ b/third_party/gpui/src/element.rs @@ -0,0 +1,769 @@ +//! Elements are the workhorses of GPUI. They are responsible for laying out and painting all of +//! the contents of a window. Elements form a tree and are laid out according to the web layout +//! standards as implemented by [taffy](https://github.com/DioxusLabs/taffy). Most of the time, +//! you won't need to interact with this module or these APIs directly. Elements provide their +//! own APIs and GPUI, or other element implementation, uses the APIs in this module to convert +//! that element tree into the pixels you see on the screen. +//! +//! # Element Basics +//! +//! Elements are constructed by calling [`Render::render()`] on the root view of the window, +//! which recursively constructs the element tree from the current state of the application,. +//! These elements are then laid out by Taffy, and painted to the screen according to their own +//! implementation of [`Element::paint()`]. Before the start of the next frame, the entire element +//! tree and any callbacks they have registered with GPUI are dropped and the process repeats. +//! +//! But some state is too simple and voluminous to store in every view that needs it, e.g. +//! whether a hover has been started or not. For this, GPUI provides the [`Element::PrepaintState`], associated type. +//! +//! # Implementing your own elements +//! +//! Elements are intended to be the low level, imperative API to GPUI. They are responsible for upholding, +//! or breaking, GPUI's features as they deem necessary. As an example, most GPUI elements are expected +//! to stay in the bounds that their parent element gives them. But with [`Window::with_content_mask`], +//! you can ignore this restriction and paint anywhere inside of the window's bounds. This is useful for overlays +//! and popups and anything else that shows up 'on top' of other elements. +//! With great power, comes great responsibility. +//! +//! However, most of the time, you won't need to implement your own elements. GPUI provides a number of +//! elements that should cover most common use cases out of the box and it's recommended that you use those +//! to construct `components`, using the [`RenderOnce`] trait and the `#[derive(IntoElement)]` macro. Only implement +//! elements when you need to take manual control of the layout and painting process, such as when using +//! your own custom layout algorithm or rendering a code editor. + +use crate::{ + App, ArenaBox, AvailableSpace, Bounds, Context, DispatchNodeId, ELEMENT_ARENA, ElementId, + FocusHandle, InspectorElementId, LayoutId, Pixels, Point, Size, Style, Window, + util::FluentBuilder, +}; +use derive_more::{Deref, DerefMut}; +pub(crate) use smallvec::SmallVec; +use std::{ + any::{Any, type_name}, + fmt::{self, Debug, Display}, + mem, panic, +}; + +/// Implemented by types that participate in laying out and painting the contents of a window. +/// Elements form a tree and are laid out according to web-based layout rules, as implemented by Taffy. +/// You can create custom elements by implementing this trait, see the module-level documentation +/// for more details. +pub trait Element: 'static + IntoElement { + /// The type of state returned from [`Element::request_layout`]. A mutable reference to this state is subsequently + /// provided to [`Element::prepaint`] and [`Element::paint`]. + type RequestLayoutState: 'static; + + /// The type of state returned from [`Element::prepaint`]. A mutable reference to this state is subsequently + /// provided to [`Element::paint`]. + type PrepaintState: 'static; + + /// If this element has a unique identifier, return it here. This is used to track elements across frames, and + /// will cause a GlobalElementId to be passed to the request_layout, prepaint, and paint methods. + /// + /// The global id can in turn be used to access state that's connected to an element with the same id across + /// frames. This id must be unique among children of the first containing element with an id. + fn id(&self) -> Option; + + /// Source location where this element was constructed, used to disambiguate elements in the + /// inspector and navigate to their source code. + fn source_location(&self) -> Option<&'static panic::Location<'static>>; + + /// Before an element can be painted, we need to know where it's going to be and how big it is. + /// Use this method to request a layout from Taffy and initialize the element's state. + fn request_layout( + &mut self, + id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState); + + /// After laying out an element, we need to commit its bounds to the current frame for hitbox + /// purposes. The state argument is the same state that was returned from [`Element::request_layout()`]. + fn prepaint( + &mut self, + id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + request_layout: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Self::PrepaintState; + + /// Once layout has been completed, this method will be called to paint the element to the screen. + /// The state argument is the same state that was returned from [`Element::request_layout()`]. + fn paint( + &mut self, + id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + request_layout: &mut Self::RequestLayoutState, + prepaint: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ); + + /// Convert this element into a dynamically-typed [`AnyElement`]. + fn into_any(self) -> AnyElement { + AnyElement::new(self) + } +} + +/// Implemented by any type that can be converted into an element. +pub trait IntoElement: Sized { + /// The specific type of element into which the implementing type is converted. + /// Useful for converting other types into elements automatically, like Strings + type Element: Element; + + /// Convert self into a type that implements [`Element`]. + fn into_element(self) -> Self::Element; + + /// Convert self into a dynamically-typed [`AnyElement`]. + fn into_any_element(self) -> AnyElement { + self.into_element().into_any() + } +} + +impl FluentBuilder for T {} + +/// An object that can be drawn to the screen. This is the trait that distinguishes "views" from +/// other entities. Views are `Entity`'s which `impl Render` and drawn to the screen. +pub trait Render: 'static + Sized { + /// Render this view into an element tree. + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement; +} + +impl Render for Empty { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + Empty + } +} + +/// You can derive [`IntoElement`] on any type that implements this trait. +/// It is used to construct reusable `components` out of plain data. Think of +/// components as a recipe for a certain pattern of elements. RenderOnce allows +/// you to invoke this pattern, without breaking the fluent builder pattern of +/// the element APIs. +pub trait RenderOnce: 'static { + /// Render this component into an element tree. Note that this method + /// takes ownership of self, as compared to [`Render::render()`] method + /// which takes a mutable reference. + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement; +} + +/// This is a helper trait to provide a uniform interface for constructing elements that +/// can accept any number of any kind of child elements +pub trait ParentElement { + /// Extend this element's children with the given child elements. + fn extend(&mut self, elements: impl IntoIterator); + + /// Add a single child element to this element. + fn child(mut self, child: impl IntoElement) -> Self + where + Self: Sized, + { + self.extend(std::iter::once(child.into_element().into_any())); + self + } + + /// Add multiple child elements to this element. + fn children(mut self, children: impl IntoIterator) -> Self + where + Self: Sized, + { + self.extend(children.into_iter().map(|child| child.into_any_element())); + self + } +} + +/// An element for rendering components. An implementation detail of the [`IntoElement`] derive macro +/// for [`RenderOnce`] +#[doc(hidden)] +pub struct Component { + component: Option, + #[cfg(debug_assertions)] + source: &'static core::panic::Location<'static>, +} + +impl Component { + /// Create a new component from the given RenderOnce type. + #[track_caller] + pub fn new(component: C) -> Self { + Component { + component: Some(component), + #[cfg(debug_assertions)] + source: core::panic::Location::caller(), + } + } +} + +impl Element for Component { + type RequestLayoutState = AnyElement; + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + #[cfg(debug_assertions)] + return Some(self.source); + + #[cfg(not(debug_assertions))] + return None; + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + window.with_global_id(ElementId::Name(type_name::().into()), |_, window| { + let mut element = self + .component + .take() + .unwrap() + .render(window, cx) + .into_any_element(); + + let layout_id = element.request_layout(window, cx); + (layout_id, element) + }) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _: Bounds, + element: &mut AnyElement, + window: &mut Window, + cx: &mut App, + ) { + window.with_global_id(ElementId::Name(type_name::().into()), |_, window| { + element.prepaint(window, cx); + }) + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _: Bounds, + element: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + window.with_global_id(ElementId::Name(type_name::().into()), |_, window| { + element.paint(window, cx); + }) + } +} + +impl IntoElement for Component { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +/// A globally unique identifier for an element, used to track state across frames. +#[derive(Deref, DerefMut, Default, Debug, Eq, PartialEq, Hash)] +pub struct GlobalElementId(pub(crate) SmallVec<[ElementId; 32]>); + +impl Display for GlobalElementId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (i, element_id) in self.0.iter().enumerate() { + if i > 0 { + write!(f, ".")?; + } + write!(f, "{}", element_id)?; + } + Ok(()) + } +} + +trait ElementObject { + fn inner_element(&mut self) -> &mut dyn Any; + + fn request_layout(&mut self, window: &mut Window, cx: &mut App) -> LayoutId; + + fn prepaint(&mut self, window: &mut Window, cx: &mut App); + + fn paint(&mut self, window: &mut Window, cx: &mut App); + + fn layout_as_root( + &mut self, + available_space: Size, + window: &mut Window, + cx: &mut App, + ) -> Size; +} + +/// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window. +pub struct Drawable { + /// The drawn element. + pub element: E, + phase: ElementDrawPhase, +} + +#[derive(Default)] +enum ElementDrawPhase { + #[default] + Start, + RequestLayout { + layout_id: LayoutId, + global_id: Option, + inspector_id: Option, + request_layout: RequestLayoutState, + }, + LayoutComputed { + layout_id: LayoutId, + global_id: Option, + inspector_id: Option, + available_space: Size, + request_layout: RequestLayoutState, + }, + Prepaint { + node_id: DispatchNodeId, + global_id: Option, + inspector_id: Option, + bounds: Bounds, + request_layout: RequestLayoutState, + prepaint: PrepaintState, + }, + Painted, +} + +/// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window. +impl Drawable { + pub(crate) fn new(element: E) -> Self { + Drawable { + element, + phase: ElementDrawPhase::Start, + } + } + + fn request_layout(&mut self, window: &mut Window, cx: &mut App) -> LayoutId { + match mem::take(&mut self.phase) { + ElementDrawPhase::Start => { + let global_id = self.element.id().map(|element_id| { + window.element_id_stack.push(element_id); + GlobalElementId(window.element_id_stack.clone()) + }); + + let inspector_id; + #[cfg(any(feature = "inspector", debug_assertions))] + { + inspector_id = self.element.source_location().map(|source| { + let path = crate::InspectorElementPath { + global_id: GlobalElementId(window.element_id_stack.clone()), + source_location: source, + }; + window.build_inspector_element_id(path) + }); + } + #[cfg(not(any(feature = "inspector", debug_assertions)))] + { + inspector_id = None; + } + + let (layout_id, request_layout) = self.element.request_layout( + global_id.as_ref(), + inspector_id.as_ref(), + window, + cx, + ); + + if global_id.is_some() { + window.element_id_stack.pop(); + } + + self.phase = ElementDrawPhase::RequestLayout { + layout_id, + global_id, + inspector_id, + request_layout, + }; + layout_id + } + _ => panic!("must call request_layout only once"), + } + } + + pub(crate) fn prepaint(&mut self, window: &mut Window, cx: &mut App) { + match mem::take(&mut self.phase) { + ElementDrawPhase::RequestLayout { + layout_id, + global_id, + inspector_id, + mut request_layout, + } + | ElementDrawPhase::LayoutComputed { + layout_id, + global_id, + inspector_id, + mut request_layout, + .. + } => { + if let Some(element_id) = self.element.id() { + window.element_id_stack.push(element_id); + debug_assert_eq!(global_id.as_ref().unwrap().0, window.element_id_stack); + } + + let bounds = window.layout_bounds(layout_id); + let node_id = window.next_frame.dispatch_tree.push_node(); + let prepaint = self.element.prepaint( + global_id.as_ref(), + inspector_id.as_ref(), + bounds, + &mut request_layout, + window, + cx, + ); + window.next_frame.dispatch_tree.pop_node(); + + if global_id.is_some() { + window.element_id_stack.pop(); + } + + self.phase = ElementDrawPhase::Prepaint { + node_id, + global_id, + inspector_id, + bounds, + request_layout, + prepaint, + }; + } + _ => panic!("must call request_layout before prepaint"), + } + } + + pub(crate) fn paint( + &mut self, + window: &mut Window, + cx: &mut App, + ) -> (E::RequestLayoutState, E::PrepaintState) { + match mem::take(&mut self.phase) { + ElementDrawPhase::Prepaint { + node_id, + global_id, + inspector_id, + bounds, + mut request_layout, + mut prepaint, + .. + } => { + if let Some(element_id) = self.element.id() { + window.element_id_stack.push(element_id); + debug_assert_eq!(global_id.as_ref().unwrap().0, window.element_id_stack); + } + + window.next_frame.dispatch_tree.set_active_node(node_id); + self.element.paint( + global_id.as_ref(), + inspector_id.as_ref(), + bounds, + &mut request_layout, + &mut prepaint, + window, + cx, + ); + + if global_id.is_some() { + window.element_id_stack.pop(); + } + + self.phase = ElementDrawPhase::Painted; + (request_layout, prepaint) + } + _ => panic!("must call prepaint before paint"), + } + } + + pub(crate) fn layout_as_root( + &mut self, + available_space: Size, + window: &mut Window, + cx: &mut App, + ) -> Size { + if matches!(&self.phase, ElementDrawPhase::Start) { + self.request_layout(window, cx); + } + + let layout_id = match mem::take(&mut self.phase) { + ElementDrawPhase::RequestLayout { + layout_id, + global_id, + inspector_id, + request_layout, + } => { + window.compute_layout(layout_id, available_space, cx); + self.phase = ElementDrawPhase::LayoutComputed { + layout_id, + global_id, + inspector_id, + available_space, + request_layout, + }; + layout_id + } + ElementDrawPhase::LayoutComputed { + layout_id, + global_id, + inspector_id, + available_space: prev_available_space, + request_layout, + } => { + if available_space != prev_available_space { + window.compute_layout(layout_id, available_space, cx); + } + self.phase = ElementDrawPhase::LayoutComputed { + layout_id, + global_id, + inspector_id, + available_space, + request_layout, + }; + layout_id + } + _ => panic!("cannot measure after painting"), + }; + + window.layout_bounds(layout_id).size + } +} + +impl ElementObject for Drawable +where + E: Element, + E::RequestLayoutState: 'static, +{ + fn inner_element(&mut self) -> &mut dyn Any { + &mut self.element + } + + fn request_layout(&mut self, window: &mut Window, cx: &mut App) -> LayoutId { + Drawable::request_layout(self, window, cx) + } + + fn prepaint(&mut self, window: &mut Window, cx: &mut App) { + Drawable::prepaint(self, window, cx); + } + + fn paint(&mut self, window: &mut Window, cx: &mut App) { + Drawable::paint(self, window, cx); + } + + fn layout_as_root( + &mut self, + available_space: Size, + window: &mut Window, + cx: &mut App, + ) -> Size { + Drawable::layout_as_root(self, available_space, window, cx) + } +} + +/// A dynamically typed element that can be used to store any element type. +pub struct AnyElement(ArenaBox); + +impl AnyElement { + pub(crate) fn new(element: E) -> Self + where + E: 'static + Element, + E::RequestLayoutState: Any, + { + let element = ELEMENT_ARENA + .with_borrow_mut(|arena| arena.alloc(|| Drawable::new(element))) + .map(|element| element as &mut dyn ElementObject); + AnyElement(element) + } + + /// Attempt to downcast a reference to the boxed element to a specific type. + pub fn downcast_mut(&mut self) -> Option<&mut T> { + self.0.inner_element().downcast_mut::() + } + + /// Request the layout ID of the element stored in this `AnyElement`. + /// Used for laying out child elements in a parent element. + pub fn request_layout(&mut self, window: &mut Window, cx: &mut App) -> LayoutId { + self.0.request_layout(window, cx) + } + + /// Prepares the element to be painted by storing its bounds, giving it a chance to draw hitboxes and + /// request autoscroll before the final paint pass is confirmed. + pub fn prepaint(&mut self, window: &mut Window, cx: &mut App) -> Option { + let focus_assigned = window.next_frame.focus.is_some(); + + self.0.prepaint(window, cx); + + if !focus_assigned && let Some(focus_id) = window.next_frame.focus { + return FocusHandle::for_id(focus_id, &cx.focus_handles); + } + + None + } + + /// Paints the element stored in this `AnyElement`. + pub fn paint(&mut self, window: &mut Window, cx: &mut App) { + self.0.paint(window, cx); + } + + /// Performs layout for this element within the given available space and returns its size. + pub fn layout_as_root( + &mut self, + available_space: Size, + window: &mut Window, + cx: &mut App, + ) -> Size { + self.0.layout_as_root(available_space, window, cx) + } + + /// Prepaints this element at the given absolute origin. + /// If any element in the subtree beneath this element is focused, its FocusHandle is returned. + pub fn prepaint_at( + &mut self, + origin: Point, + window: &mut Window, + cx: &mut App, + ) -> Option { + window.with_absolute_element_offset(origin, |window| self.prepaint(window, cx)) + } + + /// Performs layout on this element in the available space, then prepaints it at the given absolute origin. + /// If any element in the subtree beneath this element is focused, its FocusHandle is returned. + pub fn prepaint_as_root( + &mut self, + origin: Point, + available_space: Size, + window: &mut Window, + cx: &mut App, + ) -> Option { + self.layout_as_root(available_space, window, cx); + window.with_absolute_element_offset(origin, |window| self.prepaint(window, cx)) + } +} + +impl Element for AnyElement { + type RequestLayoutState = (); + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let layout_id = self.request_layout(window, cx); + (layout_id, ()) + } + + fn prepaint( + &mut self, + _: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _: Bounds, + _: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) { + self.prepaint(window, cx); + } + + fn paint( + &mut self, + _: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _: Bounds, + _: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + self.paint(window, cx); + } +} + +impl IntoElement for AnyElement { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } + + fn into_any_element(self) -> AnyElement { + self + } +} + +/// The empty element, which renders nothing. +pub struct Empty; + +impl IntoElement for Empty { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for Empty { + type RequestLayoutState = (); + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + (window.request_layout(Style::default(), None, cx), ()) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _state: &mut Self::RequestLayoutState, + _window: &mut Window, + _cx: &mut App, + ) { + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _prepaint: &mut Self::PrepaintState, + _window: &mut Window, + _cx: &mut App, + ) { + } +} diff --git a/third_party/gpui/src/elements/anchored.rs b/third_party/gpui/src/elements/anchored.rs new file mode 100644 index 0000000..f92593e --- /dev/null +++ b/third_party/gpui/src/elements/anchored.rs @@ -0,0 +1,292 @@ +use smallvec::SmallVec; + +use crate::{ + AnyElement, App, Axis, Bounds, Corner, Display, Edges, Element, GlobalElementId, + InspectorElementId, IntoElement, LayoutId, ParentElement, Pixels, Point, Position, Size, Style, + Window, point, px, +}; + +/// The state that the anchored element element uses to track its children. +pub struct AnchoredState { + child_layout_ids: SmallVec<[LayoutId; 4]>, +} + +/// An anchored element that can be used to display UI that +/// will avoid overflowing the window bounds. +pub struct Anchored { + children: SmallVec<[AnyElement; 2]>, + anchor_corner: Corner, + fit_mode: AnchoredFitMode, + anchor_position: Option>, + position_mode: AnchoredPositionMode, + offset: Option>, +} + +/// anchored gives you an element that will avoid overflowing the window bounds. +/// Its children should have no margin to avoid measurement issues. +pub fn anchored() -> Anchored { + Anchored { + children: SmallVec::new(), + anchor_corner: Corner::TopLeft, + fit_mode: AnchoredFitMode::SwitchAnchor, + anchor_position: None, + position_mode: AnchoredPositionMode::Window, + offset: None, + } +} + +impl Anchored { + /// Sets which corner of the anchored element should be anchored to the current position. + pub fn anchor(mut self, anchor: Corner) -> Self { + self.anchor_corner = anchor; + self + } + + /// Sets the position in window coordinates + /// (otherwise the location the anchored element is rendered is used) + pub fn position(mut self, anchor: Point) -> Self { + self.anchor_position = Some(anchor); + self + } + + /// Offset the final position by this amount. + /// Useful when you want to anchor to an element but offset from it, such as in PopoverMenu. + pub fn offset(mut self, offset: Point) -> Self { + self.offset = Some(offset); + self + } + + /// Sets the position mode for this anchored element. Local will have this + /// interpret its [`Anchored::position`] as relative to the parent element. + /// While Window will have it interpret the position as relative to the window. + pub fn position_mode(mut self, mode: AnchoredPositionMode) -> Self { + self.position_mode = mode; + self + } + + /// Snap to window edge instead of switching anchor corner when an overflow would occur. + pub fn snap_to_window(mut self) -> Self { + self.fit_mode = AnchoredFitMode::SnapToWindow; + self + } + + /// Snap to window edge and leave some margins. + pub fn snap_to_window_with_margin(mut self, edges: impl Into>) -> Self { + self.fit_mode = AnchoredFitMode::SnapToWindowWithMargin(edges.into()); + self + } +} + +impl ParentElement for Anchored { + fn extend(&mut self, elements: impl IntoIterator) { + self.children.extend(elements) + } +} + +impl Element for Anchored { + type RequestLayoutState = AnchoredState; + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (crate::LayoutId, Self::RequestLayoutState) { + let child_layout_ids = self + .children + .iter_mut() + .map(|child| child.request_layout(window, cx)) + .collect::>(); + + let anchored_style = Style { + position: Position::Absolute, + display: Display::Flex, + ..Style::default() + }; + + let layout_id = window.request_layout(anchored_style, child_layout_ids.iter().copied(), cx); + + (layout_id, AnchoredState { child_layout_ids }) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + request_layout: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) { + if request_layout.child_layout_ids.is_empty() { + return; + } + + let mut child_min = point(Pixels::MAX, Pixels::MAX); + let mut child_max = Point::default(); + for child_layout_id in &request_layout.child_layout_ids { + let child_bounds = window.layout_bounds(*child_layout_id); + child_min = child_min.min(&child_bounds.origin); + child_max = child_max.max(&child_bounds.bottom_right()); + } + let size: Size = (child_max - child_min).into(); + + let (origin, mut desired) = self.position_mode.get_position_and_bounds( + self.anchor_position, + self.anchor_corner, + size, + bounds, + self.offset, + ); + + let limits = Bounds { + origin: Point::default(), + size: window.viewport_size(), + }; + + if self.fit_mode == AnchoredFitMode::SwitchAnchor { + let mut anchor_corner = self.anchor_corner; + + if desired.left() < limits.left() || desired.right() > limits.right() { + let switched = Bounds::from_corner_and_size( + anchor_corner.other_side_corner_along(Axis::Horizontal), + origin, + size, + ); + if !(switched.left() < limits.left() || switched.right() > limits.right()) { + anchor_corner = anchor_corner.other_side_corner_along(Axis::Horizontal); + desired = switched + } + } + + if desired.top() < limits.top() || desired.bottom() > limits.bottom() { + let switched = Bounds::from_corner_and_size( + anchor_corner.other_side_corner_along(Axis::Vertical), + origin, + size, + ); + if !(switched.top() < limits.top() || switched.bottom() > limits.bottom()) { + desired = switched; + } + } + } + + let client_inset = window.client_inset.unwrap_or(px(0.)); + let edges = match self.fit_mode { + AnchoredFitMode::SnapToWindowWithMargin(edges) => edges, + _ => Edges::default(), + } + .map(|edge| *edge + client_inset); + + // Snap the horizontal edges of the anchored element to the horizontal edges of the window if + // its horizontal bounds overflow, aligning to the left if it is wider than the limits. + if desired.right() > limits.right() { + desired.origin.x -= desired.right() - limits.right() + edges.right; + } + if desired.left() < limits.left() { + desired.origin.x = limits.origin.x + edges.left; + } + + // Snap the vertical edges of the anchored element to the vertical edges of the window if + // its vertical bounds overflow, aligning to the top if it is taller than the limits. + if desired.bottom() > limits.bottom() { + desired.origin.y -= desired.bottom() - limits.bottom() + edges.bottom; + } + if desired.top() < limits.top() { + desired.origin.y = limits.origin.y + edges.top; + } + + let offset = desired.origin - bounds.origin; + let offset = point(offset.x.round(), offset.y.round()); + + window.with_element_offset(offset, |window| { + for child in &mut self.children { + child.prepaint(window, cx); + } + }) + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: crate::Bounds, + _request_layout: &mut Self::RequestLayoutState, + _prepaint: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + for child in &mut self.children { + child.paint(window, cx); + } + } +} + +impl IntoElement for Anchored { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +/// Which algorithm to use when fitting the anchored element to be inside the window. +#[derive(Copy, Clone, PartialEq)] +pub enum AnchoredFitMode { + /// Snap the anchored element to the window edge. + SnapToWindow, + /// Snap to window edge and leave some margins. + SnapToWindowWithMargin(Edges), + /// Switch which corner anchor this anchored element is attached to. + SwitchAnchor, +} + +/// Which algorithm to use when positioning the anchored element. +#[derive(Copy, Clone, PartialEq)] +pub enum AnchoredPositionMode { + /// Position the anchored element relative to the window. + Window, + /// Position the anchored element relative to its parent. + Local, +} + +impl AnchoredPositionMode { + fn get_position_and_bounds( + &self, + anchor_position: Option>, + anchor_corner: Corner, + size: Size, + bounds: Bounds, + offset: Option>, + ) -> (Point, Bounds) { + let offset = offset.unwrap_or_default(); + + match self { + AnchoredPositionMode::Window => { + let anchor_position = anchor_position.unwrap_or(bounds.origin); + let bounds = + Bounds::from_corner_and_size(anchor_corner, anchor_position + offset, size); + (anchor_position, bounds) + } + AnchoredPositionMode::Local => { + let anchor_position = anchor_position.unwrap_or_default(); + let bounds = Bounds::from_corner_and_size( + anchor_corner, + bounds.origin + anchor_position + offset, + size, + ); + (anchor_position, bounds) + } + } + } +} diff --git a/third_party/gpui/src/elements/animation.rs b/third_party/gpui/src/elements/animation.rs new file mode 100644 index 0000000..e72fb00 --- /dev/null +++ b/third_party/gpui/src/elements/animation.rs @@ -0,0 +1,263 @@ +use std::{ + rc::Rc, + time::{Duration, Instant}, +}; + +use crate::{ + AnyElement, App, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, Window, +}; + +pub use easing::*; +use smallvec::SmallVec; + +/// An animation that can be applied to an element. +#[derive(Clone)] +pub struct Animation { + /// The amount of time for which this animation should run + pub duration: Duration, + /// Whether to repeat this animation when it finishes + pub oneshot: bool, + /// A function that takes a delta between 0 and 1 and returns a new delta + /// between 0 and 1 based on the given easing function. + pub easing: Rc f32>, +} + +impl Animation { + /// Create a new animation with the given duration. + /// By default the animation will only run once and will use a linear easing function. + pub fn new(duration: Duration) -> Self { + Self { + duration, + oneshot: true, + easing: Rc::new(linear), + } + } + + /// Set the animation to loop when it finishes. + pub fn repeat(mut self) -> Self { + self.oneshot = false; + self + } + + /// Set the easing function to use for this animation. + /// The easing function will take a time delta between 0 and 1 and return a new delta + /// between 0 and 1 + pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self { + self.easing = Rc::new(easing); + self + } +} + +/// An extension trait for adding the animation wrapper to both Elements and Components +pub trait AnimationExt { + /// Render this component or element with an animation + fn with_animation( + self, + id: impl Into, + animation: Animation, + animator: impl Fn(Self, f32) -> Self + 'static, + ) -> AnimationElement + where + Self: Sized, + { + AnimationElement { + id: id.into(), + element: Some(self), + animator: Box::new(move |this, _, value| animator(this, value)), + animations: smallvec::smallvec![animation], + } + } + + /// Render this component or element with a chain of animations + fn with_animations( + self, + id: impl Into, + animations: Vec, + animator: impl Fn(Self, usize, f32) -> Self + 'static, + ) -> AnimationElement + where + Self: Sized, + { + AnimationElement { + id: id.into(), + element: Some(self), + animator: Box::new(animator), + animations: animations.into(), + } + } +} + +impl AnimationExt for E {} + +/// A GPUI element that applies an animation to another element +pub struct AnimationElement { + id: ElementId, + element: Option, + animations: SmallVec<[Animation; 1]>, + animator: Box E + 'static>, +} + +impl AnimationElement { + /// Returns a new [`AnimationElement`] after applying the given function + /// to the element being animated. + pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> AnimationElement { + self.element = self.element.map(f); + self + } +} + +impl IntoElement for AnimationElement { + type Element = AnimationElement; + + fn into_element(self) -> Self::Element { + self + } +} + +struct AnimationState { + start: Instant, + animation_ix: usize, +} + +impl Element for AnimationElement { + type RequestLayoutState = AnyElement; + type PrepaintState = (); + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (crate::LayoutId, Self::RequestLayoutState) { + window.with_element_state(global_id.unwrap(), |state, window| { + let mut state = state.unwrap_or_else(|| AnimationState { + start: Instant::now(), + animation_ix: 0, + }); + let animation_ix = state.animation_ix; + + let mut delta = state.start.elapsed().as_secs_f32() + / self.animations[animation_ix].duration.as_secs_f32(); + + let mut done = false; + if delta > 1.0 { + if self.animations[animation_ix].oneshot { + if animation_ix >= self.animations.len() - 1 { + done = true; + } else { + state.start = Instant::now(); + state.animation_ix += 1; + } + delta = 1.0; + } else { + delta %= 1.0; + } + } + let delta = (self.animations[animation_ix].easing)(delta); + + debug_assert!( + (0.0..=1.0).contains(&delta), + "delta should always be between 0 and 1" + ); + + let element = self.element.take().expect("should only be called once"); + let mut element = (self.animator)(element, animation_ix, delta).into_any_element(); + + if !done { + window.request_animation_frame(); + } + + ((element.request_layout(window, cx), element), state) + }) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: crate::Bounds, + element: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Self::PrepaintState { + element.prepaint(window, cx); + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: crate::Bounds, + element: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + element.paint(window, cx); + } +} + +mod easing { + use std::f32::consts::PI; + + /// The linear easing function, or delta itself + pub fn linear(delta: f32) -> f32 { + delta + } + + /// The quadratic easing function, delta * delta + pub fn quadratic(delta: f32) -> f32 { + delta * delta + } + + /// The quadratic ease-in-out function, which starts and ends slowly but speeds up in the middle + pub fn ease_in_out(delta: f32) -> f32 { + if delta < 0.5 { + 2.0 * delta * delta + } else { + let x = -2.0 * delta + 2.0; + 1.0 - x * x / 2.0 + } + } + + /// The Quint ease-out function, which starts quickly and decelerates to a stop + pub fn ease_out_quint() -> impl Fn(f32) -> f32 { + move |delta| 1.0 - (1.0 - delta).powi(5) + } + + /// Apply the given easing function, first in the forward direction and then in the reverse direction + pub fn bounce(easing: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 { + move |delta| { + if delta < 0.5 { + easing(delta * 2.0) + } else { + easing((1.0 - delta) * 2.0) + } + } + } + + /// A custom easing function for pulsating alpha that slows down as it approaches 0.1 + pub fn pulsating_between(min: f32, max: f32) -> impl Fn(f32) -> f32 { + let range = max - min; + + move |delta| { + // Use a combination of sine and cubic functions for a more natural breathing rhythm + let t = (delta * 2.0 * PI).sin(); + let breath = (t * t * t + t) / 2.0; + + // Map the breath to our desired alpha range + let normalized_alpha = (breath + 1.0) / 2.0; + + min + (normalized_alpha * range) + } + } +} diff --git a/third_party/gpui/src/elements/canvas.rs b/third_party/gpui/src/elements/canvas.rs new file mode 100644 index 0000000..d57d2f6 --- /dev/null +++ b/third_party/gpui/src/elements/canvas.rs @@ -0,0 +1,95 @@ +use refineable::Refineable as _; + +use crate::{ + App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, Pixels, + Style, StyleRefinement, Styled, Window, +}; + +/// Construct a canvas element with the given paint callback. +/// Useful for adding short term custom drawing to a view. +pub fn canvas( + prepaint: impl 'static + FnOnce(Bounds, &mut Window, &mut App) -> T, + paint: impl 'static + FnOnce(Bounds, T, &mut Window, &mut App), +) -> Canvas { + Canvas { + prepaint: Some(Box::new(prepaint)), + paint: Some(Box::new(paint)), + style: StyleRefinement::default(), + } +} + +/// A canvas element, meant for accessing the low level paint API without defining a whole +/// custom element +pub struct Canvas { + prepaint: Option, &mut Window, &mut App) -> T>>, + paint: Option, T, &mut Window, &mut App)>>, + style: StyleRefinement, +} + +impl IntoElement for Canvas { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for Canvas { + type RequestLayoutState = Style; + type PrepaintState = Option; + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (crate::LayoutId, Self::RequestLayoutState) { + let mut style = Style::default(); + style.refine(&self.style); + let layout_id = window.request_layout(style.clone(), [], cx); + (layout_id, style) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _request_layout: &mut Style, + window: &mut Window, + cx: &mut App, + ) -> Option { + Some(self.prepaint.take().unwrap()(bounds, window, cx)) + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + style: &mut Style, + prepaint: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + let prepaint = prepaint.take().unwrap(); + style.paint(bounds, window, cx, |window, cx| { + (self.paint.take().unwrap())(bounds, prepaint, window, cx) + }); + } +} + +impl Styled for Canvas { + fn style(&mut self) -> &mut crate::StyleRefinement { + &mut self.style + } +} diff --git a/third_party/gpui/src/elements/deferred.rs b/third_party/gpui/src/elements/deferred.rs new file mode 100644 index 0000000..9498734 --- /dev/null +++ b/third_party/gpui/src/elements/deferred.rs @@ -0,0 +1,96 @@ +use crate::{ + AnyElement, App, Bounds, Element, GlobalElementId, InspectorElementId, IntoElement, LayoutId, + Pixels, Window, +}; + +/// Builds a `Deferred` element, which delays the layout and paint of its child. +pub fn deferred(child: impl IntoElement) -> Deferred { + Deferred { + child: Some(child.into_any_element()), + priority: 0, + } +} + +/// An element which delays the painting of its child until after all of +/// its ancestors, while keeping its layout as part of the current element tree. +pub struct Deferred { + child: Option, + priority: usize, +} + +impl Deferred { + /// Sets the `priority` value of the `deferred` element, which + /// determines the drawing order relative to other deferred elements, + /// with higher values being drawn on top. + pub fn with_priority(mut self, priority: usize) -> Self { + self.priority = priority; + self + } +} + +impl Element for Deferred { + type RequestLayoutState = (); + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, ()) { + let layout_id = self.child.as_mut().unwrap().request_layout(window, cx); + (layout_id, ()) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + window: &mut Window, + _cx: &mut App, + ) { + let child = self.child.take().unwrap(); + let element_offset = window.element_offset(); + window.defer_draw(child, element_offset, self.priority) + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _prepaint: &mut Self::PrepaintState, + _window: &mut Window, + _cx: &mut App, + ) { + } +} + +impl IntoElement for Deferred { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Deferred { + /// Sets a priority for the element. A higher priority conceptually means painting the element + /// on top of deferred draws with a lower priority (i.e. closer to the viewer). + pub fn priority(mut self, priority: usize) -> Self { + self.priority = priority; + self + } +} diff --git a/third_party/gpui/src/elements/div.rs b/third_party/gpui/src/elements/div.rs new file mode 100644 index 0000000..4d4e176 --- /dev/null +++ b/third_party/gpui/src/elements/div.rs @@ -0,0 +1,3250 @@ +//! Div is the central, reusable element that most GPUI trees will be built from. +//! It functions as a container for other elements, and provides a number of +//! useful features for laying out and styling its children as well as binding +//! mouse events and action handlers. It is meant to be similar to the HTML `
` +//! element, but for GPUI. +//! +//! # Build your own div +//! +//! GPUI does not directly provide APIs for stateful, multi step events like `click` +//! and `drag`. We want GPUI users to be able to build their own abstractions for +//! their own needs. However, as a UI framework, we're also obliged to provide some +//! building blocks to make the process of building your own elements easier. +//! For this we have the [`Interactivity`] and the [`StyleRefinement`] structs, as well +//! as several associated traits. Together, these provide the full suite of Dom-like events +//! and Tailwind-like styling that you can use to build your own custom elements. Div is +//! constructed by combining these two systems into an all-in-one element. + +use crate::{ + AbsoluteLength, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, App, Bounds, ClickEvent, + DispatchPhase, Display, Element, ElementId, Entity, FocusHandle, Global, GlobalElementId, + Hitbox, HitboxBehavior, HitboxId, InspectorElementId, IntoElement, IsZero, KeyContext, + KeyDownEvent, KeyUpEvent, KeyboardButton, KeyboardClickEvent, LayoutId, ModifiersChangedEvent, + MouseButton, MouseClickEvent, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Overflow, + ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style, + StyleRefinement, Styled, Task, TooltipId, Visibility, Window, WindowControlArea, point, px, + size, +}; +use collections::HashMap; +use refineable::Refineable; +use smallvec::SmallVec; +use stacksafe::{StackSafe, stacksafe}; +use std::{ + any::{Any, TypeId}, + cell::RefCell, + cmp::Ordering, + fmt::Debug, + marker::PhantomData, + mem, + rc::Rc, + sync::Arc, + time::Duration, +}; +use util::ResultExt; + +use super::ImageCacheProvider; + +const DRAG_THRESHOLD: f64 = 2.; +const TOOLTIP_SHOW_DELAY: Duration = Duration::from_millis(500); +const HOVERABLE_TOOLTIP_HIDE_DELAY: Duration = Duration::from_millis(500); + +/// The styling information for a given group. +pub struct GroupStyle { + /// The identifier for this group. + pub group: SharedString, + + /// The specific style refinement that this group would apply + /// to its children. + pub style: Box, +} + +/// An event for when a drag is moving over this element, with the given state type. +pub struct DragMoveEvent { + /// The mouse move event that triggered this drag move event. + pub event: MouseMoveEvent, + + /// The bounds of this element. + pub bounds: Bounds, + drag: PhantomData, + dragged_item: Arc, +} + +impl DragMoveEvent { + /// Returns the drag state for this event. + pub fn drag<'b>(&self, cx: &'b App) -> &'b T { + cx.active_drag + .as_ref() + .and_then(|drag| drag.value.downcast_ref::()) + .expect("DragMoveEvent is only valid when the stored active drag is of the same type.") + } + + /// An item that is about to be dropped. + pub fn dragged_item(&self) -> &dyn Any { + self.dragged_item.as_ref() + } +} + +impl Interactivity { + /// Create an `Interactivity`, capturing the caller location in debug mode. + #[cfg(any(feature = "inspector", debug_assertions))] + #[track_caller] + pub fn new() -> Interactivity { + Interactivity { + source_location: Some(core::panic::Location::caller()), + ..Default::default() + } + } + + /// Create an `Interactivity`, capturing the caller location in debug mode. + #[cfg(not(any(feature = "inspector", debug_assertions)))] + pub fn new() -> Interactivity { + Interactivity::default() + } + + /// Gets the source location of construction. Returns `None` when not in debug mode. + pub fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { + #[cfg(any(feature = "inspector", debug_assertions))] + { + self.source_location + } + + #[cfg(not(any(feature = "inspector", debug_assertions)))] + { + None + } + } + + /// Bind the given callback to the mouse down event for the given mouse button, during the bubble phase + /// The imperative API equivalent of [`InteractiveElement::on_mouse_down`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to the view state from this callback. + pub fn on_mouse_down( + &mut self, + button: MouseButton, + listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, + ) { + self.mouse_down_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Bubble + && event.button == button + && hitbox.is_hovered(window) + { + (listener)(event, window, cx) + } + })); + } + + /// Bind the given callback to the mouse down event for any button, during the capture phase + /// The imperative API equivalent of [`InteractiveElement::capture_any_mouse_down`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn capture_any_mouse_down( + &mut self, + listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, + ) { + self.mouse_down_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Capture && hitbox.is_hovered(window) { + (listener)(event, window, cx) + } + })); + } + + /// Bind the given callback to the mouse down event for any button, during the bubble phase + /// the imperative API equivalent to [`InteractiveElement::on_any_mouse_down`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_any_mouse_down( + &mut self, + listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, + ) { + self.mouse_down_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { + (listener)(event, window, cx) + } + })); + } + + /// Bind the given callback to the mouse up event for the given button, during the bubble phase + /// the imperative API equivalent to [`InteractiveElement::on_mouse_up`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_mouse_up( + &mut self, + button: MouseButton, + listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static, + ) { + self.mouse_up_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Bubble + && event.button == button + && hitbox.is_hovered(window) + { + (listener)(event, window, cx) + } + })); + } + + /// Bind the given callback to the mouse up event for any button, during the capture phase + /// the imperative API equivalent to [`InteractiveElement::capture_any_mouse_up`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn capture_any_mouse_up( + &mut self, + listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static, + ) { + self.mouse_up_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Capture && hitbox.is_hovered(window) { + (listener)(event, window, cx) + } + })); + } + + /// Bind the given callback to the mouse up event for any button, during the bubble phase + /// the imperative API equivalent to [`Interactivity::on_any_mouse_up`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_any_mouse_up( + &mut self, + listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static, + ) { + self.mouse_up_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { + (listener)(event, window, cx) + } + })); + } + + /// Bind the given callback to the mouse down event, on any button, during the capture phase, + /// when the mouse is outside of the bounds of this element. + /// The imperative API equivalent to [`InteractiveElement::on_mouse_down_out`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_mouse_down_out( + &mut self, + listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, + ) { + self.mouse_down_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Capture && !hitbox.contains(&window.mouse_position()) { + (listener)(event, window, cx) + } + })); + } + + /// Bind the given callback to the mouse up event, for the given button, during the capture phase, + /// when the mouse is outside of the bounds of this element. + /// The imperative API equivalent to [`InteractiveElement::on_mouse_up_out`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_mouse_up_out( + &mut self, + button: MouseButton, + listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static, + ) { + self.mouse_up_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Capture + && event.button == button + && !hitbox.is_hovered(window) + { + (listener)(event, window, cx); + } + })); + } + + /// Bind the given callback to the mouse move event, during the bubble phase + /// The imperative API equivalent to [`InteractiveElement::on_mouse_move`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_mouse_move( + &mut self, + listener: impl Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static, + ) { + self.mouse_move_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { + (listener)(event, window, cx); + } + })); + } + + /// Bind the given callback to the mouse drag event of the given type. Note that this + /// will be called for all move events, inside or outside of this element, as long as the + /// drag was started with this element under the mouse. Useful for implementing draggable + /// UIs that don't conform to a drag and drop style interaction, like resizing. + /// The imperative API equivalent to [`InteractiveElement::on_drag_move`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_drag_move( + &mut self, + listener: impl Fn(&DragMoveEvent, &mut Window, &mut App) + 'static, + ) where + T: 'static, + { + self.mouse_move_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Capture + && let Some(drag) = &cx.active_drag + && drag.value.as_ref().type_id() == TypeId::of::() + { + (listener)( + &DragMoveEvent { + event: event.clone(), + bounds: hitbox.bounds, + drag: PhantomData, + dragged_item: Arc::clone(&drag.value), + }, + window, + cx, + ); + } + })); + } + + /// Bind the given callback to scroll wheel events during the bubble phase + /// The imperative API equivalent to [`InteractiveElement::on_scroll_wheel`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_scroll_wheel( + &mut self, + listener: impl Fn(&ScrollWheelEvent, &mut Window, &mut App) + 'static, + ) { + self.scroll_wheel_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Bubble && hitbox.should_handle_scroll(window) { + (listener)(event, window, cx); + } + })); + } + + /// Bind the given callback to an action dispatch during the capture phase + /// The imperative API equivalent to [`InteractiveElement::capture_action`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn capture_action( + &mut self, + listener: impl Fn(&A, &mut Window, &mut App) + 'static, + ) { + self.action_listeners.push(( + TypeId::of::(), + Box::new(move |action, phase, window, cx| { + let action = action.downcast_ref().unwrap(); + if phase == DispatchPhase::Capture { + (listener)(action, window, cx) + } else { + cx.propagate(); + } + }), + )); + } + + /// Bind the given callback to an action dispatch during the bubble phase + /// The imperative API equivalent to [`InteractiveElement::on_action`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_action(&mut self, listener: impl Fn(&A, &mut Window, &mut App) + 'static) { + self.action_listeners.push(( + TypeId::of::(), + Box::new(move |action, phase, window, cx| { + let action = action.downcast_ref().unwrap(); + if phase == DispatchPhase::Bubble { + (listener)(action, window, cx) + } + }), + )); + } + + /// Bind the given callback to an action dispatch, based on a dynamic action parameter + /// instead of a type parameter. Useful for component libraries that want to expose + /// action bindings to their users. + /// The imperative API equivalent to [`InteractiveElement::on_boxed_action`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_boxed_action( + &mut self, + action: &dyn Action, + listener: impl Fn(&dyn Action, &mut Window, &mut App) + 'static, + ) { + let action = action.boxed_clone(); + self.action_listeners.push(( + (*action).type_id(), + Box::new(move |_, phase, window, cx| { + if phase == DispatchPhase::Bubble { + (listener)(&*action, window, cx) + } + }), + )); + } + + /// Bind the given callback to key down events during the bubble phase + /// The imperative API equivalent to [`InteractiveElement::on_key_down`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_key_down( + &mut self, + listener: impl Fn(&KeyDownEvent, &mut Window, &mut App) + 'static, + ) { + self.key_down_listeners + .push(Box::new(move |event, phase, window, cx| { + if phase == DispatchPhase::Bubble { + (listener)(event, window, cx) + } + })); + } + + /// Bind the given callback to key down events during the capture phase + /// The imperative API equivalent to [`InteractiveElement::capture_key_down`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn capture_key_down( + &mut self, + listener: impl Fn(&KeyDownEvent, &mut Window, &mut App) + 'static, + ) { + self.key_down_listeners + .push(Box::new(move |event, phase, window, cx| { + if phase == DispatchPhase::Capture { + listener(event, window, cx) + } + })); + } + + /// Bind the given callback to key up events during the bubble phase + /// The imperative API equivalent to [`InteractiveElement::on_key_up`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_key_up(&mut self, listener: impl Fn(&KeyUpEvent, &mut Window, &mut App) + 'static) { + self.key_up_listeners + .push(Box::new(move |event, phase, window, cx| { + if phase == DispatchPhase::Bubble { + listener(event, window, cx) + } + })); + } + + /// Bind the given callback to key up events during the capture phase + /// The imperative API equivalent to [`InteractiveElement::on_key_up`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn capture_key_up( + &mut self, + listener: impl Fn(&KeyUpEvent, &mut Window, &mut App) + 'static, + ) { + self.key_up_listeners + .push(Box::new(move |event, phase, window, cx| { + if phase == DispatchPhase::Capture { + listener(event, window, cx) + } + })); + } + + /// Bind the given callback to modifiers changing events. + /// The imperative API equivalent to [`InteractiveElement::on_modifiers_changed`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_modifiers_changed( + &mut self, + listener: impl Fn(&ModifiersChangedEvent, &mut Window, &mut App) + 'static, + ) { + self.modifiers_changed_listeners + .push(Box::new(move |event, window, cx| { + listener(event, window, cx) + })); + } + + /// Bind the given callback to drop events of the given type, whether or not the drag started on this element + /// The imperative API equivalent to [`InteractiveElement::on_drop`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_drop(&mut self, listener: impl Fn(&T, &mut Window, &mut App) + 'static) { + self.drop_listeners.push(( + TypeId::of::(), + Box::new(move |dragged_value, window, cx| { + listener(dragged_value.downcast_ref().unwrap(), window, cx); + }), + )); + } + + /// Use the given predicate to determine whether or not a drop event should be dispatched to this element + /// The imperative API equivalent to [`InteractiveElement::can_drop`] + pub fn can_drop( + &mut self, + predicate: impl Fn(&dyn Any, &mut Window, &mut App) -> bool + 'static, + ) { + self.can_drop_predicate = Some(Box::new(predicate)); + } + + /// Bind the given callback to click events of this element + /// The imperative API equivalent to [`StatefulInteractiveElement::on_click`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_click(&mut self, listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) + where + Self: Sized, + { + self.click_listeners.push(Rc::new(move |event, window, cx| { + listener(event, window, cx) + })); + } + + /// On drag initiation, this callback will be used to create a new view to render the dragged value for a + /// drag and drop operation. This API should also be used as the equivalent of 'on drag start' with + /// the [`Self::on_drag_move`] API + /// The imperative API equivalent to [`StatefulInteractiveElement::on_drag`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_drag( + &mut self, + value: T, + constructor: impl Fn(&T, Point, &mut Window, &mut App) -> Entity + 'static, + ) where + Self: Sized, + T: 'static, + W: 'static + Render, + { + debug_assert!( + self.drag_listener.is_none(), + "calling on_drag more than once on the same element is not supported" + ); + self.drag_listener = Some(( + Arc::new(value), + Box::new(move |value, offset, window, cx| { + constructor(value.downcast_ref().unwrap(), offset, window, cx).into() + }), + )); + } + + /// Bind the given callback on the hover start and end events of this element. Note that the boolean + /// passed to the callback is true when the hover starts and false when it ends. + /// The imperative API equivalent to [`StatefulInteractiveElement::on_hover`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_hover(&mut self, listener: impl Fn(&bool, &mut Window, &mut App) + 'static) + where + Self: Sized, + { + debug_assert!( + self.hover_listener.is_none(), + "calling on_hover more than once on the same element is not supported" + ); + self.hover_listener = Some(Box::new(listener)); + } + + /// Use the given callback to construct a new tooltip view when the mouse hovers over this element. + /// The imperative API equivalent to [`StatefulInteractiveElement::tooltip`] + pub fn tooltip(&mut self, build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) + where + Self: Sized, + { + debug_assert!( + self.tooltip_builder.is_none(), + "calling tooltip more than once on the same element is not supported" + ); + self.tooltip_builder = Some(TooltipBuilder { + build: Rc::new(build_tooltip), + hoverable: false, + }); + } + + /// Use the given callback to construct a new tooltip view when the mouse hovers over this element. + /// The tooltip itself is also hoverable and won't disappear when the user moves the mouse into + /// the tooltip. The imperative API equivalent to [`StatefulInteractiveElement::hoverable_tooltip`] + pub fn hoverable_tooltip( + &mut self, + build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static, + ) where + Self: Sized, + { + debug_assert!( + self.tooltip_builder.is_none(), + "calling tooltip more than once on the same element is not supported" + ); + self.tooltip_builder = Some(TooltipBuilder { + build: Rc::new(build_tooltip), + hoverable: true, + }); + } + + /// Block the mouse from all interactions with elements behind this element's hitbox. Typically + /// `block_mouse_except_scroll` should be preferred. + /// + /// The imperative API equivalent to [`InteractiveElement::occlude`] + pub fn occlude_mouse(&mut self) { + self.hitbox_behavior = HitboxBehavior::BlockMouse; + } + + /// Set the bounds of this element as a window control area for the platform window. + /// The imperative API equivalent to [`InteractiveElement::window_control_area`] + pub fn window_control_area(&mut self, area: WindowControlArea) { + self.window_control = Some(area); + } + + /// Block non-scroll mouse interactions with elements behind this element's hitbox. See + /// [`Hitbox::is_hovered`] for details. + /// + /// The imperative API equivalent to [`InteractiveElement::block_mouse_except_scroll`] + pub fn block_mouse_except_scroll(&mut self) { + self.hitbox_behavior = HitboxBehavior::BlockMouseExceptScroll; + } +} + +/// A trait for elements that want to use the standard GPUI event handlers that don't +/// require any state. +pub trait InteractiveElement: Sized { + /// Retrieve the interactivity state associated with this element + fn interactivity(&mut self) -> &mut Interactivity; + + /// Assign this element to a group of elements that can be styled together + fn group(mut self, group: impl Into) -> Self { + self.interactivity().group = Some(group.into()); + self + } + + /// Assign this element an ID, so that it can be used with interactivity + fn id(mut self, id: impl Into) -> Stateful { + self.interactivity().element_id = Some(id.into()); + + Stateful { element: self } + } + + /// Track the focus state of the given focus handle on this element. + /// If the focus handle is focused by the application, this element will + /// apply its focused styles. + fn track_focus(mut self, focus_handle: &FocusHandle) -> Self { + self.interactivity().focusable = true; + self.interactivity().tracked_focus_handle = Some(focus_handle.clone()); + self + } + + /// Set whether this element is a tab stop. + /// + /// When false, the element remains in tab-index order but cannot be reached via keyboard navigation. + /// Useful for container elements: focus the container, then call `window.focus_next()` to focus + /// the first tab stop inside it while having the container element itself be unreachable via the keyboard. + /// Should only be used with `tab_index`. + fn tab_stop(mut self, tab_stop: bool) -> Self { + self.interactivity().tab_stop = tab_stop; + self + } + + /// Set index of the tab stop order, and set this node as a tab stop. + /// This will default the element to being a tab stop. See [`Self::tab_stop`] for more information. + /// This should only be used in conjunction with `tab_group` + /// in order to not interfere with the tab index of other elements. + fn tab_index(mut self, index: isize) -> Self { + self.interactivity().focusable = true; + self.interactivity().tab_index = Some(index); + self.interactivity().tab_stop = true; + self + } + + /// Designate this div as a "tab group". Tab groups have their own location in the tab-index order, + /// but for children of the tab group, the tab index is reset to 0. This can be useful for swapping + /// the order of tab stops within the group, without having to renumber all the tab stops in the whole + /// application. + fn tab_group(mut self) -> Self { + self.interactivity().tab_group = true; + if self.interactivity().tab_index.is_none() { + self.interactivity().tab_index = Some(0); + } + self + } + + /// Set the keymap context for this element. This will be used to determine + /// which action to dispatch from the keymap. + fn key_context(mut self, key_context: C) -> Self + where + C: TryInto, + E: Debug, + { + if let Some(key_context) = key_context.try_into().log_err() { + self.interactivity().key_context = Some(key_context); + } + self + } + + /// Apply the given style to this element when the mouse hovers over it + fn hover(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self { + debug_assert!( + self.interactivity().hover_style.is_none(), + "hover style already set" + ); + self.interactivity().hover_style = Some(Box::new(f(StyleRefinement::default()))); + self + } + + /// Apply the given style to this element when the mouse hovers over a group member + fn group_hover( + mut self, + group_name: impl Into, + f: impl FnOnce(StyleRefinement) -> StyleRefinement, + ) -> Self { + self.interactivity().group_hover_style = Some(GroupStyle { + group: group_name.into(), + style: Box::new(f(StyleRefinement::default())), + }); + self + } + + /// Bind the given callback to the mouse down event for the given mouse button, + /// the fluent API equivalent to [`Interactivity::on_mouse_down`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to the view state from this callback. + fn on_mouse_down( + mut self, + button: MouseButton, + listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_mouse_down(button, listener); + self + } + + #[cfg(any(test, feature = "test-support"))] + /// Set a key that can be used to look up this element's bounds + /// in the [`crate::VisualTestContext::debug_bounds`] map + /// This is a noop in release builds + fn debug_selector(mut self, f: impl FnOnce() -> String) -> Self { + self.interactivity().debug_selector = Some(f()); + self + } + + #[cfg(not(any(test, feature = "test-support")))] + /// Set a key that can be used to look up this element's bounds + /// in the [`crate::VisualTestContext::debug_bounds`] map + /// This is a noop in release builds + #[inline] + fn debug_selector(self, _: impl FnOnce() -> String) -> Self { + self + } + + /// Bind the given callback to the mouse down event for any button, during the capture phase + /// the fluent API equivalent to [`Interactivity::capture_any_mouse_down`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn capture_any_mouse_down( + mut self, + listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().capture_any_mouse_down(listener); + self + } + + /// Bind the given callback to the mouse down event for any button, during the capture phase + /// the fluent API equivalent to [`Interactivity::on_any_mouse_down`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_any_mouse_down( + mut self, + listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_any_mouse_down(listener); + self + } + + /// Bind the given callback to the mouse up event for the given button, during the bubble phase + /// the fluent API equivalent to [`Interactivity::on_mouse_up`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_mouse_up( + mut self, + button: MouseButton, + listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_mouse_up(button, listener); + self + } + + /// Bind the given callback to the mouse up event for any button, during the capture phase + /// the fluent API equivalent to [`Interactivity::capture_any_mouse_up`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn capture_any_mouse_up( + mut self, + listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().capture_any_mouse_up(listener); + self + } + + /// Bind the given callback to the mouse down event, on any button, during the capture phase, + /// when the mouse is outside of the bounds of this element. + /// The fluent API equivalent to [`Interactivity::on_mouse_down_out`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_mouse_down_out( + mut self, + listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_mouse_down_out(listener); + self + } + + /// Bind the given callback to the mouse up event, for the given button, during the capture phase, + /// when the mouse is outside of the bounds of this element. + /// The fluent API equivalent to [`Interactivity::on_mouse_up_out`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_mouse_up_out( + mut self, + button: MouseButton, + listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_mouse_up_out(button, listener); + self + } + + /// Bind the given callback to the mouse move event, during the bubble phase + /// The fluent API equivalent to [`Interactivity::on_mouse_move`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_mouse_move( + mut self, + listener: impl Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_mouse_move(listener); + self + } + + /// Bind the given callback to the mouse drag event of the given type. Note that this + /// will be called for all move events, inside or outside of this element, as long as the + /// drag was started with this element under the mouse. Useful for implementing draggable + /// UIs that don't conform to a drag and drop style interaction, like resizing. + /// The fluent API equivalent to [`Interactivity::on_drag_move`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_drag_move( + mut self, + listener: impl Fn(&DragMoveEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_drag_move(listener); + self + } + + /// Bind the given callback to scroll wheel events during the bubble phase + /// The fluent API equivalent to [`Interactivity::on_scroll_wheel`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_scroll_wheel( + mut self, + listener: impl Fn(&ScrollWheelEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_scroll_wheel(listener); + self + } + + /// Capture the given action, before normal action dispatch can fire + /// The fluent API equivalent to [`Interactivity::on_scroll_wheel`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn capture_action( + mut self, + listener: impl Fn(&A, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().capture_action(listener); + self + } + + /// Bind the given callback to an action dispatch during the bubble phase + /// The fluent API equivalent to [`Interactivity::on_action`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_action( + mut self, + listener: impl Fn(&A, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_action(listener); + self + } + + /// Bind the given callback to an action dispatch, based on a dynamic action parameter + /// instead of a type parameter. Useful for component libraries that want to expose + /// action bindings to their users. + /// The fluent API equivalent to [`Interactivity::on_boxed_action`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_boxed_action( + mut self, + action: &dyn Action, + listener: impl Fn(&dyn Action, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_boxed_action(action, listener); + self + } + + /// Bind the given callback to key down events during the bubble phase + /// The fluent API equivalent to [`Interactivity::on_key_down`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_key_down( + mut self, + listener: impl Fn(&KeyDownEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_key_down(listener); + self + } + + /// Bind the given callback to key down events during the capture phase + /// The fluent API equivalent to [`Interactivity::capture_key_down`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn capture_key_down( + mut self, + listener: impl Fn(&KeyDownEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().capture_key_down(listener); + self + } + + /// Bind the given callback to key up events during the bubble phase + /// The fluent API equivalent to [`Interactivity::on_key_up`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_key_up( + mut self, + listener: impl Fn(&KeyUpEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_key_up(listener); + self + } + + /// Bind the given callback to key up events during the capture phase + /// The fluent API equivalent to [`Interactivity::capture_key_up`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn capture_key_up( + mut self, + listener: impl Fn(&KeyUpEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().capture_key_up(listener); + self + } + + /// Bind the given callback to modifiers changing events. + /// The fluent API equivalent to [`Interactivity::on_modifiers_changed`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_modifiers_changed( + mut self, + listener: impl Fn(&ModifiersChangedEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_modifiers_changed(listener); + self + } + + /// Apply the given style when the given data type is dragged over this element + fn drag_over( + mut self, + f: impl 'static + Fn(StyleRefinement, &S, &mut Window, &mut App) -> StyleRefinement, + ) -> Self { + self.interactivity().drag_over_styles.push(( + TypeId::of::(), + Box::new(move |currently_dragged: &dyn Any, window, cx| { + f( + StyleRefinement::default(), + currently_dragged.downcast_ref::().unwrap(), + window, + cx, + ) + }), + )); + self + } + + /// Apply the given style when the given data type is dragged over this element's group + fn group_drag_over( + mut self, + group_name: impl Into, + f: impl FnOnce(StyleRefinement) -> StyleRefinement, + ) -> Self { + self.interactivity().group_drag_over_styles.push(( + TypeId::of::(), + GroupStyle { + group: group_name.into(), + style: Box::new(f(StyleRefinement::default())), + }, + )); + self + } + + /// Bind the given callback to drop events of the given type, whether or not the drag started on this element + /// The fluent API equivalent to [`Interactivity::on_drop`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_drop( + mut self, + listener: impl Fn(&T, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_drop(listener); + self + } + + /// Use the given predicate to determine whether or not a drop event should be dispatched to this element + /// The fluent API equivalent to [`Interactivity::can_drop`] + fn can_drop( + mut self, + predicate: impl Fn(&dyn Any, &mut Window, &mut App) -> bool + 'static, + ) -> Self { + self.interactivity().can_drop(predicate); + self + } + + /// Block the mouse from all interactions with elements behind this element's hitbox. Typically + /// `block_mouse_except_scroll` should be preferred. + /// The fluent API equivalent to [`Interactivity::occlude_mouse`] + fn occlude(mut self) -> Self { + self.interactivity().occlude_mouse(); + self + } + + /// Set the bounds of this element as a window control area for the platform window. + /// The fluent API equivalent to [`Interactivity::window_control_area`] + fn window_control_area(mut self, area: WindowControlArea) -> Self { + self.interactivity().window_control_area(area); + self + } + + /// Block non-scroll mouse interactions with elements behind this element's hitbox. See + /// [`Hitbox::is_hovered`] for details. + /// + /// The fluent API equivalent to [`Interactivity::block_mouse_except_scroll`] + fn block_mouse_except_scroll(mut self) -> Self { + self.interactivity().block_mouse_except_scroll(); + self + } + + /// Set the given styles to be applied when this element, specifically, is focused. + /// Requires that the element is focusable. Elements can be made focusable using [`InteractiveElement::track_focus`]. + fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self + where + Self: Sized, + { + self.interactivity().focus_style = Some(Box::new(f(StyleRefinement::default()))); + self + } + + /// Set the given styles to be applied when this element is inside another element that is focused. + /// Requires that the element is focusable. Elements can be made focusable using [`InteractiveElement::track_focus`]. + fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self + where + Self: Sized, + { + self.interactivity().in_focus_style = Some(Box::new(f(StyleRefinement::default()))); + self + } +} + +/// A trait for elements that want to use the standard GPUI interactivity features +/// that require state. +pub trait StatefulInteractiveElement: InteractiveElement { + /// Set this element to focusable. + fn focusable(mut self) -> Self { + self.interactivity().focusable = true; + self + } + + /// Set the overflow x and y to scroll. + fn overflow_scroll(mut self) -> Self { + self.interactivity().base_style.overflow.x = Some(Overflow::Scroll); + self.interactivity().base_style.overflow.y = Some(Overflow::Scroll); + self + } + + /// Set the overflow x to scroll. + fn overflow_x_scroll(mut self) -> Self { + self.interactivity().base_style.overflow.x = Some(Overflow::Scroll); + self + } + + /// Set the overflow y to scroll. + fn overflow_y_scroll(mut self) -> Self { + self.interactivity().base_style.overflow.y = Some(Overflow::Scroll); + self + } + + /// Set the space to be reserved for rendering the scrollbar. + /// + /// This will only affect the layout of the element when overflow for this element is set to + /// `Overflow::Scroll`. + fn scrollbar_width(mut self, width: impl Into) -> Self { + self.interactivity().base_style.scrollbar_width = Some(width.into()); + self + } + + /// Track the scroll state of this element with the given handle. + fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self { + self.interactivity().tracked_scroll_handle = Some(scroll_handle.clone()); + self + } + + /// Track the scroll state of this element with the given handle. + fn anchor_scroll(mut self, scroll_anchor: Option) -> Self { + self.interactivity().scroll_anchor = scroll_anchor; + self + } + + /// Set the given styles to be applied when this element is active. + fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self + where + Self: Sized, + { + self.interactivity().active_style = Some(Box::new(f(StyleRefinement::default()))); + self + } + + /// Set the given styles to be applied when this element's group is active. + fn group_active( + mut self, + group_name: impl Into, + f: impl FnOnce(StyleRefinement) -> StyleRefinement, + ) -> Self + where + Self: Sized, + { + self.interactivity().group_active_style = Some(GroupStyle { + group: group_name.into(), + style: Box::new(f(StyleRefinement::default())), + }); + self + } + + /// Bind the given callback to click events of this element + /// The fluent API equivalent to [`Interactivity::on_click`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_click(mut self, listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self + where + Self: Sized, + { + self.interactivity().on_click(listener); + self + } + + /// On drag initiation, this callback will be used to create a new view to render the dragged value for a + /// drag and drop operation. This API should also be used as the equivalent of 'on drag start' with + /// the [`InteractiveElement::on_drag_move`] API. + /// The callback also has access to the offset of triggering click from the origin of parent element. + /// The fluent API equivalent to [`Interactivity::on_drag`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_drag( + mut self, + value: T, + constructor: impl Fn(&T, Point, &mut Window, &mut App) -> Entity + 'static, + ) -> Self + where + Self: Sized, + T: 'static, + W: 'static + Render, + { + self.interactivity().on_drag(value, constructor); + self + } + + /// Bind the given callback on the hover start and end events of this element. Note that the boolean + /// passed to the callback is true when the hover starts and false when it ends. + /// The fluent API equivalent to [`Interactivity::on_hover`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_hover(mut self, listener: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self + where + Self: Sized, + { + self.interactivity().on_hover(listener); + self + } + + /// Use the given callback to construct a new tooltip view when the mouse hovers over this element. + /// The fluent API equivalent to [`Interactivity::tooltip`] + fn tooltip(mut self, build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self + where + Self: Sized, + { + self.interactivity().tooltip(build_tooltip); + self + } + + /// Use the given callback to construct a new tooltip view when the mouse hovers over this element. + /// The tooltip itself is also hoverable and won't disappear when the user moves the mouse into + /// the tooltip. The fluent API equivalent to [`Interactivity::hoverable_tooltip`] + fn hoverable_tooltip( + mut self, + build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static, + ) -> Self + where + Self: Sized, + { + self.interactivity().hoverable_tooltip(build_tooltip); + self + } +} + +pub(crate) type MouseDownListener = + Box; +pub(crate) type MouseUpListener = + Box; + +pub(crate) type MouseMoveListener = + Box; + +pub(crate) type ScrollWheelListener = + Box; + +pub(crate) type ClickListener = Rc; + +pub(crate) type DragListener = + Box, &mut Window, &mut App) -> AnyView + 'static>; + +type DropListener = Box; + +type CanDropPredicate = Box bool + 'static>; + +pub(crate) struct TooltipBuilder { + build: Rc AnyView + 'static>, + hoverable: bool, +} + +pub(crate) type KeyDownListener = + Box; + +pub(crate) type KeyUpListener = + Box; + +pub(crate) type ModifiersChangedListener = + Box; + +pub(crate) type ActionListener = + Box; + +/// Construct a new [`Div`] element +#[track_caller] +pub fn div() -> Div { + Div { + interactivity: Interactivity::new(), + children: SmallVec::default(), + prepaint_listener: None, + image_cache: None, + } +} + +/// A [`Div`] element, the all-in-one element for building complex UIs in GPUI +pub struct Div { + interactivity: Interactivity, + children: SmallVec<[StackSafe; 2]>, + prepaint_listener: Option>, &mut Window, &mut App) + 'static>>, + image_cache: Option>, +} + +impl Div { + /// Add a listener to be called when the children of this `Div` are prepainted. + /// This allows you to store the [`Bounds`] of the children for later use. + pub fn on_children_prepainted( + mut self, + listener: impl Fn(Vec>, &mut Window, &mut App) + 'static, + ) -> Self { + self.prepaint_listener = Some(Box::new(listener)); + self + } + + /// Add an image cache at the location of this div in the element tree. + pub fn image_cache(mut self, cache: impl ImageCacheProvider) -> Self { + self.image_cache = Some(Box::new(cache)); + self + } +} + +/// A frame state for a `Div` element, which contains layout IDs for its children. +/// +/// This struct is used internally by the `Div` element to manage the layout state of its children +/// during the UI update cycle. It holds a small vector of `LayoutId` values, each corresponding to +/// a child element of the `Div`. These IDs are used to query the layout engine for the computed +/// bounds of the children after the layout phase is complete. +pub struct DivFrameState { + child_layout_ids: SmallVec<[LayoutId; 2]>, +} + +/// Interactivity state displayed an manipulated in the inspector. +#[derive(Clone)] +pub struct DivInspectorState { + /// The inspected element's base style. This is used for both inspecting and modifying the + /// state. In the future it will make sense to separate the read and write, possibly tracking + /// the modifications. + #[cfg(any(feature = "inspector", debug_assertions))] + pub base_style: Box, + /// Inspects the bounds of the element. + pub bounds: Bounds, + /// Size of the children of the element, or `bounds.size` if it has no children. + pub content_size: Size, +} + +impl Styled for Div { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.interactivity.base_style + } +} + +impl InteractiveElement for Div { + fn interactivity(&mut self) -> &mut Interactivity { + &mut self.interactivity + } +} + +impl ParentElement for Div { + fn extend(&mut self, elements: impl IntoIterator) { + self.children + .extend(elements.into_iter().map(StackSafe::new)) + } +} + +impl Element for Div { + type RequestLayoutState = DivFrameState; + type PrepaintState = Option; + + fn id(&self) -> Option { + self.interactivity.element_id.clone() + } + + fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { + self.interactivity.source_location() + } + + #[stacksafe] + fn request_layout( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let mut child_layout_ids = SmallVec::new(); + let image_cache = self + .image_cache + .as_mut() + .map(|provider| provider.provide(window, cx)); + + let layout_id = window.with_image_cache(image_cache, |window| { + self.interactivity.request_layout( + global_id, + inspector_id, + window, + cx, + |style, window, cx| { + window.with_text_style(style.text_style().cloned(), |window| { + child_layout_ids = self + .children + .iter_mut() + .map(|child| child.request_layout(window, cx)) + .collect::>(); + window.request_layout(style, child_layout_ids.iter().copied(), cx) + }) + }, + ) + }); + + (layout_id, DivFrameState { child_layout_ids }) + } + + #[stacksafe] + fn prepaint( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + request_layout: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Option { + let has_prepaint_listener = self.prepaint_listener.is_some(); + let mut children_bounds = Vec::with_capacity(if has_prepaint_listener { + request_layout.child_layout_ids.len() + } else { + 0 + }); + + let mut child_min = point(Pixels::MAX, Pixels::MAX); + let mut child_max = Point::default(); + if let Some(handle) = self.interactivity.scroll_anchor.as_ref() { + *handle.last_origin.borrow_mut() = bounds.origin - window.element_offset(); + } + let content_size = if request_layout.child_layout_ids.is_empty() { + bounds.size + } else if let Some(scroll_handle) = self.interactivity.tracked_scroll_handle.as_ref() { + let mut state = scroll_handle.0.borrow_mut(); + state.child_bounds = Vec::with_capacity(request_layout.child_layout_ids.len()); + for child_layout_id in &request_layout.child_layout_ids { + let child_bounds = window.layout_bounds(*child_layout_id); + child_min = child_min.min(&child_bounds.origin); + child_max = child_max.max(&child_bounds.bottom_right()); + state.child_bounds.push(child_bounds); + } + (child_max - child_min).into() + } else { + for child_layout_id in &request_layout.child_layout_ids { + let child_bounds = window.layout_bounds(*child_layout_id); + child_min = child_min.min(&child_bounds.origin); + child_max = child_max.max(&child_bounds.bottom_right()); + + if has_prepaint_listener { + children_bounds.push(child_bounds); + } + } + (child_max - child_min).into() + }; + + if let Some(scroll_handle) = self.interactivity.tracked_scroll_handle.as_ref() { + scroll_handle.scroll_to_active_item(); + } + + self.interactivity.prepaint( + global_id, + inspector_id, + bounds, + content_size, + window, + cx, + |style, scroll_offset, hitbox, window, cx| { + // skip children + if style.display == Display::None { + return hitbox; + } + + window.with_element_offset(scroll_offset, |window| { + for child in &mut self.children { + child.prepaint(window, cx); + } + }); + + if let Some(listener) = self.prepaint_listener.as_ref() { + listener(children_bounds, window, cx); + } + + hitbox + }, + ) + } + + #[stacksafe] + fn paint( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + hitbox: &mut Option, + window: &mut Window, + cx: &mut App, + ) { + let image_cache = self + .image_cache + .as_mut() + .map(|provider| provider.provide(window, cx)); + + window.with_image_cache(image_cache, |window| { + self.interactivity.paint( + global_id, + inspector_id, + bounds, + hitbox.as_ref(), + window, + cx, + |style, window, cx| { + // skip children + if style.display == Display::None { + return; + } + + for child in &mut self.children { + child.paint(window, cx); + } + }, + ) + }); + } +} + +impl IntoElement for Div { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +/// The interactivity struct. Powers all of the general-purpose +/// interactivity in the `Div` element. +#[derive(Default)] +pub struct Interactivity { + /// The element ID of the element. In id is required to support a stateful subset of the interactivity such as on_click. + pub element_id: Option, + /// Whether the element was clicked. This will only be present after layout. + pub active: Option, + /// Whether the element was hovered. This will only be present after paint if an hitbox + /// was created for the interactive element. + pub hovered: Option, + pub(crate) tooltip_id: Option, + pub(crate) content_size: Size, + pub(crate) key_context: Option, + pub(crate) focusable: bool, + pub(crate) tracked_focus_handle: Option, + pub(crate) tracked_scroll_handle: Option, + pub(crate) scroll_anchor: Option, + pub(crate) scroll_offset: Option>>>, + pub(crate) group: Option, + /// The base style of the element, before any modifications are applied + /// by focus, active, etc. + pub base_style: Box, + pub(crate) focus_style: Option>, + pub(crate) in_focus_style: Option>, + pub(crate) hover_style: Option>, + pub(crate) group_hover_style: Option, + pub(crate) active_style: Option>, + pub(crate) group_active_style: Option, + pub(crate) drag_over_styles: Vec<( + TypeId, + Box StyleRefinement>, + )>, + pub(crate) group_drag_over_styles: Vec<(TypeId, GroupStyle)>, + pub(crate) mouse_down_listeners: Vec, + pub(crate) mouse_up_listeners: Vec, + pub(crate) mouse_move_listeners: Vec, + pub(crate) scroll_wheel_listeners: Vec, + pub(crate) key_down_listeners: Vec, + pub(crate) key_up_listeners: Vec, + pub(crate) modifiers_changed_listeners: Vec, + pub(crate) action_listeners: Vec<(TypeId, ActionListener)>, + pub(crate) drop_listeners: Vec<(TypeId, DropListener)>, + pub(crate) can_drop_predicate: Option, + pub(crate) click_listeners: Vec, + pub(crate) drag_listener: Option<(Arc, DragListener)>, + pub(crate) hover_listener: Option>, + pub(crate) tooltip_builder: Option, + pub(crate) window_control: Option, + pub(crate) hitbox_behavior: HitboxBehavior, + pub(crate) tab_index: Option, + pub(crate) tab_group: bool, + pub(crate) tab_stop: bool, + + #[cfg(any(feature = "inspector", debug_assertions))] + pub(crate) source_location: Option<&'static core::panic::Location<'static>>, + + #[cfg(any(test, feature = "test-support"))] + pub(crate) debug_selector: Option, +} + +impl Interactivity { + /// Layout this element according to this interactivity state's configured styles + pub fn request_layout( + &mut self, + global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + f: impl FnOnce(Style, &mut Window, &mut App) -> LayoutId, + ) -> LayoutId { + #[cfg(any(feature = "inspector", debug_assertions))] + window.with_inspector_state( + _inspector_id, + cx, + |inspector_state: &mut Option, _window| { + if let Some(inspector_state) = inspector_state { + self.base_style = inspector_state.base_style.clone(); + } else { + *inspector_state = Some(DivInspectorState { + base_style: self.base_style.clone(), + bounds: Default::default(), + content_size: Default::default(), + }) + } + }, + ); + + window.with_optional_element_state::( + global_id, + |element_state, window| { + let mut element_state = + element_state.map(|element_state| element_state.unwrap_or_default()); + + if let Some(element_state) = element_state.as_ref() + && cx.has_active_drag() + { + if let Some(pending_mouse_down) = element_state.pending_mouse_down.as_ref() { + *pending_mouse_down.borrow_mut() = None; + } + if let Some(clicked_state) = element_state.clicked_state.as_ref() { + *clicked_state.borrow_mut() = ElementClickedState::default(); + } + } + + // Ensure we store a focus handle in our element state if we're focusable. + // If there's an explicit focus handle we're tracking, use that. Otherwise + // create a new handle and store it in the element state, which lives for as + // as frames contain an element with this id. + if self.focusable + && self.tracked_focus_handle.is_none() + && let Some(element_state) = element_state.as_mut() + { + let mut handle = element_state + .focus_handle + .get_or_insert_with(|| cx.focus_handle()) + .clone() + .tab_stop(self.tab_stop); + + if let Some(index) = self.tab_index { + handle = handle.tab_index(index); + } + + self.tracked_focus_handle = Some(handle); + } + + if let Some(scroll_handle) = self.tracked_scroll_handle.as_ref() { + self.scroll_offset = Some(scroll_handle.0.borrow().offset.clone()); + } else if (self.base_style.overflow.x == Some(Overflow::Scroll) + || self.base_style.overflow.y == Some(Overflow::Scroll)) + && let Some(element_state) = element_state.as_mut() + { + self.scroll_offset = Some( + element_state + .scroll_offset + .get_or_insert_with(Rc::default) + .clone(), + ); + } + + let style = self.compute_style_internal(None, element_state.as_mut(), window, cx); + let layout_id = f(style, window, cx); + (layout_id, element_state) + }, + ) + } + + /// Commit the bounds of this element according to this interactivity state's configured styles. + pub fn prepaint( + &mut self, + global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + content_size: Size, + window: &mut Window, + cx: &mut App, + f: impl FnOnce(&Style, Point, Option, &mut Window, &mut App) -> R, + ) -> R { + self.content_size = content_size; + + #[cfg(any(feature = "inspector", debug_assertions))] + window.with_inspector_state( + _inspector_id, + cx, + |inspector_state: &mut Option, _window| { + if let Some(inspector_state) = inspector_state { + inspector_state.bounds = bounds; + inspector_state.content_size = content_size; + } + }, + ); + + if let Some(focus_handle) = self.tracked_focus_handle.as_ref() { + window.set_focus_handle(focus_handle, cx); + } + window.with_optional_element_state::( + global_id, + |element_state, window| { + let mut element_state = + element_state.map(|element_state| element_state.unwrap_or_default()); + let style = self.compute_style_internal(None, element_state.as_mut(), window, cx); + + if let Some(element_state) = element_state.as_mut() { + if let Some(clicked_state) = element_state.clicked_state.as_ref() { + let clicked_state = clicked_state.borrow(); + self.active = Some(clicked_state.element); + } + if let Some(active_tooltip) = element_state.active_tooltip.as_ref() { + if self.tooltip_builder.is_some() { + self.tooltip_id = set_tooltip_on_window(active_tooltip, window); + } else { + // If there is no longer a tooltip builder, remove the active tooltip. + element_state.active_tooltip.take(); + } + } + } + + window.with_text_style(style.text_style().cloned(), |window| { + window.with_content_mask( + style.overflow_mask(bounds, window.rem_size()), + |window| { + let hitbox = if self.should_insert_hitbox(&style, window, cx) { + Some(window.insert_hitbox(bounds, self.hitbox_behavior)) + } else { + None + }; + + let scroll_offset = + self.clamp_scroll_position(bounds, &style, window, cx); + let result = f(&style, scroll_offset, hitbox, window, cx); + (result, element_state) + }, + ) + }) + }, + ) + } + + fn should_insert_hitbox(&self, style: &Style, window: &Window, cx: &App) -> bool { + self.hitbox_behavior != HitboxBehavior::Normal + || self.window_control.is_some() + || style.mouse_cursor.is_some() + || self.group.is_some() + || self.scroll_offset.is_some() + || self.tracked_focus_handle.is_some() + || self.hover_style.is_some() + || self.group_hover_style.is_some() + || self.hover_listener.is_some() + || !self.mouse_up_listeners.is_empty() + || !self.mouse_down_listeners.is_empty() + || !self.mouse_move_listeners.is_empty() + || !self.click_listeners.is_empty() + || !self.scroll_wheel_listeners.is_empty() + || self.drag_listener.is_some() + || !self.drop_listeners.is_empty() + || self.tooltip_builder.is_some() + || window.is_inspector_picking(cx) + } + + fn clamp_scroll_position( + &self, + bounds: Bounds, + style: &Style, + window: &mut Window, + _cx: &mut App, + ) -> Point { + fn round_to_two_decimals(pixels: Pixels) -> Pixels { + const ROUNDING_FACTOR: f32 = 100.0; + (pixels * ROUNDING_FACTOR).round() / ROUNDING_FACTOR + } + + if let Some(scroll_offset) = self.scroll_offset.as_ref() { + let mut scroll_to_bottom = false; + let mut tracked_scroll_handle = self + .tracked_scroll_handle + .as_ref() + .map(|handle| handle.0.borrow_mut()); + if let Some(mut scroll_handle_state) = tracked_scroll_handle.as_deref_mut() { + scroll_handle_state.overflow = style.overflow; + scroll_to_bottom = mem::take(&mut scroll_handle_state.scroll_to_bottom); + } + + let rem_size = window.rem_size(); + let padding = style.padding.to_pixels(bounds.size.into(), rem_size); + let padding_size = size(padding.left + padding.right, padding.top + padding.bottom); + // The floating point values produced by Taffy and ours often vary + // slightly after ~5 decimal places. This can lead to cases where after + // subtracting these, the container becomes scrollable for less than + // 0.00000x pixels. As we generally don't benefit from a precision that + // high for the maximum scroll, we round the scroll max to 2 decimal + // places here. + let padded_content_size = self.content_size + padding_size; + let scroll_max = (padded_content_size - bounds.size) + .map(round_to_two_decimals) + .max(&Default::default()); + // Clamp scroll offset in case scroll max is smaller now (e.g., if children + // were removed or the bounds became larger). + let mut scroll_offset = scroll_offset.borrow_mut(); + + scroll_offset.x = scroll_offset.x.clamp(-scroll_max.width, px(0.)); + if scroll_to_bottom { + scroll_offset.y = -scroll_max.height; + } else { + scroll_offset.y = scroll_offset.y.clamp(-scroll_max.height, px(0.)); + } + + if let Some(mut scroll_handle_state) = tracked_scroll_handle { + scroll_handle_state.max_offset = scroll_max; + scroll_handle_state.bounds = bounds; + } + + *scroll_offset + } else { + Point::default() + } + } + + /// Paint this element according to this interactivity state's configured styles + /// and bind the element's mouse and keyboard events. + /// + /// content_size is the size of the content of the element, which may be larger than the + /// element's bounds if the element is scrollable. + /// + /// the final computed style will be passed to the provided function, along + /// with the current scroll offset + pub fn paint( + &mut self, + global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + hitbox: Option<&Hitbox>, + window: &mut Window, + cx: &mut App, + f: impl FnOnce(&Style, &mut Window, &mut App), + ) { + self.hovered = hitbox.map(|hitbox| hitbox.is_hovered(window)); + window.with_optional_element_state::( + global_id, + |element_state, window| { + let mut element_state = + element_state.map(|element_state| element_state.unwrap_or_default()); + + let style = self.compute_style_internal(hitbox, element_state.as_mut(), window, cx); + + #[cfg(any(feature = "test-support", test))] + if let Some(debug_selector) = &self.debug_selector { + window + .next_frame + .debug_bounds + .insert(debug_selector.clone(), bounds); + } + + self.paint_hover_group_handler(window, cx); + + if style.visibility == Visibility::Hidden { + return ((), element_state); + } + + let mut tab_group = None; + if self.tab_group { + tab_group = self.tab_index; + } + if let Some(focus_handle) = &self.tracked_focus_handle { + window.next_frame.tab_stops.insert(focus_handle); + } + + window.with_element_opacity(style.opacity, |window| { + style.paint(bounds, window, cx, |window: &mut Window, cx: &mut App| { + window.with_text_style(style.text_style().cloned(), |window| { + window.with_content_mask( + style.overflow_mask(bounds, window.rem_size()), + |window| { + window.with_tab_group(tab_group, |window| { + if let Some(hitbox) = hitbox { + #[cfg(debug_assertions)] + self.paint_debug_info( + global_id, hitbox, &style, window, cx, + ); + + if let Some(drag) = cx.active_drag.as_ref() { + if let Some(mouse_cursor) = drag.cursor_style { + window.set_window_cursor_style(mouse_cursor); + } + } else { + if let Some(mouse_cursor) = style.mouse_cursor { + window.set_cursor_style(mouse_cursor, hitbox); + } + } + + if let Some(group) = self.group.clone() { + GroupHitboxes::push(group, hitbox.id, cx); + } + + if let Some(area) = self.window_control { + window.insert_window_control_hitbox( + area, + hitbox.clone(), + ); + } + + self.paint_mouse_listeners( + hitbox, + element_state.as_mut(), + window, + cx, + ); + self.paint_scroll_listener(hitbox, &style, window, cx); + } + + self.paint_keyboard_listeners(window, cx); + f(&style, window, cx); + + if let Some(_hitbox) = hitbox { + #[cfg(any(feature = "inspector", debug_assertions))] + window.insert_inspector_hitbox( + _hitbox.id, + _inspector_id, + cx, + ); + + if let Some(group) = self.group.as_ref() { + GroupHitboxes::pop(group, cx); + } + } + }) + }, + ); + }); + }); + }); + + ((), element_state) + }, + ); + } + + #[cfg(debug_assertions)] + fn paint_debug_info( + &self, + global_id: Option<&GlobalElementId>, + hitbox: &Hitbox, + style: &Style, + window: &mut Window, + cx: &mut App, + ) { + use crate::{BorderStyle, TextAlign}; + + if global_id.is_some() + && (style.debug || style.debug_below || cx.has_global::()) + && hitbox.is_hovered(window) + { + const FONT_SIZE: crate::Pixels = crate::Pixels(10.); + let element_id = format!("{:?}", global_id.unwrap()); + let str_len = element_id.len(); + + let render_debug_text = |window: &mut Window| { + if let Some(text) = window + .text_system() + .shape_text( + element_id.into(), + FONT_SIZE, + &[window.text_style().to_run(str_len)], + None, + None, + ) + .ok() + .and_then(|mut text| text.pop()) + { + text.paint(hitbox.origin, FONT_SIZE, TextAlign::Left, None, window, cx) + .ok(); + + let text_bounds = crate::Bounds { + origin: hitbox.origin, + size: text.size(FONT_SIZE), + }; + if self.source_location.is_some() + && text_bounds.contains(&window.mouse_position()) + && window.modifiers().secondary() + { + let secondary_held = window.modifiers().secondary(); + window.on_key_event({ + move |e: &crate::ModifiersChangedEvent, _phase, window, _cx| { + if e.modifiers.secondary() != secondary_held + && text_bounds.contains(&window.mouse_position()) + { + window.refresh(); + } + } + }); + + let was_hovered = hitbox.is_hovered(window); + let current_view = window.current_view(); + window.on_mouse_event({ + let hitbox = hitbox.clone(); + move |_: &MouseMoveEvent, phase, window, cx| { + if phase == DispatchPhase::Capture { + let hovered = hitbox.is_hovered(window); + if hovered != was_hovered { + cx.notify(current_view) + } + } + } + }); + + window.on_mouse_event({ + let hitbox = hitbox.clone(); + let location = self.source_location.unwrap(); + move |e: &crate::MouseDownEvent, phase, window, cx| { + if text_bounds.contains(&e.position) + && phase.capture() + && hitbox.is_hovered(window) + { + cx.stop_propagation(); + let Ok(dir) = std::env::current_dir() else { + return; + }; + + eprintln!( + "This element was created at:\n{}:{}:{}", + dir.join(location.file()).to_string_lossy(), + location.line(), + location.column() + ); + } + } + }); + window.paint_quad(crate::outline( + crate::Bounds { + origin: hitbox.origin + + crate::point(crate::px(0.), FONT_SIZE - px(2.)), + size: crate::Size { + width: text_bounds.size.width, + height: crate::px(1.), + }, + }, + crate::red(), + BorderStyle::default(), + )) + } + } + }; + + window.with_text_style( + Some(crate::TextStyleRefinement { + color: Some(crate::red()), + line_height: Some(FONT_SIZE.into()), + background_color: Some(crate::white()), + ..Default::default() + }), + render_debug_text, + ) + } + } + + fn paint_mouse_listeners( + &mut self, + hitbox: &Hitbox, + element_state: Option<&mut InteractiveElementState>, + window: &mut Window, + cx: &mut App, + ) { + let is_focused = self + .tracked_focus_handle + .as_ref() + .map(|handle| handle.is_focused(window)) + .unwrap_or(false); + + // If this element can be focused, register a mouse down listener + // that will automatically transfer focus when hitting the element. + // This behavior can be suppressed by using `cx.prevent_default()`. + if let Some(focus_handle) = self.tracked_focus_handle.clone() { + let hitbox = hitbox.clone(); + window.on_mouse_event(move |_: &MouseDownEvent, phase, window, _| { + if phase == DispatchPhase::Bubble + && hitbox.is_hovered(window) + && !window.default_prevented() + { + window.focus(&focus_handle); + // If there is a parent that is also focusable, prevent it + // from transferring focus because we already did so. + window.prevent_default(); + } + }); + } + + for listener in self.mouse_down_listeners.drain(..) { + let hitbox = hitbox.clone(); + window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| { + listener(event, phase, &hitbox, window, cx); + }) + } + + for listener in self.mouse_up_listeners.drain(..) { + let hitbox = hitbox.clone(); + window.on_mouse_event(move |event: &MouseUpEvent, phase, window, cx| { + listener(event, phase, &hitbox, window, cx); + }) + } + + for listener in self.mouse_move_listeners.drain(..) { + let hitbox = hitbox.clone(); + window.on_mouse_event(move |event: &MouseMoveEvent, phase, window, cx| { + listener(event, phase, &hitbox, window, cx); + }) + } + + for listener in self.scroll_wheel_listeners.drain(..) { + let hitbox = hitbox.clone(); + window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| { + listener(event, phase, &hitbox, window, cx); + }) + } + + if self.hover_style.is_some() + || self.base_style.mouse_cursor.is_some() + || cx.active_drag.is_some() && !self.drag_over_styles.is_empty() + { + let hitbox = hitbox.clone(); + let was_hovered = hitbox.is_hovered(window); + let current_view = window.current_view(); + window.on_mouse_event(move |_: &MouseMoveEvent, phase, window, cx| { + let hovered = hitbox.is_hovered(window); + if phase == DispatchPhase::Capture && hovered != was_hovered { + cx.notify(current_view); + } + }); + } + let drag_cursor_style = self.base_style.as_ref().mouse_cursor; + + let mut drag_listener = mem::take(&mut self.drag_listener); + let drop_listeners = mem::take(&mut self.drop_listeners); + let click_listeners = mem::take(&mut self.click_listeners); + let can_drop_predicate = mem::take(&mut self.can_drop_predicate); + + if !drop_listeners.is_empty() { + let hitbox = hitbox.clone(); + window.on_mouse_event({ + move |_: &MouseUpEvent, phase, window, cx| { + if let Some(drag) = &cx.active_drag + && phase == DispatchPhase::Bubble + && hitbox.is_hovered(window) + { + let drag_state_type = drag.value.as_ref().type_id(); + for (drop_state_type, listener) in &drop_listeners { + if *drop_state_type == drag_state_type { + let drag = cx + .active_drag + .take() + .expect("checked for type drag state type above"); + + let mut can_drop = true; + if let Some(predicate) = &can_drop_predicate { + can_drop = predicate(drag.value.as_ref(), window, cx); + } + + if can_drop { + listener(drag.value.as_ref(), window, cx); + window.refresh(); + cx.stop_propagation(); + } + } + } + } + } + }); + } + + if let Some(element_state) = element_state { + if !click_listeners.is_empty() || drag_listener.is_some() { + let pending_mouse_down = element_state + .pending_mouse_down + .get_or_insert_with(Default::default) + .clone(); + + let clicked_state = element_state + .clicked_state + .get_or_insert_with(Default::default) + .clone(); + + window.on_mouse_event({ + let pending_mouse_down = pending_mouse_down.clone(); + let hitbox = hitbox.clone(); + move |event: &MouseDownEvent, phase, window, _cx| { + if phase == DispatchPhase::Bubble + && event.button == MouseButton::Left + && hitbox.is_hovered(window) + { + *pending_mouse_down.borrow_mut() = Some(event.clone()); + window.refresh(); + } + } + }); + + window.on_mouse_event({ + let pending_mouse_down = pending_mouse_down.clone(); + let hitbox = hitbox.clone(); + move |event: &MouseMoveEvent, phase, window, cx| { + if phase == DispatchPhase::Capture { + return; + } + + let mut pending_mouse_down = pending_mouse_down.borrow_mut(); + if let Some(mouse_down) = pending_mouse_down.clone() + && !cx.has_active_drag() + && (event.position - mouse_down.position).magnitude() > DRAG_THRESHOLD + && let Some((drag_value, drag_listener)) = drag_listener.take() + { + *clicked_state.borrow_mut() = ElementClickedState::default(); + let cursor_offset = event.position - hitbox.origin; + let drag = + (drag_listener)(drag_value.as_ref(), cursor_offset, window, cx); + cx.active_drag = Some(AnyDrag { + view: drag, + value: drag_value, + cursor_offset, + cursor_style: drag_cursor_style, + }); + pending_mouse_down.take(); + window.refresh(); + cx.stop_propagation(); + } + } + }); + + if is_focused { + // Press enter, space to trigger click, when the element is focused. + window.on_key_event({ + let click_listeners = click_listeners.clone(); + let hitbox = hitbox.clone(); + move |event: &KeyUpEvent, phase, window, cx| { + if phase.bubble() && !window.default_prevented() { + let stroke = &event.keystroke; + let keyboard_button = if stroke.key.eq("enter") { + Some(KeyboardButton::Enter) + } else if stroke.key.eq("space") { + Some(KeyboardButton::Space) + } else { + None + }; + + if let Some(button) = keyboard_button + && !stroke.modifiers.modified() + { + let click_event = ClickEvent::Keyboard(KeyboardClickEvent { + button, + bounds: hitbox.bounds, + }); + + for listener in &click_listeners { + listener(&click_event, window, cx); + } + } + } + } + }); + } + + window.on_mouse_event({ + let mut captured_mouse_down = None; + let hitbox = hitbox.clone(); + move |event: &MouseUpEvent, phase, window, cx| match phase { + // Clear the pending mouse down during the capture phase, + // so that it happens even if another event handler stops + // propagation. + DispatchPhase::Capture => { + let mut pending_mouse_down = pending_mouse_down.borrow_mut(); + if pending_mouse_down.is_some() && hitbox.is_hovered(window) { + captured_mouse_down = pending_mouse_down.take(); + window.refresh(); + } else if pending_mouse_down.is_some() { + // Clear the pending mouse down event (without firing click handlers) + // if the hitbox is not being hovered. + // This avoids dragging elements that changed their position + // immediately after being clicked. + // See https://github.com/zed-industries/zed/issues/24600 for more details + pending_mouse_down.take(); + window.refresh(); + } + } + // Fire click handlers during the bubble phase. + DispatchPhase::Bubble => { + if let Some(mouse_down) = captured_mouse_down.take() { + let mouse_click = ClickEvent::Mouse(MouseClickEvent { + down: mouse_down, + up: event.clone(), + }); + for listener in &click_listeners { + listener(&mouse_click, window, cx); + } + } + } + } + }); + } + + if let Some(hover_listener) = self.hover_listener.take() { + let hitbox = hitbox.clone(); + let was_hovered = element_state + .hover_state + .get_or_insert_with(Default::default) + .clone(); + let has_mouse_down = element_state + .pending_mouse_down + .get_or_insert_with(Default::default) + .clone(); + + window.on_mouse_event(move |_: &MouseMoveEvent, phase, window, cx| { + if phase != DispatchPhase::Bubble { + return; + } + let is_hovered = has_mouse_down.borrow().is_none() + && !cx.has_active_drag() + && hitbox.is_hovered(window); + let mut was_hovered = was_hovered.borrow_mut(); + + if is_hovered != *was_hovered { + *was_hovered = is_hovered; + drop(was_hovered); + + hover_listener(&is_hovered, window, cx); + } + }); + } + + if let Some(tooltip_builder) = self.tooltip_builder.take() { + let active_tooltip = element_state + .active_tooltip + .get_or_insert_with(Default::default) + .clone(); + let pending_mouse_down = element_state + .pending_mouse_down + .get_or_insert_with(Default::default) + .clone(); + + let tooltip_is_hoverable = tooltip_builder.hoverable; + let build_tooltip = Rc::new(move |window: &mut Window, cx: &mut App| { + Some(((tooltip_builder.build)(window, cx), tooltip_is_hoverable)) + }); + // Use bounds instead of testing hitbox since this is called during prepaint. + let check_is_hovered_during_prepaint = Rc::new({ + let pending_mouse_down = pending_mouse_down.clone(); + let source_bounds = hitbox.bounds; + move |window: &Window| { + pending_mouse_down.borrow().is_none() + && source_bounds.contains(&window.mouse_position()) + } + }); + let check_is_hovered = Rc::new({ + let hitbox = hitbox.clone(); + move |window: &Window| { + pending_mouse_down.borrow().is_none() && hitbox.is_hovered(window) + } + }); + register_tooltip_mouse_handlers( + &active_tooltip, + self.tooltip_id, + build_tooltip, + check_is_hovered, + check_is_hovered_during_prepaint, + window, + ); + } + + let active_state = element_state + .clicked_state + .get_or_insert_with(Default::default) + .clone(); + if active_state.borrow().is_clicked() { + window.on_mouse_event(move |_: &MouseUpEvent, phase, window, _cx| { + if phase == DispatchPhase::Capture { + *active_state.borrow_mut() = ElementClickedState::default(); + window.refresh(); + } + }); + } else { + let active_group_hitbox = self + .group_active_style + .as_ref() + .and_then(|group_active| GroupHitboxes::get(&group_active.group, cx)); + let hitbox = hitbox.clone(); + window.on_mouse_event(move |_: &MouseDownEvent, phase, window, _cx| { + if phase == DispatchPhase::Bubble && !window.default_prevented() { + let group_hovered = active_group_hitbox + .is_some_and(|group_hitbox_id| group_hitbox_id.is_hovered(window)); + let element_hovered = hitbox.is_hovered(window); + if group_hovered || element_hovered { + *active_state.borrow_mut() = ElementClickedState { + group: group_hovered, + element: element_hovered, + }; + window.refresh(); + } + } + }); + } + } + } + + fn paint_keyboard_listeners(&mut self, window: &mut Window, _cx: &mut App) { + let key_down_listeners = mem::take(&mut self.key_down_listeners); + let key_up_listeners = mem::take(&mut self.key_up_listeners); + let modifiers_changed_listeners = mem::take(&mut self.modifiers_changed_listeners); + let action_listeners = mem::take(&mut self.action_listeners); + if let Some(context) = self.key_context.clone() { + window.set_key_context(context); + } + + for listener in key_down_listeners { + window.on_key_event(move |event: &KeyDownEvent, phase, window, cx| { + listener(event, phase, window, cx); + }) + } + + for listener in key_up_listeners { + window.on_key_event(move |event: &KeyUpEvent, phase, window, cx| { + listener(event, phase, window, cx); + }) + } + + for listener in modifiers_changed_listeners { + window.on_modifiers_changed(move |event: &ModifiersChangedEvent, window, cx| { + listener(event, window, cx); + }) + } + + for (action_type, listener) in action_listeners { + window.on_action(action_type, listener) + } + } + + fn paint_hover_group_handler(&self, window: &mut Window, cx: &mut App) { + let group_hitbox = self + .group_hover_style + .as_ref() + .and_then(|group_hover| GroupHitboxes::get(&group_hover.group, cx)); + + if let Some(group_hitbox) = group_hitbox { + let was_hovered = group_hitbox.is_hovered(window); + let current_view = window.current_view(); + window.on_mouse_event(move |_: &MouseMoveEvent, phase, window, cx| { + let hovered = group_hitbox.is_hovered(window); + if phase == DispatchPhase::Capture && hovered != was_hovered { + cx.notify(current_view); + } + }); + } + } + + fn paint_scroll_listener( + &self, + hitbox: &Hitbox, + style: &Style, + window: &mut Window, + _cx: &mut App, + ) { + if let Some(scroll_offset) = self.scroll_offset.clone() { + let overflow = style.overflow; + let allow_concurrent_scroll = style.allow_concurrent_scroll; + let restrict_scroll_to_axis = style.restrict_scroll_to_axis; + let line_height = window.line_height(); + let hitbox = hitbox.clone(); + let current_view = window.current_view(); + window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| { + if phase == DispatchPhase::Bubble && hitbox.should_handle_scroll(window) { + let mut scroll_offset = scroll_offset.borrow_mut(); + let old_scroll_offset = *scroll_offset; + let delta = event.delta.pixel_delta(line_height); + + let mut delta_x = Pixels::ZERO; + if overflow.x == Overflow::Scroll { + if !delta.x.is_zero() { + delta_x = delta.x; + } else if !restrict_scroll_to_axis && overflow.y != Overflow::Scroll { + delta_x = delta.y; + } + } + let mut delta_y = Pixels::ZERO; + if overflow.y == Overflow::Scroll { + if !delta.y.is_zero() { + delta_y = delta.y; + } else if !restrict_scroll_to_axis && overflow.x != Overflow::Scroll { + delta_y = delta.x; + } + } + if !allow_concurrent_scroll && !delta_x.is_zero() && !delta_y.is_zero() { + if delta_x.abs() > delta_y.abs() { + delta_y = Pixels::ZERO; + } else { + delta_x = Pixels::ZERO; + } + } + scroll_offset.y += delta_y; + scroll_offset.x += delta_x; + if *scroll_offset != old_scroll_offset { + cx.notify(current_view); + } + } + }); + } + } + + /// Compute the visual style for this element, based on the current bounds and the element's state. + pub fn compute_style( + &self, + global_id: Option<&GlobalElementId>, + hitbox: Option<&Hitbox>, + window: &mut Window, + cx: &mut App, + ) -> Style { + window.with_optional_element_state(global_id, |element_state, window| { + let mut element_state = + element_state.map(|element_state| element_state.unwrap_or_default()); + let style = self.compute_style_internal(hitbox, element_state.as_mut(), window, cx); + (style, element_state) + }) + } + + /// Called from internal methods that have already called with_element_state. + fn compute_style_internal( + &self, + hitbox: Option<&Hitbox>, + element_state: Option<&mut InteractiveElementState>, + window: &mut Window, + cx: &mut App, + ) -> Style { + let mut style = Style::default(); + style.refine(&self.base_style); + + if let Some(focus_handle) = self.tracked_focus_handle.as_ref() { + if let Some(in_focus_style) = self.in_focus_style.as_ref() + && focus_handle.within_focused(window, cx) + { + style.refine(in_focus_style); + } + + if let Some(focus_style) = self.focus_style.as_ref() + && focus_handle.is_focused(window) + { + style.refine(focus_style); + } + } + + if let Some(hitbox) = hitbox { + if !cx.has_active_drag() { + if let Some(group_hover) = self.group_hover_style.as_ref() + && let Some(group_hitbox_id) = GroupHitboxes::get(&group_hover.group, cx) + && group_hitbox_id.is_hovered(window) + { + style.refine(&group_hover.style); + } + + if let Some(hover_style) = self.hover_style.as_ref() + && hitbox.is_hovered(window) + { + style.refine(hover_style); + } + } + + if let Some(drag) = cx.active_drag.take() { + let mut can_drop = true; + if let Some(can_drop_predicate) = &self.can_drop_predicate { + can_drop = can_drop_predicate(drag.value.as_ref(), window, cx); + } + + if can_drop { + for (state_type, group_drag_style) in &self.group_drag_over_styles { + if let Some(group_hitbox_id) = + GroupHitboxes::get(&group_drag_style.group, cx) + && *state_type == drag.value.as_ref().type_id() + && group_hitbox_id.is_hovered(window) + { + style.refine(&group_drag_style.style); + } + } + + for (state_type, build_drag_over_style) in &self.drag_over_styles { + if *state_type == drag.value.as_ref().type_id() && hitbox.is_hovered(window) + { + style.refine(&build_drag_over_style(drag.value.as_ref(), window, cx)); + } + } + } + + style.mouse_cursor = drag.cursor_style; + cx.active_drag = Some(drag); + } + } + + if let Some(element_state) = element_state { + let clicked_state = element_state + .clicked_state + .get_or_insert_with(Default::default) + .borrow(); + if clicked_state.group + && let Some(group) = self.group_active_style.as_ref() + { + style.refine(&group.style) + } + + if let Some(active_style) = self.active_style.as_ref() + && clicked_state.element + { + style.refine(active_style) + } + } + + style + } +} + +/// The per-frame state of an interactive element. Used for tracking stateful interactions like clicks +/// and scroll offsets. +#[derive(Default)] +pub struct InteractiveElementState { + pub(crate) focus_handle: Option, + pub(crate) clicked_state: Option>>, + pub(crate) hover_state: Option>>, + pub(crate) pending_mouse_down: Option>>>, + pub(crate) scroll_offset: Option>>>, + pub(crate) active_tooltip: Option>>>, +} + +/// Whether or not the element or a group that contains it is clicked by the mouse. +#[derive(Copy, Clone, Default, Eq, PartialEq)] +pub struct ElementClickedState { + /// True if this element's group has been clicked, false otherwise + pub group: bool, + + /// True if this element has been clicked, false otherwise + pub element: bool, +} + +impl ElementClickedState { + fn is_clicked(&self) -> bool { + self.group || self.element + } +} + +pub(crate) enum ActiveTooltip { + /// Currently delaying before showing the tooltip. + WaitingForShow { _task: Task<()> }, + /// Tooltip is visible, element was hovered or for hoverable tooltips, the tooltip was hovered. + Visible { + tooltip: AnyTooltip, + is_hoverable: bool, + }, + /// Tooltip is visible and hoverable, but the mouse is no longer hovering. Currently delaying + /// before hiding it. + WaitingForHide { + tooltip: AnyTooltip, + _task: Task<()>, + }, +} + +pub(crate) fn clear_active_tooltip( + active_tooltip: &Rc>>, + window: &mut Window, +) { + match active_tooltip.borrow_mut().take() { + None => {} + Some(ActiveTooltip::WaitingForShow { .. }) => {} + Some(ActiveTooltip::Visible { .. }) => window.refresh(), + Some(ActiveTooltip::WaitingForHide { .. }) => window.refresh(), + } +} + +pub(crate) fn clear_active_tooltip_if_not_hoverable( + active_tooltip: &Rc>>, + window: &mut Window, +) { + let should_clear = match active_tooltip.borrow().as_ref() { + None => false, + Some(ActiveTooltip::WaitingForShow { .. }) => false, + Some(ActiveTooltip::Visible { is_hoverable, .. }) => !is_hoverable, + Some(ActiveTooltip::WaitingForHide { .. }) => false, + }; + if should_clear { + active_tooltip.borrow_mut().take(); + window.refresh(); + } +} + +pub(crate) fn set_tooltip_on_window( + active_tooltip: &Rc>>, + window: &mut Window, +) -> Option { + let tooltip = match active_tooltip.borrow().as_ref() { + None => return None, + Some(ActiveTooltip::WaitingForShow { .. }) => return None, + Some(ActiveTooltip::Visible { tooltip, .. }) => tooltip.clone(), + Some(ActiveTooltip::WaitingForHide { tooltip, .. }) => tooltip.clone(), + }; + Some(window.set_tooltip(tooltip)) +} + +pub(crate) fn register_tooltip_mouse_handlers( + active_tooltip: &Rc>>, + tooltip_id: Option, + build_tooltip: Rc Option<(AnyView, bool)>>, + check_is_hovered: Rc bool>, + check_is_hovered_during_prepaint: Rc bool>, + window: &mut Window, +) { + window.on_mouse_event({ + let active_tooltip = active_tooltip.clone(); + let build_tooltip = build_tooltip.clone(); + let check_is_hovered = check_is_hovered.clone(); + move |_: &MouseMoveEvent, phase, window, cx| { + handle_tooltip_mouse_move( + &active_tooltip, + &build_tooltip, + &check_is_hovered, + &check_is_hovered_during_prepaint, + phase, + window, + cx, + ) + } + }); + + window.on_mouse_event({ + let active_tooltip = active_tooltip.clone(); + move |_: &MouseDownEvent, _phase, window: &mut Window, _cx| { + if !tooltip_id.is_some_and(|tooltip_id| tooltip_id.is_hovered(window)) { + clear_active_tooltip_if_not_hoverable(&active_tooltip, window); + } + } + }); + + window.on_mouse_event({ + let active_tooltip = active_tooltip.clone(); + move |_: &ScrollWheelEvent, _phase, window: &mut Window, _cx| { + if !tooltip_id.is_some_and(|tooltip_id| tooltip_id.is_hovered(window)) { + clear_active_tooltip_if_not_hoverable(&active_tooltip, window); + } + } + }); +} + +/// Handles displaying tooltips when an element is hovered. +/// +/// The mouse hovering logic also relies on being called from window prepaint in order to handle the +/// case where the element the tooltip is on is not rendered - in that case its mouse listeners are +/// also not registered. During window prepaint, the hitbox information is not available, so +/// `check_is_hovered_during_prepaint` is used which bases the check off of the absolute bounds of +/// the element. +/// +/// TODO: There's a minor bug due to the use of absolute bounds while checking during prepaint - it +/// does not know if the hitbox is occluded. In the case where a tooltip gets displayed and then +/// gets occluded after display, it will stick around until the mouse exits the hover bounds. +fn handle_tooltip_mouse_move( + active_tooltip: &Rc>>, + build_tooltip: &Rc Option<(AnyView, bool)>>, + check_is_hovered: &Rc bool>, + check_is_hovered_during_prepaint: &Rc bool>, + phase: DispatchPhase, + window: &mut Window, + cx: &mut App, +) { + // Separates logic for what mutation should occur from applying it, to avoid overlapping + // RefCell borrows. + enum Action { + None, + CancelShow, + ScheduleShow, + } + + let action = match active_tooltip.borrow().as_ref() { + None => { + let is_hovered = check_is_hovered(window); + if is_hovered && phase.bubble() { + Action::ScheduleShow + } else { + Action::None + } + } + Some(ActiveTooltip::WaitingForShow { .. }) => { + let is_hovered = check_is_hovered(window); + if is_hovered { + Action::None + } else { + Action::CancelShow + } + } + // These are handled in check_visible_and_update. + Some(ActiveTooltip::Visible { .. }) | Some(ActiveTooltip::WaitingForHide { .. }) => { + Action::None + } + }; + + match action { + Action::None => {} + Action::CancelShow => { + // Cancel waiting to show tooltip when it is no longer hovered. + active_tooltip.borrow_mut().take(); + } + Action::ScheduleShow => { + let delayed_show_task = window.spawn(cx, { + let active_tooltip = active_tooltip.clone(); + let build_tooltip = build_tooltip.clone(); + let check_is_hovered_during_prepaint = check_is_hovered_during_prepaint.clone(); + async move |cx| { + cx.background_executor().timer(TOOLTIP_SHOW_DELAY).await; + cx.update(|window, cx| { + let new_tooltip = + build_tooltip(window, cx).map(|(view, tooltip_is_hoverable)| { + let active_tooltip = active_tooltip.clone(); + ActiveTooltip::Visible { + tooltip: AnyTooltip { + view, + mouse_position: window.mouse_position(), + check_visible_and_update: Rc::new( + move |tooltip_bounds, window, cx| { + handle_tooltip_check_visible_and_update( + &active_tooltip, + tooltip_is_hoverable, + &check_is_hovered_during_prepaint, + tooltip_bounds, + window, + cx, + ) + }, + ), + }, + is_hoverable: tooltip_is_hoverable, + } + }); + *active_tooltip.borrow_mut() = new_tooltip; + window.refresh(); + }) + .ok(); + } + }); + active_tooltip + .borrow_mut() + .replace(ActiveTooltip::WaitingForShow { + _task: delayed_show_task, + }); + } + } +} + +/// Returns a callback which will be called by window prepaint to update tooltip visibility. The +/// purpose of doing this logic here instead of the mouse move handler is that the mouse move +/// handler won't get called when the element is not painted (e.g. via use of `visible_on_hover`). +fn handle_tooltip_check_visible_and_update( + active_tooltip: &Rc>>, + tooltip_is_hoverable: bool, + check_is_hovered: &Rc bool>, + tooltip_bounds: Bounds, + window: &mut Window, + cx: &mut App, +) -> bool { + // Separates logic for what mutation should occur from applying it, to avoid overlapping RefCell + // borrows. + enum Action { + None, + Hide, + ScheduleHide(AnyTooltip), + CancelHide(AnyTooltip), + } + + let is_hovered = check_is_hovered(window) + || (tooltip_is_hoverable && tooltip_bounds.contains(&window.mouse_position())); + let action = match active_tooltip.borrow().as_ref() { + Some(ActiveTooltip::Visible { tooltip, .. }) => { + if is_hovered { + Action::None + } else { + if tooltip_is_hoverable { + Action::ScheduleHide(tooltip.clone()) + } else { + Action::Hide + } + } + } + Some(ActiveTooltip::WaitingForHide { tooltip, .. }) => { + if is_hovered { + Action::CancelHide(tooltip.clone()) + } else { + Action::None + } + } + None | Some(ActiveTooltip::WaitingForShow { .. }) => Action::None, + }; + + match action { + Action::None => {} + Action::Hide => clear_active_tooltip(active_tooltip, window), + Action::ScheduleHide(tooltip) => { + let delayed_hide_task = window.spawn(cx, { + let active_tooltip = active_tooltip.clone(); + async move |cx| { + cx.background_executor() + .timer(HOVERABLE_TOOLTIP_HIDE_DELAY) + .await; + if active_tooltip.borrow_mut().take().is_some() { + cx.update(|window, _cx| window.refresh()).ok(); + } + } + }); + active_tooltip + .borrow_mut() + .replace(ActiveTooltip::WaitingForHide { + tooltip, + _task: delayed_hide_task, + }); + } + Action::CancelHide(tooltip) => { + // Cancel waiting to hide tooltip when it becomes hovered. + active_tooltip.borrow_mut().replace(ActiveTooltip::Visible { + tooltip, + is_hoverable: true, + }); + } + } + + active_tooltip.borrow().is_some() +} + +#[derive(Default)] +pub(crate) struct GroupHitboxes(HashMap>); + +impl Global for GroupHitboxes {} + +impl GroupHitboxes { + pub fn get(name: &SharedString, cx: &mut App) -> Option { + cx.default_global::() + .0 + .get(name) + .and_then(|bounds_stack| bounds_stack.last()) + .cloned() + } + + pub fn push(name: SharedString, hitbox_id: HitboxId, cx: &mut App) { + cx.default_global::() + .0 + .entry(name) + .or_default() + .push(hitbox_id); + } + + pub fn pop(name: &SharedString, cx: &mut App) { + cx.default_global::().0.get_mut(name).unwrap().pop(); + } +} + +/// A wrapper around an element that can store state, produced after assigning an ElementId. +pub struct Stateful { + pub(crate) element: E, +} + +impl Styled for Stateful +where + E: Styled, +{ + fn style(&mut self) -> &mut StyleRefinement { + self.element.style() + } +} + +impl StatefulInteractiveElement for Stateful +where + E: Element, + Self: InteractiveElement, +{ +} + +impl InteractiveElement for Stateful +where + E: InteractiveElement, +{ + fn interactivity(&mut self) -> &mut Interactivity { + self.element.interactivity() + } +} + +impl Element for Stateful +where + E: Element, +{ + type RequestLayoutState = E::RequestLayoutState; + type PrepaintState = E::PrepaintState; + + fn id(&self) -> Option { + self.element.id() + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + self.element.source_location() + } + + fn request_layout( + &mut self, + id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + self.element.request_layout(id, inspector_id, window, cx) + } + + fn prepaint( + &mut self, + id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + state: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> E::PrepaintState { + self.element + .prepaint(id, inspector_id, bounds, state, window, cx) + } + + fn paint( + &mut self, + id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + request_layout: &mut Self::RequestLayoutState, + prepaint: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + self.element.paint( + id, + inspector_id, + bounds, + request_layout, + prepaint, + window, + cx, + ); + } +} + +impl IntoElement for Stateful +where + E: Element, +{ + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl ParentElement for Stateful +where + E: ParentElement, +{ + fn extend(&mut self, elements: impl IntoIterator) { + self.element.extend(elements) + } +} + +/// Represents an element that can be scrolled *to* in its parent element. +/// Contrary to [ScrollHandle::scroll_to_active_item], an anchored element does not have to be an immediate child of the parent. +#[derive(Clone)] +pub struct ScrollAnchor { + handle: ScrollHandle, + last_origin: Rc>>, +} + +impl ScrollAnchor { + /// Creates a [ScrollAnchor] associated with a given [ScrollHandle]. + pub fn for_handle(handle: ScrollHandle) -> Self { + Self { + handle, + last_origin: Default::default(), + } + } + /// Request scroll to this item on the next frame. + pub fn scroll_to(&self, window: &mut Window, _cx: &mut App) { + let this = self.clone(); + + window.on_next_frame(move |_, _| { + let viewport_bounds = this.handle.bounds(); + let self_bounds = *this.last_origin.borrow(); + this.handle.set_offset(viewport_bounds.origin - self_bounds); + }); + } +} + +#[derive(Default, Debug)] +struct ScrollHandleState { + offset: Rc>>, + bounds: Bounds, + max_offset: Size, + child_bounds: Vec>, + scroll_to_bottom: bool, + overflow: Point, + active_item: Option, +} + +#[derive(Default, Debug, Clone, Copy)] +struct ScrollActiveItem { + index: usize, + strategy: ScrollStrategy, +} + +#[derive(Default, Debug, Clone, Copy)] +enum ScrollStrategy { + #[default] + FirstVisible, + Top, +} + +/// A handle to the scrollable aspects of an element. +/// Used for accessing scroll state, like the current scroll offset, +/// and for mutating the scroll state, like scrolling to a specific child. +#[derive(Clone, Debug)] +pub struct ScrollHandle(Rc>); + +impl Default for ScrollHandle { + fn default() -> Self { + Self::new() + } +} + +impl ScrollHandle { + /// Construct a new scroll handle. + pub fn new() -> Self { + Self(Rc::default()) + } + + /// Get the current scroll offset. + pub fn offset(&self) -> Point { + *self.0.borrow().offset.borrow() + } + + /// Get the maximum scroll offset. + pub fn max_offset(&self) -> Size { + self.0.borrow().max_offset + } + + /// Get the top child that's scrolled into view. + pub fn top_item(&self) -> usize { + let state = self.0.borrow(); + let top = state.bounds.top() - state.offset.borrow().y; + + match state.child_bounds.binary_search_by(|bounds| { + if top < bounds.top() { + Ordering::Greater + } else if top > bounds.bottom() { + Ordering::Less + } else { + Ordering::Equal + } + }) { + Ok(ix) => ix, + Err(ix) => ix.min(state.child_bounds.len().saturating_sub(1)), + } + } + + /// Get the bottom child that's scrolled into view. + pub fn bottom_item(&self) -> usize { + let state = self.0.borrow(); + let bottom = state.bounds.bottom() - state.offset.borrow().y; + + match state.child_bounds.binary_search_by(|bounds| { + if bottom < bounds.top() { + Ordering::Greater + } else if bottom > bounds.bottom() { + Ordering::Less + } else { + Ordering::Equal + } + }) { + Ok(ix) => ix, + Err(ix) => ix.min(state.child_bounds.len().saturating_sub(1)), + } + } + + /// Return the bounds into which this child is painted + pub fn bounds(&self) -> Bounds { + self.0.borrow().bounds + } + + /// Get the bounds for a specific child. + pub fn bounds_for_item(&self, ix: usize) -> Option> { + self.0.borrow().child_bounds.get(ix).cloned() + } + + /// Update [ScrollHandleState]'s active item for scrolling to in prepaint + pub fn scroll_to_item(&self, ix: usize) { + let mut state = self.0.borrow_mut(); + state.active_item = Some(ScrollActiveItem { + index: ix, + strategy: ScrollStrategy::default(), + }); + } + + /// Update [ScrollHandleState]'s active item for scrolling to in prepaint + /// This scrolls the minimal amount to ensure that the child is the first visible element + pub fn scroll_to_top_of_item(&self, ix: usize) { + let mut state = self.0.borrow_mut(); + state.active_item = Some(ScrollActiveItem { + index: ix, + strategy: ScrollStrategy::Top, + }); + } + + /// Scrolls the minimal amount to either ensure that the child is + /// fully visible or the top element of the view depends on the + /// scroll strategy + fn scroll_to_active_item(&self) { + let mut state = self.0.borrow_mut(); + + let Some(active_item) = state.active_item else { + return; + }; + + let active_item = match state.child_bounds.get(active_item.index) { + Some(bounds) => { + let mut scroll_offset = state.offset.borrow_mut(); + + match active_item.strategy { + ScrollStrategy::FirstVisible => { + if state.overflow.y == Overflow::Scroll { + if bounds.top() + scroll_offset.y < state.bounds.top() { + scroll_offset.y = state.bounds.top() - bounds.top(); + } else if bounds.bottom() + scroll_offset.y > state.bounds.bottom() { + scroll_offset.y = state.bounds.bottom() - bounds.bottom(); + } + } + } + ScrollStrategy::Top => { + scroll_offset.y = state.bounds.top() - bounds.top(); + } + } + + if state.overflow.x == Overflow::Scroll { + if bounds.left() + scroll_offset.x < state.bounds.left() { + scroll_offset.x = state.bounds.left() - bounds.left(); + } else if bounds.right() + scroll_offset.x > state.bounds.right() { + scroll_offset.x = state.bounds.right() - bounds.right(); + } + } + None + } + None => Some(active_item), + }; + state.active_item = active_item; + } + + /// Scrolls to the bottom. + pub fn scroll_to_bottom(&self) { + let mut state = self.0.borrow_mut(); + state.scroll_to_bottom = true; + } + + /// Set the offset explicitly. The offset is the distance from the top left of the + /// parent container to the top left of the first child. + /// As you scroll further down the offset becomes more negative. + pub fn set_offset(&self, mut position: Point) { + let state = self.0.borrow(); + *state.offset.borrow_mut() = position; + } + + /// Get the logical scroll top, based on a child index and a pixel offset. + pub fn logical_scroll_top(&self) -> (usize, Pixels) { + let ix = self.top_item(); + let state = self.0.borrow(); + + if let Some(child_bounds) = state.child_bounds.get(ix) { + ( + ix, + child_bounds.top() + state.offset.borrow().y - state.bounds.top(), + ) + } else { + (ix, px(0.)) + } + } + + /// Get the logical scroll bottom, based on a child index and a pixel offset. + pub fn logical_scroll_bottom(&self) -> (usize, Pixels) { + let ix = self.bottom_item(); + let state = self.0.borrow(); + + if let Some(child_bounds) = state.child_bounds.get(ix) { + ( + ix, + child_bounds.bottom() + state.offset.borrow().y - state.bounds.bottom(), + ) + } else { + (ix, px(0.)) + } + } + + /// Get the count of children for scrollable item. + pub fn children_count(&self) -> usize { + self.0.borrow().child_bounds.len() + } +} diff --git a/third_party/gpui/src/elements/image_cache.rs b/third_party/gpui/src/elements/image_cache.rs new file mode 100644 index 0000000..ee14361 --- /dev/null +++ b/third_party/gpui/src/elements/image_cache.rs @@ -0,0 +1,353 @@ +use crate::{ + AnyElement, AnyEntity, App, AppContext, Asset, AssetLogger, Bounds, Element, ElementId, Entity, + GlobalElementId, ImageAssetLoader, ImageCacheError, InspectorElementId, IntoElement, LayoutId, + ParentElement, Pixels, RenderImage, Resource, Style, StyleRefinement, Styled, Task, Window, + hash, +}; + +use futures::{FutureExt, future::Shared}; +use refineable::Refineable; +use smallvec::SmallVec; +use std::{collections::HashMap, fmt, sync::Arc}; + +/// An image cache element, all its child img elements will use the cache specified by this element. +/// Note that this could as simple as passing an `Entity` +pub fn image_cache(image_cache_provider: impl ImageCacheProvider) -> ImageCacheElement { + ImageCacheElement { + image_cache_provider: Box::new(image_cache_provider), + style: StyleRefinement::default(), + children: SmallVec::default(), + } +} + +/// A dynamically typed image cache, which can be used to store any image cache +#[derive(Clone)] +pub struct AnyImageCache { + image_cache: AnyEntity, + load_fn: fn( + image_cache: &AnyEntity, + resource: &Resource, + window: &mut Window, + cx: &mut App, + ) -> Option, ImageCacheError>>, +} + +impl From> for AnyImageCache { + fn from(image_cache: Entity) -> Self { + Self { + image_cache: image_cache.into_any(), + load_fn: any_image_cache::load::, + } + } +} + +impl AnyImageCache { + /// Load an image given a resource + /// returns the result of loading the image if it has finished loading, or None if it is still loading + pub fn load( + &self, + resource: &Resource, + window: &mut Window, + cx: &mut App, + ) -> Option, ImageCacheError>> { + (self.load_fn)(&self.image_cache, resource, window, cx) + } +} + +mod any_image_cache { + use super::*; + + pub(crate) fn load( + image_cache: &AnyEntity, + resource: &Resource, + window: &mut Window, + cx: &mut App, + ) -> Option, ImageCacheError>> { + let image_cache = image_cache.clone().downcast::().unwrap(); + image_cache.update(cx, |image_cache, cx| image_cache.load(resource, window, cx)) + } +} + +/// An image cache element. +pub struct ImageCacheElement { + image_cache_provider: Box, + style: StyleRefinement, + children: SmallVec<[AnyElement; 2]>, +} + +impl ParentElement for ImageCacheElement { + fn extend(&mut self, elements: impl IntoIterator) { + self.children.extend(elements) + } +} + +impl Styled for ImageCacheElement { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} + +impl IntoElement for ImageCacheElement { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for ImageCacheElement { + type RequestLayoutState = SmallVec<[LayoutId; 4]>; + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let image_cache = self.image_cache_provider.provide(window, cx); + window.with_image_cache(Some(image_cache), |window| { + let child_layout_ids = self + .children + .iter_mut() + .map(|child| child.request_layout(window, cx)) + .collect::>(); + let mut style = Style::default(); + style.refine(&self.style); + let layout_id = window.request_layout(style, child_layout_ids.iter().copied(), cx); + (layout_id, child_layout_ids) + }) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Self::PrepaintState { + for child in &mut self.children { + child.prepaint(window, cx); + } + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _prepaint: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + let image_cache = self.image_cache_provider.provide(window, cx); + window.with_image_cache(Some(image_cache), |window| { + for child in &mut self.children { + child.paint(window, cx); + } + }) + } +} + +/// An image loading task associated with an image cache. +pub type ImageLoadingTask = Shared, ImageCacheError>>>; + +/// An image cache item +pub enum ImageCacheItem { + /// The associated image is currently loading + Loading(ImageLoadingTask), + /// This item has loaded an image. + Loaded(Result, ImageCacheError>), +} + +impl std::fmt::Debug for ImageCacheItem { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let status = match self { + ImageCacheItem::Loading(_) => &"Loading...".to_string(), + ImageCacheItem::Loaded(render_image) => &format!("{:?}", render_image), + }; + f.debug_struct("ImageCacheItem") + .field("status", status) + .finish() + } +} + +impl ImageCacheItem { + /// Attempt to get the image from the cache item. + pub fn get(&mut self) -> Option, ImageCacheError>> { + match self { + ImageCacheItem::Loading(task) => { + let res = task.now_or_never()?; + *self = ImageCacheItem::Loaded(res.clone()); + Some(res) + } + ImageCacheItem::Loaded(res) => Some(res.clone()), + } + } +} + +/// An object that can handle the caching and unloading of images. +/// Implementations of this trait should ensure that images are removed from all windows when they are no longer needed. +pub trait ImageCache: 'static { + /// Load an image given a resource + /// returns the result of loading the image if it has finished loading, or None if it is still loading + fn load( + &mut self, + resource: &Resource, + window: &mut Window, + cx: &mut App, + ) -> Option, ImageCacheError>>; +} + +/// An object that can create an ImageCache during the render phase. +/// See the ImageCache trait for more information. +pub trait ImageCacheProvider: 'static { + /// Called during the request_layout phase to create an ImageCache. + fn provide(&mut self, _window: &mut Window, _cx: &mut App) -> AnyImageCache; +} + +impl ImageCacheProvider for Entity { + fn provide(&mut self, _window: &mut Window, _cx: &mut App) -> AnyImageCache { + self.clone().into() + } +} + +/// An implementation of ImageCache, that uses an LRU caching strategy to unload images when the cache is full +pub struct RetainAllImageCache(HashMap); + +impl fmt::Debug for RetainAllImageCache { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("HashMapImageCache") + .field("num_images", &self.0.len()) + .finish() + } +} + +impl RetainAllImageCache { + /// Create a new image cache. + #[inline] + pub fn new(cx: &mut App) -> Entity { + let e = cx.new(|_cx| RetainAllImageCache(HashMap::new())); + cx.observe_release(&e, |image_cache, cx| { + for (_, mut item) in std::mem::replace(&mut image_cache.0, HashMap::new()) { + if let Some(Ok(image)) = item.get() { + cx.drop_image(image, None); + } + } + }) + .detach(); + e + } + + /// Load an image from the given source. + /// + /// Returns `None` if the image is loading. + pub fn load( + &mut self, + source: &Resource, + window: &mut Window, + cx: &mut App, + ) -> Option, ImageCacheError>> { + let hash = hash(source); + + if let Some(item) = self.0.get_mut(&hash) { + return item.get(); + } + + let fut = AssetLogger::::load(source.clone(), cx); + let task = cx.background_executor().spawn(fut).shared(); + self.0.insert(hash, ImageCacheItem::Loading(task.clone())); + + let entity = window.current_view(); + window + .spawn(cx, { + async move |cx| { + _ = task.await; + cx.on_next_frame(move |_, cx| { + cx.notify(entity); + }); + } + }) + .detach(); + + None + } + + /// Clear the image cache. + pub fn clear(&mut self, window: &mut Window, cx: &mut App) { + for (_, mut item) in std::mem::replace(&mut self.0, HashMap::new()) { + if let Some(Ok(image)) = item.get() { + cx.drop_image(image, Some(window)); + } + } + } + + /// Remove the image from the cache by the given source. + pub fn remove(&mut self, source: &Resource, window: &mut Window, cx: &mut App) { + let hash = hash(source); + if let Some(mut item) = self.0.remove(&hash) + && let Some(Ok(image)) = item.get() + { + cx.drop_image(image, Some(window)); + } + } + + /// Returns the number of images in the cache. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns true if the cache is empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl ImageCache for RetainAllImageCache { + fn load( + &mut self, + resource: &Resource, + window: &mut Window, + cx: &mut App, + ) -> Option, ImageCacheError>> { + RetainAllImageCache::load(self, resource, window, cx) + } +} + +/// Constructs a retain-all image cache that uses the element state associated with the given ID. +pub fn retain_all(id: impl Into) -> RetainAllImageCacheProvider { + RetainAllImageCacheProvider { id: id.into() } +} + +/// A provider struct for creating a retain-all image cache inline +pub struct RetainAllImageCacheProvider { + id: ElementId, +} + +impl ImageCacheProvider for RetainAllImageCacheProvider { + fn provide(&mut self, window: &mut Window, cx: &mut App) -> AnyImageCache { + window + .with_global_id(self.id.clone(), |global_id, window| { + window.with_element_state::, _>( + global_id, + |cache, _window| { + let mut cache = cache.unwrap_or_else(|| RetainAllImageCache::new(cx)); + (cache.clone(), cache) + }, + ) + }) + .into() + } +} diff --git a/third_party/gpui/src/elements/img.rs b/third_party/gpui/src/elements/img.rs new file mode 100644 index 0000000..075c7cf --- /dev/null +++ b/third_party/gpui/src/elements/img.rs @@ -0,0 +1,767 @@ +use crate::{ + AnyElement, AnyImageCache, App, Asset, AssetLogger, Bounds, DefiniteLength, Element, ElementId, + Entity, GlobalElementId, Hitbox, Image, ImageCache, InspectorElementId, InteractiveElement, + Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource, + SMOOTH_SVG_SCALE_FACTOR, SharedString, SharedUri, StyleRefinement, Styled, SvgSize, Task, + Window, px, swap_rgba_pa_to_bgra, +}; +use anyhow::{Context as _, Result}; + +use futures::{AsyncReadExt, Future}; +use image::{ + AnimationDecoder, DynamicImage, Frame, ImageBuffer, ImageError, ImageFormat, Rgba, + codecs::{gif::GifDecoder, webp::WebPDecoder}, +}; +use smallvec::SmallVec; +use std::{ + fs, + io::{self, Cursor}, + ops::{Deref, DerefMut}, + path::{Path, PathBuf}, + str::FromStr, + sync::Arc, + time::{Duration, Instant}, +}; +use thiserror::Error; +use util::ResultExt; + +use super::{Stateful, StatefulInteractiveElement}; + +/// The delay before showing the loading state. +pub const LOADING_DELAY: Duration = Duration::from_millis(200); + +/// A type alias to the resource loader that the `img()` element uses. +/// +/// Note: that this is only for Resources, like URLs or file paths. +/// Custom loaders, or external images will not use this asset loader +pub type ImgResourceLoader = AssetLogger; + +/// A source of image content. +#[derive(Clone)] +pub enum ImageSource { + /// The image content will be loaded from some resource location + Resource(Resource), + /// Cached image data + Render(Arc), + /// Cached image data + Image(Arc), + /// A custom loading function to use + Custom(Arc Option, ImageCacheError>>>), +} + +fn is_uri(uri: &str) -> bool { + http_client::Uri::from_str(uri).is_ok() +} + +impl From for ImageSource { + fn from(value: SharedUri) -> Self { + Self::Resource(Resource::Uri(value)) + } +} + +impl<'a> From<&'a str> for ImageSource { + fn from(s: &'a str) -> Self { + if is_uri(s) { + Self::Resource(Resource::Uri(s.to_string().into())) + } else { + Self::Resource(Resource::Embedded(s.to_string().into())) + } + } +} + +impl From for ImageSource { + fn from(s: String) -> Self { + if is_uri(&s) { + Self::Resource(Resource::Uri(s.into())) + } else { + Self::Resource(Resource::Embedded(s.into())) + } + } +} + +impl From for ImageSource { + fn from(s: SharedString) -> Self { + s.as_ref().into() + } +} + +impl From<&Path> for ImageSource { + fn from(value: &Path) -> Self { + Self::Resource(value.to_path_buf().into()) + } +} + +impl From> for ImageSource { + fn from(value: Arc) -> Self { + Self::Resource(value.into()) + } +} + +impl From for ImageSource { + fn from(value: PathBuf) -> Self { + Self::Resource(value.into()) + } +} + +impl From> for ImageSource { + fn from(value: Arc) -> Self { + Self::Render(value) + } +} + +impl From> for ImageSource { + fn from(value: Arc) -> Self { + Self::Image(value) + } +} + +impl From for ImageSource +where + F: Fn(&mut Window, &mut App) -> Option, ImageCacheError>> + 'static, +{ + fn from(value: F) -> Self { + Self::Custom(Arc::new(value)) + } +} + +/// The style of an image element. +pub struct ImageStyle { + grayscale: bool, + object_fit: ObjectFit, + loading: Option AnyElement>>, + fallback: Option AnyElement>>, +} + +impl Default for ImageStyle { + fn default() -> Self { + Self { + grayscale: false, + object_fit: ObjectFit::Contain, + loading: None, + fallback: None, + } + } +} + +/// Style an image element. +pub trait StyledImage: Sized { + /// Get a mutable [ImageStyle] from the element. + fn image_style(&mut self) -> &mut ImageStyle; + + /// Set the image to be displayed in grayscale. + fn grayscale(mut self, grayscale: bool) -> Self { + self.image_style().grayscale = grayscale; + self + } + + /// Set the object fit for the image. + fn object_fit(mut self, object_fit: ObjectFit) -> Self { + self.image_style().object_fit = object_fit; + self + } + + /// Set the object fit for the image. + fn with_fallback(mut self, fallback: impl Fn() -> AnyElement + 'static) -> Self { + self.image_style().fallback = Some(Box::new(fallback)); + self + } + + /// Set the object fit for the image. + fn with_loading(mut self, loading: impl Fn() -> AnyElement + 'static) -> Self { + self.image_style().loading = Some(Box::new(loading)); + self + } +} + +impl StyledImage for Img { + fn image_style(&mut self) -> &mut ImageStyle { + &mut self.style + } +} + +impl StyledImage for Stateful { + fn image_style(&mut self) -> &mut ImageStyle { + &mut self.element.style + } +} + +/// An image element. +pub struct Img { + interactivity: Interactivity, + source: ImageSource, + style: ImageStyle, + image_cache: Option, +} + +/// Create a new image element. +#[track_caller] +pub fn img(source: impl Into) -> Img { + Img { + interactivity: Interactivity::new(), + source: source.into(), + style: ImageStyle::default(), + image_cache: None, + } +} + +impl Img { + /// A list of all format extensions currently supported by this img element + pub fn extensions() -> &'static [&'static str] { + // This is the list in [image::ImageFormat::from_extension] + `svg` + &[ + "avif", "jpg", "jpeg", "png", "gif", "webp", "tif", "tiff", "tga", "dds", "bmp", "ico", + "hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg", + ] + } + + /// Sets the image cache for the current node. + /// + /// If the `image_cache` is not explicitly provided, the function will determine the image cache by: + /// + /// 1. Checking if any ancestor node of the current node contains an `ImageCacheElement`, If such a node exists, the image cache specified by that ancestor will be used. + /// 2. If no ancestor node contains an `ImageCacheElement`, the global image cache will be used as a fallback. + /// + /// This mechanism provides a flexible way to manage image caching, allowing precise control when needed, + /// while ensuring a default behavior when no cache is explicitly specified. + #[inline] + pub fn image_cache(self, image_cache: &Entity) -> Self { + Self { + image_cache: Some(image_cache.clone().into()), + ..self + } + } +} + +impl Deref for Stateful { + type Target = Img; + + fn deref(&self) -> &Self::Target { + &self.element + } +} + +impl DerefMut for Stateful { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.element + } +} + +/// The image state between frames +struct ImgState { + frame_index: usize, + last_frame_time: Option, + started_loading: Option<(Instant, Task<()>)>, +} + +/// The image layout state between frames +pub struct ImgLayoutState { + frame_index: usize, + replacement: Option, +} + +impl Element for Img { + type RequestLayoutState = ImgLayoutState; + type PrepaintState = Option; + + fn id(&self) -> Option { + self.interactivity.element_id.clone() + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + self.interactivity.source_location() + } + + fn request_layout( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let mut layout_state = ImgLayoutState { + frame_index: 0, + replacement: None, + }; + + window.with_optional_element_state(global_id, |state, window| { + let mut state = state.map(|state| { + state.unwrap_or(ImgState { + frame_index: 0, + last_frame_time: None, + started_loading: None, + }) + }); + + let frame_index = state.as_ref().map(|state| state.frame_index).unwrap_or(0); + + let layout_id = self.interactivity.request_layout( + global_id, + inspector_id, + window, + cx, + |mut style, window, cx| { + let mut replacement_id = None; + + match self.source.use_data( + self.image_cache + .clone() + .or_else(|| window.image_cache_stack.last().cloned()), + window, + cx, + ) { + Some(Ok(data)) => { + if let Some(state) = &mut state { + let frame_count = data.frame_count(); + if frame_count > 1 { + let current_time = Instant::now(); + if let Some(last_frame_time) = state.last_frame_time { + let elapsed = current_time - last_frame_time; + let frame_duration = + Duration::from(data.delay(state.frame_index)); + + if elapsed >= frame_duration { + state.frame_index = + (state.frame_index + 1) % frame_count; + state.last_frame_time = + Some(current_time - (elapsed - frame_duration)); + } + } else { + state.last_frame_time = Some(current_time); + } + } + state.started_loading = None; + } + + let image_size = data.render_size(frame_index); + style.aspect_ratio = Some(image_size.width / image_size.height); + + if let Length::Auto = style.size.width { + style.size.width = match style.size.height { + Length::Definite(DefiniteLength::Absolute(abs_length)) => { + let height_px = abs_length.to_pixels(window.rem_size()); + Length::Definite( + px(image_size.width.0 * height_px.0 + / image_size.height.0) + .into(), + ) + } + _ => Length::Definite(image_size.width.into()), + }; + } + + if let Length::Auto = style.size.height { + style.size.height = match style.size.width { + Length::Definite(DefiniteLength::Absolute(abs_length)) => { + let width_px = abs_length.to_pixels(window.rem_size()); + Length::Definite( + px(image_size.height.0 * width_px.0 + / image_size.width.0) + .into(), + ) + } + _ => Length::Definite(image_size.height.into()), + }; + } + + if global_id.is_some() && data.frame_count() > 1 { + window.request_animation_frame(); + } + } + Some(_err) => { + if let Some(fallback) = self.style.fallback.as_ref() { + let mut element = fallback(); + replacement_id = Some(element.request_layout(window, cx)); + layout_state.replacement = Some(element); + } + if let Some(state) = &mut state { + state.started_loading = None; + } + } + None => { + if let Some(state) = &mut state { + if let Some((started_loading, _)) = state.started_loading { + if started_loading.elapsed() > LOADING_DELAY + && let Some(loading) = self.style.loading.as_ref() + { + let mut element = loading(); + replacement_id = Some(element.request_layout(window, cx)); + layout_state.replacement = Some(element); + } + } else { + let current_view = window.current_view(); + let task = window.spawn(cx, async move |cx| { + cx.background_executor().timer(LOADING_DELAY).await; + cx.update(move |_, cx| { + cx.notify(current_view); + }) + .ok(); + }); + state.started_loading = Some((Instant::now(), task)); + } + } + } + } + + window.request_layout(style, replacement_id, cx) + }, + ); + + layout_state.frame_index = frame_index; + + ((layout_id, layout_state), state) + }) + } + + fn prepaint( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + request_layout: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Self::PrepaintState { + self.interactivity.prepaint( + global_id, + inspector_id, + bounds, + bounds.size, + window, + cx, + |_, _, hitbox, window, cx| { + if let Some(replacement) = &mut request_layout.replacement { + replacement.prepaint(window, cx); + } + + hitbox + }, + ) + } + + fn paint( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + layout_state: &mut Self::RequestLayoutState, + hitbox: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + let source = self.source.clone(); + self.interactivity.paint( + global_id, + inspector_id, + bounds, + hitbox.as_ref(), + window, + cx, + |style, window, cx| { + if let Some(Ok(data)) = source.use_data( + self.image_cache + .clone() + .or_else(|| window.image_cache_stack.last().cloned()), + window, + cx, + ) { + let new_bounds = self + .style + .object_fit + .get_bounds(bounds, data.size(layout_state.frame_index)); + let corner_radii = style + .corner_radii + .to_pixels(window.rem_size()) + .clamp_radii_for_quad_size(new_bounds.size); + window + .paint_image( + new_bounds, + corner_radii, + data, + layout_state.frame_index, + self.style.grayscale, + ) + .log_err(); + } else if let Some(replacement) = &mut layout_state.replacement { + replacement.paint(window, cx); + } + }, + ) + } +} + +impl Styled for Img { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.interactivity.base_style + } +} + +impl InteractiveElement for Img { + fn interactivity(&mut self) -> &mut Interactivity { + &mut self.interactivity + } +} + +impl IntoElement for Img { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl StatefulInteractiveElement for Img {} + +impl ImageSource { + pub(crate) fn use_data( + &self, + cache: Option, + window: &mut Window, + cx: &mut App, + ) -> Option, ImageCacheError>> { + match self { + ImageSource::Resource(resource) => { + if let Some(cache) = cache { + cache.load(resource, window, cx) + } else { + window.use_asset::(resource, cx) + } + } + ImageSource::Custom(loading_fn) => loading_fn(window, cx), + ImageSource::Render(data) => Some(Ok(data.to_owned())), + ImageSource::Image(data) => window.use_asset::>(data, cx), + } + } + + pub(crate) fn get_data( + &self, + cache: Option, + window: &mut Window, + cx: &mut App, + ) -> Option, ImageCacheError>> { + match self { + ImageSource::Resource(resource) => { + if let Some(cache) = cache { + cache.load(resource, window, cx) + } else { + window.get_asset::(resource, cx) + } + } + ImageSource::Custom(loading_fn) => loading_fn(window, cx), + ImageSource::Render(data) => Some(Ok(data.to_owned())), + ImageSource::Image(data) => window.get_asset::>(data, cx), + } + } + + /// Remove this image source from the asset system + pub fn remove_asset(&self, cx: &mut App) { + match self { + ImageSource::Resource(resource) => { + cx.remove_asset::(resource); + } + ImageSource::Custom(_) | ImageSource::Render(_) => {} + ImageSource::Image(data) => cx.remove_asset::>(data), + } + } +} + +#[derive(Clone)] +enum ImageDecoder {} + +impl Asset for ImageDecoder { + type Source = Arc; + type Output = Result, ImageCacheError>; + + fn load( + source: Self::Source, + cx: &mut App, + ) -> impl Future + Send + 'static { + let renderer = cx.svg_renderer(); + async move { source.to_image_data(renderer).map_err(Into::into) } + } +} + +/// An image loader for the GPUI asset system +#[derive(Clone)] +pub enum ImageAssetLoader {} + +impl Asset for ImageAssetLoader { + type Source = Resource; + type Output = Result, ImageCacheError>; + + fn load( + source: Self::Source, + cx: &mut App, + ) -> impl Future + Send + 'static { + let client = cx.http_client(); + // TODO: Can we make SVGs always rescale? + // let scale_factor = cx.scale_factor(); + let svg_renderer = cx.svg_renderer(); + let asset_source = cx.asset_source().clone(); + async move { + let bytes = match source.clone() { + Resource::Path(uri) => fs::read(uri.as_ref())?, + Resource::Uri(uri) => { + let mut response = client + .get(uri.as_ref(), ().into(), true) + .await + .with_context(|| format!("loading image asset from {uri:?}"))?; + let mut body = Vec::new(); + response.body_mut().read_to_end(&mut body).await?; + if !response.status().is_success() { + let mut body = String::from_utf8_lossy(&body).into_owned(); + let first_line = body.lines().next().unwrap_or("").trim_end(); + body.truncate(first_line.len()); + return Err(ImageCacheError::BadStatus { + uri, + status: response.status(), + body, + }); + } + body + } + Resource::Embedded(path) => { + let data = asset_source.load(&path).ok().flatten(); + if let Some(data) = data { + data.to_vec() + } else { + return Err(ImageCacheError::Asset( + format!("Embedded resource not found: {}", path).into(), + )); + } + } + }; + + let data = if let Ok(format) = image::guess_format(&bytes) { + let data = match format { + ImageFormat::Gif => { + let decoder = GifDecoder::new(Cursor::new(&bytes))?; + let mut frames = SmallVec::new(); + + for frame in decoder.into_frames() { + let mut frame = frame?; + // Convert from RGBA to BGRA. + for pixel in frame.buffer_mut().chunks_exact_mut(4) { + pixel.swap(0, 2); + } + frames.push(frame); + } + + frames + } + ImageFormat::WebP => { + let mut decoder = WebPDecoder::new(Cursor::new(&bytes))?; + + if decoder.has_animation() { + let _ = decoder.set_background_color(Rgba([0, 0, 0, 0])); + let mut frames = SmallVec::new(); + + for frame in decoder.into_frames() { + let mut frame = frame?; + // Convert from RGBA to BGRA. + for pixel in frame.buffer_mut().chunks_exact_mut(4) { + pixel.swap(0, 2); + } + frames.push(frame); + } + + frames + } else { + let mut data = DynamicImage::from_decoder(decoder)?.into_rgba8(); + + // Convert from RGBA to BGRA. + for pixel in data.chunks_exact_mut(4) { + pixel.swap(0, 2); + } + + SmallVec::from_elem(Frame::new(data), 1) + } + } + _ => { + let mut data = + image::load_from_memory_with_format(&bytes, format)?.into_rgba8(); + + // Convert from RGBA to BGRA. + for pixel in data.chunks_exact_mut(4) { + pixel.swap(0, 2); + } + + SmallVec::from_elem(Frame::new(data), 1) + } + }; + + RenderImage::new(data) + } else { + let pixmap = + // TODO: Can we make svgs always rescale? + svg_renderer.render_pixmap(&bytes, SvgSize::ScaleFactor(SMOOTH_SVG_SCALE_FACTOR))?; + + let mut buffer = + ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap(); + + for pixel in buffer.chunks_exact_mut(4) { + swap_rgba_pa_to_bgra(pixel); + } + + let mut image = RenderImage::new(SmallVec::from_elem(Frame::new(buffer), 1)); + image.scale_factor = SMOOTH_SVG_SCALE_FACTOR; + image + }; + + Ok(Arc::new(data)) + } + } +} + +/// An error that can occur when interacting with the image cache. +#[derive(Debug, Error, Clone)] +pub enum ImageCacheError { + /// Some other kind of error occurred + #[error("error: {0}")] + Other(#[from] Arc), + /// An error that occurred while reading the image from disk. + #[error("IO error: {0}")] + Io(Arc), + /// An error that occurred while processing an image. + #[error("unexpected http status for {uri}: {status}, body: {body}")] + BadStatus { + /// The URI of the image. + uri: SharedUri, + /// The HTTP status code. + status: http_client::StatusCode, + /// The HTTP response body. + body: String, + }, + /// An error that occurred while processing an asset. + #[error("asset error: {0}")] + Asset(SharedString), + /// An error that occurred while processing an image. + #[error("image error: {0}")] + Image(Arc), + /// An error that occurred while processing an SVG. + #[error("svg error: {0}")] + Usvg(Arc), +} + +impl From for ImageCacheError { + fn from(value: anyhow::Error) -> Self { + Self::Other(Arc::new(value)) + } +} + +impl From for ImageCacheError { + fn from(value: io::Error) -> Self { + Self::Io(Arc::new(value)) + } +} + +impl From for ImageCacheError { + fn from(value: usvg::Error) -> Self { + Self::Usvg(Arc::new(value)) + } +} + +impl From for ImageCacheError { + fn from(value: image::ImageError) -> Self { + Self::Image(Arc::new(value)) + } +} diff --git a/third_party/gpui/src/elements/list.rs b/third_party/gpui/src/elements/list.rs new file mode 100644 index 0000000..7856620 --- /dev/null +++ b/third_party/gpui/src/elements/list.rs @@ -0,0 +1,1287 @@ +//! A list element that can be used to render a large number of differently sized elements +//! efficiently. Clients of this API need to ensure that elements outside of the scrolled +//! area do not change their height for this element to function correctly. If your elements +//! do change height, notify the list element via [`ListState::splice`] or [`ListState::reset`]. +//! In order to minimize re-renders, this element's state is stored intrusively +//! on your own views, so that your code can coordinate directly with the list element's cached state. +//! +//! If all of your elements are the same height, see [`crate::UniformList`] for a simpler API + +use crate::{ + AnyElement, App, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, Element, EntityId, + FocusHandle, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, IntoElement, + Overflow, Pixels, Point, ScrollDelta, ScrollWheelEvent, Size, Style, StyleRefinement, Styled, + Window, point, px, size, +}; +use collections::VecDeque; +use refineable::Refineable as _; +use std::{cell::RefCell, ops::Range, rc::Rc}; +use sum_tree::{Bias, Dimensions, SumTree}; + +type RenderItemFn = dyn FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static; + +/// Construct a new list element +pub fn list( + state: ListState, + render_item: impl FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static, +) -> List { + List { + state, + render_item: Box::new(render_item), + style: StyleRefinement::default(), + sizing_behavior: ListSizingBehavior::default(), + } +} + +/// A list element +pub struct List { + state: ListState, + render_item: Box, + style: StyleRefinement, + sizing_behavior: ListSizingBehavior, +} + +impl List { + /// Set the sizing behavior for the list. + pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self { + self.sizing_behavior = behavior; + self + } +} + +/// The list state that views must hold on behalf of the list element. +#[derive(Clone)] +pub struct ListState(Rc>); + +impl std::fmt::Debug for ListState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("ListState") + } +} + +struct StateInner { + last_layout_bounds: Option>, + last_padding: Option>, + items: SumTree, + logical_scroll_top: Option, + alignment: ListAlignment, + overdraw: Pixels, + reset: bool, + #[allow(clippy::type_complexity)] + scroll_handler: Option>, + scrollbar_drag_start_height: Option, + measuring_behavior: ListMeasuringBehavior, +} + +/// Whether the list is scrolling from top to bottom or bottom to top. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ListAlignment { + /// The list is scrolling from top to bottom, like most lists. + Top, + /// The list is scrolling from bottom to top, like a chat log. + Bottom, +} + +/// A scroll event that has been converted to be in terms of the list's items. +pub struct ListScrollEvent { + /// The range of items currently visible in the list, after applying the scroll event. + pub visible_range: Range, + + /// The number of items that are currently visible in the list, after applying the scroll event. + pub count: usize, + + /// Whether the list has been scrolled. + pub is_scrolled: bool, +} + +/// The sizing behavior to apply during layout. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ListSizingBehavior { + /// The list should calculate its size based on the size of its items. + Infer, + /// The list should not calculate a fixed size. + #[default] + Auto, +} + +/// The measuring behavior to apply during layout. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ListMeasuringBehavior { + /// Measure all items in the list. + /// Note: This can be expensive for the first frame in a large list. + Measure(bool), + /// Only measure visible items + #[default] + Visible, +} + +impl ListMeasuringBehavior { + fn reset(&mut self) { + match self { + ListMeasuringBehavior::Measure(has_measured) => *has_measured = false, + ListMeasuringBehavior::Visible => {} + } + } +} + +/// The horizontal sizing behavior to apply during layout. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ListHorizontalSizingBehavior { + /// List items' width can never exceed the width of the list. + #[default] + FitList, + /// List items' width may go over the width of the list, if any item is wider. + Unconstrained, +} + +struct LayoutItemsResponse { + max_item_width: Pixels, + scroll_top: ListOffset, + item_layouts: VecDeque, +} + +struct ItemLayout { + index: usize, + element: AnyElement, + size: Size, +} + +/// Frame state used by the [List] element after layout. +pub struct ListPrepaintState { + hitbox: Hitbox, + layout: LayoutItemsResponse, +} + +#[derive(Clone)] +enum ListItem { + Unmeasured { + focus_handle: Option, + }, + Measured { + size: Size, + focus_handle: Option, + }, +} + +impl ListItem { + fn size(&self) -> Option> { + if let ListItem::Measured { size, .. } = self { + Some(*size) + } else { + None + } + } + + fn focus_handle(&self) -> Option { + match self { + ListItem::Unmeasured { focus_handle } | ListItem::Measured { focus_handle, .. } => { + focus_handle.clone() + } + } + } + + fn contains_focused(&self, window: &Window, cx: &App) -> bool { + match self { + ListItem::Unmeasured { focus_handle } | ListItem::Measured { focus_handle, .. } => { + focus_handle + .as_ref() + .is_some_and(|handle| handle.contains_focused(window, cx)) + } + } + } +} + +#[derive(Clone, Debug, Default, PartialEq)] +struct ListItemSummary { + count: usize, + rendered_count: usize, + unrendered_count: usize, + height: Pixels, + has_focus_handles: bool, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +struct Count(usize); + +#[derive(Clone, Debug, Default)] +struct Height(Pixels); + +impl ListState { + /// Construct a new list state, for storage on a view. + /// + /// The overdraw parameter controls how much extra space is rendered + /// above and below the visible area. Elements within this area will + /// be measured even though they are not visible. This can help ensure + /// that the list doesn't flicker or pop in when scrolling. + pub fn new(item_count: usize, alignment: ListAlignment, overdraw: Pixels) -> Self { + let this = Self(Rc::new(RefCell::new(StateInner { + last_layout_bounds: None, + last_padding: None, + items: SumTree::default(), + logical_scroll_top: None, + alignment, + overdraw, + scroll_handler: None, + reset: false, + scrollbar_drag_start_height: None, + measuring_behavior: ListMeasuringBehavior::default(), + }))); + this.splice(0..0, item_count); + this + } + + /// Set the list to measure all items in the list in the first layout phase. + /// + /// This is useful for ensuring that the scrollbar size is correct instead of based on only rendered elements. + pub fn measure_all(self) -> Self { + self.0.borrow_mut().measuring_behavior = ListMeasuringBehavior::Measure(false); + self + } + + /// Reset this instantiation of the list state. + /// + /// Note that this will cause scroll events to be dropped until the next paint. + pub fn reset(&self, element_count: usize) { + let old_count = { + let state = &mut *self.0.borrow_mut(); + state.reset = true; + state.measuring_behavior.reset(); + state.logical_scroll_top = None; + state.scrollbar_drag_start_height = None; + state.items.summary().count + }; + + self.splice(0..old_count, element_count); + } + + /// The number of items in this list. + pub fn item_count(&self) -> usize { + self.0.borrow().items.summary().count + } + + /// Inform the list state that the items in `old_range` have been replaced + /// by `count` new items that must be recalculated. + pub fn splice(&self, old_range: Range, count: usize) { + self.splice_focusable(old_range, (0..count).map(|_| None)) + } + + /// Register with the list state that the items in `old_range` have been replaced + /// by new items. As opposed to [`Self::splice`], this method allows an iterator of optional focus handles + /// to be supplied to properly integrate with items in the list that can be focused. If a focused item + /// is scrolled out of view, the list will continue to render it to allow keyboard interaction. + pub fn splice_focusable( + &self, + old_range: Range, + focus_handles: impl IntoIterator>, + ) { + let state = &mut *self.0.borrow_mut(); + + let mut old_items = state.items.cursor::(()); + let mut new_items = old_items.slice(&Count(old_range.start), Bias::Right); + old_items.seek_forward(&Count(old_range.end), Bias::Right); + + let mut spliced_count = 0; + new_items.extend( + focus_handles.into_iter().map(|focus_handle| { + spliced_count += 1; + ListItem::Unmeasured { focus_handle } + }), + (), + ); + new_items.append(old_items.suffix(), ()); + drop(old_items); + state.items = new_items; + + if let Some(ListOffset { + item_ix, + offset_in_item, + }) = state.logical_scroll_top.as_mut() + { + if old_range.contains(item_ix) { + *item_ix = old_range.start; + *offset_in_item = px(0.); + } else if old_range.end <= *item_ix { + *item_ix = *item_ix - (old_range.end - old_range.start) + spliced_count; + } + } + } + + /// Set a handler that will be called when the list is scrolled. + pub fn set_scroll_handler( + &self, + handler: impl FnMut(&ListScrollEvent, &mut Window, &mut App) + 'static, + ) { + self.0.borrow_mut().scroll_handler = Some(Box::new(handler)) + } + + /// Get the current scroll offset, in terms of the list's items. + pub fn logical_scroll_top(&self) -> ListOffset { + self.0.borrow().logical_scroll_top() + } + + /// Scroll the list by the given offset + pub fn scroll_by(&self, distance: Pixels) { + if distance == px(0.) { + return; + } + + let current_offset = self.logical_scroll_top(); + let state = &mut *self.0.borrow_mut(); + let mut cursor = state.items.cursor::(()); + cursor.seek(&Count(current_offset.item_ix), Bias::Right); + + let start_pixel_offset = cursor.start().height + current_offset.offset_in_item; + let new_pixel_offset = (start_pixel_offset + distance).max(px(0.)); + if new_pixel_offset > start_pixel_offset { + cursor.seek_forward(&Height(new_pixel_offset), Bias::Right); + } else { + cursor.seek(&Height(new_pixel_offset), Bias::Right); + } + + state.logical_scroll_top = Some(ListOffset { + item_ix: cursor.start().count, + offset_in_item: new_pixel_offset - cursor.start().height, + }); + } + + /// Scroll the list to the given offset + pub fn scroll_to(&self, mut scroll_top: ListOffset) { + let state = &mut *self.0.borrow_mut(); + let item_count = state.items.summary().count; + if scroll_top.item_ix >= item_count { + scroll_top.item_ix = item_count; + scroll_top.offset_in_item = px(0.); + } + + state.logical_scroll_top = Some(scroll_top); + } + + /// Scroll the list to the given item, such that the item is fully visible. + pub fn scroll_to_reveal_item(&self, ix: usize) { + let state = &mut *self.0.borrow_mut(); + + let mut scroll_top = state.logical_scroll_top(); + let height = state + .last_layout_bounds + .map_or(px(0.), |bounds| bounds.size.height); + let padding = state.last_padding.unwrap_or_default(); + + if ix <= scroll_top.item_ix { + scroll_top.item_ix = ix; + scroll_top.offset_in_item = px(0.); + } else { + let mut cursor = state.items.cursor::(()); + cursor.seek(&Count(ix + 1), Bias::Right); + let bottom = cursor.start().height + padding.top; + let goal_top = px(0.).max(bottom - height + padding.bottom); + + cursor.seek(&Height(goal_top), Bias::Left); + let start_ix = cursor.start().count; + let start_item_top = cursor.start().height; + + if start_ix >= scroll_top.item_ix { + scroll_top.item_ix = start_ix; + scroll_top.offset_in_item = goal_top - start_item_top; + } + } + + state.logical_scroll_top = Some(scroll_top); + } + + /// Get the bounds for the given item in window coordinates, if it's + /// been rendered. + pub fn bounds_for_item(&self, ix: usize) -> Option> { + let state = &*self.0.borrow(); + + let bounds = state.last_layout_bounds.unwrap_or_default(); + let scroll_top = state.logical_scroll_top(); + if ix < scroll_top.item_ix { + return None; + } + + let mut cursor = state.items.cursor::>(()); + cursor.seek(&Count(scroll_top.item_ix), Bias::Right); + + let scroll_top = cursor.start().1.0 + scroll_top.offset_in_item; + + cursor.seek_forward(&Count(ix), Bias::Right); + if let Some(&ListItem::Measured { size, .. }) = cursor.item() { + let &Dimensions(Count(count), Height(top), _) = cursor.start(); + if count == ix { + let top = bounds.top() + top - scroll_top; + return Some(Bounds::from_corners( + point(bounds.left(), top), + point(bounds.right(), top + size.height), + )); + } + } + None + } + + /// Call this method when the user starts dragging the scrollbar. + /// + /// This will prevent the height reported to the scrollbar from changing during the drag + /// as items in the overdraw get measured, and help offset scroll position changes accordingly. + pub fn scrollbar_drag_started(&self) { + let mut state = self.0.borrow_mut(); + state.scrollbar_drag_start_height = Some(state.items.summary().height); + } + + /// Called when the user stops dragging the scrollbar. + /// + /// See `scrollbar_drag_started`. + pub fn scrollbar_drag_ended(&self) { + self.0.borrow_mut().scrollbar_drag_start_height.take(); + } + + /// Set the offset from the scrollbar + pub fn set_offset_from_scrollbar(&self, point: Point) { + self.0.borrow_mut().set_offset_from_scrollbar(point); + } + + /// Returns the maximum scroll offset according to the items we have measured. + /// This value remains constant while dragging to prevent the scrollbar from moving away unexpectedly. + pub fn max_offset_for_scrollbar(&self) -> Size { + let state = self.0.borrow(); + let bounds = state.last_layout_bounds.unwrap_or_default(); + + let height = state + .scrollbar_drag_start_height + .unwrap_or_else(|| state.items.summary().height); + + Size::new(Pixels::ZERO, Pixels::ZERO.max(height - bounds.size.height)) + } + + /// Returns the current scroll offset adjusted for the scrollbar + pub fn scroll_px_offset_for_scrollbar(&self) -> Point { + let state = &self.0.borrow(); + let logical_scroll_top = state.logical_scroll_top(); + + let mut cursor = state.items.cursor::(()); + let summary: ListItemSummary = + cursor.summary(&Count(logical_scroll_top.item_ix), Bias::Right); + let content_height = state.items.summary().height; + let drag_offset = + // if dragging the scrollbar, we want to offset the point if the height changed + content_height - state.scrollbar_drag_start_height.unwrap_or(content_height); + let offset = summary.height + logical_scroll_top.offset_in_item - drag_offset; + + Point::new(px(0.), -offset) + } + + /// Return the bounds of the viewport in pixels. + pub fn viewport_bounds(&self) -> Bounds { + self.0.borrow().last_layout_bounds.unwrap_or_default() + } +} + +impl StateInner { + fn visible_range(&self, height: Pixels, scroll_top: &ListOffset) -> Range { + let mut cursor = self.items.cursor::(()); + cursor.seek(&Count(scroll_top.item_ix), Bias::Right); + let start_y = cursor.start().height + scroll_top.offset_in_item; + cursor.seek_forward(&Height(start_y + height), Bias::Left); + scroll_top.item_ix..cursor.start().count + 1 + } + + fn scroll( + &mut self, + scroll_top: &ListOffset, + height: Pixels, + delta: Point, + current_view: EntityId, + window: &mut Window, + cx: &mut App, + ) { + // Drop scroll events after a reset, since we can't calculate + // the new logical scroll top without the item heights + if self.reset { + return; + } + + let padding = self.last_padding.unwrap_or_default(); + let scroll_max = + (self.items.summary().height + padding.top + padding.bottom - height).max(px(0.)); + let new_scroll_top = (self.scroll_top(scroll_top) - delta.y) + .max(px(0.)) + .min(scroll_max); + + if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max { + self.logical_scroll_top = None; + } else { + let (start, ..) = + self.items + .find::((), &Height(new_scroll_top), Bias::Right); + let item_ix = start.count; + let offset_in_item = new_scroll_top - start.height; + self.logical_scroll_top = Some(ListOffset { + item_ix, + offset_in_item, + }); + } + + if self.scroll_handler.is_some() { + let visible_range = self.visible_range(height, scroll_top); + self.scroll_handler.as_mut().unwrap()( + &ListScrollEvent { + visible_range, + count: self.items.summary().count, + is_scrolled: self.logical_scroll_top.is_some(), + }, + window, + cx, + ); + } + + cx.notify(current_view); + } + + fn logical_scroll_top(&self) -> ListOffset { + self.logical_scroll_top + .unwrap_or_else(|| match self.alignment { + ListAlignment::Top => ListOffset { + item_ix: 0, + offset_in_item: px(0.), + }, + ListAlignment::Bottom => ListOffset { + item_ix: self.items.summary().count, + offset_in_item: px(0.), + }, + }) + } + + fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels { + let (start, ..) = self.items.find::( + (), + &Count(logical_scroll_top.item_ix), + Bias::Right, + ); + start.height + logical_scroll_top.offset_in_item + } + + fn layout_all_items( + &mut self, + available_width: Pixels, + render_item: &mut RenderItemFn, + window: &mut Window, + cx: &mut App, + ) { + match &mut self.measuring_behavior { + ListMeasuringBehavior::Visible => { + return; + } + ListMeasuringBehavior::Measure(has_measured) => { + if *has_measured { + return; + } + *has_measured = true; + } + } + + let mut cursor = self.items.cursor::(()); + let available_item_space = size( + AvailableSpace::Definite(available_width), + AvailableSpace::MinContent, + ); + + let mut measured_items = Vec::default(); + + for (ix, item) in cursor.enumerate() { + let size = item.size().unwrap_or_else(|| { + let mut element = render_item(ix, window, cx); + element.layout_as_root(available_item_space, window, cx) + }); + + measured_items.push(ListItem::Measured { + size, + focus_handle: item.focus_handle(), + }); + } + + self.items = SumTree::from_iter(measured_items, ()); + } + + fn layout_items( + &mut self, + available_width: Option, + available_height: Pixels, + padding: &Edges, + render_item: &mut RenderItemFn, + window: &mut Window, + cx: &mut App, + ) -> LayoutItemsResponse { + let old_items = self.items.clone(); + let mut measured_items = VecDeque::new(); + let mut item_layouts = VecDeque::new(); + let mut rendered_height = padding.top; + let mut max_item_width = px(0.); + let mut scroll_top = self.logical_scroll_top(); + let mut rendered_focused_item = false; + + let available_item_space = size( + available_width.map_or(AvailableSpace::MinContent, |width| { + AvailableSpace::Definite(width) + }), + AvailableSpace::MinContent, + ); + + let mut cursor = old_items.cursor::(()); + + // Render items after the scroll top, including those in the trailing overdraw + cursor.seek(&Count(scroll_top.item_ix), Bias::Right); + for (ix, item) in cursor.by_ref().enumerate() { + let visible_height = rendered_height - scroll_top.offset_in_item; + if visible_height >= available_height + self.overdraw { + break; + } + + // Use the previously cached height and focus handle if available + let mut size = item.size(); + + // If we're within the visible area or the height wasn't cached, render and measure the item's element + if visible_height < available_height || size.is_none() { + let item_index = scroll_top.item_ix + ix; + let mut element = render_item(item_index, window, cx); + let element_size = element.layout_as_root(available_item_space, window, cx); + size = Some(element_size); + if visible_height < available_height { + item_layouts.push_back(ItemLayout { + index: item_index, + element, + size: element_size, + }); + if item.contains_focused(window, cx) { + rendered_focused_item = true; + } + } + } + + let size = size.unwrap(); + rendered_height += size.height; + max_item_width = max_item_width.max(size.width); + measured_items.push_back(ListItem::Measured { + size, + focus_handle: item.focus_handle(), + }); + } + rendered_height += padding.bottom; + + // Prepare to start walking upward from the item at the scroll top. + cursor.seek(&Count(scroll_top.item_ix), Bias::Right); + + // If the rendered items do not fill the visible region, then adjust + // the scroll top upward. + if rendered_height - scroll_top.offset_in_item < available_height { + while rendered_height < available_height { + cursor.prev(); + if let Some(item) = cursor.item() { + let item_index = cursor.start().0; + let mut element = render_item(item_index, window, cx); + let element_size = element.layout_as_root(available_item_space, window, cx); + let focus_handle = item.focus_handle(); + rendered_height += element_size.height; + measured_items.push_front(ListItem::Measured { + size: element_size, + focus_handle, + }); + item_layouts.push_front(ItemLayout { + index: item_index, + element, + size: element_size, + }); + if item.contains_focused(window, cx) { + rendered_focused_item = true; + } + } else { + break; + } + } + + scroll_top = ListOffset { + item_ix: cursor.start().0, + offset_in_item: rendered_height - available_height, + }; + + match self.alignment { + ListAlignment::Top => { + scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.)); + self.logical_scroll_top = Some(scroll_top); + } + ListAlignment::Bottom => { + scroll_top = ListOffset { + item_ix: cursor.start().0, + offset_in_item: rendered_height - available_height, + }; + self.logical_scroll_top = None; + } + }; + } + + // Measure items in the leading overdraw + let mut leading_overdraw = scroll_top.offset_in_item; + while leading_overdraw < self.overdraw { + cursor.prev(); + if let Some(item) = cursor.item() { + let size = if let ListItem::Measured { size, .. } = item { + *size + } else { + let mut element = render_item(cursor.start().0, window, cx); + element.layout_as_root(available_item_space, window, cx) + }; + + leading_overdraw += size.height; + measured_items.push_front(ListItem::Measured { + size, + focus_handle: item.focus_handle(), + }); + } else { + break; + } + } + + let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len()); + let mut cursor = old_items.cursor::(()); + let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right); + new_items.extend(measured_items, ()); + cursor.seek(&Count(measured_range.end), Bias::Right); + new_items.append(cursor.suffix(), ()); + self.items = new_items; + + // If none of the visible items are focused, check if an off-screen item is focused + // and include it to be rendered after the visible items so keyboard interaction continues + // to work for it. + if !rendered_focused_item { + let mut cursor = self + .items + .filter::<_, Count>((), |summary| summary.has_focus_handles); + cursor.next(); + while let Some(item) = cursor.item() { + if item.contains_focused(window, cx) { + let item_index = cursor.start().0; + let mut element = render_item(cursor.start().0, window, cx); + let size = element.layout_as_root(available_item_space, window, cx); + item_layouts.push_back(ItemLayout { + index: item_index, + element, + size, + }); + break; + } + cursor.next(); + } + } + + LayoutItemsResponse { + max_item_width, + scroll_top, + item_layouts, + } + } + + fn prepaint_items( + &mut self, + bounds: Bounds, + padding: Edges, + autoscroll: bool, + render_item: &mut RenderItemFn, + window: &mut Window, + cx: &mut App, + ) -> Result { + window.transact(|window| { + match self.measuring_behavior { + ListMeasuringBehavior::Measure(has_measured) if !has_measured => { + self.layout_all_items(bounds.size.width, render_item, window, cx); + } + _ => {} + } + + let mut layout_response = self.layout_items( + Some(bounds.size.width), + bounds.size.height, + &padding, + render_item, + window, + cx, + ); + + // Avoid honoring autoscroll requests from elements other than our children. + window.take_autoscroll(); + + // Only paint the visible items, if there is actually any space for them (taking padding into account) + if bounds.size.height > padding.top + padding.bottom { + let mut item_origin = bounds.origin + Point::new(px(0.), padding.top); + item_origin.y -= layout_response.scroll_top.offset_in_item; + for item in &mut layout_response.item_layouts { + window.with_content_mask(Some(ContentMask { bounds }), |window| { + item.element.prepaint_at(item_origin, window, cx); + }); + + if let Some(autoscroll_bounds) = window.take_autoscroll() + && autoscroll + { + if autoscroll_bounds.top() < bounds.top() { + return Err(ListOffset { + item_ix: item.index, + offset_in_item: autoscroll_bounds.top() - item_origin.y, + }); + } else if autoscroll_bounds.bottom() > bounds.bottom() { + let mut cursor = self.items.cursor::(()); + cursor.seek(&Count(item.index), Bias::Right); + let mut height = bounds.size.height - padding.top - padding.bottom; + + // Account for the height of the element down until the autoscroll bottom. + height -= autoscroll_bounds.bottom() - item_origin.y; + + // Keep decreasing the scroll top until we fill all the available space. + while height > Pixels::ZERO { + cursor.prev(); + let Some(item) = cursor.item() else { break }; + + let size = item.size().unwrap_or_else(|| { + let mut item = render_item(cursor.start().0, window, cx); + let item_available_size = + size(bounds.size.width.into(), AvailableSpace::MinContent); + item.layout_as_root(item_available_size, window, cx) + }); + height -= size.height; + } + + return Err(ListOffset { + item_ix: cursor.start().0, + offset_in_item: if height < Pixels::ZERO { + -height + } else { + Pixels::ZERO + }, + }); + } + } + + item_origin.y += item.size.height; + } + } else { + layout_response.item_layouts.clear(); + } + + Ok(layout_response) + }) + } + + // Scrollbar support + + fn set_offset_from_scrollbar(&mut self, point: Point) { + let Some(bounds) = self.last_layout_bounds else { + return; + }; + let height = bounds.size.height; + + let padding = self.last_padding.unwrap_or_default(); + let content_height = self.items.summary().height; + let scroll_max = (content_height + padding.top + padding.bottom - height).max(px(0.)); + let drag_offset = + // if dragging the scrollbar, we want to offset the point if the height changed + content_height - self.scrollbar_drag_start_height.unwrap_or(content_height); + let new_scroll_top = (point.y - drag_offset).abs().max(px(0.)).min(scroll_max); + + if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max { + self.logical_scroll_top = None; + } else { + let (start, _, _) = + self.items + .find::((), &Height(new_scroll_top), Bias::Right); + + let item_ix = start.count; + let offset_in_item = new_scroll_top - start.height; + self.logical_scroll_top = Some(ListOffset { + item_ix, + offset_in_item, + }); + } + } +} + +impl std::fmt::Debug for ListItem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Unmeasured { .. } => write!(f, "Unrendered"), + Self::Measured { size, .. } => f.debug_struct("Rendered").field("size", size).finish(), + } + } +} + +/// An offset into the list's items, in terms of the item index and the number +/// of pixels off the top left of the item. +#[derive(Debug, Clone, Copy, Default)] +pub struct ListOffset { + /// The index of an item in the list + pub item_ix: usize, + /// The number of pixels to offset from the item index. + pub offset_in_item: Pixels, +} + +impl Element for List { + type RequestLayoutState = (); + type PrepaintState = ListPrepaintState; + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (crate::LayoutId, Self::RequestLayoutState) { + let layout_id = match self.sizing_behavior { + ListSizingBehavior::Infer => { + let mut style = Style::default(); + style.overflow.y = Overflow::Scroll; + style.refine(&self.style); + window.with_text_style(style.text_style().cloned(), |window| { + let state = &mut *self.state.0.borrow_mut(); + + let available_height = if let Some(last_bounds) = state.last_layout_bounds { + last_bounds.size.height + } else { + // If we don't have the last layout bounds (first render), + // we might just use the overdraw value as the available height to layout enough items. + state.overdraw + }; + let padding = style.padding.to_pixels( + state.last_layout_bounds.unwrap_or_default().size.into(), + window.rem_size(), + ); + + let layout_response = state.layout_items( + None, + available_height, + &padding, + &mut self.render_item, + window, + cx, + ); + let max_element_width = layout_response.max_item_width; + + let summary = state.items.summary(); + let total_height = summary.height; + + window.request_measured_layout( + style, + move |known_dimensions, available_space, _window, _cx| { + let width = + known_dimensions + .width + .unwrap_or(match available_space.width { + AvailableSpace::Definite(x) => x, + AvailableSpace::MinContent | AvailableSpace::MaxContent => { + max_element_width + } + }); + let height = match available_space.height { + AvailableSpace::Definite(height) => total_height.min(height), + AvailableSpace::MinContent | AvailableSpace::MaxContent => { + total_height + } + }; + size(width, height) + }, + ) + }) + } + ListSizingBehavior::Auto => { + let mut style = Style::default(); + style.refine(&self.style); + window.with_text_style(style.text_style().cloned(), |window| { + window.request_layout(style, None, cx) + }) + } + }; + (layout_id, ()) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> ListPrepaintState { + let state = &mut *self.state.0.borrow_mut(); + state.reset = false; + + let mut style = Style::default(); + style.refine(&self.style); + + let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); + + // If the width of the list has changed, invalidate all cached item heights + if state + .last_layout_bounds + .is_none_or(|last_bounds| last_bounds.size.width != bounds.size.width) + { + let new_items = SumTree::from_iter( + state.items.iter().map(|item| ListItem::Unmeasured { + focus_handle: item.focus_handle(), + }), + (), + ); + + state.items = new_items; + } + + let padding = style + .padding + .to_pixels(bounds.size.into(), window.rem_size()); + let layout = + match state.prepaint_items(bounds, padding, true, &mut self.render_item, window, cx) { + Ok(layout) => layout, + Err(autoscroll_request) => { + state.logical_scroll_top = Some(autoscroll_request); + state + .prepaint_items(bounds, padding, false, &mut self.render_item, window, cx) + .unwrap() + } + }; + + state.last_layout_bounds = Some(bounds); + state.last_padding = Some(padding); + ListPrepaintState { hitbox, layout } + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _: &mut Self::RequestLayoutState, + prepaint: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + let current_view = window.current_view(); + window.with_content_mask(Some(ContentMask { bounds }), |window| { + for item in &mut prepaint.layout.item_layouts { + item.element.paint(window, cx); + } + }); + + let list_state = self.state.clone(); + let height = bounds.size.height; + let scroll_top = prepaint.layout.scroll_top; + let hitbox_id = prepaint.hitbox.id; + let mut accumulated_scroll_delta = ScrollDelta::default(); + window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| { + if phase == DispatchPhase::Bubble && hitbox_id.should_handle_scroll(window) { + accumulated_scroll_delta = accumulated_scroll_delta.coalesce(event.delta); + let pixel_delta = accumulated_scroll_delta.pixel_delta(px(20.)); + list_state.0.borrow_mut().scroll( + &scroll_top, + height, + pixel_delta, + current_view, + window, + cx, + ) + } + }); + } +} + +impl IntoElement for List { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Styled for List { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} + +impl sum_tree::Item for ListItem { + type Summary = ListItemSummary; + + fn summary(&self, _: ()) -> Self::Summary { + match self { + ListItem::Unmeasured { focus_handle } => ListItemSummary { + count: 1, + rendered_count: 0, + unrendered_count: 1, + height: px(0.), + has_focus_handles: focus_handle.is_some(), + }, + ListItem::Measured { + size, focus_handle, .. + } => ListItemSummary { + count: 1, + rendered_count: 1, + unrendered_count: 0, + height: size.height, + has_focus_handles: focus_handle.is_some(), + }, + } + } +} + +impl sum_tree::ContextLessSummary for ListItemSummary { + fn zero() -> Self { + Default::default() + } + + fn add_summary(&mut self, summary: &Self) { + self.count += summary.count; + self.rendered_count += summary.rendered_count; + self.unrendered_count += summary.unrendered_count; + self.height += summary.height; + self.has_focus_handles |= summary.has_focus_handles; + } +} + +impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count { + fn zero(_cx: ()) -> Self { + Default::default() + } + + fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) { + self.0 += summary.count; + } +} + +impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height { + fn zero(_cx: ()) -> Self { + Default::default() + } + + fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) { + self.0 += summary.height; + } +} + +impl sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Count { + fn cmp(&self, other: &ListItemSummary, _: ()) -> std::cmp::Ordering { + self.0.partial_cmp(&other.count).unwrap() + } +} + +impl sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Height { + fn cmp(&self, other: &ListItemSummary, _: ()) -> std::cmp::Ordering { + self.0.partial_cmp(&other.height).unwrap() + } +} + +#[cfg(test)] +mod test { + + use gpui::{ScrollDelta, ScrollWheelEvent}; + + use crate::{self as gpui, TestAppContext}; + + #[gpui::test] + fn test_reset_after_paint_before_scroll(cx: &mut TestAppContext) { + use crate::{ + AppContext, Context, Element, IntoElement, ListState, Render, Styled, Window, div, + list, point, px, size, + }; + + let cx = cx.add_empty_window(); + + let state = ListState::new(5, crate::ListAlignment::Top, px(10.)); + + // Ensure that the list is scrolled to the top + state.scroll_to(gpui::ListOffset { + item_ix: 0, + offset_in_item: px(0.0), + }); + + struct TestView(ListState); + impl Render for TestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + list(self.0.clone(), |_, _, _| { + div().h(px(10.)).w_full().into_any() + }) + .w_full() + .h_full() + } + } + + // Paint + cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| { + cx.new(|_| TestView(state.clone())) + }); + + // Reset + state.reset(5); + + // And then receive a scroll event _before_ the next paint + cx.simulate_event(ScrollWheelEvent { + position: point(px(1.), px(1.)), + delta: ScrollDelta::Pixels(point(px(0.), px(-500.))), + ..Default::default() + }); + + // Scroll position should stay at the top of the list + assert_eq!(state.logical_scroll_top().item_ix, 0); + assert_eq!(state.logical_scroll_top().offset_in_item, px(0.)); + } + + #[gpui::test] + fn test_scroll_by_positive_and_negative_distance(cx: &mut TestAppContext) { + use crate::{ + AppContext, Context, Element, IntoElement, ListState, Render, Styled, Window, div, + list, point, px, size, + }; + + let cx = cx.add_empty_window(); + + let state = ListState::new(5, crate::ListAlignment::Top, px(10.)); + + struct TestView(ListState); + impl Render for TestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + list(self.0.clone(), |_, _, _| { + div().h(px(20.)).w_full().into_any() + }) + .w_full() + .h_full() + } + } + + // Paint + cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| { + cx.new(|_| TestView(state.clone())) + }); + + // Test positive distance: start at item 1, move down 30px + state.scroll_by(px(30.)); + + // Should move to item 2 + let offset = state.logical_scroll_top(); + assert_eq!(offset.item_ix, 1); + assert_eq!(offset.offset_in_item, px(10.)); + + // Test negative distance: start at item 2, move up 30px + state.scroll_by(px(-30.)); + + // Should move back to item 1 + let offset = state.logical_scroll_top(); + assert_eq!(offset.item_ix, 0); + assert_eq!(offset.offset_in_item, px(0.)); + + // Test zero distance + state.scroll_by(px(0.)); + let offset = state.logical_scroll_top(); + assert_eq!(offset.item_ix, 0); + assert_eq!(offset.offset_in_item, px(0.)); + } +} diff --git a/third_party/gpui/src/elements/mod.rs b/third_party/gpui/src/elements/mod.rs new file mode 100644 index 0000000..bfbc08b --- /dev/null +++ b/third_party/gpui/src/elements/mod.rs @@ -0,0 +1,25 @@ +mod anchored; +mod animation; +mod canvas; +mod deferred; +mod div; +mod image_cache; +mod img; +mod list; +mod surface; +mod svg; +mod text; +mod uniform_list; + +pub use anchored::*; +pub use animation::*; +pub use canvas::*; +pub use deferred::*; +pub use div::*; +pub use image_cache::*; +pub use img::*; +pub use list::*; +pub use surface::*; +pub use svg::*; +pub use text::*; +pub use uniform_list::*; diff --git a/third_party/gpui/src/elements/surface.rs b/third_party/gpui/src/elements/surface.rs new file mode 100644 index 0000000..b4fced1 --- /dev/null +++ b/third_party/gpui/src/elements/surface.rs @@ -0,0 +1,120 @@ +use crate::{ + App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId, + ObjectFit, Pixels, Style, StyleRefinement, Styled, Window, +}; +#[cfg(target_os = "macos")] +use core_video::pixel_buffer::CVPixelBuffer; +use refineable::Refineable; + +/// A source of a surface's content. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SurfaceSource { + /// A macOS image buffer from CoreVideo + #[cfg(target_os = "macos")] + Surface(CVPixelBuffer), +} + +#[cfg(target_os = "macos")] +impl From for SurfaceSource { + fn from(value: CVPixelBuffer) -> Self { + SurfaceSource::Surface(value) + } +} + +/// A surface element. +pub struct Surface { + source: SurfaceSource, + object_fit: ObjectFit, + style: StyleRefinement, +} + +/// Create a new surface element. +pub fn surface(source: impl Into) -> Surface { + Surface { + source: source.into(), + object_fit: ObjectFit::Contain, + style: Default::default(), + } +} + +impl Surface { + /// Set the object fit for the image. + pub fn object_fit(mut self, object_fit: ObjectFit) -> Self { + self.object_fit = object_fit; + self + } +} + +impl Element for Surface { + type RequestLayoutState = (); + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let mut style = Style::default(); + style.refine(&self.style); + let layout_id = window.request_layout(style, [], cx); + (layout_id, ()) + } + + fn prepaint( + &mut self, + _global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _window: &mut Window, + _cx: &mut App, + ) -> Self::PrepaintState { + } + + fn paint( + &mut self, + _global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + #[cfg_attr(not(target_os = "macos"), allow(unused_variables))] bounds: Bounds, + _: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + #[cfg_attr(not(target_os = "macos"), allow(unused_variables))] window: &mut Window, + _: &mut App, + ) { + match &self.source { + #[cfg(target_os = "macos")] + SurfaceSource::Surface(surface) => { + let size = crate::size(surface.get_width().into(), surface.get_height().into()); + let new_bounds = self.object_fit.get_bounds(bounds, size); + // TODO: Add support for corner_radii + window.paint_surface(new_bounds, surface.clone()); + } + #[allow(unreachable_patterns)] + _ => {} + } + } +} + +impl IntoElement for Surface { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Styled for Surface { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} diff --git a/third_party/gpui/src/elements/svg.rs b/third_party/gpui/src/elements/svg.rs new file mode 100644 index 0000000..a55245d --- /dev/null +++ b/third_party/gpui/src/elements/svg.rs @@ -0,0 +1,221 @@ +use crate::{ + App, Bounds, Element, GlobalElementId, Hitbox, InspectorElementId, InteractiveElement, + Interactivity, IntoElement, LayoutId, Pixels, Point, Radians, SharedString, Size, + StyleRefinement, Styled, TransformationMatrix, Window, geometry::Negate as _, point, px, + radians, size, +}; +use util::ResultExt; + +/// An SVG element. +pub struct Svg { + interactivity: Interactivity, + transformation: Option, + path: Option, +} + +/// Create a new SVG element. +#[track_caller] +pub fn svg() -> Svg { + Svg { + interactivity: Interactivity::new(), + transformation: None, + path: None, + } +} + +impl Svg { + /// Set the path to the SVG file for this element. + pub fn path(mut self, path: impl Into) -> Self { + self.path = Some(path.into()); + self + } + + /// Transform the SVG element with the given transformation. + /// Note that this won't effect the hitbox or layout of the element, only the rendering. + pub fn with_transformation(mut self, transformation: Transformation) -> Self { + self.transformation = Some(transformation); + self + } +} + +impl Element for Svg { + type RequestLayoutState = (); + type PrepaintState = Option; + + fn id(&self) -> Option { + self.interactivity.element_id.clone() + } + + fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { + self.interactivity.source_location() + } + + fn request_layout( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let layout_id = self.interactivity.request_layout( + global_id, + inspector_id, + window, + cx, + |style, window, cx| window.request_layout(style, None, cx), + ); + (layout_id, ()) + } + + fn prepaint( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Option { + self.interactivity.prepaint( + global_id, + inspector_id, + bounds, + bounds.size, + window, + cx, + |_, _, hitbox, _, _| hitbox, + ) + } + + fn paint( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + hitbox: &mut Option, + window: &mut Window, + cx: &mut App, + ) where + Self: Sized, + { + self.interactivity.paint( + global_id, + inspector_id, + bounds, + hitbox.as_ref(), + window, + cx, + |style, window, cx| { + if let Some((path, color)) = self.path.as_ref().zip(style.text.color) { + let transformation = self + .transformation + .as_ref() + .map(|transformation| { + transformation.into_matrix(bounds.center(), window.scale_factor()) + }) + .unwrap_or_default(); + + window + .paint_svg(bounds, path.clone(), transformation, color, cx) + .log_err(); + } + }, + ) + } +} + +impl IntoElement for Svg { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Styled for Svg { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.interactivity.base_style + } +} + +impl InteractiveElement for Svg { + fn interactivity(&mut self) -> &mut Interactivity { + &mut self.interactivity + } +} + +/// A transformation to apply to an SVG element. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Transformation { + scale: Size, + translate: Point, + rotate: Radians, +} + +impl Default for Transformation { + fn default() -> Self { + Self { + scale: size(1.0, 1.0), + translate: point(px(0.0), px(0.0)), + rotate: radians(0.0), + } + } +} + +impl Transformation { + /// Create a new Transformation with the specified scale along each axis. + pub fn scale(scale: Size) -> Self { + Self { + scale, + translate: point(px(0.0), px(0.0)), + rotate: radians(0.0), + } + } + + /// Create a new Transformation with the specified translation. + pub fn translate(translate: Point) -> Self { + Self { + scale: size(1.0, 1.0), + translate, + rotate: radians(0.0), + } + } + + /// Create a new Transformation with the specified rotation in radians. + pub fn rotate(rotate: impl Into) -> Self { + let rotate = rotate.into(); + Self { + scale: size(1.0, 1.0), + translate: point(px(0.0), px(0.0)), + rotate, + } + } + + /// Update the scaling factor of this transformation. + pub fn with_scaling(mut self, scale: Size) -> Self { + self.scale = scale; + self + } + + /// Update the translation value of this transformation. + pub fn with_translation(mut self, translate: Point) -> Self { + self.translate = translate; + self + } + + /// Update the rotation angle of this transformation. + pub fn with_rotation(mut self, rotate: impl Into) -> Self { + self.rotate = rotate.into(); + self + } + + fn into_matrix(self, center: Point, scale_factor: f32) -> TransformationMatrix { + //Note: if you read this as a sequence of matrix multiplications, start from the bottom + TransformationMatrix::unit() + .translate(center.scale(scale_factor) + self.translate.scale(scale_factor)) + .rotate(self.rotate) + .scale(self.scale) + .translate(center.scale(scale_factor).negate()) + } +} diff --git a/third_party/gpui/src/elements/text.rs b/third_party/gpui/src/elements/text.rs new file mode 100644 index 0000000..5d34ccf --- /dev/null +++ b/third_party/gpui/src/elements/text.rs @@ -0,0 +1,914 @@ +use crate::{ + ActiveTooltip, AnyView, App, Bounds, DispatchPhase, Element, ElementId, GlobalElementId, + HighlightStyle, Hitbox, HitboxBehavior, InspectorElementId, IntoElement, LayoutId, + MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextOverflow, + TextRun, TextStyle, TooltipId, WhiteSpace, Window, WrappedLine, WrappedLineLayout, + register_tooltip_mouse_handlers, set_tooltip_on_window, +}; +use anyhow::Context as _; +use smallvec::SmallVec; +use std::{ + cell::{Cell, RefCell}, + mem, + ops::Range, + rc::Rc, + sync::Arc, +}; +use util::ResultExt; + +impl Element for &'static str { + type RequestLayoutState = TextLayout; + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let mut state = TextLayout::default(); + let layout_id = state.layout(SharedString::from(*self), None, window, cx); + (layout_id, state) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + text_layout: &mut Self::RequestLayoutState, + _window: &mut Window, + _cx: &mut App, + ) { + text_layout.prepaint(bounds, self) + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + text_layout: &mut TextLayout, + _: &mut (), + window: &mut Window, + cx: &mut App, + ) { + text_layout.paint(self, window, cx) + } +} + +impl IntoElement for &'static str { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl IntoElement for String { + type Element = SharedString; + + fn into_element(self) -> Self::Element { + self.into() + } +} + +impl Element for SharedString { + type RequestLayoutState = TextLayout; + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let mut state = TextLayout::default(); + let layout_id = state.layout(self.clone(), None, window, cx); + (layout_id, state) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + text_layout: &mut Self::RequestLayoutState, + _window: &mut Window, + _cx: &mut App, + ) { + text_layout.prepaint(bounds, self.as_ref()) + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + text_layout: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + text_layout.paint(self.as_ref(), window, cx) + } +} + +impl IntoElement for SharedString { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +/// Renders text with runs of different styles. +/// +/// Callers are responsible for setting the correct style for each run. +/// For text with a uniform style, you can usually avoid calling this constructor +/// and just pass text directly. +pub struct StyledText { + text: SharedString, + runs: Option>, + delayed_highlights: Option, HighlightStyle)>>, + layout: TextLayout, +} + +impl StyledText { + /// Construct a new styled text element from the given string. + pub fn new(text: impl Into) -> Self { + StyledText { + text: text.into(), + runs: None, + delayed_highlights: None, + layout: TextLayout::default(), + } + } + + /// Get the layout for this element. This can be used to map indices to pixels and vice versa. + pub fn layout(&self) -> &TextLayout { + &self.layout + } + + /// Set the styling attributes for the given text, as well as + /// as any ranges of text that have had their style customized. + pub fn with_default_highlights( + mut self, + default_style: &TextStyle, + highlights: impl IntoIterator, HighlightStyle)>, + ) -> Self { + debug_assert!( + self.delayed_highlights.is_none(), + "Can't use `with_default_highlights` and `with_highlights`" + ); + let runs = Self::compute_runs(&self.text, default_style, highlights); + self.with_runs(runs) + } + + /// Set the styling attributes for the given text, as well as + /// as any ranges of text that have had their style customized. + pub fn with_highlights( + mut self, + highlights: impl IntoIterator, HighlightStyle)>, + ) -> Self { + debug_assert!( + self.runs.is_none(), + "Can't use `with_highlights` and `with_default_highlights`" + ); + self.delayed_highlights = Some( + highlights + .into_iter() + .inspect(|(run, _)| { + debug_assert!(self.text.is_char_boundary(run.start)); + debug_assert!(self.text.is_char_boundary(run.end)); + }) + .collect::>(), + ); + self + } + + fn compute_runs( + text: &str, + default_style: &TextStyle, + highlights: impl IntoIterator, HighlightStyle)>, + ) -> Vec { + let mut runs = Vec::new(); + let mut ix = 0; + for (range, highlight) in highlights { + if ix < range.start { + debug_assert!(text.is_char_boundary(range.start)); + runs.push(default_style.clone().to_run(range.start - ix)); + } + debug_assert!(text.is_char_boundary(range.end)); + runs.push( + default_style + .clone() + .highlight(highlight) + .to_run(range.len()), + ); + ix = range.end; + } + if ix < text.len() { + runs.push(default_style.to_run(text.len() - ix)); + } + runs + } + + /// Set the text runs for this piece of text. + pub fn with_runs(mut self, runs: Vec) -> Self { + let mut text = &**self.text; + for run in &runs { + text = text.get(run.len..).expect("invalid text run"); + } + assert!(text.is_empty(), "invalid text run"); + self.runs = Some(runs); + self + } +} + +impl Element for StyledText { + type RequestLayoutState = (); + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let runs = self.runs.take().or_else(|| { + self.delayed_highlights.take().map(|delayed_highlights| { + Self::compute_runs(&self.text, &window.text_style(), delayed_highlights) + }) + }); + + let layout_id = self.layout.layout(self.text.clone(), runs, window, cx); + (layout_id, ()) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _: &mut Self::RequestLayoutState, + _window: &mut Window, + _cx: &mut App, + ) { + self.layout.prepaint(bounds, &self.text) + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + self.layout.paint(&self.text, window, cx) + } +} + +impl IntoElement for StyledText { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +/// The Layout for TextElement. This can be used to map indices to pixels and vice versa. +#[derive(Default, Clone)] +pub struct TextLayout(Rc>>); + +struct TextLayoutInner { + len: usize, + lines: SmallVec<[WrappedLine; 1]>, + line_height: Pixels, + wrap_width: Option, + size: Option>, + bounds: Option>, +} + +impl TextLayout { + fn layout( + &self, + text: SharedString, + runs: Option>, + window: &mut Window, + _: &mut App, + ) -> LayoutId { + let text_style = window.text_style(); + let font_size = text_style.font_size.to_pixels(window.rem_size()); + let line_height = text_style + .line_height + .to_pixels(font_size.into(), window.rem_size()); + + let mut runs = if let Some(runs) = runs { + runs + } else { + vec![text_style.to_run(text.len())] + }; + + window.request_measured_layout(Default::default(), { + let element_state = self.clone(); + + move |known_dimensions, available_space, window, cx| { + let wrap_width = if text_style.white_space == WhiteSpace::Normal { + known_dimensions.width.or(match available_space.width { + crate::AvailableSpace::Definite(x) => Some(x), + _ => None, + }) + } else { + None + }; + + let (truncate_width, truncation_suffix) = + if let Some(text_overflow) = text_style.text_overflow.clone() { + let width = known_dimensions.width.or(match available_space.width { + crate::AvailableSpace::Definite(x) => match text_style.line_clamp { + Some(max_lines) => Some(x * max_lines), + None => Some(x), + }, + _ => None, + }); + + match text_overflow { + TextOverflow::Truncate(s) => (width, s), + } + } else { + (None, "".into()) + }; + + if let Some(text_layout) = element_state.0.borrow().as_ref() + && text_layout.size.is_some() + && (wrap_width.is_none() || wrap_width == text_layout.wrap_width) + { + return text_layout.size.unwrap(); + } + + let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size); + let text = if let Some(truncate_width) = truncate_width { + line_wrapper.truncate_line( + text.clone(), + truncate_width, + &truncation_suffix, + &mut runs, + ) + } else { + text.clone() + }; + let len = text.len(); + + let Some(lines) = window + .text_system() + .shape_text( + text, + font_size, + &runs, + wrap_width, // Wrap if we know the width. + text_style.line_clamp, // Limit the number of lines if line_clamp is set. + ) + .log_err() + else { + element_state.0.borrow_mut().replace(TextLayoutInner { + lines: Default::default(), + len: 0, + line_height, + wrap_width, + size: Some(Size::default()), + bounds: None, + }); + return Size::default(); + }; + + let mut size: Size = Size::default(); + for line in &lines { + let line_size = line.size(line_height); + size.height += line_size.height; + size.width = size.width.max(line_size.width).ceil(); + } + + element_state.0.borrow_mut().replace(TextLayoutInner { + lines, + len, + line_height, + wrap_width, + size: Some(size), + bounds: None, + }); + + size + } + }) + } + + fn prepaint(&self, bounds: Bounds, text: &str) { + let mut element_state = self.0.borrow_mut(); + let element_state = element_state + .as_mut() + .with_context(|| format!("measurement has not been performed on {text}")) + .unwrap(); + element_state.bounds = Some(bounds); + } + + fn paint(&self, text: &str, window: &mut Window, cx: &mut App) { + let element_state = self.0.borrow(); + let element_state = element_state + .as_ref() + .with_context(|| format!("measurement has not been performed on {text}")) + .unwrap(); + let bounds = element_state + .bounds + .with_context(|| format!("prepaint has not been performed on {text}")) + .unwrap(); + + let line_height = element_state.line_height; + let mut line_origin = bounds.origin; + let text_style = window.text_style(); + for line in &element_state.lines { + line.paint_background( + line_origin, + line_height, + text_style.text_align, + Some(bounds), + window, + cx, + ) + .log_err(); + line.paint( + line_origin, + line_height, + text_style.text_align, + Some(bounds), + window, + cx, + ) + .log_err(); + line_origin.y += line.size(line_height).height; + } + } + + /// Get the byte index into the input of the pixel position. + pub fn index_for_position(&self, mut position: Point) -> Result { + let element_state = self.0.borrow(); + let element_state = element_state + .as_ref() + .expect("measurement has not been performed"); + let bounds = element_state + .bounds + .expect("prepaint has not been performed"); + + if position.y < bounds.top() { + return Err(0); + } + + let line_height = element_state.line_height; + let mut line_origin = bounds.origin; + let mut line_start_ix = 0; + for line in &element_state.lines { + let line_bottom = line_origin.y + line.size(line_height).height; + if position.y > line_bottom { + line_origin.y = line_bottom; + line_start_ix += line.len() + 1; + } else { + let position_within_line = position - line_origin; + match line.index_for_position(position_within_line, line_height) { + Ok(index_within_line) => return Ok(line_start_ix + index_within_line), + Err(index_within_line) => return Err(line_start_ix + index_within_line), + } + } + } + + Err(line_start_ix.saturating_sub(1)) + } + + /// Get the pixel position for the given byte index. + pub fn position_for_index(&self, index: usize) -> Option> { + let element_state = self.0.borrow(); + let element_state = element_state + .as_ref() + .expect("measurement has not been performed"); + let bounds = element_state + .bounds + .expect("prepaint has not been performed"); + let line_height = element_state.line_height; + + let mut line_origin = bounds.origin; + let mut line_start_ix = 0; + + for line in &element_state.lines { + let line_end_ix = line_start_ix + line.len(); + if index < line_start_ix { + break; + } else if index > line_end_ix { + line_origin.y += line.size(line_height).height; + line_start_ix = line_end_ix + 1; + continue; + } else { + let ix_within_line = index - line_start_ix; + return Some(line_origin + line.position_for_index(ix_within_line, line_height)?); + } + } + + None + } + + /// Retrieve the layout for the line containing the given byte index. + pub fn line_layout_for_index(&self, index: usize) -> Option> { + let element_state = self.0.borrow(); + let element_state = element_state + .as_ref() + .expect("measurement has not been performed"); + let bounds = element_state + .bounds + .expect("prepaint has not been performed"); + let line_height = element_state.line_height; + + let mut line_origin = bounds.origin; + let mut line_start_ix = 0; + + for line in &element_state.lines { + let line_end_ix = line_start_ix + line.len(); + if index < line_start_ix { + break; + } else if index > line_end_ix { + line_origin.y += line.size(line_height).height; + line_start_ix = line_end_ix + 1; + continue; + } else { + return Some(line.layout.clone()); + } + } + + None + } + + /// The bounds of this layout. + pub fn bounds(&self) -> Bounds { + self.0.borrow().as_ref().unwrap().bounds.unwrap() + } + + /// The line height for this layout. + pub fn line_height(&self) -> Pixels { + self.0.borrow().as_ref().unwrap().line_height + } + + /// The UTF-8 length of the underlying text. + pub fn len(&self) -> usize { + self.0.borrow().as_ref().unwrap().len + } + + /// The text for this layout. + pub fn text(&self) -> String { + self.0 + .borrow() + .as_ref() + .unwrap() + .lines + .iter() + .map(|s| s.text.to_string()) + .collect::>() + .join("\n") + } + + /// The text for this layout (with soft-wraps as newlines) + pub fn wrapped_text(&self) -> String { + let mut lines = Vec::new(); + for wrapped in self.0.borrow().as_ref().unwrap().lines.iter() { + let mut seen = 0; + for boundary in wrapped.layout.wrap_boundaries.iter() { + let index = wrapped.layout.unwrapped_layout.runs[boundary.run_ix].glyphs + [boundary.glyph_ix] + .index; + + lines.push(wrapped.text[seen..index].to_string()); + seen = index; + } + lines.push(wrapped.text[seen..].to_string()); + } + + lines.join("\n") + } +} + +/// A text element that can be interacted with. +pub struct InteractiveText { + element_id: ElementId, + text: StyledText, + click_listener: + Option], InteractiveTextClickEvent, &mut Window, &mut App)>>, + hover_listener: Option, MouseMoveEvent, &mut Window, &mut App)>>, + tooltip_builder: Option Option>>, + tooltip_id: Option, + clickable_ranges: Vec>, +} + +struct InteractiveTextClickEvent { + mouse_down_index: usize, + mouse_up_index: usize, +} + +#[doc(hidden)] +#[derive(Default)] +pub struct InteractiveTextState { + mouse_down_index: Rc>>, + hovered_index: Rc>>, + active_tooltip: Rc>>, +} + +/// InteractiveTest is a wrapper around StyledText that adds mouse interactions. +impl InteractiveText { + /// Creates a new InteractiveText from the given text. + pub fn new(id: impl Into, text: StyledText) -> Self { + Self { + element_id: id.into(), + text, + click_listener: None, + hover_listener: None, + tooltip_builder: None, + tooltip_id: None, + clickable_ranges: Vec::new(), + } + } + + /// on_click is called when the user clicks on one of the given ranges, passing the index of + /// the clicked range. + pub fn on_click( + mut self, + ranges: Vec>, + listener: impl Fn(usize, &mut Window, &mut App) + 'static, + ) -> Self { + self.click_listener = Some(Box::new(move |ranges, event, window, cx| { + for (range_ix, range) in ranges.iter().enumerate() { + if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index) + { + listener(range_ix, window, cx); + } + } + })); + self.clickable_ranges = ranges; + self + } + + /// on_hover is called when the mouse moves over a character within the text, passing the + /// index of the hovered character, or None if the mouse leaves the text. + pub fn on_hover( + mut self, + listener: impl Fn(Option, MouseMoveEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.hover_listener = Some(Box::new(listener)); + self + } + + /// tooltip lets you specify a tooltip for a given character index in the string. + pub fn tooltip( + mut self, + builder: impl Fn(usize, &mut Window, &mut App) -> Option + 'static, + ) -> Self { + self.tooltip_builder = Some(Rc::new(builder)); + self + } +} + +impl Element for InteractiveText { + type RequestLayoutState = (); + type PrepaintState = Hitbox; + + fn id(&self) -> Option { + Some(self.element_id.clone()) + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + self.text.request_layout(None, inspector_id, window, cx) + } + + fn prepaint( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + state: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Hitbox { + window.with_optional_element_state::( + global_id, + |interactive_state, window| { + let mut interactive_state = interactive_state + .map(|interactive_state| interactive_state.unwrap_or_default()); + + if let Some(interactive_state) = interactive_state.as_mut() { + if self.tooltip_builder.is_some() { + self.tooltip_id = + set_tooltip_on_window(&interactive_state.active_tooltip, window); + } else { + // If there is no longer a tooltip builder, remove the active tooltip. + interactive_state.active_tooltip.take(); + } + } + + self.text + .prepaint(None, inspector_id, bounds, state, window, cx); + let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); + (hitbox, interactive_state) + }, + ) + } + + fn paint( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _: &mut Self::RequestLayoutState, + hitbox: &mut Hitbox, + window: &mut Window, + cx: &mut App, + ) { + let current_view = window.current_view(); + let text_layout = self.text.layout().clone(); + window.with_element_state::( + global_id.unwrap(), + |interactive_state, window| { + let mut interactive_state = interactive_state.unwrap_or_default(); + if let Some(click_listener) = self.click_listener.take() { + let mouse_position = window.mouse_position(); + if let Ok(ix) = text_layout.index_for_position(mouse_position) + && self + .clickable_ranges + .iter() + .any(|range| range.contains(&ix)) + { + window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox) + } + + let text_layout = text_layout.clone(); + let mouse_down = interactive_state.mouse_down_index.clone(); + if let Some(mouse_down_index) = mouse_down.get() { + let hitbox = hitbox.clone(); + let clickable_ranges = mem::take(&mut self.clickable_ranges); + window.on_mouse_event( + move |event: &MouseUpEvent, phase, window: &mut Window, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { + if let Ok(mouse_up_index) = + text_layout.index_for_position(event.position) + { + click_listener( + &clickable_ranges, + InteractiveTextClickEvent { + mouse_down_index, + mouse_up_index, + }, + window, + cx, + ) + } + + mouse_down.take(); + window.refresh(); + } + }, + ); + } else { + let hitbox = hitbox.clone(); + window.on_mouse_event(move |event: &MouseDownEvent, phase, window, _| { + if phase == DispatchPhase::Bubble + && hitbox.is_hovered(window) + && let Ok(mouse_down_index) = + text_layout.index_for_position(event.position) + { + mouse_down.set(Some(mouse_down_index)); + window.refresh(); + } + }); + } + } + + window.on_mouse_event({ + let mut hover_listener = self.hover_listener.take(); + let hitbox = hitbox.clone(); + let text_layout = text_layout.clone(); + let hovered_index = interactive_state.hovered_index.clone(); + move |event: &MouseMoveEvent, phase, window, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { + let current = hovered_index.get(); + let updated = text_layout.index_for_position(event.position).ok(); + if current != updated { + hovered_index.set(updated); + if let Some(hover_listener) = hover_listener.as_ref() { + hover_listener(updated, event.clone(), window, cx); + } + cx.notify(current_view); + } + } + } + }); + + if let Some(tooltip_builder) = self.tooltip_builder.clone() { + let active_tooltip = interactive_state.active_tooltip.clone(); + let build_tooltip = Rc::new({ + let tooltip_is_hoverable = false; + let text_layout = text_layout.clone(); + move |window: &mut Window, cx: &mut App| { + text_layout + .index_for_position(window.mouse_position()) + .ok() + .and_then(|position| tooltip_builder(position, window, cx)) + .map(|view| (view, tooltip_is_hoverable)) + } + }); + + // Use bounds instead of testing hitbox since this is called during prepaint. + let check_is_hovered_during_prepaint = Rc::new({ + let source_bounds = hitbox.bounds; + let text_layout = text_layout.clone(); + let pending_mouse_down = interactive_state.mouse_down_index.clone(); + move |window: &Window| { + text_layout + .index_for_position(window.mouse_position()) + .is_ok() + && source_bounds.contains(&window.mouse_position()) + && pending_mouse_down.get().is_none() + } + }); + + let check_is_hovered = Rc::new({ + let hitbox = hitbox.clone(); + let text_layout = text_layout.clone(); + let pending_mouse_down = interactive_state.mouse_down_index.clone(); + move |window: &Window| { + text_layout + .index_for_position(window.mouse_position()) + .is_ok() + && hitbox.is_hovered(window) + && pending_mouse_down.get().is_none() + } + }); + + register_tooltip_mouse_handlers( + &active_tooltip, + self.tooltip_id, + build_tooltip, + check_is_hovered, + check_is_hovered_during_prepaint, + window, + ); + } + + self.text + .paint(None, inspector_id, bounds, &mut (), &mut (), window, cx); + + ((), interactive_state) + }, + ); + } +} + +impl IntoElement for InteractiveText { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} diff --git a/third_party/gpui/src/elements/uniform_list.rs b/third_party/gpui/src/elements/uniform_list.rs new file mode 100644 index 0000000..3b721d4 --- /dev/null +++ b/third_party/gpui/src/elements/uniform_list.rs @@ -0,0 +1,710 @@ +//! A scrollable list of elements with uniform height, optimized for large lists. +//! Rather than use the full taffy layout system, uniform_list simply measures +//! the first element and then lays out all remaining elements in a line based on that +//! measurement. This is much faster than the full layout system, but only works for +//! elements with uniform height. + +use crate::{ + AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, Entity, + GlobalElementId, Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement, + IsZero, LayoutId, ListSizingBehavior, Overflow, Pixels, Point, ScrollHandle, Size, + StyleRefinement, Styled, Window, point, size, +}; +use smallvec::SmallVec; +use std::{cell::RefCell, cmp, ops::Range, rc::Rc}; + +use super::ListHorizontalSizingBehavior; + +/// uniform_list provides lazy rendering for a set of items that are of uniform height. +/// When rendered into a container with overflow-y: hidden and a fixed (or max) height, +/// uniform_list will only render the visible subset of items. +#[track_caller] +pub fn uniform_list( + id: impl Into, + item_count: usize, + f: impl 'static + Fn(Range, &mut Window, &mut App) -> Vec, +) -> UniformList +where + R: IntoElement, +{ + let id = id.into(); + let mut base_style = StyleRefinement::default(); + base_style.overflow.y = Some(Overflow::Scroll); + + let render_range = move |range: Range, window: &mut Window, cx: &mut App| { + f(range, window, cx) + .into_iter() + .map(|component| component.into_any_element()) + .collect() + }; + + UniformList { + item_count, + item_to_measure_index: 0, + render_items: Box::new(render_range), + decorations: Vec::new(), + interactivity: Interactivity { + element_id: Some(id), + base_style: Box::new(base_style), + ..Interactivity::new() + }, + scroll_handle: None, + sizing_behavior: ListSizingBehavior::default(), + horizontal_sizing_behavior: ListHorizontalSizingBehavior::default(), + } +} + +/// A list element for efficiently laying out and displaying a list of uniform-height elements. +pub struct UniformList { + item_count: usize, + item_to_measure_index: usize, + render_items: Box< + dyn for<'a> Fn(Range, &'a mut Window, &'a mut App) -> SmallVec<[AnyElement; 64]>, + >, + decorations: Vec>, + interactivity: Interactivity, + scroll_handle: Option, + sizing_behavior: ListSizingBehavior, + horizontal_sizing_behavior: ListHorizontalSizingBehavior, +} + +/// Frame state used by the [UniformList]. +pub struct UniformListFrameState { + items: SmallVec<[AnyElement; 32]>, + decorations: SmallVec<[AnyElement; 2]>, +} + +/// A handle for controlling the scroll position of a uniform list. +/// This should be stored in your view and passed to the uniform_list on each frame. +#[derive(Clone, Debug, Default)] +pub struct UniformListScrollHandle(pub Rc>); + +/// Where to place the element scrolled to. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ScrollStrategy { + /// Place the element at the top of the list's viewport. + Top, + /// Attempt to place the element in the middle of the list's viewport. + /// May not be possible if there's not enough list items above the item scrolled to: + /// in this case, the element will be placed at the closest possible position. + Center, + /// Attempt to place the element at the bottom of the list's viewport. + /// May not be possible if there's not enough list items above the item scrolled to: + /// in this case, the element will be placed at the closest possible position. + Bottom, +} + +#[derive(Clone, Copy, Debug)] +#[allow(missing_docs)] +pub struct DeferredScrollToItem { + /// The item index to scroll to + pub item_index: usize, + /// The scroll strategy to use + pub strategy: ScrollStrategy, + /// The offset in number of items + pub offset: usize, + pub scroll_strict: bool, +} + +#[derive(Clone, Debug, Default)] +#[allow(missing_docs)] +pub struct UniformListScrollState { + pub base_handle: ScrollHandle, + pub deferred_scroll_to_item: Option, + /// Size of the item, captured during last layout. + pub last_item_size: Option, + /// Whether the list was vertically flipped during last layout. + pub y_flipped: bool, +} + +#[derive(Copy, Clone, Debug, Default)] +/// The size of the item and its contents. +pub struct ItemSize { + /// The size of the item. + pub item: Size, + /// The size of the item's contents, which may be larger than the item itself, + /// if the item was bounded by a parent element. + pub contents: Size, +} + +impl UniformListScrollHandle { + /// Create a new scroll handle to bind to a uniform list. + pub fn new() -> Self { + Self(Rc::new(RefCell::new(UniformListScrollState { + base_handle: ScrollHandle::new(), + deferred_scroll_to_item: None, + last_item_size: None, + y_flipped: false, + }))) + } + + /// Scroll the list so that the given item index is visible. + /// + /// This uses non-strict scrolling: if the item is already fully visible, no scrolling occurs. + /// If the item is out of view, it scrolls the minimum amount to bring it into view according + /// to the strategy. + pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) { + self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem { + item_index: ix, + strategy, + offset: 0, + scroll_strict: false, + }); + } + + /// Scroll the list so that the given item index is at scroll strategy position. + /// + /// This uses strict scrolling: the item will always be scrolled to match the strategy position, + /// even if it's already visible. Use this when you need precise positioning. + pub fn scroll_to_item_strict(&self, ix: usize, strategy: ScrollStrategy) { + self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem { + item_index: ix, + strategy, + offset: 0, + scroll_strict: true, + }); + } + + /// Scroll the list to the given item index with an offset in number of items. + /// + /// This uses non-strict scrolling: if the item is already visible within the offset region, + /// no scrolling occurs. + /// + /// The offset parameter shrinks the effective viewport by the specified number of items + /// from the corresponding edge, then applies the scroll strategy within that reduced viewport: + /// - `ScrollStrategy::Top`: Shrinks from top, positions item at the new top + /// - `ScrollStrategy::Center`: Shrinks from top, centers item in the reduced viewport + /// - `ScrollStrategy::Bottom`: Shrinks from bottom, positions item at the new bottom + pub fn scroll_to_item_with_offset(&self, ix: usize, strategy: ScrollStrategy, offset: usize) { + self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem { + item_index: ix, + strategy, + offset, + scroll_strict: false, + }); + } + + /// Scroll the list so that the given item index is at the exact scroll strategy position with an offset. + /// + /// This uses strict scrolling: the item will always be scrolled to match the strategy position, + /// even if it's already visible. + /// + /// The offset parameter shrinks the effective viewport by the specified number of items + /// from the corresponding edge, then applies the scroll strategy within that reduced viewport: + /// - `ScrollStrategy::Top`: Shrinks from top, positions item at the new top + /// - `ScrollStrategy::Center`: Shrinks from top, centers item in the reduced viewport + /// - `ScrollStrategy::Bottom`: Shrinks from bottom, positions item at the new bottom + pub fn scroll_to_item_strict_with_offset( + &self, + ix: usize, + strategy: ScrollStrategy, + offset: usize, + ) { + self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem { + item_index: ix, + strategy, + offset, + scroll_strict: true, + }); + } + + /// Check if the list is flipped vertically. + pub fn y_flipped(&self) -> bool { + self.0.borrow().y_flipped + } + + /// Get the index of the topmost visible child. + #[cfg(any(test, feature = "test-support"))] + pub fn logical_scroll_top_index(&self) -> usize { + let this = self.0.borrow(); + this.deferred_scroll_to_item + .as_ref() + .map(|deferred| deferred.item_index) + .unwrap_or_else(|| this.base_handle.logical_scroll_top().0) + } + + /// Checks if the list can be scrolled vertically. + pub fn is_scrollable(&self) -> bool { + if let Some(size) = self.0.borrow().last_item_size { + size.contents.height > size.item.height + } else { + false + } + } +} + +impl Styled for UniformList { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.interactivity.base_style + } +} + +impl Element for UniformList { + type RequestLayoutState = UniformListFrameState; + type PrepaintState = Option; + + fn id(&self) -> Option { + self.interactivity.element_id.clone() + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let max_items = self.item_count; + let item_size = self.measure_item(None, window, cx); + let layout_id = self.interactivity.request_layout( + global_id, + inspector_id, + window, + cx, + |style, window, cx| match self.sizing_behavior { + ListSizingBehavior::Infer => { + window.with_text_style(style.text_style().cloned(), |window| { + window.request_measured_layout( + style, + move |known_dimensions, available_space, _window, _cx| { + let desired_height = item_size.height * max_items; + let width = known_dimensions.width.unwrap_or(match available_space + .width + { + AvailableSpace::Definite(x) => x, + AvailableSpace::MinContent | AvailableSpace::MaxContent => { + item_size.width + } + }); + let height = match available_space.height { + AvailableSpace::Definite(height) => desired_height.min(height), + AvailableSpace::MinContent | AvailableSpace::MaxContent => { + desired_height + } + }; + size(width, height) + }, + ) + }) + } + ListSizingBehavior::Auto => window + .with_text_style(style.text_style().cloned(), |window| { + window.request_layout(style, None, cx) + }), + }, + ); + + ( + layout_id, + UniformListFrameState { + items: SmallVec::new(), + decorations: SmallVec::new(), + }, + ) + } + + fn prepaint( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + frame_state: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Option { + let style = self + .interactivity + .compute_style(global_id, None, window, cx); + let border = style.border_widths.to_pixels(window.rem_size()); + let padding = style + .padding + .to_pixels(bounds.size.into(), window.rem_size()); + + let padded_bounds = Bounds::from_corners( + bounds.origin + point(border.left + padding.left, border.top + padding.top), + bounds.bottom_right() + - point(border.right + padding.right, border.bottom + padding.bottom), + ); + + let can_scroll_horizontally = matches!( + self.horizontal_sizing_behavior, + ListHorizontalSizingBehavior::Unconstrained + ); + + let longest_item_size = self.measure_item(None, window, cx); + let content_width = if can_scroll_horizontally { + padded_bounds.size.width.max(longest_item_size.width) + } else { + padded_bounds.size.width + }; + let content_size = Size { + width: content_width, + height: longest_item_size.height * self.item_count + padding.top + padding.bottom, + }; + + let shared_scroll_offset = self.interactivity.scroll_offset.clone().unwrap(); + let item_height = longest_item_size.height; + let shared_scroll_to_item = self.scroll_handle.as_mut().and_then(|handle| { + let mut handle = handle.0.borrow_mut(); + handle.last_item_size = Some(ItemSize { + item: padded_bounds.size, + contents: content_size, + }); + handle.deferred_scroll_to_item.take() + }); + + self.interactivity.prepaint( + global_id, + inspector_id, + bounds, + content_size, + window, + cx, + |_style, mut scroll_offset, hitbox, window, cx| { + let y_flipped = if let Some(scroll_handle) = &self.scroll_handle { + let scroll_state = scroll_handle.0.borrow(); + scroll_state.y_flipped + } else { + false + }; + + if self.item_count > 0 { + let content_height = + item_height * self.item_count + padding.top + padding.bottom; + let is_scrolled_vertically = !scroll_offset.y.is_zero(); + let min_vertical_scroll_offset = padded_bounds.size.height - content_height; + if is_scrolled_vertically && scroll_offset.y < min_vertical_scroll_offset { + shared_scroll_offset.borrow_mut().y = min_vertical_scroll_offset; + scroll_offset.y = min_vertical_scroll_offset; + } + + let content_width = content_size.width + padding.left + padding.right; + let is_scrolled_horizontally = + can_scroll_horizontally && !scroll_offset.x.is_zero(); + if is_scrolled_horizontally && content_width <= padded_bounds.size.width { + shared_scroll_offset.borrow_mut().x = Pixels::ZERO; + scroll_offset.x = Pixels::ZERO; + } + + if let Some(deferred_scroll) = shared_scroll_to_item { + let mut ix = deferred_scroll.item_index; + if y_flipped { + ix = self.item_count.saturating_sub(ix + 1); + } + let list_height = padded_bounds.size.height; + let mut updated_scroll_offset = shared_scroll_offset.borrow_mut(); + let item_top = item_height * ix + padding.top; + let item_bottom = item_top + item_height; + let scroll_top = -updated_scroll_offset.y; + let offset_pixels = item_height * deferred_scroll.offset; + let mut scrolled_to_top = false; + + if item_top < scroll_top + padding.top + offset_pixels { + scrolled_to_top = true; + updated_scroll_offset.y = -(item_top) + padding.top + offset_pixels; + } else if item_bottom > scroll_top + list_height - padding.bottom { + scrolled_to_top = true; + updated_scroll_offset.y = -(item_bottom - list_height) - padding.bottom; + } + + if deferred_scroll.scroll_strict + || (scrolled_to_top + && (item_top < scroll_top + offset_pixels + || item_bottom > scroll_top + list_height)) + { + match deferred_scroll.strategy { + ScrollStrategy::Top => { + updated_scroll_offset.y = -(item_top - offset_pixels) + .max(Pixels::ZERO) + .min(content_height - list_height) + .max(Pixels::ZERO); + } + ScrollStrategy::Center => { + let item_center = item_top + item_height / 2.0; + + let viewport_height = list_height - offset_pixels; + let viewport_center = offset_pixels + viewport_height / 2.0; + let target_scroll_top = item_center - viewport_center; + + updated_scroll_offset.y = -target_scroll_top + .max(Pixels::ZERO) + .min(content_height - list_height) + .max(Pixels::ZERO); + } + ScrollStrategy::Bottom => { + updated_scroll_offset.y = -(item_bottom - list_height + + offset_pixels) + .max(Pixels::ZERO) + .min(content_height - list_height) + .max(Pixels::ZERO); + } + } + } + scroll_offset = *updated_scroll_offset + } + + let first_visible_element_ix = + (-(scroll_offset.y + padding.top) / item_height).floor() as usize; + let last_visible_element_ix = ((-scroll_offset.y + padded_bounds.size.height) + / item_height) + .ceil() as usize; + + let visible_range = first_visible_element_ix + ..cmp::min(last_visible_element_ix, self.item_count); + + let items = if y_flipped { + let flipped_range = self.item_count.saturating_sub(visible_range.end) + ..self.item_count.saturating_sub(visible_range.start); + let mut items = (self.render_items)(flipped_range, window, cx); + items.reverse(); + items + } else { + (self.render_items)(visible_range.clone(), window, cx) + }; + + let content_mask = ContentMask { bounds }; + window.with_content_mask(Some(content_mask), |window| { + for (mut item, ix) in items.into_iter().zip(visible_range.clone()) { + let item_origin = padded_bounds.origin + + point( + if can_scroll_horizontally { + scroll_offset.x + padding.left + } else { + scroll_offset.x + }, + item_height * ix + scroll_offset.y + padding.top, + ); + let available_width = if can_scroll_horizontally { + padded_bounds.size.width + scroll_offset.x.abs() + } else { + padded_bounds.size.width + }; + let available_space = size( + AvailableSpace::Definite(available_width), + AvailableSpace::Definite(item_height), + ); + item.layout_as_root(available_space, window, cx); + item.prepaint_at(item_origin, window, cx); + frame_state.items.push(item); + } + + let bounds = Bounds::new( + padded_bounds.origin + + point( + if can_scroll_horizontally { + scroll_offset.x + padding.left + } else { + scroll_offset.x + }, + scroll_offset.y + padding.top, + ), + padded_bounds.size, + ); + for decoration in &self.decorations { + let mut decoration = decoration.as_ref().compute( + visible_range.clone(), + bounds, + scroll_offset, + item_height, + self.item_count, + window, + cx, + ); + let available_space = size( + AvailableSpace::Definite(bounds.size.width), + AvailableSpace::Definite(bounds.size.height), + ); + decoration.layout_as_root(available_space, window, cx); + decoration.prepaint_at(bounds.origin, window, cx); + frame_state.decorations.push(decoration); + } + }); + } + + hitbox + }, + ) + } + + fn paint( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + request_layout: &mut Self::RequestLayoutState, + hitbox: &mut Option, + window: &mut Window, + cx: &mut App, + ) { + self.interactivity.paint( + global_id, + inspector_id, + bounds, + hitbox.as_ref(), + window, + cx, + |_, window, cx| { + for item in &mut request_layout.items { + item.paint(window, cx); + } + for decoration in &mut request_layout.decorations { + decoration.paint(window, cx); + } + }, + ) + } +} + +impl IntoElement for UniformList { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +/// A decoration for a [`UniformList`]. This can be used for various things, +/// such as rendering indent guides, or other visual effects. +pub trait UniformListDecoration { + /// Compute the decoration element, given the visible range of list items, + /// the bounds of the list, and the height of each item. + fn compute( + &self, + visible_range: Range, + bounds: Bounds, + scroll_offset: Point, + item_height: Pixels, + item_count: usize, + window: &mut Window, + cx: &mut App, + ) -> AnyElement; +} + +impl UniformListDecoration for Entity { + fn compute( + &self, + visible_range: Range, + bounds: Bounds, + scroll_offset: Point, + item_height: Pixels, + item_count: usize, + window: &mut Window, + cx: &mut App, + ) -> AnyElement { + self.update(cx, |inner, cx| { + inner.compute( + visible_range, + bounds, + scroll_offset, + item_height, + item_count, + window, + cx, + ) + }) + } +} + +impl UniformList { + /// Selects a specific list item for measurement. + pub fn with_width_from_item(mut self, item_index: Option) -> Self { + self.item_to_measure_index = item_index.unwrap_or(0); + self + } + + /// Sets the sizing behavior, similar to the `List` element. + pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self { + self.sizing_behavior = behavior; + self + } + + /// Sets the horizontal sizing behavior, controlling the way list items laid out horizontally. + /// With [`ListHorizontalSizingBehavior::Unconstrained`] behavior, every item and the list itself will + /// have the size of the widest item and lay out pushing the `end_slot` to the right end. + pub fn with_horizontal_sizing_behavior( + mut self, + behavior: ListHorizontalSizingBehavior, + ) -> Self { + self.horizontal_sizing_behavior = behavior; + match behavior { + ListHorizontalSizingBehavior::FitList => { + self.interactivity.base_style.overflow.x = None; + } + ListHorizontalSizingBehavior::Unconstrained => { + self.interactivity.base_style.overflow.x = Some(Overflow::Scroll); + } + } + self + } + + /// Adds a decoration element to the list. + pub fn with_decoration(mut self, decoration: impl UniformListDecoration + 'static) -> Self { + self.decorations.push(Box::new(decoration)); + self + } + + fn measure_item( + &self, + list_width: Option, + window: &mut Window, + cx: &mut App, + ) -> Size { + if self.item_count == 0 { + return Size::default(); + } + + let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1); + let mut items = (self.render_items)(item_ix..item_ix + 1, window, cx); + let Some(mut item_to_measure) = items.pop() else { + return Size::default(); + }; + let available_space = size( + list_width.map_or(AvailableSpace::MinContent, |width| { + AvailableSpace::Definite(width) + }), + AvailableSpace::MinContent, + ); + item_to_measure.layout_as_root(available_space, window, cx) + } + + /// Track and render scroll state of this list with reference to the given scroll handle. + pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self { + self.interactivity.tracked_scroll_handle = Some(handle.0.borrow().base_handle.clone()); + self.scroll_handle = Some(handle); + self + } + + /// Sets whether the list is flipped vertically, such that item 0 appears at the bottom. + pub fn y_flipped(mut self, y_flipped: bool) -> Self { + if let Some(ref scroll_handle) = self.scroll_handle { + let mut scroll_state = scroll_handle.0.borrow_mut(); + let mut base_handle = &scroll_state.base_handle; + let offset = base_handle.offset(); + match scroll_state.last_item_size { + Some(last_size) if scroll_state.y_flipped != y_flipped => { + let new_y_offset = + -(offset.y + last_size.contents.height - last_size.item.height); + base_handle.set_offset(point(offset.x, new_y_offset)); + scroll_state.y_flipped = y_flipped; + } + // Handle case where list is initially flipped. + None if y_flipped => { + base_handle.set_offset(point(offset.x, Pixels::MIN)); + scroll_state.y_flipped = y_flipped; + } + _ => {} + } + } + self + } +} + +impl InteractiveElement for UniformList { + fn interactivity(&mut self) -> &mut crate::Interactivity { + &mut self.interactivity + } +} diff --git a/third_party/gpui/src/executor.rs b/third_party/gpui/src/executor.rs new file mode 100644 index 0000000..ab5cafc --- /dev/null +++ b/third_party/gpui/src/executor.rs @@ -0,0 +1,611 @@ +use crate::{App, PlatformDispatcher}; +use async_task::Runnable; +use futures::channel::mpsc; +use smol::prelude::*; +use std::mem::ManuallyDrop; +use std::panic::Location; +use std::thread::{self, ThreadId}; +use std::{ + fmt::Debug, + marker::PhantomData, + mem, + num::NonZeroUsize, + pin::Pin, + rc::Rc, + sync::{ + Arc, + atomic::{AtomicUsize, Ordering::SeqCst}, + }, + task::{Context, Poll}, + time::{Duration, Instant}, +}; +use util::TryFutureExt; +use waker_fn::waker_fn; + +#[cfg(any(test, feature = "test-support"))] +use rand::rngs::StdRng; + +/// A pointer to the executor that is currently running, +/// for spawning background tasks. +#[derive(Clone)] +pub struct BackgroundExecutor { + #[doc(hidden)] + pub dispatcher: Arc, +} + +/// A pointer to the executor that is currently running, +/// for spawning tasks on the main thread. +/// +/// This is intentionally `!Send` via the `not_send` marker field. This is because +/// `ForegroundExecutor::spawn` does not require `Send` but checks at runtime that the future is +/// only polled from the same thread it was spawned from. These checks would fail when spawning +/// foreground tasks from from background threads. +#[derive(Clone)] +pub struct ForegroundExecutor { + #[doc(hidden)] + pub dispatcher: Arc, + not_send: PhantomData>, +} + +/// Task is a primitive that allows work to happen in the background. +/// +/// It implements [`Future`] so you can `.await` on it. +/// +/// If you drop a task it will be cancelled immediately. Calling [`Task::detach`] allows +/// the task to continue running, but with no way to return a value. +#[must_use] +#[derive(Debug)] +pub struct Task(TaskState); + +#[derive(Debug)] +enum TaskState { + /// A task that is ready to return a value + Ready(Option), + + /// A task that is currently running. + Spawned(async_task::Task), +} + +impl Task { + /// Creates a new task that will resolve with the value + pub fn ready(val: T) -> Self { + Task(TaskState::Ready(Some(val))) + } + + /// Detaching a task runs it to completion in the background + pub fn detach(self) { + match self { + Task(TaskState::Ready(_)) => {} + Task(TaskState::Spawned(task)) => task.detach(), + } + } +} + +impl Task> +where + T: 'static, + E: 'static + Debug, +{ + /// Run the task to completion in the background and log any + /// errors that occur. + #[track_caller] + pub fn detach_and_log_err(self, cx: &App) { + let location = core::panic::Location::caller(); + cx.foreground_executor() + .spawn(self.log_tracked_err(*location)) + .detach(); + } +} + +impl Future for Task { + type Output = T; + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + match unsafe { self.get_unchecked_mut() } { + Task(TaskState::Ready(val)) => Poll::Ready(val.take().unwrap()), + Task(TaskState::Spawned(task)) => task.poll(cx), + } + } +} + +/// A task label is an opaque identifier that you can use to +/// refer to a task in tests. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub struct TaskLabel(NonZeroUsize); + +impl Default for TaskLabel { + fn default() -> Self { + Self::new() + } +} + +impl TaskLabel { + /// Construct a new task label. + pub fn new() -> Self { + static NEXT_TASK_LABEL: AtomicUsize = AtomicUsize::new(1); + Self(NEXT_TASK_LABEL.fetch_add(1, SeqCst).try_into().unwrap()) + } +} + +type AnyLocalFuture = Pin>>; + +type AnyFuture = Pin>>; + +/// BackgroundExecutor lets you run things on background threads. +/// In production this is a thread pool with no ordering guarantees. +/// In tests this is simulated by running tasks one by one in a deterministic +/// (but arbitrary) order controlled by the `SEED` environment variable. +impl BackgroundExecutor { + #[doc(hidden)] + pub fn new(dispatcher: Arc) -> Self { + Self { dispatcher } + } + + /// Enqueues the given future to be run to completion on a background thread. + pub fn spawn(&self, future: impl Future + Send + 'static) -> Task + where + R: Send + 'static, + { + self.spawn_internal::(Box::pin(future), None) + } + + /// Enqueues the given future to be run to completion on a background thread. + /// The given label can be used to control the priority of the task in tests. + pub fn spawn_labeled( + &self, + label: TaskLabel, + future: impl Future + Send + 'static, + ) -> Task + where + R: Send + 'static, + { + self.spawn_internal::(Box::pin(future), Some(label)) + } + + fn spawn_internal( + &self, + future: AnyFuture, + label: Option, + ) -> Task { + let dispatcher = self.dispatcher.clone(); + let (runnable, task) = + async_task::spawn(future, move |runnable| dispatcher.dispatch(runnable, label)); + runnable.schedule(); + Task(TaskState::Spawned(task)) + } + + /// Used by the test harness to run an async test in a synchronous fashion. + #[cfg(any(test, feature = "test-support"))] + #[track_caller] + pub fn block_test(&self, future: impl Future) -> R { + if let Ok(value) = self.block_internal(false, future, None) { + value + } else { + unreachable!() + } + } + + /// Block the current thread until the given future resolves. + /// Consider using `block_with_timeout` instead. + pub fn block(&self, future: impl Future) -> R { + if let Ok(value) = self.block_internal(true, future, None) { + value + } else { + unreachable!() + } + } + + #[cfg(not(any(test, feature = "test-support")))] + pub(crate) fn block_internal( + &self, + _background_only: bool, + future: Fut, + timeout: Option, + ) -> Result + use> { + use std::time::Instant; + + let mut future = Box::pin(future); + if timeout == Some(Duration::ZERO) { + return Err(future); + } + let deadline = timeout.map(|timeout| Instant::now() + timeout); + + let parker = parking::Parker::new(); + let unparker = parker.unparker(); + let waker = waker_fn(move || { + unparker.unpark(); + }); + let mut cx = std::task::Context::from_waker(&waker); + + loop { + match future.as_mut().poll(&mut cx) { + Poll::Ready(result) => return Ok(result), + Poll::Pending => { + let timeout = + deadline.map(|deadline| deadline.saturating_duration_since(Instant::now())); + if let Some(timeout) = timeout { + if !parker.park_timeout(timeout) + && deadline.is_some_and(|deadline| deadline < Instant::now()) + { + return Err(future); + } + } else { + parker.park(); + } + } + } + } + } + + #[cfg(any(test, feature = "test-support"))] + #[track_caller] + pub(crate) fn block_internal( + &self, + background_only: bool, + future: Fut, + timeout: Option, + ) -> Result + use> { + use std::sync::atomic::AtomicBool; + + use parking::Parker; + + let mut future = Box::pin(future); + if timeout == Some(Duration::ZERO) { + return Err(future); + } + let Some(dispatcher) = self.dispatcher.as_test() else { + return Err(future); + }; + + let mut max_ticks = if timeout.is_some() { + dispatcher.gen_block_on_ticks() + } else { + usize::MAX + }; + + let parker = Parker::new(); + let unparker = parker.unparker(); + + let awoken = Arc::new(AtomicBool::new(false)); + let waker = waker_fn({ + let awoken = awoken.clone(); + let unparker = unparker.clone(); + move || { + awoken.store(true, SeqCst); + unparker.unpark(); + } + }); + let mut cx = std::task::Context::from_waker(&waker); + + loop { + match future.as_mut().poll(&mut cx) { + Poll::Ready(result) => return Ok(result), + Poll::Pending => { + if max_ticks == 0 { + return Err(future); + } + max_ticks -= 1; + + if !dispatcher.tick(background_only) { + if awoken.swap(false, SeqCst) { + continue; + } + + if !dispatcher.parking_allowed() { + if dispatcher.advance_clock_to_next_delayed() { + continue; + } + let mut backtrace_message = String::new(); + let mut waiting_message = String::new(); + if let Some(backtrace) = dispatcher.waiting_backtrace() { + backtrace_message = + format!("\nbacktrace of waiting future:\n{:?}", backtrace); + } + if let Some(waiting_hint) = dispatcher.waiting_hint() { + waiting_message = format!("\n waiting on: {}\n", waiting_hint); + } + panic!( + "parked with nothing left to run{waiting_message}{backtrace_message}", + ) + } + dispatcher.set_unparker(unparker.clone()); + parker.park(); + } + } + } + } + } + + /// Block the current thread until the given future resolves + /// or `duration` has elapsed. + pub fn block_with_timeout( + &self, + duration: Duration, + future: Fut, + ) -> Result + use> { + self.block_internal(true, future, Some(duration)) + } + + /// Scoped lets you start a number of tasks and waits + /// for all of them to complete before returning. + pub async fn scoped<'scope, F>(&self, scheduler: F) + where + F: FnOnce(&mut Scope<'scope>), + { + let mut scope = Scope::new(self.clone()); + (scheduler)(&mut scope); + let spawned = mem::take(&mut scope.futures) + .into_iter() + .map(|f| self.spawn(f)) + .collect::>(); + for task in spawned { + task.await; + } + } + + /// Get the current time. + /// + /// Calling this instead of `std::time::Instant::now` allows the use + /// of fake timers in tests. + pub fn now(&self) -> Instant { + self.dispatcher.now() + } + + /// Returns a task that will complete after the given duration. + /// Depending on other concurrent tasks the elapsed duration may be longer + /// than requested. + pub fn timer(&self, duration: Duration) -> Task<()> { + if duration.is_zero() { + return Task::ready(()); + } + let (runnable, task) = async_task::spawn(async move {}, { + let dispatcher = self.dispatcher.clone(); + move |runnable| dispatcher.dispatch_after(duration, runnable) + }); + runnable.schedule(); + Task(TaskState::Spawned(task)) + } + + /// in tests, start_waiting lets you indicate which task is waiting (for debugging only) + #[cfg(any(test, feature = "test-support"))] + pub fn start_waiting(&self) { + self.dispatcher.as_test().unwrap().start_waiting(); + } + + /// in tests, removes the debugging data added by start_waiting + #[cfg(any(test, feature = "test-support"))] + pub fn finish_waiting(&self) { + self.dispatcher.as_test().unwrap().finish_waiting(); + } + + /// in tests, run an arbitrary number of tasks (determined by the SEED environment variable) + #[cfg(any(test, feature = "test-support"))] + pub fn simulate_random_delay(&self) -> impl Future + use<> { + self.dispatcher.as_test().unwrap().simulate_random_delay() + } + + /// in tests, indicate that a given task from `spawn_labeled` should run after everything else + #[cfg(any(test, feature = "test-support"))] + pub fn deprioritize(&self, task_label: TaskLabel) { + self.dispatcher.as_test().unwrap().deprioritize(task_label) + } + + /// in tests, move time forward. This does not run any tasks, but does make `timer`s ready. + #[cfg(any(test, feature = "test-support"))] + pub fn advance_clock(&self, duration: Duration) { + self.dispatcher.as_test().unwrap().advance_clock(duration) + } + + /// in tests, run one task. + #[cfg(any(test, feature = "test-support"))] + pub fn tick(&self) -> bool { + self.dispatcher.as_test().unwrap().tick(false) + } + + /// in tests, run all tasks that are ready to run. If after doing so + /// the test still has outstanding tasks, this will panic. (See also [`Self::allow_parking`]) + #[cfg(any(test, feature = "test-support"))] + pub fn run_until_parked(&self) { + self.dispatcher.as_test().unwrap().run_until_parked() + } + + /// in tests, prevents `run_until_parked` from panicking if there are outstanding tasks. + /// This is useful when you are integrating other (non-GPUI) futures, like disk access, that + /// do take real async time to run. + #[cfg(any(test, feature = "test-support"))] + pub fn allow_parking(&self) { + self.dispatcher.as_test().unwrap().allow_parking(); + } + + /// undoes the effect of [`Self::allow_parking`]. + #[cfg(any(test, feature = "test-support"))] + pub fn forbid_parking(&self) { + self.dispatcher.as_test().unwrap().forbid_parking(); + } + + /// adds detail to the "parked with nothing let to run" message. + #[cfg(any(test, feature = "test-support"))] + pub fn set_waiting_hint(&self, msg: Option) { + self.dispatcher.as_test().unwrap().set_waiting_hint(msg); + } + + /// in tests, returns the rng used by the dispatcher and seeded by the `SEED` environment variable + #[cfg(any(test, feature = "test-support"))] + pub fn rng(&self) -> StdRng { + self.dispatcher.as_test().unwrap().rng() + } + + /// How many CPUs are available to the dispatcher. + pub fn num_cpus(&self) -> usize { + #[cfg(any(test, feature = "test-support"))] + return 4; + + #[cfg(not(any(test, feature = "test-support")))] + return num_cpus::get(); + } + + /// Whether we're on the main thread. + pub fn is_main_thread(&self) -> bool { + self.dispatcher.is_main_thread() + } + + #[cfg(any(test, feature = "test-support"))] + /// in tests, control the number of ticks that `block_with_timeout` will run before timing out. + pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive) { + self.dispatcher.as_test().unwrap().set_block_on_ticks(range); + } +} + +/// ForegroundExecutor runs things on the main thread. +impl ForegroundExecutor { + /// Creates a new ForegroundExecutor from the given PlatformDispatcher. + pub fn new(dispatcher: Arc) -> Self { + Self { + dispatcher, + not_send: PhantomData, + } + } + + /// Enqueues the given Task to run on the main thread at some point in the future. + #[track_caller] + pub fn spawn(&self, future: impl Future + 'static) -> Task + where + R: 'static, + { + let dispatcher = self.dispatcher.clone(); + + #[track_caller] + fn inner( + dispatcher: Arc, + future: AnyLocalFuture, + ) -> Task { + let (runnable, task) = spawn_local_with_source_location(future, move |runnable| { + dispatcher.dispatch_on_main_thread(runnable) + }); + runnable.schedule(); + Task(TaskState::Spawned(task)) + } + inner::(dispatcher, Box::pin(future)) + } +} + +/// Variant of `async_task::spawn_local` that includes the source location of the spawn in panics. +/// +/// Copy-modified from: +/// +#[track_caller] +fn spawn_local_with_source_location( + future: Fut, + schedule: S, +) -> (Runnable<()>, async_task::Task) +where + Fut: Future + 'static, + Fut::Output: 'static, + S: async_task::Schedule<()> + Send + Sync + 'static, +{ + #[inline] + fn thread_id() -> ThreadId { + std::thread_local! { + static ID: ThreadId = thread::current().id(); + } + ID.try_with(|id| *id) + .unwrap_or_else(|_| thread::current().id()) + } + + struct Checked { + id: ThreadId, + inner: ManuallyDrop, + location: &'static Location<'static>, + } + + impl Drop for Checked { + fn drop(&mut self) { + assert!( + self.id == thread_id(), + "local task dropped by a thread that didn't spawn it. Task spawned at {}", + self.location + ); + unsafe { ManuallyDrop::drop(&mut self.inner) }; + } + } + + impl Future for Checked { + type Output = F::Output; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + assert!( + self.id == thread_id(), + "local task polled by a thread that didn't spawn it. Task spawned at {}", + self.location + ); + unsafe { self.map_unchecked_mut(|c| &mut *c.inner).poll(cx) } + } + } + + // Wrap the future into one that checks which thread it's on. + let future = Checked { + id: thread_id(), + inner: ManuallyDrop::new(future), + location: Location::caller(), + }; + + unsafe { async_task::spawn_unchecked(future, schedule) } +} + +/// Scope manages a set of tasks that are enqueued and waited on together. See [`BackgroundExecutor::scoped`]. +pub struct Scope<'a> { + executor: BackgroundExecutor, + futures: Vec + Send + 'static>>>, + tx: Option>, + rx: mpsc::Receiver<()>, + lifetime: PhantomData<&'a ()>, +} + +impl<'a> Scope<'a> { + fn new(executor: BackgroundExecutor) -> Self { + let (tx, rx) = mpsc::channel(1); + Self { + executor, + tx: Some(tx), + rx, + futures: Default::default(), + lifetime: PhantomData, + } + } + + /// How many CPUs are available to the dispatcher. + pub fn num_cpus(&self) -> usize { + self.executor.num_cpus() + } + + /// Spawn a future into this scope. + pub fn spawn(&mut self, f: F) + where + F: Future + Send + 'a, + { + let tx = self.tx.clone().unwrap(); + + // SAFETY: The 'a lifetime is guaranteed to outlive any of these futures because + // dropping this `Scope` blocks until all of the futures have resolved. + let f = unsafe { + mem::transmute::< + Pin + Send + 'a>>, + Pin + Send + 'static>>, + >(Box::pin(async move { + f.await; + drop(tx); + })) + }; + self.futures.push(f); + } +} + +impl Drop for Scope<'_> { + fn drop(&mut self) { + self.tx.take().unwrap(); + + // Wait until the channel is closed, which means that all of the spawned + // futures have resolved. + self.executor.block(self.rx.next()); + } +} diff --git a/third_party/gpui/src/geometry.rs b/third_party/gpui/src/geometry.rs new file mode 100644 index 0000000..fa6f90b --- /dev/null +++ b/third_party/gpui/src/geometry.rs @@ -0,0 +1,3912 @@ +//! The GPUI geometry module is a collection of types and traits that +//! can be used to describe common units, concepts, and the relationships +//! between them. + +use anyhow::{Context as _, anyhow}; +use core::fmt::Debug; +use derive_more::{Add, AddAssign, Div, DivAssign, Mul, Neg, Sub, SubAssign}; +use refineable::Refineable; +use schemars::{JsonSchema, json_schema}; +use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; +use std::borrow::Cow; +use std::ops::Range; +use std::{ + cmp::{self, PartialOrd}, + fmt::{self, Display}, + hash::Hash, + ops::{Add, Div, Mul, MulAssign, Neg, Sub}, +}; +use taffy::prelude::{TaffyGridLine, TaffyGridSpan}; + +use crate::{App, DisplayId}; + +/// Axis in a 2D cartesian space. +#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] +pub enum Axis { + /// The y axis, or up and down + Vertical, + /// The x axis, or left and right + Horizontal, +} + +impl Axis { + /// Swap this axis to the opposite axis. + pub fn invert(self) -> Self { + match self { + Axis::Vertical => Axis::Horizontal, + Axis::Horizontal => Axis::Vertical, + } + } +} + +/// A trait for accessing the given unit along a certain axis. +pub trait Along { + /// The unit associated with this type + type Unit; + + /// Returns the unit along the given axis. + fn along(&self, axis: Axis) -> Self::Unit; + + /// Applies the given function to the unit along the given axis and returns a new value. + fn apply_along(&self, axis: Axis, f: impl FnOnce(Self::Unit) -> Self::Unit) -> Self; +} + +/// Describes a location in a 2D cartesian space. +/// +/// It holds two public fields, `x` and `y`, which represent the coordinates in the space. +/// The type `T` for the coordinates can be any type that implements `Default`, `Clone`, and `Debug`. +/// +/// # Examples +/// +/// ``` +/// # use gpui::Point; +/// let point = Point { x: 10, y: 20 }; +/// println!("{:?}", point); // Outputs: Point { x: 10, y: 20 } +/// ``` +#[derive( + Refineable, + Default, + Add, + AddAssign, + Sub, + SubAssign, + Copy, + Debug, + PartialEq, + Eq, + Serialize, + Deserialize, + JsonSchema, + Hash, +)] +#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +#[repr(C)] +pub struct Point { + /// The x coordinate of the point. + pub x: T, + /// The y coordinate of the point. + pub y: T, +} + +/// Constructs a new `Point` with the given x and y coordinates. +/// +/// # Arguments +/// +/// * `x` - The x coordinate of the point. +/// * `y` - The y coordinate of the point. +/// +/// # Returns +/// +/// Returns a `Point` with the specified coordinates. +/// +/// # Examples +/// +/// ``` +/// use gpui::point; +/// let p = point(10, 20); +/// assert_eq!(p.x, 10); +/// assert_eq!(p.y, 20); +/// ``` +pub const fn point(x: T, y: T) -> Point { + Point { x, y } +} + +impl Point { + /// Creates a new `Point` with the specified `x` and `y` coordinates. + /// + /// # Arguments + /// + /// * `x` - The horizontal coordinate of the point. + /// * `y` - The vertical coordinate of the point. + /// + /// # Examples + /// + /// ``` + /// use gpui::Point; + /// let p = Point::new(10, 20); + /// assert_eq!(p.x, 10); + /// assert_eq!(p.y, 20); + /// ``` + pub const fn new(x: T, y: T) -> Self { + Self { x, y } + } + + /// Transforms the point to a `Point` by applying the given function to both coordinates. + /// + /// This method allows for converting a `Point` to a `Point` by specifying a closure + /// that defines how to convert between the two types. The closure is applied to both the `x` + /// and `y` coordinates, resulting in a new point of the desired type. + /// + /// # Arguments + /// + /// * `f` - A closure that takes a value of type `T` and returns a value of type `U`. + /// + /// # Examples + /// + /// ``` + /// # use gpui::Point; + /// let p = Point { x: 3, y: 4 }; + /// let p_float = p.map(|coord| coord as f32); + /// assert_eq!(p_float, Point { x: 3.0, y: 4.0 }); + /// ``` + #[must_use] + pub fn map(&self, f: impl Fn(T) -> U) -> Point { + Point { + x: f(self.x.clone()), + y: f(self.y.clone()), + } + } +} + +impl Along for Point { + type Unit = T; + + fn along(&self, axis: Axis) -> T { + match axis { + Axis::Horizontal => self.x.clone(), + Axis::Vertical => self.y.clone(), + } + } + + fn apply_along(&self, axis: Axis, f: impl FnOnce(T) -> T) -> Point { + match axis { + Axis::Horizontal => Point { + x: f(self.x.clone()), + y: self.y.clone(), + }, + Axis::Vertical => Point { + x: self.x.clone(), + y: f(self.y.clone()), + }, + } + } +} + +impl Negate for Point { + fn negate(self) -> Self { + self.map(Negate::negate) + } +} + +impl Point { + /// Scales the point by a given factor, which is typically derived from the resolution + /// of a target display to ensure proper sizing of UI elements. + /// + /// # Arguments + /// + /// * `factor` - The scaling factor to apply to both the x and y coordinates. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Point, Pixels, ScaledPixels}; + /// let p = Point { x: Pixels::from(10.0), y: Pixels::from(20.0) }; + /// let scaled_p = p.scale(1.5); + /// assert_eq!(scaled_p, Point { x: ScaledPixels::from(15.0), y: ScaledPixels::from(30.0) }); + /// ``` + pub fn scale(&self, factor: f32) -> Point { + Point { + x: self.x.scale(factor), + y: self.y.scale(factor), + } + } + + /// Calculates the Euclidean distance from the origin (0, 0) to this point. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Pixels, Point}; + /// let p = Point { x: Pixels::from(3.0), y: Pixels::from(4.0) }; + /// assert_eq!(p.magnitude(), 5.0); + /// ``` + pub fn magnitude(&self) -> f64 { + ((self.x.0.powi(2) + self.y.0.powi(2)) as f64).sqrt() + } +} + +impl Point +where + T: Sub + Clone + Debug + Default + PartialEq, +{ + /// Get the position of this point, relative to the given origin + pub fn relative_to(&self, origin: &Point) -> Point { + point( + self.x.clone() - origin.x.clone(), + self.y.clone() - origin.y.clone(), + ) + } +} + +impl Mul for Point +where + T: Mul + Clone + Debug + Default + PartialEq, + Rhs: Clone + Debug, +{ + type Output = Point; + + fn mul(self, rhs: Rhs) -> Self::Output { + Point { + x: self.x * rhs.clone(), + y: self.y * rhs, + } + } +} + +impl MulAssign for Point +where + T: Mul + Clone + Debug + Default + PartialEq, + S: Clone, +{ + fn mul_assign(&mut self, rhs: S) { + self.x = self.x.clone() * rhs.clone(); + self.y = self.y.clone() * rhs; + } +} + +impl Div for Point +where + T: Div + Clone + Debug + Default + PartialEq, + S: Clone, +{ + type Output = Self; + + fn div(self, rhs: S) -> Self::Output { + Self { + x: self.x / rhs.clone(), + y: self.y / rhs, + } + } +} + +impl Point +where + T: PartialOrd + Clone + Debug + Default + PartialEq, +{ + /// Returns a new point with the maximum values of each dimension from `self` and `other`. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Point` to compare with `self`. + /// + /// # Examples + /// + /// ``` + /// # use gpui::Point; + /// let p1 = Point { x: 3, y: 7 }; + /// let p2 = Point { x: 5, y: 2 }; + /// let max_point = p1.max(&p2); + /// assert_eq!(max_point, Point { x: 5, y: 7 }); + /// ``` + pub fn max(&self, other: &Self) -> Self { + Point { + x: if self.x > other.x { + self.x.clone() + } else { + other.x.clone() + }, + y: if self.y > other.y { + self.y.clone() + } else { + other.y.clone() + }, + } + } + + /// Returns a new point with the minimum values of each dimension from `self` and `other`. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Point` to compare with `self`. + /// + /// # Examples + /// + /// ``` + /// # use gpui::Point; + /// let p1 = Point { x: 3, y: 7 }; + /// let p2 = Point { x: 5, y: 2 }; + /// let min_point = p1.min(&p2); + /// assert_eq!(min_point, Point { x: 3, y: 2 }); + /// ``` + pub fn min(&self, other: &Self) -> Self { + Point { + x: if self.x <= other.x { + self.x.clone() + } else { + other.x.clone() + }, + y: if self.y <= other.y { + self.y.clone() + } else { + other.y.clone() + }, + } + } + + /// Clamps the point to a specified range. + /// + /// Given a minimum point and a maximum point, this method constrains the current point + /// such that its coordinates do not exceed the range defined by the minimum and maximum points. + /// If the current point's coordinates are less than the minimum, they are set to the minimum. + /// If they are greater than the maximum, they are set to the maximum. + /// + /// # Arguments + /// + /// * `min` - A reference to a `Point` representing the minimum allowable coordinates. + /// * `max` - A reference to a `Point` representing the maximum allowable coordinates. + /// + /// # Examples + /// + /// ``` + /// # use gpui::Point; + /// let p = Point { x: 10, y: 20 }; + /// let min = Point { x: 0, y: 5 }; + /// let max = Point { x: 15, y: 25 }; + /// let clamped_p = p.clamp(&min, &max); + /// assert_eq!(clamped_p, Point { x: 10, y: 20 }); + /// + /// let p_out_of_bounds = Point { x: -5, y: 30 }; + /// let clamped_p_out_of_bounds = p_out_of_bounds.clamp(&min, &max); + /// assert_eq!(clamped_p_out_of_bounds, Point { x: 0, y: 25 }); + /// ``` + pub fn clamp(&self, min: &Self, max: &Self) -> Self { + self.max(min).min(max) + } +} + +impl Clone for Point { + fn clone(&self) -> Self { + Self { + x: self.x.clone(), + y: self.y.clone(), + } + } +} + +impl Display for Point { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "({}, {})", self.x, self.y) + } +} + +/// A structure representing a two-dimensional size with width and height in a given unit. +/// +/// This struct is generic over the type `T`, which can be any type that implements `Clone`, `Default`, and `Debug`. +/// It is commonly used to specify dimensions for elements in a UI, such as a window or element. +#[derive(Refineable, Default, Clone, Copy, PartialEq, Div, Hash, Serialize, Deserialize)] +#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +#[repr(C)] +pub struct Size { + /// The width component of the size. + pub width: T, + /// The height component of the size. + pub height: T, +} + +impl Size { + /// Create a new Size, a synonym for [`size`] + pub fn new(width: T, height: T) -> Self { + size(width, height) + } +} + +/// Constructs a new `Size` with the provided width and height. +/// +/// # Arguments +/// +/// * `width` - The width component of the `Size`. +/// * `height` - The height component of the `Size`. +/// +/// # Examples +/// +/// ``` +/// use gpui::size; +/// let my_size = size(10, 20); +/// assert_eq!(my_size.width, 10); +/// assert_eq!(my_size.height, 20); +/// ``` +pub const fn size(width: T, height: T) -> Size +where + T: Clone + Debug + Default + PartialEq, +{ + Size { width, height } +} + +impl Size +where + T: Clone + Debug + Default + PartialEq, +{ + /// Applies a function to the width and height of the size, producing a new `Size`. + /// + /// This method allows for converting a `Size` to a `Size` by specifying a closure + /// that defines how to convert between the two types. The closure is applied to both the `width` + /// and `height`, resulting in a new size of the desired type. + /// + /// # Arguments + /// + /// * `f` - A closure that takes a value of type `T` and returns a value of type `U`. + /// + /// # Examples + /// + /// ``` + /// # use gpui::Size; + /// let my_size = Size { width: 10, height: 20 }; + /// let my_new_size = my_size.map(|dimension| dimension as f32 * 1.5); + /// assert_eq!(my_new_size, Size { width: 15.0, height: 30.0 }); + /// ``` + pub fn map(&self, f: impl Fn(T) -> U) -> Size + where + U: Clone + Debug + Default + PartialEq, + { + Size { + width: f(self.width.clone()), + height: f(self.height.clone()), + } + } +} + +impl Size +where + T: Clone + Debug + Default + PartialEq + Half, +{ + /// Compute the center point of the size.g + pub fn center(&self) -> Point { + Point { + x: self.width.half(), + y: self.height.half(), + } + } +} + +impl Size { + /// Scales the size by a given factor. + /// + /// This method multiplies both the width and height by the provided scaling factor, + /// resulting in a new `Size` that is proportionally larger or smaller + /// depending on the factor. + /// + /// # Arguments + /// + /// * `factor` - The scaling factor to apply to the width and height. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Size, Pixels, ScaledPixels}; + /// let size = Size { width: Pixels::from(100.0), height: Pixels::from(50.0) }; + /// let scaled_size = size.scale(2.0); + /// assert_eq!(scaled_size, Size { width: ScaledPixels::from(200.0), height: ScaledPixels::from(100.0) }); + /// ``` + pub fn scale(&self, factor: f32) -> Size { + Size { + width: self.width.scale(factor), + height: self.height.scale(factor), + } + } +} + +impl Along for Size +where + T: Clone + Debug + Default + PartialEq, +{ + type Unit = T; + + fn along(&self, axis: Axis) -> T { + match axis { + Axis::Horizontal => self.width.clone(), + Axis::Vertical => self.height.clone(), + } + } + + /// Returns the value of this size along the given axis. + fn apply_along(&self, axis: Axis, f: impl FnOnce(T) -> T) -> Self { + match axis { + Axis::Horizontal => Size { + width: f(self.width.clone()), + height: self.height.clone(), + }, + Axis::Vertical => Size { + width: self.width.clone(), + height: f(self.height.clone()), + }, + } + } +} + +impl Size +where + T: PartialOrd + Clone + Debug + Default + PartialEq, +{ + /// Returns a new `Size` with the maximum width and height from `self` and `other`. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Size` to compare with `self`. + /// + /// # Examples + /// + /// ``` + /// # use gpui::Size; + /// let size1 = Size { width: 30, height: 40 }; + /// let size2 = Size { width: 50, height: 20 }; + /// let max_size = size1.max(&size2); + /// assert_eq!(max_size, Size { width: 50, height: 40 }); + /// ``` + pub fn max(&self, other: &Self) -> Self { + Size { + width: if self.width >= other.width { + self.width.clone() + } else { + other.width.clone() + }, + height: if self.height >= other.height { + self.height.clone() + } else { + other.height.clone() + }, + } + } + + /// Returns a new `Size` with the minimum width and height from `self` and `other`. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Size` to compare with `self`. + /// + /// # Examples + /// + /// ``` + /// # use gpui::Size; + /// let size1 = Size { width: 30, height: 40 }; + /// let size2 = Size { width: 50, height: 20 }; + /// let min_size = size1.min(&size2); + /// assert_eq!(min_size, Size { width: 30, height: 20 }); + /// ``` + pub fn min(&self, other: &Self) -> Self { + Size { + width: if self.width >= other.width { + other.width.clone() + } else { + self.width.clone() + }, + height: if self.height >= other.height { + other.height.clone() + } else { + self.height.clone() + }, + } + } +} + +impl Sub for Size +where + T: Sub + Clone + Debug + Default + PartialEq, +{ + type Output = Size; + + fn sub(self, rhs: Self) -> Self::Output { + Size { + width: self.width - rhs.width, + height: self.height - rhs.height, + } + } +} + +impl Add for Size +where + T: Add + Clone + Debug + Default + PartialEq, +{ + type Output = Size; + + fn add(self, rhs: Self) -> Self::Output { + Size { + width: self.width + rhs.width, + height: self.height + rhs.height, + } + } +} + +impl Mul for Size +where + T: Mul + Clone + Debug + Default + PartialEq, + Rhs: Clone + Debug + Default + PartialEq, +{ + type Output = Size; + + fn mul(self, rhs: Rhs) -> Self::Output { + Size { + width: self.width * rhs.clone(), + height: self.height * rhs, + } + } +} + +impl MulAssign for Size +where + T: Mul + Clone + Debug + Default + PartialEq, + S: Clone, +{ + fn mul_assign(&mut self, rhs: S) { + self.width = self.width.clone() * rhs.clone(); + self.height = self.height.clone() * rhs; + } +} + +impl Eq for Size where T: Eq + Clone + Debug + Default + PartialEq {} + +impl Debug for Size +where + T: Clone + Debug + Default + PartialEq, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Size {{ {:?} × {:?} }}", self.width, self.height) + } +} + +impl Display for Size { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} × {}", self.width, self.height) + } +} + +impl From> for Size { + fn from(point: Point) -> Self { + Self { + width: point.x, + height: point.y, + } + } +} + +impl From> for Size { + fn from(size: Size) -> Self { + Size { + width: size.width.into(), + height: size.height.into(), + } + } +} + +impl From> for Size { + fn from(size: Size) -> Self { + Size { + width: size.width.into(), + height: size.height.into(), + } + } +} + +impl Size { + /// Returns a `Size` with both width and height set to fill the available space. + /// + /// This function creates a `Size` instance where both the width and height are set to `Length::Definite(DefiniteLength::Fraction(1.0))`, + /// which represents 100% of the available space in both dimensions. + /// + /// # Returns + /// + /// A `Size` that will fill the available space when used in a layout. + pub fn full() -> Self { + Self { + width: relative(1.).into(), + height: relative(1.).into(), + } + } +} + +impl Size { + /// Returns a `Size` with both width and height set to `auto`, which allows the layout engine to determine the size. + /// + /// This function creates a `Size` instance where both the width and height are set to `Length::Auto`, + /// indicating that their size should be computed based on the layout context, such as the content size or + /// available space. + /// + /// # Returns + /// + /// A `Size` with width and height set to `Length::Auto`. + pub fn auto() -> Self { + Self { + width: Length::Auto, + height: Length::Auto, + } + } +} + +/// Represents a rectangular area in a 2D space with an origin point and a size. +/// +/// The `Bounds` struct is generic over a type `T` which represents the type of the coordinate system. +/// The origin is represented as a `Point` which defines the top left corner of the rectangle, +/// and the size is represented as a `Size` which defines the width and height of the rectangle. +/// +/// # Examples +/// +/// ``` +/// # use gpui::{Bounds, Point, Size}; +/// let origin = Point { x: 0, y: 0 }; +/// let size = Size { width: 10, height: 20 }; +/// let bounds = Bounds::new(origin, size); +/// +/// assert_eq!(bounds.origin, origin); +/// assert_eq!(bounds.size, size); +/// ``` +#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)] +#[refineable(Debug)] +#[repr(C)] +pub struct Bounds { + /// The origin point of this area. + pub origin: Point, + /// The size of the rectangle. + pub size: Size, +} + +/// Create a bounds with the given origin and size +pub fn bounds( + origin: Point, + size: Size, +) -> Bounds { + Bounds { origin, size } +} + +impl Bounds { + /// Generate a centered bounds for the given display or primary display if none is provided + pub fn centered(display_id: Option, size: Size, cx: &App) -> Self { + let display = display_id + .and_then(|id| cx.find_display(id)) + .or_else(|| cx.primary_display()); + + display + .map(|display| Bounds::centered_at(display.bounds().center(), size)) + .unwrap_or_else(|| Bounds { + origin: point(px(0.), px(0.)), + size, + }) + } + + /// Generate maximized bounds for the given display or primary display if none is provided + pub fn maximized(display_id: Option, cx: &App) -> Self { + let display = display_id + .and_then(|id| cx.find_display(id)) + .or_else(|| cx.primary_display()); + + display + .map(|display| display.bounds()) + .unwrap_or_else(|| Bounds { + origin: point(px(0.), px(0.)), + size: size(px(1024.), px(768.)), + }) + } +} + +impl Bounds +where + T: Clone + Debug + Default + PartialEq, +{ + /// Creates a new `Bounds` with the specified origin and size. + /// + /// # Arguments + /// + /// * `origin` - A `Point` representing the origin of the bounds. + /// * `size` - A `Size` representing the size of the bounds. + /// + /// # Returns + /// + /// Returns a `Bounds` that has the given origin and size. + pub fn new(origin: Point, size: Size) -> Self { + Bounds { origin, size } + } +} + +impl Bounds +where + T: Sub + Clone + Debug + Default + PartialEq, +{ + /// Constructs a `Bounds` from two corner points: the top left and bottom right corners. + /// + /// This function calculates the origin and size of the `Bounds` based on the provided corner points. + /// The origin is set to the top left corner, and the size is determined by the difference between + /// the x and y coordinates of the bottom right and top left points. + /// + /// # Arguments + /// + /// * `top_left` - A `Point` representing the top left corner of the rectangle. + /// * `bottom_right` - A `Point` representing the bottom right corner of the rectangle. + /// + /// # Returns + /// + /// Returns a `Bounds` that encompasses the area defined by the two corner points. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point}; + /// let top_left = Point { x: 0, y: 0 }; + /// let bottom_right = Point { x: 10, y: 10 }; + /// let bounds = Bounds::from_corners(top_left, bottom_right); + /// + /// assert_eq!(bounds.origin, top_left); + /// assert_eq!(bounds.size.width, 10); + /// assert_eq!(bounds.size.height, 10); + /// ``` + pub fn from_corners(top_left: Point, bottom_right: Point) -> Self { + let origin = Point { + x: top_left.x.clone(), + y: top_left.y.clone(), + }; + let size = Size { + width: bottom_right.x - top_left.x, + height: bottom_right.y - top_left.y, + }; + Bounds { origin, size } + } + + /// Constructs a `Bounds` from a corner point and size. The specified corner will be placed at + /// the specified origin. + pub fn from_corner_and_size(corner: Corner, origin: Point, size: Size) -> Bounds { + let origin = match corner { + Corner::TopLeft => origin, + Corner::TopRight => Point { + x: origin.x - size.width.clone(), + y: origin.y, + }, + Corner::BottomLeft => Point { + x: origin.x, + y: origin.y - size.height.clone(), + }, + Corner::BottomRight => Point { + x: origin.x - size.width.clone(), + y: origin.y - size.height.clone(), + }, + }; + + Bounds { origin, size } + } +} + +impl Bounds +where + T: Sub + Half + Clone + Debug + Default + PartialEq, +{ + /// Creates a new bounds centered at the given point. + pub fn centered_at(center: Point, size: Size) -> Self { + let origin = Point { + x: center.x - size.width.half(), + y: center.y - size.height.half(), + }; + Self::new(origin, size) + } +} + +impl Bounds +where + T: PartialOrd + Add + Clone + Debug + Default + PartialEq, +{ + /// Checks if this `Bounds` intersects with another `Bounds`. + /// + /// Two `Bounds` instances intersect if they overlap in the 2D space they occupy. + /// This method checks if there is any overlapping area between the two bounds. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Bounds` to check for intersection with. + /// + /// # Returns + /// + /// Returns `true` if there is any intersection between the two bounds, `false` otherwise. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size}; + /// let bounds1 = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let bounds2 = Bounds { + /// origin: Point { x: 5, y: 5 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let bounds3 = Bounds { + /// origin: Point { x: 20, y: 20 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// + /// assert_eq!(bounds1.intersects(&bounds2), true); // Overlapping bounds + /// assert_eq!(bounds1.intersects(&bounds3), false); // Non-overlapping bounds + /// ``` + pub fn intersects(&self, other: &Bounds) -> bool { + let my_lower_right = self.bottom_right(); + let their_lower_right = other.bottom_right(); + + self.origin.x < their_lower_right.x + && my_lower_right.x > other.origin.x + && self.origin.y < their_lower_right.y + && my_lower_right.y > other.origin.y + } +} + +impl Bounds +where + T: Add + Half + Clone + Debug + Default + PartialEq, +{ + /// Returns the center point of the bounds. + /// + /// Calculates the center by taking the origin's x and y coordinates and adding half the width and height + /// of the bounds, respectively. The center is represented as a `Point` where `T` is the type of the + /// coordinate system. + /// + /// # Returns + /// + /// A `Point` representing the center of the bounds. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 20 }, + /// }; + /// let center = bounds.center(); + /// assert_eq!(center, Point { x: 5, y: 10 }); + /// ``` + pub fn center(&self) -> Point { + Point { + x: self.origin.x.clone() + self.size.width.clone().half(), + y: self.origin.y.clone() + self.size.height.clone().half(), + } + } +} + +impl Bounds +where + T: Add + Clone + Debug + Default + PartialEq, +{ + /// Calculates the half perimeter of a rectangle defined by the bounds. + /// + /// The half perimeter is calculated as the sum of the width and the height of the rectangle. + /// This method is generic over the type `T` which must implement the `Sub` trait to allow + /// calculation of the width and height from the bounds' origin and size, as well as the `Add` trait + /// to sum the width and height for the half perimeter. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 20 }, + /// }; + /// let half_perimeter = bounds.half_perimeter(); + /// assert_eq!(half_perimeter, 30); + /// ``` + pub fn half_perimeter(&self) -> T { + self.size.width.clone() + self.size.height.clone() + } +} + +impl Bounds +where + T: Add + Sub + Clone + Debug + Default + PartialEq, +{ + /// Dilates the bounds by a specified amount in all directions. + /// + /// This method expands the bounds by the given `amount`, increasing the size + /// and adjusting the origin so that the bounds grow outwards equally in all directions. + /// The resulting bounds will have its width and height increased by twice the `amount` + /// (since it grows in both directions), and the origin will be moved by `-amount` + /// in both the x and y directions. + /// + /// # Arguments + /// + /// * `amount` - The amount by which to dilate the bounds. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size}; + /// let mut bounds = Bounds { + /// origin: Point { x: 10, y: 10 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let expanded_bounds = bounds.dilate(5); + /// assert_eq!(expanded_bounds, Bounds { + /// origin: Point { x: 5, y: 5 }, + /// size: Size { width: 20, height: 20 }, + /// }); + /// ``` + #[must_use] + pub fn dilate(&self, amount: T) -> Bounds { + let double_amount = amount.clone() + amount.clone(); + Bounds { + origin: self.origin.clone() - point(amount.clone(), amount), + size: self.size.clone() + size(double_amount.clone(), double_amount), + } + } + + /// Extends the bounds different amounts in each direction. + #[must_use] + pub fn extend(&self, amount: Edges) -> Bounds { + Bounds { + origin: self.origin.clone() - point(amount.left.clone(), amount.top.clone()), + size: self.size.clone() + + size( + amount.left.clone() + amount.right.clone(), + amount.top.clone() + amount.bottom, + ), + } + } +} + +impl Bounds +where + T: Add + + Sub + + Neg + + Clone + + Debug + + Default + + PartialEq, +{ + /// Inset the bounds by a specified amount. Equivalent to `dilate` with the amount negated. + /// + /// Note that this may panic if T does not support negative values. + pub fn inset(&self, amount: T) -> Self { + self.dilate(-amount) + } +} + +impl + Sub + Clone + Debug + Default + PartialEq> + Bounds +{ + /// Calculates the intersection of two `Bounds` objects. + /// + /// This method computes the overlapping region of two `Bounds`. If the bounds do not intersect, + /// the resulting `Bounds` will have a size with width and height of zero. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Bounds` to intersect with. + /// + /// # Returns + /// + /// Returns a `Bounds` representing the intersection area. If there is no intersection, + /// the returned `Bounds` will have a size with width and height of zero. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size}; + /// let bounds1 = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let bounds2 = Bounds { + /// origin: Point { x: 5, y: 5 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let intersection = bounds1.intersect(&bounds2); + /// + /// assert_eq!(intersection, Bounds { + /// origin: Point { x: 5, y: 5 }, + /// size: Size { width: 5, height: 5 }, + /// }); + /// ``` + pub fn intersect(&self, other: &Self) -> Self { + let upper_left = self.origin.max(&other.origin); + let bottom_right = self.bottom_right().min(&other.bottom_right()); + Self::from_corners(upper_left, bottom_right) + } + + /// Computes the union of two `Bounds`. + /// + /// This method calculates the smallest `Bounds` that contains both the current `Bounds` and the `other` `Bounds`. + /// The resulting `Bounds` will have an origin that is the minimum of the origins of the two `Bounds`, + /// and a size that encompasses the furthest extents of both `Bounds`. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Bounds` to create a union with. + /// + /// # Returns + /// + /// Returns a `Bounds` representing the union of the two `Bounds`. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size}; + /// let bounds1 = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let bounds2 = Bounds { + /// origin: Point { x: 5, y: 5 }, + /// size: Size { width: 15, height: 15 }, + /// }; + /// let union_bounds = bounds1.union(&bounds2); + /// + /// assert_eq!(union_bounds, Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 20, height: 20 }, + /// }); + /// ``` + pub fn union(&self, other: &Self) -> Self { + let top_left = self.origin.min(&other.origin); + let bottom_right = self.bottom_right().max(&other.bottom_right()); + Bounds::from_corners(top_left, bottom_right) + } +} + +impl Bounds +where + T: Add + Sub + Clone + Debug + Default + PartialEq, +{ + /// Computes the space available within outer bounds. + pub fn space_within(&self, outer: &Self) -> Edges { + Edges { + top: self.top() - outer.top(), + right: outer.right() - self.right(), + bottom: outer.bottom() - self.bottom(), + left: self.left() - outer.left(), + } + } +} + +impl Mul for Bounds +where + T: Mul + Clone + Debug + Default + PartialEq, + Point: Mul>, + Rhs: Clone + Debug + Default + PartialEq, +{ + type Output = Bounds; + + fn mul(self, rhs: Rhs) -> Self::Output { + Bounds { + origin: self.origin * rhs.clone(), + size: self.size * rhs, + } + } +} + +impl MulAssign for Bounds +where + T: Mul + Clone + Debug + Default + PartialEq, + S: Clone, +{ + fn mul_assign(&mut self, rhs: S) { + self.origin *= rhs.clone(); + self.size *= rhs; + } +} + +impl Div for Bounds +where + Size: Div>, + T: Div + Clone + Debug + Default + PartialEq, + S: Clone, +{ + type Output = Self; + + fn div(self, rhs: S) -> Self { + Self { + origin: self.origin / rhs.clone(), + size: self.size / rhs, + } + } +} + +impl Add> for Bounds +where + T: Add + Clone + Debug + Default + PartialEq, +{ + type Output = Self; + + fn add(self, rhs: Point) -> Self { + Self { + origin: self.origin + rhs, + size: self.size, + } + } +} + +impl Sub> for Bounds +where + T: Sub + Clone + Debug + Default + PartialEq, +{ + type Output = Self; + + fn sub(self, rhs: Point) -> Self { + Self { + origin: self.origin - rhs, + size: self.size, + } + } +} + +impl Bounds +where + T: Add + Clone + Debug + Default + PartialEq, +{ + /// Returns the top edge of the bounds. + /// + /// # Returns + /// + /// A value of type `T` representing the y-coordinate of the top edge of the bounds. + pub fn top(&self) -> T { + self.origin.y.clone() + } + + /// Returns the bottom edge of the bounds. + /// + /// # Returns + /// + /// A value of type `T` representing the y-coordinate of the bottom edge of the bounds. + pub fn bottom(&self) -> T { + self.origin.y.clone() + self.size.height.clone() + } + + /// Returns the left edge of the bounds. + /// + /// # Returns + /// + /// A value of type `T` representing the x-coordinate of the left edge of the bounds. + pub fn left(&self) -> T { + self.origin.x.clone() + } + + /// Returns the right edge of the bounds. + /// + /// # Returns + /// + /// A value of type `T` representing the x-coordinate of the right edge of the bounds. + pub fn right(&self) -> T { + self.origin.x.clone() + self.size.width.clone() + } + + /// Returns the top right corner point of the bounds. + /// + /// # Returns + /// + /// A `Point` representing the top right corner of the bounds. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 20 }, + /// }; + /// let top_right = bounds.top_right(); + /// assert_eq!(top_right, Point { x: 10, y: 0 }); + /// ``` + pub fn top_right(&self) -> Point { + Point { + x: self.origin.x.clone() + self.size.width.clone(), + y: self.origin.y.clone(), + } + } + + /// Returns the bottom right corner point of the bounds. + /// + /// # Returns + /// + /// A `Point` representing the bottom right corner of the bounds. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 20 }, + /// }; + /// let bottom_right = bounds.bottom_right(); + /// assert_eq!(bottom_right, Point { x: 10, y: 20 }); + /// ``` + pub fn bottom_right(&self) -> Point { + Point { + x: self.origin.x.clone() + self.size.width.clone(), + y: self.origin.y.clone() + self.size.height.clone(), + } + } + + /// Returns the bottom left corner point of the bounds. + /// + /// # Returns + /// + /// A `Point` representing the bottom left corner of the bounds. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 20 }, + /// }; + /// let bottom_left = bounds.bottom_left(); + /// assert_eq!(bottom_left, Point { x: 0, y: 20 }); + /// ``` + pub fn bottom_left(&self) -> Point { + Point { + x: self.origin.x.clone(), + y: self.origin.y.clone() + self.size.height.clone(), + } + } + + /// Returns the requested corner point of the bounds. + /// + /// # Returns + /// + /// A `Point` representing the corner of the bounds requested by the parameter. + /// + /// # Examples + /// + /// ``` + /// use gpui::{Bounds, Corner, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 20 }, + /// }; + /// let bottom_left = bounds.corner(Corner::BottomLeft); + /// assert_eq!(bottom_left, Point { x: 0, y: 20 }); + /// ``` + pub fn corner(&self, corner: Corner) -> Point { + match corner { + Corner::TopLeft => self.origin.clone(), + Corner::TopRight => self.top_right(), + Corner::BottomLeft => self.bottom_left(), + Corner::BottomRight => self.bottom_right(), + } + } +} + +impl Bounds +where + T: Add + PartialOrd + Clone + Debug + Default + PartialEq, +{ + /// Checks if the given point is within the bounds. + /// + /// This method determines whether a point lies inside the rectangle defined by the bounds, + /// including the edges. The point is considered inside if its x-coordinate is greater than + /// or equal to the left edge and less than or equal to the right edge, and its y-coordinate + /// is greater than or equal to the top edge and less than or equal to the bottom edge of the bounds. + /// + /// # Arguments + /// + /// * `point` - A reference to a `Point` that represents the point to check. + /// + /// # Returns + /// + /// Returns `true` if the point is within the bounds, `false` otherwise. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Point, Bounds, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let inside_point = Point { x: 5, y: 5 }; + /// let outside_point = Point { x: 15, y: 15 }; + /// + /// assert!(bounds.contains(&inside_point)); + /// assert!(!bounds.contains(&outside_point)); + /// ``` + pub fn contains(&self, point: &Point) -> bool { + point.x >= self.origin.x + && point.x <= self.origin.x.clone() + self.size.width.clone() + && point.y >= self.origin.y + && point.y <= self.origin.y.clone() + self.size.height.clone() + } + + /// Checks if this bounds is completely contained within another bounds. + /// + /// This method determines whether the current bounds is entirely enclosed by the given bounds. + /// A bounds is considered to be contained within another if its origin (top-left corner) and + /// its bottom-right corner are both contained within the other bounds. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Bounds` that might contain this bounds. + /// + /// # Returns + /// + /// Returns `true` if this bounds is completely inside the other bounds, `false` otherwise. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size}; + /// let outer_bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 20, height: 20 }, + /// }; + /// let inner_bounds = Bounds { + /// origin: Point { x: 5, y: 5 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let overlapping_bounds = Bounds { + /// origin: Point { x: 15, y: 15 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// + /// assert!(inner_bounds.is_contained_within(&outer_bounds)); + /// assert!(!overlapping_bounds.is_contained_within(&outer_bounds)); + /// ``` + pub fn is_contained_within(&self, other: &Self) -> bool { + other.contains(&self.origin) && other.contains(&self.bottom_right()) + } + + /// Applies a function to the origin and size of the bounds, producing a new `Bounds`. + /// + /// This method allows for converting a `Bounds` to a `Bounds` by specifying a closure + /// that defines how to convert between the two types. The closure is applied to the `origin` and + /// `size` fields, resulting in new bounds of the desired type. + /// + /// # Arguments + /// + /// * `f` - A closure that takes a value of type `T` and returns a value of type `U`. + /// + /// # Returns + /// + /// Returns a new `Bounds` with the origin and size mapped by the provided function. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 10.0, y: 10.0 }, + /// size: Size { width: 10.0, height: 20.0 }, + /// }; + /// let new_bounds = bounds.map(|value| value as f64 * 1.5); + /// + /// assert_eq!(new_bounds, Bounds { + /// origin: Point { x: 15.0, y: 15.0 }, + /// size: Size { width: 15.0, height: 30.0 }, + /// }); + /// ``` + pub fn map(&self, f: impl Fn(T) -> U) -> Bounds + where + U: Clone + Debug + Default + PartialEq, + { + Bounds { + origin: self.origin.map(&f), + size: self.size.map(f), + } + } + + /// Applies a function to the origin of the bounds, producing a new `Bounds` with the new origin + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 10.0, y: 10.0 }, + /// size: Size { width: 10.0, height: 20.0 }, + /// }; + /// let new_bounds = bounds.map_origin(|value| value * 1.5); + /// + /// assert_eq!(new_bounds, Bounds { + /// origin: Point { x: 15.0, y: 15.0 }, + /// size: Size { width: 10.0, height: 20.0 }, + /// }); + /// ``` + pub fn map_origin(self, f: impl Fn(T) -> T) -> Bounds { + Bounds { + origin: self.origin.map(f), + size: self.size, + } + } + + /// Applies a function to the origin of the bounds, producing a new `Bounds` with the new origin + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 10.0, y: 10.0 }, + /// size: Size { width: 10.0, height: 20.0 }, + /// }; + /// let new_bounds = bounds.map_size(|value| value * 1.5); + /// + /// assert_eq!(new_bounds, Bounds { + /// origin: Point { x: 10.0, y: 10.0 }, + /// size: Size { width: 15.0, height: 30.0 }, + /// }); + /// ``` + pub fn map_size(self, f: impl Fn(T) -> T) -> Bounds { + Bounds { + origin: self.origin, + size: self.size.map(f), + } + } +} + +impl Bounds +where + T: Add + Sub + PartialOrd + Clone + Debug + Default + PartialEq, +{ + /// Convert a point to the coordinate space defined by this Bounds + pub fn localize(&self, point: &Point) -> Option> { + self.contains(point) + .then(|| point.relative_to(&self.origin)) + } +} + +/// Checks if the bounds represent an empty area. +/// +/// # Returns +/// +/// Returns `true` if either the width or the height of the bounds is less than or equal to zero, indicating an empty area. +impl Bounds { + /// Checks if the bounds represent an empty area. + /// + /// # Returns + /// + /// Returns `true` if either the width or the height of the bounds is less than or equal to zero, indicating an empty area. + #[must_use] + pub fn is_empty(&self) -> bool { + self.size.width <= T::default() || self.size.height <= T::default() + } +} + +impl> Display for Bounds { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{} - {} (size {})", + self.origin, + self.bottom_right(), + self.size + ) + } +} + +impl Size { + /// Converts the size from physical to logical pixels. + pub(crate) fn to_pixels(self, scale_factor: f32) -> Size { + size( + px(self.width.0 as f32 / scale_factor), + px(self.height.0 as f32 / scale_factor), + ) + } +} + +impl Size { + /// Converts the size from logical to physical pixels. + pub(crate) fn to_device_pixels(self, scale_factor: f32) -> Size { + size( + DevicePixels((self.width.0 * scale_factor).round() as i32), + DevicePixels((self.height.0 * scale_factor).round() as i32), + ) + } +} + +impl Bounds { + /// Scales the bounds by a given factor, typically used to adjust for display scaling. + /// + /// This method multiplies the origin and size of the bounds by the provided scaling factor, + /// resulting in a new `Bounds` that is proportionally larger or smaller + /// depending on the scaling factor. This can be used to ensure that the bounds are properly + /// scaled for different display densities. + /// + /// # Arguments + /// + /// * `factor` - The scaling factor to apply to the origin and size, typically the display's scaling factor. + /// + /// # Returns + /// + /// Returns a new `Bounds` that represents the scaled bounds. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size, Pixels, ScaledPixels, DevicePixels}; + /// let bounds = Bounds { + /// origin: Point { x: Pixels::from(10.0), y: Pixels::from(20.0) }, + /// size: Size { width: Pixels::from(30.0), height: Pixels::from(40.0) }, + /// }; + /// let display_scale_factor = 2.0; + /// let scaled_bounds = bounds.scale(display_scale_factor); + /// assert_eq!(scaled_bounds, Bounds { + /// origin: Point { + /// x: ScaledPixels::from(20.0), + /// y: ScaledPixels::from(40.0), + /// }, + /// size: Size { + /// width: ScaledPixels::from(60.0), + /// height: ScaledPixels::from(80.0) + /// }, + /// }); + /// ``` + pub fn scale(&self, factor: f32) -> Bounds { + Bounds { + origin: self.origin.scale(factor), + size: self.size.scale(factor), + } + } + + /// Convert the bounds from logical pixels to physical pixels + pub fn to_device_pixels(self, factor: f32) -> Bounds { + Bounds { + origin: point( + DevicePixels((self.origin.x.0 * factor).round() as i32), + DevicePixels((self.origin.y.0 * factor).round() as i32), + ), + size: self.size.to_device_pixels(factor), + } + } +} + +impl Bounds { + /// Convert the bounds from physical pixels to logical pixels + pub fn to_pixels(self, scale_factor: f32) -> Bounds { + Bounds { + origin: point( + px(self.origin.x.0 as f32 / scale_factor), + px(self.origin.y.0 as f32 / scale_factor), + ), + size: self.size.to_pixels(scale_factor), + } + } +} + +impl Copy for Bounds {} + +/// Represents the edges of a box in a 2D space, such as padding or margin. +/// +/// Each field represents the size of the edge on one side of the box: `top`, `right`, `bottom`, and `left`. +/// +/// # Examples +/// +/// ``` +/// # use gpui::Edges; +/// let edges = Edges { +/// top: 10.0, +/// right: 20.0, +/// bottom: 30.0, +/// left: 40.0, +/// }; +/// +/// assert_eq!(edges.top, 10.0); +/// assert_eq!(edges.right, 20.0); +/// assert_eq!(edges.bottom, 30.0); +/// assert_eq!(edges.left, 40.0); +/// ``` +#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)] +#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +#[repr(C)] +pub struct Edges { + /// The size of the top edge. + pub top: T, + /// The size of the right edge. + pub right: T, + /// The size of the bottom edge. + pub bottom: T, + /// The size of the left edge. + pub left: T, +} + +impl Mul for Edges +where + T: Mul + Clone + Debug + Default + PartialEq, +{ + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + Self { + top: self.top.clone() * rhs.top, + right: self.right.clone() * rhs.right, + bottom: self.bottom.clone() * rhs.bottom, + left: self.left * rhs.left, + } + } +} + +impl MulAssign for Edges +where + T: Mul + Clone + Debug + Default + PartialEq, + S: Clone, +{ + fn mul_assign(&mut self, rhs: S) { + self.top = self.top.clone() * rhs.clone(); + self.right = self.right.clone() * rhs.clone(); + self.bottom = self.bottom.clone() * rhs.clone(); + self.left = self.left.clone() * rhs; + } +} + +impl Copy for Edges {} + +impl Edges { + /// Constructs `Edges` where all sides are set to the same specified value. + /// + /// This function creates an `Edges` instance with the `top`, `right`, `bottom`, and `left` fields all initialized + /// to the same value provided as an argument. This is useful when you want to have uniform edges around a box, + /// such as padding or margin with the same size on all sides. + /// + /// # Arguments + /// + /// * `value` - The value to set for all four sides of the edges. + /// + /// # Returns + /// + /// An `Edges` instance with all sides set to the given value. + /// + /// # Examples + /// + /// ``` + /// # use gpui::Edges; + /// let uniform_edges = Edges::all(10.0); + /// assert_eq!(uniform_edges.top, 10.0); + /// assert_eq!(uniform_edges.right, 10.0); + /// assert_eq!(uniform_edges.bottom, 10.0); + /// assert_eq!(uniform_edges.left, 10.0); + /// ``` + pub fn all(value: T) -> Self { + Self { + top: value.clone(), + right: value.clone(), + bottom: value.clone(), + left: value, + } + } + + /// Applies a function to each field of the `Edges`, producing a new `Edges`. + /// + /// This method allows for converting an `Edges` to an `Edges` by specifying a closure + /// that defines how to convert between the two types. The closure is applied to each field + /// (`top`, `right`, `bottom`, `left`), resulting in new edges of the desired type. + /// + /// # Arguments + /// + /// * `f` - A closure that takes a reference to a value of type `T` and returns a value of type `U`. + /// + /// # Returns + /// + /// Returns a new `Edges` with each field mapped by the provided function. + /// + /// # Examples + /// + /// ``` + /// # use gpui::Edges; + /// let edges = Edges { top: 10, right: 20, bottom: 30, left: 40 }; + /// let edges_float = edges.map(|&value| value as f32 * 1.1); + /// assert_eq!(edges_float, Edges { top: 11.0, right: 22.0, bottom: 33.0, left: 44.0 }); + /// ``` + pub fn map(&self, f: impl Fn(&T) -> U) -> Edges + where + U: Clone + Debug + Default + PartialEq, + { + Edges { + top: f(&self.top), + right: f(&self.right), + bottom: f(&self.bottom), + left: f(&self.left), + } + } + + /// Checks if any of the edges satisfy a given predicate. + /// + /// This method applies a predicate function to each field of the `Edges` and returns `true` if any field satisfies the predicate. + /// + /// # Arguments + /// + /// * `predicate` - A closure that takes a reference to a value of type `T` and returns a `bool`. + /// + /// # Returns + /// + /// Returns `true` if the predicate returns `true` for any of the edge values, `false` otherwise. + /// + /// # Examples + /// + /// ``` + /// # use gpui::Edges; + /// let edges = Edges { + /// top: 10, + /// right: 0, + /// bottom: 5, + /// left: 0, + /// }; + /// + /// assert!(edges.any(|value| *value == 0)); + /// assert!(edges.any(|value| *value > 0)); + /// assert!(!edges.any(|value| *value > 10)); + /// ``` + pub fn any bool>(&self, predicate: F) -> bool { + predicate(&self.top) + || predicate(&self.right) + || predicate(&self.bottom) + || predicate(&self.left) + } +} + +impl Edges { + /// Sets the edges of the `Edges` struct to `auto`, which is a special value that allows the layout engine to automatically determine the size of the edges. + /// + /// This is typically used in layout contexts where the exact size of the edges is not important, or when the size should be calculated based on the content or container. + /// + /// # Returns + /// + /// Returns an `Edges` with all edges set to `Length::Auto`. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Edges, Length}; + /// let auto_edges = Edges::auto(); + /// assert_eq!(auto_edges.top, Length::Auto); + /// assert_eq!(auto_edges.right, Length::Auto); + /// assert_eq!(auto_edges.bottom, Length::Auto); + /// assert_eq!(auto_edges.left, Length::Auto); + /// ``` + pub fn auto() -> Self { + Self { + top: Length::Auto, + right: Length::Auto, + bottom: Length::Auto, + left: Length::Auto, + } + } + + /// Sets the edges of the `Edges` struct to zero, which means no size or thickness. + /// + /// This is typically used when you want to specify that a box (like a padding or margin area) + /// should have no edges, effectively making it non-existent or invisible in layout calculations. + /// + /// # Returns + /// + /// Returns an `Edges` with all edges set to zero length. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{DefiniteLength, Edges, Length, Pixels}; + /// let no_edges = Edges::::zero(); + /// assert_eq!(no_edges.top, Length::Definite(DefiniteLength::from(Pixels::ZERO))); + /// assert_eq!(no_edges.right, Length::Definite(DefiniteLength::from(Pixels::ZERO))); + /// assert_eq!(no_edges.bottom, Length::Definite(DefiniteLength::from(Pixels::ZERO))); + /// assert_eq!(no_edges.left, Length::Definite(DefiniteLength::from(Pixels::ZERO))); + /// ``` + pub fn zero() -> Self { + Self { + top: px(0.).into(), + right: px(0.).into(), + bottom: px(0.).into(), + left: px(0.).into(), + } + } +} + +impl Edges { + /// Sets the edges of the `Edges` struct to zero, which means no size or thickness. + /// + /// This is typically used when you want to specify that a box (like a padding or margin area) + /// should have no edges, effectively making it non-existent or invisible in layout calculations. + /// + /// # Returns + /// + /// Returns an `Edges` with all edges set to zero length. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{px, DefiniteLength, Edges}; + /// let no_edges = Edges::::zero(); + /// assert_eq!(no_edges.top, DefiniteLength::from(px(0.))); + /// assert_eq!(no_edges.right, DefiniteLength::from(px(0.))); + /// assert_eq!(no_edges.bottom, DefiniteLength::from(px(0.))); + /// assert_eq!(no_edges.left, DefiniteLength::from(px(0.))); + /// ``` + pub fn zero() -> Self { + Self { + top: px(0.).into(), + right: px(0.).into(), + bottom: px(0.).into(), + left: px(0.).into(), + } + } + + /// Converts the `DefiniteLength` to `Pixels` based on the parent size and the REM size. + /// + /// This method allows for a `DefiniteLength` value to be converted into pixels, taking into account + /// the size of the parent element (for percentage-based lengths) and the size of a rem unit (for rem-based lengths). + /// + /// # Arguments + /// + /// * `parent_size` - `Size` representing the size of the parent element. + /// * `rem_size` - `Pixels` representing the size of one REM unit. + /// + /// # Returns + /// + /// Returns an `Edges` representing the edges with lengths converted to pixels. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Edges, DefiniteLength, px, AbsoluteLength, rems, Size}; + /// let edges = Edges { + /// top: DefiniteLength::Absolute(AbsoluteLength::Pixels(px(10.0))), + /// right: DefiniteLength::Fraction(0.5), + /// bottom: DefiniteLength::Absolute(AbsoluteLength::Rems(rems(2.0))), + /// left: DefiniteLength::Fraction(0.25), + /// }; + /// let parent_size = Size { + /// width: AbsoluteLength::Pixels(px(200.0)), + /// height: AbsoluteLength::Pixels(px(100.0)), + /// }; + /// let rem_size = px(16.0); + /// let edges_in_pixels = edges.to_pixels(parent_size, rem_size); + /// + /// assert_eq!(edges_in_pixels.top, px(10.0)); // Absolute length in pixels + /// assert_eq!(edges_in_pixels.right, px(100.0)); // 50% of parent width + /// assert_eq!(edges_in_pixels.bottom, px(32.0)); // 2 rems + /// assert_eq!(edges_in_pixels.left, px(50.0)); // 25% of parent width + /// ``` + pub fn to_pixels(self, parent_size: Size, rem_size: Pixels) -> Edges { + Edges { + top: self.top.to_pixels(parent_size.height, rem_size), + right: self.right.to_pixels(parent_size.width, rem_size), + bottom: self.bottom.to_pixels(parent_size.height, rem_size), + left: self.left.to_pixels(parent_size.width, rem_size), + } + } +} + +impl Edges { + /// Sets the edges of the `Edges` struct to zero, which means no size or thickness. + /// + /// This is typically used when you want to specify that a box (like a padding or margin area) + /// should have no edges, effectively making it non-existent or invisible in layout calculations. + /// + /// # Returns + /// + /// Returns an `Edges` with all edges set to zero length. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{AbsoluteLength, Edges, Pixels}; + /// let no_edges = Edges::::zero(); + /// assert_eq!(no_edges.top, AbsoluteLength::Pixels(Pixels::ZERO)); + /// assert_eq!(no_edges.right, AbsoluteLength::Pixels(Pixels::ZERO)); + /// assert_eq!(no_edges.bottom, AbsoluteLength::Pixels(Pixels::ZERO)); + /// assert_eq!(no_edges.left, AbsoluteLength::Pixels(Pixels::ZERO)); + /// ``` + pub fn zero() -> Self { + Self { + top: px(0.).into(), + right: px(0.).into(), + bottom: px(0.).into(), + left: px(0.).into(), + } + } + + /// Converts the `AbsoluteLength` to `Pixels` based on the `rem_size`. + /// + /// If the `AbsoluteLength` is already in pixels, it simply returns the corresponding `Pixels` value. + /// If the `AbsoluteLength` is in rems, it multiplies the number of rems by the `rem_size` to convert it to pixels. + /// + /// # Arguments + /// + /// * `rem_size` - The size of one rem unit in pixels. + /// + /// # Returns + /// + /// Returns an `Edges` representing the edges with lengths converted to pixels. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Edges, AbsoluteLength, Pixels, px, rems}; + /// let edges = Edges { + /// top: AbsoluteLength::Pixels(px(10.0)), + /// right: AbsoluteLength::Rems(rems(1.0)), + /// bottom: AbsoluteLength::Pixels(px(20.0)), + /// left: AbsoluteLength::Rems(rems(2.0)), + /// }; + /// let rem_size = px(16.0); + /// let edges_in_pixels = edges.to_pixels(rem_size); + /// + /// assert_eq!(edges_in_pixels.top, px(10.0)); // Already in pixels + /// assert_eq!(edges_in_pixels.right, px(16.0)); // 1 rem converted to pixels + /// assert_eq!(edges_in_pixels.bottom, px(20.0)); // Already in pixels + /// assert_eq!(edges_in_pixels.left, px(32.0)); // 2 rems converted to pixels + /// ``` + pub fn to_pixels(self, rem_size: Pixels) -> Edges { + Edges { + top: self.top.to_pixels(rem_size), + right: self.right.to_pixels(rem_size), + bottom: self.bottom.to_pixels(rem_size), + left: self.left.to_pixels(rem_size), + } + } +} + +impl Edges { + /// Scales the `Edges` by a given factor, returning `Edges`. + /// + /// This method is typically used for adjusting the edge sizes for different display densities or scaling factors. + /// + /// # Arguments + /// + /// * `factor` - The scaling factor to apply to each edge. + /// + /// # Returns + /// + /// Returns a new `Edges` where each edge is the result of scaling the original edge by the given factor. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Edges, Pixels, ScaledPixels}; + /// let edges = Edges { + /// top: Pixels::from(10.0), + /// right: Pixels::from(20.0), + /// bottom: Pixels::from(30.0), + /// left: Pixels::from(40.0), + /// }; + /// let scaled_edges = edges.scale(2.0); + /// assert_eq!(scaled_edges.top, ScaledPixels::from(20.0)); + /// assert_eq!(scaled_edges.right, ScaledPixels::from(40.0)); + /// assert_eq!(scaled_edges.bottom, ScaledPixels::from(60.0)); + /// assert_eq!(scaled_edges.left, ScaledPixels::from(80.0)); + /// ``` + pub fn scale(&self, factor: f32) -> Edges { + Edges { + top: self.top.scale(factor), + right: self.right.scale(factor), + bottom: self.bottom.scale(factor), + left: self.left.scale(factor), + } + } + + /// Returns the maximum value of any edge. + /// + /// # Returns + /// + /// The maximum `Pixels` value among all four edges. + pub fn max(&self) -> Pixels { + self.top.max(self.right).max(self.bottom).max(self.left) + } +} + +impl From for Edges { + fn from(val: f32) -> Self { + let val: Pixels = val.into(); + val.into() + } +} + +impl From for Edges { + fn from(val: Pixels) -> Self { + Edges { + top: val, + right: val, + bottom: val, + left: val, + } + } +} + +/// Identifies a corner of a 2d box. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Corner { + /// The top left corner + TopLeft, + /// The top right corner + TopRight, + /// The bottom left corner + BottomLeft, + /// The bottom right corner + BottomRight, +} + +impl Corner { + /// Returns the directly opposite corner. + /// + /// # Examples + /// + /// ``` + /// # use gpui::Corner; + /// assert_eq!(Corner::TopLeft.opposite_corner(), Corner::BottomRight); + /// ``` + #[must_use] + pub fn opposite_corner(self) -> Self { + match self { + Corner::TopLeft => Corner::BottomRight, + Corner::TopRight => Corner::BottomLeft, + Corner::BottomLeft => Corner::TopRight, + Corner::BottomRight => Corner::TopLeft, + } + } + + /// Returns the corner across from this corner, moving along the specified axis. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Axis, Corner}; + /// let result = Corner::TopLeft.other_side_corner_along(Axis::Horizontal); + /// assert_eq!(result, Corner::TopRight); + /// ``` + #[must_use] + pub fn other_side_corner_along(self, axis: Axis) -> Self { + match axis { + Axis::Vertical => match self { + Corner::TopLeft => Corner::BottomLeft, + Corner::TopRight => Corner::BottomRight, + Corner::BottomLeft => Corner::TopLeft, + Corner::BottomRight => Corner::TopRight, + }, + Axis::Horizontal => match self { + Corner::TopLeft => Corner::TopRight, + Corner::TopRight => Corner::TopLeft, + Corner::BottomLeft => Corner::BottomRight, + Corner::BottomRight => Corner::BottomLeft, + }, + } + } +} + +/// Represents the corners of a box in a 2D space, such as border radius. +/// +/// Each field represents the size of the corner on one side of the box: `top_left`, `top_right`, `bottom_right`, and `bottom_left`. +#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)] +#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +#[repr(C)] +pub struct Corners { + /// The value associated with the top left corner. + pub top_left: T, + /// The value associated with the top right corner. + pub top_right: T, + /// The value associated with the bottom right corner. + pub bottom_right: T, + /// The value associated with the bottom left corner. + pub bottom_left: T, +} + +impl Corners +where + T: Clone + Debug + Default + PartialEq, +{ + /// Constructs `Corners` where all sides are set to the same specified value. + /// + /// This function creates a `Corners` instance with the `top_left`, `top_right`, `bottom_right`, and `bottom_left` fields all initialized + /// to the same value provided as an argument. This is useful when you want to have uniform corners around a box, + /// such as a uniform border radius on a rectangle. + /// + /// # Arguments + /// + /// * `value` - The value to set for all four corners. + /// + /// # Returns + /// + /// An `Corners` instance with all corners set to the given value. + /// + /// # Examples + /// + /// ``` + /// # use gpui::Corners; + /// let uniform_corners = Corners::all(5.0); + /// assert_eq!(uniform_corners.top_left, 5.0); + /// assert_eq!(uniform_corners.top_right, 5.0); + /// assert_eq!(uniform_corners.bottom_right, 5.0); + /// assert_eq!(uniform_corners.bottom_left, 5.0); + /// ``` + pub fn all(value: T) -> Self { + Self { + top_left: value.clone(), + top_right: value.clone(), + bottom_right: value.clone(), + bottom_left: value, + } + } + + /// Returns the requested corner. + /// + /// # Returns + /// + /// A `Point` representing the corner requested by the parameter. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Corner, Corners}; + /// let corners = Corners { + /// top_left: 1, + /// top_right: 2, + /// bottom_left: 3, + /// bottom_right: 4 + /// }; + /// assert_eq!(corners.corner(Corner::BottomLeft), 3); + /// ``` + #[must_use] + pub fn corner(&self, corner: Corner) -> T { + match corner { + Corner::TopLeft => self.top_left.clone(), + Corner::TopRight => self.top_right.clone(), + Corner::BottomLeft => self.bottom_left.clone(), + Corner::BottomRight => self.bottom_right.clone(), + } + } +} + +impl Corners { + /// Converts the `AbsoluteLength` to `Pixels` based on the provided rem size. + /// + /// # Arguments + /// + /// * `rem_size` - The size of one REM unit in pixels, used for conversion if the `AbsoluteLength` is in REMs. + /// + /// # Returns + /// + /// Returns a `Corners` instance with each corner's length converted to pixels. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Corners, AbsoluteLength, Pixels, Rems, Size}; + /// let corners = Corners { + /// top_left: AbsoluteLength::Pixels(Pixels::from(15.0)), + /// top_right: AbsoluteLength::Rems(Rems(1.0)), + /// bottom_right: AbsoluteLength::Pixels(Pixels::from(30.0)), + /// bottom_left: AbsoluteLength::Rems(Rems(2.0)), + /// }; + /// let rem_size = Pixels::from(16.0); + /// let corners_in_pixels = corners.to_pixels(rem_size); + /// + /// assert_eq!(corners_in_pixels.top_left, Pixels::from(15.0)); + /// assert_eq!(corners_in_pixels.top_right, Pixels::from(16.0)); // 1 rem converted to pixels + /// assert_eq!(corners_in_pixels.bottom_right, Pixels::from(30.0)); + /// assert_eq!(corners_in_pixels.bottom_left, Pixels::from(32.0)); // 2 rems converted to pixels + /// ``` + pub fn to_pixels(self, rem_size: Pixels) -> Corners { + Corners { + top_left: self.top_left.to_pixels(rem_size), + top_right: self.top_right.to_pixels(rem_size), + bottom_right: self.bottom_right.to_pixels(rem_size), + bottom_left: self.bottom_left.to_pixels(rem_size), + } + } +} + +impl Corners { + /// Scales the `Corners` by a given factor, returning `Corners`. + /// + /// This method is typically used for adjusting the corner sizes for different display densities or scaling factors. + /// + /// # Arguments + /// + /// * `factor` - The scaling factor to apply to each corner. + /// + /// # Returns + /// + /// Returns a new `Corners` where each corner is the result of scaling the original corner by the given factor. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Corners, Pixels, ScaledPixels}; + /// let corners = Corners { + /// top_left: Pixels::from(10.0), + /// top_right: Pixels::from(20.0), + /// bottom_right: Pixels::from(30.0), + /// bottom_left: Pixels::from(40.0), + /// }; + /// let scaled_corners = corners.scale(2.0); + /// assert_eq!(scaled_corners.top_left, ScaledPixels::from(20.0)); + /// assert_eq!(scaled_corners.top_right, ScaledPixels::from(40.0)); + /// assert_eq!(scaled_corners.bottom_right, ScaledPixels::from(60.0)); + /// assert_eq!(scaled_corners.bottom_left, ScaledPixels::from(80.0)); + /// ``` + #[must_use] + pub fn scale(&self, factor: f32) -> Corners { + Corners { + top_left: self.top_left.scale(factor), + top_right: self.top_right.scale(factor), + bottom_right: self.bottom_right.scale(factor), + bottom_left: self.bottom_left.scale(factor), + } + } + + /// Returns the maximum value of any corner. + /// + /// # Returns + /// + /// The maximum `Pixels` value among all four corners. + #[must_use] + pub fn max(&self) -> Pixels { + self.top_left + .max(self.top_right) + .max(self.bottom_right) + .max(self.bottom_left) + } +} + +impl + Ord + Clone + Debug + Default + PartialEq> Corners { + /// Clamps corner radii to be less than or equal to half the shortest side of a quad. + /// + /// # Arguments + /// + /// * `size` - The size of the quad which limits the size of the corner radii. + /// + /// # Returns + /// + /// Corner radii values clamped to fit. + #[must_use] + pub fn clamp_radii_for_quad_size(self, size: Size) -> Corners { + let max = cmp::min(size.width, size.height) / 2.; + Corners { + top_left: cmp::min(self.top_left, max.clone()), + top_right: cmp::min(self.top_right, max.clone()), + bottom_right: cmp::min(self.bottom_right, max.clone()), + bottom_left: cmp::min(self.bottom_left, max), + } + } +} + +impl Corners { + /// Applies a function to each field of the `Corners`, producing a new `Corners`. + /// + /// This method allows for converting a `Corners` to a `Corners` by specifying a closure + /// that defines how to convert between the two types. The closure is applied to each field + /// (`top_left`, `top_right`, `bottom_right`, `bottom_left`), resulting in new corners of the desired type. + /// + /// # Arguments + /// + /// * `f` - A closure that takes a reference to a value of type `T` and returns a value of type `U`. + /// + /// # Returns + /// + /// Returns a new `Corners` with each field mapped by the provided function. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Corners, Pixels, Rems}; + /// let corners = Corners { + /// top_left: Pixels::from(10.0), + /// top_right: Pixels::from(20.0), + /// bottom_right: Pixels::from(30.0), + /// bottom_left: Pixels::from(40.0), + /// }; + /// let corners_in_rems = corners.map(|&px| Rems(f32::from(px) / 16.0)); + /// assert_eq!(corners_in_rems, Corners { + /// top_left: Rems(0.625), + /// top_right: Rems(1.25), + /// bottom_right: Rems(1.875), + /// bottom_left: Rems(2.5), + /// }); + /// ``` + #[must_use] + pub fn map(&self, f: impl Fn(&T) -> U) -> Corners + where + U: Clone + Debug + Default + PartialEq, + { + Corners { + top_left: f(&self.top_left), + top_right: f(&self.top_right), + bottom_right: f(&self.bottom_right), + bottom_left: f(&self.bottom_left), + } + } +} + +impl Mul for Corners +where + T: Mul + Clone + Debug + Default + PartialEq, +{ + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + Self { + top_left: self.top_left.clone() * rhs.top_left, + top_right: self.top_right.clone() * rhs.top_right, + bottom_right: self.bottom_right.clone() * rhs.bottom_right, + bottom_left: self.bottom_left * rhs.bottom_left, + } + } +} + +impl MulAssign for Corners +where + T: Mul + Clone + Debug + Default + PartialEq, + S: Clone, +{ + fn mul_assign(&mut self, rhs: S) { + self.top_left = self.top_left.clone() * rhs.clone(); + self.top_right = self.top_right.clone() * rhs.clone(); + self.bottom_right = self.bottom_right.clone() * rhs.clone(); + self.bottom_left = self.bottom_left.clone() * rhs; + } +} + +impl Copy for Corners where T: Copy + Clone + Debug + Default + PartialEq {} + +impl From for Corners { + fn from(val: f32) -> Self { + Corners { + top_left: val.into(), + top_right: val.into(), + bottom_right: val.into(), + bottom_left: val.into(), + } + } +} + +impl From for Corners { + fn from(val: Pixels) -> Self { + Corners { + top_left: val, + top_right: val, + bottom_right: val, + bottom_left: val, + } + } +} + +/// Represents an angle in Radians +#[derive( + Clone, + Copy, + Default, + Add, + AddAssign, + Sub, + SubAssign, + Neg, + Div, + DivAssign, + PartialEq, + Serialize, + Deserialize, + Debug, +)] +#[repr(transparent)] +pub struct Radians(pub f32); + +/// Create a `Radian` from a raw value +pub fn radians(value: f32) -> Radians { + Radians(value) +} + +/// A type representing a percentage value. +#[derive( + Clone, + Copy, + Default, + Add, + AddAssign, + Sub, + SubAssign, + Neg, + Div, + DivAssign, + PartialEq, + Serialize, + Deserialize, + Debug, +)] +#[repr(transparent)] +pub struct Percentage(pub f32); + +/// Generate a `Radian` from a percentage of a full circle. +pub fn percentage(value: f32) -> Percentage { + debug_assert!( + (0.0..=1.0).contains(&value), + "Percentage must be between 0 and 1" + ); + Percentage(value) +} + +impl From for Radians { + fn from(value: Percentage) -> Self { + radians(value.0 * std::f32::consts::PI * 2.0) + } +} + +/// Represents a length in pixels, the base unit of measurement in the UI framework. +/// +/// `Pixels` is a value type that represents an absolute length in pixels, which is used +/// for specifying sizes, positions, and distances in the UI. It is the fundamental unit +/// of measurement for all visual elements and layout calculations. +/// +/// The inner value is an `f32`, allowing for sub-pixel precision which can be useful for +/// anti-aliasing and animations. However, when applied to actual pixel grids, the value +/// is typically rounded to the nearest integer. +/// +/// # Examples +/// +/// ``` +/// use gpui::{Pixels, ScaledPixels}; +/// +/// // Define a length of 10 pixels +/// let length = Pixels::from(10.0); +/// +/// // Define a length and scale it by a factor of 2 +/// let scaled_length = length.scale(2.0); +/// assert_eq!(scaled_length, ScaledPixels::from(20.0)); +/// ``` +#[derive( + Clone, + Copy, + Default, + Add, + AddAssign, + Sub, + SubAssign, + Neg, + Div, + DivAssign, + PartialEq, + Serialize, + Deserialize, + JsonSchema, +)] +#[repr(transparent)] +pub struct Pixels(pub(crate) f32); + +impl Div for Pixels { + type Output = f32; + + fn div(self, rhs: Self) -> Self::Output { + self.0 / rhs.0 + } +} + +impl std::ops::DivAssign for Pixels { + fn div_assign(&mut self, rhs: Self) { + *self = Self(self.0 / rhs.0); + } +} + +impl std::ops::RemAssign for Pixels { + fn rem_assign(&mut self, rhs: Self) { + self.0 %= rhs.0; + } +} + +impl std::ops::Rem for Pixels { + type Output = Self; + + fn rem(self, rhs: Self) -> Self { + Self(self.0 % rhs.0) + } +} + +impl Mul for Pixels { + type Output = Self; + + fn mul(self, rhs: f32) -> Self { + Self(self.0 * rhs) + } +} + +impl Mul for f32 { + type Output = Pixels; + + fn mul(self, rhs: Pixels) -> Self::Output { + rhs * self + } +} + +impl Mul for Pixels { + type Output = Self; + + fn mul(self, rhs: usize) -> Self { + self * (rhs as f32) + } +} + +impl Mul for usize { + type Output = Pixels; + + fn mul(self, rhs: Pixels) -> Pixels { + rhs * self + } +} + +impl MulAssign for Pixels { + fn mul_assign(&mut self, rhs: f32) { + self.0 *= rhs; + } +} + +impl Display for Pixels { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}px", self.0) + } +} + +impl Debug for Pixels { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(self, f) + } +} + +impl TryFrom<&'_ str> for Pixels { + type Error = anyhow::Error; + + fn try_from(value: &'_ str) -> Result { + value + .strip_suffix("px") + .context("expected 'px' suffix") + .and_then(|number| Ok(number.parse()?)) + .map(Self) + } +} + +impl Pixels { + /// Represents zero pixels. + pub const ZERO: Pixels = Pixels(0.0); + /// The maximum value that can be represented by `Pixels`. + pub const MAX: Pixels = Pixels(f32::MAX); + /// The minimum value that can be represented by `Pixels`. + pub const MIN: Pixels = Pixels(f32::MIN); + + /// Floors the `Pixels` value to the nearest whole number. + /// + /// # Returns + /// + /// Returns a new `Pixels` instance with the floored value. + pub fn floor(&self) -> Self { + Self(self.0.floor()) + } + + /// Rounds the `Pixels` value to the nearest whole number. + /// + /// # Returns + /// + /// Returns a new `Pixels` instance with the rounded value. + pub fn round(&self) -> Self { + Self(self.0.round()) + } + + /// Returns the ceiling of the `Pixels` value to the nearest whole number. + /// + /// # Returns + /// + /// Returns a new `Pixels` instance with the ceiling value. + pub fn ceil(&self) -> Self { + Self(self.0.ceil()) + } + + /// Scales the `Pixels` value by a given factor, producing `ScaledPixels`. + /// + /// This method is used when adjusting pixel values for display scaling factors, + /// such as high DPI (dots per inch) or Retina displays, where the pixel density is higher and + /// thus requires scaling to maintain visual consistency and readability. + /// + /// The resulting `ScaledPixels` represent the scaled value which can be used for rendering + /// calculations where display scaling is considered. + #[must_use] + pub fn scale(&self, factor: f32) -> ScaledPixels { + ScaledPixels(self.0 * factor) + } + + /// Raises the `Pixels` value to a given power. + /// + /// # Arguments + /// + /// * `exponent` - The exponent to raise the `Pixels` value by. + /// + /// # Returns + /// + /// Returns a new `Pixels` instance with the value raised to the given exponent. + pub fn pow(&self, exponent: f32) -> Self { + Self(self.0.powf(exponent)) + } + + /// Returns the absolute value of the `Pixels`. + /// + /// # Returns + /// + /// A new `Pixels` instance with the absolute value of the original `Pixels`. + pub fn abs(&self) -> Self { + Self(self.0.abs()) + } + + /// Returns the sign of the `Pixels` value. + /// + /// # Returns + /// + /// Returns: + /// * `1.0` if the value is positive + /// * `-1.0` if the value is negative + pub fn signum(&self) -> f32 { + self.0.signum() + } + + /// Returns the f64 value of `Pixels`. + /// + /// # Returns + /// + /// A f64 value of the `Pixels`. + pub fn to_f64(self) -> f64 { + self.0 as f64 + } +} + +impl Eq for Pixels {} + +impl PartialOrd for Pixels { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Pixels { + fn cmp(&self, other: &Self) -> cmp::Ordering { + self.0.total_cmp(&other.0) + } +} + +impl std::hash::Hash for Pixels { + fn hash(&self, state: &mut H) { + self.0.to_bits().hash(state); + } +} + +impl From for Pixels { + fn from(pixels: f64) -> Self { + Pixels(pixels as f32) + } +} + +impl From for Pixels { + fn from(pixels: f32) -> Self { + Pixels(pixels) + } +} + +impl From for f32 { + fn from(pixels: Pixels) -> Self { + pixels.0 + } +} + +impl From<&Pixels> for f32 { + fn from(pixels: &Pixels) -> Self { + pixels.0 + } +} + +impl From for f64 { + fn from(pixels: Pixels) -> Self { + pixels.0 as f64 + } +} + +impl From for u32 { + fn from(pixels: Pixels) -> Self { + pixels.0 as u32 + } +} + +impl From<&Pixels> for u32 { + fn from(pixels: &Pixels) -> Self { + pixels.0 as u32 + } +} + +impl From for Pixels { + fn from(pixels: u32) -> Self { + Pixels(pixels as f32) + } +} + +impl From for usize { + fn from(pixels: Pixels) -> Self { + pixels.0 as usize + } +} + +impl From for Pixels { + fn from(pixels: usize) -> Self { + Pixels(pixels as f32) + } +} + +/// Represents physical pixels on the display. +/// +/// `DevicePixels` is a unit of measurement that refers to the actual pixels on a device's screen. +/// This type is used when precise pixel manipulation is required, such as rendering graphics or +/// interfacing with hardware that operates on the pixel level. Unlike logical pixels that may be +/// affected by the device's scale factor, `DevicePixels` always correspond to real pixels on the +/// display. +#[derive( + Add, + AddAssign, + Clone, + Copy, + Default, + Div, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, + Sub, + SubAssign, + Serialize, + Deserialize, +)] +#[repr(transparent)] +pub struct DevicePixels(pub i32); + +impl DevicePixels { + /// Converts the `DevicePixels` value to the number of bytes needed to represent it in memory. + /// + /// This function is useful when working with graphical data that needs to be stored in a buffer, + /// such as images or framebuffers, where each pixel may be represented by a specific number of bytes. + /// + /// # Arguments + /// + /// * `bytes_per_pixel` - The number of bytes used to represent a single pixel. + /// + /// # Returns + /// + /// The number of bytes required to represent the `DevicePixels` value in memory. + /// + /// # Examples + /// + /// ``` + /// # use gpui::DevicePixels; + /// let pixels = DevicePixels(10); // 10 device pixels + /// let bytes_per_pixel = 4; // Assume each pixel is represented by 4 bytes (e.g., RGBA) + /// let total_bytes = pixels.to_bytes(bytes_per_pixel); + /// assert_eq!(total_bytes, 40); // 10 pixels * 4 bytes/pixel = 40 bytes + /// ``` + pub fn to_bytes(self, bytes_per_pixel: u8) -> u32 { + self.0 as u32 * bytes_per_pixel as u32 + } +} + +impl fmt::Debug for DevicePixels { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} px (device)", self.0) + } +} + +impl From for i32 { + fn from(device_pixels: DevicePixels) -> Self { + device_pixels.0 + } +} + +impl From for DevicePixels { + fn from(device_pixels: i32) -> Self { + DevicePixels(device_pixels) + } +} + +impl From for DevicePixels { + fn from(device_pixels: u32) -> Self { + DevicePixels(device_pixels as i32) + } +} + +impl From for u32 { + fn from(device_pixels: DevicePixels) -> Self { + device_pixels.0 as u32 + } +} + +impl From for u64 { + fn from(device_pixels: DevicePixels) -> Self { + device_pixels.0 as u64 + } +} + +impl From for DevicePixels { + fn from(device_pixels: u64) -> Self { + DevicePixels(device_pixels as i32) + } +} + +impl From for usize { + fn from(device_pixels: DevicePixels) -> Self { + device_pixels.0 as usize + } +} + +impl From for DevicePixels { + fn from(device_pixels: usize) -> Self { + DevicePixels(device_pixels as i32) + } +} + +/// Represents scaled pixels that take into account the device's scale factor. +/// +/// `ScaledPixels` are used to ensure that UI elements appear at the correct size on devices +/// with different pixel densities. When a device has a higher scale factor (such as Retina displays), +/// a single logical pixel may correspond to multiple physical pixels. By using `ScaledPixels`, +/// dimensions and positions can be specified in a way that scales appropriately across different +/// display resolutions. +#[derive(Clone, Copy, Default, Add, AddAssign, Sub, SubAssign, Div, DivAssign, PartialEq)] +#[repr(transparent)] +pub struct ScaledPixels(pub(crate) f32); + +impl ScaledPixels { + /// Floors the `ScaledPixels` value to the nearest whole number. + /// + /// # Returns + /// + /// Returns a new `ScaledPixels` instance with the floored value. + pub fn floor(&self) -> Self { + Self(self.0.floor()) + } + + /// Rounds the `ScaledPixels` value to the nearest whole number. + /// + /// # Returns + /// + /// Returns a new `ScaledPixels` instance with the rounded value. + pub fn round(&self) -> Self { + Self(self.0.round()) + } + + /// Ceils the `ScaledPixels` value to the nearest whole number. + /// + /// # Returns + /// + /// Returns a new `ScaledPixels` instance with the ceiled value. + pub fn ceil(&self) -> Self { + Self(self.0.ceil()) + } +} + +impl Eq for ScaledPixels {} + +impl PartialOrd for ScaledPixels { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ScaledPixels { + fn cmp(&self, other: &Self) -> cmp::Ordering { + self.0.total_cmp(&other.0) + } +} + +impl Debug for ScaledPixels { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}px (scaled)", self.0) + } +} + +impl From for DevicePixels { + fn from(scaled: ScaledPixels) -> Self { + DevicePixels(scaled.0.ceil() as i32) + } +} + +impl From for ScaledPixels { + fn from(device: DevicePixels) -> Self { + ScaledPixels(device.0 as f32) + } +} + +impl From for f64 { + fn from(scaled_pixels: ScaledPixels) -> Self { + scaled_pixels.0 as f64 + } +} + +impl From for u32 { + fn from(pixels: ScaledPixels) -> Self { + pixels.0 as u32 + } +} + +impl From for ScaledPixels { + fn from(pixels: f32) -> Self { + Self(pixels) + } +} + +impl Div for ScaledPixels { + type Output = f32; + + fn div(self, rhs: Self) -> Self::Output { + self.0 / rhs.0 + } +} + +impl std::ops::DivAssign for ScaledPixels { + fn div_assign(&mut self, rhs: Self) { + *self = Self(self.0 / rhs.0); + } +} + +impl std::ops::RemAssign for ScaledPixels { + fn rem_assign(&mut self, rhs: Self) { + self.0 %= rhs.0; + } +} + +impl std::ops::Rem for ScaledPixels { + type Output = Self; + + fn rem(self, rhs: Self) -> Self { + Self(self.0 % rhs.0) + } +} + +impl Mul for ScaledPixels { + type Output = Self; + + fn mul(self, rhs: f32) -> Self { + Self(self.0 * rhs) + } +} + +impl Mul for f32 { + type Output = ScaledPixels; + + fn mul(self, rhs: ScaledPixels) -> Self::Output { + rhs * self + } +} + +impl Mul for ScaledPixels { + type Output = Self; + + fn mul(self, rhs: usize) -> Self { + self * (rhs as f32) + } +} + +impl Mul for usize { + type Output = ScaledPixels; + + fn mul(self, rhs: ScaledPixels) -> ScaledPixels { + rhs * self + } +} + +impl MulAssign for ScaledPixels { + fn mul_assign(&mut self, rhs: f32) { + self.0 *= rhs; + } +} + +/// Represents a length in rems, a unit based on the font-size of the window, which can be assigned with [`Window::set_rem_size`][set_rem_size]. +/// +/// Rems are used for defining lengths that are scalable and consistent across different UI elements. +/// The value of `1rem` is typically equal to the font-size of the root element (often the `` element in browsers), +/// making it a flexible unit that adapts to the user's text size preferences. In this framework, `rems` serve a similar +/// purpose, allowing for scalable and accessible design that can adjust to different display settings or user preferences. +/// +/// For example, if the root element's font-size is `16px`, then `1rem` equals `16px`. A length of `2rems` would then be `32px`. +/// +/// [set_rem_size]: crate::Window::set_rem_size +#[derive(Clone, Copy, Default, Add, Sub, Mul, Div, Neg, PartialEq)] +pub struct Rems(pub f32); + +impl Rems { + /// Convert this Rem value to pixels. + pub fn to_pixels(self, rem_size: Pixels) -> Pixels { + self * rem_size + } +} + +impl Mul for Rems { + type Output = Pixels; + + fn mul(self, other: Pixels) -> Pixels { + Pixels(self.0 * other.0) + } +} + +impl Display for Rems { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}rem", self.0) + } +} + +impl Debug for Rems { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(self, f) + } +} + +impl TryFrom<&'_ str> for Rems { + type Error = anyhow::Error; + + fn try_from(value: &'_ str) -> Result { + value + .strip_suffix("rem") + .context("expected 'rem' suffix") + .and_then(|number| Ok(number.parse()?)) + .map(Self) + } +} + +/// Represents an absolute length in pixels or rems. +/// +/// `AbsoluteLength` can be either a fixed number of pixels, which is an absolute measurement not +/// affected by the current font size, or a number of rems, which is relative to the font size of +/// the root element. It is used for specifying dimensions that are either independent of or +/// related to the typographic scale. +#[derive(Clone, Copy, Neg, PartialEq)] +pub enum AbsoluteLength { + /// A length in pixels. + Pixels(Pixels), + /// A length in rems. + Rems(Rems), +} + +impl AbsoluteLength { + /// Checks if the absolute length is zero. + pub fn is_zero(&self) -> bool { + match self { + AbsoluteLength::Pixels(px) => px.0 == 0.0, + AbsoluteLength::Rems(rems) => rems.0 == 0.0, + } + } +} + +impl From for AbsoluteLength { + fn from(pixels: Pixels) -> Self { + AbsoluteLength::Pixels(pixels) + } +} + +impl From for AbsoluteLength { + fn from(rems: Rems) -> Self { + AbsoluteLength::Rems(rems) + } +} + +impl AbsoluteLength { + /// Converts an `AbsoluteLength` to `Pixels` based on a given `rem_size`. + /// + /// # Arguments + /// + /// * `rem_size` - The size of one rem in pixels. + /// + /// # Returns + /// + /// Returns the `AbsoluteLength` as `Pixels`. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{AbsoluteLength, Pixels, Rems}; + /// let length_in_pixels = AbsoluteLength::Pixels(Pixels::from(42.0)); + /// let length_in_rems = AbsoluteLength::Rems(Rems(2.0)); + /// let rem_size = Pixels::from(16.0); + /// + /// assert_eq!(length_in_pixels.to_pixels(rem_size), Pixels::from(42.0)); + /// assert_eq!(length_in_rems.to_pixels(rem_size), Pixels::from(32.0)); + /// ``` + pub fn to_pixels(self, rem_size: Pixels) -> Pixels { + match self { + AbsoluteLength::Pixels(pixels) => pixels, + AbsoluteLength::Rems(rems) => rems.to_pixels(rem_size), + } + } + + /// Converts an `AbsoluteLength` to `Rems` based on a given `rem_size`. + /// + /// # Arguments + /// + /// * `rem_size` - The size of one rem in pixels. + /// + /// # Returns + /// + /// Returns the `AbsoluteLength` as `Pixels`. + pub fn to_rems(self, rem_size: Pixels) -> Rems { + match self { + AbsoluteLength::Pixels(pixels) => Rems(pixels.0 / rem_size.0), + AbsoluteLength::Rems(rems) => rems, + } + } +} + +impl Default for AbsoluteLength { + fn default() -> Self { + px(0.).into() + } +} + +impl Display for AbsoluteLength { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Pixels(pixels) => write!(f, "{pixels}"), + Self::Rems(rems) => write!(f, "{rems}"), + } + } +} + +impl Debug for AbsoluteLength { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(self, f) + } +} + +const EXPECTED_ABSOLUTE_LENGTH: &str = "number with 'px' or 'rem' suffix"; + +impl TryFrom<&'_ str> for AbsoluteLength { + type Error = anyhow::Error; + + fn try_from(value: &'_ str) -> Result { + if let Ok(pixels) = value.try_into() { + Ok(Self::Pixels(pixels)) + } else if let Ok(rems) = value.try_into() { + Ok(Self::Rems(rems)) + } else { + Err(anyhow!( + "invalid AbsoluteLength '{value}', expected {EXPECTED_ABSOLUTE_LENGTH}" + )) + } + } +} + +impl JsonSchema for AbsoluteLength { + fn schema_name() -> Cow<'static, str> { + "AbsoluteLength".into() + } + + fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + json_schema!({ + "type": "string", + "pattern": r"^-?\d+(\.\d+)?(px|rem)$" + }) + } +} + +impl<'de> Deserialize<'de> for AbsoluteLength { + fn deserialize>(deserializer: D) -> Result { + struct StringVisitor; + + impl de::Visitor<'_> for StringVisitor { + type Value = AbsoluteLength; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{EXPECTED_ABSOLUTE_LENGTH}") + } + + fn visit_str(self, value: &str) -> Result { + AbsoluteLength::try_from(value).map_err(E::custom) + } + } + + deserializer.deserialize_str(StringVisitor) + } +} + +impl Serialize for AbsoluteLength { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&format!("{self}")) + } +} + +/// A non-auto length that can be defined in pixels, rems, or percent of parent. +/// +/// This enum represents lengths that have a specific value, as opposed to lengths that are automatically +/// determined by the context. It includes absolute lengths in pixels or rems, and relative lengths as a +/// fraction of the parent's size. +#[derive(Clone, Copy, Neg, PartialEq)] +pub enum DefiniteLength { + /// An absolute length specified in pixels or rems. + Absolute(AbsoluteLength), + /// A relative length specified as a fraction of the parent's size, between 0 and 1. + Fraction(f32), +} + +impl DefiniteLength { + /// Converts the `DefiniteLength` to `Pixels` based on a given `base_size` and `rem_size`. + /// + /// If the `DefiniteLength` is an absolute length, it will be directly converted to `Pixels`. + /// If it is a fraction, the fraction will be multiplied by the `base_size` to get the length in pixels. + /// + /// # Arguments + /// + /// * `base_size` - The base size in `AbsoluteLength` to which the fraction will be applied. + /// * `rem_size` - The size of one rem in pixels, used to convert rems to pixels. + /// + /// # Returns + /// + /// Returns the `DefiniteLength` as `Pixels`. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{DefiniteLength, AbsoluteLength, Pixels, px, rems}; + /// let length_in_pixels = DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0))); + /// let length_in_rems = DefiniteLength::Absolute(AbsoluteLength::Rems(rems(2.0))); + /// let length_as_fraction = DefiniteLength::Fraction(0.5); + /// let base_size = AbsoluteLength::Pixels(px(100.0)); + /// let rem_size = px(16.0); + /// + /// assert_eq!(length_in_pixels.to_pixels(base_size, rem_size), Pixels::from(42.0)); + /// assert_eq!(length_in_rems.to_pixels(base_size, rem_size), Pixels::from(32.0)); + /// assert_eq!(length_as_fraction.to_pixels(base_size, rem_size), Pixels::from(50.0)); + /// ``` + pub fn to_pixels(self, base_size: AbsoluteLength, rem_size: Pixels) -> Pixels { + match self { + DefiniteLength::Absolute(size) => size.to_pixels(rem_size), + DefiniteLength::Fraction(fraction) => match base_size { + AbsoluteLength::Pixels(px) => px * fraction, + AbsoluteLength::Rems(rems) => rems * rem_size * fraction, + }, + } + } +} + +impl Debug for DefiniteLength { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(self, f) + } +} + +impl Display for DefiniteLength { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DefiniteLength::Absolute(length) => write!(f, "{length}"), + DefiniteLength::Fraction(fraction) => write!(f, "{}%", (fraction * 100.0) as i32), + } + } +} + +const EXPECTED_DEFINITE_LENGTH: &str = "expected number with 'px', 'rem', or '%' suffix"; + +impl TryFrom<&'_ str> for DefiniteLength { + type Error = anyhow::Error; + + fn try_from(value: &'_ str) -> Result { + if let Some(percentage) = value.strip_suffix('%') { + let fraction: f32 = percentage.parse::().with_context(|| { + format!("invalid DefiniteLength '{value}', expected {EXPECTED_DEFINITE_LENGTH}") + })?; + Ok(DefiniteLength::Fraction(fraction / 100.0)) + } else if let Ok(absolute_length) = value.try_into() { + Ok(DefiniteLength::Absolute(absolute_length)) + } else { + Err(anyhow!( + "invalid DefiniteLength '{value}', expected {EXPECTED_DEFINITE_LENGTH}" + )) + } + } +} + +impl JsonSchema for DefiniteLength { + fn schema_name() -> Cow<'static, str> { + "DefiniteLength".into() + } + + fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + json_schema!({ + "type": "string", + "pattern": r"^-?\d+(\.\d+)?(px|rem|%)$" + }) + } +} + +impl<'de> Deserialize<'de> for DefiniteLength { + fn deserialize>(deserializer: D) -> Result { + struct StringVisitor; + + impl de::Visitor<'_> for StringVisitor { + type Value = DefiniteLength; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{EXPECTED_DEFINITE_LENGTH}") + } + + fn visit_str(self, value: &str) -> Result { + DefiniteLength::try_from(value).map_err(E::custom) + } + } + + deserializer.deserialize_str(StringVisitor) + } +} + +impl Serialize for DefiniteLength { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&format!("{self}")) + } +} + +impl From for DefiniteLength { + fn from(pixels: Pixels) -> Self { + Self::Absolute(pixels.into()) + } +} + +impl From for DefiniteLength { + fn from(rems: Rems) -> Self { + Self::Absolute(rems.into()) + } +} + +impl From for DefiniteLength { + fn from(length: AbsoluteLength) -> Self { + Self::Absolute(length) + } +} + +impl Default for DefiniteLength { + fn default() -> Self { + Self::Absolute(AbsoluteLength::default()) + } +} + +/// A length that can be defined in pixels, rems, percent of parent, or auto. +#[derive(Clone, Copy, PartialEq)] +pub enum Length { + /// A definite length specified either in pixels, rems, or as a fraction of the parent's size. + Definite(DefiniteLength), + /// An automatic length that is determined by the context in which it is used. + Auto, +} + +impl Debug for Length { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(self, f) + } +} + +impl Display for Length { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Length::Definite(definite_length) => write!(f, "{}", definite_length), + Length::Auto => write!(f, "auto"), + } + } +} + +const EXPECTED_LENGTH: &str = "expected 'auto' or number with 'px', 'rem', or '%' suffix"; + +impl TryFrom<&'_ str> for Length { + type Error = anyhow::Error; + + fn try_from(value: &'_ str) -> Result { + if value == "auto" { + Ok(Length::Auto) + } else if let Ok(definite_length) = value.try_into() { + Ok(Length::Definite(definite_length)) + } else { + Err(anyhow!( + "invalid Length '{value}', expected {EXPECTED_LENGTH}" + )) + } + } +} + +impl JsonSchema for Length { + fn schema_name() -> Cow<'static, str> { + "Length".into() + } + + fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + json_schema!({ + "type": "string", + "pattern": r"^(auto|-?\d+(\.\d+)?(px|rem|%))$" + }) + } +} + +impl<'de> Deserialize<'de> for Length { + fn deserialize>(deserializer: D) -> Result { + struct StringVisitor; + + impl de::Visitor<'_> for StringVisitor { + type Value = Length; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{EXPECTED_LENGTH}") + } + + fn visit_str(self, value: &str) -> Result { + Length::try_from(value).map_err(E::custom) + } + } + + deserializer.deserialize_str(StringVisitor) + } +} + +impl Serialize for Length { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&format!("{self}")) + } +} + +/// Constructs a `DefiniteLength` representing a relative fraction of a parent size. +/// +/// This function creates a `DefiniteLength` that is a specified fraction of a parent's dimension. +/// The fraction should be a floating-point number between 0.0 and 1.0, where 1.0 represents 100% of the parent's size. +/// +/// # Arguments +/// +/// * `fraction` - The fraction of the parent's size, between 0.0 and 1.0. +/// +/// # Returns +/// +/// A `DefiniteLength` representing the relative length as a fraction of the parent's size. +pub const fn relative(fraction: f32) -> DefiniteLength { + DefiniteLength::Fraction(fraction) +} + +/// Returns the Golden Ratio, i.e. `~(1.0 + sqrt(5.0)) / 2.0`. +pub fn phi() -> DefiniteLength { + relative(1.618_034) +} + +/// Constructs a `Rems` value representing a length in rems. +/// +/// # Arguments +/// +/// * `rems` - The number of rems for the length. +/// +/// # Returns +/// +/// A `Rems` representing the specified number of rems. +pub fn rems(rems: f32) -> Rems { + Rems(rems) +} + +/// Constructs a `Pixels` value representing a length in pixels. +/// +/// # Arguments +/// +/// * `pixels` - The number of pixels for the length. +/// +/// # Returns +/// +/// A `Pixels` representing the specified number of pixels. +pub const fn px(pixels: f32) -> Pixels { + Pixels(pixels) +} + +/// Returns a `Length` representing an automatic length. +/// +/// The `auto` length is often used in layout calculations where the length should be determined +/// by the layout context itself rather than being explicitly set. This is commonly used in CSS +/// for properties like `width`, `height`, `margin`, `padding`, etc., where `auto` can be used +/// to instruct the layout engine to calculate the size based on other factors like the size of the +/// container or the intrinsic size of the content. +/// +/// # Returns +/// +/// A `Length` variant set to `Auto`. +pub fn auto() -> Length { + Length::Auto +} + +impl From for Length { + fn from(pixels: Pixels) -> Self { + Self::Definite(pixels.into()) + } +} + +impl From for Length { + fn from(rems: Rems) -> Self { + Self::Definite(rems.into()) + } +} + +impl From for Length { + fn from(length: DefiniteLength) -> Self { + Self::Definite(length) + } +} + +impl From for Length { + fn from(length: AbsoluteLength) -> Self { + Self::Definite(length.into()) + } +} + +impl Default for Length { + fn default() -> Self { + Self::Definite(DefiniteLength::default()) + } +} + +impl From<()> for Length { + fn from(_: ()) -> Self { + Self::Definite(DefiniteLength::default()) + } +} + +/// A location in a grid layout. +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, JsonSchema, Default)] +pub struct GridLocation { + /// The rows this item uses within the grid. + pub row: Range, + /// The columns this item uses within the grid. + pub column: Range, +} + +/// The placement of an item within a grid layout's column or row. +#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, JsonSchema, Default)] +pub enum GridPlacement { + /// The grid line index to place this item. + Line(i16), + /// The number of grid lines to span. + Span(u16), + /// Automatically determine the placement, equivalent to Span(1) + #[default] + Auto, +} + +impl From for taffy::GridPlacement { + fn from(placement: GridPlacement) -> Self { + match placement { + GridPlacement::Line(index) => taffy::GridPlacement::from_line_index(index), + GridPlacement::Span(span) => taffy::GridPlacement::from_span(span), + GridPlacement::Auto => taffy::GridPlacement::Auto, + } + } +} + +/// Provides a trait for types that can calculate half of their value. +/// +/// The `Half` trait is used for types that can be evenly divided, returning a new instance of the same type +/// representing half of the original value. This is commonly used for types that represent measurements or sizes, +/// such as lengths or pixels, where halving is a frequent operation during layout calculations or animations. +pub trait Half { + /// Returns half of the current value. + /// + /// # Returns + /// + /// A new instance of the implementing type, representing half of the original value. + fn half(&self) -> Self; +} + +impl Half for i32 { + fn half(&self) -> Self { + self / 2 + } +} + +impl Half for f32 { + fn half(&self) -> Self { + self / 2. + } +} + +impl Half for DevicePixels { + fn half(&self) -> Self { + Self(self.0 / 2) + } +} + +impl Half for ScaledPixels { + fn half(&self) -> Self { + Self(self.0 / 2.) + } +} + +impl Half for Pixels { + fn half(&self) -> Self { + Self(self.0 / 2.) + } +} + +impl Half for Rems { + fn half(&self) -> Self { + Self(self.0 / 2.) + } +} + +/// Provides a trait for types that can negate their values. +pub trait Negate { + /// Returns the negation of the given value + fn negate(self) -> Self; +} + +impl Negate for i32 { + fn negate(self) -> Self { + -self + } +} + +impl Negate for f32 { + fn negate(self) -> Self { + -self + } +} + +impl Negate for DevicePixels { + fn negate(self) -> Self { + Self(-self.0) + } +} + +impl Negate for ScaledPixels { + fn negate(self) -> Self { + Self(-self.0) + } +} + +impl Negate for Pixels { + fn negate(self) -> Self { + Self(-self.0) + } +} + +impl Negate for Rems { + fn negate(self) -> Self { + Self(-self.0) + } +} + +/// A trait for checking if a value is zero. +/// +/// This trait provides a method to determine if a value is considered to be zero. +/// It is implemented for various numeric and length-related types where the concept +/// of zero is applicable. This can be useful for comparisons, optimizations, or +/// determining if an operation has a neutral effect. +pub trait IsZero { + /// Determines if the value is zero. + /// + /// # Returns + /// + /// Returns `true` if the value is zero, `false` otherwise. + fn is_zero(&self) -> bool; +} + +impl IsZero for DevicePixels { + fn is_zero(&self) -> bool { + self.0 == 0 + } +} + +impl IsZero for ScaledPixels { + fn is_zero(&self) -> bool { + self.0 == 0. + } +} + +impl IsZero for Pixels { + fn is_zero(&self) -> bool { + self.0 == 0. + } +} + +impl IsZero for Rems { + fn is_zero(&self) -> bool { + self.0 == 0. + } +} + +impl IsZero for AbsoluteLength { + fn is_zero(&self) -> bool { + match self { + AbsoluteLength::Pixels(pixels) => pixels.is_zero(), + AbsoluteLength::Rems(rems) => rems.is_zero(), + } + } +} + +impl IsZero for DefiniteLength { + fn is_zero(&self) -> bool { + match self { + DefiniteLength::Absolute(length) => length.is_zero(), + DefiniteLength::Fraction(fraction) => *fraction == 0., + } + } +} + +impl IsZero for Length { + fn is_zero(&self) -> bool { + match self { + Length::Definite(length) => length.is_zero(), + Length::Auto => false, + } + } +} + +impl IsZero for Point { + fn is_zero(&self) -> bool { + self.x.is_zero() && self.y.is_zero() + } +} + +impl IsZero for Size +where + T: IsZero + Clone + Debug + Default + PartialEq, +{ + fn is_zero(&self) -> bool { + self.width.is_zero() || self.height.is_zero() + } +} + +impl IsZero for Bounds { + fn is_zero(&self) -> bool { + self.size.is_zero() + } +} + +impl IsZero for Corners +where + T: IsZero + Clone + Debug + Default + PartialEq, +{ + fn is_zero(&self) -> bool { + self.top_left.is_zero() + && self.top_right.is_zero() + && self.bottom_right.is_zero() + && self.bottom_left.is_zero() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bounds_intersects() { + let bounds1 = Bounds { + origin: Point { x: 0.0, y: 0.0 }, + size: Size { + width: 5.0, + height: 5.0, + }, + }; + let bounds2 = Bounds { + origin: Point { x: 4.0, y: 4.0 }, + size: Size { + width: 5.0, + height: 5.0, + }, + }; + let bounds3 = Bounds { + origin: Point { x: 10.0, y: 10.0 }, + size: Size { + width: 5.0, + height: 5.0, + }, + }; + + // Test Case 1: Intersecting bounds + assert!(bounds1.intersects(&bounds2)); + + // Test Case 2: Non-Intersecting bounds + assert!(!bounds1.intersects(&bounds3)); + + // Test Case 3: Bounds intersecting with themselves + assert!(bounds1.intersects(&bounds1)); + } +} diff --git a/third_party/gpui/src/global.rs b/third_party/gpui/src/global.rs new file mode 100644 index 0000000..a16934c --- /dev/null +++ b/third_party/gpui/src/global.rs @@ -0,0 +1,75 @@ +use crate::{App, BorrowAppContext}; + +/// A marker trait for types that can be stored in GPUI's global state. +/// +/// This trait exists to provide type-safe access to globals by ensuring only +/// types that implement [`Global`] can be used with the accessor methods. For +/// example, trying to access a global with a type that does not implement +/// [`Global`] will result in a compile-time error. +/// +/// Implement this on types you want to store in the context as a global. +/// +/// ## Restricting Access to Globals +/// +/// In some situations you may need to store some global state, but want to +/// restrict access to reading it or writing to it. +/// +/// In these cases, Rust's visibility system can be used to restrict access to +/// a global value. For example, you can create a private struct that implements +/// [`Global`] and holds the global state. Then create a newtype struct that wraps +/// the global type and create custom accessor methods to expose the desired subset +/// of operations. +pub trait Global: 'static { + // This trait is intentionally left empty, by virtue of being a marker trait. + // + // Use additional traits with blanket implementations to attach functionality + // to types that implement `Global`. +} + +/// A trait for reading a global value from the context. +pub trait ReadGlobal { + /// Returns the global instance of the implementing type. + /// + /// Panics if a global for that type has not been assigned. + fn global(cx: &App) -> &Self; +} + +impl ReadGlobal for T { + fn global(cx: &App) -> &Self { + cx.global::() + } +} + +/// A trait for updating a global value in the context. +pub trait UpdateGlobal { + /// Updates the global instance of the implementing type using the provided closure. + /// + /// This method provides the closure with mutable access to the context and the global simultaneously. + fn update_global(cx: &mut C, update: F) -> R + where + C: BorrowAppContext, + F: FnOnce(&mut Self, &mut C) -> R; + + /// Set the global instance of the implementing type. + fn set_global(cx: &mut C, global: Self) + where + C: BorrowAppContext; +} + +impl UpdateGlobal for T { + #[track_caller] + fn update_global(cx: &mut C, update: F) -> R + where + C: BorrowAppContext, + F: FnOnce(&mut Self, &mut C) -> R, + { + cx.update_global(update) + } + + fn set_global(cx: &mut C, global: Self) + where + C: BorrowAppContext, + { + cx.set_global(global) + } +} diff --git a/third_party/gpui/src/gpui.rs b/third_party/gpui/src/gpui.rs new file mode 100644 index 0000000..805dbbd --- /dev/null +++ b/third_party/gpui/src/gpui.rs @@ -0,0 +1,311 @@ +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] +#![allow(clippy::type_complexity)] // Not useful, GPUI makes heavy use of callbacks +#![allow(clippy::collapsible_else_if)] // False positives in platform specific code +#![allow(unused_mut)] // False positives in platform specific code + +extern crate self as gpui; + +#[macro_use] +mod action; +mod app; + +mod arena; +mod asset_cache; +mod assets; +mod bounds_tree; +mod color; +/// The default colors used by GPUI. +pub mod colors; +mod element; +mod elements; +mod executor; +mod geometry; +mod global; +mod input; +mod inspector; +mod interactive; +mod key_dispatch; +mod keymap; +mod path_builder; +mod platform; +pub mod prelude; +mod scene; +mod shared_string; +mod shared_uri; +mod style; +mod styled; +mod subscription; +mod svg_renderer; +mod tab_stop; +mod taffy; +#[cfg(any(test, feature = "test-support"))] +pub mod test; +mod text_system; +mod util; +mod view; +mod window; + +#[cfg(doc)] +pub mod _ownership_and_data_flow; + +/// Do not touch, here be dragons for use by gpui_macros and such. +#[doc(hidden)] +pub mod private { + pub use anyhow; + pub use inventory; + pub use schemars; + pub use serde; + pub use serde_json; +} + +mod seal { + /// A mechanism for restricting implementations of a trait to only those in GPUI. + /// See: + pub trait Sealed {} +} + +pub use action::*; +pub use anyhow::Result; +pub use app::*; +pub(crate) use arena::*; +pub use asset_cache::*; +pub use assets::*; +pub use color::*; +pub use ctor::ctor; +pub use element::*; +pub use elements::*; +pub use executor::*; +pub use geometry::*; +pub use global::*; +pub use gpui_macros::{AppContext, IntoElement, Render, VisualContext, register_action, test}; +pub use http_client; +pub use input::*; +pub use inspector::*; +pub use interactive::*; +use key_dispatch::*; +pub use keymap::*; +pub use path_builder::*; +pub use platform::*; +pub use refineable::*; +pub use scene::*; +pub use shared_string::*; +pub use shared_uri::*; +pub use smol::Timer; +pub use style::*; +pub use styled::*; +pub use subscription::*; +use svg_renderer::*; +pub(crate) use tab_stop::*; +pub use taffy::{AvailableSpace, LayoutId}; +#[cfg(any(test, feature = "test-support"))] +pub use test::*; +pub use text_system::*; +#[cfg(any(test, feature = "test-support"))] +pub use util::smol_timeout; +pub use util::{FutureExt, Timeout, arc_cow::ArcCow}; +pub use view::*; +pub use window::*; + +use std::{any::Any, borrow::BorrowMut, future::Future}; +use taffy::TaffyLayoutEngine; + +/// The context trait, allows the different contexts in GPUI to be used +/// interchangeably for certain operations. +pub trait AppContext { + /// The result type for this context, used for async contexts that + /// can't hold a direct reference to the application context. + type Result; + + /// Create a new entity in the app context. + #[expect( + clippy::wrong_self_convention, + reason = "`App::new` is an ubiquitous function for creating entities" + )] + fn new( + &mut self, + build_entity: impl FnOnce(&mut Context) -> T, + ) -> Self::Result>; + + /// Reserve a slot for a entity to be inserted later. + /// The returned [Reservation] allows you to obtain the [EntityId] for the future entity. + fn reserve_entity(&mut self) -> Self::Result>; + + /// Insert a new entity in the app context based on a [Reservation] previously obtained from [`reserve_entity`]. + /// + /// [`reserve_entity`]: Self::reserve_entity + fn insert_entity( + &mut self, + reservation: Reservation, + build_entity: impl FnOnce(&mut Context) -> T, + ) -> Self::Result>; + + /// Update a entity in the app context. + fn update_entity( + &mut self, + handle: &Entity, + update: impl FnOnce(&mut T, &mut Context) -> R, + ) -> Self::Result + where + T: 'static; + + /// Update a entity in the app context. + fn as_mut<'a, T>(&'a mut self, handle: &Entity) -> Self::Result> + where + T: 'static; + + /// Read a entity from the app context. + fn read_entity( + &self, + handle: &Entity, + read: impl FnOnce(&T, &App) -> R, + ) -> Self::Result + where + T: 'static; + + /// Update a window for the given handle. + fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result + where + F: FnOnce(AnyView, &mut Window, &mut App) -> T; + + /// Read a window off of the application context. + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(Entity, &App) -> R, + ) -> Result + where + T: 'static; + + /// Spawn a future on a background thread + fn background_spawn(&self, future: impl Future + Send + 'static) -> Task + where + R: Send + 'static; + + /// Read a global from this app context + fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> Self::Result + where + G: Global; +} + +/// Returned by [Context::reserve_entity] to later be passed to [Context::insert_entity]. +/// Allows you to obtain the [EntityId] for a entity before it is created. +pub struct Reservation(pub(crate) Slot); + +impl Reservation { + /// Returns the [EntityId] that will be associated with the entity once it is inserted. + pub fn entity_id(&self) -> EntityId { + self.0.entity_id() + } +} + +/// This trait is used for the different visual contexts in GPUI that +/// require a window to be present. +pub trait VisualContext: AppContext { + /// Returns the handle of the window associated with this context. + fn window_handle(&self) -> AnyWindowHandle; + + /// Update a view with the given callback + fn update_window_entity( + &mut self, + entity: &Entity, + update: impl FnOnce(&mut T, &mut Window, &mut Context) -> R, + ) -> Self::Result; + + /// Create a new entity, with access to `Window`. + fn new_window_entity( + &mut self, + build_entity: impl FnOnce(&mut Window, &mut Context) -> T, + ) -> Self::Result>; + + /// Replace the root view of a window with a new view. + fn replace_root_view( + &mut self, + build_view: impl FnOnce(&mut Window, &mut Context) -> V, + ) -> Self::Result> + where + V: 'static + Render; + + /// Focus a entity in the window, if it implements the [`Focusable`] trait. + fn focus(&mut self, entity: &Entity) -> Self::Result<()> + where + V: Focusable; +} + +/// A trait for tying together the types of a GPUI entity and the events it can +/// emit. +pub trait EventEmitter: 'static {} + +/// A helper trait for auto-implementing certain methods on contexts that +/// can be used interchangeably. +pub trait BorrowAppContext { + /// Set a global value on the context. + fn set_global(&mut self, global: T); + /// Updates the global state of the given type. + fn update_global(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R + where + G: Global; + /// Updates the global state of the given type, creating a default if it didn't exist before. + fn update_default_global(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R + where + G: Global + Default; +} + +impl BorrowAppContext for C +where + C: BorrowMut, +{ + fn set_global(&mut self, global: G) { + self.borrow_mut().set_global(global) + } + + #[track_caller] + fn update_global(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R + where + G: Global, + { + let mut global = self.borrow_mut().lease_global::(); + let result = f(&mut global, self); + self.borrow_mut().end_global_lease(global); + result + } + + fn update_default_global(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R + where + G: Global + Default, + { + self.borrow_mut().default_global::(); + self.update_global(f) + } +} + +/// A flatten equivalent for anyhow `Result`s. +pub trait Flatten { + /// Convert this type into a simple `Result`. + fn flatten(self) -> Result; +} + +impl Flatten for Result> { + fn flatten(self) -> Result { + self? + } +} + +impl Flatten for Result { + fn flatten(self) -> Result { + self + } +} + +/// Information about the GPU GPUI is running on. +#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone)] +pub struct GpuSpecs { + /// Whether the GPU is really a fake (like `llvmpipe`) running on the CPU. + pub is_software_emulated: bool, + /// The name of the device, as reported by Vulkan. + pub device_name: String, + /// The name of the driver, as reported by Vulkan. + pub driver_name: String, + /// Further information about the driver, as reported by Vulkan. + pub driver_info: String, +} diff --git a/third_party/gpui/src/input.rs b/third_party/gpui/src/input.rs new file mode 100644 index 0000000..dc36ef9 --- /dev/null +++ b/third_party/gpui/src/input.rs @@ -0,0 +1,180 @@ +use crate::{App, Bounds, Context, Entity, InputHandler, Pixels, UTF16Selection, Window}; +use std::ops::Range; + +/// Implement this trait to allow views to handle textual input when implementing an editor, field, etc. +/// +/// Once your view implements this trait, you can use it to construct an [`ElementInputHandler`]. +/// This input handler can then be assigned during paint by calling [`Window::handle_input`]. +/// +/// See [`InputHandler`] for details on how to implement each method. +pub trait EntityInputHandler: 'static + Sized { + /// See [`InputHandler::text_for_range`] for details + fn text_for_range( + &mut self, + range: Range, + adjusted_range: &mut Option>, + window: &mut Window, + cx: &mut Context, + ) -> Option; + + /// See [`InputHandler::selected_text_range`] for details + fn selected_text_range( + &mut self, + ignore_disabled_input: bool, + window: &mut Window, + cx: &mut Context, + ) -> Option; + + /// See [`InputHandler::marked_text_range`] for details + fn marked_text_range( + &self, + window: &mut Window, + cx: &mut Context, + ) -> Option>; + + /// See [`InputHandler::unmark_text`] for details + fn unmark_text(&mut self, window: &mut Window, cx: &mut Context); + + /// See [`InputHandler::replace_text_in_range`] for details + fn replace_text_in_range( + &mut self, + range: Option>, + text: &str, + window: &mut Window, + cx: &mut Context, + ); + + /// See [`InputHandler::replace_and_mark_text_in_range`] for details + fn replace_and_mark_text_in_range( + &mut self, + range: Option>, + new_text: &str, + new_selected_range: Option>, + window: &mut Window, + cx: &mut Context, + ); + + /// See [`InputHandler::bounds_for_range`] for details + fn bounds_for_range( + &mut self, + range_utf16: Range, + element_bounds: Bounds, + window: &mut Window, + cx: &mut Context, + ) -> Option>; + + /// See [`InputHandler::character_index_for_point`] for details + fn character_index_for_point( + &mut self, + point: crate::Point, + window: &mut Window, + cx: &mut Context, + ) -> Option; +} + +/// The canonical implementation of [`crate::PlatformInputHandler`]. Call [`Window::handle_input`] +/// with an instance during your element's paint. +pub struct ElementInputHandler { + view: Entity, + element_bounds: Bounds, +} + +impl ElementInputHandler { + /// Used in [`Element::paint`][element_paint] with the element's bounds, a `Window`, and a `App` context. + /// + /// [element_paint]: crate::Element::paint + pub fn new(element_bounds: Bounds, view: Entity) -> Self { + ElementInputHandler { + view, + element_bounds, + } + } +} + +impl InputHandler for ElementInputHandler { + fn selected_text_range( + &mut self, + ignore_disabled_input: bool, + window: &mut Window, + cx: &mut App, + ) -> Option { + self.view.update(cx, |view, cx| { + view.selected_text_range(ignore_disabled_input, window, cx) + }) + } + + fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option> { + self.view + .update(cx, |view, cx| view.marked_text_range(window, cx)) + } + + fn text_for_range( + &mut self, + range_utf16: Range, + adjusted_range: &mut Option>, + window: &mut Window, + cx: &mut App, + ) -> Option { + self.view.update(cx, |view, cx| { + view.text_for_range(range_utf16, adjusted_range, window, cx) + }) + } + + fn replace_text_in_range( + &mut self, + replacement_range: Option>, + text: &str, + window: &mut Window, + cx: &mut App, + ) { + self.view.update(cx, |view, cx| { + view.replace_text_in_range(replacement_range, text, window, cx) + }); + } + + fn replace_and_mark_text_in_range( + &mut self, + range_utf16: Option>, + new_text: &str, + new_selected_range: Option>, + window: &mut Window, + cx: &mut App, + ) { + self.view.update(cx, |view, cx| { + view.replace_and_mark_text_in_range( + range_utf16, + new_text, + new_selected_range, + window, + cx, + ) + }); + } + + fn unmark_text(&mut self, window: &mut Window, cx: &mut App) { + self.view + .update(cx, |view, cx| view.unmark_text(window, cx)); + } + + fn bounds_for_range( + &mut self, + range_utf16: Range, + window: &mut Window, + cx: &mut App, + ) -> Option> { + self.view.update(cx, |view, cx| { + view.bounds_for_range(range_utf16, self.element_bounds, window, cx) + }) + } + + fn character_index_for_point( + &mut self, + point: crate::Point, + window: &mut Window, + cx: &mut App, + ) -> Option { + self.view.update(cx, |view, cx| { + view.character_index_for_point(point, window, cx) + }) + } +} diff --git a/third_party/gpui/src/inspector.rs b/third_party/gpui/src/inspector.rs new file mode 100644 index 0000000..9f86576 --- /dev/null +++ b/third_party/gpui/src/inspector.rs @@ -0,0 +1,254 @@ +/// A unique identifier for an element that can be inspected. +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +pub struct InspectorElementId { + /// Stable part of the ID. + #[cfg(any(feature = "inspector", debug_assertions))] + pub path: std::rc::Rc, + /// Disambiguates elements that have the same path. + #[cfg(any(feature = "inspector", debug_assertions))] + pub instance_id: usize, +} + +impl Into for &InspectorElementId { + fn into(self) -> InspectorElementId { + self.clone() + } +} + +#[cfg(any(feature = "inspector", debug_assertions))] +pub use conditional::*; + +#[cfg(any(feature = "inspector", debug_assertions))] +mod conditional { + use super::*; + use crate::{AnyElement, App, Context, Empty, IntoElement, Render, Window}; + use collections::FxHashMap; + use std::any::{Any, TypeId}; + + /// `GlobalElementId` qualified by source location of element construction. + #[derive(Debug, Eq, PartialEq, Hash)] + pub struct InspectorElementPath { + /// The path to the nearest ancestor element that has an `ElementId`. + #[cfg(any(feature = "inspector", debug_assertions))] + pub global_id: crate::GlobalElementId, + /// Source location where this element was constructed. + #[cfg(any(feature = "inspector", debug_assertions))] + pub source_location: &'static std::panic::Location<'static>, + } + + impl Clone for InspectorElementPath { + fn clone(&self) -> Self { + Self { + global_id: crate::GlobalElementId(self.global_id.0.clone()), + source_location: self.source_location, + } + } + } + + impl Into for &InspectorElementPath { + fn into(self) -> InspectorElementPath { + self.clone() + } + } + + /// Function set on `App` to render the inspector UI. + pub type InspectorRenderer = + Box) -> AnyElement>; + + /// Manages inspector state - which element is currently selected and whether the inspector is + /// in picking mode. + pub struct Inspector { + active_element: Option, + pub(crate) pick_depth: Option, + } + + struct InspectedElement { + id: InspectorElementId, + states: FxHashMap>, + } + + impl InspectedElement { + fn new(id: InspectorElementId) -> Self { + InspectedElement { + id, + states: FxHashMap::default(), + } + } + } + + impl Inspector { + pub(crate) fn new() -> Self { + Self { + active_element: None, + pick_depth: Some(0.0), + } + } + + pub(crate) fn select(&mut self, id: InspectorElementId, window: &mut Window) { + self.set_active_element_id(id, window); + self.pick_depth = None; + } + + pub(crate) fn hover(&mut self, id: InspectorElementId, window: &mut Window) { + if self.is_picking() { + let changed = self.set_active_element_id(id, window); + if changed { + self.pick_depth = Some(0.0); + } + } + } + + pub(crate) fn set_active_element_id( + &mut self, + id: InspectorElementId, + window: &mut Window, + ) -> bool { + let changed = Some(&id) != self.active_element_id(); + if changed { + self.active_element = Some(InspectedElement::new(id)); + window.refresh(); + } + changed + } + + /// ID of the currently hovered or selected element. + pub fn active_element_id(&self) -> Option<&InspectorElementId> { + self.active_element.as_ref().map(|e| &e.id) + } + + pub(crate) fn with_active_element_state( + &mut self, + window: &mut Window, + f: impl FnOnce(&mut Option, &mut Window) -> R, + ) -> R { + let Some(active_element) = &mut self.active_element else { + return f(&mut None, window); + }; + + let type_id = TypeId::of::(); + let mut inspector_state = active_element + .states + .remove(&type_id) + .map(|state| *state.downcast().unwrap()); + + let result = f(&mut inspector_state, window); + + if let Some(inspector_state) = inspector_state { + active_element + .states + .insert(type_id, Box::new(inspector_state)); + } + + result + } + + /// Starts element picking mode, allowing the user to select elements by clicking. + pub fn start_picking(&mut self) { + self.pick_depth = Some(0.0); + } + + /// Returns whether the inspector is currently in picking mode. + pub fn is_picking(&self) -> bool { + self.pick_depth.is_some() + } + + /// Renders elements for all registered inspector states of the active inspector element. + pub fn render_inspector_states( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> Vec { + let mut elements = Vec::new(); + if let Some(active_element) = self.active_element.take() { + for (type_id, state) in &active_element.states { + if let Some(render_inspector) = cx + .inspector_element_registry + .renderers_by_type_id + .remove(type_id) + { + let mut element = (render_inspector)( + active_element.id.clone(), + state.as_ref(), + window, + cx, + ); + elements.push(element); + cx.inspector_element_registry + .renderers_by_type_id + .insert(*type_id, render_inspector); + } + } + + self.active_element = Some(active_element); + } + + elements + } + } + + impl Render for Inspector { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + if let Some(inspector_renderer) = cx.inspector_renderer.take() { + let result = inspector_renderer(self, window, cx); + cx.inspector_renderer = Some(inspector_renderer); + result + } else { + Empty.into_any_element() + } + } + } + + #[derive(Default)] + pub(crate) struct InspectorElementRegistry { + renderers_by_type_id: FxHashMap< + TypeId, + Box AnyElement>, + >, + } + + impl InspectorElementRegistry { + pub fn register( + &mut self, + f: impl 'static + Fn(InspectorElementId, &T, &mut Window, &mut App) -> R, + ) { + self.renderers_by_type_id.insert( + TypeId::of::(), + Box::new(move |id, value, window, cx| { + let value = value.downcast_ref().unwrap(); + f(id, value, window, cx).into_any_element() + }), + ); + } + } +} + +/// Provides definitions used by `#[derive_inspector_reflection]`. +#[cfg(any(feature = "inspector", debug_assertions))] +pub mod inspector_reflection { + use std::any::Any; + + /// Reification of a function that has the signature `fn some_fn(T) -> T`. Provides the name, + /// documentation, and ability to invoke the function. + #[derive(Clone, Copy)] + pub struct FunctionReflection { + /// The name of the function + pub name: &'static str, + /// The method + pub function: fn(Box) -> Box, + /// Documentation for the function + pub documentation: Option<&'static str>, + /// `PhantomData` for the type of the argument and result + pub _type: std::marker::PhantomData, + } + + impl FunctionReflection { + /// Invoke this method on a value and return the result. + pub fn invoke(&self, value: T) -> T { + let boxed = Box::new(value) as Box; + let result = (self.function)(boxed); + *result + .downcast::() + .expect("Type mismatch in reflection invoke") + } + } +} diff --git a/third_party/gpui/src/interactive.rs b/third_party/gpui/src/interactive.rs new file mode 100644 index 0000000..dafe623 --- /dev/null +++ b/third_party/gpui/src/interactive.rs @@ -0,0 +1,670 @@ +use crate::{ + Bounds, Capslock, Context, Empty, IntoElement, Keystroke, Modifiers, Pixels, Point, Render, + Window, point, seal::Sealed, +}; +use smallvec::SmallVec; +use std::{any::Any, fmt::Debug, ops::Deref, path::PathBuf}; + +/// An event from a platform input source. +pub trait InputEvent: Sealed + 'static { + /// Convert this event into the platform input enum. + fn to_platform_input(self) -> PlatformInput; +} + +/// A key event from the platform. +pub trait KeyEvent: InputEvent {} + +/// A mouse event from the platform. +pub trait MouseEvent: InputEvent {} + +/// The key down event equivalent for the platform. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct KeyDownEvent { + /// The keystroke that was generated. + pub keystroke: Keystroke, + + /// Whether the key is currently held down. + pub is_held: bool, +} + +impl Sealed for KeyDownEvent {} +impl InputEvent for KeyDownEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::KeyDown(self) + } +} +impl KeyEvent for KeyDownEvent {} + +/// The key up event equivalent for the platform. +#[derive(Clone, Debug)] +pub struct KeyUpEvent { + /// The keystroke that was released. + pub keystroke: Keystroke, +} + +impl Sealed for KeyUpEvent {} +impl InputEvent for KeyUpEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::KeyUp(self) + } +} +impl KeyEvent for KeyUpEvent {} + +/// The modifiers changed event equivalent for the platform. +#[derive(Clone, Debug, Default)] +pub struct ModifiersChangedEvent { + /// The new state of the modifier keys + pub modifiers: Modifiers, + /// The new state of the capslock key + pub capslock: Capslock, +} + +impl Sealed for ModifiersChangedEvent {} +impl InputEvent for ModifiersChangedEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::ModifiersChanged(self) + } +} +impl KeyEvent for ModifiersChangedEvent {} + +impl Deref for ModifiersChangedEvent { + type Target = Modifiers; + + fn deref(&self) -> &Self::Target { + &self.modifiers + } +} + +/// The phase of a touch motion event. +/// Based on the winit enum of the same name. +#[derive(Clone, Copy, Debug, Default)] +pub enum TouchPhase { + /// The touch started. + Started, + /// The touch event is moving. + #[default] + Moved, + /// The touch phase has ended + Ended, +} + +/// A mouse down event from the platform +#[derive(Clone, Debug, Default)] +pub struct MouseDownEvent { + /// Which mouse button was pressed. + pub button: MouseButton, + + /// The position of the mouse on the window. + pub position: Point, + + /// The modifiers that were held down when the mouse was pressed. + pub modifiers: Modifiers, + + /// The number of times the button has been clicked. + pub click_count: usize, + + /// Whether this is the first, focusing click. + pub first_mouse: bool, +} + +impl Sealed for MouseDownEvent {} +impl InputEvent for MouseDownEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::MouseDown(self) + } +} +impl MouseEvent for MouseDownEvent {} + +/// A mouse up event from the platform +#[derive(Clone, Debug, Default)] +pub struct MouseUpEvent { + /// Which mouse button was released. + pub button: MouseButton, + + /// The position of the mouse on the window. + pub position: Point, + + /// The modifiers that were held down when the mouse was released. + pub modifiers: Modifiers, + + /// The number of times the button has been clicked. + pub click_count: usize, +} + +impl Sealed for MouseUpEvent {} +impl InputEvent for MouseUpEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::MouseUp(self) + } +} +impl MouseEvent for MouseUpEvent {} + +/// A click event, generated when a mouse button is pressed and released. +#[derive(Clone, Debug, Default)] +pub struct MouseClickEvent { + /// The mouse event when the button was pressed. + pub down: MouseDownEvent, + + /// The mouse event when the button was released. + pub up: MouseUpEvent, +} + +/// A click event that was generated by a keyboard button being pressed and released. +#[derive(Clone, Debug, Default)] +pub struct KeyboardClickEvent { + /// The keyboard button that was pressed to trigger the click. + pub button: KeyboardButton, + + /// The bounds of the element that was clicked. + pub bounds: Bounds, +} + +/// A click event, generated when a mouse button or keyboard button is pressed and released. +#[derive(Clone, Debug)] +pub enum ClickEvent { + /// A click event trigger by a mouse button being pressed and released. + Mouse(MouseClickEvent), + /// A click event trigger by a keyboard button being pressed and released. + Keyboard(KeyboardClickEvent), +} + +impl Default for ClickEvent { + fn default() -> Self { + ClickEvent::Keyboard(KeyboardClickEvent::default()) + } +} + +impl ClickEvent { + /// Returns the modifiers that were held during the click event + /// + /// `Keyboard`: The keyboard click events never have modifiers. + /// `Mouse`: Modifiers that were held during the mouse key up event. + pub fn modifiers(&self) -> Modifiers { + match self { + // Click events are only generated from keyboard events _without any modifiers_, so we know the modifiers are always Default + ClickEvent::Keyboard(_) => Modifiers::default(), + // Click events on the web only reflect the modifiers for the keyup event, + // tested via observing the behavior of the `ClickEvent.shiftKey` field in Chrome 138 + // under various combinations of modifiers and keyUp / keyDown events. + ClickEvent::Mouse(event) => event.up.modifiers, + } + } + + /// Returns the position of the click event + /// + /// `Keyboard`: The bottom left corner of the clicked hitbox + /// `Mouse`: The position of the mouse when the button was released. + pub fn position(&self) -> Point { + match self { + ClickEvent::Keyboard(event) => event.bounds.bottom_left(), + ClickEvent::Mouse(event) => event.up.position, + } + } + + /// Returns the mouse position of the click event + /// + /// `Keyboard`: None + /// `Mouse`: The position of the mouse when the button was released. + pub fn mouse_position(&self) -> Option> { + match self { + ClickEvent::Keyboard(_) => None, + ClickEvent::Mouse(event) => Some(event.up.position), + } + } + + /// Returns if this was a right click + /// + /// `Keyboard`: false + /// `Mouse`: Whether the right button was pressed and released + pub fn is_right_click(&self) -> bool { + match self { + ClickEvent::Keyboard(_) => false, + ClickEvent::Mouse(event) => { + event.down.button == MouseButton::Right && event.up.button == MouseButton::Right + } + } + } + + /// Returns whether the click was a standard click + /// + /// `Keyboard`: Always true + /// `Mouse`: Left button pressed and released + pub fn standard_click(&self) -> bool { + match self { + ClickEvent::Keyboard(_) => true, + ClickEvent::Mouse(event) => { + event.down.button == MouseButton::Left && event.up.button == MouseButton::Left + } + } + } + + /// Returns whether the click focused the element + /// + /// `Keyboard`: false, keyboard clicks only work if an element is already focused + /// `Mouse`: Whether this was the first focusing click + pub fn first_focus(&self) -> bool { + match self { + ClickEvent::Keyboard(_) => false, + ClickEvent::Mouse(event) => event.down.first_mouse, + } + } + + /// Returns the click count of the click event + /// + /// `Keyboard`: Always 1 + /// `Mouse`: Count of clicks from MouseUpEvent + pub fn click_count(&self) -> usize { + match self { + ClickEvent::Keyboard(_) => 1, + ClickEvent::Mouse(event) => event.up.click_count, + } + } + + /// Returns whether the click event is generated by a keyboard event + pub fn is_keyboard(&self) -> bool { + match self { + ClickEvent::Mouse(_) => false, + ClickEvent::Keyboard(_) => true, + } + } +} + +/// An enum representing the keyboard button that was pressed for a click event. +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, Default)] +pub enum KeyboardButton { + /// Enter key was clicked + #[default] + Enter, + /// Space key was clicked + Space, +} + +/// An enum representing the mouse button that was pressed. +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)] +pub enum MouseButton { + /// The left mouse button. + Left, + + /// The right mouse button. + Right, + + /// The middle mouse button. + Middle, + + /// A navigation button, such as back or forward. + Navigate(NavigationDirection), +} + +impl MouseButton { + /// Get all the mouse buttons in a list. + pub fn all() -> Vec { + vec![ + MouseButton::Left, + MouseButton::Right, + MouseButton::Middle, + MouseButton::Navigate(NavigationDirection::Back), + MouseButton::Navigate(NavigationDirection::Forward), + ] + } +} + +impl Default for MouseButton { + fn default() -> Self { + Self::Left + } +} + +/// A navigation direction, such as back or forward. +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)] +pub enum NavigationDirection { + /// The back button. + Back, + + /// The forward button. + Forward, +} + +impl Default for NavigationDirection { + fn default() -> Self { + Self::Back + } +} + +/// A mouse move event from the platform +#[derive(Clone, Debug, Default)] +pub struct MouseMoveEvent { + /// The position of the mouse on the window. + pub position: Point, + + /// The mouse button that was pressed, if any. + pub pressed_button: Option, + + /// The modifiers that were held down when the mouse was moved. + pub modifiers: Modifiers, +} + +impl Sealed for MouseMoveEvent {} +impl InputEvent for MouseMoveEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::MouseMove(self) + } +} +impl MouseEvent for MouseMoveEvent {} + +impl MouseMoveEvent { + /// Returns true if the left mouse button is currently held down. + pub fn dragging(&self) -> bool { + self.pressed_button == Some(MouseButton::Left) + } +} + +/// A mouse wheel event from the platform +#[derive(Clone, Debug, Default)] +pub struct ScrollWheelEvent { + /// The position of the mouse on the window. + pub position: Point, + + /// The change in scroll wheel position for this event. + pub delta: ScrollDelta, + + /// The modifiers that were held down when the mouse was moved. + pub modifiers: Modifiers, + + /// The phase of the touch event. + pub touch_phase: TouchPhase, +} + +impl Sealed for ScrollWheelEvent {} +impl InputEvent for ScrollWheelEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::ScrollWheel(self) + } +} +impl MouseEvent for ScrollWheelEvent {} + +impl Deref for ScrollWheelEvent { + type Target = Modifiers; + + fn deref(&self) -> &Self::Target { + &self.modifiers + } +} + +/// The scroll delta for a scroll wheel event. +#[derive(Clone, Copy, Debug)] +pub enum ScrollDelta { + /// An exact scroll delta in pixels. + Pixels(Point), + /// An inexact scroll delta in lines. + Lines(Point), +} + +impl Default for ScrollDelta { + fn default() -> Self { + Self::Lines(Default::default()) + } +} + +impl ScrollDelta { + /// Returns true if this is a precise scroll delta in pixels. + pub fn precise(&self) -> bool { + match self { + ScrollDelta::Pixels(_) => true, + ScrollDelta::Lines(_) => false, + } + } + + /// Converts this scroll event into exact pixels. + pub fn pixel_delta(&self, line_height: Pixels) -> Point { + match self { + ScrollDelta::Pixels(delta) => *delta, + ScrollDelta::Lines(delta) => point(line_height * delta.x, line_height * delta.y), + } + } + + /// Combines two scroll deltas into one. + /// If the signs of the deltas are the same (both positive or both negative), + /// the deltas are added together. If the signs are opposite, the second delta + /// (other) is used, effectively overriding the first delta. + pub fn coalesce(self, other: ScrollDelta) -> ScrollDelta { + match (self, other) { + (ScrollDelta::Pixels(a), ScrollDelta::Pixels(b)) => { + let x = if a.x.signum() == b.x.signum() { + a.x + b.x + } else { + b.x + }; + + let y = if a.y.signum() == b.y.signum() { + a.y + b.y + } else { + b.y + }; + + ScrollDelta::Pixels(point(x, y)) + } + + (ScrollDelta::Lines(a), ScrollDelta::Lines(b)) => { + let x = if a.x.signum() == b.x.signum() { + a.x + b.x + } else { + b.x + }; + + let y = if a.y.signum() == b.y.signum() { + a.y + b.y + } else { + b.y + }; + + ScrollDelta::Lines(point(x, y)) + } + + _ => other, + } + } +} + +/// A mouse exit event from the platform, generated when the mouse leaves the window. +#[derive(Clone, Debug, Default)] +pub struct MouseExitEvent { + /// The position of the mouse relative to the window. + pub position: Point, + /// The mouse button that was pressed, if any. + pub pressed_button: Option, + /// The modifiers that were held down when the mouse was moved. + pub modifiers: Modifiers, +} + +impl Sealed for MouseExitEvent {} +impl InputEvent for MouseExitEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::MouseExited(self) + } +} +impl MouseEvent for MouseExitEvent {} + +impl Deref for MouseExitEvent { + type Target = Modifiers; + + fn deref(&self) -> &Self::Target { + &self.modifiers + } +} + +/// A collection of paths from the platform, such as from a file drop. +#[derive(Debug, Clone, Default)] +pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>); + +impl ExternalPaths { + /// Convert this collection of paths into a slice. + pub fn paths(&self) -> &[PathBuf] { + &self.0 + } +} + +impl Render for ExternalPaths { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + // the platform will render icons for the dragged files + Empty + } +} + +/// A file drop event from the platform, generated when files are dragged and dropped onto the window. +#[derive(Debug, Clone)] +pub enum FileDropEvent { + /// The files have entered the window. + Entered { + /// The position of the mouse relative to the window. + position: Point, + /// The paths of the files that are being dragged. + paths: ExternalPaths, + }, + /// The files are being dragged over the window + Pending { + /// The position of the mouse relative to the window. + position: Point, + }, + /// The files have been dropped onto the window. + Submit { + /// The position of the mouse relative to the window. + position: Point, + }, + /// The user has stopped dragging the files over the window. + Exited, +} + +impl Sealed for FileDropEvent {} +impl InputEvent for FileDropEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::FileDrop(self) + } +} +impl MouseEvent for FileDropEvent {} + +/// An enum corresponding to all kinds of platform input events. +#[derive(Clone, Debug)] +pub enum PlatformInput { + /// A key was pressed. + KeyDown(KeyDownEvent), + /// A key was released. + KeyUp(KeyUpEvent), + /// The keyboard modifiers were changed. + ModifiersChanged(ModifiersChangedEvent), + /// The mouse was pressed. + MouseDown(MouseDownEvent), + /// The mouse was released. + MouseUp(MouseUpEvent), + /// The mouse was moved. + MouseMove(MouseMoveEvent), + /// The mouse exited the window. + MouseExited(MouseExitEvent), + /// The scroll wheel was used. + ScrollWheel(ScrollWheelEvent), + /// Files were dragged and dropped onto the window. + FileDrop(FileDropEvent), +} + +impl PlatformInput { + pub(crate) fn mouse_event(&self) -> Option<&dyn Any> { + match self { + PlatformInput::KeyDown { .. } => None, + PlatformInput::KeyUp { .. } => None, + PlatformInput::ModifiersChanged { .. } => None, + PlatformInput::MouseDown(event) => Some(event), + PlatformInput::MouseUp(event) => Some(event), + PlatformInput::MouseMove(event) => Some(event), + PlatformInput::MouseExited(event) => Some(event), + PlatformInput::ScrollWheel(event) => Some(event), + PlatformInput::FileDrop(event) => Some(event), + } + } + + pub(crate) fn keyboard_event(&self) -> Option<&dyn Any> { + match self { + PlatformInput::KeyDown(event) => Some(event), + PlatformInput::KeyUp(event) => Some(event), + PlatformInput::ModifiersChanged(event) => Some(event), + PlatformInput::MouseDown(_) => None, + PlatformInput::MouseUp(_) => None, + PlatformInput::MouseMove(_) => None, + PlatformInput::MouseExited(_) => None, + PlatformInput::ScrollWheel(_) => None, + PlatformInput::FileDrop(_) => None, + } + } +} + +#[cfg(test)] +mod test { + + use crate::{ + self as gpui, AppContext as _, Context, FocusHandle, InteractiveElement, IntoElement, + KeyBinding, Keystroke, ParentElement, Render, TestAppContext, Window, div, + }; + + struct TestView { + saw_key_down: bool, + saw_action: bool, + focus_handle: FocusHandle, + } + + actions!(test_only, [TestAction]); + + impl Render for TestView { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + div().id("testview").child( + div() + .key_context("parent") + .on_key_down(cx.listener(|this, _, _, cx| { + cx.stop_propagation(); + this.saw_key_down = true + })) + .on_action(cx.listener(|this: &mut TestView, _: &TestAction, _, _| { + this.saw_action = true + })) + .child( + div() + .key_context("nested") + .track_focus(&self.focus_handle) + .into_element(), + ), + ) + } + } + + #[gpui::test] + fn test_on_events(cx: &mut TestAppContext) { + let window = cx.update(|cx| { + cx.open_window(Default::default(), |_, cx| { + cx.new(|cx| TestView { + saw_key_down: false, + saw_action: false, + focus_handle: cx.focus_handle(), + }) + }) + .unwrap() + }); + + cx.update(|cx| { + cx.bind_keys(vec![KeyBinding::new("ctrl-g", TestAction, Some("parent"))]); + }); + + window + .update(cx, |test_view, window, _cx| { + window.focus(&test_view.focus_handle) + }) + .unwrap(); + + cx.dispatch_keystroke(*window, Keystroke::parse("a").unwrap()); + cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").unwrap()); + + window + .update(cx, |test_view, _, _| { + assert!(test_view.saw_key_down || test_view.saw_action); + assert!(test_view.saw_key_down); + assert!(test_view.saw_action); + }) + .unwrap(); + } +} diff --git a/third_party/gpui/src/key_dispatch.rs b/third_party/gpui/src/key_dispatch.rs new file mode 100644 index 0000000..03ee31f --- /dev/null +++ b/third_party/gpui/src/key_dispatch.rs @@ -0,0 +1,843 @@ +//! KeyDispatch is where GPUI deals with binding actions to key events. +//! +//! The key pieces to making a key binding work are to define an action, +//! implement a method that takes that action as a type parameter, +//! and then to register the action during render on a focused node +//! with a keymap context: +//! +//! ```ignore +//! actions!(editor,[Undo, Redo]); +//! +//! impl Editor { +//! fn undo(&mut self, _: &Undo, _window: &mut Window, _cx: &mut Context) { ... } +//! fn redo(&mut self, _: &Redo, _window: &mut Window, _cx: &mut Context) { ... } +//! } +//! +//! impl Render for Editor { +//! fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { +//! div() +//! .track_focus(&self.focus_handle(cx)) +//! .key_context("Editor") +//! .on_action(cx.listener(Editor::undo)) +//! .on_action(cx.listener(Editor::redo)) +//! ... +//! } +//! } +//!``` +//! +//! The keybindings themselves are managed independently by calling cx.bind_keys(). +//! (Though mostly when developing Zed itself, you just need to add a new line to +//! assets/keymaps/default-{platform}.json). +//! +//! ```ignore +//! cx.bind_keys([ +//! KeyBinding::new("cmd-z", Editor::undo, Some("Editor")), +//! KeyBinding::new("cmd-shift-z", Editor::redo, Some("Editor")), +//! ]) +//! ``` +//! +//! With all of this in place, GPUI will ensure that if you have an Editor that contains +//! the focus, hitting cmd-z will Undo. +//! +//! In real apps, it is a little more complicated than this, because typically you have +//! several nested views that each register keyboard handlers. In this case action matching +//! bubbles up from the bottom. For example in Zed, the Workspace is the top-level view, which contains Pane's, which contain Editors. If there are conflicting keybindings defined +//! then the Editor's bindings take precedence over the Pane's bindings, which take precedence over the Workspace. +//! +//! In GPUI, keybindings are not limited to just single keystrokes, you can define +//! sequences by separating the keys with a space: +//! +//! KeyBinding::new("cmd-k left", pane::SplitLeft, Some("Pane")) + +use crate::{ + Action, ActionRegistry, App, DispatchPhase, EntityId, FocusId, KeyBinding, KeyContext, Keymap, + Keystroke, ModifiersChangedEvent, Window, +}; +use collections::FxHashMap; +use smallvec::SmallVec; +use std::{ + any::{Any, TypeId}, + cell::RefCell, + mem, + ops::Range, + rc::Rc, +}; + +/// ID of a node within `DispatchTree`. Note that these are **not** stable between frames, and so a +/// `DispatchNodeId` should only be used with the `DispatchTree` that provided it. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub(crate) struct DispatchNodeId(usize); + +pub(crate) struct DispatchTree { + node_stack: Vec, + pub(crate) context_stack: Vec, + view_stack: Vec, + nodes: Vec, + focusable_node_ids: FxHashMap, + view_node_ids: FxHashMap, + keymap: Rc>, + action_registry: Rc, +} + +#[derive(Default)] +pub(crate) struct DispatchNode { + pub key_listeners: Vec, + pub action_listeners: Vec, + pub modifiers_changed_listeners: Vec, + pub context: Option, + pub focus_id: Option, + view_id: Option, + parent: Option, +} + +pub(crate) struct ReusedSubtree { + old_range: Range, + new_range: Range, + contains_focus: bool, +} + +impl ReusedSubtree { + pub fn refresh_node_id(&self, node_id: DispatchNodeId) -> DispatchNodeId { + debug_assert!( + self.old_range.contains(&node_id.0), + "node {} was not part of the reused subtree {:?}", + node_id.0, + self.old_range + ); + DispatchNodeId((node_id.0 - self.old_range.start) + self.new_range.start) + } + + pub fn contains_focus(&self) -> bool { + self.contains_focus + } +} + +#[derive(Default, Debug)] +pub(crate) struct Replay { + pub(crate) keystroke: Keystroke, + pub(crate) bindings: SmallVec<[KeyBinding; 1]>, +} + +#[derive(Default, Debug)] +pub(crate) struct DispatchResult { + pub(crate) pending: SmallVec<[Keystroke; 1]>, + pub(crate) bindings: SmallVec<[KeyBinding; 1]>, + pub(crate) to_replay: SmallVec<[Replay; 1]>, + pub(crate) context_stack: Vec, +} + +type KeyListener = Rc; +type ModifiersChangedListener = Rc; + +#[derive(Clone)] +pub(crate) struct DispatchActionListener { + pub(crate) action_type: TypeId, + pub(crate) listener: Rc, +} + +impl DispatchTree { + pub fn new(keymap: Rc>, action_registry: Rc) -> Self { + Self { + node_stack: Vec::new(), + context_stack: Vec::new(), + view_stack: Vec::new(), + nodes: Vec::new(), + focusable_node_ids: FxHashMap::default(), + view_node_ids: FxHashMap::default(), + keymap, + action_registry, + } + } + + pub fn clear(&mut self) { + self.node_stack.clear(); + self.context_stack.clear(); + self.view_stack.clear(); + self.nodes.clear(); + self.focusable_node_ids.clear(); + self.view_node_ids.clear(); + } + + pub fn len(&self) -> usize { + self.nodes.len() + } + + pub fn push_node(&mut self) -> DispatchNodeId { + let parent = self.node_stack.last().copied(); + let node_id = DispatchNodeId(self.nodes.len()); + + self.nodes.push(DispatchNode { + parent, + ..Default::default() + }); + self.node_stack.push(node_id); + node_id + } + + pub fn set_active_node(&mut self, node_id: DispatchNodeId) { + let next_node_parent = self.nodes[node_id.0].parent; + while self.node_stack.last().copied() != next_node_parent && !self.node_stack.is_empty() { + self.pop_node(); + } + + if self.node_stack.last().copied() == next_node_parent { + self.node_stack.push(node_id); + let active_node = &self.nodes[node_id.0]; + if let Some(view_id) = active_node.view_id { + self.view_stack.push(view_id) + } + if let Some(context) = active_node.context.clone() { + self.context_stack.push(context); + } + } else { + debug_assert_eq!(self.node_stack.len(), 0); + + let mut current_node_id = Some(node_id); + while let Some(node_id) = current_node_id { + let node = &self.nodes[node_id.0]; + if let Some(context) = node.context.clone() { + self.context_stack.push(context); + } + if node.view_id.is_some() { + self.view_stack.push(node.view_id.unwrap()); + } + self.node_stack.push(node_id); + current_node_id = node.parent; + } + + self.context_stack.reverse(); + self.view_stack.reverse(); + self.node_stack.reverse(); + } + } + + pub fn set_key_context(&mut self, context: KeyContext) { + self.active_node().context = Some(context.clone()); + self.context_stack.push(context); + } + + pub fn set_focus_id(&mut self, focus_id: FocusId) { + let node_id = *self.node_stack.last().unwrap(); + self.nodes[node_id.0].focus_id = Some(focus_id); + self.focusable_node_ids.insert(focus_id, node_id); + } + + pub fn set_view_id(&mut self, view_id: EntityId) { + if self.view_stack.last().copied() != Some(view_id) { + let node_id = *self.node_stack.last().unwrap(); + self.nodes[node_id.0].view_id = Some(view_id); + self.view_node_ids.insert(view_id, node_id); + self.view_stack.push(view_id); + } + } + + pub fn pop_node(&mut self) { + let node = &self.nodes[self.active_node_id().unwrap().0]; + if node.context.is_some() { + self.context_stack.pop(); + } + if node.view_id.is_some() { + self.view_stack.pop(); + } + self.node_stack.pop(); + } + + fn move_node(&mut self, source: &mut DispatchNode) { + self.push_node(); + if let Some(context) = source.context.clone() { + self.set_key_context(context); + } + if let Some(focus_id) = source.focus_id { + self.set_focus_id(focus_id); + } + if let Some(view_id) = source.view_id { + self.set_view_id(view_id); + } + + let target = self.active_node(); + target.key_listeners = mem::take(&mut source.key_listeners); + target.action_listeners = mem::take(&mut source.action_listeners); + target.modifiers_changed_listeners = mem::take(&mut source.modifiers_changed_listeners); + } + + pub fn reuse_subtree( + &mut self, + old_range: Range, + source: &mut Self, + focus: Option, + ) -> ReusedSubtree { + let new_range = self.nodes.len()..self.nodes.len() + old_range.len(); + + let mut contains_focus = false; + let mut source_stack = vec![]; + for (source_node_id, source_node) in source + .nodes + .iter_mut() + .enumerate() + .skip(old_range.start) + .take(old_range.len()) + { + let source_node_id = DispatchNodeId(source_node_id); + while let Some(source_ancestor) = source_stack.last() { + if source_node.parent == Some(*source_ancestor) { + break; + } else { + source_stack.pop(); + self.pop_node(); + } + } + + source_stack.push(source_node_id); + if source_node.focus_id.is_some() && source_node.focus_id == focus { + contains_focus = true; + } + self.move_node(source_node); + } + + while !source_stack.is_empty() { + source_stack.pop(); + self.pop_node(); + } + + ReusedSubtree { + old_range, + new_range, + contains_focus, + } + } + + pub fn truncate(&mut self, index: usize) { + for node in &self.nodes[index..] { + if let Some(focus_id) = node.focus_id { + self.focusable_node_ids.remove(&focus_id); + } + + if let Some(view_id) = node.view_id { + self.view_node_ids.remove(&view_id); + } + } + self.nodes.truncate(index); + } + + pub fn on_key_event(&mut self, listener: KeyListener) { + self.active_node().key_listeners.push(listener); + } + + pub fn on_modifiers_changed(&mut self, listener: ModifiersChangedListener) { + self.active_node() + .modifiers_changed_listeners + .push(listener); + } + + pub fn on_action( + &mut self, + action_type: TypeId, + listener: Rc, + ) { + self.active_node() + .action_listeners + .push(DispatchActionListener { + action_type, + listener, + }); + } + + pub fn focus_contains(&self, parent: FocusId, child: FocusId) -> bool { + if parent == child { + return true; + } + + if let Some(parent_node_id) = self.focusable_node_ids.get(&parent) { + let mut current_node_id = self.focusable_node_ids.get(&child).copied(); + while let Some(node_id) = current_node_id { + if node_id == *parent_node_id { + return true; + } + current_node_id = self.nodes[node_id.0].parent; + } + } + false + } + + pub fn available_actions(&self, target: DispatchNodeId) -> Vec> { + let mut actions = Vec::>::new(); + for node_id in self.dispatch_path(target) { + let node = &self.nodes[node_id.0]; + for DispatchActionListener { action_type, .. } in &node.action_listeners { + if let Err(ix) = actions.binary_search_by_key(action_type, |a| a.as_any().type_id()) + { + // Intentionally silence these errors without logging. + // If an action cannot be built by default, it's not available. + let action = self.action_registry.build_action_type(action_type).ok(); + if let Some(action) = action { + actions.insert(ix, action); + } + } + } + } + actions + } + + pub fn is_action_available(&self, action: &dyn Action, target: DispatchNodeId) -> bool { + for node_id in self.dispatch_path(target) { + let node = &self.nodes[node_id.0]; + if node + .action_listeners + .iter() + .any(|listener| listener.action_type == action.as_any().type_id()) + { + return true; + } + } + false + } + + /// Returns key bindings that invoke an action on the currently focused element. Bindings are + /// returned in the order they were added. For display, the last binding should take precedence. + /// + /// Bindings are only included if they are the highest precedence match for their keystrokes, so + /// shadowed bindings are not included. + pub fn bindings_for_action( + &self, + action: &dyn Action, + context_stack: &[KeyContext], + ) -> Vec { + // Ideally this would return a `DoubleEndedIterator` to avoid `highest_precedence_*` + // methods, but this can't be done very cleanly since keymap must be borrowed. + let keymap = self.keymap.borrow(); + keymap + .bindings_for_action(action) + .filter(|binding| { + Self::binding_matches_predicate_and_not_shadowed(&keymap, binding, context_stack) + }) + .cloned() + .collect() + } + + /// Returns the highest precedence binding for the given action and context stack. This is the + /// same as the last result of `bindings_for_action`, but more efficient than getting all bindings. + pub fn highest_precedence_binding_for_action( + &self, + action: &dyn Action, + context_stack: &[KeyContext], + ) -> Option { + let keymap = self.keymap.borrow(); + keymap + .bindings_for_action(action) + .rev() + .find(|binding| { + Self::binding_matches_predicate_and_not_shadowed(&keymap, binding, context_stack) + }) + .cloned() + } + + fn binding_matches_predicate_and_not_shadowed( + keymap: &Keymap, + binding: &KeyBinding, + context_stack: &[KeyContext], + ) -> bool { + let (bindings, _) = keymap.bindings_for_input(&binding.keystrokes, context_stack); + if let Some(found) = bindings.iter().next() { + found.action.partial_eq(binding.action.as_ref()) + } else { + false + } + } + + fn bindings_for_input( + &self, + input: &[Keystroke], + dispatch_path: &SmallVec<[DispatchNodeId; 32]>, + ) -> (SmallVec<[KeyBinding; 1]>, bool, Vec) { + let context_stack: Vec = dispatch_path + .iter() + .filter_map(|node_id| self.node(*node_id).context.clone()) + .collect(); + + let (bindings, partial) = self + .keymap + .borrow() + .bindings_for_input(input, &context_stack); + (bindings, partial, context_stack) + } + + /// dispatch_key processes the keystroke + /// input should be set to the value of `pending` from the previous call to dispatch_key. + /// This returns three instructions to the input handler: + /// - bindings: any bindings to execute before processing this keystroke + /// - pending: the new set of pending keystrokes to store + /// - to_replay: any keystroke that had been pushed to pending, but are no-longer matched, + /// these should be replayed first. + pub fn dispatch_key( + &mut self, + mut input: SmallVec<[Keystroke; 1]>, + keystroke: Keystroke, + dispatch_path: &SmallVec<[DispatchNodeId; 32]>, + ) -> DispatchResult { + input.push(keystroke.clone()); + let (bindings, pending, context_stack) = self.bindings_for_input(&input, dispatch_path); + + if pending { + return DispatchResult { + pending: input, + context_stack, + ..Default::default() + }; + } else if !bindings.is_empty() { + return DispatchResult { + bindings, + context_stack, + ..Default::default() + }; + } else if input.len() == 1 { + return DispatchResult { + context_stack, + ..Default::default() + }; + } + input.pop(); + + let (suffix, mut to_replay) = self.replay_prefix(input, dispatch_path); + + let mut result = self.dispatch_key(suffix, keystroke, dispatch_path); + to_replay.extend(result.to_replay); + result.to_replay = to_replay; + result + } + + /// If the user types a matching prefix of a binding and then waits for a timeout + /// flush_dispatch() converts any previously pending input to replay events. + pub fn flush_dispatch( + &mut self, + input: SmallVec<[Keystroke; 1]>, + dispatch_path: &SmallVec<[DispatchNodeId; 32]>, + ) -> SmallVec<[Replay; 1]> { + let (suffix, mut to_replay) = self.replay_prefix(input, dispatch_path); + + if !suffix.is_empty() { + to_replay.extend(self.flush_dispatch(suffix, dispatch_path)) + } + + to_replay + } + + /// Converts the longest prefix of input to a replay event and returns the rest. + fn replay_prefix( + &self, + mut input: SmallVec<[Keystroke; 1]>, + dispatch_path: &SmallVec<[DispatchNodeId; 32]>, + ) -> (SmallVec<[Keystroke; 1]>, SmallVec<[Replay; 1]>) { + let mut to_replay: SmallVec<[Replay; 1]> = Default::default(); + for last in (0..input.len()).rev() { + let (bindings, _, _) = self.bindings_for_input(&input[0..=last], dispatch_path); + if !bindings.is_empty() { + to_replay.push(Replay { + keystroke: input.drain(0..=last).next_back().unwrap(), + bindings, + }); + break; + } + } + if to_replay.is_empty() { + to_replay.push(Replay { + keystroke: input.remove(0), + ..Default::default() + }); + } + (input, to_replay) + } + + pub fn dispatch_path(&self, target: DispatchNodeId) -> SmallVec<[DispatchNodeId; 32]> { + let mut dispatch_path: SmallVec<[DispatchNodeId; 32]> = SmallVec::new(); + let mut current_node_id = Some(target); + while let Some(node_id) = current_node_id { + dispatch_path.push(node_id); + current_node_id = self.nodes.get(node_id.0).and_then(|node| node.parent); + } + dispatch_path.reverse(); // Reverse the path so it goes from the root to the focused node. + dispatch_path + } + + pub fn focus_path(&self, focus_id: FocusId) -> SmallVec<[FocusId; 8]> { + let mut focus_path: SmallVec<[FocusId; 8]> = SmallVec::new(); + let mut current_node_id = self.focusable_node_ids.get(&focus_id).copied(); + while let Some(node_id) = current_node_id { + let node = self.node(node_id); + if let Some(focus_id) = node.focus_id { + focus_path.push(focus_id); + } + current_node_id = node.parent; + } + focus_path.reverse(); // Reverse the path so it goes from the root to the focused node. + focus_path + } + + pub fn view_path(&self, view_id: EntityId) -> SmallVec<[EntityId; 8]> { + let mut view_path: SmallVec<[EntityId; 8]> = SmallVec::new(); + let mut current_node_id = self.view_node_ids.get(&view_id).copied(); + while let Some(node_id) = current_node_id { + let node = self.node(node_id); + if let Some(view_id) = node.view_id { + view_path.push(view_id); + } + current_node_id = node.parent; + } + view_path.reverse(); // Reverse the path so it goes from the root to the view node. + view_path + } + + pub fn node(&self, node_id: DispatchNodeId) -> &DispatchNode { + &self.nodes[node_id.0] + } + + fn active_node(&mut self) -> &mut DispatchNode { + let active_node_id = self.active_node_id().unwrap(); + &mut self.nodes[active_node_id.0] + } + + pub fn focusable_node_id(&self, target: FocusId) -> Option { + self.focusable_node_ids.get(&target).copied() + } + + pub fn root_node_id(&self) -> DispatchNodeId { + debug_assert!(!self.nodes.is_empty()); + DispatchNodeId(0) + } + + pub fn active_node_id(&self) -> Option { + self.node_stack.last().copied() + } +} + +#[cfg(test)] +mod tests { + use crate::{ + self as gpui, Element, ElementId, GlobalElementId, InspectorElementId, LayoutId, Style, + }; + use core::panic; + use std::{cell::RefCell, ops::Range, rc::Rc}; + + use crate::{ + Action, ActionRegistry, App, Bounds, Context, DispatchTree, FocusHandle, InputHandler, + IntoElement, KeyBinding, KeyContext, Keymap, Pixels, Point, Render, TestAppContext, + UTF16Selection, Window, + }; + + #[derive(PartialEq, Eq)] + struct TestAction; + + impl Action for TestAction { + fn name(&self) -> &'static str { + "test::TestAction" + } + + fn name_for_type() -> &'static str + where + Self: ::std::marker::Sized, + { + "test::TestAction" + } + + fn partial_eq(&self, action: &dyn Action) -> bool { + action.as_any().downcast_ref::() == Some(self) + } + + fn boxed_clone(&self) -> std::boxed::Box { + Box::new(TestAction) + } + + fn build(_value: serde_json::Value) -> anyhow::Result> + where + Self: Sized, + { + Ok(Box::new(TestAction)) + } + } + + #[test] + fn test_keybinding_for_action_bounds() { + let keymap = Keymap::new(vec![KeyBinding::new( + "cmd-n", + TestAction, + Some("ProjectPanel"), + )]); + + let mut registry = ActionRegistry::default(); + + registry.load_action::(); + + let keymap = Rc::new(RefCell::new(keymap)); + + let tree = DispatchTree::new(keymap, Rc::new(registry)); + + let contexts = vec![ + KeyContext::parse("Workspace").unwrap(), + KeyContext::parse("ProjectPanel").unwrap(), + ]; + + let keybinding = tree.bindings_for_action(&TestAction, &contexts); + + assert!(keybinding[0].action.partial_eq(&TestAction)) + } + + #[crate::test] + fn test_input_handler_pending(cx: &mut TestAppContext) { + #[derive(Clone)] + struct CustomElement { + focus_handle: FocusHandle, + text: Rc>, + } + impl CustomElement { + fn new(cx: &mut Context) -> Self { + Self { + focus_handle: cx.focus_handle(), + text: Rc::default(), + } + } + } + impl Element for CustomElement { + type RequestLayoutState = (); + + type PrepaintState = (); + + fn id(&self) -> Option { + Some("custom".into()) + } + fn source_location(&self) -> Option<&'static panic::Location<'static>> { + None + } + fn request_layout( + &mut self, + _: Option<&GlobalElementId>, + _: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + (window.request_layout(Style::default(), [], cx), ()) + } + fn prepaint( + &mut self, + _: Option<&GlobalElementId>, + _: Option<&InspectorElementId>, + _: Bounds, + _: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Self::PrepaintState { + window.set_focus_handle(&self.focus_handle, cx); + } + fn paint( + &mut self, + _: Option<&GlobalElementId>, + _: Option<&InspectorElementId>, + _: Bounds, + _: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + let mut key_context = KeyContext::default(); + key_context.add("Terminal"); + window.set_key_context(key_context); + window.handle_input(&self.focus_handle, self.clone(), cx); + window.on_action(std::any::TypeId::of::(), |_, _, _, _| {}); + } + } + impl IntoElement for CustomElement { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } + } + + impl InputHandler for CustomElement { + fn selected_text_range( + &mut self, + _: bool, + _: &mut Window, + _: &mut App, + ) -> Option { + None + } + + fn marked_text_range(&mut self, _: &mut Window, _: &mut App) -> Option> { + None + } + + fn text_for_range( + &mut self, + _: Range, + _: &mut Option>, + _: &mut Window, + _: &mut App, + ) -> Option { + None + } + + fn replace_text_in_range( + &mut self, + replacement_range: Option>, + text: &str, + _: &mut Window, + _: &mut App, + ) { + if replacement_range.is_some() { + unimplemented!() + } + self.text.borrow_mut().push_str(text) + } + + fn replace_and_mark_text_in_range( + &mut self, + replacement_range: Option>, + new_text: &str, + _: Option>, + _: &mut Window, + _: &mut App, + ) { + if replacement_range.is_some() { + unimplemented!() + } + self.text.borrow_mut().push_str(new_text) + } + + fn unmark_text(&mut self, _: &mut Window, _: &mut App) {} + + fn bounds_for_range( + &mut self, + _: Range, + _: &mut Window, + _: &mut App, + ) -> Option> { + None + } + + fn character_index_for_point( + &mut self, + _: Point, + _: &mut Window, + _: &mut App, + ) -> Option { + None + } + } + impl Render for CustomElement { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + self.clone() + } + } + + cx.update(|cx| { + cx.bind_keys([KeyBinding::new("ctrl-b", TestAction, Some("Terminal"))]); + cx.bind_keys([KeyBinding::new("ctrl-b h", TestAction, Some("Terminal"))]); + }); + let (test, cx) = cx.add_window_view(|_, cx| CustomElement::new(cx)); + cx.update(|window, cx| { + window.focus(&test.read(cx).focus_handle); + window.activate_window(); + }); + cx.simulate_keystrokes("ctrl-b ["); + test.update(cx, |test, _| assert_eq!(test.text.borrow().as_str(), "[")) + } +} diff --git a/third_party/gpui/src/keymap.rs b/third_party/gpui/src/keymap.rs new file mode 100644 index 0000000..e261233 --- /dev/null +++ b/third_party/gpui/src/keymap.rs @@ -0,0 +1,713 @@ +mod binding; +mod context; + +pub use binding::*; +pub use context::*; + +use crate::{Action, AsKeystroke, Keystroke, is_no_action}; +use collections::{HashMap, HashSet}; +use smallvec::SmallVec; +use std::any::TypeId; + +/// An opaque identifier of which version of the keymap is currently active. +/// The keymap's version is changed whenever bindings are added or removed. +#[derive(Copy, Clone, Eq, PartialEq, Default)] +pub struct KeymapVersion(usize); + +/// A collection of key bindings for the user's application. +#[derive(Default)] +pub struct Keymap { + bindings: Vec, + binding_indices_by_action_id: HashMap>, + no_action_binding_indices: Vec, + version: KeymapVersion, +} + +/// Index of a binding within a keymap. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub struct BindingIndex(usize); + +impl Keymap { + /// Create a new keymap with the given bindings. + pub fn new(bindings: Vec) -> Self { + let mut this = Self::default(); + this.add_bindings(bindings); + this + } + + /// Get the current version of the keymap. + pub fn version(&self) -> KeymapVersion { + self.version + } + + /// Add more bindings to the keymap. + pub fn add_bindings>(&mut self, bindings: T) { + for binding in bindings { + let action_id = binding.action().as_any().type_id(); + if is_no_action(&*binding.action) { + self.no_action_binding_indices.push(self.bindings.len()); + } else { + self.binding_indices_by_action_id + .entry(action_id) + .or_default() + .push(self.bindings.len()); + } + self.bindings.push(binding); + } + + self.version.0 += 1; + } + + /// Reset this keymap to its initial state. + pub fn clear(&mut self) { + self.bindings.clear(); + self.binding_indices_by_action_id.clear(); + self.no_action_binding_indices.clear(); + self.version.0 += 1; + } + + /// Iterate over all bindings, in the order they were added. + pub fn bindings(&self) -> impl DoubleEndedIterator + ExactSizeIterator { + self.bindings.iter() + } + + /// Iterate over all bindings for the given action, in the order they were added. For display, + /// the last binding should take precedence. + pub fn bindings_for_action<'a>( + &'a self, + action: &'a dyn Action, + ) -> impl 'a + DoubleEndedIterator { + let action_id = action.type_id(); + let binding_indices = self + .binding_indices_by_action_id + .get(&action_id) + .map_or(&[] as _, SmallVec::as_slice) + .iter(); + + binding_indices.filter_map(|ix| { + let binding = &self.bindings[*ix]; + if !binding.action().partial_eq(action) { + return None; + } + + for null_ix in &self.no_action_binding_indices { + if null_ix > ix { + let null_binding = &self.bindings[*null_ix]; + if null_binding.keystrokes == binding.keystrokes { + let null_binding_matches = + match (&null_binding.context_predicate, &binding.context_predicate) { + (None, _) => true, + (Some(_), None) => false, + (Some(null_predicate), Some(predicate)) => { + null_predicate.is_superset(predicate) + } + }; + if null_binding_matches { + return None; + } + } + } + } + + Some(binding) + }) + } + + /// Returns all bindings that might match the input without checking context. The bindings + /// returned in precedence order (reverse of the order they were added to the keymap). + pub fn all_bindings_for_input(&self, input: &[Keystroke]) -> Vec { + self.bindings() + .rev() + .filter_map(|binding| { + binding.match_keystrokes(input).filter(|pending| !pending)?; + Some(binding.clone()) + }) + .collect() + } + + /// Returns a list of bindings that match the given input, and a boolean indicating whether or + /// not more bindings might match if the input was longer. Bindings are returned in precedence + /// order (higher precedence first, reverse of the order they were added to the keymap). + /// + /// Precedence is defined by the depth in the tree (matches on the Editor take precedence over + /// matches on the Pane, then the Workspace, etc.). Bindings with no context are treated as the + /// same as the deepest context. + /// + /// In the case of multiple bindings at the same depth, the ones added to the keymap later take + /// precedence. User bindings are added after built-in bindings so that they take precedence. + /// + /// If a user has disabled a binding with `"x": null` it will not be returned. Disabled bindings + /// are evaluated with the same precedence rules so you can disable a rule in a given context + /// only. + pub fn bindings_for_input( + &self, + input: &[impl AsKeystroke], + context_stack: &[KeyContext], + ) -> (SmallVec<[KeyBinding; 1]>, bool) { + let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new(); + let mut pending_bindings = SmallVec::<[(BindingIndex, &KeyBinding); 1]>::new(); + + for (ix, binding) in self.bindings().enumerate().rev() { + let Some(depth) = self.binding_enabled(binding, context_stack) else { + continue; + }; + let Some(pending) = binding.match_keystrokes(input) else { + continue; + }; + + if !pending { + matched_bindings.push((depth, BindingIndex(ix), binding)); + } else { + pending_bindings.push((BindingIndex(ix), binding)); + } + } + + matched_bindings.sort_by(|(depth_a, ix_a, _), (depth_b, ix_b, _)| { + depth_b.cmp(depth_a).then(ix_b.cmp(ix_a)) + }); + + let mut bindings: SmallVec<[_; 1]> = SmallVec::new(); + let mut first_binding_index = None; + + for (_, ix, binding) in matched_bindings { + if is_no_action(&*binding.action) { + // Only break if this is a user-defined NoAction binding + // This allows user keymaps to override base keymap NoAction bindings + if let Some(meta) = binding.meta { + if meta.0 == 0 { + break; + } + } else { + // If no meta is set, assume it's a user binding for safety + break; + } + // For non-user NoAction bindings, continue searching for user overrides + continue; + } + bindings.push(binding.clone()); + first_binding_index.get_or_insert(ix); + } + + let mut pending = HashSet::default(); + for (ix, binding) in pending_bindings.into_iter().rev() { + if let Some(binding_ix) = first_binding_index + && binding_ix > ix + { + continue; + } + if is_no_action(&*binding.action) { + pending.remove(&&binding.keystrokes); + continue; + } + pending.insert(&binding.keystrokes); + } + + (bindings, !pending.is_empty()) + } + /// Check if the given binding is enabled, given a certain key context. + /// Returns the deepest depth at which the binding matches, or None if it doesn't match. + fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option { + if let Some(predicate) = &binding.context_predicate { + predicate.depth_of(contexts) + } else { + Some(contexts.len()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate as gpui; + use gpui::NoAction; + + actions!( + test_only, + [ActionAlpha, ActionBeta, ActionGamma, ActionDelta,] + ); + + #[test] + fn test_keymap() { + let bindings = [ + KeyBinding::new("ctrl-a", ActionAlpha {}, None), + KeyBinding::new("ctrl-a", ActionBeta {}, Some("pane")), + KeyBinding::new("ctrl-a", ActionGamma {}, Some("editor && mode==full")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings.clone()); + + // global bindings are enabled in all contexts + assert_eq!(keymap.binding_enabled(&bindings[0], &[]), Some(0)); + assert_eq!( + keymap.binding_enabled(&bindings[0], &[KeyContext::parse("terminal").unwrap()]), + Some(1) + ); + + // contextual bindings are enabled in contexts that match their predicate + assert_eq!( + keymap.binding_enabled(&bindings[1], &[KeyContext::parse("barf x=y").unwrap()]), + None + ); + assert_eq!( + keymap.binding_enabled(&bindings[1], &[KeyContext::parse("pane x=y").unwrap()]), + Some(1) + ); + + assert_eq!( + keymap.binding_enabled(&bindings[2], &[KeyContext::parse("editor").unwrap()]), + None + ); + assert_eq!( + keymap.binding_enabled( + &bindings[2], + &[KeyContext::parse("editor mode=full").unwrap()] + ), + Some(1) + ); + } + + #[test] + fn test_depth_precedence() { + let bindings = [ + KeyBinding::new("ctrl-a", ActionBeta {}, Some("pane")), + KeyBinding::new("ctrl-a", ActionGamma {}, Some("editor")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + let (result, pending) = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-a").unwrap()], + &[ + KeyContext::parse("pane").unwrap(), + KeyContext::parse("editor").unwrap(), + ], + ); + + assert!(!pending); + assert_eq!(result.len(), 2); + assert!(result[0].action.partial_eq(&ActionGamma {})); + assert!(result[1].action.partial_eq(&ActionBeta {})); + } + + #[test] + fn test_keymap_disabled() { + let bindings = [ + KeyBinding::new("ctrl-a", ActionAlpha {}, Some("editor")), + KeyBinding::new("ctrl-b", ActionAlpha {}, Some("editor")), + KeyBinding::new("ctrl-a", NoAction {}, Some("editor && mode==full")), + KeyBinding::new("ctrl-b", NoAction {}, None), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + // binding is only enabled in a specific context + assert!( + keymap + .bindings_for_input( + &[Keystroke::parse("ctrl-a").unwrap()], + &[KeyContext::parse("barf").unwrap()], + ) + .0 + .is_empty() + ); + assert!( + !keymap + .bindings_for_input( + &[Keystroke::parse("ctrl-a").unwrap()], + &[KeyContext::parse("editor").unwrap()], + ) + .0 + .is_empty() + ); + + // binding is disabled in a more specific context + assert!( + keymap + .bindings_for_input( + &[Keystroke::parse("ctrl-a").unwrap()], + &[KeyContext::parse("editor mode=full").unwrap()], + ) + .0 + .is_empty() + ); + + // binding is globally disabled + assert!( + keymap + .bindings_for_input( + &[Keystroke::parse("ctrl-b").unwrap()], + &[KeyContext::parse("barf").unwrap()], + ) + .0 + .is_empty() + ); + } + + #[test] + /// Tests for https://github.com/zed-industries/zed/issues/30259 + fn test_multiple_keystroke_binding_disabled() { + let bindings = [ + KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")), + KeyBinding::new("space w w", NoAction {}, Some("editor")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + let space = || Keystroke::parse("space").unwrap(); + let w = || Keystroke::parse("w").unwrap(); + + let space_w = [space(), w()]; + let space_w_w = [space(), w(), w()]; + + let workspace_context = || [KeyContext::parse("workspace").unwrap()]; + + let editor_workspace_context = || { + [ + KeyContext::parse("workspace").unwrap(), + KeyContext::parse("editor").unwrap(), + ] + }; + + // Ensure `space` results in pending input on the workspace, but not editor + let space_workspace = keymap.bindings_for_input(&[space()], &workspace_context()); + assert!(space_workspace.0.is_empty()); + assert!(space_workspace.1); + + let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); + assert!(space_editor.0.is_empty()); + assert!(!space_editor.1); + + // Ensure `space w` results in pending input on the workspace, but not editor + let space_w_workspace = keymap.bindings_for_input(&space_w, &workspace_context()); + assert!(space_w_workspace.0.is_empty()); + assert!(space_w_workspace.1); + + let space_w_editor = keymap.bindings_for_input(&space_w, &editor_workspace_context()); + assert!(space_w_editor.0.is_empty()); + assert!(!space_w_editor.1); + + // Ensure `space w w` results in the binding in the workspace, but not in the editor + let space_w_w_workspace = keymap.bindings_for_input(&space_w_w, &workspace_context()); + assert!(!space_w_w_workspace.0.is_empty()); + assert!(!space_w_w_workspace.1); + + let space_w_w_editor = keymap.bindings_for_input(&space_w_w, &editor_workspace_context()); + assert!(space_w_w_editor.0.is_empty()); + assert!(!space_w_w_editor.1); + + // Now test what happens if we have another binding defined AFTER the NoAction + // that should result in pending + let bindings = [ + KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")), + KeyBinding::new("space w w", NoAction {}, Some("editor")), + KeyBinding::new("space w x", ActionAlpha {}, Some("editor")), + ]; + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); + assert!(space_editor.0.is_empty()); + assert!(space_editor.1); + + // Now test what happens if we have another binding defined BEFORE the NoAction + // that should result in pending + let bindings = [ + KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")), + KeyBinding::new("space w x", ActionAlpha {}, Some("editor")), + KeyBinding::new("space w w", NoAction {}, Some("editor")), + ]; + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); + assert!(space_editor.0.is_empty()); + assert!(space_editor.1); + + // Now test what happens if we have another binding defined at a higher context + // that should result in pending + let bindings = [ + KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")), + KeyBinding::new("space w x", ActionAlpha {}, Some("workspace")), + KeyBinding::new("space w w", NoAction {}, Some("editor")), + ]; + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); + assert!(space_editor.0.is_empty()); + assert!(space_editor.1); + } + + #[test] + fn test_override_multikey() { + let bindings = [ + KeyBinding::new("ctrl-w left", ActionAlpha {}, Some("editor")), + KeyBinding::new("ctrl-w", NoAction {}, Some("editor")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + // Ensure `space` results in pending input on the workspace, but not editor + let (result, pending) = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-w").unwrap()], + &[KeyContext::parse("editor").unwrap()], + ); + assert!(result.is_empty()); + assert!(pending); + + let bindings = [ + KeyBinding::new("ctrl-w left", ActionAlpha {}, Some("editor")), + KeyBinding::new("ctrl-w", ActionBeta {}, Some("editor")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + // Ensure `space` results in pending input on the workspace, but not editor + let (result, pending) = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-w").unwrap()], + &[KeyContext::parse("editor").unwrap()], + ); + assert_eq!(result.len(), 1); + assert!(!pending); + } + + #[test] + fn test_simple_disable() { + let bindings = [ + KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")), + KeyBinding::new("ctrl-x", NoAction {}, Some("editor")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + // Ensure `space` results in pending input on the workspace, but not editor + let (result, pending) = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-x").unwrap()], + &[KeyContext::parse("editor").unwrap()], + ); + assert!(result.is_empty()); + assert!(!pending); + } + + #[test] + fn test_fail_to_disable() { + // disabled at the wrong level + let bindings = [ + KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")), + KeyBinding::new("ctrl-x", NoAction {}, Some("workspace")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + // Ensure `space` results in pending input on the workspace, but not editor + let (result, pending) = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-x").unwrap()], + &[ + KeyContext::parse("workspace").unwrap(), + KeyContext::parse("editor").unwrap(), + ], + ); + assert_eq!(result.len(), 1); + assert!(!pending); + } + + #[test] + fn test_disable_deeper() { + let bindings = [ + KeyBinding::new("ctrl-x", ActionAlpha {}, Some("workspace")), + KeyBinding::new("ctrl-x", NoAction {}, Some("editor")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + // Ensure `space` results in pending input on the workspace, but not editor + let (result, pending) = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-x").unwrap()], + &[ + KeyContext::parse("workspace").unwrap(), + KeyContext::parse("editor").unwrap(), + ], + ); + assert_eq!(result.len(), 0); + assert!(!pending); + } + + #[test] + fn test_pending_match_enabled() { + let bindings = [ + KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")), + KeyBinding::new("ctrl-x 0", ActionAlpha, Some("Workspace")), + ]; + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + let matched = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-x")].map(Result::unwrap), + &[ + KeyContext::parse("Workspace"), + KeyContext::parse("Pane"), + KeyContext::parse("Editor vim_mode=normal"), + ] + .map(Result::unwrap), + ); + assert_eq!(matched.0.len(), 1); + assert!(matched.0[0].action.partial_eq(&ActionBeta)); + assert!(matched.1); + } + + #[test] + fn test_pending_match_enabled_extended() { + let bindings = [ + KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")), + KeyBinding::new("ctrl-x 0", NoAction, Some("Workspace")), + ]; + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + let matched = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-x")].map(Result::unwrap), + &[ + KeyContext::parse("Workspace"), + KeyContext::parse("Pane"), + KeyContext::parse("Editor vim_mode=normal"), + ] + .map(Result::unwrap), + ); + assert_eq!(matched.0.len(), 1); + assert!(matched.0[0].action.partial_eq(&ActionBeta)); + assert!(!matched.1); + let bindings = [ + KeyBinding::new("ctrl-x", ActionBeta, Some("Workspace")), + KeyBinding::new("ctrl-x 0", NoAction, Some("vim_mode == normal")), + ]; + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + let matched = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-x")].map(Result::unwrap), + &[ + KeyContext::parse("Workspace"), + KeyContext::parse("Pane"), + KeyContext::parse("Editor vim_mode=normal"), + ] + .map(Result::unwrap), + ); + assert_eq!(matched.0.len(), 1); + assert!(matched.0[0].action.partial_eq(&ActionBeta)); + assert!(!matched.1); + } + + #[test] + fn test_overriding_prefix() { + let bindings = [ + KeyBinding::new("ctrl-x 0", ActionAlpha, Some("Workspace")), + KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")), + ]; + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + let matched = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-x")].map(Result::unwrap), + &[ + KeyContext::parse("Workspace"), + KeyContext::parse("Pane"), + KeyContext::parse("Editor vim_mode=normal"), + ] + .map(Result::unwrap), + ); + assert_eq!(matched.0.len(), 1); + assert!(matched.0[0].action.partial_eq(&ActionBeta)); + assert!(!matched.1); + } + + #[test] + fn test_context_precedence_with_same_source() { + // Test case: User has both Workspace and Editor bindings for the same key + // Editor binding should take precedence over Workspace binding + let bindings = [ + KeyBinding::new("cmd-r", ActionAlpha {}, Some("Workspace")), + KeyBinding::new("cmd-r", ActionBeta {}, Some("Editor")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + // Test with context stack: [Workspace, Editor] (Editor is deeper) + let (result, _) = keymap.bindings_for_input( + &[Keystroke::parse("cmd-r").unwrap()], + &[ + KeyContext::parse("Workspace").unwrap(), + KeyContext::parse("Editor").unwrap(), + ], + ); + + // Both bindings should be returned, but Editor binding should be first (highest precedence) + assert_eq!(result.len(), 2); + assert!(result[0].action.partial_eq(&ActionBeta {})); // Editor binding first + assert!(result[1].action.partial_eq(&ActionAlpha {})); // Workspace binding second + } + + #[test] + fn test_bindings_for_action() { + let bindings = [ + KeyBinding::new("ctrl-a", ActionAlpha {}, Some("pane")), + KeyBinding::new("ctrl-b", ActionBeta {}, Some("editor && mode == full")), + KeyBinding::new("ctrl-c", ActionGamma {}, Some("workspace")), + KeyBinding::new("ctrl-a", NoAction {}, Some("pane && active")), + KeyBinding::new("ctrl-b", NoAction {}, Some("editor")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + assert_bindings(&keymap, &ActionAlpha {}, &["ctrl-a"]); + assert_bindings(&keymap, &ActionBeta {}, &[]); + assert_bindings(&keymap, &ActionGamma {}, &["ctrl-c"]); + + #[track_caller] + fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) { + let actual = keymap + .bindings_for_action(action) + .map(|binding| binding.keystrokes[0].inner().unparse()) + .collect::>(); + assert_eq!(actual, expected, "{:?}", action); + } + } + + #[test] + fn test_source_precedence_sorting() { + // KeybindSource precedence: User (0) > Vim (1) > Base (2) > Default (3) + // Test that user keymaps take precedence over default keymaps at the same context depth + let mut keymap = Keymap::default(); + + // Add a default keymap binding first + let mut default_binding = KeyBinding::new("cmd-r", ActionAlpha {}, Some("Editor")); + default_binding.set_meta(KeyBindingMetaIndex(3)); // Default source + keymap.add_bindings([default_binding]); + + // Add a user keymap binding + let mut user_binding = KeyBinding::new("cmd-r", ActionBeta {}, Some("Editor")); + user_binding.set_meta(KeyBindingMetaIndex(0)); // User source + keymap.add_bindings([user_binding]); + + // Test with Editor context stack + let (result, _) = keymap.bindings_for_input( + &[Keystroke::parse("cmd-r").unwrap()], + &[KeyContext::parse("Editor").unwrap()], + ); + + // User binding should take precedence over default binding + assert_eq!(result.len(), 2); + assert!(result[0].action.partial_eq(&ActionBeta {})); + assert!(result[1].action.partial_eq(&ActionAlpha {})); + } +} diff --git a/third_party/gpui/src/keymap/binding.rs b/third_party/gpui/src/keymap/binding.rs new file mode 100644 index 0000000..fc4b329 --- /dev/null +++ b/third_party/gpui/src/keymap/binding.rs @@ -0,0 +1,143 @@ +use std::rc::Rc; + +use crate::{ + Action, AsKeystroke, DummyKeyboardMapper, InvalidKeystrokeError, KeyBindingContextPredicate, + KeybindingKeystroke, Keystroke, PlatformKeyboardMapper, SharedString, +}; +use smallvec::SmallVec; + +/// A keybinding and its associated metadata, from the keymap. +pub struct KeyBinding { + pub(crate) action: Box, + pub(crate) keystrokes: SmallVec<[KeybindingKeystroke; 2]>, + pub(crate) context_predicate: Option>, + pub(crate) meta: Option, + /// The json input string used when building the keybinding, if any + pub(crate) action_input: Option, +} + +impl Clone for KeyBinding { + fn clone(&self) -> Self { + KeyBinding { + action: self.action.boxed_clone(), + keystrokes: self.keystrokes.clone(), + context_predicate: self.context_predicate.clone(), + meta: self.meta, + action_input: self.action_input.clone(), + } + } +} + +impl KeyBinding { + /// Construct a new keybinding from the given data. Panics on parse error. + pub fn new(keystrokes: &str, action: A, context: Option<&str>) -> Self { + let context_predicate = + context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into()); + Self::load( + keystrokes, + Box::new(action), + context_predicate, + false, + None, + &DummyKeyboardMapper, + ) + .unwrap() + } + + /// Load a keybinding from the given raw data. + pub fn load( + keystrokes: &str, + action: Box, + context_predicate: Option>, + use_key_equivalents: bool, + action_input: Option, + keyboard_mapper: &dyn PlatformKeyboardMapper, + ) -> std::result::Result { + let keystrokes: SmallVec<[KeybindingKeystroke; 2]> = keystrokes + .split_whitespace() + .map(|source| { + let keystroke = Keystroke::parse(source)?; + Ok(KeybindingKeystroke::new_with_mapper( + keystroke, + use_key_equivalents, + keyboard_mapper, + )) + }) + .collect::>()?; + + Ok(Self { + keystrokes, + action, + context_predicate, + meta: None, + action_input, + }) + } + + /// Set the metadata for this binding. + pub fn with_meta(mut self, meta: KeyBindingMetaIndex) -> Self { + self.meta = Some(meta); + self + } + + /// Set the metadata for this binding. + pub fn set_meta(&mut self, meta: KeyBindingMetaIndex) { + self.meta = Some(meta); + } + + /// Check if the given keystrokes match this binding. + pub fn match_keystrokes(&self, typed: &[impl AsKeystroke]) -> Option { + if self.keystrokes.len() < typed.len() { + return None; + } + + for (target, typed) in self.keystrokes.iter().zip(typed.iter()) { + if !typed.as_keystroke().should_match(target) { + return None; + } + } + + Some(self.keystrokes.len() > typed.len()) + } + + /// Get the keystrokes associated with this binding + pub fn keystrokes(&self) -> &[KeybindingKeystroke] { + self.keystrokes.as_slice() + } + + /// Get the action associated with this binding + pub fn action(&self) -> &dyn Action { + self.action.as_ref() + } + + /// Get the predicate used to match this binding + pub fn predicate(&self) -> Option> { + self.context_predicate.as_ref().map(|rc| rc.clone()) + } + + /// Get the metadata for this binding + pub fn meta(&self) -> Option { + self.meta + } + + /// Get the action input associated with the action for this binding + pub fn action_input(&self) -> Option { + self.action_input.clone() + } +} + +impl std::fmt::Debug for KeyBinding { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("KeyBinding") + .field("keystrokes", &self.keystrokes) + .field("context_predicate", &self.context_predicate) + .field("action", &self.action.name()) + .finish() + } +} + +/// A unique identifier for retrieval of metadata associated with a key binding. +/// Intended to be used as an index or key into a user-defined store of metadata +/// associated with the binding, such as the source of the binding. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct KeyBindingMetaIndex(pub u32); diff --git a/third_party/gpui/src/keymap/context.rs b/third_party/gpui/src/keymap/context.rs new file mode 100644 index 0000000..960bd17 --- /dev/null +++ b/third_party/gpui/src/keymap/context.rs @@ -0,0 +1,760 @@ +use crate::SharedString; +use anyhow::{Context as _, Result}; +use std::fmt; + +/// A datastructure for resolving whether an action should be dispatched +/// at this point in the element tree. Contains a set of identifiers +/// and/or key value pairs representing the current context for the +/// keymap. +#[derive(Clone, Default, Eq, PartialEq, Hash)] +pub struct KeyContext(Vec); + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +/// An entry in a KeyContext +pub struct ContextEntry { + /// The key (or name if no value) + pub key: SharedString, + /// The value + pub value: Option, +} + +impl<'a> TryFrom<&'a str> for KeyContext { + type Error = anyhow::Error; + + fn try_from(value: &'a str) -> Result { + Self::parse(value) + } +} + +impl KeyContext { + /// Initialize a new [`KeyContext`] that contains an `os` key set to either `macos`, `linux`, `windows` or `unknown`. + pub fn new_with_defaults() -> Self { + let mut context = Self::default(); + #[cfg(target_os = "macos")] + context.set("os", "macos"); + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + context.set("os", "linux"); + #[cfg(target_os = "windows")] + context.set("os", "windows"); + #[cfg(not(any( + target_os = "macos", + target_os = "linux", + target_os = "freebsd", + target_os = "windows" + )))] + context.set("os", "unknown"); + context + } + + /// Returns the primary context entry (usually the name of the component) + pub fn primary(&self) -> Option<&ContextEntry> { + self.0.iter().find(|p| p.value.is_none()) + } + + /// Returns everything except the primary context entry. + pub fn secondary(&self) -> impl Iterator { + let primary = self.primary(); + self.0.iter().filter(move |&p| Some(p) != primary) + } + + /// Parse a key context from a string. + /// The key context format is very simple: + /// - either a single identifier, such as `StatusBar` + /// - or a key value pair, such as `mode = visible` + /// - separated by whitespace, such as `StatusBar mode = visible` + pub fn parse(source: &str) -> Result { + let mut context = Self::default(); + let source = skip_whitespace(source); + Self::parse_expr(source, &mut context)?; + Ok(context) + } + + fn parse_expr(mut source: &str, context: &mut Self) -> Result<()> { + if source.is_empty() { + return Ok(()); + } + + let key = source + .chars() + .take_while(|c| is_identifier_char(*c)) + .collect::(); + source = skip_whitespace(&source[key.len()..]); + if let Some(suffix) = source.strip_prefix('=') { + source = skip_whitespace(suffix); + let value = source + .chars() + .take_while(|c| is_identifier_char(*c)) + .collect::(); + source = skip_whitespace(&source[value.len()..]); + context.set(key, value); + } else { + context.add(key); + } + + Self::parse_expr(source, context) + } + + /// Check if this context is empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Clear this context. + pub fn clear(&mut self) { + self.0.clear(); + } + + /// Extend this context with another context. + pub fn extend(&mut self, other: &Self) { + for entry in &other.0 { + if !self.contains(&entry.key) { + self.0.push(entry.clone()); + } + } + } + + /// Add an identifier to this context, if it's not already in this context. + pub fn add>(&mut self, identifier: I) { + let key = identifier.into(); + + if !self.contains(&key) { + self.0.push(ContextEntry { key, value: None }) + } + } + + /// Set a key value pair in this context, if it's not already set. + pub fn set, S2: Into>(&mut self, key: S1, value: S2) { + let key = key.into(); + if !self.contains(&key) { + self.0.push(ContextEntry { + key, + value: Some(value.into()), + }) + } + } + + /// Check if this context contains a given identifier or key. + pub fn contains(&self, key: &str) -> bool { + self.0.iter().any(|entry| entry.key.as_ref() == key) + } + + /// Get the associated value for a given identifier or key. + pub fn get(&self, key: &str) -> Option<&SharedString> { + self.0 + .iter() + .find(|entry| entry.key.as_ref() == key)? + .value + .as_ref() + } +} + +impl fmt::Debug for KeyContext { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut entries = self.0.iter().peekable(); + while let Some(entry) = entries.next() { + if let Some(ref value) = entry.value { + write!(f, "{}={}", entry.key, value)?; + } else { + write!(f, "{}", entry.key)?; + } + if entries.peek().is_some() { + write!(f, " ")?; + } + } + Ok(()) + } +} + +/// A datastructure for resolving whether an action should be dispatched +/// Representing a small language for describing which contexts correspond +/// to which actions. +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum KeyBindingContextPredicate { + /// A predicate that will match a given identifier. + Identifier(SharedString), + /// A predicate that will match a given key-value pair. + Equal(SharedString, SharedString), + /// A predicate that will match a given key-value pair not being present. + NotEqual(SharedString, SharedString), + /// A predicate that will match a given predicate appearing below another predicate. + /// in the element tree + Descendant( + Box, + Box, + ), + /// Predicate that will invert another predicate. + Not(Box), + /// A predicate that will match if both of its children match. + And( + Box, + Box, + ), + /// A predicate that will match if either of its children match. + Or( + Box, + Box, + ), +} + +impl fmt::Display for KeyBindingContextPredicate { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Identifier(name) => write!(f, "{}", name), + Self::Equal(left, right) => write!(f, "{} == {}", left, right), + Self::NotEqual(left, right) => write!(f, "{} != {}", left, right), + Self::Not(pred) => write!(f, "!{}", pred), + Self::Descendant(parent, child) => write!(f, "{} > {}", parent, child), + Self::And(left, right) => write!(f, "({} && {})", left, right), + Self::Or(left, right) => write!(f, "({} || {})", left, right), + } + } +} + +impl KeyBindingContextPredicate { + /// Parse a string in the same format as the keymap's context field. + /// + /// A basic equivalence check against a set of identifiers can performed by + /// simply writing a string: + /// + /// `StatusBar` -> A predicate that will match a context with the identifier `StatusBar` + /// + /// You can also specify a key-value pair: + /// + /// `mode == visible` -> A predicate that will match a context with the key `mode` + /// with the value `visible` + /// + /// And a logical operations combining these two checks: + /// + /// `StatusBar && mode == visible` -> A predicate that will match a context with the + /// identifier `StatusBar` and the key `mode` + /// with the value `visible` + /// + /// + /// There is also a special child `>` operator that will match a predicate that is + /// below another predicate: + /// + /// `StatusBar > mode == visible` -> A predicate that will match a context identifier `StatusBar` + /// and a child context that has the key `mode` with the + /// value `visible` + /// + /// This syntax supports `!=`, `||` and `&&` as logical operators. + /// You can also preface an operation or check with a `!` to negate it. + pub fn parse(source: &str) -> Result { + let source = skip_whitespace(source); + let (predicate, rest) = Self::parse_expr(source, 0)?; + if let Some(next) = rest.chars().next() { + anyhow::bail!("unexpected character '{next:?}'"); + } else { + Ok(predicate) + } + } + + /// Find the deepest depth at which the predicate matches. + pub fn depth_of(&self, contexts: &[KeyContext]) -> Option { + for depth in (0..=contexts.len()).rev() { + let context_slice = &contexts[0..depth]; + if self.eval_inner(context_slice, contexts) { + return Some(depth); + } + } + None + } + + /// Eval a predicate against a set of contexts, arranged from lowest to highest. + #[allow(unused)] + pub(crate) fn eval(&self, contexts: &[KeyContext]) -> bool { + self.eval_inner(contexts, contexts) + } + + /// Eval a predicate against a set of contexts, arranged from lowest to highest. + pub fn eval_inner(&self, contexts: &[KeyContext], all_contexts: &[KeyContext]) -> bool { + let Some(context) = contexts.last() else { + return false; + }; + match self { + Self::Identifier(name) => context.contains(name), + Self::Equal(left, right) => context + .get(left) + .map(|value| value == right) + .unwrap_or(false), + Self::NotEqual(left, right) => context + .get(left) + .map(|value| value != right) + .unwrap_or(true), + Self::Not(pred) => { + for i in 0..all_contexts.len() { + if pred.eval_inner(&all_contexts[..=i], all_contexts) { + return false; + } + } + true + } + // Workspace > Pane > Editor + // + // Pane > (Pane > Editor) // should match? + // (Pane > Pane) > Editor // should not match? + // Pane > !Workspace <-- should match? + // !Workspace <-- shouldn't match? + Self::Descendant(parent, child) => { + for i in 0..contexts.len() - 1 { + // [Workspace > Pane], [Editor] + if parent.eval_inner(&contexts[..=i], all_contexts) { + if !child.eval_inner(&contexts[i + 1..], &contexts[i + 1..]) { + return false; + } + return true; + } + } + false + } + Self::And(left, right) => { + left.eval_inner(contexts, all_contexts) && right.eval_inner(contexts, all_contexts) + } + Self::Or(left, right) => { + left.eval_inner(contexts, all_contexts) || right.eval_inner(contexts, all_contexts) + } + } + } + + /// Returns whether or not this predicate matches all possible contexts matched by + /// the other predicate. + pub fn is_superset(&self, other: &Self) -> bool { + if self == other { + return true; + } + + if let KeyBindingContextPredicate::Or(left, right) = self { + return left.is_superset(other) || right.is_superset(other); + } + + match other { + KeyBindingContextPredicate::Descendant(_, child) => self.is_superset(child), + KeyBindingContextPredicate::And(left, right) => { + self.is_superset(left) || self.is_superset(right) + } + KeyBindingContextPredicate::Identifier(_) => false, + KeyBindingContextPredicate::Equal(_, _) => false, + KeyBindingContextPredicate::NotEqual(_, _) => false, + KeyBindingContextPredicate::Not(_) => false, + KeyBindingContextPredicate::Or(_, _) => false, + } + } + + fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> { + type Op = fn( + KeyBindingContextPredicate, + KeyBindingContextPredicate, + ) -> Result; + + let (mut predicate, rest) = Self::parse_primary(source)?; + source = rest; + + 'parse: loop { + for (operator, precedence, constructor) in [ + (">", PRECEDENCE_CHILD, Self::new_child as Op), + ("&&", PRECEDENCE_AND, Self::new_and as Op), + ("||", PRECEDENCE_OR, Self::new_or as Op), + ("==", PRECEDENCE_EQ, Self::new_eq as Op), + ("!=", PRECEDENCE_EQ, Self::new_neq as Op), + ] { + if source.starts_with(operator) && precedence >= min_precedence { + source = skip_whitespace(&source[operator.len()..]); + let (right, rest) = Self::parse_expr(source, precedence + 1)?; + predicate = constructor(predicate, right)?; + source = rest; + continue 'parse; + } + } + break; + } + + Ok((predicate, source)) + } + + fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> { + let next = source.chars().next().context("unexpected end")?; + match next { + '(' => { + source = skip_whitespace(&source[1..]); + let (predicate, rest) = Self::parse_expr(source, 0)?; + let stripped = rest.strip_prefix(')').context("expected a ')'")?; + source = skip_whitespace(stripped); + Ok((predicate, source)) + } + '!' => { + let source = skip_whitespace(&source[1..]); + let (predicate, source) = Self::parse_expr(source, PRECEDENCE_NOT)?; + Ok((KeyBindingContextPredicate::Not(Box::new(predicate)), source)) + } + _ if is_identifier_char(next) => { + let len = source + .find(|c: char| !is_identifier_char(c) && !is_vim_operator_char(c)) + .unwrap_or(source.len()); + let (identifier, rest) = source.split_at(len); + source = skip_whitespace(rest); + Ok(( + KeyBindingContextPredicate::Identifier(identifier.to_string().into()), + source, + )) + } + _ if is_vim_operator_char(next) => { + let (operator, rest) = source.split_at(1); + source = skip_whitespace(rest); + Ok(( + KeyBindingContextPredicate::Identifier(operator.to_string().into()), + source, + )) + } + _ => anyhow::bail!("unexpected character '{next:?}'"), + } + } + + fn new_or(self, other: Self) -> Result { + Ok(Self::Or(Box::new(self), Box::new(other))) + } + + fn new_and(self, other: Self) -> Result { + Ok(Self::And(Box::new(self), Box::new(other))) + } + + fn new_child(self, other: Self) -> Result { + Ok(Self::Descendant(Box::new(self), Box::new(other))) + } + + fn new_eq(self, other: Self) -> Result { + if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) { + Ok(Self::Equal(left, right)) + } else { + anyhow::bail!("operands of == must be identifiers"); + } + } + + fn new_neq(self, other: Self) -> Result { + if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) { + Ok(Self::NotEqual(left, right)) + } else { + anyhow::bail!("operands of != must be identifiers"); + } + } +} + +const PRECEDENCE_CHILD: u32 = 1; +const PRECEDENCE_OR: u32 = 2; +const PRECEDENCE_AND: u32 = 3; +const PRECEDENCE_EQ: u32 = 4; +const PRECEDENCE_NOT: u32 = 5; + +fn is_identifier_char(c: char) -> bool { + c.is_alphanumeric() || c == '_' || c == '-' +} + +fn is_vim_operator_char(c: char) -> bool { + c == '>' || c == '<' || c == '~' || c == '"' || c == '?' +} + +fn skip_whitespace(source: &str) -> &str { + let len = source + .find(|c: char| !c.is_whitespace()) + .unwrap_or(source.len()); + &source[len..] +} + +#[cfg(test)] +mod tests { + use core::slice; + + use super::*; + use crate as gpui; + use KeyBindingContextPredicate::*; + + #[test] + fn test_actions_definition() { + { + actions!(test_only, [A, B, C, D, E, F, G]); + } + + { + actions!( + test_only, + [ + H, I, J, K, L, M, N, // Don't wrap, test the trailing comma + ] + ); + } + } + + #[test] + fn test_parse_context() { + let mut expected = KeyContext::default(); + expected.add("baz"); + expected.set("foo", "bar"); + assert_eq!(KeyContext::parse("baz foo=bar").unwrap(), expected); + assert_eq!(KeyContext::parse("baz foo = bar").unwrap(), expected); + assert_eq!( + KeyContext::parse(" baz foo = bar baz").unwrap(), + expected + ); + assert_eq!(KeyContext::parse(" baz foo = bar").unwrap(), expected); + } + + #[test] + fn test_parse_identifiers() { + // Identifiers + assert_eq!( + KeyBindingContextPredicate::parse("abc12").unwrap(), + Identifier("abc12".into()) + ); + assert_eq!( + KeyBindingContextPredicate::parse("_1a").unwrap(), + Identifier("_1a".into()) + ); + } + + #[test] + fn test_parse_negations() { + assert_eq!( + KeyBindingContextPredicate::parse("!abc").unwrap(), + Not(Box::new(Identifier("abc".into()))) + ); + assert_eq!( + KeyBindingContextPredicate::parse(" ! ! abc").unwrap(), + Not(Box::new(Not(Box::new(Identifier("abc".into()))))) + ); + } + + #[test] + fn test_parse_equality_operators() { + assert_eq!( + KeyBindingContextPredicate::parse("a == b").unwrap(), + Equal("a".into(), "b".into()) + ); + assert_eq!( + KeyBindingContextPredicate::parse("c!=d").unwrap(), + NotEqual("c".into(), "d".into()) + ); + assert_eq!( + KeyBindingContextPredicate::parse("c == !d") + .unwrap_err() + .to_string(), + "operands of == must be identifiers" + ); + } + + #[test] + fn test_parse_boolean_operators() { + assert_eq!( + KeyBindingContextPredicate::parse("a || b").unwrap(), + Or( + Box::new(Identifier("a".into())), + Box::new(Identifier("b".into())) + ) + ); + assert_eq!( + KeyBindingContextPredicate::parse("a || !b && c").unwrap(), + Or( + Box::new(Identifier("a".into())), + Box::new(And( + Box::new(Not(Box::new(Identifier("b".into())))), + Box::new(Identifier("c".into())) + )) + ) + ); + assert_eq!( + KeyBindingContextPredicate::parse("a && b || c&&d").unwrap(), + Or( + Box::new(And( + Box::new(Identifier("a".into())), + Box::new(Identifier("b".into())) + )), + Box::new(And( + Box::new(Identifier("c".into())), + Box::new(Identifier("d".into())) + )) + ) + ); + assert_eq!( + KeyBindingContextPredicate::parse("a == b && c || d == e && f").unwrap(), + Or( + Box::new(And( + Box::new(Equal("a".into(), "b".into())), + Box::new(Identifier("c".into())) + )), + Box::new(And( + Box::new(Equal("d".into(), "e".into())), + Box::new(Identifier("f".into())) + )) + ) + ); + assert_eq!( + KeyBindingContextPredicate::parse("a && b && c && d").unwrap(), + And( + Box::new(And( + Box::new(And( + Box::new(Identifier("a".into())), + Box::new(Identifier("b".into())) + )), + Box::new(Identifier("c".into())), + )), + Box::new(Identifier("d".into())) + ), + ); + } + + #[test] + fn test_parse_parenthesized_expressions() { + assert_eq!( + KeyBindingContextPredicate::parse("a && (b == c || d != e)").unwrap(), + And( + Box::new(Identifier("a".into())), + Box::new(Or( + Box::new(Equal("b".into(), "c".into())), + Box::new(NotEqual("d".into(), "e".into())), + )), + ), + ); + assert_eq!( + KeyBindingContextPredicate::parse(" ( a || b ) ").unwrap(), + Or( + Box::new(Identifier("a".into())), + Box::new(Identifier("b".into())), + ) + ); + } + + #[test] + fn test_is_superset() { + assert_is_superset("editor", "editor", true); + assert_is_superset("editor", "workspace", false); + + assert_is_superset("editor", "editor && vim_mode", true); + assert_is_superset("editor", "mode == full && editor", true); + assert_is_superset("editor && mode == full", "editor", false); + + assert_is_superset("editor", "something > editor", true); + assert_is_superset("editor", "editor > menu", false); + + assert_is_superset("foo || bar || baz", "bar", true); + assert_is_superset("foo || bar || baz", "quux", false); + + #[track_caller] + fn assert_is_superset(a: &str, b: &str, result: bool) { + let a = KeyBindingContextPredicate::parse(a).unwrap(); + let b = KeyBindingContextPredicate::parse(b).unwrap(); + assert_eq!(a.is_superset(&b), result, "({a:?}).is_superset({b:?})"); + } + } + + #[test] + fn test_child_operator() { + let predicate = KeyBindingContextPredicate::parse("parent > child").unwrap(); + + let parent_context = KeyContext::try_from("parent").unwrap(); + let child_context = KeyContext::try_from("child").unwrap(); + + let contexts = vec![parent_context.clone(), child_context.clone()]; + assert!(predicate.eval(&contexts)); + + let grandparent_context = KeyContext::try_from("grandparent").unwrap(); + + let contexts = vec![ + grandparent_context, + parent_context.clone(), + child_context.clone(), + ]; + assert!(predicate.eval(&contexts)); + + let other_context = KeyContext::try_from("other").unwrap(); + + let contexts = vec![other_context.clone(), child_context.clone()]; + assert!(!predicate.eval(&contexts)); + + let contexts = vec![parent_context.clone(), other_context, child_context.clone()]; + assert!(predicate.eval(&contexts)); + + assert!(!predicate.eval(&[])); + assert!(!predicate.eval(slice::from_ref(&child_context))); + assert!(!predicate.eval(&[parent_context])); + + let zany_predicate = KeyBindingContextPredicate::parse("child > child").unwrap(); + assert!(!zany_predicate.eval(slice::from_ref(&child_context))); + assert!(zany_predicate.eval(&[child_context.clone(), child_context])); + } + + #[test] + fn test_not_operator() { + let not_predicate = KeyBindingContextPredicate::parse("!editor").unwrap(); + let editor_context = KeyContext::try_from("editor").unwrap(); + let workspace_context = KeyContext::try_from("workspace").unwrap(); + let parent_context = KeyContext::try_from("parent").unwrap(); + let child_context = KeyContext::try_from("child").unwrap(); + + assert!(not_predicate.eval(slice::from_ref(&workspace_context))); + assert!(!not_predicate.eval(slice::from_ref(&editor_context))); + assert!(!not_predicate.eval(&[editor_context.clone(), workspace_context.clone()])); + assert!(!not_predicate.eval(&[workspace_context.clone(), editor_context.clone()])); + + let complex_not = KeyBindingContextPredicate::parse("!editor && workspace").unwrap(); + assert!(complex_not.eval(slice::from_ref(&workspace_context))); + assert!(!complex_not.eval(&[editor_context.clone(), workspace_context.clone()])); + + let not_mode_predicate = KeyBindingContextPredicate::parse("!(mode == full)").unwrap(); + let mut mode_context = KeyContext::default(); + mode_context.set("mode", "full"); + assert!(!not_mode_predicate.eval(&[mode_context.clone()])); + + let mut other_mode_context = KeyContext::default(); + other_mode_context.set("mode", "partial"); + assert!(not_mode_predicate.eval(&[other_mode_context])); + + let not_descendant = KeyBindingContextPredicate::parse("!(parent > child)").unwrap(); + assert!(not_descendant.eval(slice::from_ref(&parent_context))); + assert!(not_descendant.eval(slice::from_ref(&child_context))); + assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()])); + + let not_descendant = KeyBindingContextPredicate::parse("parent > !child").unwrap(); + assert!(!not_descendant.eval(slice::from_ref(&parent_context))); + assert!(!not_descendant.eval(slice::from_ref(&child_context))); + assert!(!not_descendant.eval(&[parent_context, child_context])); + + let double_not = KeyBindingContextPredicate::parse("!!editor").unwrap(); + assert!(double_not.eval(slice::from_ref(&editor_context))); + assert!(!double_not.eval(slice::from_ref(&workspace_context))); + + // Test complex descendant cases + let workspace_context = KeyContext::try_from("Workspace").unwrap(); + let pane_context = KeyContext::try_from("Pane").unwrap(); + let editor_context = KeyContext::try_from("Editor").unwrap(); + + // Workspace > Pane > Editor + let workspace_pane_editor = vec![ + workspace_context.clone(), + pane_context.clone(), + editor_context.clone(), + ]; + + // Pane > (Pane > Editor) - should not match + let pane_pane_editor = KeyBindingContextPredicate::parse("Pane > (Pane > Editor)").unwrap(); + assert!(!pane_pane_editor.eval(&workspace_pane_editor)); + + let workspace_pane_editor_predicate = + KeyBindingContextPredicate::parse("Workspace > Pane > Editor").unwrap(); + assert!(workspace_pane_editor_predicate.eval(&workspace_pane_editor)); + + // (Pane > Pane) > Editor - should not match + let pane_pane_then_editor = + KeyBindingContextPredicate::parse("(Pane > Pane) > Editor").unwrap(); + assert!(!pane_pane_then_editor.eval(&workspace_pane_editor)); + + // Pane > !Workspace - should match + let pane_not_workspace = KeyBindingContextPredicate::parse("Pane > !Workspace").unwrap(); + assert!(pane_not_workspace.eval(&[pane_context.clone(), editor_context.clone()])); + assert!(!pane_not_workspace.eval(&[pane_context.clone(), workspace_context.clone()])); + + // !Workspace - shouldn't match when Workspace is in the context + let not_workspace = KeyBindingContextPredicate::parse("!Workspace").unwrap(); + assert!(!not_workspace.eval(slice::from_ref(&workspace_context))); + assert!(not_workspace.eval(slice::from_ref(&pane_context))); + assert!(not_workspace.eval(slice::from_ref(&editor_context))); + assert!(!not_workspace.eval(&workspace_pane_editor)); + } +} diff --git a/third_party/gpui/src/path_builder.rs b/third_party/gpui/src/path_builder.rs new file mode 100644 index 0000000..40a6e71 --- /dev/null +++ b/third_party/gpui/src/path_builder.rs @@ -0,0 +1,347 @@ +use anyhow::Error; +use etagere::euclid::{Point2D, Vector2D}; +use lyon::geom::Angle; +use lyon::math::{Vector, vector}; +use lyon::path::traits::SvgPathBuilder; +use lyon::path::{ArcFlags, Polygon}; +use lyon::tessellation::{ + BuffersBuilder, FillTessellator, FillVertex, StrokeTessellator, StrokeVertex, VertexBuffers, +}; + +pub use lyon::math::Transform; +pub use lyon::tessellation::{FillOptions, FillRule, StrokeOptions}; + +use crate::{Path, Pixels, Point, point, px}; + +/// Style of the PathBuilder +pub enum PathStyle { + /// Stroke style + Stroke(StrokeOptions), + /// Fill style + Fill(FillOptions), +} + +/// A [`Path`] builder. +pub struct PathBuilder { + raw: lyon::path::builder::WithSvg, + transform: Option, + /// PathStyle of the PathBuilder + pub style: PathStyle, + dash_array: Option>, +} + +impl From for PathBuilder { + fn from(builder: lyon::path::Builder) -> Self { + Self { + raw: builder.with_svg(), + ..Default::default() + } + } +} + +impl From> for PathBuilder { + fn from(raw: lyon::path::builder::WithSvg) -> Self { + Self { + raw, + ..Default::default() + } + } +} + +impl From for Point { + fn from(p: lyon::math::Point) -> Self { + point(px(p.x), px(p.y)) + } +} + +impl From> for lyon::math::Point { + fn from(p: Point) -> Self { + lyon::math::point(p.x.0, p.y.0) + } +} + +impl From> for Vector { + fn from(p: Point) -> Self { + vector(p.x.0, p.y.0) + } +} + +impl From> for Point2D { + fn from(p: Point) -> Self { + Point2D::new(p.x.0, p.y.0) + } +} + +impl Default for PathBuilder { + fn default() -> Self { + Self { + raw: lyon::path::Path::builder().with_svg(), + style: PathStyle::Fill(FillOptions::default()), + transform: None, + dash_array: None, + } + } +} + +impl PathBuilder { + /// Creates a new [`PathBuilder`] to build a Stroke path. + pub fn stroke(width: Pixels) -> Self { + Self { + style: PathStyle::Stroke(StrokeOptions::default().with_line_width(width.0)), + ..Self::default() + } + } + + /// Creates a new [`PathBuilder`] to build a Fill path. + pub fn fill() -> Self { + Self::default() + } + + /// Sets the style of the [`PathBuilder`]. + pub fn with_style(self, style: PathStyle) -> Self { + Self { style, ..self } + } + + /// Sets the dash array of the [`PathBuilder`]. + /// + /// [MDN](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/stroke-dasharray) + pub fn dash_array(mut self, dash_array: &[Pixels]) -> Self { + // If an odd number of values is provided, then the list of values is repeated to yield an even number of values. + // Thus, 5,3,2 is equivalent to 5,3,2,5,3,2. + let array = if dash_array.len() % 2 == 1 { + let mut new_dash_array = dash_array.to_vec(); + new_dash_array.extend_from_slice(dash_array); + new_dash_array + } else { + dash_array.to_vec() + }; + + self.dash_array = Some(array); + self + } + + /// Move the current point to the given point. + #[inline] + pub fn move_to(&mut self, to: Point) { + self.raw.move_to(to.into()); + } + + /// Draw a straight line from the current point to the given point. + #[inline] + pub fn line_to(&mut self, to: Point) { + self.raw.line_to(to.into()); + } + + /// Draw a curve from the current point to the given point, using the given control point. + #[inline] + pub fn curve_to(&mut self, to: Point, ctrl: Point) { + self.raw.quadratic_bezier_to(ctrl.into(), to.into()); + } + + /// Adds a cubic Bézier to the [`Path`] given its two control points + /// and its end point. + #[inline] + pub fn cubic_bezier_to( + &mut self, + to: Point, + control_a: Point, + control_b: Point, + ) { + self.raw + .cubic_bezier_to(control_a.into(), control_b.into(), to.into()); + } + + /// Adds an elliptical arc. + pub fn arc_to( + &mut self, + radii: Point, + x_rotation: Pixels, + large_arc: bool, + sweep: bool, + to: Point, + ) { + self.raw.arc_to( + radii.into(), + Angle::degrees(x_rotation.into()), + ArcFlags { large_arc, sweep }, + to.into(), + ); + } + + /// Equivalent to `arc_to` in relative coordinates. + pub fn relative_arc_to( + &mut self, + radii: Point, + x_rotation: Pixels, + large_arc: bool, + sweep: bool, + to: Point, + ) { + self.raw.relative_arc_to( + radii.into(), + Angle::degrees(x_rotation.into()), + ArcFlags { large_arc, sweep }, + to.into(), + ); + } + + /// Adds a polygon. + pub fn add_polygon(&mut self, points: &[Point], closed: bool) { + let points = points.iter().copied().map(|p| p.into()).collect::>(); + self.raw.add_polygon(Polygon { + points: points.as_ref(), + closed, + }); + } + + /// Close the current sub-path. + #[inline] + pub fn close(&mut self) { + self.raw.close(); + } + + /// Applies a transform to the path. + #[inline] + pub fn transform(&mut self, transform: Transform) { + self.transform = Some(transform); + } + + /// Applies a translation to the path. + #[inline] + pub fn translate(&mut self, to: Point) { + if let Some(transform) = self.transform { + self.transform = Some(transform.then_translate(Vector2D::new(to.x.0, to.y.0))); + } else { + self.transform = Some(Transform::translation(to.x.0, to.y.0)) + } + } + + /// Applies a scale to the path. + #[inline] + pub fn scale(&mut self, scale: f32) { + if let Some(transform) = self.transform { + self.transform = Some(transform.then_scale(scale, scale)); + } else { + self.transform = Some(Transform::scale(scale, scale)); + } + } + + /// Applies a rotation to the path. + /// + /// The `angle` is in degrees value in the range 0.0 to 360.0. + #[inline] + pub fn rotate(&mut self, angle: f32) { + let radians = angle.to_radians(); + if let Some(transform) = self.transform { + self.transform = Some(transform.then_rotate(Angle::radians(radians))); + } else { + self.transform = Some(Transform::rotation(Angle::radians(radians))); + } + } + + /// Builds into a [`Path`]. + #[inline] + pub fn build(self) -> Result, Error> { + let path = if let Some(transform) = self.transform { + self.raw.build().transformed(&transform) + } else { + self.raw.build() + }; + + match self.style { + PathStyle::Stroke(options) => Self::tessellate_stroke(self.dash_array, &path, &options), + PathStyle::Fill(options) => Self::tessellate_fill(&path, &options), + } + } + + fn tessellate_fill( + path: &lyon::path::Path, + options: &FillOptions, + ) -> Result, Error> { + // Will contain the result of the tessellation. + let mut buf: VertexBuffers = VertexBuffers::new(); + let mut tessellator = FillTessellator::new(); + + // Compute the tessellation. + tessellator.tessellate_path( + path, + options, + &mut BuffersBuilder::new(&mut buf, |vertex: FillVertex| vertex.position()), + )?; + + Ok(Self::build_path(buf)) + } + + fn tessellate_stroke( + dash_array: Option>, + path: &lyon::path::Path, + options: &StrokeOptions, + ) -> Result, Error> { + let path = if let Some(dash_array) = dash_array { + let measurements = lyon::algorithms::measure::PathMeasurements::from_path(path, 0.01); + let mut sampler = measurements + .create_sampler(path, lyon::algorithms::measure::SampleType::Normalized); + let mut builder = lyon::path::Path::builder(); + + let total_length = sampler.length(); + let dash_array_len = dash_array.len(); + let mut pos = 0.; + let mut dash_index = 0; + while pos < total_length { + let dash_length = dash_array[dash_index % dash_array_len].0; + let next_pos = (pos + dash_length).min(total_length); + if dash_index % 2 == 0 { + let start = pos / total_length; + let end = next_pos / total_length; + sampler.split_range(start..end, &mut builder); + } + pos = next_pos; + dash_index += 1; + } + + &builder.build() + } else { + path + }; + + // Will contain the result of the tessellation. + let mut buf: VertexBuffers = VertexBuffers::new(); + let mut tessellator = StrokeTessellator::new(); + + // Compute the tessellation. + tessellator.tessellate_path( + path, + options, + &mut BuffersBuilder::new(&mut buf, |vertex: StrokeVertex| vertex.position()), + )?; + + Ok(Self::build_path(buf)) + } + + /// Builds a [`Path`] from a [`lyon::tessellation::VertexBuffers`]. + pub fn build_path(buf: VertexBuffers) -> Path { + if buf.vertices.is_empty() { + return Path::new(Point::default()); + } + + let first_point = buf.vertices[0]; + + let mut path = Path::new(first_point.into()); + for i in 0..buf.indices.len() / 3 { + let i0 = buf.indices[i * 3] as usize; + let i1 = buf.indices[i * 3 + 1] as usize; + let i2 = buf.indices[i * 3 + 2] as usize; + + let v0 = buf.vertices[i0]; + let v1 = buf.vertices[i1]; + let v2 = buf.vertices[i2]; + + path.push_triangle( + (v0.into(), v1.into(), v2.into()), + (point(0., 1.), point(0., 1.), point(0., 1.)), + ); + } + + path + } +} diff --git a/third_party/gpui/src/platform.rs b/third_party/gpui/src/platform.rs new file mode 100644 index 0000000..047a005 --- /dev/null +++ b/third_party/gpui/src/platform.rs @@ -0,0 +1,1862 @@ +mod app_menu; +mod keyboard; +mod keystroke; + +#[cfg(any(target_os = "linux", target_os = "freebsd"))] +mod linux; + +#[cfg(target_os = "macos")] +mod mac; + +#[cfg(any( + all( + any(target_os = "linux", target_os = "freebsd"), + any(feature = "x11", feature = "wayland") + ), + all(target_os = "macos", feature = "macos-blade") +))] +mod blade; + +#[cfg(any(test, feature = "test-support"))] +mod test; + +#[cfg(target_os = "windows")] +mod windows; + +#[cfg(all( + feature = "screen-capture", + any( + target_os = "windows", + all( + any(target_os = "linux", target_os = "freebsd"), + any(feature = "wayland", feature = "x11"), + ) + ) +))] +pub(crate) mod scap_screen_capture; + +use crate::{ + Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds, + DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun, + ForegroundExecutor, GlyphId, GpuSpecs, ImageSource, Keymap, LineLayout, Pixels, PlatformInput, + Point, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Scene, ShapedGlyph, + ShapedRun, SharedString, Size, SvgRenderer, SvgSize, SystemWindowTab, Task, TaskLabel, Window, + WindowControlArea, hash, point, px, size, +}; +use anyhow::Result; +use async_task::Runnable; +use futures::channel::oneshot; +use image::codecs::gif::GifDecoder; +use image::{AnimationDecoder as _, Frame}; +use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; +use schemars::JsonSchema; +use seahash::SeaHasher; +use serde::{Deserialize, Serialize}; +use smallvec::SmallVec; +use std::borrow::Cow; +use std::hash::{Hash, Hasher}; +use std::io::Cursor; +use std::ops; +use std::time::{Duration, Instant}; +use std::{ + fmt::{self, Debug}, + ops::Range, + path::{Path, PathBuf}, + rc::Rc, + sync::Arc, +}; +use strum::EnumIter; +use uuid::Uuid; + +pub use app_menu::*; +pub use keyboard::*; +pub use keystroke::*; + +#[cfg(any(target_os = "linux", target_os = "freebsd"))] +pub(crate) use linux::*; +#[cfg(target_os = "macos")] +pub(crate) use mac::*; +pub use semantic_version::SemanticVersion; +#[cfg(any(test, feature = "test-support"))] +pub(crate) use test::*; +#[cfg(target_os = "windows")] +pub(crate) use windows::*; + +#[cfg(any(test, feature = "test-support"))] +pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream}; + +/// Returns a background executor for the current platform. +pub fn background_executor() -> BackgroundExecutor { + current_platform(true).background_executor() +} + +#[cfg(target_os = "macos")] +pub(crate) fn current_platform(headless: bool) -> Rc { + Rc::new(MacPlatform::new(headless)) +} + +#[cfg(any(target_os = "linux", target_os = "freebsd"))] +pub(crate) fn current_platform(headless: bool) -> Rc { + #[cfg(feature = "x11")] + use anyhow::Context as _; + + if headless { + return Rc::new(HeadlessClient::new()); + } + + match guess_compositor() { + #[cfg(feature = "wayland")] + "Wayland" => Rc::new(WaylandClient::new()), + + #[cfg(feature = "x11")] + "X11" => Rc::new( + X11Client::new() + .context("Failed to initialize X11 client.") + .unwrap(), + ), + + "Headless" => Rc::new(HeadlessClient::new()), + _ => unreachable!(), + } +} + +/// Return which compositor we're guessing we'll use. +/// Does not attempt to connect to the given compositor +#[cfg(any(target_os = "linux", target_os = "freebsd"))] +#[inline] +pub fn guess_compositor() -> &'static str { + if std::env::var_os("ZED_HEADLESS").is_some() { + return "Headless"; + } + + #[cfg(feature = "wayland")] + let wayland_display = std::env::var_os("WAYLAND_DISPLAY"); + #[cfg(not(feature = "wayland"))] + let wayland_display: Option = None; + + #[cfg(feature = "x11")] + let x11_display = std::env::var_os("DISPLAY"); + #[cfg(not(feature = "x11"))] + let x11_display: Option = None; + + let use_wayland = wayland_display.is_some_and(|display| !display.is_empty()); + let use_x11 = x11_display.is_some_and(|display| !display.is_empty()); + + if use_wayland { + "Wayland" + } else if use_x11 { + "X11" + } else { + "Headless" + } +} + +#[cfg(target_os = "windows")] +pub(crate) fn current_platform(_headless: bool) -> Rc { + Rc::new( + WindowsPlatform::new() + .inspect_err(|err| show_error("Failed to launch", err.to_string())) + .unwrap(), + ) +} + +pub(crate) trait Platform: 'static { + fn background_executor(&self) -> BackgroundExecutor; + fn foreground_executor(&self) -> ForegroundExecutor; + fn text_system(&self) -> Arc; + + fn run(&self, on_finish_launching: Box); + fn quit(&self); + fn restart(&self, binary_path: Option); + fn activate(&self, ignoring_other_apps: bool); + fn hide(&self); + fn hide_other_apps(&self); + fn unhide_other_apps(&self); + + fn displays(&self) -> Vec>; + fn primary_display(&self) -> Option>; + fn active_window(&self) -> Option; + fn window_stack(&self) -> Option> { + None + } + + #[cfg(feature = "screen-capture")] + fn is_screen_capture_supported(&self) -> bool; + #[cfg(not(feature = "screen-capture"))] + fn is_screen_capture_supported(&self) -> bool { + false + } + #[cfg(feature = "screen-capture")] + fn screen_capture_sources(&self) + -> oneshot::Receiver>>>; + #[cfg(not(feature = "screen-capture"))] + fn screen_capture_sources( + &self, + ) -> oneshot::Receiver>>> { + let (sources_tx, sources_rx) = oneshot::channel(); + sources_tx + .send(Err(anyhow::anyhow!( + "gpui was compiled without the screen-capture feature" + ))) + .ok(); + sources_rx + } + + fn open_window( + &self, + handle: AnyWindowHandle, + options: WindowParams, + ) -> anyhow::Result>; + + /// Returns the appearance of the application's windows. + fn window_appearance(&self) -> WindowAppearance; + + fn open_url(&self, url: &str); + fn on_open_urls(&self, callback: Box)>); + fn register_url_scheme(&self, url: &str) -> Task>; + + fn prompt_for_paths( + &self, + options: PathPromptOptions, + ) -> oneshot::Receiver>>>; + fn prompt_for_new_path( + &self, + directory: &Path, + suggested_name: Option<&str>, + ) -> oneshot::Receiver>>; + fn can_select_mixed_files_and_dirs(&self) -> bool; + fn reveal_path(&self, path: &Path); + fn open_with_system(&self, path: &Path); + + fn on_quit(&self, callback: Box); + fn on_reopen(&self, callback: Box); + + fn set_menus(&self, menus: Vec, keymap: &Keymap); + fn get_menus(&self) -> Option> { + None + } + + fn set_dock_menu(&self, menu: Vec, keymap: &Keymap); + fn perform_dock_menu_action(&self, _action: usize) {} + fn add_recent_document(&self, _path: &Path) {} + fn update_jump_list( + &self, + _menus: Vec, + _entries: Vec>, + ) -> Vec> { + Vec::new() + } + fn on_app_menu_action(&self, callback: Box); + fn on_will_open_app_menu(&self, callback: Box); + fn on_validate_app_menu_command(&self, callback: Box bool>); + + fn compositor_name(&self) -> &'static str { + "" + } + fn app_path(&self) -> Result; + fn path_for_auxiliary_executable(&self, name: &str) -> Result; + + fn set_cursor_style(&self, style: CursorStyle); + fn should_auto_hide_scrollbars(&self) -> bool; + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + fn write_to_primary(&self, item: ClipboardItem); + fn write_to_clipboard(&self, item: ClipboardItem); + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + fn read_from_primary(&self) -> Option; + fn read_from_clipboard(&self) -> Option; + + fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task>; + fn read_credentials(&self, url: &str) -> Task)>>>; + fn delete_credentials(&self, url: &str) -> Task>; + + fn keyboard_layout(&self) -> Box; + fn keyboard_mapper(&self) -> Rc; + fn on_keyboard_layout_change(&self, callback: Box); +} + +/// A handle to a platform's display, e.g. a monitor or laptop screen. +pub trait PlatformDisplay: Send + Sync + Debug { + /// Get the ID for this display + fn id(&self) -> DisplayId; + + /// Returns a stable identifier for this display that can be persisted and used + /// across system restarts. + fn uuid(&self) -> Result; + + /// Get the bounds for this display + fn bounds(&self) -> Bounds; + + /// Get the default bounds for this display to place a window + fn default_bounds(&self) -> Bounds { + let bounds = self.bounds(); + let center = bounds.center(); + let clipped_window_size = DEFAULT_WINDOW_SIZE.min(&bounds.size); + + let offset = clipped_window_size / 2.0; + let origin = point(center.x - offset.width, center.y - offset.height); + Bounds::new(origin, clipped_window_size) + } +} + +/// Metadata for a given [ScreenCaptureSource] +#[derive(Clone)] +pub struct SourceMetadata { + /// Opaque identifier of this screen. + pub id: u64, + /// Human-readable label for this source. + pub label: Option, + /// Whether this source is the main display. + pub is_main: Option, + /// Video resolution of this source. + pub resolution: Size, +} + +/// A source of on-screen video content that can be captured. +pub trait ScreenCaptureSource { + /// Returns metadata for this source. + fn metadata(&self) -> Result; + + /// Start capture video from this source, invoking the given callback + /// with each frame. + fn stream( + &self, + foreground_executor: &ForegroundExecutor, + frame_callback: Box, + ) -> oneshot::Receiver>>; +} + +/// A video stream captured from a screen. +pub trait ScreenCaptureStream { + /// Returns metadata for this source. + fn metadata(&self) -> Result; +} + +/// A frame of video captured from a screen. +pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame); + +/// An opaque identifier for a hardware display +#[derive(PartialEq, Eq, Hash, Copy, Clone)] +pub struct DisplayId(pub(crate) u32); + +impl From for u32 { + fn from(id: DisplayId) -> Self { + id.0 + } +} + +impl Debug for DisplayId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "DisplayId({})", self.0) + } +} + +/// Which part of the window to resize +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResizeEdge { + /// The top edge + Top, + /// The top right corner + TopRight, + /// The right edge + Right, + /// The bottom right corner + BottomRight, + /// The bottom edge + Bottom, + /// The bottom left corner + BottomLeft, + /// The left edge + Left, + /// The top left corner + TopLeft, +} + +/// A type to describe the appearance of a window +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)] +pub enum WindowDecorations { + #[default] + /// Server side decorations + Server, + /// Client side decorations + Client, +} + +/// A type to describe how this window is currently configured +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)] +pub enum Decorations { + /// The window is configured to use server side decorations + #[default] + Server, + /// The window is configured to use client side decorations + Client { + /// The edge tiling state + tiling: Tiling, + }, +} + +/// What window controls this platform supports +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct WindowControls { + /// Whether this platform supports fullscreen + pub fullscreen: bool, + /// Whether this platform supports maximize + pub maximize: bool, + /// Whether this platform supports minimize + pub minimize: bool, + /// Whether this platform supports a window menu + pub window_menu: bool, +} + +impl Default for WindowControls { + fn default() -> Self { + // Assume that we can do anything, unless told otherwise + Self { + fullscreen: true, + maximize: true, + minimize: true, + window_menu: true, + } + } +} + +/// A type to describe which sides of the window are currently tiled in some way +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)] +pub struct Tiling { + /// Whether the top edge is tiled + pub top: bool, + /// Whether the left edge is tiled + pub left: bool, + /// Whether the right edge is tiled + pub right: bool, + /// Whether the bottom edge is tiled + pub bottom: bool, +} + +impl Tiling { + /// Initializes a [`Tiling`] type with all sides tiled + pub fn tiled() -> Self { + Self { + top: true, + left: true, + right: true, + bottom: true, + } + } + + /// Whether any edge is tiled + pub fn is_tiled(&self) -> bool { + self.top || self.left || self.right || self.bottom + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] +pub(crate) struct RequestFrameOptions { + pub(crate) require_presentation: bool, + /// Force refresh of all rendering states when true + pub(crate) force_render: bool, +} + +pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { + fn bounds(&self) -> Bounds; + fn is_maximized(&self) -> bool; + fn window_bounds(&self) -> WindowBounds; + fn content_size(&self) -> Size; + fn resize(&mut self, size: Size); + fn scale_factor(&self) -> f32; + fn appearance(&self) -> WindowAppearance; + fn display(&self) -> Option>; + fn mouse_position(&self) -> Point; + fn modifiers(&self) -> Modifiers; + fn capslock(&self) -> Capslock; + fn set_input_handler(&mut self, input_handler: PlatformInputHandler); + fn take_input_handler(&mut self) -> Option; + fn prompt( + &self, + level: PromptLevel, + msg: &str, + detail: Option<&str>, + answers: &[PromptButton], + ) -> Option>; + fn activate(&self); + fn is_active(&self) -> bool; + fn is_hovered(&self) -> bool; + fn set_title(&mut self, title: &str); + fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance); + fn minimize(&self); + fn zoom(&self); + fn toggle_fullscreen(&self); + fn is_fullscreen(&self) -> bool; + fn on_request_frame(&self, callback: Box); + fn on_input(&self, callback: Box DispatchEventResult>); + fn on_active_status_change(&self, callback: Box); + fn on_hover_status_change(&self, callback: Box); + fn on_resize(&self, callback: Box, f32)>); + fn on_moved(&self, callback: Box); + fn on_should_close(&self, callback: Box bool>); + fn on_hit_test_window_control(&self, callback: Box Option>); + fn on_close(&self, callback: Box); + fn on_appearance_changed(&self, callback: Box); + fn draw(&self, scene: &Scene); + fn completed_frame(&self) {} + fn sprite_atlas(&self) -> Arc; + + // macOS specific methods + fn get_title(&self) -> String { + String::new() + } + fn tabbed_windows(&self) -> Option> { + None + } + fn tab_bar_visible(&self) -> bool { + false + } + fn set_edited(&mut self, _edited: bool) {} + fn show_character_palette(&self) {} + fn titlebar_double_click(&self) {} + fn on_move_tab_to_new_window(&self, _callback: Box) {} + fn on_merge_all_windows(&self, _callback: Box) {} + fn on_select_previous_tab(&self, _callback: Box) {} + fn on_select_next_tab(&self, _callback: Box) {} + fn on_toggle_tab_bar(&self, _callback: Box) {} + fn merge_all_windows(&self) {} + fn move_tab_to_new_window(&self) {} + fn toggle_window_tab_overview(&self) {} + fn set_tabbing_identifier(&self, _identifier: Option) {} + + #[cfg(target_os = "windows")] + fn get_raw_handle(&self) -> windows::HWND; + + // Linux specific methods + fn inner_window_bounds(&self) -> WindowBounds { + self.window_bounds() + } + fn request_decorations(&self, _decorations: WindowDecorations) {} + fn show_window_menu(&self, _position: Point) {} + fn start_window_move(&self) {} + fn start_window_resize(&self, _edge: ResizeEdge) {} + fn window_decorations(&self) -> Decorations { + Decorations::Server + } + fn set_app_id(&mut self, _app_id: &str) {} + fn map_window(&mut self) -> anyhow::Result<()> { + Ok(()) + } + fn window_controls(&self) -> WindowControls { + WindowControls::default() + } + fn set_client_inset(&self, _inset: Pixels) {} + fn gpu_specs(&self) -> Option; + + fn update_ime_position(&self, _bounds: Bounds); + + #[cfg(any(test, feature = "test-support"))] + fn as_test(&mut self) -> Option<&mut TestWindow> { + None + } +} + +/// This type is public so that our test macro can generate and use it, but it should not +/// be considered part of our public API. +#[doc(hidden)] +pub trait PlatformDispatcher: Send + Sync { + fn is_main_thread(&self) -> bool; + fn dispatch(&self, runnable: Runnable, label: Option); + fn dispatch_on_main_thread(&self, runnable: Runnable); + fn dispatch_after(&self, duration: Duration, runnable: Runnable); + fn now(&self) -> Instant { + Instant::now() + } + + #[cfg(any(test, feature = "test-support"))] + fn as_test(&self) -> Option<&TestDispatcher> { + None + } +} + +pub(crate) trait PlatformTextSystem: Send + Sync { + fn add_fonts(&self, fonts: Vec>) -> Result<()>; + fn all_font_names(&self) -> Vec; + fn font_id(&self, descriptor: &Font) -> Result; + fn font_metrics(&self, font_id: FontId) -> FontMetrics; + fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result>; + fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result>; + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option; + fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result>; + fn rasterize_glyph( + &self, + params: &RenderGlyphParams, + raster_bounds: Bounds, + ) -> Result<(Size, Vec)>; + fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout; +} + +pub(crate) struct NoopTextSystem; + +impl NoopTextSystem { + #[allow(dead_code)] + pub fn new() -> Self { + Self + } +} + +impl PlatformTextSystem for NoopTextSystem { + fn add_fonts(&self, _fonts: Vec>) -> Result<()> { + Ok(()) + } + + fn all_font_names(&self) -> Vec { + Vec::new() + } + + fn font_id(&self, _descriptor: &Font) -> Result { + Ok(FontId(1)) + } + + fn font_metrics(&self, _font_id: FontId) -> FontMetrics { + FontMetrics { + units_per_em: 1000, + ascent: 1025.0, + descent: -275.0, + line_gap: 0.0, + underline_position: -95.0, + underline_thickness: 60.0, + cap_height: 698.0, + x_height: 516.0, + bounding_box: Bounds { + origin: Point { + x: -260.0, + y: -245.0, + }, + size: Size { + width: 1501.0, + height: 1364.0, + }, + }, + } + } + + fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result> { + Ok(Bounds { + origin: Point { x: 54.0, y: 0.0 }, + size: size(392.0, 528.0), + }) + } + + fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result> { + Ok(size(600.0 * glyph_id.0 as f32, 0.0)) + } + + fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option { + Some(GlyphId(ch.len_utf16() as u32)) + } + + fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result> { + Ok(Default::default()) + } + + fn rasterize_glyph( + &self, + _params: &RenderGlyphParams, + raster_bounds: Bounds, + ) -> Result<(Size, Vec)> { + Ok((raster_bounds.size, Vec::new())) + } + + fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout { + let mut position = px(0.); + let metrics = self.font_metrics(FontId(0)); + let em_width = font_size + * self + .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap()) + .unwrap() + .width + / metrics.units_per_em as f32; + let mut glyphs = Vec::new(); + for (ix, c) in text.char_indices() { + if let Some(glyph) = self.glyph_for_char(FontId(0), c) { + glyphs.push(ShapedGlyph { + id: glyph, + position: point(position, px(0.)), + index: ix, + is_emoji: glyph.0 == 2, + }); + if glyph.0 == 2 { + position += em_width * 2.0; + } else { + position += em_width; + } + } else { + position += em_width + } + } + let mut runs = Vec::default(); + if !glyphs.is_empty() { + runs.push(ShapedRun { + font_id: FontId(0), + glyphs, + }); + } else { + position = px(0.); + } + + LineLayout { + font_size, + width: position, + ascent: font_size * (metrics.ascent / metrics.units_per_em as f32), + descent: font_size * (metrics.descent / metrics.units_per_em as f32), + runs, + len: text.len(), + } + } +} + +#[derive(PartialEq, Eq, Hash, Clone)] +pub(crate) enum AtlasKey { + Glyph(RenderGlyphParams), + Svg(RenderSvgParams), + Image(RenderImageParams), +} + +impl AtlasKey { + #[cfg_attr( + all( + any(target_os = "linux", target_os = "freebsd"), + not(any(feature = "x11", feature = "wayland")) + ), + allow(dead_code) + )] + pub(crate) fn texture_kind(&self) -> AtlasTextureKind { + match self { + AtlasKey::Glyph(params) => { + if params.is_emoji { + AtlasTextureKind::Polychrome + } else { + AtlasTextureKind::Monochrome + } + } + AtlasKey::Svg(_) => AtlasTextureKind::Monochrome, + AtlasKey::Image(_) => AtlasTextureKind::Polychrome, + } + } +} + +impl From for AtlasKey { + fn from(params: RenderGlyphParams) -> Self { + Self::Glyph(params) + } +} + +impl From for AtlasKey { + fn from(params: RenderSvgParams) -> Self { + Self::Svg(params) + } +} + +impl From for AtlasKey { + fn from(params: RenderImageParams) -> Self { + Self::Image(params) + } +} + +pub(crate) trait PlatformAtlas: Send + Sync { + fn get_or_insert_with<'a>( + &self, + key: &AtlasKey, + build: &mut dyn FnMut() -> Result, Cow<'a, [u8]>)>>, + ) -> Result>; + fn remove(&self, key: &AtlasKey); +} + +struct AtlasTextureList { + textures: Vec>, + free_list: Vec, +} + +impl Default for AtlasTextureList { + fn default() -> Self { + Self { + textures: Vec::default(), + free_list: Vec::default(), + } + } +} + +impl ops::Index for AtlasTextureList { + type Output = Option; + + fn index(&self, index: usize) -> &Self::Output { + &self.textures[index] + } +} + +impl AtlasTextureList { + #[allow(unused)] + fn drain(&mut self) -> std::vec::Drain<'_, Option> { + self.free_list.clear(); + self.textures.drain(..) + } + + #[allow(dead_code)] + fn iter_mut(&mut self) -> impl DoubleEndedIterator { + self.textures.iter_mut().flatten() + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +#[repr(C)] +pub(crate) struct AtlasTile { + pub(crate) texture_id: AtlasTextureId, + pub(crate) tile_id: TileId, + pub(crate) padding: u32, + pub(crate) bounds: Bounds, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[repr(C)] +pub(crate) struct AtlasTextureId { + // We use u32 instead of usize for Metal Shader Language compatibility + pub(crate) index: u32, + pub(crate) kind: AtlasTextureKind, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[repr(C)] +#[cfg_attr( + all( + any(target_os = "linux", target_os = "freebsd"), + not(any(feature = "x11", feature = "wayland")) + ), + allow(dead_code) +)] +pub(crate) enum AtlasTextureKind { + Monochrome = 0, + Polychrome = 1, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[repr(C)] +pub(crate) struct TileId(pub(crate) u32); + +impl From for TileId { + fn from(id: etagere::AllocId) -> Self { + Self(id.serialize()) + } +} + +impl From for etagere::AllocId { + fn from(id: TileId) -> Self { + Self::deserialize(id.0) + } +} + +pub(crate) struct PlatformInputHandler { + cx: AsyncWindowContext, + handler: Box, +} + +#[cfg_attr( + all( + any(target_os = "linux", target_os = "freebsd"), + not(any(feature = "x11", feature = "wayland")) + ), + allow(dead_code) +)] +impl PlatformInputHandler { + pub fn new(cx: AsyncWindowContext, handler: Box) -> Self { + Self { cx, handler } + } + + fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option { + self.cx + .update(|window, cx| { + self.handler + .selected_text_range(ignore_disabled_input, window, cx) + }) + .ok() + .flatten() + } + + #[cfg_attr(target_os = "windows", allow(dead_code))] + fn marked_text_range(&mut self) -> Option> { + self.cx + .update(|window, cx| self.handler.marked_text_range(window, cx)) + .ok() + .flatten() + } + + #[cfg_attr( + any(target_os = "linux", target_os = "freebsd", target_os = "windows"), + allow(dead_code) + )] + fn text_for_range( + &mut self, + range_utf16: Range, + adjusted: &mut Option>, + ) -> Option { + self.cx + .update(|window, cx| { + self.handler + .text_for_range(range_utf16, adjusted, window, cx) + }) + .ok() + .flatten() + } + + fn replace_text_in_range(&mut self, replacement_range: Option>, text: &str) { + self.cx + .update(|window, cx| { + self.handler + .replace_text_in_range(replacement_range, text, window, cx); + }) + .ok(); + } + + pub fn replace_and_mark_text_in_range( + &mut self, + range_utf16: Option>, + new_text: &str, + new_selected_range: Option>, + ) { + self.cx + .update(|window, cx| { + self.handler.replace_and_mark_text_in_range( + range_utf16, + new_text, + new_selected_range, + window, + cx, + ) + }) + .ok(); + } + + #[cfg_attr(target_os = "windows", allow(dead_code))] + fn unmark_text(&mut self) { + self.cx + .update(|window, cx| self.handler.unmark_text(window, cx)) + .ok(); + } + + fn bounds_for_range(&mut self, range_utf16: Range) -> Option> { + self.cx + .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx)) + .ok() + .flatten() + } + + #[allow(dead_code)] + fn apple_press_and_hold_enabled(&mut self) -> bool { + self.handler.apple_press_and_hold_enabled() + } + + pub(crate) fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) { + self.handler.replace_text_in_range(None, input, window, cx); + } + + pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option> { + let selection = self.handler.selected_text_range(true, window, cx)?; + self.handler.bounds_for_range( + if selection.reversed { + selection.range.start..selection.range.start + } else { + selection.range.end..selection.range.end + }, + window, + cx, + ) + } + + #[allow(unused)] + pub fn character_index_for_point(&mut self, point: Point) -> Option { + self.cx + .update(|window, cx| self.handler.character_index_for_point(point, window, cx)) + .ok() + .flatten() + } +} + +/// A struct representing a selection in a text buffer, in UTF16 characters. +/// This is different from a range because the head may be before the tail. +#[derive(Debug)] +pub struct UTF16Selection { + /// The range of text in the document this selection corresponds to + /// in UTF16 characters. + pub range: Range, + /// Whether the head of this selection is at the start (true), or end (false) + /// of the range + pub reversed: bool, +} + +/// Zed's interface for handling text input from the platform's IME system +/// This is currently a 1:1 exposure of the NSTextInputClient API: +/// +/// +pub trait InputHandler: 'static { + /// Get the range of the user's currently selected text, if any + /// Corresponds to [selectedRange()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438242-selectedrange) + /// + /// Return value is in terms of UTF-16 characters, from 0 to the length of the document + fn selected_text_range( + &mut self, + ignore_disabled_input: bool, + window: &mut Window, + cx: &mut App, + ) -> Option; + + /// Get the range of the currently marked text, if any + /// Corresponds to [markedRange()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438250-markedrange) + /// + /// Return value is in terms of UTF-16 characters, from 0 to the length of the document + fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option>; + + /// Get the text for the given document range in UTF-16 characters + /// Corresponds to [attributedSubstring(forProposedRange: actualRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438238-attributedsubstring) + /// + /// range_utf16 is in terms of UTF-16 characters + fn text_for_range( + &mut self, + range_utf16: Range, + adjusted_range: &mut Option>, + window: &mut Window, + cx: &mut App, + ) -> Option; + + /// Replace the text in the given document range with the given text + /// Corresponds to [insertText(_:replacementRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438258-inserttext) + /// + /// replacement_range is in terms of UTF-16 characters + fn replace_text_in_range( + &mut self, + replacement_range: Option>, + text: &str, + window: &mut Window, + cx: &mut App, + ); + + /// Replace the text in the given document range with the given text, + /// and mark the given text as part of an IME 'composing' state + /// Corresponds to [setMarkedText(_:selectedRange:replacementRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438246-setmarkedtext) + /// + /// range_utf16 is in terms of UTF-16 characters + /// new_selected_range is in terms of UTF-16 characters + fn replace_and_mark_text_in_range( + &mut self, + range_utf16: Option>, + new_text: &str, + new_selected_range: Option>, + window: &mut Window, + cx: &mut App, + ); + + /// Remove the IME 'composing' state from the document + /// Corresponds to [unmarkText()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438239-unmarktext) + fn unmark_text(&mut self, window: &mut Window, cx: &mut App); + + /// Get the bounds of the given document range in screen coordinates + /// Corresponds to [firstRect(forCharacterRange:actualRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438240-firstrect) + /// + /// This is used for positioning the IME candidate window + fn bounds_for_range( + &mut self, + range_utf16: Range, + window: &mut Window, + cx: &mut App, + ) -> Option>; + + /// Get the character offset for the given point in terms of UTF16 characters + /// + /// Corresponds to [characterIndexForPoint:](https://developer.apple.com/documentation/appkit/nstextinputclient/characterindex(for:)) + fn character_index_for_point( + &mut self, + point: Point, + window: &mut Window, + cx: &mut App, + ) -> Option; + + /// Allows a given input context to opt into getting raw key repeats instead of + /// sending these to the platform. + /// TODO: Ideally we should be able to set ApplePressAndHoldEnabled in NSUserDefaults + /// (which is how iTerm does it) but it doesn't seem to work for me. + #[allow(dead_code)] + fn apple_press_and_hold_enabled(&mut self) -> bool { + true + } +} + +/// The variables that can be configured when creating a new window +#[derive(Debug)] +pub struct WindowOptions { + /// Specifies the state and bounds of the window in screen coordinates. + /// - `None`: Inherit the bounds. + /// - `Some(WindowBounds)`: Open a window with corresponding state and its restore size. + pub window_bounds: Option, + + /// The titlebar configuration of the window + pub titlebar: Option, + + /// Whether the window should be focused when created + pub focus: bool, + + /// Whether the window should be shown when created + pub show: bool, + + /// The kind of window to create + pub kind: WindowKind, + + /// Whether the window should be movable by the user + pub is_movable: bool, + + /// Whether the window should be resizable by the user + pub is_resizable: bool, + + /// Whether the window should be minimized by the user + pub is_minimizable: bool, + + /// The display to create the window on, if this is None, + /// the window will be created on the main display + pub display_id: Option, + + /// The appearance of the window background. + pub window_background: WindowBackgroundAppearance, + + /// Application identifier of the window. Can by used by desktop environments to group applications together. + pub app_id: Option, + + /// Window minimum size + pub window_min_size: Option>, + + /// Whether to use client or server side decorations. Wayland only + /// Note that this may be ignored. + pub window_decorations: Option, + + /// Tab group name, allows opening the window as a native tab on macOS 10.12+. Windows with the same tabbing identifier will be grouped together. + pub tabbing_identifier: Option, +} + +/// The variables that can be configured when creating a new window +#[derive(Debug)] +#[cfg_attr( + all( + any(target_os = "linux", target_os = "freebsd"), + not(any(feature = "x11", feature = "wayland")) + ), + allow(dead_code) +)] +pub(crate) struct WindowParams { + pub bounds: Bounds, + + /// The titlebar configuration of the window + #[cfg_attr(feature = "wayland", allow(dead_code))] + pub titlebar: Option, + + /// The kind of window to create + #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] + pub kind: WindowKind, + + /// Whether the window should be movable by the user + #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] + pub is_movable: bool, + + /// Whether the window should be resizable by the user + #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] + pub is_resizable: bool, + + /// Whether the window should be minimized by the user + #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] + pub is_minimizable: bool, + + #[cfg_attr( + any(target_os = "linux", target_os = "freebsd", target_os = "windows"), + allow(dead_code) + )] + pub focus: bool, + + #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] + pub show: bool, + + #[cfg_attr(feature = "wayland", allow(dead_code))] + pub display_id: Option, + + pub window_min_size: Option>, + #[cfg(target_os = "macos")] + pub tabbing_identifier: Option, +} + +/// Represents the status of how a window should be opened. +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum WindowBounds { + /// Indicates that the window should open in a windowed state with the given bounds. + Windowed(Bounds), + /// Indicates that the window should open in a maximized state. + /// The bounds provided here represent the restore size of the window. + Maximized(Bounds), + /// Indicates that the window should open in fullscreen mode. + /// The bounds provided here represent the restore size of the window. + Fullscreen(Bounds), +} + +impl Default for WindowBounds { + fn default() -> Self { + WindowBounds::Windowed(Bounds::default()) + } +} + +impl WindowBounds { + /// Retrieve the inner bounds + pub fn get_bounds(&self) -> Bounds { + match self { + WindowBounds::Windowed(bounds) => *bounds, + WindowBounds::Maximized(bounds) => *bounds, + WindowBounds::Fullscreen(bounds) => *bounds, + } + } + + /// Creates a new window bounds that centers the window on the screen. + pub fn centered(size: Size, cx: &App) -> Self { + WindowBounds::Windowed(Bounds::centered(None, size, cx)) + } +} + +impl Default for WindowOptions { + fn default() -> Self { + Self { + window_bounds: None, + titlebar: Some(TitlebarOptions { + title: Default::default(), + appears_transparent: Default::default(), + traffic_light_position: Default::default(), + }), + focus: true, + show: true, + kind: WindowKind::Normal, + is_movable: true, + is_resizable: true, + is_minimizable: true, + display_id: None, + window_background: WindowBackgroundAppearance::default(), + app_id: None, + window_min_size: None, + window_decorations: None, + tabbing_identifier: None, + } + } +} + +/// The options that can be configured for a window's titlebar +#[derive(Debug, Default)] +pub struct TitlebarOptions { + /// The initial title of the window + pub title: Option, + + /// Should the default system titlebar be hidden to allow for a custom-drawn titlebar? (macOS and Windows only) + /// Refer to [`WindowOptions::window_decorations`] on Linux + pub appears_transparent: bool, + + /// The position of the macOS traffic light buttons + pub traffic_light_position: Option>, +} + +/// The kind of window to create +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum WindowKind { + /// A normal application window + Normal, + + /// A window that appears above all other windows, usually used for alerts or popups + /// use sparingly! + PopUp, + + /// A floating window that appears on top of its parent window + Floating, +} + +/// The appearance of the window, as defined by the operating system. +/// +/// On macOS, this corresponds to named [`NSAppearance`](https://developer.apple.com/documentation/appkit/nsappearance) +/// values. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum WindowAppearance { + /// A light appearance. + /// + /// On macOS, this corresponds to the `aqua` appearance. + Light, + + /// A light appearance with vibrant colors. + /// + /// On macOS, this corresponds to the `NSAppearanceNameVibrantLight` appearance. + VibrantLight, + + /// A dark appearance. + /// + /// On macOS, this corresponds to the `darkAqua` appearance. + Dark, + + /// A dark appearance with vibrant colors. + /// + /// On macOS, this corresponds to the `NSAppearanceNameVibrantDark` appearance. + VibrantDark, +} + +impl Default for WindowAppearance { + fn default() -> Self { + Self::Light + } +} + +/// The appearance of the background of the window itself, when there is +/// no content or the content is transparent. +#[derive(Copy, Clone, Debug, Default, PartialEq)] +pub enum WindowBackgroundAppearance { + /// Opaque. + /// + /// This lets the window manager know that content behind this + /// window does not need to be drawn. + /// + /// Actual color depends on the system and themes should define a fully + /// opaque background color instead. + #[default] + Opaque, + /// Plain alpha transparency. + Transparent, + /// Transparency, but the contents behind the window are blurred. + /// + /// Not always supported. + Blurred, +} + +/// The options that can be configured for a file dialog prompt +#[derive(Clone, Debug)] +pub struct PathPromptOptions { + /// Should the prompt allow files to be selected? + pub files: bool, + /// Should the prompt allow directories to be selected? + pub directories: bool, + /// Should the prompt allow multiple files to be selected? + pub multiple: bool, + /// The prompt to show to a user when selecting a path + pub prompt: Option, +} + +/// What kind of prompt styling to show +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum PromptLevel { + /// A prompt that is shown when the user should be notified of something + Info, + + /// A prompt that is shown when the user needs to be warned of a potential problem + Warning, + + /// A prompt that is shown when a critical problem has occurred + Critical, +} + +/// Prompt Button +#[derive(Clone, Debug, PartialEq)] +pub enum PromptButton { + /// Ok button + Ok(SharedString), + /// Cancel button + Cancel(SharedString), + /// Other button + Other(SharedString), +} + +impl PromptButton { + /// Create a button with label + pub fn new(label: impl Into) -> Self { + PromptButton::Other(label.into()) + } + + /// Create an Ok button + pub fn ok(label: impl Into) -> Self { + PromptButton::Ok(label.into()) + } + + /// Create a Cancel button + pub fn cancel(label: impl Into) -> Self { + PromptButton::Cancel(label.into()) + } + + #[allow(dead_code)] + pub(crate) fn is_cancel(&self) -> bool { + matches!(self, PromptButton::Cancel(_)) + } + + /// Returns the label of the button + pub fn label(&self) -> &SharedString { + match self { + PromptButton::Ok(label) => label, + PromptButton::Cancel(label) => label, + PromptButton::Other(label) => label, + } + } +} + +impl From<&str> for PromptButton { + fn from(value: &str) -> Self { + match value.to_lowercase().as_str() { + "ok" => PromptButton::Ok("Ok".into()), + "cancel" => PromptButton::Cancel("Cancel".into()), + _ => PromptButton::Other(SharedString::from(value.to_owned())), + } + } +} + +/// The style of the cursor (pointer) +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] +pub enum CursorStyle { + /// The default cursor + Arrow, + + /// A text input cursor + /// corresponds to the CSS cursor value `text` + IBeam, + + /// A crosshair cursor + /// corresponds to the CSS cursor value `crosshair` + Crosshair, + + /// A closed hand cursor + /// corresponds to the CSS cursor value `grabbing` + ClosedHand, + + /// An open hand cursor + /// corresponds to the CSS cursor value `grab` + OpenHand, + + /// A pointing hand cursor + /// corresponds to the CSS cursor value `pointer` + PointingHand, + + /// A resize left cursor + /// corresponds to the CSS cursor value `w-resize` + ResizeLeft, + + /// A resize right cursor + /// corresponds to the CSS cursor value `e-resize` + ResizeRight, + + /// A resize cursor to the left and right + /// corresponds to the CSS cursor value `ew-resize` + ResizeLeftRight, + + /// A resize up cursor + /// corresponds to the CSS cursor value `n-resize` + ResizeUp, + + /// A resize down cursor + /// corresponds to the CSS cursor value `s-resize` + ResizeDown, + + /// A resize cursor directing up and down + /// corresponds to the CSS cursor value `ns-resize` + ResizeUpDown, + + /// A resize cursor directing up-left and down-right + /// corresponds to the CSS cursor value `nesw-resize` + ResizeUpLeftDownRight, + + /// A resize cursor directing up-right and down-left + /// corresponds to the CSS cursor value `nwse-resize` + ResizeUpRightDownLeft, + + /// A cursor indicating that the item/column can be resized horizontally. + /// corresponds to the CSS cursor value `col-resize` + ResizeColumn, + + /// A cursor indicating that the item/row can be resized vertically. + /// corresponds to the CSS cursor value `row-resize` + ResizeRow, + + /// A text input cursor for vertical layout + /// corresponds to the CSS cursor value `vertical-text` + IBeamCursorForVerticalLayout, + + /// A cursor indicating that the operation is not allowed + /// corresponds to the CSS cursor value `not-allowed` + OperationNotAllowed, + + /// A cursor indicating that the operation will result in a link + /// corresponds to the CSS cursor value `alias` + DragLink, + + /// A cursor indicating that the operation will result in a copy + /// corresponds to the CSS cursor value `copy` + DragCopy, + + /// A cursor indicating that the operation will result in a context menu + /// corresponds to the CSS cursor value `context-menu` + ContextualMenu, + + /// Hide the cursor + None, +} + +impl Default for CursorStyle { + fn default() -> Self { + Self::Arrow + } +} + +/// A clipboard item that should be copied to the clipboard +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ClipboardItem { + entries: Vec, +} + +/// Either a ClipboardString or a ClipboardImage +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ClipboardEntry { + /// A string entry + String(ClipboardString), + /// An image entry + Image(Image), +} + +impl ClipboardItem { + /// Create a new ClipboardItem::String with no associated metadata + pub fn new_string(text: String) -> Self { + Self { + entries: vec![ClipboardEntry::String(ClipboardString::new(text))], + } + } + + /// Create a new ClipboardItem::String with the given text and associated metadata + pub fn new_string_with_metadata(text: String, metadata: String) -> Self { + Self { + entries: vec![ClipboardEntry::String(ClipboardString { + text, + metadata: Some(metadata), + })], + } + } + + /// Create a new ClipboardItem::String with the given text and associated metadata + pub fn new_string_with_json_metadata(text: String, metadata: T) -> Self { + Self { + entries: vec![ClipboardEntry::String( + ClipboardString::new(text).with_json_metadata(metadata), + )], + } + } + + /// Create a new ClipboardItem::Image with the given image with no associated metadata + pub fn new_image(image: &Image) -> Self { + Self { + entries: vec![ClipboardEntry::Image(image.clone())], + } + } + + /// Concatenates together all the ClipboardString entries in the item. + /// Returns None if there were no ClipboardString entries. + pub fn text(&self) -> Option { + let mut answer = String::new(); + let mut any_entries = false; + + for entry in self.entries.iter() { + if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry { + answer.push_str(text); + any_entries = true; + } + } + + if any_entries { Some(answer) } else { None } + } + + /// If this item is one ClipboardEntry::String, returns its metadata. + #[cfg_attr(not(target_os = "windows"), allow(dead_code))] + pub fn metadata(&self) -> Option<&String> { + match self.entries().first() { + Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => { + clipboard_string.metadata.as_ref() + } + _ => None, + } + } + + /// Get the item's entries + pub fn entries(&self) -> &[ClipboardEntry] { + &self.entries + } + + /// Get owned versions of the item's entries + pub fn into_entries(self) -> impl Iterator { + self.entries.into_iter() + } +} + +impl From for ClipboardEntry { + fn from(value: ClipboardString) -> Self { + Self::String(value) + } +} + +impl From for ClipboardEntry { + fn from(value: String) -> Self { + Self::from(ClipboardString::from(value)) + } +} + +impl From for ClipboardEntry { + fn from(value: Image) -> Self { + Self::Image(value) + } +} + +impl From for ClipboardItem { + fn from(value: ClipboardEntry) -> Self { + Self { + entries: vec![value], + } + } +} + +impl From for ClipboardItem { + fn from(value: String) -> Self { + Self::from(ClipboardEntry::from(value)) + } +} + +impl From for ClipboardItem { + fn from(value: Image) -> Self { + Self::from(ClipboardEntry::from(value)) + } +} + +/// One of the editor's supported image formats (e.g. PNG, JPEG) - used when dealing with images in the clipboard +#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)] +pub enum ImageFormat { + // Sorted from most to least likely to be pasted into an editor, + // which matters when we iterate through them trying to see if + // clipboard content matches them. + /// .png + Png, + /// .jpeg or .jpg + Jpeg, + /// .webp + Webp, + /// .gif + Gif, + /// .svg + Svg, + /// .bmp + Bmp, + /// .tif or .tiff + Tiff, +} + +impl ImageFormat { + /// Returns the mime type for the ImageFormat + pub const fn mime_type(self) -> &'static str { + match self { + ImageFormat::Png => "image/png", + ImageFormat::Jpeg => "image/jpeg", + ImageFormat::Webp => "image/webp", + ImageFormat::Gif => "image/gif", + ImageFormat::Svg => "image/svg+xml", + ImageFormat::Bmp => "image/bmp", + ImageFormat::Tiff => "image/tiff", + } + } + + /// Returns the ImageFormat for the given mime type + pub fn from_mime_type(mime_type: &str) -> Option { + match mime_type { + "image/png" => Some(Self::Png), + "image/jpeg" | "image/jpg" => Some(Self::Jpeg), + "image/webp" => Some(Self::Webp), + "image/gif" => Some(Self::Gif), + "image/svg+xml" => Some(Self::Svg), + "image/bmp" => Some(Self::Bmp), + "image/tiff" | "image/tif" => Some(Self::Tiff), + _ => None, + } + } +} + +/// An image, with a format and certain bytes +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Image { + /// The image format the bytes represent (e.g. PNG) + pub format: ImageFormat, + /// The raw image bytes + pub bytes: Vec, + /// The unique ID for the image + id: u64, +} + +impl Hash for Image { + fn hash(&self, state: &mut H) { + state.write_u64(self.id); + } +} + +impl Image { + /// An empty image containing no data + pub fn empty() -> Self { + Self::from_bytes(ImageFormat::Png, Vec::new()) + } + + /// Create an image from a format and bytes + pub fn from_bytes(format: ImageFormat, bytes: Vec) -> Self { + Self { + id: hash(&bytes), + format, + bytes, + } + } + + /// Get this image's ID + pub fn id(&self) -> u64 { + self.id + } + + /// Use the GPUI `use_asset` API to make this image renderable + pub fn use_render_image( + self: Arc, + window: &mut Window, + cx: &mut App, + ) -> Option> { + ImageSource::Image(self) + .use_data(None, window, cx) + .and_then(|result| result.ok()) + } + + /// Use the GPUI `get_asset` API to make this image renderable + pub fn get_render_image( + self: Arc, + window: &mut Window, + cx: &mut App, + ) -> Option> { + ImageSource::Image(self) + .get_data(None, window, cx) + .and_then(|result| result.ok()) + } + + /// Use the GPUI `remove_asset` API to drop this image, if possible. + pub fn remove_asset(self: Arc, cx: &mut App) { + ImageSource::Image(self).remove_asset(cx); + } + + /// Convert the clipboard image to an `ImageData` object. + pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result> { + fn frames_for_image( + bytes: &[u8], + format: image::ImageFormat, + ) -> Result> { + let mut data = image::load_from_memory_with_format(bytes, format)?.into_rgba8(); + + // Convert from RGBA to BGRA. + for pixel in data.chunks_exact_mut(4) { + pixel.swap(0, 2); + } + + Ok(SmallVec::from_elem(Frame::new(data), 1)) + } + + let frames = match self.format { + ImageFormat::Gif => { + let decoder = GifDecoder::new(Cursor::new(&self.bytes))?; + let mut frames = SmallVec::new(); + + for frame in decoder.into_frames() { + let mut frame = frame?; + // Convert from RGBA to BGRA. + for pixel in frame.buffer_mut().chunks_exact_mut(4) { + pixel.swap(0, 2); + } + frames.push(frame); + } + + frames + } + ImageFormat::Png => frames_for_image(&self.bytes, image::ImageFormat::Png)?, + ImageFormat::Jpeg => frames_for_image(&self.bytes, image::ImageFormat::Jpeg)?, + ImageFormat::Webp => frames_for_image(&self.bytes, image::ImageFormat::WebP)?, + ImageFormat::Bmp => frames_for_image(&self.bytes, image::ImageFormat::Bmp)?, + ImageFormat::Tiff => frames_for_image(&self.bytes, image::ImageFormat::Tiff)?, + ImageFormat::Svg => { + let pixmap = svg_renderer.render_pixmap(&self.bytes, SvgSize::ScaleFactor(1.0))?; + + let buffer = + image::ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()) + .unwrap(); + + SmallVec::from_elem(Frame::new(buffer), 1) + } + }; + + Ok(Arc::new(RenderImage::new(frames))) + } + + /// Get the format of the clipboard image + pub fn format(&self) -> ImageFormat { + self.format + } + + /// Get the raw bytes of the clipboard image + pub fn bytes(&self) -> &[u8] { + self.bytes.as_slice() + } +} + +/// A clipboard item that should be copied to the clipboard +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ClipboardString { + pub(crate) text: String, + pub(crate) metadata: Option, +} + +impl ClipboardString { + /// Create a new clipboard string with the given text + pub fn new(text: String) -> Self { + Self { + text, + metadata: None, + } + } + + /// Return a new clipboard item with the metadata replaced by the given metadata, + /// after serializing it as JSON. + pub fn with_json_metadata(mut self, metadata: T) -> Self { + self.metadata = Some(serde_json::to_string(&metadata).unwrap()); + self + } + + /// Get the text of the clipboard string + pub fn text(&self) -> &String { + &self.text + } + + /// Get the owned text of the clipboard string + pub fn into_text(self) -> String { + self.text + } + + /// Get the metadata of the clipboard string, formatted as JSON + pub fn metadata_json(&self) -> Option + where + T: for<'a> Deserialize<'a>, + { + self.metadata + .as_ref() + .and_then(|m| serde_json::from_str(m).ok()) + } + + #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] + pub(crate) fn text_hash(text: &str) -> u64 { + let mut hasher = SeaHasher::new(); + text.hash(&mut hasher); + hasher.finish() + } +} + +impl From for ClipboardString { + fn from(value: String) -> Self { + Self { + text: value, + metadata: None, + } + } +} diff --git a/third_party/gpui/src/platform/app_menu.rs b/third_party/gpui/src/platform/app_menu.rs new file mode 100644 index 0000000..4069fee --- /dev/null +++ b/third_party/gpui/src/platform/app_menu.rs @@ -0,0 +1,252 @@ +use crate::{Action, App, Platform, SharedString}; +use util::ResultExt; + +/// A menu of the application, either a main menu or a submenu +pub struct Menu { + /// The name of the menu + pub name: SharedString, + + /// The items in the menu + pub items: Vec, +} + +impl Menu { + /// Create an OwnedMenu from this Menu + pub fn owned(self) -> OwnedMenu { + OwnedMenu { + name: self.name.to_string().into(), + items: self.items.into_iter().map(|item| item.owned()).collect(), + } + } +} + +/// OS menus are menus that are recognized by the operating system +/// This allows the operating system to provide specialized items for +/// these menus +pub struct OsMenu { + /// The name of the menu + pub name: SharedString, + + /// The type of menu + pub menu_type: SystemMenuType, +} + +impl OsMenu { + /// Create an OwnedOsMenu from this OsMenu + pub fn owned(self) -> OwnedOsMenu { + OwnedOsMenu { + name: self.name.to_string().into(), + menu_type: self.menu_type, + } + } +} + +/// The type of system menu +#[derive(Copy, Clone, Eq, PartialEq)] +pub enum SystemMenuType { + /// The 'Services' menu in the Application menu on macOS + Services, +} + +/// The different kinds of items that can be in a menu +pub enum MenuItem { + /// A separator between items + Separator, + + /// A submenu + Submenu(Menu), + + /// A menu, managed by the system (for example, the Services menu on macOS) + SystemMenu(OsMenu), + + /// An action that can be performed + Action { + /// The name of this menu item + name: SharedString, + + /// the action to perform when this menu item is selected + action: Box, + + /// The OS Action that corresponds to this action, if any + /// See [`OsAction`] for more information + os_action: Option, + }, +} + +impl MenuItem { + /// Creates a new menu item that is a separator + pub fn separator() -> Self { + Self::Separator + } + + /// Creates a new menu item that is a submenu + pub fn submenu(menu: Menu) -> Self { + Self::Submenu(menu) + } + + /// Creates a new submenu that is populated by the OS + pub fn os_submenu(name: impl Into, menu_type: SystemMenuType) -> Self { + Self::SystemMenu(OsMenu { + name: name.into(), + menu_type, + }) + } + + /// Creates a new menu item that invokes an action + pub fn action(name: impl Into, action: impl Action) -> Self { + Self::Action { + name: name.into(), + action: Box::new(action), + os_action: None, + } + } + + /// Creates a new menu item that invokes an action and has an OS action + pub fn os_action( + name: impl Into, + action: impl Action, + os_action: OsAction, + ) -> Self { + Self::Action { + name: name.into(), + action: Box::new(action), + os_action: Some(os_action), + } + } + + /// Create an OwnedMenuItem from this MenuItem + pub fn owned(self) -> OwnedMenuItem { + match self { + MenuItem::Separator => OwnedMenuItem::Separator, + MenuItem::Submenu(submenu) => OwnedMenuItem::Submenu(submenu.owned()), + MenuItem::Action { + name, + action, + os_action, + } => OwnedMenuItem::Action { + name: name.into(), + action, + os_action, + }, + MenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.owned()), + } + } +} + +/// OS menus are menus that are recognized by the operating system +/// This allows the operating system to provide specialized items for +/// these menus +#[derive(Clone)] +pub struct OwnedOsMenu { + /// The name of the menu + pub name: SharedString, + + /// The type of menu + pub menu_type: SystemMenuType, +} + +/// A menu of the application, either a main menu or a submenu +#[derive(Clone)] +pub struct OwnedMenu { + /// The name of the menu + pub name: SharedString, + + /// The items in the menu + pub items: Vec, +} + +/// The different kinds of items that can be in a menu +pub enum OwnedMenuItem { + /// A separator between items + Separator, + + /// A submenu + Submenu(OwnedMenu), + + /// A menu, managed by the system (for example, the Services menu on macOS) + SystemMenu(OwnedOsMenu), + + /// An action that can be performed + Action { + /// The name of this menu item + name: String, + + /// the action to perform when this menu item is selected + action: Box, + + /// The OS Action that corresponds to this action, if any + /// See [`OsAction`] for more information + os_action: Option, + }, +} + +impl Clone for OwnedMenuItem { + fn clone(&self) -> Self { + match self { + OwnedMenuItem::Separator => OwnedMenuItem::Separator, + OwnedMenuItem::Submenu(submenu) => OwnedMenuItem::Submenu(submenu.clone()), + OwnedMenuItem::Action { + name, + action, + os_action, + } => OwnedMenuItem::Action { + name: name.clone(), + action: action.boxed_clone(), + os_action: *os_action, + }, + OwnedMenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.clone()), + } + } +} + +// TODO: As part of the global selections refactor, these should +// be moved to GPUI-provided actions that make this association +// without leaking the platform details to GPUI users + +/// OS actions are actions that are recognized by the operating system +/// This allows the operating system to provide specialized behavior for +/// these actions +#[derive(Copy, Clone, Eq, PartialEq)] +pub enum OsAction { + /// The 'cut' action + Cut, + + /// The 'copy' action + Copy, + + /// The 'paste' action + Paste, + + /// The 'select all' action + SelectAll, + + /// The 'undo' action + Undo, + + /// The 'redo' action + Redo, +} + +pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &App) { + platform.on_will_open_app_menu(Box::new({ + let cx = cx.to_async(); + move || { + cx.update(|cx| cx.clear_pending_keystrokes()).ok(); + } + })); + + platform.on_validate_app_menu_command(Box::new({ + let cx = cx.to_async(); + move |action| { + cx.update(|cx| cx.is_action_available(action)) + .unwrap_or(false) + } + })); + + platform.on_app_menu_action(Box::new({ + let cx = cx.to_async(); + move |action| { + cx.update(|cx| cx.dispatch_action(action)).log_err(); + } + })); +} diff --git a/third_party/gpui/src/platform/blade.rs b/third_party/gpui/src/platform/blade.rs new file mode 100644 index 0000000..9d966d8 --- /dev/null +++ b/third_party/gpui/src/platform/blade.rs @@ -0,0 +1,11 @@ +#[cfg(target_os = "macos")] +mod apple_compat; +mod blade_atlas; +mod blade_context; +mod blade_renderer; + +#[cfg(target_os = "macos")] +pub(crate) use apple_compat::*; +pub(crate) use blade_atlas::*; +pub(crate) use blade_context::*; +pub(crate) use blade_renderer::*; diff --git a/third_party/gpui/src/platform/blade/apple_compat.rs b/third_party/gpui/src/platform/blade/apple_compat.rs new file mode 100644 index 0000000..a75ddfa --- /dev/null +++ b/third_party/gpui/src/platform/blade/apple_compat.rs @@ -0,0 +1,60 @@ +use super::{BladeContext, BladeRenderer, BladeSurfaceConfig}; +use blade_graphics as gpu; +use std::{ffi::c_void, ptr::NonNull}; + +#[derive(Clone)] +pub struct Context { + inner: BladeContext, +} +impl Default for Context { + fn default() -> Self { + Self { + inner: BladeContext::new().unwrap(), + } + } +} + +pub type Renderer = BladeRenderer; + +pub unsafe fn new_renderer( + context: Context, + _native_window: *mut c_void, + native_view: *mut c_void, + bounds: crate::Size, + transparent: bool, +) -> Renderer { + use raw_window_handle as rwh; + struct RawWindow { + view: *mut c_void, + } + + impl rwh::HasWindowHandle for RawWindow { + fn window_handle(&self) -> Result, rwh::HandleError> { + let view = NonNull::new(self.view).unwrap(); + let handle = rwh::AppKitWindowHandle::new(view); + Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) }) + } + } + impl rwh::HasDisplayHandle for RawWindow { + fn display_handle(&self) -> Result, rwh::HandleError> { + let handle = rwh::AppKitDisplayHandle::new(); + Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) }) + } + } + + BladeRenderer::new( + &context.inner, + &RawWindow { + view: native_view as *mut _, + }, + BladeSurfaceConfig { + size: gpu::Extent { + width: bounds.width as u32, + height: bounds.height as u32, + depth: 1, + }, + transparent, + }, + ) + .unwrap() +} diff --git a/third_party/gpui/src/platform/blade/blade_atlas.rs b/third_party/gpui/src/platform/blade/blade_atlas.rs new file mode 100644 index 0000000..9b9299d --- /dev/null +++ b/third_party/gpui/src/platform/blade/blade_atlas.rs @@ -0,0 +1,384 @@ +use crate::{ + AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, DevicePixels, PlatformAtlas, + Point, Size, platform::AtlasTextureList, +}; +use anyhow::Result; +use blade_graphics as gpu; +use blade_util::{BufferBelt, BufferBeltDescriptor}; +use collections::FxHashMap; +use etagere::BucketedAtlasAllocator; +use parking_lot::Mutex; +use std::{borrow::Cow, ops, sync::Arc}; + +pub(crate) struct BladeAtlas(Mutex); + +struct PendingUpload { + id: AtlasTextureId, + bounds: Bounds, + data: gpu::BufferPiece, +} + +struct BladeAtlasState { + gpu: Arc, + upload_belt: BufferBelt, + storage: BladeAtlasStorage, + tiles_by_key: FxHashMap, + initializations: Vec, + uploads: Vec, +} + +#[cfg(gles)] +unsafe impl Send for BladeAtlasState {} + +impl BladeAtlasState { + fn destroy(&mut self) { + self.storage.destroy(&self.gpu); + self.upload_belt.destroy(&self.gpu); + } +} + +pub struct BladeTextureInfo { + pub raw_view: gpu::TextureView, +} + +impl BladeAtlas { + pub(crate) fn new(gpu: &Arc) -> Self { + BladeAtlas(Mutex::new(BladeAtlasState { + gpu: Arc::clone(gpu), + upload_belt: BufferBelt::new(BufferBeltDescriptor { + 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 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]; + BladeTextureInfo { + raw_view: texture.raw_view, + } + } +} + +impl PlatformAtlas for BladeAtlas { + fn get_or_insert_with<'a>( + &self, + key: &AtlasKey, + build: &mut dyn FnMut() -> Result, Cow<'a, [u8]>)>>, + ) -> Result> { + let mut lock = self.0.lock(); + if let Some(tile) = lock.tiles_by_key.get(key) { + Ok(Some(tile.clone())) + } else { + profiling::scope!("new tile"); + let Some((size, bytes)) = build()? else { + return Ok(None); + }; + 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(Some(tile)) + } + } + + fn remove(&self, key: &AtlasKey) { + let mut lock = self.0.lock(); + + let Some(id) = lock.tiles_by_key.remove(key).map(|tile| tile.texture_id) else { + return; + }; + + let Some(texture_slot) = lock.storage[id.kind].textures.get_mut(id.index as usize) else { + return; + }; + + if let Some(mut texture) = texture_slot.take() { + texture.decrement_ref_count(); + if texture.is_unreferenced() { + lock.storage[id.kind] + .free_list + .push(texture.id.index as usize); + texture.destroy(&lock.gpu); + } else { + *texture_slot = Some(texture); + } + } + } +} + +impl BladeAtlasState { + fn allocate(&mut self, size: Size, texture_kind: AtlasTextureKind) -> AtlasTile { + { + let textures = &mut self.storage[texture_kind]; + + if let Some(tile) = textures + .iter_mut() + .rev() + .find_map(|texture| texture.allocate(size)) + { + return tile; + } + } + + 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; + } + } + + 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, + sample_count: 1, + dimension: gpu::TextureDimension::D2, + usage, + external: None, + }); + let raw_view = self.gpu.create_texture_view( + raw, + gpu::TextureViewDesc { + name: "", + format, + dimension: gpu::ViewDimension::D2, + subresources: &Default::default(), + }, + ); + + let texture_list = &mut self.storage[kind]; + let index = texture_list.free_list.pop(); + + let atlas_texture = BladeAtlasTexture { + id: AtlasTextureId { + index: index.unwrap_or(texture_list.textures.len()) as u32, + kind, + }, + allocator: etagere::BucketedAtlasAllocator::new(size.into()), + format, + raw, + raw_view, + live_atlas_keys: 0, + }; + + self.initializations.push(atlas_texture.id); + + if let Some(ix) = index { + texture_list.textures[ix] = Some(atlas_texture); + texture_list.textures.get_mut(ix).unwrap().as_mut().unwrap() + } else { + texture_list.textures.push(Some(atlas_texture)); + texture_list.textures.last_mut().unwrap().as_mut().unwrap() + } + } + + fn upload_texture(&mut self, id: AtlasTextureId, bounds: Bounds, bytes: &[u8]) { + let data = self.upload_belt.alloc_bytes(bytes, &self.gpu); + self.uploads.push(PendingUpload { id, bounds, data }); + } + + fn flush_initializations(&mut self, encoder: &mut gpu::CommandEncoder) { + for id in self.initializations.drain(..) { + let texture = &self.storage[id]; + encoder.init_texture(texture.raw); + } + } + + fn flush(&mut self, encoder: &mut gpu::CommandEncoder) { + self.flush_initializations(encoder); + + let mut transfers = encoder.transfer("atlas"); + 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: AtlasTextureList, + polychrome_textures: AtlasTextureList, +} + +impl ops::Index for BladeAtlasStorage { + type Output = AtlasTextureList; + fn index(&self, kind: AtlasTextureKind) -> &Self::Output { + match kind { + crate::AtlasTextureKind::Monochrome => &self.monochrome_textures, + crate::AtlasTextureKind::Polychrome => &self.polychrome_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, + } + } +} + +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, + }; + textures[id.index as usize].as_ref().unwrap() + } +} + +impl BladeAtlasStorage { + fn destroy(&mut self, gpu: &gpu::Context) { + for mut texture in self.monochrome_textures.drain().flatten() { + texture.destroy(gpu); + } + for mut texture in self.polychrome_textures.drain().flatten() { + texture.destroy(gpu); + } + } +} + +struct BladeAtlasTexture { + id: AtlasTextureId, + allocator: BucketedAtlasAllocator, + raw: gpu::Texture, + raw_view: gpu::TextureView, + format: gpu::TextureFormat, + live_atlas_keys: u32, +} + +impl BladeAtlasTexture { + 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, + }, + }; + self.live_atlas_keys += 1; + 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 + } + + fn decrement_ref_count(&mut self) { + self.live_atlas_keys -= 1; + } + + fn is_unreferenced(&mut self) -> bool { + self.live_atlas_keys == 0 + } +} + +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/third_party/gpui/src/platform/blade/blade_context.rs b/third_party/gpui/src/platform/blade/blade_context.rs new file mode 100644 index 0000000..12c68a1 --- /dev/null +++ b/third_party/gpui/src/platform/blade/blade_context.rs @@ -0,0 +1,80 @@ +use anyhow::Context as _; +use blade_graphics as gpu; +use std::sync::Arc; +use util::ResultExt; + +#[cfg_attr(target_os = "macos", derive(Clone))] +pub struct BladeContext { + pub(super) gpu: Arc, +} + +impl BladeContext { + pub fn new() -> anyhow::Result { + let device_id_forced = match std::env::var("ZED_DEVICE_ID") { + Ok(val) => parse_pci_id(&val) + .context("Failed to parse device ID from `ZED_DEVICE_ID` environment variable") + .log_err(), + Err(std::env::VarError::NotPresent) => None, + err => { + err.context("Failed to read value of `ZED_DEVICE_ID` environment variable") + .log_err(); + None + } + }; + let gpu = Arc::new( + unsafe { + gpu::Context::init(gpu::ContextDesc { + presentation: true, + validation: false, + device_id: device_id_forced.unwrap_or(0), + ..Default::default() + }) + } + .map_err(|e| anyhow::anyhow!("{e:?}"))?, + ); + Ok(Self { gpu }) + } +} + +fn parse_pci_id(id: &str) -> anyhow::Result { + let mut id = id.trim(); + + if id.starts_with("0x") || id.starts_with("0X") { + id = &id[2..]; + } + let is_hex_string = id.chars().all(|c| c.is_ascii_hexdigit()); + let is_4_chars = id.len() == 4; + anyhow::ensure!( + is_4_chars && is_hex_string, + "Expected a 4 digit PCI ID in hexadecimal format" + ); + + u32::from_str_radix(id, 16).context("parsing PCI ID as hex") +} + +#[cfg(test)] +mod tests { + use super::parse_pci_id; + + #[test] + fn test_parse_device_id() { + assert!(parse_pci_id("0xABCD").is_ok()); + assert!(parse_pci_id("ABCD").is_ok()); + assert!(parse_pci_id("abcd").is_ok()); + assert!(parse_pci_id("1234").is_ok()); + assert!(parse_pci_id("123").is_err()); + assert_eq!( + parse_pci_id(&format!("{:x}", 0x1234)).unwrap(), + parse_pci_id(&format!("{:X}", 0x1234)).unwrap(), + ); + + assert_eq!( + parse_pci_id(&format!("{:#x}", 0x1234)).unwrap(), + parse_pci_id(&format!("{:#X}", 0x1234)).unwrap(), + ); + assert_eq!( + parse_pci_id(&format!("{:#x}", 0x1234)).unwrap(), + parse_pci_id(&format!("{:#X}", 0x1234)).unwrap(), + ); + } +} diff --git a/third_party/gpui/src/platform/blade/blade_renderer.rs b/third_party/gpui/src/platform/blade/blade_renderer.rs new file mode 100644 index 0000000..d00fbdc --- /dev/null +++ b/third_party/gpui/src/platform/blade/blade_renderer.rs @@ -0,0 +1,1072 @@ +// Doing `if let` gives you nice scoping with passes/encoders +#![allow(irrefutable_let_patterns)] + +use super::{BladeAtlas, BladeContext}; +use crate::{ + Background, Bounds, DevicePixels, GpuSpecs, MonochromeSprite, Path, Point, PolychromeSprite, + PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size, Underline, +}; +use blade_graphics as gpu; +use blade_util::{BufferBelt, BufferBeltDescriptor}; +use bytemuck::{Pod, Zeroable}; +#[cfg(target_os = "macos")] +use media::core_video::CVMetalTextureCache; +use std::sync::Arc; + +const MAX_FRAME_TIME_MS: u32 = 10000; + +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +struct GlobalParams { + viewport_size: [f32; 2], + premultiplied_alpha: u32, + pad: u32, +} + +//Note: we can't use `Bounds` directly here because +// it doesn't implement Pod + Zeroable +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +struct PodBounds { + origin: [f32; 2], + size: [f32; 2], +} + +impl From> for PodBounds { + fn from(bounds: Bounds) -> Self { + Self { + origin: [bounds.origin.x.0, bounds.origin.y.0], + size: [bounds.size.width.0, bounds.size.height.0], + } + } +} + +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +struct SurfaceParams { + bounds: PodBounds, + content_mask: PodBounds, +} + +#[derive(blade_macros::ShaderData)] +struct ShaderQuadsData { + globals: GlobalParams, + b_quads: gpu::BufferPiece, +} + +#[derive(blade_macros::ShaderData)] +struct ShaderShadowsData { + globals: GlobalParams, + b_shadows: gpu::BufferPiece, +} + +#[derive(blade_macros::ShaderData)] +struct ShaderPathRasterizationData { + globals: GlobalParams, + b_path_vertices: gpu::BufferPiece, +} + +#[derive(blade_macros::ShaderData)] +struct ShaderPathsData { + globals: GlobalParams, + t_sprite: gpu::TextureView, + s_sprite: gpu::Sampler, + b_path_sprites: gpu::BufferPiece, +} + +#[derive(blade_macros::ShaderData)] +struct ShaderUnderlinesData { + globals: GlobalParams, + b_underlines: gpu::BufferPiece, +} + +#[derive(blade_macros::ShaderData)] +struct ShaderMonoSpritesData { + globals: GlobalParams, + gamma_ratios: [f32; 4], + grayscale_enhanced_contrast: f32, + t_sprite: gpu::TextureView, + s_sprite: gpu::Sampler, + b_mono_sprites: gpu::BufferPiece, +} + +#[derive(blade_macros::ShaderData)] +struct ShaderPolySpritesData { + globals: GlobalParams, + t_sprite: gpu::TextureView, + s_sprite: gpu::Sampler, + b_poly_sprites: gpu::BufferPiece, +} + +#[derive(blade_macros::ShaderData)] +struct ShaderSurfacesData { + globals: GlobalParams, + surface_locals: SurfaceParams, + t_y: gpu::TextureView, + t_cb_cr: gpu::TextureView, + s_surface: gpu::Sampler, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[repr(C)] +struct PathSprite { + bounds: Bounds, +} + +#[derive(Clone, Debug)] +#[repr(C)] +struct PathRasterizationVertex { + xy_position: Point, + st_position: Point, + color: Background, + bounds: Bounds, +} + +struct BladePipelines { + quads: gpu::RenderPipeline, + shadows: gpu::RenderPipeline, + path_rasterization: gpu::RenderPipeline, + paths: gpu::RenderPipeline, + underlines: gpu::RenderPipeline, + mono_sprites: gpu::RenderPipeline, + poly_sprites: gpu::RenderPipeline, + surfaces: gpu::RenderPipeline, +} + +impl BladePipelines { + fn new(gpu: &gpu::Context, surface_info: gpu::SurfaceInfo, path_sample_count: u32) -> Self { + use gpu::ShaderData as _; + + log::info!( + "Initializing Blade pipelines for surface {:?}", + surface_info + ); + let shader = gpu.create_shader(gpu::ShaderDesc { + source: include_str!("shaders.wgsl"), + }); + shader.check_struct_size::(); + shader.check_struct_size::(); + shader.check_struct_size::(); + shader.check_struct_size::(); + shader.check_struct_size::(); + shader.check_struct_size::(); + shader.check_struct_size::(); + shader.check_struct_size::(); + shader.check_struct_size::(); + + // See https://apoorvaj.io/alpha-compositing-opengl-blending-and-premultiplied-alpha/ + let blend_mode = match surface_info.alpha { + gpu::AlphaMode::Ignored => gpu::BlendState::ALPHA_BLENDING, + gpu::AlphaMode::PreMultiplied => gpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING, + gpu::AlphaMode::PostMultiplied => gpu::BlendState::ALPHA_BLENDING, + }; + let color_targets = &[gpu::ColorTargetState { + format: surface_info.format, + blend: Some(blend_mode), + write_mask: gpu::ColorWrites::default(), + }]; + + Self { + quads: gpu.create_render_pipeline(gpu::RenderPipelineDesc { + name: "quads", + data_layouts: &[&ShaderQuadsData::layout()], + vertex: shader.at("vs_quad"), + vertex_fetches: &[], + primitive: gpu::PrimitiveState { + topology: gpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + fragment: Some(shader.at("fs_quad")), + color_targets, + multisample_state: gpu::MultisampleState::default(), + }), + shadows: gpu.create_render_pipeline(gpu::RenderPipelineDesc { + name: "shadows", + data_layouts: &[&ShaderShadowsData::layout()], + vertex: shader.at("vs_shadow"), + vertex_fetches: &[], + primitive: gpu::PrimitiveState { + topology: gpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + fragment: Some(shader.at("fs_shadow")), + color_targets, + multisample_state: gpu::MultisampleState::default(), + }), + path_rasterization: gpu.create_render_pipeline(gpu::RenderPipelineDesc { + name: "path_rasterization", + data_layouts: &[&ShaderPathRasterizationData::layout()], + vertex: shader.at("vs_path_rasterization"), + vertex_fetches: &[], + primitive: gpu::PrimitiveState { + topology: gpu::PrimitiveTopology::TriangleList, + ..Default::default() + }, + depth_stencil: None, + fragment: Some(shader.at("fs_path_rasterization")), + // The original implementation was using ADDITIVE blende mode, + // I don't know why + // color_targets: &[gpu::ColorTargetState { + // format: PATH_TEXTURE_FORMAT, + // blend: Some(gpu::BlendState::ADDITIVE), + // write_mask: gpu::ColorWrites::default(), + // }], + color_targets: &[gpu::ColorTargetState { + format: surface_info.format, + blend: Some(gpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: gpu::ColorWrites::default(), + }], + multisample_state: gpu::MultisampleState { + sample_count: path_sample_count, + ..Default::default() + }, + }), + paths: gpu.create_render_pipeline(gpu::RenderPipelineDesc { + name: "paths", + data_layouts: &[&ShaderPathsData::layout()], + vertex: shader.at("vs_path"), + vertex_fetches: &[], + primitive: gpu::PrimitiveState { + topology: gpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + fragment: Some(shader.at("fs_path")), + color_targets: &[gpu::ColorTargetState { + format: surface_info.format, + blend: Some(gpu::BlendState { + color: gpu::BlendComponent::OVER, + alpha: gpu::BlendComponent::ADDITIVE, + }), + write_mask: gpu::ColorWrites::default(), + }], + multisample_state: gpu::MultisampleState::default(), + }), + underlines: gpu.create_render_pipeline(gpu::RenderPipelineDesc { + name: "underlines", + data_layouts: &[&ShaderUnderlinesData::layout()], + vertex: shader.at("vs_underline"), + vertex_fetches: &[], + primitive: gpu::PrimitiveState { + topology: gpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + fragment: Some(shader.at("fs_underline")), + color_targets, + multisample_state: gpu::MultisampleState::default(), + }), + mono_sprites: gpu.create_render_pipeline(gpu::RenderPipelineDesc { + name: "mono-sprites", + data_layouts: &[&ShaderMonoSpritesData::layout()], + vertex: shader.at("vs_mono_sprite"), + vertex_fetches: &[], + primitive: gpu::PrimitiveState { + topology: gpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + fragment: Some(shader.at("fs_mono_sprite")), + color_targets, + multisample_state: gpu::MultisampleState::default(), + }), + poly_sprites: gpu.create_render_pipeline(gpu::RenderPipelineDesc { + name: "poly-sprites", + data_layouts: &[&ShaderPolySpritesData::layout()], + vertex: shader.at("vs_poly_sprite"), + vertex_fetches: &[], + primitive: gpu::PrimitiveState { + topology: gpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + fragment: Some(shader.at("fs_poly_sprite")), + color_targets, + multisample_state: gpu::MultisampleState::default(), + }), + surfaces: gpu.create_render_pipeline(gpu::RenderPipelineDesc { + name: "surfaces", + data_layouts: &[&ShaderSurfacesData::layout()], + vertex: shader.at("vs_surface"), + vertex_fetches: &[], + primitive: gpu::PrimitiveState { + topology: gpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + fragment: Some(shader.at("fs_surface")), + color_targets, + multisample_state: gpu::MultisampleState::default(), + }), + } + } + + fn destroy(&mut self, gpu: &gpu::Context) { + gpu.destroy_render_pipeline(&mut self.quads); + gpu.destroy_render_pipeline(&mut self.shadows); + gpu.destroy_render_pipeline(&mut self.path_rasterization); + gpu.destroy_render_pipeline(&mut self.paths); + gpu.destroy_render_pipeline(&mut self.underlines); + gpu.destroy_render_pipeline(&mut self.mono_sprites); + gpu.destroy_render_pipeline(&mut self.poly_sprites); + gpu.destroy_render_pipeline(&mut self.surfaces); + } +} + +pub struct BladeSurfaceConfig { + pub size: gpu::Extent, + pub transparent: bool, +} + +//Note: we could see some of these fields moved into `BladeContext` +// so that they are shared between windows. E.g. `pipelines`. +// But that is complicated by the fact that pipelines depend on +// the format and alpha mode. +pub struct BladeRenderer { + gpu: Arc, + surface: gpu::Surface, + surface_config: gpu::SurfaceConfig, + command_encoder: gpu::CommandEncoder, + last_sync_point: Option, + pipelines: BladePipelines, + instance_belt: BufferBelt, + atlas: Arc, + atlas_sampler: gpu::Sampler, + #[cfg(target_os = "macos")] + core_video_texture_cache: CVMetalTextureCache, + path_intermediate_texture: gpu::Texture, + path_intermediate_texture_view: gpu::TextureView, + path_intermediate_msaa_texture: Option, + path_intermediate_msaa_texture_view: Option, + rendering_parameters: RenderingParameters, +} + +impl BladeRenderer { + pub fn new( + context: &BladeContext, + window: &I, + config: BladeSurfaceConfig, + ) -> anyhow::Result { + let surface_config = gpu::SurfaceConfig { + size: config.size, + usage: gpu::TextureUsage::TARGET, + display_sync: gpu::DisplaySync::Recent, + color_space: gpu::ColorSpace::Srgb, + allow_exclusive_full_screen: false, + transparent: config.transparent, + }; + let surface = context + .gpu + .create_surface_configured(window, surface_config) + .map_err(|err| anyhow::anyhow!("Failed to create surface: {err:?}"))?; + + let command_encoder = context.gpu.create_command_encoder(gpu::CommandEncoderDesc { + name: "main", + buffer_count: 2, + }); + let rendering_parameters = RenderingParameters::from_env(context); + let pipelines = BladePipelines::new( + &context.gpu, + surface.info(), + rendering_parameters.path_sample_count, + ); + let instance_belt = BufferBelt::new(BufferBeltDescriptor { + memory: gpu::Memory::Shared, + min_chunk_size: 0x1000, + alignment: 0x40, // Vulkan `minStorageBufferOffsetAlignment` on Intel Xe + }); + let atlas = Arc::new(BladeAtlas::new(&context.gpu)); + let atlas_sampler = context.gpu.create_sampler(gpu::SamplerDesc { + name: "path rasterization sampler", + mag_filter: gpu::FilterMode::Linear, + min_filter: gpu::FilterMode::Linear, + ..Default::default() + }); + + let (path_intermediate_texture, path_intermediate_texture_view) = + create_path_intermediate_texture( + &context.gpu, + surface.info().format, + config.size.width, + config.size.height, + ); + let (path_intermediate_msaa_texture, path_intermediate_msaa_texture_view) = + create_msaa_texture_if_needed( + &context.gpu, + surface.info().format, + config.size.width, + config.size.height, + rendering_parameters.path_sample_count, + ) + .unzip(); + + #[cfg(target_os = "macos")] + let core_video_texture_cache = unsafe { + CVMetalTextureCache::new( + objc2::rc::Retained::as_ptr(&context.gpu.metal_device()) as *mut _ + ) + .unwrap() + }; + + Ok(Self { + gpu: Arc::clone(&context.gpu), + surface, + surface_config, + command_encoder, + last_sync_point: None, + pipelines, + instance_belt, + atlas, + atlas_sampler, + #[cfg(target_os = "macos")] + core_video_texture_cache, + path_intermediate_texture, + path_intermediate_texture_view, + path_intermediate_msaa_texture, + path_intermediate_msaa_texture_view, + rendering_parameters, + }) + } + + fn wait_for_gpu(&mut self) { + if let Some(last_sp) = self.last_sync_point.take() + && !self.gpu.wait_for(&last_sp, MAX_FRAME_TIME_MS) + { + log::error!("GPU hung"); + #[cfg(target_os = "linux")] + if self.gpu.device_information().driver_name == "radv" { + log::error!( + "there's a known bug with amdgpu/radv, try setting ZED_PATH_SAMPLE_COUNT=0 as a workaround" + ); + log::error!( + "if that helps you're running into https://github.com/zed-industries/zed/issues/26143" + ); + } + log::error!( + "your device information is: {:?}", + self.gpu.device_information() + ); + while !self.gpu.wait_for(&last_sp, MAX_FRAME_TIME_MS) {} + } + } + + pub fn update_drawable_size(&mut self, size: Size) { + self.update_drawable_size_impl(size, false); + } + + /// Like `update_drawable_size` but skips the check that the size has changed. This is useful in + /// cases like restoring a window from minimization where the size is the same but the + /// renderer's swap chain needs to be recreated. + #[cfg_attr( + any(target_os = "macos", target_os = "linux", target_os = "freebsd"), + allow(dead_code) + )] + pub fn update_drawable_size_even_if_unchanged(&mut self, size: Size) { + self.update_drawable_size_impl(size, true); + } + + fn update_drawable_size_impl(&mut self, size: Size, always_resize: bool) { + let gpu_size = gpu::Extent { + width: size.width.0 as u32, + height: size.height.0 as u32, + depth: 1, + }; + + if always_resize || gpu_size != self.surface_config.size { + self.wait_for_gpu(); + self.surface_config.size = gpu_size; + self.gpu + .reconfigure_surface(&mut self.surface, self.surface_config); + self.gpu.destroy_texture(self.path_intermediate_texture); + self.gpu + .destroy_texture_view(self.path_intermediate_texture_view); + if let Some(msaa_texture) = self.path_intermediate_msaa_texture { + self.gpu.destroy_texture(msaa_texture); + } + if let Some(msaa_view) = self.path_intermediate_msaa_texture_view { + self.gpu.destroy_texture_view(msaa_view); + } + let (path_intermediate_texture, path_intermediate_texture_view) = + create_path_intermediate_texture( + &self.gpu, + self.surface.info().format, + gpu_size.width, + gpu_size.height, + ); + self.path_intermediate_texture = path_intermediate_texture; + self.path_intermediate_texture_view = path_intermediate_texture_view; + let (path_intermediate_msaa_texture, path_intermediate_msaa_texture_view) = + create_msaa_texture_if_needed( + &self.gpu, + self.surface.info().format, + gpu_size.width, + gpu_size.height, + self.rendering_parameters.path_sample_count, + ) + .unzip(); + self.path_intermediate_msaa_texture = path_intermediate_msaa_texture; + self.path_intermediate_msaa_texture_view = path_intermediate_msaa_texture_view; + } + } + + pub fn update_transparency(&mut self, transparent: bool) { + if transparent != self.surface_config.transparent { + self.wait_for_gpu(); + self.surface_config.transparent = transparent; + self.gpu + .reconfigure_surface(&mut self.surface, self.surface_config); + self.pipelines.destroy(&self.gpu); + self.pipelines = BladePipelines::new( + &self.gpu, + self.surface.info(), + self.rendering_parameters.path_sample_count, + ); + } + } + + #[cfg_attr( + any(target_os = "macos", feature = "wayland", target_os = "windows"), + allow(dead_code) + )] + pub fn viewport_size(&self) -> gpu::Extent { + self.surface_config.size + } + + pub fn sprite_atlas(&self) -> &Arc { + &self.atlas + } + + #[cfg_attr(target_os = "macos", allow(dead_code))] + pub fn gpu_specs(&self) -> GpuSpecs { + let info = self.gpu.device_information(); + + GpuSpecs { + is_software_emulated: info.is_software_emulated, + device_name: info.device_name.clone(), + driver_name: info.driver_name.clone(), + driver_info: info.driver_info.clone(), + } + } + + #[cfg(target_os = "macos")] + pub fn layer(&self) -> metal::MetalLayer { + unsafe { foreign_types::ForeignType::from_ptr(self.layer_ptr()) } + } + + #[cfg(target_os = "macos")] + pub fn layer_ptr(&self) -> *mut metal::CAMetalLayer { + objc2::rc::Retained::as_ptr(&self.surface.metal_layer()) as *mut _ + } + + #[profiling::function] + fn draw_paths_to_intermediate( + &mut self, + paths: &[Path], + width: f32, + height: f32, + ) { + self.command_encoder + .init_texture(self.path_intermediate_texture); + if let Some(msaa_texture) = self.path_intermediate_msaa_texture { + self.command_encoder.init_texture(msaa_texture); + } + + let target = if let Some(msaa_view) = self.path_intermediate_msaa_texture_view { + gpu::RenderTarget { + view: msaa_view, + init_op: gpu::InitOp::Clear(gpu::TextureColor::TransparentBlack), + finish_op: gpu::FinishOp::ResolveTo(self.path_intermediate_texture_view), + } + } else { + gpu::RenderTarget { + view: self.path_intermediate_texture_view, + init_op: gpu::InitOp::Clear(gpu::TextureColor::TransparentBlack), + finish_op: gpu::FinishOp::Store, + } + }; + if let mut pass = self.command_encoder.render( + "rasterize paths", + gpu::RenderTargetSet { + colors: &[target], + depth_stencil: None, + }, + ) { + let globals = GlobalParams { + viewport_size: [width, height], + premultiplied_alpha: 0, + pad: 0, + }; + let mut encoder = pass.with(&self.pipelines.path_rasterization); + + let mut vertices = Vec::new(); + for path in paths { + vertices.extend(path.vertices.iter().map(|v| PathRasterizationVertex { + xy_position: v.xy_position, + st_position: v.st_position, + color: path.color, + bounds: path.clipped_bounds(), + })); + } + let vertex_buf = unsafe { self.instance_belt.alloc_typed(&vertices, &self.gpu) }; + encoder.bind( + 0, + &ShaderPathRasterizationData { + globals, + b_path_vertices: vertex_buf, + }, + ); + encoder.draw(0, vertices.len() as u32, 0, 1); + } + } + + pub fn destroy(&mut self) { + self.wait_for_gpu(); + self.atlas.destroy(); + self.gpu.destroy_sampler(self.atlas_sampler); + self.instance_belt.destroy(&self.gpu); + self.gpu.destroy_command_encoder(&mut self.command_encoder); + self.pipelines.destroy(&self.gpu); + self.gpu.destroy_surface(&mut self.surface); + self.gpu.destroy_texture(self.path_intermediate_texture); + self.gpu + .destroy_texture_view(self.path_intermediate_texture_view); + if let Some(msaa_texture) = self.path_intermediate_msaa_texture { + self.gpu.destroy_texture(msaa_texture); + } + if let Some(msaa_view) = self.path_intermediate_msaa_texture_view { + self.gpu.destroy_texture_view(msaa_view); + } + } + + pub fn draw(&mut self, scene: &Scene) { + self.command_encoder.start(); + self.atlas.before_frame(&mut self.command_encoder); + + let frame = { + profiling::scope!("acquire frame"); + self.surface.acquire_frame() + }; + self.command_encoder.init_texture(frame.texture()); + + let globals = GlobalParams { + viewport_size: [ + self.surface_config.size.width as f32, + self.surface_config.size.height as f32, + ], + premultiplied_alpha: match self.surface.info().alpha { + gpu::AlphaMode::Ignored | gpu::AlphaMode::PostMultiplied => 0, + gpu::AlphaMode::PreMultiplied => 1, + }, + pad: 0, + }; + + let mut pass = self.command_encoder.render( + "main", + gpu::RenderTargetSet { + colors: &[gpu::RenderTarget { + view: frame.texture_view(), + init_op: gpu::InitOp::Clear(gpu::TextureColor::TransparentBlack), + finish_op: gpu::FinishOp::Store, + }], + depth_stencil: None, + }, + ); + + profiling::scope!("render pass"); + for batch in scene.batches() { + match batch { + PrimitiveBatch::Quads(quads) => { + let instance_buf = unsafe { self.instance_belt.alloc_typed(quads, &self.gpu) }; + let mut encoder = pass.with(&self.pipelines.quads); + encoder.bind( + 0, + &ShaderQuadsData { + globals, + b_quads: instance_buf, + }, + ); + encoder.draw(0, 4, 0, quads.len() as u32); + } + PrimitiveBatch::Shadows(shadows) => { + let instance_buf = + unsafe { self.instance_belt.alloc_typed(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 Some(first_path) = paths.first() else { + continue; + }; + drop(pass); + self.draw_paths_to_intermediate( + paths, + self.surface_config.size.width as f32, + self.surface_config.size.height as f32, + ); + pass = self.command_encoder.render( + "main", + gpu::RenderTargetSet { + colors: &[gpu::RenderTarget { + view: frame.texture_view(), + init_op: gpu::InitOp::Load, + finish_op: gpu::FinishOp::Store, + }], + depth_stencil: None, + }, + ); + let mut encoder = pass.with(&self.pipelines.paths); + // When copying paths from the intermediate texture to the drawable, + // each pixel must only be copied once, in case of transparent paths. + // + // If all paths have the same draw order, then their bounds are all + // disjoint, so we can copy each path's bounds individually. If this + // batch combines different draw orders, we perform a single copy + // for a minimal spanning rect. + let sprites = if paths.last().unwrap().order == first_path.order { + paths + .iter() + .map(|path| PathSprite { + bounds: path.clipped_bounds(), + }) + .collect() + } else { + let mut bounds = first_path.clipped_bounds(); + for path in paths.iter().skip(1) { + bounds = bounds.union(&path.clipped_bounds()); + } + vec![PathSprite { bounds }] + }; + let instance_buf = + unsafe { self.instance_belt.alloc_typed(&sprites, &self.gpu) }; + encoder.bind( + 0, + &ShaderPathsData { + globals, + t_sprite: self.path_intermediate_texture_view, + s_sprite: self.atlas_sampler, + b_path_sprites: instance_buf, + }, + ); + encoder.draw(0, 4, 0, sprites.len() as u32); + } + PrimitiveBatch::Underlines(underlines) => { + let instance_buf = + unsafe { self.instance_belt.alloc_typed(underlines, &self.gpu) }; + let mut encoder = pass.with(&self.pipelines.underlines); + encoder.bind( + 0, + &ShaderUnderlinesData { + globals, + b_underlines: instance_buf, + }, + ); + encoder.draw(0, 4, 0, underlines.len() as u32); + } + PrimitiveBatch::MonochromeSprites { + texture_id, + sprites, + } => { + let tex_info = self.atlas.get_texture_info(texture_id); + let instance_buf = + unsafe { self.instance_belt.alloc_typed(sprites, &self.gpu) }; + let mut encoder = pass.with(&self.pipelines.mono_sprites); + encoder.bind( + 0, + &ShaderMonoSpritesData { + globals, + gamma_ratios: self.rendering_parameters.gamma_ratios, + grayscale_enhanced_contrast: self + .rendering_parameters + .grayscale_enhanced_contrast, + t_sprite: tex_info.raw_view, + s_sprite: self.atlas_sampler, + b_mono_sprites: instance_buf, + }, + ); + encoder.draw(0, 4, 0, sprites.len() as u32); + } + PrimitiveBatch::PolychromeSprites { + texture_id, + sprites, + } => { + let tex_info = self.atlas.get_texture_info(texture_id); + let instance_buf = + unsafe { self.instance_belt.alloc_typed(sprites, &self.gpu) }; + let mut encoder = pass.with(&self.pipelines.poly_sprites); + encoder.bind( + 0, + &ShaderPolySpritesData { + globals, + t_sprite: tex_info.raw_view, + s_sprite: self.atlas_sampler, + b_poly_sprites: instance_buf, + }, + ); + encoder.draw(0, 4, 0, sprites.len() as u32); + } + PrimitiveBatch::Surfaces(surfaces) => { + let mut _encoder = pass.with(&self.pipelines.surfaces); + + for surface in surfaces { + #[cfg(not(target_os = "macos"))] + { + let _ = surface; + continue; + }; + + #[cfg(target_os = "macos")] + { + let (t_y, t_cb_cr) = unsafe { + use core_foundation::base::TCFType as _; + use std::ptr; + + assert_eq!( + surface.image_buffer.get_pixel_format(), + core_video::pixel_buffer::kCVPixelFormatType_420YpCbCr8BiPlanarFullRange + ); + + let y_texture = self + .core_video_texture_cache + .create_texture_from_image( + surface.image_buffer.as_concrete_TypeRef(), + ptr::null(), + metal::MTLPixelFormat::R8Unorm, + surface.image_buffer.get_width_of_plane(0), + surface.image_buffer.get_height_of_plane(0), + 0, + ) + .unwrap(); + let cb_cr_texture = self + .core_video_texture_cache + .create_texture_from_image( + surface.image_buffer.as_concrete_TypeRef(), + ptr::null(), + metal::MTLPixelFormat::RG8Unorm, + surface.image_buffer.get_width_of_plane(1), + surface.image_buffer.get_height_of_plane(1), + 1, + ) + .unwrap(); + ( + gpu::TextureView::from_metal_texture( + &objc2::rc::Retained::retain( + foreign_types::ForeignTypeRef::as_ptr( + y_texture.as_texture_ref(), + ) + as *mut objc2::runtime::ProtocolObject< + dyn objc2_metal::MTLTexture, + >, + ) + .unwrap(), + gpu::TexelAspects::COLOR, + ), + gpu::TextureView::from_metal_texture( + &objc2::rc::Retained::retain( + foreign_types::ForeignTypeRef::as_ptr( + cb_cr_texture.as_texture_ref(), + ) + as *mut objc2::runtime::ProtocolObject< + dyn objc2_metal::MTLTexture, + >, + ) + .unwrap(), + gpu::TexelAspects::COLOR, + ), + ) + }; + + _encoder.bind( + 0, + &ShaderSurfacesData { + globals, + surface_locals: SurfaceParams { + bounds: surface.bounds.into(), + content_mask: surface.content_mask.bounds.into(), + }, + t_y, + t_cb_cr, + s_surface: self.atlas_sampler, + }, + ); + + _encoder.draw(0, 4, 0, 1); + } + } + } + } + } + drop(pass); + + self.command_encoder.present(frame); + let sync_point = self.gpu.submit(&mut self.command_encoder); + + profiling::scope!("finish"); + self.instance_belt.flush(&sync_point); + self.atlas.after_frame(&sync_point); + + self.wait_for_gpu(); + self.last_sync_point = Some(sync_point); + } +} + +fn create_path_intermediate_texture( + gpu: &gpu::Context, + format: gpu::TextureFormat, + width: u32, + height: u32, +) -> (gpu::Texture, gpu::TextureView) { + let texture = gpu.create_texture(gpu::TextureDesc { + name: "path intermediate", + format, + size: gpu::Extent { + width, + height, + depth: 1, + }, + array_layer_count: 1, + mip_level_count: 1, + sample_count: 1, + dimension: gpu::TextureDimension::D2, + usage: gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE | gpu::TextureUsage::TARGET, + external: None, + }); + let texture_view = gpu.create_texture_view( + texture, + gpu::TextureViewDesc { + name: "path intermediate view", + format, + dimension: gpu::ViewDimension::D2, + subresources: &Default::default(), + }, + ); + (texture, texture_view) +} + +fn create_msaa_texture_if_needed( + gpu: &gpu::Context, + format: gpu::TextureFormat, + width: u32, + height: u32, + sample_count: u32, +) -> Option<(gpu::Texture, gpu::TextureView)> { + if sample_count <= 1 { + return None; + } + let texture_msaa = gpu.create_texture(gpu::TextureDesc { + name: "path intermediate msaa", + format, + size: gpu::Extent { + width, + height, + depth: 1, + }, + array_layer_count: 1, + mip_level_count: 1, + sample_count, + dimension: gpu::TextureDimension::D2, + usage: gpu::TextureUsage::TARGET, + external: None, + }); + let texture_view_msaa = gpu.create_texture_view( + texture_msaa, + gpu::TextureViewDesc { + name: "path intermediate msaa view", + format, + dimension: gpu::ViewDimension::D2, + subresources: &Default::default(), + }, + ); + + Some((texture_msaa, texture_view_msaa)) +} + +/// A set of parameters that can be set using a corresponding environment variable. +struct RenderingParameters { + // Env var: ZED_PATH_SAMPLE_COUNT + // workaround for https://github.com/zed-industries/zed/issues/26143 + path_sample_count: u32, + + // Env var: ZED_FONTS_GAMMA + // Allowed range [1.0, 2.2], other values are clipped + // Default: 1.8 + gamma_ratios: [f32; 4], + // Env var: ZED_FONTS_GRAYSCALE_ENHANCED_CONTRAST + // Allowed range: [0.0, ..), other values are clipped + // Default: 1.0 + grayscale_enhanced_contrast: f32, +} + +impl RenderingParameters { + fn from_env(context: &BladeContext) -> Self { + use std::env; + + let path_sample_count = env::var("ZED_PATH_SAMPLE_COUNT") + .ok() + .and_then(|v| v.parse().ok()) + .or_else(|| { + [4, 2, 1] + .into_iter() + .find(|&n| (context.gpu.capabilities().sample_count_mask & n) != 0) + }) + .unwrap_or(1); + let gamma = env::var("ZED_FONTS_GAMMA") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(1.8_f32) + .clamp(1.0, 2.2); + let gamma_ratios = Self::get_gamma_ratios(gamma); + let grayscale_enhanced_contrast = env::var("ZED_FONTS_GRAYSCALE_ENHANCED_CONTRAST") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(1.0_f32) + .max(0.0); + + Self { + path_sample_count, + gamma_ratios, + grayscale_enhanced_contrast, + } + } + + // Gamma ratios for brightening/darkening edges for better contrast + // https://github.com/microsoft/terminal/blob/1283c0f5b99a2961673249fa77c6b986efb5086c/src/renderer/atlas/dwrite.cpp#L50 + fn get_gamma_ratios(gamma: f32) -> [f32; 4] { + const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [ + [0.0000 / 4.0, 0.0000 / 4.0, 0.0000 / 4.0, 0.0000 / 4.0], // gamma = 1.0 + [0.0166 / 4.0, -0.0807 / 4.0, 0.2227 / 4.0, -0.0751 / 4.0], // gamma = 1.1 + [0.0350 / 4.0, -0.1760 / 4.0, 0.4325 / 4.0, -0.1370 / 4.0], // gamma = 1.2 + [0.0543 / 4.0, -0.2821 / 4.0, 0.6302 / 4.0, -0.1876 / 4.0], // gamma = 1.3 + [0.0739 / 4.0, -0.3963 / 4.0, 0.8167 / 4.0, -0.2287 / 4.0], // gamma = 1.4 + [0.0933 / 4.0, -0.5161 / 4.0, 0.9926 / 4.0, -0.2616 / 4.0], // gamma = 1.5 + [0.1121 / 4.0, -0.6395 / 4.0, 1.1588 / 4.0, -0.2877 / 4.0], // gamma = 1.6 + [0.1300 / 4.0, -0.7649 / 4.0, 1.3159 / 4.0, -0.3080 / 4.0], // gamma = 1.7 + [0.1469 / 4.0, -0.8911 / 4.0, 1.4644 / 4.0, -0.3234 / 4.0], // gamma = 1.8 + [0.1627 / 4.0, -1.0170 / 4.0, 1.6051 / 4.0, -0.3347 / 4.0], // gamma = 1.9 + [0.1773 / 4.0, -1.1420 / 4.0, 1.7385 / 4.0, -0.3426 / 4.0], // gamma = 2.0 + [0.1908 / 4.0, -1.2652 / 4.0, 1.8650 / 4.0, -0.3476 / 4.0], // gamma = 2.1 + [0.2031 / 4.0, -1.3864 / 4.0, 1.9851 / 4.0, -0.3501 / 4.0], // gamma = 2.2 + ]; + + const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32; + const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32; + + let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10; + let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index]; + + [ + ratios[0] * NORM13, + ratios[1] * NORM24, + ratios[2] * NORM13, + ratios[3] * NORM24, + ] + } +} diff --git a/third_party/gpui/src/platform/blade/shaders.wgsl b/third_party/gpui/src/platform/blade/shaders.wgsl new file mode 100644 index 0000000..1de8ad4 --- /dev/null +++ b/third_party/gpui/src/platform/blade/shaders.wgsl @@ -0,0 +1,1296 @@ +/* Functions useful for debugging: + +// A heat map color for debugging (blue -> cyan -> green -> yellow -> red). +fn heat_map_color(value: f32, minValue: f32, maxValue: f32, position: vec2) -> vec4 { + // Normalize value to 0-1 range + let t = clamp((value - minValue) / (maxValue - minValue), 0.0, 1.0); + + // Heat map color calculation + let r = t * t; + let g = 4.0 * t * (1.0 - t); + let b = (1.0 - t) * (1.0 - t); + let heat_color = vec3(r, g, b); + + // Create a checkerboard pattern (black and white) + let sum = floor(position.x / 3) + floor(position.y / 3); + let is_odd = fract(sum * 0.5); // 0.0 for even, 0.5 for odd + let checker_value = is_odd * 2.0; // 0.0 for even, 1.0 for odd + let checker_color = vec3(checker_value); + + // Determine if value is in range (1.0 if in range, 0.0 if out of range) + let in_range = step(minValue, value) * step(value, maxValue); + + // Mix checkerboard and heat map based on whether value is in range + let final_color = mix(checker_color, heat_color, in_range); + + return vec4(final_color, 1.0); +} + +*/ + +fn color_brightness(color: vec3) -> f32 { + // REC. 601 luminance coefficients for perceived brightness + return dot(color, vec3(0.30, 0.59, 0.11)); +} + +fn light_on_dark_contrast(enhancedContrast: f32, color: vec3) -> f32 { + let brightness = color_brightness(color); + let multiplier = saturate(4.0 * (0.75 - brightness)); + return enhancedContrast * multiplier; +} + +fn enhance_contrast(alpha: f32, k: f32) -> f32 { + return alpha * (k + 1.0) / (alpha * k + 1.0); +} + +fn apply_alpha_correction(a: f32, b: f32, g: vec4) -> f32 { + let brightness_adjustment = g.x * b + g.y; + let correction = brightness_adjustment * a + (g.z * b + g.w); + return a + a * (1.0 - a) * correction; +} + +fn apply_contrast_and_gamma_correction(sample: f32, color: vec3, enhanced_contrast_factor: f32, gamma_ratios: vec4) -> f32 { + let enhanced_contrast = light_on_dark_contrast(enhanced_contrast_factor, color); + let brightness = color_brightness(color); + + let contrasted = enhance_contrast(sample, enhanced_contrast); + return apply_alpha_correction(contrasted, brightness, gamma_ratios); +} + +struct GlobalParams { + viewport_size: vec2, + premultiplied_alpha: u32, + pad: u32, +} + +var globals: GlobalParams; +var gamma_ratios: vec4; +var grayscale_enhanced_contrast: f32; +var t_sprite: texture_2d; +var s_sprite: sampler; + +const M_PI_F: f32 = 3.1415926; +const GRAYSCALE_FACTORS: vec3 = vec3(0.2126, 0.7152, 0.0722); + +struct Bounds { + origin: vec2, + size: vec2, +} + +struct Corners { + top_left: f32, + top_right: f32, + bottom_right: f32, + bottom_left: f32, +} + +struct Edges { + top: f32, + right: f32, + bottom: f32, + left: f32, +} + +struct Hsla { + h: f32, + s: f32, + l: f32, + a: f32, +} + +struct LinearColorStop { + color: Hsla, + percentage: f32, +} + +struct Background { + // 0u is Solid + // 1u is LinearGradient + // 2u is PatternSlash + tag: u32, + // 0u is sRGB linear color + // 1u is Oklab color + color_space: u32, + solid: Hsla, + gradient_angle_or_pattern_height: f32, + colors: array, + pad: u32, +} + +struct AtlasTextureId { + index: u32, + kind: u32, +} + +struct AtlasBounds { + origin: vec2, + size: vec2, +} + +struct AtlasTile { + texture_id: AtlasTextureId, + tile_id: u32, + padding: u32, + bounds: AtlasBounds, +} + +struct TransformationMatrix { + rotation_scale: mat2x2, + translation: vec2, +} + +fn to_device_position_impl(position: vec2) -> vec4 { + let device_position = position / globals.viewport_size * vec2(2.0, -2.0) + vec2(-1.0, 1.0); + return vec4(device_position, 0.0, 1.0); +} + +fn to_device_position(unit_vertex: vec2, bounds: Bounds) -> vec4 { + let position = unit_vertex * vec2(bounds.size) + bounds.origin; + return to_device_position_impl(position); +} + +fn to_device_position_transformed(unit_vertex: vec2, bounds: Bounds, transform: TransformationMatrix) -> vec4 { + let position = unit_vertex * vec2(bounds.size) + bounds.origin; + //Note: Rust side stores it as row-major, so transposing here + let transformed = transpose(transform.rotation_scale) * position + transform.translation; + return to_device_position_impl(transformed); +} + +fn to_tile_position(unit_vertex: vec2, tile: AtlasTile) -> vec2 { + let atlas_size = vec2(textureDimensions(t_sprite, 0)); + return (vec2(tile.bounds.origin) + unit_vertex * vec2(tile.bounds.size)) / atlas_size; +} + +fn distance_from_clip_rect_impl(position: vec2, clip_bounds: Bounds) -> vec4 { + let tl = position - clip_bounds.origin; + let br = clip_bounds.origin + clip_bounds.size - position; + return vec4(tl.x, br.x, tl.y, br.y); +} + +fn distance_from_clip_rect(unit_vertex: vec2, bounds: Bounds, clip_bounds: Bounds) -> vec4 { + let position = unit_vertex * vec2(bounds.size) + bounds.origin; + return distance_from_clip_rect_impl(position, clip_bounds); +} + +fn distance_from_clip_rect_transformed(unit_vertex: vec2, bounds: Bounds, clip_bounds: Bounds, transform: TransformationMatrix) -> vec4 { + let position = unit_vertex * vec2(bounds.size) + bounds.origin; + let transformed = transpose(transform.rotation_scale) * position + transform.translation; + return distance_from_clip_rect_impl(transformed, clip_bounds); +} + +// https://gamedev.stackexchange.com/questions/92015/optimized-linear-to-srgb-glsl +fn srgb_to_linear(srgb: vec3) -> vec3 { + let cutoff = srgb < vec3(0.04045); + let higher = pow((srgb + vec3(0.055)) / vec3(1.055), vec3(2.4)); + let lower = srgb / vec3(12.92); + return select(higher, lower, cutoff); +} + +fn srgb_to_linear_component(a: f32) -> f32 { + let cutoff = a < 0.04045; + let higher = pow((a + 0.055) / 1.055, 2.4); + let lower = a / 12.92; + return select(higher, lower, cutoff); +} + +fn linear_to_srgb(linear: vec3) -> vec3 { + let cutoff = linear < vec3(0.0031308); + let higher = vec3(1.055) * pow(linear, vec3(1.0 / 2.4)) - vec3(0.055); + let lower = linear * vec3(12.92); + return select(higher, lower, cutoff); +} + +/// Convert a linear color to sRGBA space. +fn linear_to_srgba(color: vec4) -> vec4 { + return vec4(linear_to_srgb(color.rgb), color.a); +} + +/// Convert a sRGBA color to linear space. +fn srgba_to_linear(color: vec4) -> vec4 { + return vec4(srgb_to_linear(color.rgb), color.a); +} + +/// Hsla to linear RGBA conversion. +fn hsla_to_rgba(hsla: Hsla) -> vec4 { + let h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range + let s = hsla.s; + let l = hsla.l; + let a = hsla.a; + + let c = (1.0 - abs(2.0 * l - 1.0)) * s; + let x = c * (1.0 - abs(h % 2.0 - 1.0)); + let m = l - c / 2.0; + var color = vec3(m); + + if (h >= 0.0 && h < 1.0) { + color.r += c; + color.g += x; + } else if (h >= 1.0 && h < 2.0) { + color.r += x; + color.g += c; + } else if (h >= 2.0 && h < 3.0) { + color.g += c; + color.b += x; + } else if (h >= 3.0 && h < 4.0) { + color.g += x; + color.b += c; + } else if (h >= 4.0 && h < 5.0) { + color.r += x; + color.b += c; + } else { + color.r += c; + color.b += x; + } + + return vec4(color, a); +} + +/// Convert a linear sRGB to Oklab space. +/// Reference: https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab +fn linear_srgb_to_oklab(color: vec4) -> vec4 { + let l = 0.4122214708 * color.r + 0.5363325363 * color.g + 0.0514459929 * color.b; + let m = 0.2119034982 * color.r + 0.6806995451 * color.g + 0.1073969566 * color.b; + let s = 0.0883024619 * color.r + 0.2817188376 * color.g + 0.6299787005 * color.b; + + let l_ = pow(l, 1.0 / 3.0); + let m_ = pow(m, 1.0 / 3.0); + let s_ = pow(s, 1.0 / 3.0); + + return vec4( + 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_, + 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_, + 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_, + color.a + ); +} + +/// Convert an Oklab color to linear sRGB space. +fn oklab_to_linear_srgb(color: vec4) -> vec4 { + let l_ = color.r + 0.3963377774 * color.g + 0.2158037573 * color.b; + let m_ = color.r - 0.1055613458 * color.g - 0.0638541728 * color.b; + let s_ = color.r - 0.0894841775 * color.g - 1.2914855480 * color.b; + + let l = l_ * l_ * l_; + let m = m_ * m_ * m_; + let s = s_ * s_ * s_; + + return vec4( + 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, + -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, + -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s, + color.a + ); +} + +fn over(below: vec4, above: vec4) -> vec4 { + let alpha = above.a + below.a * (1.0 - above.a); + let color = (above.rgb * above.a + below.rgb * below.a * (1.0 - above.a)) / alpha; + return vec4(color, alpha); +} + +// A standard gaussian function, used for weighting samples +fn gaussian(x: f32, sigma: f32) -> f32{ + return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * M_PI_F) * sigma); +} + +// This approximates the error function, needed for the gaussian integral +fn erf(v: vec2) -> vec2 { + let s = sign(v); + let a = abs(v); + let r1 = 1.0 + (0.278393 + (0.230389 + (0.000972 + 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; +} + +// Selects corner radius based on quadrant. +fn pick_corner_radius(center_to_point: vec2, radii: Corners) -> f32 { + if (center_to_point.x < 0.0) { + if (center_to_point.y < 0.0) { + return radii.top_left; + } else { + return radii.bottom_left; + } + } else { + if (center_to_point.y < 0.0) { + return radii.top_right; + } else { + return radii.bottom_right; + } + } +} + +// Signed distance of the point to the quad's border - positive outside the +// border, and negative inside. +// +// See comments on similar code using `quad_sdf_impl` in `fs_quad` for +// explanation. +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 corner_to_point = abs(center_to_point) - half_size; + let corner_center_to_point = corner_to_point + corner_radius; + return quad_sdf_impl(corner_center_to_point, corner_radius); +} + +fn quad_sdf_impl(corner_center_to_point: vec2, corner_radius: f32) -> f32 { + if (corner_radius == 0.0) { + // Fast path for unrounded corners. + return max(corner_center_to_point.x, corner_center_to_point.y); + } else { + // Signed distance of the point from a quad that is inset by corner_radius. + // It is negative inside this quad, and positive outside. + let signed_distance_to_inset_quad = + // 0 inside the inset quad, and positive outside. + length(max(vec2(0.0), corner_center_to_point)) + + // 0 outside the inset quad, and negative inside. + min(0.0, max(corner_center_to_point.x, corner_center_to_point.y)); + + return signed_distance_to_inset_quad - corner_radius; + } +} + +// Abstract away the final color transformation based on the +// target alpha compositing mode. +fn blend_color(color: vec4, alpha_factor: f32) -> vec4 { + let alpha = color.a * alpha_factor; + let multiplier = select(1.0, alpha, globals.premultiplied_alpha != 0u); + return vec4(color.rgb * multiplier, alpha); +} + + +struct GradientColor { + solid: vec4, + color0: vec4, + color1: vec4, +} + +fn prepare_gradient_color(tag: u32, color_space: u32, + solid: Hsla, colors: array) -> GradientColor { + var result = GradientColor(); + + if (tag == 0u || tag == 2u) { + result.solid = hsla_to_rgba(solid); + } else if (tag == 1u) { + // The hsla_to_rgba is returns a linear sRGB color + result.color0 = hsla_to_rgba(colors[0].color); + result.color1 = hsla_to_rgba(colors[1].color); + + // Prepare color space in vertex for avoid conversion + // in fragment shader for performance reasons + if (color_space == 0u) { + // sRGB + result.color0 = linear_to_srgba(result.color0); + result.color1 = linear_to_srgba(result.color1); + } else if (color_space == 1u) { + // Oklab + result.color0 = linear_srgb_to_oklab(result.color0); + result.color1 = linear_srgb_to_oklab(result.color1); + } + } + + return result; +} + +fn gradient_color(background: Background, position: vec2, bounds: Bounds, + solid_color: vec4, color0: vec4, color1: vec4) -> vec4 { + var background_color = vec4(0.0); + + switch (background.tag) { + default: { + return solid_color; + } + case 1u: { + // Linear gradient background. + // -90 degrees to match the CSS gradient angle. + let angle = background.gradient_angle_or_pattern_height; + let radians = (angle % 360.0 - 90.0) * M_PI_F / 180.0; + var direction = vec2(cos(radians), sin(radians)); + let stop0_percentage = background.colors[0].percentage; + let stop1_percentage = background.colors[1].percentage; + + // Expand the short side to be the same as the long side + if (bounds.size.x > bounds.size.y) { + direction.y *= bounds.size.y / bounds.size.x; + } else { + direction.x *= bounds.size.x / bounds.size.y; + } + + // Get the t value for the linear gradient with the color stop percentages. + let half_size = bounds.size / 2.0; + let center = bounds.origin + half_size; + let center_to_point = position - center; + var t = dot(center_to_point, direction) / length(direction); + // Check the direct to determine the use x or y + if (abs(direction.x) > abs(direction.y)) { + t = (t + half_size.x) / bounds.size.x; + } else { + t = (t + half_size.y) / bounds.size.y; + } + + // Adjust t based on the stop percentages + t = (t - stop0_percentage) / (stop1_percentage - stop0_percentage); + t = clamp(t, 0.0, 1.0); + + switch (background.color_space) { + default: { + background_color = srgba_to_linear(mix(color0, color1, t)); + } + case 1u: { + let oklab_color = mix(color0, color1, t); + background_color = oklab_to_linear_srgb(oklab_color); + } + } + } + case 2u: { + let gradient_angle_or_pattern_height = background.gradient_angle_or_pattern_height; + let pattern_width = (gradient_angle_or_pattern_height / 65535.0f) / 255.0f; + let pattern_interval = (gradient_angle_or_pattern_height % 65535.0f) / 255.0f; + let pattern_height = pattern_width + pattern_interval; + let stripe_angle = M_PI_F / 4.0; + let pattern_period = pattern_height * sin(stripe_angle); + let rotation = mat2x2( + cos(stripe_angle), -sin(stripe_angle), + sin(stripe_angle), cos(stripe_angle) + ); + let relative_position = position - bounds.origin; + let rotated_point = rotation * relative_position; + let pattern = rotated_point.x % pattern_period; + let distance = min(pattern, pattern_period - pattern) - pattern_period * (pattern_width / pattern_height) / 2.0f; + background_color = solid_color; + background_color.a *= saturate(0.5 - distance); + } + } + + return background_color; +} + +// --- quads --- // + +struct Quad { + order: u32, + border_style: u32, + bounds: Bounds, + content_mask: Bounds, + background: Background, + border_color: Hsla, + corner_radii: Corners, + border_widths: Edges, +} +var b_quads: array; + +struct QuadVarying { + @builtin(position) position: vec4, + @location(0) @interpolate(flat) border_color: vec4, + @location(1) @interpolate(flat) quad_id: u32, + // TODO: use `clip_distance` once Naga supports it + @location(2) clip_distances: vec4, + @location(3) @interpolate(flat) background_solid: vec4, + @location(4) @interpolate(flat) background_color0: vec4, + @location(5) @interpolate(flat) background_color1: 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); + + let gradient = prepare_gradient_color( + quad.background.tag, + quad.background.color_space, + quad.background.solid, + quad.background.colors + ); + out.background_solid = gradient.solid; + out.background_color0 = gradient.color0; + out.background_color1 = gradient.color1; + 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 background_color = gradient_color(quad.background, input.position.xy, quad.bounds, + input.background_solid, input.background_color0, input.background_color1); + + let unrounded = quad.corner_radii.top_left == 0.0 && + quad.corner_radii.bottom_left == 0.0 && + quad.corner_radii.top_right == 0.0 && + quad.corner_radii.bottom_right == 0.0; + + // Fast path when the quad is not rounded and doesn't have any border + if (quad.border_widths.top == 0.0 && + quad.border_widths.left == 0.0 && + quad.border_widths.right == 0.0 && + quad.border_widths.bottom == 0.0 && + unrounded) { + return blend_color(background_color, 1.0); + } + + let size = quad.bounds.size; + let half_size = size / 2.0; + let point = input.position.xy - quad.bounds.origin; + let center_to_point = point - half_size; + + // Signed distance field threshold for inclusion of pixels. 0.5 is the + // minimum distance between the center of the pixel and the edge. + let antialias_threshold = 0.5; + + // Radius of the nearest corner + let corner_radius = pick_corner_radius(center_to_point, quad.corner_radii); + + // Width of the nearest borders + let border = vec2( + select( + quad.border_widths.right, + quad.border_widths.left, + center_to_point.x < 0.0), + select( + quad.border_widths.bottom, + quad.border_widths.top, + center_to_point.y < 0.0)); + + // 0-width borders are reduced so that `inner_sdf >= antialias_threshold`. + // The purpose of this is to not draw antialiasing pixels in this case. + let reduced_border = + vec2(select(border.x, -antialias_threshold, border.x == 0.0), + select(border.y, -antialias_threshold, border.y == 0.0)); + + // Vector from the corner of the quad bounds to the point, after mirroring + // the point into the bottom right quadrant. Both components are <= 0. + let corner_to_point = abs(center_to_point) - half_size; + + // Vector from the point to the center of the rounded corner's circle, also + // mirrored into bottom right quadrant. + let corner_center_to_point = corner_to_point + corner_radius; + + // Whether the nearest point on the border is rounded + let is_near_rounded_corner = + corner_center_to_point.x >= 0 && + corner_center_to_point.y >= 0; + + // Vector from straight border inner corner to point. + let straight_border_inner_corner_to_point = corner_to_point + reduced_border; + + // Whether the point is beyond the inner edge of the straight border. + let is_beyond_inner_straight_border = + straight_border_inner_corner_to_point.x > 0 || + straight_border_inner_corner_to_point.y > 0; + + // Whether the point is far enough inside the quad, such that the pixels are + // not affected by the straight border. + let is_within_inner_straight_border = + straight_border_inner_corner_to_point.x < -antialias_threshold && + straight_border_inner_corner_to_point.y < -antialias_threshold; + + // Fast path for points that must be part of the background. + // + // This could be optimized further for large rounded corners by including + // points in an inscribed rectangle, or some other quick linear check. + // However, that might negatively impact performance in the case of + // reasonable sizes for rounded corners. + if (is_within_inner_straight_border && !is_near_rounded_corner) { + return blend_color(background_color, 1.0); + } + + // Signed distance of the point to the outside edge of the quad's border. It + // is positive outside this edge, and negative inside. + let outer_sdf = quad_sdf_impl(corner_center_to_point, corner_radius); + + // Approximate signed distance of the point to the inside edge of the quad's + // border. It is negative outside this edge (within the border), and + // positive inside. + // + // This is not always an accurate signed distance: + // * The rounded portions with varying border width use an approximation of + // nearest-point-on-ellipse. + // * When it is quickly known to be outside the edge, -1.0 is used. + var inner_sdf = 0.0; + if (corner_center_to_point.x <= 0 || corner_center_to_point.y <= 0) { + // Fast paths for straight borders. + inner_sdf = -max(straight_border_inner_corner_to_point.x, + straight_border_inner_corner_to_point.y); + } else if (is_beyond_inner_straight_border) { + // Fast path for points that must be outside the inner edge. + inner_sdf = -1.0; + } else if (reduced_border.x == reduced_border.y) { + // Fast path for circular inner edge. + inner_sdf = -(outer_sdf + reduced_border.x); + } else { + let ellipse_radii = max(vec2(0.0), corner_radius - reduced_border); + inner_sdf = quarter_ellipse_sdf(corner_center_to_point, ellipse_radii); + } + + // Negative when inside the border + let border_sdf = max(inner_sdf, outer_sdf); + + var color = background_color; + if (border_sdf < antialias_threshold) { + var border_color = input.border_color; + + // Dashed border logic when border_style == 1 + if (quad.border_style == 1) { + // Position along the perimeter in "dash space", where each dash + // period has length 1 + var t = 0.0; + + // Total number of dash periods, so that the dash spacing can be + // adjusted to evenly divide it + var max_t = 0.0; + + // Border width is proportional to dash size. This is the behavior + // used by browsers, but also avoids dashes from different segments + // overlapping when dash size is smaller than the border width. + // + // Dash pattern: (2 * border width) dash, (1 * border width) gap + let dash_length_per_width = 2.0; + let dash_gap_per_width = 1.0; + let dash_period_per_width = dash_length_per_width + dash_gap_per_width; + + // Since the dash size is determined by border width, the density of + // dashes varies. Multiplying a pixel distance by this returns a + // position in dash space - it has units (dash period / pixels). So + // a dash velocity of (1 / 10) is 1 dash every 10 pixels. + var dash_velocity = 0.0; + + // Dividing this by the border width gives the dash velocity + let dv_numerator = 1.0 / dash_period_per_width; + + if (unrounded) { + // When corners aren't rounded, the dashes are separately laid + // out on each straight line, rather than around the whole + // perimeter. This way each line starts and ends with a dash. + let is_horizontal = + corner_center_to_point.x < + corner_center_to_point.y; + + // When applying dashed borders to just some, not all, the sides. + // The way we chose border widths above sometimes comes with a 0 width value. + // So we choose again to avoid division by zero. + // TODO: A better solution exists taking a look at the whole file. + // this does not fix single dashed borders at the corners + let dashed_border = vec2( + max( + quad.border_widths.bottom, + quad.border_widths.top, + ), + max( + quad.border_widths.right, + quad.border_widths.left, + ) + ); + + let border_width = select(dashed_border.y, dashed_border.x, is_horizontal); + dash_velocity = dv_numerator / border_width; + t = select(point.y, point.x, is_horizontal) * dash_velocity; + max_t = select(size.y, size.x, is_horizontal) * dash_velocity; + } else { + // When corners are rounded, the dashes are laid out clockwise + // around the whole perimeter. + + let r_tr = quad.corner_radii.top_right; + let r_br = quad.corner_radii.bottom_right; + let r_bl = quad.corner_radii.bottom_left; + let r_tl = quad.corner_radii.top_left; + + let w_t = quad.border_widths.top; + let w_r = quad.border_widths.right; + let w_b = quad.border_widths.bottom; + let w_l = quad.border_widths.left; + + // Straight side dash velocities + let dv_t = select(dv_numerator / w_t, 0.0, w_t <= 0.0); + let dv_r = select(dv_numerator / w_r, 0.0, w_r <= 0.0); + let dv_b = select(dv_numerator / w_b, 0.0, w_b <= 0.0); + let dv_l = select(dv_numerator / w_l, 0.0, w_l <= 0.0); + + // Straight side lengths in dash space + let s_t = (size.x - r_tl - r_tr) * dv_t; + let s_r = (size.y - r_tr - r_br) * dv_r; + let s_b = (size.x - r_br - r_bl) * dv_b; + let s_l = (size.y - r_bl - r_tl) * dv_l; + + let corner_dash_velocity_tr = corner_dash_velocity(dv_t, dv_r); + let corner_dash_velocity_br = corner_dash_velocity(dv_b, dv_r); + let corner_dash_velocity_bl = corner_dash_velocity(dv_b, dv_l); + let corner_dash_velocity_tl = corner_dash_velocity(dv_t, dv_l); + + // Corner lengths in dash space + let c_tr = r_tr * (M_PI_F / 2.0) * corner_dash_velocity_tr; + let c_br = r_br * (M_PI_F / 2.0) * corner_dash_velocity_br; + let c_bl = r_bl * (M_PI_F / 2.0) * corner_dash_velocity_bl; + let c_tl = r_tl * (M_PI_F / 2.0) * corner_dash_velocity_tl; + + // Cumulative dash space upto each segment + let upto_tr = s_t; + let upto_r = upto_tr + c_tr; + let upto_br = upto_r + s_r; + let upto_b = upto_br + c_br; + let upto_bl = upto_b + s_b; + let upto_l = upto_bl + c_bl; + let upto_tl = upto_l + s_l; + max_t = upto_tl + c_tl; + + if (is_near_rounded_corner) { + let radians = atan2(corner_center_to_point.y, + corner_center_to_point.x); + let corner_t = radians * corner_radius; + + if (center_to_point.x >= 0.0) { + if (center_to_point.y < 0.0) { + dash_velocity = corner_dash_velocity_tr; + // Subtracted because radians is pi/2 to 0 when + // going clockwise around the top right corner, + // since the y axis has been flipped + t = upto_r - corner_t * dash_velocity; + } else { + dash_velocity = corner_dash_velocity_br; + // Added because radians is 0 to pi/2 when going + // clockwise around the bottom-right corner + t = upto_br + corner_t * dash_velocity; + } + } else { + if (center_to_point.y >= 0.0) { + dash_velocity = corner_dash_velocity_bl; + // Subtracted because radians is pi/2 to 0 when + // going clockwise around the bottom-left corner, + // since the x axis has been flipped + t = upto_l - corner_t * dash_velocity; + } else { + dash_velocity = corner_dash_velocity_tl; + // Added because radians is 0 to pi/2 when going + // clockwise around the top-left corner, since both + // axis were flipped + t = upto_tl + corner_t * dash_velocity; + } + } + } else { + // Straight borders + let is_horizontal = + corner_center_to_point.x < + corner_center_to_point.y; + if (is_horizontal) { + if (center_to_point.y < 0.0) { + dash_velocity = dv_t; + t = (point.x - r_tl) * dash_velocity; + } else { + dash_velocity = dv_b; + t = upto_bl - (point.x - r_bl) * dash_velocity; + } + } else { + if (center_to_point.x < 0.0) { + dash_velocity = dv_l; + t = upto_tl - (point.y - r_tl) * dash_velocity; + } else { + dash_velocity = dv_r; + t = upto_r + (point.y - r_tr) * dash_velocity; + } + } + } + } + + let dash_length = dash_length_per_width / dash_period_per_width; + let desired_dash_gap = dash_gap_per_width / dash_period_per_width; + + // Straight borders should start and end with a dash, so max_t is + // reduced to cause this. + max_t -= select(0.0, dash_length, unrounded); + if (max_t >= 1.0) { + // Adjust dash gap to evenly divide max_t. + let dash_count = floor(max_t); + let dash_period = max_t / dash_count; + border_color.a *= dash_alpha( + t, + dash_period, + dash_length, + dash_velocity, + antialias_threshold); + } else if (unrounded) { + // When there isn't enough space for the full gap between the + // two start / end dashes of a straight border, reduce gap to + // make them fit. + let dash_gap = max_t - dash_length; + if (dash_gap > 0.0) { + let dash_period = dash_length + dash_gap; + border_color.a *= dash_alpha( + t, + dash_period, + dash_length, + dash_velocity, + antialias_threshold); + } + } + } + + // 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(background_color, border_color); + color = mix(background_color, blended_border, + saturate(antialias_threshold - inner_sdf)); + } + + return blend_color(color, saturate(antialias_threshold - outer_sdf)); +} + +// Returns the dash velocity of a corner given the dash velocity of the two +// sides, by returning the slower velocity (larger dashes). +// +// Since 0 is used for dash velocity when the border width is 0 (instead of +// +inf), this returns the other dash velocity in that case. +// +// An alternative to this might be to appropriately interpolate the dash +// velocity around the corner, but that seems overcomplicated. +fn corner_dash_velocity(dv1: f32, dv2: f32) -> f32 { + if (dv1 == 0.0) { + return dv2; + } else if (dv2 == 0.0) { + return dv1; + } else { + return min(dv1, dv2); + } +} + +// Returns alpha used to render antialiased dashes. +// `t` is within the dash when `fmod(t, period) < length`. +fn dash_alpha(t: f32, period: f32, length: f32, dash_velocity: f32, antialias_threshold: f32) -> f32 { + let half_period = period / 2; + let half_length = length / 2; + // Value in [-half_period, half_period]. + // The dash is in [-half_length, half_length]. + let centered = fmod(t + half_period - half_length, period) - half_period; + // Signed distance for the dash, negative values are inside the dash. + let signed_distance = abs(centered) - half_length; + // Antialiased alpha based on the signed distance. + return saturate(antialias_threshold - signed_distance / dash_velocity); +} + +// This approximates distance to the nearest point to a quarter ellipse in a way +// that is sufficient for anti-aliasing when the ellipse is not very eccentric. +// The components of `point` are expected to be positive. +// +// Negative on the outside and positive on the inside. +fn quarter_ellipse_sdf(point: vec2, radii: vec2) -> f32 { + // Scale the space to treat the ellipse like a unit circle. + let circle_vec = point / radii; + let unit_circle_sdf = length(circle_vec) - 1.0; + // Approximate up-scaling of the length by using the average of the radii. + // + // TODO: A better solution would be to use the gradient of the implicit + // function for an ellipse to approximate a scaling factor. + return unit_circle_sdf * (radii.x + radii.y) * -0.5; +} + +// Modulus that has the same sign as `a`. +fn fmod(a: f32, b: f32) -> f32 { + return a - b * trunc(a / b); +} + +// --- shadows --- // + +struct Shadow { + order: u32, + blur_radius: f32, + bounds: Bounds, + corner_radii: Corners, + content_mask: Bounds, + color: Hsla, +} +var b_shadows: array; + +struct ShadowVarying { + @builtin(position) position: vec4, + @location(0) @interpolate(flat) color: vec4, + @location(1) @interpolate(flat) shadow_id: u32, + //TODO: use `clip_distance` once Naga supports it + @location(3) clip_distances: vec4, +} + +@vertex +fn vs_shadow(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> ShadowVarying { + let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); + var shadow = b_shadows[instance_id]; + + let margin = 3.0 * shadow.blur_radius; + // Set the bounds of the shadow and adjust its size based on the shadow's + // spread radius to achieve the spreading effect + shadow.bounds.origin -= vec2(margin); + shadow.bounds.size += 2.0 * vec2(margin); + + var out = ShadowVarying(); + out.position = to_device_position(unit_vertex, shadow.bounds); + out.color = hsla_to_rgba(shadow.color); + out.shadow_id = instance_id; + out.clip_distances = distance_from_clip_rect(unit_vertex, shadow.bounds, shadow.content_mask); + return out; +} + +@fragment +fn fs_shadow(input: ShadowVarying) -> @location(0) vec4 { + // Alpha clip first, since we don't have `clip_distance`. + if (any(input.clip_distances < vec4(0.0))) { + return vec4(0.0); + } + + let shadow = b_shadows[input.shadow_id]; + let half_size = shadow.bounds.size / 2.0; + let center = shadow.bounds.origin + half_size; + let center_to_point = input.position.xy - center; + + let corner_radius = pick_corner_radius(center_to_point, shadow.corner_radii); + + // The signal is only non-zero in a limited range, so don't waste samples + let low = center_to_point.y - half_size.y; + let high = center_to_point.y + half_size.y; + let start = clamp(-3.0 * shadow.blur_radius, low, high); + let end = clamp(3.0 * shadow.blur_radius, low, high); + + // Accumulate samples (we can get away with surprisingly few samples) + let step = (end - start) / 4.0; + var y = start + step * 0.5; + var alpha = 0.0; + for (var i = 0; i < 4; i += 1) { + let blur = blur_along_x(center_to_point.x, center_to_point.y - y, + shadow.blur_radius, corner_radius, half_size); + alpha += blur * gaussian(y, shadow.blur_radius) * step; + y += step; + } + + return blend_color(input.color, alpha); +} + +// --- path rasterization --- // + +struct PathRasterizationVertex { + xy_position: vec2, + st_position: vec2, + color: Background, + bounds: Bounds, +} + +var b_path_vertices: array; + +struct PathRasterizationVarying { + @builtin(position) position: vec4, + @location(0) st_position: vec2, + @location(1) vertex_id: u32, + //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.vertex_id = vertex_id; + out.clip_distances = distance_from_clip_rect_impl(v.xy_position, v.bounds); + return out; +} + +@fragment +fn fs_path_rasterization(input: PathRasterizationVarying) -> @location(0) vec4 { + let dx = dpdx(input.st_position); + let dy = dpdy(input.st_position); + if (any(input.clip_distances < vec4(0.0))) { + return vec4(0.0); + } + + let v = b_path_vertices[input.vertex_id]; + let background = v.color; + let bounds = v.bounds; + + var alpha: f32; + if (length(vec2(dx.x, dy.x)) < 0.001) { + // If the gradient is too small, return a solid color. + alpha = 1.0; + } else { + let gradient = 2.0 * input.st_position.xx * vec2(dx.x, dy.x) - vec2(dx.y, dy.y); + let f = input.st_position.x * input.st_position.x - input.st_position.y; + let distance = f / length(gradient); + alpha = saturate(0.5 - distance); + } + let gradient_color = prepare_gradient_color( + background.tag, + background.color_space, + background.solid, + background.colors, + ); + let color = gradient_color(background, input.position.xy, bounds, + gradient_color.solid, gradient_color.color0, gradient_color.color1); + return vec4(color.rgb * color.a * alpha, color.a * alpha); +} + +// --- paths --- // + +struct PathSprite { + bounds: Bounds, +} +var b_path_sprites: array; + +struct PathVarying { + @builtin(position) position: vec4, + @location(0) texture_coords: vec2, +} + +@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. + let device_position = to_device_position(unit_vertex, sprite.bounds); + // For screen-space intermediate texture, convert screen position to texture coordinates + let screen_position = sprite.bounds.origin + unit_vertex * sprite.bounds.size; + let texture_coords = screen_position / globals.viewport_size; + + var out = PathVarying(); + out.position = device_position; + out.texture_coords = texture_coords; + + return out; +} + +@fragment +fn fs_path(input: PathVarying) -> @location(0) vec4 { + let sample = textureSample(t_sprite, s_sprite, input.texture_coords); + return sample; +} + +// --- underlines --- // + +struct Underline { + order: u32, + pad: u32, + bounds: Bounds, + content_mask: Bounds, + color: Hsla, + thickness: f32, + wavy: u32, +} +var b_underlines: array; + +struct UnderlineVarying { + @builtin(position) position: vec4, + @location(0) @interpolate(flat) color: vec4, + @location(1) @interpolate(flat) underline_id: u32, + //TODO: use `clip_distance` once Naga supports it + @location(3) clip_distances: vec4, +} + +@vertex +fn vs_underline(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> UnderlineVarying { + let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); + let underline = b_underlines[instance_id]; + + var out = UnderlineVarying(); + out.position = to_device_position(unit_vertex, underline.bounds); + out.color = hsla_to_rgba(underline.color); + out.underline_id = instance_id; + out.clip_distances = distance_from_clip_rect(unit_vertex, underline.bounds, underline.content_mask); + return out; +} + +@fragment +fn fs_underline(input: UnderlineVarying) -> @location(0) vec4 { + const WAVE_FREQUENCY: f32 = 2.0; + const WAVE_HEIGHT_RATIO: f32 = 0.8; + + // 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 blend_color(input.color, input.color.a); + } + + 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 * WAVE_FREQUENCY * underline.thickness / underline.bounds.size.y; + let amplitude = (underline.thickness * WAVE_HEIGHT_RATIO) / underline.bounds.size.y; + + let sine = sin(st.x * frequency) * amplitude; + let dSine = cos(st.x * frequency) * amplitude * frequency; + let distance = (st.y - sine) / sqrt(1.0 + dSine * dSine); + let distance_in_pixels = distance * underline.bounds.size.y; + let distance_from_top_border = distance_in_pixels - half_thickness; + let distance_from_bottom_border = distance_in_pixels + half_thickness; + let alpha = saturate(0.5 - max(-distance_from_bottom_border, distance_from_top_border)); + return blend_color(input.color, alpha * input.color.a); +} + +// --- monochrome sprites --- // + +struct MonochromeSprite { + order: u32, + pad: u32, + bounds: Bounds, + content_mask: Bounds, + color: Hsla, + tile: AtlasTile, + transformation: TransformationMatrix, +} +var b_mono_sprites: array; + +struct MonoSpriteVarying { + @builtin(position) position: vec4, + @location(0) tile_position: vec2, + @location(1) @interpolate(flat) color: vec4, + @location(3) clip_distances: vec4, +} + +@vertex +fn vs_mono_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> MonoSpriteVarying { + let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); + let sprite = b_mono_sprites[instance_id]; + + var out = MonoSpriteVarying(); + out.position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation); + + out.tile_position = to_tile_position(unit_vertex, sprite.tile); + out.color = hsla_to_rgba(sprite.color); + out.clip_distances = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation); + return out; +} + +@fragment +fn fs_mono_sprite(input: MonoSpriteVarying) -> @location(0) vec4 { + let sample = textureSample(t_sprite, s_sprite, input.tile_position).r; + let alpha_corrected = apply_contrast_and_gamma_correction(sample, input.color.rgb, grayscale_enhanced_contrast, gamma_ratios); + + // Alpha clip after using the derivatives. + if (any(input.clip_distances < vec4(0.0))) { + return vec4(0.0); + } + + // convert to srgb space as the rest of the code (output swapchain) expects that + return blend_color(input.color, alpha_corrected); +} + +// --- polychrome sprites --- // + +struct PolychromeSprite { + order: u32, + pad: u32, + grayscale: u32, + opacity: f32, + bounds: Bounds, + content_mask: Bounds, + corner_radii: Corners, + tile: AtlasTile, +} +var b_poly_sprites: array; + +struct PolySpriteVarying { + @builtin(position) position: vec4, + @location(0) tile_position: vec2, + @location(1) @interpolate(flat) sprite_id: u32, + @location(3) clip_distances: vec4, +} + +@vertex +fn vs_poly_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> PolySpriteVarying { + let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); + let sprite = b_poly_sprites[instance_id]; + + var out = PolySpriteVarying(); + out.position = to_device_position(unit_vertex, sprite.bounds); + out.tile_position = to_tile_position(unit_vertex, sprite.tile); + out.sprite_id = instance_id; + out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask); + return out; +} + +@fragment +fn fs_poly_sprite(input: PolySpriteVarying) -> @location(0) vec4 { + let sample = textureSample(t_sprite, s_sprite, input.tile_position); + // Alpha clip after using the derivatives. + if (any(input.clip_distances < vec4(0.0))) { + return vec4(0.0); + } + + let sprite = b_poly_sprites[input.sprite_id]; + let distance = quad_sdf(input.position.xy, sprite.bounds, sprite.corner_radii); + + var color = sample; + if ((sprite.grayscale & 0xFFu) != 0u) { + let grayscale = dot(color.rgb, GRAYSCALE_FACTORS); + color = vec4(vec3(grayscale), sample.a); + } + return blend_color(color, sprite.opacity * saturate(0.5 - distance)); +} + +// --- surfaces --- // + +struct SurfaceParams { + bounds: Bounds, + content_mask: Bounds, +} + +var surface_locals: SurfaceParams; +var t_y: texture_2d; +var t_cb_cr: texture_2d; +var s_surface: sampler; + +const ycbcr_to_RGB = mat4x4( + vec4( 1.0000f, 1.0000f, 1.0000f, 0.0), + vec4( 0.0000f, -0.3441f, 1.7720f, 0.0), + vec4( 1.4020f, -0.7141f, 0.0000f, 0.0), + vec4(-0.7010f, 0.5291f, -0.8860f, 1.0), +); + +struct SurfaceVarying { + @builtin(position) position: vec4, + @location(0) texture_position: vec2, + @location(3) clip_distances: vec4, +} + +@vertex +fn vs_surface(@builtin(vertex_index) vertex_id: u32) -> SurfaceVarying { + let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); + + var out = SurfaceVarying(); + out.position = to_device_position(unit_vertex, surface_locals.bounds); + out.texture_position = unit_vertex; + out.clip_distances = distance_from_clip_rect(unit_vertex, surface_locals.bounds, surface_locals.content_mask); + return out; +} + +@fragment +fn fs_surface(input: SurfaceVarying) -> @location(0) vec4 { + // Alpha clip after using the derivatives. + if (any(input.clip_distances < vec4(0.0))) { + return vec4(0.0); + } + + let y_cb_cr = vec4( + textureSampleLevel(t_y, s_surface, input.texture_position, 0.0).r, + textureSampleLevel(t_cb_cr, s_surface, input.texture_position, 0.0).rg, + 1.0); + + return ycbcr_to_RGB * y_cb_cr; +} diff --git a/third_party/gpui/src/platform/keyboard.rs b/third_party/gpui/src/platform/keyboard.rs new file mode 100644 index 0000000..10b8620 --- /dev/null +++ b/third_party/gpui/src/platform/keyboard.rs @@ -0,0 +1,41 @@ +use collections::HashMap; + +use crate::{KeybindingKeystroke, Keystroke}; + +/// A trait for platform-specific keyboard layouts +pub trait PlatformKeyboardLayout { + /// Get the keyboard layout ID, which should be unique to the layout + fn id(&self) -> &str; + /// Get the keyboard layout display name + fn name(&self) -> &str; +} + +/// A trait for platform-specific keyboard mappings +pub trait PlatformKeyboardMapper { + /// Map a key equivalent to its platform-specific representation + fn map_key_equivalent( + &self, + keystroke: Keystroke, + use_key_equivalents: bool, + ) -> KeybindingKeystroke; + /// Get the key equivalents for the current keyboard layout, + /// only used on macOS + fn get_key_equivalents(&self) -> Option<&HashMap>; +} + +/// A dummy implementation of the platform keyboard mapper +pub struct DummyKeyboardMapper; + +impl PlatformKeyboardMapper for DummyKeyboardMapper { + fn map_key_equivalent( + &self, + keystroke: Keystroke, + _use_key_equivalents: bool, + ) -> KeybindingKeystroke { + KeybindingKeystroke::from_keystroke(keystroke) + } + + fn get_key_equivalents(&self) -> Option<&HashMap> { + None + } +} diff --git a/third_party/gpui/src/platform/keystroke.rs b/third_party/gpui/src/platform/keystroke.rs new file mode 100644 index 0000000..4a2bfc7 --- /dev/null +++ b/third_party/gpui/src/platform/keystroke.rs @@ -0,0 +1,767 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::{ + error::Error, + fmt::{Display, Write}, +}; + +use crate::PlatformKeyboardMapper; + +/// This is a helper trait so that we can simplify the implementation of some functions +pub trait AsKeystroke { + /// Returns the GPUI representation of the keystroke. + fn as_keystroke(&self) -> &Keystroke; +} + +/// A keystroke and associated metadata generated by the platform +#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)] +pub struct Keystroke { + /// the state of the modifier keys at the time the keystroke was generated + pub modifiers: Modifiers, + + /// key is the character printed on the key that was pressed + /// e.g. for option-s, key is "s" + /// On layouts that do not have ascii keys (e.g. Thai) + /// this will be the ASCII-equivalent character (q instead of ๆ), + /// and the typed character will be present in key_char. + pub key: String, + + /// key_char is the character that could have been typed when + /// this binding was pressed. + /// e.g. for s this is "s", for option-s "ß", and cmd-s None + pub key_char: Option, +} + +/// Represents a keystroke that can be used in keybindings and displayed to the user. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeybindingKeystroke { + /// The GPUI representation of the keystroke. + inner: Keystroke, + /// The modifiers to display. + #[cfg(target_os = "windows")] + display_modifiers: Modifiers, + /// The key to display. + #[cfg(target_os = "windows")] + display_key: String, +} + +/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use +/// markdown to display it. +#[derive(Debug)] +pub struct InvalidKeystrokeError { + /// The invalid keystroke. + pub keystroke: String, +} + +impl Error for InvalidKeystrokeError {} + +impl Display for InvalidKeystrokeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Invalid keystroke \"{}\". {}", + self.keystroke, KEYSTROKE_PARSE_EXPECTED_MESSAGE + ) + } +} + +/// Sentence explaining what keystroke parser expects, starting with "Expected ..." +pub const KEYSTROKE_PARSE_EXPECTED_MESSAGE: &str = "Expected a sequence of modifiers \ + (`ctrl`, `alt`, `shift`, `fn`, `cmd`, `super`, or `win`) \ + followed by a key, separated by `-`."; + +impl Keystroke { + /// When matching a key we cannot know whether the user intended to type + /// the key_char or the key itself. On some non-US keyboards keys we use in our + /// bindings are behind option (for example `$` is typed `alt-ç` on a Czech keyboard), + /// and on some keyboards the IME handler converts a sequence of keys into a + /// specific character (for example `"` is typed as `" space` on a brazilian keyboard). + /// + /// This method assumes that `self` was typed and `target' is in the keymap, and checks + /// both possibilities for self against the target. + pub fn should_match(&self, target: &KeybindingKeystroke) -> bool { + #[cfg(not(target_os = "windows"))] + if let Some(key_char) = self + .key_char + .as_ref() + .filter(|key_char| key_char != &&self.key) + { + let ime_modifiers = Modifiers { + control: self.modifiers.control, + platform: self.modifiers.platform, + ..Default::default() + }; + + if &target.inner.key == key_char && target.inner.modifiers == ime_modifiers { + return true; + } + } + + #[cfg(target_os = "windows")] + if let Some(key_char) = self + .key_char + .as_ref() + .filter(|key_char| key_char != &&self.key) + { + // On Windows, if key_char is set, then the typed keystroke produced the key_char + if &target.inner.key == key_char && target.inner.modifiers == Modifiers::none() { + return true; + } + } + + target.inner.modifiers == self.modifiers && target.inner.key == self.key + } + + /// key syntax is: + /// [secondary-][ctrl-][alt-][shift-][cmd-][fn-]key[->key_char] + /// key_char syntax is only used for generating test events, + /// secondary means "cmd" on macOS and "ctrl" on other platforms + /// when matching a key with an key_char set will be matched without it. + pub fn parse(source: &str) -> std::result::Result { + let mut modifiers = Modifiers::none(); + let mut key = None; + let mut key_char = None; + + let mut components = source.split('-').peekable(); + while let Some(component) = components.next() { + if component.eq_ignore_ascii_case("ctrl") { + modifiers.control = true; + continue; + } + if component.eq_ignore_ascii_case("alt") { + modifiers.alt = true; + continue; + } + if component.eq_ignore_ascii_case("shift") { + modifiers.shift = true; + continue; + } + if component.eq_ignore_ascii_case("fn") { + modifiers.function = true; + continue; + } + if component.eq_ignore_ascii_case("secondary") { + if cfg!(target_os = "macos") { + modifiers.platform = true; + } else { + modifiers.control = true; + }; + continue; + } + + let is_platform = component.eq_ignore_ascii_case("cmd") + || component.eq_ignore_ascii_case("super") + || component.eq_ignore_ascii_case("win"); + + if is_platform { + modifiers.platform = true; + continue; + } + + let mut key_str = component.to_string(); + + if let Some(next) = components.peek() { + if next.is_empty() && source.ends_with('-') { + key = Some(String::from("-")); + break; + } else if next.len() > 1 && next.starts_with('>') { + key = Some(key_str); + key_char = Some(String::from(&next[1..])); + components.next(); + } else { + return Err(InvalidKeystrokeError { + keystroke: source.to_owned(), + }); + } + continue; + } + + if component.len() == 1 && component.as_bytes()[0].is_ascii_uppercase() { + // Convert to shift + lowercase char + modifiers.shift = true; + key_str.make_ascii_lowercase(); + } else { + // convert ascii chars to lowercase so that named keys like "tab" and "enter" + // are accepted case insensitively and stored how we expect so they are matched properly + key_str.make_ascii_lowercase() + } + key = Some(key_str); + } + + // Allow for the user to specify a keystroke modifier as the key itself + // This sets the `key` to the modifier, and disables the modifier + key = key.or_else(|| { + use std::mem; + // std::mem::take clears bool incase its true + if mem::take(&mut modifiers.shift) { + Some("shift".to_string()) + } else if mem::take(&mut modifiers.control) { + Some("control".to_string()) + } else if mem::take(&mut modifiers.alt) { + Some("alt".to_string()) + } else if mem::take(&mut modifiers.platform) { + Some("platform".to_string()) + } else if mem::take(&mut modifiers.function) { + Some("function".to_string()) + } else { + None + } + }); + + let key = key.ok_or_else(|| InvalidKeystrokeError { + keystroke: source.to_owned(), + })?; + + Ok(Keystroke { + modifiers, + key, + key_char, + }) + } + + /// Produces a representation of this key that Parse can understand. + pub fn unparse(&self) -> String { + unparse(&self.modifiers, &self.key) + } + + /// Returns true if this keystroke left + /// the ime system in an incomplete state. + pub fn is_ime_in_progress(&self) -> bool { + self.key_char.is_none() + && (is_printable_key(&self.key) || self.key.is_empty()) + && !(self.modifiers.platform + || self.modifiers.control + || self.modifiers.function + || self.modifiers.alt) + } + + /// Returns a new keystroke with the key_char filled. + /// This is used for dispatch_keystroke where we want users to + /// be able to simulate typing "space", etc. + pub fn with_simulated_ime(mut self) -> Self { + if self.key_char.is_none() + && !self.modifiers.platform + && !self.modifiers.control + && !self.modifiers.function + && !self.modifiers.alt + { + self.key_char = match self.key.as_str() { + "space" => Some(" ".into()), + "tab" => Some("\t".into()), + "enter" => Some("\n".into()), + key if !is_printable_key(key) || key.is_empty() => None, + key => { + if self.modifiers.shift { + Some(key.to_uppercase()) + } else { + Some(key.into()) + } + } + } + } + self + } +} + +impl KeybindingKeystroke { + #[cfg(target_os = "windows")] + pub(crate) fn new(inner: Keystroke, display_modifiers: Modifiers, display_key: String) -> Self { + KeybindingKeystroke { + inner, + display_modifiers, + display_key, + } + } + + /// Create a new keybinding keystroke from the given keystroke using the given keyboard mapper. + pub fn new_with_mapper( + inner: Keystroke, + use_key_equivalents: bool, + keyboard_mapper: &dyn PlatformKeyboardMapper, + ) -> Self { + keyboard_mapper.map_key_equivalent(inner, use_key_equivalents) + } + + /// Create a new keybinding keystroke from the given keystroke, without any platform-specific mapping. + pub fn from_keystroke(keystroke: Keystroke) -> Self { + #[cfg(target_os = "windows")] + { + let key = keystroke.key.clone(); + let modifiers = keystroke.modifiers; + KeybindingKeystroke { + inner: keystroke, + display_modifiers: modifiers, + display_key: key, + } + } + #[cfg(not(target_os = "windows"))] + { + KeybindingKeystroke { inner: keystroke } + } + } + + /// Returns the GPUI representation of the keystroke. + pub fn inner(&self) -> &Keystroke { + &self.inner + } + + /// Returns the modifiers. + /// + /// Platform-specific behavior: + /// - On macOS and Linux, this modifiers is the same as `inner.modifiers`, which is the GPUI representation of the keystroke. + /// - On Windows, this modifiers is the display modifiers, for example, a `ctrl-@` keystroke will have `inner.modifiers` as + /// `Modifiers::control()` and `display_modifiers` as `Modifiers::control_shift()`. + pub fn modifiers(&self) -> &Modifiers { + #[cfg(target_os = "windows")] + { + &self.display_modifiers + } + #[cfg(not(target_os = "windows"))] + { + &self.inner.modifiers + } + } + + /// Returns the key. + /// + /// Platform-specific behavior: + /// - On macOS and Linux, this key is the same as `inner.key`, which is the GPUI representation of the keystroke. + /// - On Windows, this key is the display key, for example, a `ctrl-@` keystroke will have `inner.key` as `@` and `display_key` as `2`. + pub fn key(&self) -> &str { + #[cfg(target_os = "windows")] + { + &self.display_key + } + #[cfg(not(target_os = "windows"))] + { + &self.inner.key + } + } + + /// Sets the modifiers. On Windows this modifies both `inner.modifiers` and `display_modifiers`. + pub fn set_modifiers(&mut self, modifiers: Modifiers) { + self.inner.modifiers = modifiers; + #[cfg(target_os = "windows")] + { + self.display_modifiers = modifiers; + } + } + + /// Sets the key. On Windows this modifies both `inner.key` and `display_key`. + pub fn set_key(&mut self, key: String) { + #[cfg(target_os = "windows")] + { + self.display_key = key.clone(); + } + self.inner.key = key; + } + + /// Produces a representation of this key that Parse can understand. + pub fn unparse(&self) -> String { + #[cfg(target_os = "windows")] + { + unparse(&self.display_modifiers, &self.display_key) + } + #[cfg(not(target_os = "windows"))] + { + unparse(&self.inner.modifiers, &self.inner.key) + } + } + + /// Removes the key_char + pub fn remove_key_char(&mut self) { + self.inner.key_char = None; + } +} + +fn is_printable_key(key: &str) -> bool { + !matches!( + key, + "f1" | "f2" + | "f3" + | "f4" + | "f5" + | "f6" + | "f7" + | "f8" + | "f9" + | "f10" + | "f11" + | "f12" + | "f13" + | "f14" + | "f15" + | "f16" + | "f17" + | "f18" + | "f19" + | "f20" + | "f21" + | "f22" + | "f23" + | "f24" + | "f25" + | "f26" + | "f27" + | "f28" + | "f29" + | "f30" + | "f31" + | "f32" + | "f33" + | "f34" + | "f35" + | "backspace" + | "delete" + | "left" + | "right" + | "up" + | "down" + | "pageup" + | "pagedown" + | "insert" + | "home" + | "end" + | "back" + | "forward" + | "escape" + ) +} + +impl std::fmt::Display for Keystroke { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + display_modifiers(&self.modifiers, f)?; + display_key(&self.key, f) + } +} + +impl std::fmt::Display for KeybindingKeystroke { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + display_modifiers(self.modifiers(), f)?; + display_key(self.key(), f) + } +} + +/// The state of the modifier keys at some point in time +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)] +pub struct Modifiers { + /// The control key + #[serde(default)] + pub control: bool, + + /// The alt key + /// Sometimes also known as the 'meta' key + #[serde(default)] + pub alt: bool, + + /// The shift key + #[serde(default)] + pub shift: bool, + + /// The command key, on macos + /// the windows key, on windows + /// the super key, on linux + #[serde(default)] + pub platform: bool, + + /// The function key + #[serde(default)] + pub function: bool, +} + +impl Modifiers { + /// Returns whether any modifier key is pressed. + pub fn modified(&self) -> bool { + self.control || self.alt || self.shift || self.platform || self.function + } + + /// Whether the semantically 'secondary' modifier key is pressed. + /// + /// On macOS, this is the command key. + /// On Linux and Windows, this is the control key. + pub fn secondary(&self) -> bool { + #[cfg(target_os = "macos")] + { + self.platform + } + + #[cfg(not(target_os = "macos"))] + { + self.control + } + } + + /// Returns how many modifier keys are pressed. + pub fn number_of_modifiers(&self) -> u8 { + self.control as u8 + + self.alt as u8 + + self.shift as u8 + + self.platform as u8 + + self.function as u8 + } + + /// Returns [`Modifiers`] with no modifiers. + pub fn none() -> Modifiers { + Default::default() + } + + /// Returns [`Modifiers`] with just the command key. + pub fn command() -> Modifiers { + Modifiers { + platform: true, + ..Default::default() + } + } + + /// A Returns [`Modifiers`] with just the secondary key pressed. + pub fn secondary_key() -> Modifiers { + #[cfg(target_os = "macos")] + { + Modifiers { + platform: true, + ..Default::default() + } + } + + #[cfg(not(target_os = "macos"))] + { + Modifiers { + control: true, + ..Default::default() + } + } + } + + /// Returns [`Modifiers`] with just the windows key. + pub fn windows() -> Modifiers { + Modifiers { + platform: true, + ..Default::default() + } + } + + /// Returns [`Modifiers`] with just the super key. + pub fn super_key() -> Modifiers { + Modifiers { + platform: true, + ..Default::default() + } + } + + /// Returns [`Modifiers`] with just control. + pub fn control() -> Modifiers { + Modifiers { + control: true, + ..Default::default() + } + } + + /// Returns [`Modifiers`] with just alt. + pub fn alt() -> Modifiers { + Modifiers { + alt: true, + ..Default::default() + } + } + + /// Returns [`Modifiers`] with just shift. + pub fn shift() -> Modifiers { + Modifiers { + shift: true, + ..Default::default() + } + } + + /// Returns [`Modifiers`] with command + shift. + pub fn command_shift() -> Modifiers { + Modifiers { + shift: true, + platform: true, + ..Default::default() + } + } + + /// Returns [`Modifiers`] with command + shift. + pub fn control_shift() -> Modifiers { + Modifiers { + shift: true, + control: true, + ..Default::default() + } + } + + /// Checks if this [`Modifiers`] is a subset of another [`Modifiers`]. + pub fn is_subset_of(&self, other: &Modifiers) -> bool { + (*other & *self) == *self + } +} + +impl std::ops::BitOr for Modifiers { + type Output = Self; + + fn bitor(mut self, other: Self) -> Self::Output { + self |= other; + self + } +} + +impl std::ops::BitOrAssign for Modifiers { + fn bitor_assign(&mut self, other: Self) { + self.control |= other.control; + self.alt |= other.alt; + self.shift |= other.shift; + self.platform |= other.platform; + self.function |= other.function; + } +} + +impl std::ops::BitXor for Modifiers { + type Output = Self; + fn bitxor(mut self, rhs: Self) -> Self::Output { + self ^= rhs; + self + } +} + +impl std::ops::BitXorAssign for Modifiers { + fn bitxor_assign(&mut self, other: Self) { + self.control ^= other.control; + self.alt ^= other.alt; + self.shift ^= other.shift; + self.platform ^= other.platform; + self.function ^= other.function; + } +} + +impl std::ops::BitAnd for Modifiers { + type Output = Self; + fn bitand(mut self, rhs: Self) -> Self::Output { + self &= rhs; + self + } +} + +impl std::ops::BitAndAssign for Modifiers { + fn bitand_assign(&mut self, other: Self) { + self.control &= other.control; + self.alt &= other.alt; + self.shift &= other.shift; + self.platform &= other.platform; + self.function &= other.function; + } +} + +/// The state of the capslock key at some point in time +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)] +pub struct Capslock { + /// The capslock key is on + #[serde(default)] + pub on: bool, +} + +impl AsKeystroke for Keystroke { + fn as_keystroke(&self) -> &Keystroke { + self + } +} + +impl AsKeystroke for KeybindingKeystroke { + fn as_keystroke(&self) -> &Keystroke { + &self.inner + } +} + +fn display_modifiers(modifiers: &Modifiers, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if modifiers.control { + #[cfg(target_os = "macos")] + f.write_char('^')?; + + #[cfg(not(target_os = "macos"))] + write!(f, "ctrl-")?; + } + if modifiers.alt { + #[cfg(target_os = "macos")] + f.write_char('⌥')?; + + #[cfg(not(target_os = "macos"))] + write!(f, "alt-")?; + } + if modifiers.platform { + #[cfg(target_os = "macos")] + f.write_char('⌘')?; + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + f.write_char('❖')?; + + #[cfg(target_os = "windows")] + f.write_char('⊞')?; + } + if modifiers.shift { + #[cfg(target_os = "macos")] + f.write_char('⇧')?; + + #[cfg(not(target_os = "macos"))] + write!(f, "shift-")?; + } + Ok(()) +} + +fn display_key(key: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let key = match key { + #[cfg(target_os = "macos")] + "backspace" => '⌫', + #[cfg(target_os = "macos")] + "up" => '↑', + #[cfg(target_os = "macos")] + "down" => '↓', + #[cfg(target_os = "macos")] + "left" => '←', + #[cfg(target_os = "macos")] + "right" => '→', + #[cfg(target_os = "macos")] + "tab" => '⇥', + #[cfg(target_os = "macos")] + "escape" => '⎋', + #[cfg(target_os = "macos")] + "shift" => '⇧', + #[cfg(target_os = "macos")] + "control" => '⌃', + #[cfg(target_os = "macos")] + "alt" => '⌥', + #[cfg(target_os = "macos")] + "platform" => '⌘', + + key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(), + key => return f.write_str(key), + }; + f.write_char(key) +} + +#[inline] +fn unparse(modifiers: &Modifiers, key: &str) -> String { + let mut result = String::new(); + if modifiers.function { + result.push_str("fn-"); + } + if modifiers.control { + result.push_str("ctrl-"); + } + if modifiers.alt { + result.push_str("alt-"); + } + if modifiers.platform { + #[cfg(target_os = "macos")] + result.push_str("cmd-"); + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + result.push_str("super-"); + + #[cfg(target_os = "windows")] + result.push_str("win-"); + } + if modifiers.shift { + result.push_str("shift-"); + } + result.push_str(&key); + result +} diff --git a/third_party/gpui/src/platform/linux.rs b/third_party/gpui/src/platform/linux.rs new file mode 100644 index 0000000..5221f71 --- /dev/null +++ b/third_party/gpui/src/platform/linux.rs @@ -0,0 +1,29 @@ +mod dispatcher; +mod headless; +mod keyboard; +mod platform; +#[cfg(any(feature = "wayland", feature = "x11"))] +mod text_system; +#[cfg(feature = "wayland")] +mod wayland; +#[cfg(feature = "x11")] +mod x11; + +#[cfg(any(feature = "wayland", feature = "x11"))] +mod xdg_desktop_portal; + +pub(crate) use dispatcher::*; +pub(crate) use headless::*; +pub(crate) use keyboard::*; +pub(crate) use platform::*; +#[cfg(any(feature = "wayland", feature = "x11"))] +pub(crate) use text_system::*; +#[cfg(feature = "wayland")] +pub(crate) use wayland::*; +#[cfg(feature = "x11")] +pub(crate) use x11::*; + +#[cfg(all(feature = "screen-capture", any(feature = "wayland", feature = "x11")))] +pub(crate) type PlatformScreenCaptureFrame = scap::frame::Frame; +#[cfg(not(all(feature = "screen-capture", any(feature = "wayland", feature = "x11"))))] +pub(crate) type PlatformScreenCaptureFrame = (); diff --git a/third_party/gpui/src/platform/linux/dispatcher.rs b/third_party/gpui/src/platform/linux/dispatcher.rs new file mode 100644 index 0000000..9ca1f76 --- /dev/null +++ b/third_party/gpui/src/platform/linux/dispatcher.rs @@ -0,0 +1,129 @@ +use crate::{PlatformDispatcher, TaskLabel}; +use async_task::Runnable; +use calloop::{ + EventLoop, + channel::{self, Sender}, + timer::TimeoutAction, +}; +use std::{ + thread, + time::{Duration, Instant}, +}; +use util::ResultExt; + +struct TimerAfter { + duration: Duration, + runnable: Runnable, +} + +pub(crate) struct LinuxDispatcher { + main_sender: Sender, + timer_sender: Sender, + background_sender: flume::Sender, + _background_threads: Vec>, + main_thread_id: thread::ThreadId, +} + +impl LinuxDispatcher { + pub fn new(main_sender: Sender) -> Self { + let (background_sender, background_receiver) = flume::unbounded::(); + let thread_count = std::thread::available_parallelism() + .map(|i| i.get()) + .unwrap_or(1); + + let mut background_threads = (0..thread_count) + .map(|i| { + let receiver = background_receiver.clone(); + std::thread::Builder::new() + .name(format!("Worker-{i}")) + .spawn(move || { + for runnable in receiver { + let start = Instant::now(); + + runnable.run(); + + log::trace!( + "background thread {}: ran runnable. took: {:?}", + i, + start.elapsed() + ); + } + }) + .unwrap() + }) + .collect::>(); + + let (timer_sender, timer_channel) = calloop::channel::channel::(); + let timer_thread = std::thread::Builder::new() + .name("Timer".to_owned()) + .spawn(|| { + let mut event_loop: EventLoop<()> = + EventLoop::try_new().expect("Failed to initialize timer loop!"); + + let handle = event_loop.handle(); + let timer_handle = event_loop.handle(); + handle + .insert_source(timer_channel, move |e, _, _| { + if let channel::Event::Msg(timer) = e { + // This has to be in an option to satisfy the borrow checker. The callback below should only be scheduled once. + let mut runnable = Some(timer.runnable); + timer_handle + .insert_source( + calloop::timer::Timer::from_duration(timer.duration), + move |_, _, _| { + if let Some(runnable) = runnable.take() { + runnable.run(); + } + TimeoutAction::Drop + }, + ) + .expect("Failed to start timer"); + } + }) + .expect("Failed to start timer thread"); + + event_loop.run(None, &mut (), |_| {}).log_err(); + }) + .unwrap(); + + background_threads.push(timer_thread); + + Self { + main_sender, + timer_sender, + background_sender, + _background_threads: background_threads, + main_thread_id: thread::current().id(), + } + } +} + +impl PlatformDispatcher for LinuxDispatcher { + fn is_main_thread(&self) -> bool { + thread::current().id() == self.main_thread_id + } + + fn dispatch(&self, runnable: Runnable, _: Option) { + self.background_sender.send(runnable).unwrap(); + } + + fn dispatch_on_main_thread(&self, runnable: Runnable) { + self.main_sender.send(runnable).unwrap_or_else(|runnable| { + // NOTE: Runnable may wrap a Future that is !Send. + // + // This is usually safe because we only poll it on the main thread. + // However if the send fails, we know that: + // 1. main_receiver has been dropped (which implies the app is shutting down) + // 2. we are on a background thread. + // It is not safe to drop something !Send on the wrong thread, and + // the app will exit soon anyway, so we must forget the runnable. + std::mem::forget(runnable); + }); + } + + fn dispatch_after(&self, duration: Duration, runnable: Runnable) { + self.timer_sender + .send(TimerAfter { duration, runnable }) + .ok(); + } +} diff --git a/third_party/gpui/src/platform/linux/headless.rs b/third_party/gpui/src/platform/linux/headless.rs new file mode 100644 index 0000000..2237aeb --- /dev/null +++ b/third_party/gpui/src/platform/linux/headless.rs @@ -0,0 +1,3 @@ +mod client; + +pub(crate) use client::*; diff --git a/third_party/gpui/src/platform/linux/headless/client.rs b/third_party/gpui/src/platform/linux/headless/client.rs new file mode 100644 index 0000000..da54db3 --- /dev/null +++ b/third_party/gpui/src/platform/linux/headless/client.rs @@ -0,0 +1,134 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use calloop::{EventLoop, LoopHandle}; +use util::ResultExt; + +use crate::platform::linux::LinuxClient; +use crate::platform::{LinuxCommon, PlatformWindow}; +use crate::{ + AnyWindowHandle, CursorStyle, DisplayId, LinuxKeyboardLayout, PlatformDisplay, + PlatformKeyboardLayout, WindowParams, +}; + +pub struct HeadlessClientState { + pub(crate) _loop_handle: LoopHandle<'static, HeadlessClient>, + pub(crate) event_loop: Option>, + pub(crate) common: LinuxCommon, +} + +#[derive(Clone)] +pub(crate) struct HeadlessClient(Rc>); + +impl HeadlessClient { + pub(crate) fn new() -> Self { + let event_loop = EventLoop::try_new().unwrap(); + + let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal()); + + let handle = event_loop.handle(); + + handle + .insert_source(main_receiver, |event, _, _: &mut HeadlessClient| { + if let calloop::channel::Event::Msg(runnable) = event { + runnable.run(); + } + }) + .ok(); + + HeadlessClient(Rc::new(RefCell::new(HeadlessClientState { + event_loop: Some(event_loop), + _loop_handle: handle, + common, + }))) + } +} + +impl LinuxClient for HeadlessClient { + fn with_common(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R { + f(&mut self.0.borrow_mut().common) + } + + fn keyboard_layout(&self) -> Box { + Box::new(LinuxKeyboardLayout::new("unknown".into())) + } + + fn displays(&self) -> Vec> { + vec![] + } + + fn primary_display(&self) -> Option> { + None + } + + fn display(&self, _id: DisplayId) -> Option> { + None + } + + #[cfg(feature = "screen-capture")] + fn is_screen_capture_supported(&self) -> bool { + false + } + + #[cfg(feature = "screen-capture")] + fn screen_capture_sources( + &self, + ) -> futures::channel::oneshot::Receiver>>> + { + let (mut tx, rx) = futures::channel::oneshot::channel(); + tx.send(Err(anyhow::anyhow!( + "Headless mode does not support screen capture." + ))) + .ok(); + rx + } + + fn active_window(&self) -> Option { + None + } + + fn window_stack(&self) -> Option> { + None + } + + fn open_window( + &self, + _handle: AnyWindowHandle, + _params: WindowParams, + ) -> anyhow::Result> { + anyhow::bail!("neither DISPLAY nor WAYLAND_DISPLAY is set. You can run in headless mode"); + } + + fn compositor_name(&self) -> &'static str { + "headless" + } + + fn set_cursor_style(&self, _style: CursorStyle) {} + + fn open_uri(&self, _uri: &str) {} + + fn reveal_path(&self, _path: std::path::PathBuf) {} + + fn write_to_primary(&self, _item: crate::ClipboardItem) {} + + fn write_to_clipboard(&self, _item: crate::ClipboardItem) {} + + fn read_from_primary(&self) -> Option { + None + } + + fn read_from_clipboard(&self) -> Option { + None + } + + fn run(&self) { + let mut event_loop = self + .0 + .borrow_mut() + .event_loop + .take() + .expect("App is already running"); + + event_loop.run(None, &mut self.clone(), |_| {}).log_err(); + } +} diff --git a/third_party/gpui/src/platform/linux/keyboard.rs b/third_party/gpui/src/platform/linux/keyboard.rs new file mode 100644 index 0000000..4e83cc4 --- /dev/null +++ b/third_party/gpui/src/platform/linux/keyboard.rs @@ -0,0 +1,22 @@ +use crate::{PlatformKeyboardLayout, SharedString}; + +#[derive(Clone)] +pub(crate) struct LinuxKeyboardLayout { + name: SharedString, +} + +impl PlatformKeyboardLayout for LinuxKeyboardLayout { + fn id(&self) -> &str { + &self.name + } + + fn name(&self) -> &str { + &self.name + } +} + +impl LinuxKeyboardLayout { + pub(crate) fn new(name: SharedString) -> Self { + Self { name } + } +} diff --git a/third_party/gpui/src/platform/linux/platform.rs b/third_party/gpui/src/platform/linux/platform.rs new file mode 100644 index 0000000..322f5d7 --- /dev/null +++ b/third_party/gpui/src/platform/linux/platform.rs @@ -0,0 +1,1039 @@ +use std::{ + env, + path::{Path, PathBuf}, + process::Command, + rc::Rc, + sync::Arc, +}; +#[cfg(any(feature = "wayland", feature = "x11"))] +use std::{ + ffi::OsString, + fs::File, + io::Read as _, + os::fd::{AsFd, AsRawFd, FromRawFd}, + time::Duration, +}; + +use anyhow::{Context as _, anyhow}; +use async_task::Runnable; +use calloop::{LoopSignal, channel::Channel}; +use futures::channel::oneshot; +use util::ResultExt as _; +#[cfg(any(feature = "wayland", feature = "x11"))] +use xkbcommon::xkb::{self, Keycode, Keysym, State}; + +use crate::{ + Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, + ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions, + Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, + PlatformTextSystem, PlatformWindow, Point, Result, Task, WindowAppearance, WindowParams, px, +}; + +#[cfg(any(feature = "wayland", feature = "x11"))] +pub(crate) const SCROLL_LINES: f32 = 3.0; + +// Values match the defaults on GTK. +// Taken from https://github.com/GNOME/gtk/blob/main/gtk/gtksettings.c#L320 +#[cfg(any(feature = "wayland", feature = "x11"))] +pub(crate) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400); +pub(crate) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0); +pub(crate) const KEYRING_LABEL: &str = "zed-github-account"; + +#[cfg(any(feature = "wayland", feature = "x11"))] +const FILE_PICKER_PORTAL_MISSING: &str = + "Couldn't open file picker due to missing xdg-desktop-portal implementation."; + +pub trait LinuxClient { + fn compositor_name(&self) -> &'static str; + fn with_common(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R; + fn keyboard_layout(&self) -> Box; + fn displays(&self) -> Vec>; + #[allow(unused)] + fn display(&self, id: DisplayId) -> Option>; + fn primary_display(&self) -> Option>; + #[cfg(feature = "screen-capture")] + fn is_screen_capture_supported(&self) -> bool; + #[cfg(feature = "screen-capture")] + fn screen_capture_sources( + &self, + ) -> oneshot::Receiver>>>; + + fn open_window( + &self, + handle: AnyWindowHandle, + options: WindowParams, + ) -> anyhow::Result>; + fn set_cursor_style(&self, style: CursorStyle); + fn open_uri(&self, uri: &str); + fn reveal_path(&self, path: PathBuf); + fn write_to_primary(&self, item: ClipboardItem); + fn write_to_clipboard(&self, item: ClipboardItem); + fn read_from_primary(&self) -> Option; + fn read_from_clipboard(&self) -> Option; + fn active_window(&self) -> Option; + fn window_stack(&self) -> Option>; + fn run(&self); + + #[cfg(any(feature = "wayland", feature = "x11"))] + fn window_identifier( + &self, + ) -> impl Future> + Send + 'static { + std::future::ready::>(None) + } +} + +#[derive(Default)] +pub(crate) struct PlatformHandlers { + pub(crate) open_urls: Option)>>, + pub(crate) quit: Option>, + pub(crate) reopen: Option>, + pub(crate) app_menu_action: Option>, + pub(crate) will_open_app_menu: Option>, + pub(crate) validate_app_menu_command: Option bool>>, + pub(crate) keyboard_layout_change: Option>, +} + +pub(crate) struct LinuxCommon { + pub(crate) background_executor: BackgroundExecutor, + pub(crate) foreground_executor: ForegroundExecutor, + pub(crate) text_system: Arc, + pub(crate) appearance: WindowAppearance, + pub(crate) auto_hide_scrollbars: bool, + pub(crate) callbacks: PlatformHandlers, + pub(crate) signal: LoopSignal, + pub(crate) menus: Vec, +} + +impl LinuxCommon { + pub fn new(signal: LoopSignal) -> (Self, Channel) { + let (main_sender, main_receiver) = calloop::channel::channel::(); + + #[cfg(any(feature = "wayland", feature = "x11"))] + let text_system = Arc::new(crate::CosmicTextSystem::new()); + #[cfg(not(any(feature = "wayland", feature = "x11")))] + let text_system = Arc::new(crate::NoopTextSystem::new()); + + let callbacks = PlatformHandlers::default(); + + let dispatcher = Arc::new(LinuxDispatcher::new(main_sender)); + + let background_executor = BackgroundExecutor::new(dispatcher.clone()); + + let common = LinuxCommon { + background_executor, + foreground_executor: ForegroundExecutor::new(dispatcher), + text_system, + appearance: WindowAppearance::Light, + auto_hide_scrollbars: false, + callbacks, + signal, + menus: Vec::new(), + }; + + (common, main_receiver) + } +} + +impl Platform for P { + fn background_executor(&self) -> BackgroundExecutor { + self.with_common(|common| common.background_executor.clone()) + } + + fn foreground_executor(&self) -> ForegroundExecutor { + self.with_common(|common| common.foreground_executor.clone()) + } + + fn text_system(&self) -> Arc { + self.with_common(|common| common.text_system.clone()) + } + + fn keyboard_layout(&self) -> Box { + self.keyboard_layout() + } + + fn keyboard_mapper(&self) -> Rc { + Rc::new(crate::DummyKeyboardMapper) + } + + fn on_keyboard_layout_change(&self, callback: Box) { + self.with_common(|common| common.callbacks.keyboard_layout_change = Some(callback)); + } + + fn run(&self, on_finish_launching: Box) { + on_finish_launching(); + + LinuxClient::run(self); + + let quit = self.with_common(|common| common.callbacks.quit.take()); + if let Some(mut fun) = quit { + fun(); + } + } + + fn quit(&self) { + self.with_common(|common| common.signal.stop()); + } + + fn compositor_name(&self) -> &'static str { + self.compositor_name() + } + + fn restart(&self, binary_path: Option) { + use std::os::unix::process::CommandExt as _; + + // get the process id of the current process + let app_pid = std::process::id().to_string(); + // get the path to the executable + let app_path = if let Some(path) = binary_path { + path + } else { + match self.app_path() { + Ok(path) => path, + Err(err) => { + log::error!("Failed to get app path: {:?}", err); + return; + } + } + }; + + log::info!("Restarting process, using app path: {:?}", app_path); + + // Script to wait for the current process to exit and then restart the app. + let script = format!( + r#" + while kill -0 {pid} 2>/dev/null; do + sleep 0.1 + done + + {app_path} + "#, + pid = app_pid, + app_path = app_path.display() + ); + + #[allow( + clippy::disallowed_methods, + reason = "We are restarting ourselves, using std command thus is fine" + )] + let restart_process = Command::new("/usr/bin/env") + .arg("bash") + .arg("-c") + .arg(script) + .process_group(0) + .spawn(); + + match restart_process { + Ok(_) => self.quit(), + Err(e) => log::error!("failed to spawn restart script: {:?}", e), + } + } + + fn activate(&self, _ignoring_other_apps: bool) { + log::info!("activate is not implemented on Linux, ignoring the call") + } + + fn hide(&self) { + log::info!("hide is not implemented on Linux, ignoring the call") + } + + fn hide_other_apps(&self) { + log::info!("hide_other_apps is not implemented on Linux, ignoring the call") + } + + fn unhide_other_apps(&self) { + log::info!("unhide_other_apps is not implemented on Linux, ignoring the call") + } + + fn primary_display(&self) -> Option> { + self.primary_display() + } + + fn displays(&self) -> Vec> { + self.displays() + } + + #[cfg(feature = "screen-capture")] + fn is_screen_capture_supported(&self) -> bool { + self.is_screen_capture_supported() + } + + #[cfg(feature = "screen-capture")] + fn screen_capture_sources( + &self, + ) -> oneshot::Receiver>>> { + self.screen_capture_sources() + } + + fn active_window(&self) -> Option { + self.active_window() + } + + fn window_stack(&self) -> Option> { + self.window_stack() + } + + fn open_window( + &self, + handle: AnyWindowHandle, + options: WindowParams, + ) -> anyhow::Result> { + self.open_window(handle, options) + } + + fn open_url(&self, url: &str) { + self.open_uri(url); + } + + fn on_open_urls(&self, callback: Box)>) { + self.with_common(|common| common.callbacks.open_urls = Some(callback)); + } + + fn prompt_for_paths( + &self, + options: PathPromptOptions, + ) -> oneshot::Receiver>>> { + let (done_tx, done_rx) = oneshot::channel(); + + #[cfg(not(any(feature = "wayland", feature = "x11")))] + let _ = (done_tx.send(Ok(None)), options); + + #[cfg(any(feature = "wayland", feature = "x11"))] + let identifier = self.window_identifier(); + + #[cfg(any(feature = "wayland", feature = "x11"))] + self.foreground_executor() + .spawn(async move { + let title = if options.directories { + "Open Folder" + } else { + "Open File" + }; + + let request = match ashpd::desktop::file_chooser::OpenFileRequest::default() + .identifier(identifier.await) + .modal(true) + .title(title) + .accept_label(options.prompt.as_ref().map(crate::SharedString::as_str)) + .multiple(options.multiple) + .directory(options.directories) + .send() + .await + { + Ok(request) => request, + Err(err) => { + let result = match err { + ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING), + err => err.into(), + }; + let _ = done_tx.send(Err(result)); + return; + } + }; + + let result = match request.response() { + Ok(response) => Ok(Some( + response + .uris() + .iter() + .filter_map(|uri| uri.to_file_path().ok()) + .collect::>(), + )), + Err(ashpd::Error::Response(_)) => Ok(None), + Err(e) => Err(e.into()), + }; + let _ = done_tx.send(result); + }) + .detach(); + done_rx + } + + fn prompt_for_new_path( + &self, + directory: &Path, + suggested_name: Option<&str>, + ) -> oneshot::Receiver>> { + let (done_tx, done_rx) = oneshot::channel(); + + #[cfg(not(any(feature = "wayland", feature = "x11")))] + let _ = (done_tx.send(Ok(None)), directory, suggested_name); + + #[cfg(any(feature = "wayland", feature = "x11"))] + let identifier = self.window_identifier(); + + #[cfg(any(feature = "wayland", feature = "x11"))] + self.foreground_executor() + .spawn({ + let directory = directory.to_owned(); + let suggested_name = suggested_name.map(|s| s.to_owned()); + + async move { + let mut request_builder = + ashpd::desktop::file_chooser::SaveFileRequest::default() + .identifier(identifier.await) + .modal(true) + .title("Save File") + .current_folder(directory) + .expect("pathbuf should not be nul terminated"); + + if let Some(suggested_name) = suggested_name { + request_builder = request_builder.current_name(suggested_name.as_str()); + } + + let request = match request_builder.send().await { + Ok(request) => request, + Err(err) => { + let result = match err { + ashpd::Error::PortalNotFound(_) => { + anyhow!(FILE_PICKER_PORTAL_MISSING) + } + err => err.into(), + }; + let _ = done_tx.send(Err(result)); + return; + } + }; + + let result = match request.response() { + Ok(response) => Ok(response + .uris() + .first() + .and_then(|uri| uri.to_file_path().ok())), + Err(ashpd::Error::Response(_)) => Ok(None), + Err(e) => Err(e.into()), + }; + let _ = done_tx.send(result); + } + }) + .detach(); + + done_rx + } + + fn can_select_mixed_files_and_dirs(&self) -> bool { + // org.freedesktop.portal.FileChooser only supports "pick files" and "pick directories". + false + } + + fn reveal_path(&self, path: &Path) { + self.reveal_path(path.to_owned()); + } + + fn open_with_system(&self, path: &Path) { + let path = path.to_owned(); + self.background_executor() + .spawn(async move { + let _ = smol::process::Command::new("xdg-open") + .arg(path) + .spawn() + .context("invoking xdg-open") + .log_err()? + .status() + .await + .log_err()?; + Some(()) + }) + .detach(); + } + + fn on_quit(&self, callback: Box) { + self.with_common(|common| { + common.callbacks.quit = Some(callback); + }); + } + + fn on_reopen(&self, callback: Box) { + self.with_common(|common| { + common.callbacks.reopen = Some(callback); + }); + } + + fn on_app_menu_action(&self, callback: Box) { + self.with_common(|common| { + common.callbacks.app_menu_action = Some(callback); + }); + } + + fn on_will_open_app_menu(&self, callback: Box) { + self.with_common(|common| { + common.callbacks.will_open_app_menu = Some(callback); + }); + } + + fn on_validate_app_menu_command(&self, callback: Box bool>) { + self.with_common(|common| { + common.callbacks.validate_app_menu_command = Some(callback); + }); + } + + fn app_path(&self) -> Result { + // get the path of the executable of the current process + let app_path = env::current_exe()?; + Ok(app_path) + } + + fn set_menus(&self, menus: Vec, _keymap: &Keymap) { + self.with_common(|common| { + common.menus = menus.into_iter().map(|menu| menu.owned()).collect(); + }) + } + + fn get_menus(&self) -> Option> { + self.with_common(|common| Some(common.menus.clone())) + } + + fn set_dock_menu(&self, _menu: Vec, _keymap: &Keymap) { + // todo(linux) + } + + fn path_for_auxiliary_executable(&self, _name: &str) -> Result { + Err(anyhow::Error::msg( + "Platform::path_for_auxiliary_executable is not implemented yet", + )) + } + + fn set_cursor_style(&self, style: CursorStyle) { + self.set_cursor_style(style) + } + + fn should_auto_hide_scrollbars(&self) -> bool { + self.with_common(|common| common.auto_hide_scrollbars) + } + + fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task> { + let url = url.to_string(); + let username = username.to_string(); + let password = password.to_vec(); + self.background_executor().spawn(async move { + let keyring = oo7::Keyring::new().await?; + keyring.unlock().await?; + keyring + .create_item( + KEYRING_LABEL, + &vec![("url", &url), ("username", &username)], + password, + true, + ) + .await?; + Ok(()) + }) + } + + fn read_credentials(&self, url: &str) -> Task)>>> { + let url = url.to_string(); + self.background_executor().spawn(async move { + let keyring = oo7::Keyring::new().await?; + keyring.unlock().await?; + + let items = keyring.search_items(&vec![("url", &url)]).await?; + + for item in items.into_iter() { + if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) { + let attributes = item.attributes().await?; + let username = attributes + .get("username") + .context("Cannot find username in stored credentials")?; + item.unlock().await?; + let secret = item.secret().await?; + + // we lose the zeroizing capabilities at this boundary, + // a current limitation GPUI's credentials api + return Ok(Some((username.to_string(), secret.to_vec()))); + } else { + continue; + } + } + Ok(None) + }) + } + + fn delete_credentials(&self, url: &str) -> Task> { + let url = url.to_string(); + self.background_executor().spawn(async move { + let keyring = oo7::Keyring::new().await?; + keyring.unlock().await?; + + let items = keyring.search_items(&vec![("url", &url)]).await?; + + for item in items.into_iter() { + if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) { + item.delete().await?; + return Ok(()); + } + } + + Ok(()) + }) + } + + fn window_appearance(&self) -> WindowAppearance { + self.with_common(|common| common.appearance) + } + + fn register_url_scheme(&self, _: &str) -> Task> { + Task::ready(Err(anyhow!("register_url_scheme unimplemented"))) + } + + fn write_to_primary(&self, item: ClipboardItem) { + self.write_to_primary(item) + } + + fn write_to_clipboard(&self, item: ClipboardItem) { + self.write_to_clipboard(item) + } + + fn read_from_primary(&self) -> Option { + self.read_from_primary() + } + + fn read_from_clipboard(&self) -> Option { + self.read_from_clipboard() + } + + fn add_recent_document(&self, _path: &Path) {} +} + +#[cfg(any(feature = "wayland", feature = "x11"))] +pub(super) fn open_uri_internal( + executor: BackgroundExecutor, + uri: &str, + activation_token: Option, +) { + if let Some(uri) = ashpd::url::Url::parse(uri).log_err() { + executor + .spawn(async move { + match ashpd::desktop::open_uri::OpenFileRequest::default() + .activation_token(activation_token.clone().map(ashpd::ActivationToken::from)) + .send_uri(&uri) + .await + { + Ok(_) => return, + Err(e) => log::error!("Failed to open with dbus: {}", e), + } + + for mut command in open::commands(uri.to_string()) { + if let Some(token) = activation_token.as_ref() { + command.env("XDG_ACTIVATION_TOKEN", token); + } + let program = format!("{:?}", command.get_program()); + match smol::process::Command::from(command).spawn() { + Ok(mut cmd) => { + cmd.status().await.log_err(); + return; + } + Err(e) => { + log::error!("Failed to open with {}: {}", program, e) + } + } + } + }) + .detach(); + } +} + +#[cfg(any(feature = "x11", feature = "wayland"))] +pub(super) fn reveal_path_internal( + executor: BackgroundExecutor, + path: PathBuf, + activation_token: Option, +) { + executor + .spawn(async move { + if let Some(dir) = File::open(path.clone()).log_err() { + match ashpd::desktop::open_uri::OpenDirectoryRequest::default() + .activation_token(activation_token.map(ashpd::ActivationToken::from)) + .send(&dir.as_fd()) + .await + { + Ok(_) => return, + Err(e) => log::error!("Failed to open with dbus: {}", e), + } + if path.is_dir() { + open::that_detached(path).log_err(); + } else { + open::that_detached(path.parent().unwrap_or(Path::new(""))).log_err(); + } + } + }) + .detach(); +} + +#[allow(unused)] +pub(super) fn is_within_click_distance(a: Point, b: Point) -> bool { + let diff = a - b; + diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE +} + +#[cfg(any(feature = "wayland", feature = "x11"))] +pub(super) fn get_xkb_compose_state(cx: &xkb::Context) -> Option { + let mut locales = Vec::default(); + if let Some(locale) = env::var_os("LC_CTYPE") { + locales.push(locale); + } + locales.push(OsString::from("C")); + let mut state: Option = None; + for locale in locales { + if let Ok(table) = + xkb::compose::Table::new_from_locale(cx, &locale, xkb::compose::COMPILE_NO_FLAGS) + { + state = Some(xkb::compose::State::new( + &table, + xkb::compose::STATE_NO_FLAGS, + )); + break; + } + } + state +} + +#[cfg(any(feature = "wayland", feature = "x11"))] +pub(super) unsafe fn read_fd(mut fd: filedescriptor::FileDescriptor) -> Result> { + let mut file = unsafe { File::from_raw_fd(fd.as_raw_fd()) }; + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer)?; + Ok(buffer) +} + +#[cfg(any(feature = "wayland", feature = "x11"))] +pub(super) const DEFAULT_CURSOR_ICON_NAME: &str = "left_ptr"; + +impl CursorStyle { + #[cfg(any(feature = "wayland", feature = "x11"))] + pub(super) fn to_icon_names(self) -> &'static [&'static str] { + // Based on cursor names from chromium: + // https://github.com/chromium/chromium/blob/d3069cf9c973dc3627fa75f64085c6a86c8f41bf/ui/base/cursor/cursor_factory.cc#L113 + match self { + CursorStyle::Arrow => &[DEFAULT_CURSOR_ICON_NAME], + CursorStyle::IBeam => &["text", "xterm"], + CursorStyle::Crosshair => &["crosshair", "cross"], + CursorStyle::ClosedHand => &["closedhand", "grabbing", "hand2"], + CursorStyle::OpenHand => &["openhand", "grab", "hand1"], + CursorStyle::PointingHand => &["pointer", "hand", "hand2"], + CursorStyle::ResizeLeft => &["w-resize", "left_side"], + CursorStyle::ResizeRight => &["e-resize", "right_side"], + CursorStyle::ResizeLeftRight => &["ew-resize", "sb_h_double_arrow"], + CursorStyle::ResizeUp => &["n-resize", "top_side"], + CursorStyle::ResizeDown => &["s-resize", "bottom_side"], + CursorStyle::ResizeUpDown => &["sb_v_double_arrow", "ns-resize"], + CursorStyle::ResizeUpLeftDownRight => &["size_fdiag", "bd_double_arrow", "nwse-resize"], + CursorStyle::ResizeUpRightDownLeft => &["size_bdiag", "nesw-resize", "fd_double_arrow"], + CursorStyle::ResizeColumn => &["col-resize", "sb_h_double_arrow"], + CursorStyle::ResizeRow => &["row-resize", "sb_v_double_arrow"], + CursorStyle::IBeamCursorForVerticalLayout => &["vertical-text"], + CursorStyle::OperationNotAllowed => &["not-allowed", "crossed_circle"], + CursorStyle::DragLink => &["alias"], + CursorStyle::DragCopy => &["copy"], + CursorStyle::ContextualMenu => &["context-menu"], + CursorStyle::None => { + #[cfg(debug_assertions)] + panic!("CursorStyle::None should be handled separately in the client"); + #[cfg(not(debug_assertions))] + &[DEFAULT_CURSOR_ICON_NAME] + } + } + } +} + +#[cfg(any(feature = "wayland", feature = "x11"))] +pub(super) fn log_cursor_icon_warning(message: impl std::fmt::Display) { + if let Ok(xcursor_path) = env::var("XCURSOR_PATH") { + log::warn!( + "{:#}\ncursor icon loading may be failing if XCURSOR_PATH environment variable is invalid. \ + XCURSOR_PATH overrides the default icon search. Its current value is '{}'", + message, + xcursor_path + ); + } else { + log::warn!("{:#}", message); + } +} + +#[cfg(any(feature = "wayland", feature = "x11"))] +fn guess_ascii(keycode: Keycode, shift: bool) -> Option { + let c = match (keycode.raw(), shift) { + (24, _) => 'q', + (25, _) => 'w', + (26, _) => 'e', + (27, _) => 'r', + (28, _) => 't', + (29, _) => 'y', + (30, _) => 'u', + (31, _) => 'i', + (32, _) => 'o', + (33, _) => 'p', + (34, false) => '[', + (34, true) => '{', + (35, false) => ']', + (35, true) => '}', + (38, _) => 'a', + (39, _) => 's', + (40, _) => 'd', + (41, _) => 'f', + (42, _) => 'g', + (43, _) => 'h', + (44, _) => 'j', + (45, _) => 'k', + (46, _) => 'l', + (47, false) => ';', + (47, true) => ':', + (48, false) => '\'', + (48, true) => '"', + (49, false) => '`', + (49, true) => '~', + (51, false) => '\\', + (51, true) => '|', + (52, _) => 'z', + (53, _) => 'x', + (54, _) => 'c', + (55, _) => 'v', + (56, _) => 'b', + (57, _) => 'n', + (58, _) => 'm', + (59, false) => ',', + (59, true) => '>', + (60, false) => '.', + (60, true) => '<', + (61, false) => '/', + (61, true) => '?', + + _ => return None, + }; + + Some(c) +} + +#[cfg(any(feature = "wayland", feature = "x11"))] +impl crate::Keystroke { + pub(super) fn from_xkb( + state: &State, + mut modifiers: crate::Modifiers, + keycode: Keycode, + ) -> Self { + let key_utf32 = state.key_get_utf32(keycode); + let key_utf8 = state.key_get_utf8(keycode); + let key_sym = state.key_get_one_sym(keycode); + + let key = match key_sym { + Keysym::Return => "enter".to_owned(), + Keysym::Prior => "pageup".to_owned(), + Keysym::Next => "pagedown".to_owned(), + Keysym::ISO_Left_Tab => "tab".to_owned(), + Keysym::KP_Prior => "pageup".to_owned(), + Keysym::KP_Next => "pagedown".to_owned(), + Keysym::XF86_Back => "back".to_owned(), + Keysym::XF86_Forward => "forward".to_owned(), + Keysym::XF86_Cut => "cut".to_owned(), + Keysym::XF86_Copy => "copy".to_owned(), + Keysym::XF86_Paste => "paste".to_owned(), + Keysym::XF86_New => "new".to_owned(), + Keysym::XF86_Open => "open".to_owned(), + Keysym::XF86_Save => "save".to_owned(), + + Keysym::comma => ",".to_owned(), + Keysym::period => ".".to_owned(), + Keysym::less => "<".to_owned(), + Keysym::greater => ">".to_owned(), + Keysym::slash => "/".to_owned(), + Keysym::question => "?".to_owned(), + + Keysym::semicolon => ";".to_owned(), + Keysym::colon => ":".to_owned(), + Keysym::apostrophe => "'".to_owned(), + Keysym::quotedbl => "\"".to_owned(), + + Keysym::bracketleft => "[".to_owned(), + Keysym::braceleft => "{".to_owned(), + Keysym::bracketright => "]".to_owned(), + Keysym::braceright => "}".to_owned(), + Keysym::backslash => "\\".to_owned(), + Keysym::bar => "|".to_owned(), + + Keysym::grave => "`".to_owned(), + Keysym::asciitilde => "~".to_owned(), + Keysym::exclam => "!".to_owned(), + Keysym::at => "@".to_owned(), + Keysym::numbersign => "#".to_owned(), + Keysym::dollar => "$".to_owned(), + Keysym::percent => "%".to_owned(), + Keysym::asciicircum => "^".to_owned(), + Keysym::ampersand => "&".to_owned(), + Keysym::asterisk => "*".to_owned(), + Keysym::parenleft => "(".to_owned(), + Keysym::parenright => ")".to_owned(), + Keysym::minus => "-".to_owned(), + Keysym::underscore => "_".to_owned(), + Keysym::equal => "=".to_owned(), + Keysym::plus => "+".to_owned(), + Keysym::space => "space".to_owned(), + Keysym::BackSpace => "backspace".to_owned(), + Keysym::Tab => "tab".to_owned(), + Keysym::Delete => "delete".to_owned(), + Keysym::Escape => "escape".to_owned(), + + Keysym::Left => "left".to_owned(), + Keysym::Right => "right".to_owned(), + Keysym::Up => "up".to_owned(), + Keysym::Down => "down".to_owned(), + Keysym::Home => "home".to_owned(), + Keysym::End => "end".to_owned(), + Keysym::Insert => "insert".to_owned(), + + _ => { + let name = xkb::keysym_get_name(key_sym).to_lowercase(); + if key_sym.is_keypad_key() { + name.replace("kp_", "") + } else if let Some(key) = key_utf8.chars().next() + && key_utf8.len() == 1 + && key.is_ascii() + { + if key.is_ascii_graphic() { + key_utf8.to_lowercase() + // map ctrl-a to `a` + // ctrl-0..9 may emit control codes like ctrl-[, but + // we don't want to map them to `[` + } else if key_utf32 <= 0x1f + && !name.chars().next().is_some_and(|c| c.is_ascii_digit()) + { + ((key_utf32 as u8 + 0x40) as char) + .to_ascii_lowercase() + .to_string() + } else { + name + } + } else if let Some(key_en) = guess_ascii(keycode, modifiers.shift) { + String::from(key_en) + } else { + name + } + } + }; + + if modifiers.shift { + // we only include the shift for upper-case letters by convention, + // so don't include for numbers and symbols, but do include for + // tab/enter, etc. + if key.chars().count() == 1 && key.to_lowercase() == key.to_uppercase() { + modifiers.shift = false; + } + } + + // Ignore control characters (and DEL) for the purposes of key_char + let key_char = + (key_utf32 >= 32 && key_utf32 != 127 && !key_utf8.is_empty()).then_some(key_utf8); + + Self { + modifiers, + key, + key_char, + } + } + + /** + * Returns which symbol the dead key represents + * + */ + pub fn underlying_dead_key(keysym: Keysym) -> Option { + match keysym { + Keysym::dead_grave => Some("`".to_owned()), + Keysym::dead_acute => Some("´".to_owned()), + Keysym::dead_circumflex => Some("^".to_owned()), + Keysym::dead_tilde => Some("~".to_owned()), + Keysym::dead_macron => Some("¯".to_owned()), + Keysym::dead_breve => Some("˘".to_owned()), + Keysym::dead_abovedot => Some("˙".to_owned()), + Keysym::dead_diaeresis => Some("¨".to_owned()), + Keysym::dead_abovering => Some("˚".to_owned()), + Keysym::dead_doubleacute => Some("˝".to_owned()), + Keysym::dead_caron => Some("ˇ".to_owned()), + Keysym::dead_cedilla => Some("¸".to_owned()), + Keysym::dead_ogonek => Some("˛".to_owned()), + Keysym::dead_iota => Some("ͅ".to_owned()), + Keysym::dead_voiced_sound => Some("゙".to_owned()), + Keysym::dead_semivoiced_sound => Some("゚".to_owned()), + Keysym::dead_belowdot => Some("̣̣".to_owned()), + Keysym::dead_hook => Some("̡".to_owned()), + Keysym::dead_horn => Some("̛".to_owned()), + Keysym::dead_stroke => Some("̶̶".to_owned()), + Keysym::dead_abovecomma => Some("̓̓".to_owned()), + Keysym::dead_abovereversedcomma => Some("ʽ".to_owned()), + Keysym::dead_doublegrave => Some("̏".to_owned()), + Keysym::dead_belowring => Some("˳".to_owned()), + Keysym::dead_belowmacron => Some("̱".to_owned()), + Keysym::dead_belowcircumflex => Some("ꞈ".to_owned()), + Keysym::dead_belowtilde => Some("̰".to_owned()), + Keysym::dead_belowbreve => Some("̮".to_owned()), + Keysym::dead_belowdiaeresis => Some("̤".to_owned()), + Keysym::dead_invertedbreve => Some("̯".to_owned()), + Keysym::dead_belowcomma => Some("̦".to_owned()), + Keysym::dead_currency => None, + Keysym::dead_lowline => None, + Keysym::dead_aboveverticalline => None, + Keysym::dead_belowverticalline => None, + Keysym::dead_longsolidusoverlay => None, + Keysym::dead_a => None, + Keysym::dead_A => None, + Keysym::dead_e => None, + Keysym::dead_E => None, + Keysym::dead_i => None, + Keysym::dead_I => None, + Keysym::dead_o => None, + Keysym::dead_O => None, + Keysym::dead_u => None, + Keysym::dead_U => None, + Keysym::dead_small_schwa => Some("ə".to_owned()), + Keysym::dead_capital_schwa => Some("Ə".to_owned()), + Keysym::dead_greek => None, + _ => None, + } + } +} + +#[cfg(any(feature = "wayland", feature = "x11"))] +impl crate::Modifiers { + pub(super) fn from_xkb(keymap_state: &State) -> Self { + let shift = keymap_state.mod_name_is_active(xkb::MOD_NAME_SHIFT, xkb::STATE_MODS_EFFECTIVE); + let alt = keymap_state.mod_name_is_active(xkb::MOD_NAME_ALT, xkb::STATE_MODS_EFFECTIVE); + let control = + keymap_state.mod_name_is_active(xkb::MOD_NAME_CTRL, xkb::STATE_MODS_EFFECTIVE); + let platform = + keymap_state.mod_name_is_active(xkb::MOD_NAME_LOGO, xkb::STATE_MODS_EFFECTIVE); + Self { + shift, + alt, + control, + platform, + function: false, + } + } +} + +#[cfg(any(feature = "wayland", feature = "x11"))] +impl crate::Capslock { + pub(super) fn from_xkb(keymap_state: &State) -> Self { + let on = keymap_state.mod_name_is_active(xkb::MOD_NAME_CAPS, xkb::STATE_MODS_EFFECTIVE); + Self { on } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Point, px}; + + #[test] + fn test_is_within_click_distance() { + let zero = Point::new(px(0.0), px(0.0)); + assert!(is_within_click_distance(zero, Point::new(px(5.0), px(5.0)))); + assert!(is_within_click_distance( + zero, + Point::new(px(-4.9), px(5.0)) + )); + assert!(is_within_click_distance( + Point::new(px(3.0), px(2.0)), + Point::new(px(-2.0), px(-2.0)) + )); + assert!(!is_within_click_distance( + zero, + Point::new(px(5.0), px(5.1)) + ),); + } +} diff --git a/third_party/gpui/src/platform/linux/text_system.rs b/third_party/gpui/src/platform/linux/text_system.rs new file mode 100644 index 0000000..958d509 --- /dev/null +++ b/third_party/gpui/src/platform/linux/text_system.rs @@ -0,0 +1,581 @@ +use crate::{ + Bounds, DevicePixels, Font, FontFeatures, FontId, FontMetrics, FontRun, FontStyle, FontWeight, + GlyphId, LineLayout, Pixels, PlatformTextSystem, Point, RenderGlyphParams, SUBPIXEL_VARIANTS_X, + SUBPIXEL_VARIANTS_Y, ShapedGlyph, ShapedRun, SharedString, Size, point, size, +}; +use anyhow::{Context as _, Ok, Result}; +use collections::HashMap; +use cosmic_text::{ + Attrs, AttrsList, CacheKey, Family, Font as CosmicTextFont, FontFeatures as CosmicFontFeatures, + FontSystem, ShapeBuffer, ShapeLine, SwashCache, +}; + +use itertools::Itertools; +use parking_lot::RwLock; +use pathfinder_geometry::{ + rect::{RectF, RectI}, + vector::{Vector2F, Vector2I}, +}; +use smallvec::SmallVec; +use std::{borrow::Cow, sync::Arc}; + +pub(crate) struct CosmicTextSystem(RwLock); + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct FontKey { + family: SharedString, + features: FontFeatures, +} + +impl FontKey { + fn new(family: SharedString, features: FontFeatures) -> Self { + Self { family, features } + } +} + +struct CosmicTextSystemState { + swash_cache: SwashCache, + font_system: FontSystem, + scratch: ShapeBuffer, + /// Contains all already loaded fonts, including all faces. Indexed by `FontId`. + loaded_fonts: Vec, + /// Caches the `FontId`s associated with a specific family to avoid iterating the font database + /// for every font face in a family. + font_ids_by_family_cache: HashMap>, +} + +struct LoadedFont { + font: Arc, + features: CosmicFontFeatures, + is_known_emoji_font: bool, +} + +impl CosmicTextSystem { + pub(crate) fn new() -> Self { + // todo(linux) make font loading non-blocking + let mut font_system = FontSystem::new(); + + Self(RwLock::new(CosmicTextSystemState { + font_system, + swash_cache: SwashCache::new(), + scratch: ShapeBuffer::default(), + loaded_fonts: Vec::new(), + font_ids_by_family_cache: HashMap::default(), + })) + } +} + +impl Default for CosmicTextSystem { + fn default() -> Self { + Self::new() + } +} + +impl PlatformTextSystem for CosmicTextSystem { + fn add_fonts(&self, fonts: Vec>) -> Result<()> { + self.0.write().add_fonts(fonts) + } + + fn all_font_names(&self) -> Vec { + let mut result = self + .0 + .read() + .font_system + .db() + .faces() + .filter_map(|face| face.families.first().map(|family| family.0.clone())) + .collect_vec(); + result.sort(); + result.dedup(); + result + } + + fn font_id(&self, font: &Font) -> Result { + // todo(linux): Do we need to use CosmicText's Font APIs? Can we consolidate this to use font_kit? + let mut state = self.0.write(); + let key = FontKey::new(font.family.clone(), font.features.clone()); + let candidates = if let Some(font_ids) = state.font_ids_by_family_cache.get(&key) { + font_ids.as_slice() + } else { + let font_ids = state.load_family(&font.family, &font.features)?; + state.font_ids_by_family_cache.insert(key.clone(), font_ids); + state.font_ids_by_family_cache[&key].as_ref() + }; + + // todo(linux) ideally we would make fontdb's `find_best_match` pub instead of using font-kit here + let candidate_properties = candidates + .iter() + .map(|font_id| { + let database_id = state.loaded_font(*font_id).font.id(); + let face_info = state.font_system.db().face(database_id).expect(""); + face_info_into_properties(face_info) + }) + .collect::>(); + + let ix = + font_kit::matching::find_best_match(&candidate_properties, &font_into_properties(font)) + .context("requested font family contains no font matching the other parameters")?; + + Ok(candidates[ix]) + } + + fn font_metrics(&self, font_id: FontId) -> FontMetrics { + let metrics = self + .0 + .read() + .loaded_font(font_id) + .font + .as_swash() + .metrics(&[]); + + FontMetrics { + units_per_em: metrics.units_per_em as u32, + ascent: metrics.ascent, + descent: -metrics.descent, // todo(linux) confirm this is correct + line_gap: metrics.leading, + underline_position: metrics.underline_offset, + underline_thickness: metrics.stroke_size, + cap_height: metrics.cap_height, + x_height: metrics.x_height, + // todo(linux): Compute this correctly + bounding_box: Bounds { + origin: point(0.0, 0.0), + size: size(metrics.max_width, metrics.ascent + metrics.descent), + }, + } + } + + fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + let lock = self.0.read(); + let glyph_metrics = lock.loaded_font(font_id).font.as_swash().glyph_metrics(&[]); + let glyph_id = glyph_id.0 as u16; + // todo(linux): Compute this correctly + // see https://github.com/servo/font-kit/blob/master/src/loaders/freetype.rs#L614-L620 + Ok(Bounds { + origin: point(0.0, 0.0), + size: size( + glyph_metrics.advance_width(glyph_id), + glyph_metrics.advance_height(glyph_id), + ), + }) + } + + fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + self.0.read().advance(font_id, glyph_id) + } + + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { + self.0.read().glyph_for_char(font_id, ch) + } + + fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result> { + self.0.write().raster_bounds(params) + } + + fn rasterize_glyph( + &self, + params: &RenderGlyphParams, + raster_bounds: Bounds, + ) -> Result<(Size, Vec)> { + self.0.write().rasterize_glyph(params, raster_bounds) + } + + fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout { + self.0.write().layout_line(text, font_size, runs) + } +} + +impl CosmicTextSystemState { + fn loaded_font(&self, font_id: FontId) -> &LoadedFont { + &self.loaded_fonts[font_id.0] + } + + #[profiling::function] + fn add_fonts(&mut self, fonts: Vec>) -> Result<()> { + let db = self.font_system.db_mut(); + for bytes in fonts { + match bytes { + Cow::Borrowed(embedded_font) => { + db.load_font_data(embedded_font.to_vec()); + } + Cow::Owned(bytes) => { + db.load_font_data(bytes); + } + } + } + Ok(()) + } + + #[profiling::function] + fn load_family( + &mut self, + name: &str, + features: &FontFeatures, + ) -> Result> { + // TODO: Determine the proper system UI font. + let name = crate::text_system::font_name_with_fallbacks(name, "IBM Plex Sans"); + + let families = self + .font_system + .db() + .faces() + .filter(|face| face.families.iter().any(|family| *name == family.0)) + .map(|face| (face.id, face.post_script_name.clone())) + .collect::>(); + + let mut loaded_font_ids = SmallVec::new(); + for (font_id, postscript_name) in families { + let font = self + .font_system + .get_font(font_id) + .context("Could not load font")?; + + // HACK: To let the storybook run and render Windows caption icons. We should actually do better font fallback. + let allowed_bad_font_names = [ + "SegoeFluentIcons", // NOTE: Segoe fluent icons postscript name is inconsistent + "Segoe Fluent Icons", + ]; + + if font.as_swash().charmap().map('m') == 0 + && !allowed_bad_font_names.contains(&postscript_name.as_str()) + { + self.font_system.db_mut().remove_face(font.id()); + continue; + }; + + let font_id = FontId(self.loaded_fonts.len()); + loaded_font_ids.push(font_id); + self.loaded_fonts.push(LoadedFont { + font, + features: features.try_into()?, + is_known_emoji_font: check_is_known_emoji_font(&postscript_name), + }); + } + + Ok(loaded_font_ids) + } + + fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + let glyph_metrics = self.loaded_font(font_id).font.as_swash().glyph_metrics(&[]); + Ok(Size { + width: glyph_metrics.advance_width(glyph_id.0 as u16), + height: glyph_metrics.advance_height(glyph_id.0 as u16), + }) + } + + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { + let glyph_id = self.loaded_font(font_id).font.as_swash().charmap().map(ch); + if glyph_id == 0 { + None + } else { + Some(GlyphId(glyph_id.into())) + } + } + + fn raster_bounds(&mut self, params: &RenderGlyphParams) -> Result> { + let font = &self.loaded_fonts[params.font_id.0].font; + let subpixel_shift = point( + params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor, + params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor, + ); + let image = self + .swash_cache + .get_image( + &mut self.font_system, + CacheKey::new( + font.id(), + params.glyph_id.0 as u16, + (params.font_size * params.scale_factor).into(), + (subpixel_shift.x, subpixel_shift.y.trunc()), + cosmic_text::CacheKeyFlags::empty(), + ) + .0, + ) + .clone() + .with_context(|| format!("no image for {params:?} in font {font:?}"))?; + Ok(Bounds { + origin: point(image.placement.left.into(), (-image.placement.top).into()), + size: size(image.placement.width.into(), image.placement.height.into()), + }) + } + + #[profiling::function] + fn rasterize_glyph( + &mut self, + params: &RenderGlyphParams, + glyph_bounds: Bounds, + ) -> Result<(Size, Vec)> { + if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 { + anyhow::bail!("glyph bounds are empty"); + } else { + let bitmap_size = glyph_bounds.size; + let font = &self.loaded_fonts[params.font_id.0].font; + let subpixel_shift = point( + params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor, + params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor, + ); + let mut image = self + .swash_cache + .get_image( + &mut self.font_system, + CacheKey::new( + font.id(), + params.glyph_id.0 as u16, + (params.font_size * params.scale_factor).into(), + (subpixel_shift.x, subpixel_shift.y.trunc()), + cosmic_text::CacheKeyFlags::empty(), + ) + .0, + ) + .clone() + .with_context(|| format!("no image for {params:?} in font {font:?}"))?; + + if params.is_emoji { + // Convert from RGBA to BGRA. + for pixel in image.data.chunks_exact_mut(4) { + pixel.swap(0, 2); + } + } + + Ok((bitmap_size, image.data)) + } + } + + /// This is used when cosmic_text has chosen a fallback font instead of using the requested + /// font, typically to handle some unicode characters. When this happens, `loaded_fonts` may not + /// yet have an entry for this fallback font, and so one is added. + /// + /// Note that callers shouldn't use this `FontId` somewhere that will retrieve the corresponding + /// `LoadedFont.features`, as it will have an arbitrarily chosen or empty value. The only + /// current use of this field is for the *input* of `layout_line`, and so it's fine to use + /// `font_id_for_cosmic_id` when computing the *output* of `layout_line`. + fn font_id_for_cosmic_id(&mut self, id: cosmic_text::fontdb::ID) -> FontId { + if let Some(ix) = self + .loaded_fonts + .iter() + .position(|loaded_font| loaded_font.font.id() == id) + { + FontId(ix) + } else { + let font = self.font_system.get_font(id).unwrap(); + let face = self.font_system.db().face(id).unwrap(); + + let font_id = FontId(self.loaded_fonts.len()); + self.loaded_fonts.push(LoadedFont { + font, + features: CosmicFontFeatures::new(), + is_known_emoji_font: check_is_known_emoji_font(&face.post_script_name), + }); + + font_id + } + } + + #[profiling::function] + fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout { + let mut attrs_list = AttrsList::new(&Attrs::new()); + let mut offs = 0; + for run in font_runs { + let loaded_font = self.loaded_font(run.font_id); + let font = self.font_system.db().face(loaded_font.font.id()).unwrap(); + + attrs_list.add_span( + offs..(offs + run.len), + &Attrs::new() + .metadata(run.font_id.0) + .family(Family::Name(&font.families.first().unwrap().0)) + .stretch(font.stretch) + .style(font.style) + .weight(font.weight) + .font_features(loaded_font.features.clone()), + ); + offs += run.len; + } + + let line = ShapeLine::new( + &mut self.font_system, + text, + &attrs_list, + cosmic_text::Shaping::Advanced, + 4, + ); + let mut layout_lines = Vec::with_capacity(1); + line.layout_to_buffer( + &mut self.scratch, + font_size.0, + None, // We do our own wrapping + cosmic_text::Wrap::None, + None, + &mut layout_lines, + None, + ); + let layout = layout_lines.first().unwrap(); + + let mut runs: Vec = Vec::new(); + for glyph in &layout.glyphs { + let mut font_id = FontId(glyph.metadata); + let mut loaded_font = self.loaded_font(font_id); + if loaded_font.font.id() != glyph.font_id { + font_id = self.font_id_for_cosmic_id(glyph.font_id); + loaded_font = self.loaded_font(font_id); + } + let is_emoji = loaded_font.is_known_emoji_font; + + // HACK: Prevent crash caused by variation selectors. + if glyph.glyph_id == 3 && is_emoji { + continue; + } + + let shaped_glyph = ShapedGlyph { + id: GlyphId(glyph.glyph_id as u32), + position: point(glyph.x.into(), glyph.y.into()), + index: glyph.start, + is_emoji, + }; + + if let Some(last_run) = runs + .last_mut() + .filter(|last_run| last_run.font_id == font_id) + { + last_run.glyphs.push(shaped_glyph); + } else { + runs.push(ShapedRun { + font_id, + glyphs: vec![shaped_glyph], + }); + } + } + + LineLayout { + font_size, + width: layout.w.into(), + ascent: layout.max_ascent.into(), + descent: layout.max_descent.into(), + runs, + len: text.len(), + } + } +} + +impl TryFrom<&FontFeatures> for CosmicFontFeatures { + type Error = anyhow::Error; + + fn try_from(features: &FontFeatures) -> Result { + let mut result = CosmicFontFeatures::new(); + for feature in features.0.iter() { + let name_bytes: [u8; 4] = feature + .0 + .as_bytes() + .try_into() + .context("Incorrect feature flag format")?; + + let tag = cosmic_text::FeatureTag::new(&name_bytes); + + result.set(tag, feature.1); + } + Ok(result) + } +} + +impl From for Bounds { + fn from(rect: RectF) -> Self { + Bounds { + origin: point(rect.origin_x(), rect.origin_y()), + size: size(rect.width(), rect.height()), + } + } +} + +impl From for Bounds { + fn from(rect: RectI) -> Self { + Bounds { + origin: point(DevicePixels(rect.origin_x()), DevicePixels(rect.origin_y())), + size: size(DevicePixels(rect.width()), DevicePixels(rect.height())), + } + } +} + +impl From for Size { + fn from(value: Vector2I) -> Self { + size(value.x().into(), value.y().into()) + } +} + +impl From for Bounds { + fn from(rect: RectI) -> Self { + Bounds { + origin: point(rect.origin_x(), rect.origin_y()), + size: size(rect.width(), rect.height()), + } + } +} + +impl From> for Vector2I { + fn from(size: Point) -> Self { + Vector2I::new(size.x as i32, size.y as i32) + } +} + +impl From for Size { + fn from(vec: Vector2F) -> Self { + size(vec.x(), vec.y()) + } +} + +impl From for cosmic_text::Weight { + fn from(value: FontWeight) -> Self { + cosmic_text::Weight(value.0 as u16) + } +} + +impl From for cosmic_text::Style { + fn from(style: FontStyle) -> Self { + match style { + FontStyle::Normal => cosmic_text::Style::Normal, + FontStyle::Italic => cosmic_text::Style::Italic, + FontStyle::Oblique => cosmic_text::Style::Oblique, + } + } +} + +fn font_into_properties(font: &crate::Font) -> font_kit::properties::Properties { + font_kit::properties::Properties { + style: match font.style { + crate::FontStyle::Normal => font_kit::properties::Style::Normal, + crate::FontStyle::Italic => font_kit::properties::Style::Italic, + crate::FontStyle::Oblique => font_kit::properties::Style::Oblique, + }, + weight: font_kit::properties::Weight(font.weight.0), + stretch: Default::default(), + } +} + +fn face_info_into_properties( + face_info: &cosmic_text::fontdb::FaceInfo, +) -> font_kit::properties::Properties { + font_kit::properties::Properties { + style: match face_info.style { + cosmic_text::Style::Normal => font_kit::properties::Style::Normal, + cosmic_text::Style::Italic => font_kit::properties::Style::Italic, + cosmic_text::Style::Oblique => font_kit::properties::Style::Oblique, + }, + // both libs use the same values for weight + weight: font_kit::properties::Weight(face_info.weight.0.into()), + stretch: match face_info.stretch { + cosmic_text::Stretch::Condensed => font_kit::properties::Stretch::CONDENSED, + cosmic_text::Stretch::Expanded => font_kit::properties::Stretch::EXPANDED, + cosmic_text::Stretch::ExtraCondensed => font_kit::properties::Stretch::EXTRA_CONDENSED, + cosmic_text::Stretch::ExtraExpanded => font_kit::properties::Stretch::EXTRA_EXPANDED, + cosmic_text::Stretch::Normal => font_kit::properties::Stretch::NORMAL, + cosmic_text::Stretch::SemiCondensed => font_kit::properties::Stretch::SEMI_CONDENSED, + cosmic_text::Stretch::SemiExpanded => font_kit::properties::Stretch::SEMI_EXPANDED, + cosmic_text::Stretch::UltraCondensed => font_kit::properties::Stretch::ULTRA_CONDENSED, + cosmic_text::Stretch::UltraExpanded => font_kit::properties::Stretch::ULTRA_EXPANDED, + }, + } +} + +fn check_is_known_emoji_font(postscript_name: &str) -> bool { + // TODO: Include other common emoji fonts + postscript_name == "NotoColorEmoji" +} diff --git a/third_party/gpui/src/platform/linux/wayland.rs b/third_party/gpui/src/platform/linux/wayland.rs new file mode 100644 index 0000000..487bc9f --- /dev/null +++ b/third_party/gpui/src/platform/linux/wayland.rs @@ -0,0 +1,46 @@ +mod client; +mod clipboard; +mod cursor; +mod display; +mod serial; +mod window; + +pub(crate) use client::*; + +use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape; + +use crate::CursorStyle; + +impl CursorStyle { + pub(super) fn to_shape(self) -> Shape { + match self { + CursorStyle::Arrow => Shape::Default, + CursorStyle::IBeam => Shape::Text, + CursorStyle::Crosshair => Shape::Crosshair, + CursorStyle::ClosedHand => Shape::Grabbing, + CursorStyle::OpenHand => Shape::Grab, + CursorStyle::PointingHand => Shape::Pointer, + CursorStyle::ResizeLeft => Shape::WResize, + CursorStyle::ResizeRight => Shape::EResize, + CursorStyle::ResizeLeftRight => Shape::EwResize, + CursorStyle::ResizeUp => Shape::NResize, + CursorStyle::ResizeDown => Shape::SResize, + CursorStyle::ResizeUpDown => Shape::NsResize, + CursorStyle::ResizeUpLeftDownRight => Shape::NwseResize, + CursorStyle::ResizeUpRightDownLeft => Shape::NeswResize, + CursorStyle::ResizeColumn => Shape::ColResize, + CursorStyle::ResizeRow => Shape::RowResize, + CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText, + CursorStyle::OperationNotAllowed => Shape::NotAllowed, + CursorStyle::DragLink => Shape::Alias, + CursorStyle::DragCopy => Shape::Copy, + CursorStyle::ContextualMenu => Shape::ContextMenu, + CursorStyle::None => { + #[cfg(debug_assertions)] + panic!("CursorStyle::None should be handled separately in the client"); + #[cfg(not(debug_assertions))] + Shape::Default + } + } + } +} diff --git a/third_party/gpui/src/platform/linux/wayland/client.rs b/third_party/gpui/src/platform/linux/wayland/client.rs new file mode 100644 index 0000000..1ebdda3 --- /dev/null +++ b/third_party/gpui/src/platform/linux/wayland/client.rs @@ -0,0 +1,2159 @@ +use std::{ + cell::{RefCell, RefMut}, + hash::Hash, + os::fd::{AsRawFd, BorrowedFd}, + path::PathBuf, + rc::{Rc, Weak}, + time::{Duration, Instant}, +}; + +use ashpd::WindowIdentifier; +use calloop::{ + EventLoop, LoopHandle, + timer::{TimeoutAction, Timer}, +}; +use calloop_wayland_source::WaylandSource; +use collections::HashMap; +use filedescriptor::Pipe; +use http_client::Url; +use smallvec::SmallVec; +use util::ResultExt; +use wayland_backend::client::ObjectId; +use wayland_backend::protocol::WEnum; +use wayland_client::event_created_child; +use wayland_client::globals::{GlobalList, GlobalListContents, registry_queue_init}; +use wayland_client::protocol::wl_callback::{self, WlCallback}; +use wayland_client::protocol::wl_data_device_manager::DndAction; +use wayland_client::protocol::wl_data_offer::WlDataOffer; +use wayland_client::protocol::wl_pointer::AxisSource; +use wayland_client::protocol::{ + wl_data_device, wl_data_device_manager, wl_data_offer, wl_data_source, wl_output, wl_region, +}; +use wayland_client::{ + Connection, Dispatch, Proxy, QueueHandle, delegate_noop, + protocol::{ + wl_buffer, wl_compositor, wl_keyboard, wl_pointer, wl_registry, wl_seat, wl_shm, + wl_shm_pool, wl_surface, + }, +}; +use wayland_protocols::wp::cursor_shape::v1::client::{ + wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1, +}; +use wayland_protocols::wp::fractional_scale::v1::client::{ + wp_fractional_scale_manager_v1, wp_fractional_scale_v1, +}; +use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::{ + self, ZwpPrimarySelectionOfferV1, +}; +use wayland_protocols::wp::primary_selection::zv1::client::{ + zwp_primary_selection_device_manager_v1, zwp_primary_selection_device_v1, + zwp_primary_selection_source_v1, +}; +use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_v3::{ + ContentHint, ContentPurpose, +}; +use wayland_protocols::wp::text_input::zv3::client::{ + zwp_text_input_manager_v3, zwp_text_input_v3, +}; +use wayland_protocols::wp::viewporter::client::{wp_viewport, wp_viewporter}; +use wayland_protocols::xdg::activation::v1::client::{xdg_activation_token_v1, xdg_activation_v1}; +use wayland_protocols::xdg::decoration::zv1::client::{ + zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1, +}; +use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base}; +use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager}; +use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1; +use xkbcommon::xkb::{self, KEYMAP_COMPILE_NO_FLAGS, Keycode}; + +use super::{ + display::WaylandDisplay, + window::{ImeInput, WaylandWindowStatePtr}, +}; + +use crate::platform::{PlatformWindow, blade::BladeContext}; +use crate::{ + AnyWindowHandle, Bounds, Capslock, CursorStyle, DOUBLE_CLICK_INTERVAL, DevicePixels, DisplayId, + FileDropEvent, ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, LinuxCommon, + LinuxKeyboardLayout, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, + MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, PlatformDisplay, + PlatformInput, PlatformKeyboardLayout, Point, SCROLL_LINES, ScrollDelta, ScrollWheelEvent, + Size, TouchPhase, WindowParams, point, px, size, +}; +use crate::{ + SharedString, + platform::linux::{ + LinuxClient, get_xkb_compose_state, is_within_click_distance, open_uri_internal, read_fd, + reveal_path_internal, + wayland::{ + clipboard::{Clipboard, DataOffer, FILE_LIST_MIME_TYPE, TEXT_MIME_TYPES}, + cursor::Cursor, + serial::{SerialKind, SerialTracker}, + window::WaylandWindow, + }, + xdg_desktop_portal::{Event as XDPEvent, XDPEventSource}, + }, +}; + +/// Used to convert evdev scancode to xkb scancode +const MIN_KEYCODE: u32 = 8; + +const UNKNOWN_KEYBOARD_LAYOUT_NAME: SharedString = SharedString::new_static("unknown"); + +#[derive(Clone)] +pub struct Globals { + pub qh: QueueHandle, + pub activation: Option, + pub compositor: wl_compositor::WlCompositor, + pub cursor_shape_manager: Option, + pub data_device_manager: Option, + pub primary_selection_manager: + Option, + pub wm_base: xdg_wm_base::XdgWmBase, + pub shm: wl_shm::WlShm, + pub seat: wl_seat::WlSeat, + pub viewporter: Option, + pub fractional_scale_manager: + Option, + pub decoration_manager: Option, + pub blur_manager: Option, + pub text_input_manager: Option, + pub executor: ForegroundExecutor, +} + +impl Globals { + fn new( + globals: GlobalList, + executor: ForegroundExecutor, + qh: QueueHandle, + seat: wl_seat::WlSeat, + ) -> Self { + Globals { + activation: globals.bind(&qh, 1..=1, ()).ok(), + compositor: globals + .bind( + &qh, + wl_surface::REQ_SET_BUFFER_SCALE_SINCE + ..=wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE, + (), + ) + .unwrap(), + cursor_shape_manager: globals.bind(&qh, 1..=1, ()).ok(), + data_device_manager: globals + .bind( + &qh, + WL_DATA_DEVICE_MANAGER_VERSION..=WL_DATA_DEVICE_MANAGER_VERSION, + (), + ) + .ok(), + primary_selection_manager: globals.bind(&qh, 1..=1, ()).ok(), + shm: globals.bind(&qh, 1..=1, ()).unwrap(), + seat, + wm_base: globals.bind(&qh, 2..=5, ()).unwrap(), + viewporter: globals.bind(&qh, 1..=1, ()).ok(), + fractional_scale_manager: globals.bind(&qh, 1..=1, ()).ok(), + decoration_manager: globals.bind(&qh, 1..=1, ()).ok(), + blur_manager: globals.bind(&qh, 1..=1, ()).ok(), + text_input_manager: globals.bind(&qh, 1..=1, ()).ok(), + executor, + qh, + } + } +} + +#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)] +pub struct InProgressOutput { + name: Option, + scale: Option, + position: Option>, + size: Option>, +} + +impl InProgressOutput { + fn complete(&self) -> Option { + if let Some((position, size)) = self.position.zip(self.size) { + let scale = self.scale.unwrap_or(1); + Some(Output { + name: self.name.clone(), + scale, + bounds: Bounds::new(position, size), + }) + } else { + None + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct Output { + pub name: Option, + pub scale: i32, + pub bounds: Bounds, +} + +pub(crate) struct WaylandClientState { + serial_tracker: SerialTracker, + globals: Globals, + gpu_context: BladeContext, + wl_seat: wl_seat::WlSeat, // TODO: Multi seat support + wl_pointer: Option, + wl_keyboard: Option, + cursor_shape_device: Option, + data_device: Option, + primary_selection: Option, + text_input: Option, + pre_edit_text: Option, + ime_pre_edit: Option, + composing: bool, + // Surface to Window mapping + windows: HashMap, + // Output to scale mapping + outputs: HashMap, + in_progress_outputs: HashMap, + keyboard_layout: LinuxKeyboardLayout, + keymap_state: Option, + compose_state: Option, + drag: DragState, + click: ClickState, + repeat: KeyRepeat, + pub modifiers: Modifiers, + pub capslock: Capslock, + axis_source: AxisSource, + pub mouse_location: Option>, + continuous_scroll_delta: Option>, + discrete_scroll_delta: Option>, + vertical_modifier: f32, + horizontal_modifier: f32, + scroll_event_received: bool, + enter_token: Option<()>, + button_pressed: Option, + mouse_focused_window: Option, + keyboard_focused_window: Option, + loop_handle: LoopHandle<'static, WaylandClientStatePtr>, + cursor_style: Option, + clipboard: Clipboard, + data_offers: Vec>, + primary_data_offer: Option>, + cursor: Cursor, + pending_activation: Option, + event_loop: Option>, + common: LinuxCommon, +} + +pub struct DragState { + data_offer: Option, + window: Option, + position: Point, +} + +pub struct ClickState { + last_mouse_button: Option, + last_click: Instant, + last_location: Point, + current_count: usize, +} + +pub(crate) struct KeyRepeat { + characters_per_second: u32, + delay: Duration, + current_id: u64, + current_keycode: Option, +} + +pub(crate) enum PendingActivation { + /// URI to open in the web browser. + Uri(String), + /// Path to open in the file explorer. + Path(PathBuf), + /// A window from ourselves to raise. + Window(ObjectId), +} + +/// This struct is required to conform to Rust's orphan rules, so we can dispatch on the state but hand the +/// window to GPUI. +#[derive(Clone)] +pub struct WaylandClientStatePtr(Weak>); + +impl WaylandClientStatePtr { + pub fn get_client(&self) -> Rc> { + self.0 + .upgrade() + .expect("The pointer should always be valid when dispatching in wayland") + } + + pub fn get_serial(&self, kind: SerialKind) -> u32 { + self.0.upgrade().unwrap().borrow().serial_tracker.get(kind) + } + + pub fn set_pending_activation(&self, window: ObjectId) { + self.0.upgrade().unwrap().borrow_mut().pending_activation = + Some(PendingActivation::Window(window)); + } + + pub fn enable_ime(&self) { + let client = self.get_client(); + let mut state = client.borrow_mut(); + let Some(mut text_input) = state.text_input.take() else { + return; + }; + + text_input.enable(); + text_input.set_content_type(ContentHint::None, ContentPurpose::Normal); + if let Some(window) = state.keyboard_focused_window.clone() { + drop(state); + if let Some(area) = window.get_ime_area() { + text_input.set_cursor_rectangle( + area.origin.x.0 as i32, + area.origin.y.0 as i32, + area.size.width.0 as i32, + area.size.height.0 as i32, + ); + } + state = client.borrow_mut(); + } + text_input.commit(); + state.text_input = Some(text_input); + } + + pub fn disable_ime(&self) { + let client = self.get_client(); + let mut state = client.borrow_mut(); + state.composing = false; + if let Some(text_input) = &state.text_input { + text_input.disable(); + text_input.commit(); + } + } + + pub fn update_ime_position(&self, bounds: Bounds) { + let client = self.get_client(); + let mut state = client.borrow_mut(); + if state.composing || state.text_input.is_none() || state.pre_edit_text.is_some() { + return; + } + + let text_input = state.text_input.as_ref().unwrap(); + text_input.set_cursor_rectangle( + bounds.origin.x.0 as i32, + bounds.origin.y.0 as i32, + bounds.size.width.0 as i32, + bounds.size.height.0 as i32, + ); + text_input.commit(); + } + + pub fn handle_keyboard_layout_change(&self) { + let client = self.get_client(); + let mut state = client.borrow_mut(); + let changed = if let Some(keymap_state) = &state.keymap_state { + let layout_idx = keymap_state.serialize_layout(xkbcommon::xkb::STATE_LAYOUT_EFFECTIVE); + let keymap = keymap_state.get_keymap(); + let layout_name = keymap.layout_get_name(layout_idx); + let changed = layout_name != state.keyboard_layout.name(); + if changed { + state.keyboard_layout = LinuxKeyboardLayout::new(layout_name.to_string().into()); + } + changed + } else { + let changed = &UNKNOWN_KEYBOARD_LAYOUT_NAME != state.keyboard_layout.name(); + if changed { + state.keyboard_layout = LinuxKeyboardLayout::new(UNKNOWN_KEYBOARD_LAYOUT_NAME); + } + changed + }; + + if changed && let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() + { + drop(state); + callback(); + state = client.borrow_mut(); + state.common.callbacks.keyboard_layout_change = Some(callback); + } + } + + pub fn drop_window(&self, surface_id: &ObjectId) { + let mut client = self.get_client(); + let mut state = client.borrow_mut(); + let closed_window = state.windows.remove(surface_id).unwrap(); + if let Some(window) = state.mouse_focused_window.take() + && !window.ptr_eq(&closed_window) + { + state.mouse_focused_window = Some(window); + } + if let Some(window) = state.keyboard_focused_window.take() + && !window.ptr_eq(&closed_window) + { + state.keyboard_focused_window = Some(window); + } + if state.windows.is_empty() { + state.common.signal.stop(); + } + } +} + +#[derive(Clone)] +pub struct WaylandClient(Rc>); + +impl Drop for WaylandClient { + fn drop(&mut self) { + let mut state = self.0.borrow_mut(); + state.windows.clear(); + + if let Some(wl_pointer) = &state.wl_pointer { + wl_pointer.release(); + } + if let Some(cursor_shape_device) = &state.cursor_shape_device { + cursor_shape_device.destroy(); + } + if let Some(data_device) = &state.data_device { + data_device.release(); + } + if let Some(text_input) = &state.text_input { + text_input.destroy(); + } + } +} + +const WL_DATA_DEVICE_MANAGER_VERSION: u32 = 3; + +fn wl_seat_version(version: u32) -> u32 { + // We rely on the wl_pointer.frame event + const WL_SEAT_MIN_VERSION: u32 = 5; + const WL_SEAT_MAX_VERSION: u32 = 9; + + if version < WL_SEAT_MIN_VERSION { + panic!( + "wl_seat below required version: {} < {}", + version, WL_SEAT_MIN_VERSION + ); + } + + version.clamp(WL_SEAT_MIN_VERSION, WL_SEAT_MAX_VERSION) +} + +fn wl_output_version(version: u32) -> u32 { + const WL_OUTPUT_MIN_VERSION: u32 = 2; + const WL_OUTPUT_MAX_VERSION: u32 = 4; + + if version < WL_OUTPUT_MIN_VERSION { + panic!( + "wl_output below required version: {} < {}", + version, WL_OUTPUT_MIN_VERSION + ); + } + + version.clamp(WL_OUTPUT_MIN_VERSION, WL_OUTPUT_MAX_VERSION) +} + +impl WaylandClient { + pub(crate) fn new() -> Self { + let conn = Connection::connect_to_env().unwrap(); + + let (globals, mut event_queue) = + registry_queue_init::(&conn).unwrap(); + let qh = event_queue.handle(); + + let mut seat: Option = None; + #[allow(clippy::mutable_key_type)] + let mut in_progress_outputs = HashMap::default(); + globals.contents().with_list(|list| { + for global in list { + match &global.interface[..] { + "wl_seat" => { + seat = Some(globals.registry().bind::( + global.name, + wl_seat_version(global.version), + &qh, + (), + )); + } + "wl_output" => { + let output = globals.registry().bind::( + global.name, + wl_output_version(global.version), + &qh, + (), + ); + in_progress_outputs.insert(output.id(), InProgressOutput::default()); + } + _ => {} + } + } + }); + + let event_loop = EventLoop::::try_new().unwrap(); + + let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal()); + + let handle = event_loop.handle(); + handle + .insert_source(main_receiver, { + let handle = handle.clone(); + move |event, _, _: &mut WaylandClientStatePtr| { + if let calloop::channel::Event::Msg(runnable) = event { + handle.insert_idle(|_| { + runnable.run(); + }); + } + } + }) + .unwrap(); + + let gpu_context = BladeContext::new().expect("Unable to init GPU context"); + + let seat = seat.unwrap(); + let globals = Globals::new( + globals, + common.foreground_executor.clone(), + qh.clone(), + seat.clone(), + ); + + let data_device = globals + .data_device_manager + .as_ref() + .map(|data_device_manager| data_device_manager.get_data_device(&seat, &qh, ())); + + let primary_selection = globals + .primary_selection_manager + .as_ref() + .map(|primary_selection_manager| primary_selection_manager.get_device(&seat, &qh, ())); + + let mut cursor = Cursor::new(&conn, &globals, 24); + + handle + .insert_source(XDPEventSource::new(&common.background_executor), { + move |event, _, client| match event { + XDPEvent::WindowAppearance(appearance) => { + if let Some(client) = client.0.upgrade() { + let mut client = client.borrow_mut(); + + client.common.appearance = appearance; + + for window in client.windows.values_mut() { + window.set_appearance(appearance); + } + } + } + XDPEvent::CursorTheme(theme) => { + if let Some(client) = client.0.upgrade() { + let mut client = client.borrow_mut(); + client.cursor.set_theme(theme); + } + } + XDPEvent::CursorSize(size) => { + if let Some(client) = client.0.upgrade() { + let mut client = client.borrow_mut(); + client.cursor.set_size(size); + } + } + } + }) + .unwrap(); + + let mut state = Rc::new(RefCell::new(WaylandClientState { + serial_tracker: SerialTracker::new(), + globals, + gpu_context, + wl_seat: seat, + wl_pointer: None, + wl_keyboard: None, + cursor_shape_device: None, + data_device, + primary_selection, + text_input: None, + pre_edit_text: None, + ime_pre_edit: None, + composing: false, + outputs: HashMap::default(), + in_progress_outputs, + windows: HashMap::default(), + common, + keyboard_layout: LinuxKeyboardLayout::new(UNKNOWN_KEYBOARD_LAYOUT_NAME), + keymap_state: None, + compose_state: None, + drag: DragState { + data_offer: None, + window: None, + position: Point::default(), + }, + click: ClickState { + last_click: Instant::now(), + last_mouse_button: None, + last_location: Point::default(), + current_count: 0, + }, + repeat: KeyRepeat { + characters_per_second: 16, + delay: Duration::from_millis(500), + current_id: 0, + current_keycode: None, + }, + modifiers: Modifiers { + shift: false, + control: false, + alt: false, + function: false, + platform: false, + }, + capslock: Capslock { on: false }, + scroll_event_received: false, + axis_source: AxisSource::Wheel, + mouse_location: None, + continuous_scroll_delta: None, + discrete_scroll_delta: None, + vertical_modifier: -1.0, + horizontal_modifier: -1.0, + button_pressed: None, + mouse_focused_window: None, + keyboard_focused_window: None, + loop_handle: handle.clone(), + enter_token: None, + cursor_style: None, + clipboard: Clipboard::new(conn.clone(), handle.clone()), + data_offers: Vec::new(), + primary_data_offer: None, + cursor, + pending_activation: None, + event_loop: Some(event_loop), + })); + + WaylandSource::new(conn, event_queue) + .insert(handle) + .unwrap(); + + Self(state) + } +} + +impl LinuxClient for WaylandClient { + fn keyboard_layout(&self) -> Box { + Box::new(self.0.borrow().keyboard_layout.clone()) + } + + fn displays(&self) -> Vec> { + self.0 + .borrow() + .outputs + .iter() + .map(|(id, output)| { + Rc::new(WaylandDisplay { + id: id.clone(), + name: output.name.clone(), + bounds: output.bounds.to_pixels(output.scale as f32), + }) as Rc + }) + .collect() + } + + fn display(&self, id: DisplayId) -> Option> { + self.0 + .borrow() + .outputs + .iter() + .find_map(|(object_id, output)| { + (object_id.protocol_id() == id.0).then(|| { + Rc::new(WaylandDisplay { + id: object_id.clone(), + name: output.name.clone(), + bounds: output.bounds.to_pixels(output.scale as f32), + }) as Rc + }) + }) + } + + fn primary_display(&self) -> Option> { + None + } + + #[cfg(feature = "screen-capture")] + fn is_screen_capture_supported(&self) -> bool { + false + } + + #[cfg(feature = "screen-capture")] + fn screen_capture_sources( + &self, + ) -> futures::channel::oneshot::Receiver>>> + { + // TODO: Get screen capture working on wayland. Be sure to try window resizing as that may + // be tricky. + // + // start_scap_default_target_source() + let (sources_tx, sources_rx) = futures::channel::oneshot::channel(); + sources_tx + .send(Err(anyhow::anyhow!( + "Wayland screen capture not yet implemented." + ))) + .ok(); + sources_rx + } + + fn open_window( + &self, + handle: AnyWindowHandle, + params: WindowParams, + ) -> anyhow::Result> { + let mut state = self.0.borrow_mut(); + + let parent = state.keyboard_focused_window.as_ref().map(|w| w.toplevel()); + + let (window, surface_id) = WaylandWindow::new( + handle, + state.globals.clone(), + &state.gpu_context, + WaylandClientStatePtr(Rc::downgrade(&self.0)), + params, + state.common.appearance, + parent, + )?; + state.windows.insert(surface_id, window.0.clone()); + + Ok(Box::new(window)) + } + + fn set_cursor_style(&self, style: CursorStyle) { + let mut state = self.0.borrow_mut(); + + let need_update = state.cursor_style != Some(style); + + if need_update { + let serial = state.serial_tracker.get(SerialKind::MouseEnter); + state.cursor_style = Some(style); + + if let CursorStyle::None = style { + let wl_pointer = state + .wl_pointer + .clone() + .expect("window is focused by pointer"); + wl_pointer.set_cursor(serial, None, 0, 0); + } else if let Some(cursor_shape_device) = &state.cursor_shape_device { + cursor_shape_device.set_shape(serial, style.to_shape()); + } else if let Some(focused_window) = &state.mouse_focused_window { + // cursor-shape-v1 isn't supported, set the cursor using a surface. + let wl_pointer = state + .wl_pointer + .clone() + .expect("window is focused by pointer"); + let scale = focused_window.primary_output_scale(); + state + .cursor + .set_icon(&wl_pointer, serial, style.to_icon_names(), scale); + } + } + } + + fn open_uri(&self, uri: &str) { + let mut state = self.0.borrow_mut(); + if let (Some(activation), Some(window)) = ( + state.globals.activation.clone(), + state.mouse_focused_window.clone(), + ) { + state.pending_activation = Some(PendingActivation::Uri(uri.to_string())); + let token = activation.get_activation_token(&state.globals.qh, ()); + let serial = state.serial_tracker.get(SerialKind::MousePress); + token.set_serial(serial, &state.wl_seat); + token.set_surface(&window.surface()); + token.commit(); + } else { + let executor = state.common.background_executor.clone(); + open_uri_internal(executor, uri, None); + } + } + + fn reveal_path(&self, path: PathBuf) { + let mut state = self.0.borrow_mut(); + if let (Some(activation), Some(window)) = ( + state.globals.activation.clone(), + state.mouse_focused_window.clone(), + ) { + state.pending_activation = Some(PendingActivation::Path(path)); + let token = activation.get_activation_token(&state.globals.qh, ()); + let serial = state.serial_tracker.get(SerialKind::MousePress); + token.set_serial(serial, &state.wl_seat); + token.set_surface(&window.surface()); + token.commit(); + } else { + let executor = state.common.background_executor.clone(); + reveal_path_internal(executor, path, None); + } + } + + fn with_common(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R { + f(&mut self.0.borrow_mut().common) + } + + fn run(&self) { + let mut event_loop = self + .0 + .borrow_mut() + .event_loop + .take() + .expect("App is already running"); + + event_loop + .run( + None, + &mut WaylandClientStatePtr(Rc::downgrade(&self.0)), + |_| {}, + ) + .log_err(); + } + + fn write_to_primary(&self, item: crate::ClipboardItem) { + let mut state = self.0.borrow_mut(); + let (Some(primary_selection_manager), Some(primary_selection)) = ( + state.globals.primary_selection_manager.clone(), + state.primary_selection.clone(), + ) else { + return; + }; + if state.mouse_focused_window.is_some() || state.keyboard_focused_window.is_some() { + state.clipboard.set_primary(item); + let serial = state.serial_tracker.get(SerialKind::KeyPress); + let data_source = primary_selection_manager.create_source(&state.globals.qh, ()); + for mime_type in TEXT_MIME_TYPES { + data_source.offer(mime_type.to_string()); + } + data_source.offer(state.clipboard.self_mime()); + primary_selection.set_selection(Some(&data_source), serial); + } + } + + fn write_to_clipboard(&self, item: crate::ClipboardItem) { + let mut state = self.0.borrow_mut(); + let (Some(data_device_manager), Some(data_device)) = ( + state.globals.data_device_manager.clone(), + state.data_device.clone(), + ) else { + return; + }; + if state.mouse_focused_window.is_some() || state.keyboard_focused_window.is_some() { + state.clipboard.set(item); + let serial = state.serial_tracker.get(SerialKind::KeyPress); + let data_source = data_device_manager.create_data_source(&state.globals.qh, ()); + for mime_type in TEXT_MIME_TYPES { + data_source.offer(mime_type.to_string()); + } + data_source.offer(state.clipboard.self_mime()); + data_device.set_selection(Some(&data_source), serial); + } + } + + fn read_from_primary(&self) -> Option { + self.0.borrow_mut().clipboard.read_primary() + } + + fn read_from_clipboard(&self) -> Option { + self.0.borrow_mut().clipboard.read() + } + + fn active_window(&self) -> Option { + self.0 + .borrow_mut() + .keyboard_focused_window + .as_ref() + .map(|window| window.handle()) + } + + fn window_stack(&self) -> Option> { + None + } + + fn compositor_name(&self) -> &'static str { + "Wayland" + } + + fn window_identifier(&self) -> impl Future> + Send + 'static { + async fn inner(surface: Option) -> Option { + if let Some(surface) = surface { + ashpd::WindowIdentifier::from_wayland(&surface).await + } else { + None + } + } + + let client_state = self.0.borrow(); + let active_window = client_state.keyboard_focused_window.as_ref(); + inner(active_window.map(|aw| aw.surface())) + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + registry: &wl_registry::WlRegistry, + event: wl_registry::Event, + _: &GlobalListContents, + _: &Connection, + qh: &QueueHandle, + ) { + let mut client = this.get_client(); + let mut state = client.borrow_mut(); + + match event { + wl_registry::Event::Global { + name, + interface, + version, + } => match &interface[..] { + "wl_seat" => { + if let Some(wl_pointer) = state.wl_pointer.take() { + wl_pointer.release(); + } + if let Some(wl_keyboard) = state.wl_keyboard.take() { + wl_keyboard.release(); + } + state.wl_seat.release(); + state.wl_seat = registry.bind::( + name, + wl_seat_version(version), + qh, + (), + ); + } + "wl_output" => { + let output = registry.bind::( + name, + wl_output_version(version), + qh, + (), + ); + + state + .in_progress_outputs + .insert(output.id(), InProgressOutput::default()); + } + _ => {} + }, + wl_registry::Event::GlobalRemove { name: _ } => { + // TODO: handle global removal + } + _ => {} + } + } +} + +delegate_noop!(WaylandClientStatePtr: ignore xdg_activation_v1::XdgActivationV1); +delegate_noop!(WaylandClientStatePtr: ignore wl_compositor::WlCompositor); +delegate_noop!(WaylandClientStatePtr: ignore wp_cursor_shape_device_v1::WpCursorShapeDeviceV1); +delegate_noop!(WaylandClientStatePtr: ignore wp_cursor_shape_manager_v1::WpCursorShapeManagerV1); +delegate_noop!(WaylandClientStatePtr: ignore wl_data_device_manager::WlDataDeviceManager); +delegate_noop!(WaylandClientStatePtr: ignore zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1); +delegate_noop!(WaylandClientStatePtr: ignore wl_shm::WlShm); +delegate_noop!(WaylandClientStatePtr: ignore wl_shm_pool::WlShmPool); +delegate_noop!(WaylandClientStatePtr: ignore wl_buffer::WlBuffer); +delegate_noop!(WaylandClientStatePtr: ignore wl_region::WlRegion); +delegate_noop!(WaylandClientStatePtr: ignore wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1); +delegate_noop!(WaylandClientStatePtr: ignore zxdg_decoration_manager_v1::ZxdgDecorationManagerV1); +delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur_manager::OrgKdeKwinBlurManager); +delegate_noop!(WaylandClientStatePtr: ignore zwp_text_input_manager_v3::ZwpTextInputManagerV3); +delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur::OrgKdeKwinBlur); +delegate_noop!(WaylandClientStatePtr: ignore wp_viewporter::WpViewporter); +delegate_noop!(WaylandClientStatePtr: ignore wp_viewport::WpViewport); + +impl Dispatch for WaylandClientStatePtr { + fn event( + state: &mut WaylandClientStatePtr, + _: &wl_callback::WlCallback, + event: wl_callback::Event, + surface_id: &ObjectId, + _: &Connection, + _: &QueueHandle, + ) { + let client = state.get_client(); + let mut state = client.borrow_mut(); + let Some(window) = get_window(&mut state, surface_id) else { + return; + }; + drop(state); + + if let wl_callback::Event::Done { .. } = event { + window.frame(); + } + } +} + +fn get_window( + mut state: &mut RefMut, + surface_id: &ObjectId, +) -> Option { + state.windows.get(surface_id).cloned() +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + surface: &wl_surface::WlSurface, + event: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let mut client = this.get_client(); + let mut state = client.borrow_mut(); + + let Some(window) = get_window(&mut state, &surface.id()) else { + return; + }; + #[allow(clippy::mutable_key_type)] + let outputs = state.outputs.clone(); + drop(state); + + window.handle_surface_event(event, outputs); + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + output: &wl_output::WlOutput, + event: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let mut client = this.get_client(); + let mut state = client.borrow_mut(); + + let Some(mut in_progress_output) = state.in_progress_outputs.get_mut(&output.id()) else { + return; + }; + + match event { + wl_output::Event::Name { name } => { + in_progress_output.name = Some(name); + } + wl_output::Event::Scale { factor } => { + in_progress_output.scale = Some(factor); + } + wl_output::Event::Geometry { x, y, .. } => { + in_progress_output.position = Some(point(DevicePixels(x), DevicePixels(y))) + } + wl_output::Event::Mode { width, height, .. } => { + in_progress_output.size = Some(size(DevicePixels(width), DevicePixels(height))) + } + wl_output::Event::Done => { + if let Some(complete) = in_progress_output.complete() { + state.outputs.insert(output.id(), complete); + } + state.in_progress_outputs.remove(&output.id()); + } + _ => {} + } + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + state: &mut Self, + _: &xdg_surface::XdgSurface, + event: xdg_surface::Event, + surface_id: &ObjectId, + _: &Connection, + _: &QueueHandle, + ) { + let client = state.get_client(); + let mut state = client.borrow_mut(); + let Some(window) = get_window(&mut state, surface_id) else { + return; + }; + drop(state); + window.handle_xdg_surface_event(event); + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + _: &xdg_toplevel::XdgToplevel, + event: ::Event, + surface_id: &ObjectId, + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + let Some(window) = get_window(&mut state, surface_id) else { + return; + }; + + drop(state); + let should_close = window.handle_toplevel_event(event); + + if should_close { + // The close logic will be handled in drop_window() + window.close(); + } + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + _: &mut Self, + wm_base: &xdg_wm_base::XdgWmBase, + event: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let xdg_wm_base::Event::Ping { serial } = event { + wm_base.pong(serial); + } + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + token: &xdg_activation_token_v1::XdgActivationTokenV1, + event: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + if let xdg_activation_token_v1::Event::Done { token } = event { + let executor = state.common.background_executor.clone(); + match state.pending_activation.take() { + Some(PendingActivation::Uri(uri)) => open_uri_internal(executor, &uri, Some(token)), + Some(PendingActivation::Path(path)) => { + reveal_path_internal(executor, path, Some(token)) + } + Some(PendingActivation::Window(window)) => { + let Some(window) = get_window(&mut state, &window) else { + return; + }; + let activation = state.globals.activation.as_ref().unwrap(); + activation.activate(token, &window.surface()); + } + None => log::error!("activation token received with no pending activation"), + } + } + + token.destroy(); + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + state: &mut Self, + seat: &wl_seat::WlSeat, + event: wl_seat::Event, + _: &(), + _: &Connection, + qh: &QueueHandle, + ) { + if let wl_seat::Event::Capabilities { + capabilities: WEnum::Value(capabilities), + } = event + { + let client = state.get_client(); + let mut state = client.borrow_mut(); + if capabilities.contains(wl_seat::Capability::Keyboard) { + let keyboard = seat.get_keyboard(qh, ()); + + state.text_input = state + .globals + .text_input_manager + .as_ref() + .map(|text_input_manager| text_input_manager.get_text_input(seat, qh, ())); + + if let Some(wl_keyboard) = &state.wl_keyboard { + wl_keyboard.release(); + } + + state.wl_keyboard = Some(keyboard); + } + if capabilities.contains(wl_seat::Capability::Pointer) { + let pointer = seat.get_pointer(qh, ()); + state.cursor_shape_device = state + .globals + .cursor_shape_manager + .as_ref() + .map(|cursor_shape_manager| cursor_shape_manager.get_pointer(&pointer, qh, ())); + + if let Some(wl_pointer) = &state.wl_pointer { + wl_pointer.release(); + } + + state.wl_pointer = Some(pointer); + } + } + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + _: &wl_keyboard::WlKeyboard, + event: wl_keyboard::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let mut client = this.get_client(); + let mut state = client.borrow_mut(); + match event { + wl_keyboard::Event::RepeatInfo { rate, delay } => { + state.repeat.characters_per_second = rate as u32; + state.repeat.delay = Duration::from_millis(delay as u64); + } + wl_keyboard::Event::Keymap { + format: WEnum::Value(format), + fd, + size, + .. + } => { + if format != wl_keyboard::KeymapFormat::XkbV1 { + log::error!("Received keymap format {:?}, expected XkbV1", format); + return; + } + let xkb_context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); + let keymap = unsafe { + xkb::Keymap::new_from_fd( + &xkb_context, + fd, + size as usize, + XKB_KEYMAP_FORMAT_TEXT_V1, + KEYMAP_COMPILE_NO_FLAGS, + ) + .log_err() + .flatten() + .expect("Failed to create keymap") + }; + state.keymap_state = Some(xkb::State::new(&keymap)); + state.compose_state = get_xkb_compose_state(&xkb_context); + drop(state); + + this.handle_keyboard_layout_change(); + } + wl_keyboard::Event::Enter { surface, .. } => { + state.keyboard_focused_window = get_window(&mut state, &surface.id()); + state.enter_token = Some(()); + + if let Some(window) = state.keyboard_focused_window.clone() { + drop(state); + window.set_focused(true); + } + } + wl_keyboard::Event::Leave { surface, .. } => { + let keyboard_focused_window = get_window(&mut state, &surface.id()); + state.keyboard_focused_window = None; + state.enter_token.take(); + // Prevent keyboard events from repeating after opening e.g. a file chooser and closing it quickly + state.repeat.current_id += 1; + + if let Some(window) = keyboard_focused_window { + if let Some(ref mut compose) = state.compose_state { + compose.reset(); + } + state.pre_edit_text.take(); + drop(state); + window.handle_ime(ImeInput::DeleteText); + window.set_focused(false); + } + } + wl_keyboard::Event::Modifiers { + mods_depressed, + mods_latched, + mods_locked, + group, + .. + } => { + let focused_window = state.keyboard_focused_window.clone(); + + let keymap_state = state.keymap_state.as_mut().unwrap(); + let old_layout = + keymap_state.serialize_layout(xkbcommon::xkb::STATE_LAYOUT_EFFECTIVE); + keymap_state.update_mask(mods_depressed, mods_latched, mods_locked, 0, 0, group); + state.modifiers = Modifiers::from_xkb(keymap_state); + let keymap_state = state.keymap_state.as_mut().unwrap(); + state.capslock = Capslock::from_xkb(keymap_state); + + let input = PlatformInput::ModifiersChanged(ModifiersChangedEvent { + modifiers: state.modifiers, + capslock: state.capslock, + }); + drop(state); + + if let Some(focused_window) = focused_window { + focused_window.handle_input(input); + } + + if group != old_layout { + this.handle_keyboard_layout_change(); + } + } + wl_keyboard::Event::Key { + serial, + key, + state: WEnum::Value(key_state), + .. + } => { + state.serial_tracker.update(SerialKind::KeyPress, serial); + + let focused_window = state.keyboard_focused_window.clone(); + let Some(focused_window) = focused_window else { + return; + }; + + let keymap_state = state.keymap_state.as_ref().unwrap(); + let keycode = Keycode::from(key + MIN_KEYCODE); + let keysym = keymap_state.key_get_one_sym(keycode); + + match key_state { + wl_keyboard::KeyState::Pressed if !keysym.is_modifier_key() => { + let mut keystroke = + Keystroke::from_xkb(keymap_state, state.modifiers, keycode); + if let Some(mut compose) = state.compose_state.take() { + compose.feed(keysym); + match compose.status() { + xkb::Status::Composing => { + keystroke.key_char = None; + state.pre_edit_text = + compose.utf8().or(Keystroke::underlying_dead_key(keysym)); + let pre_edit = + state.pre_edit_text.clone().unwrap_or(String::default()); + drop(state); + focused_window.handle_ime(ImeInput::SetMarkedText(pre_edit)); + state = client.borrow_mut(); + } + + xkb::Status::Composed => { + state.pre_edit_text.take(); + keystroke.key_char = compose.utf8(); + if let Some(keysym) = compose.keysym() { + keystroke.key = xkb::keysym_get_name(keysym); + } + } + xkb::Status::Cancelled => { + let pre_edit = state.pre_edit_text.take(); + let new_pre_edit = Keystroke::underlying_dead_key(keysym); + state.pre_edit_text = new_pre_edit.clone(); + drop(state); + if let Some(pre_edit) = pre_edit { + focused_window.handle_ime(ImeInput::InsertText(pre_edit)); + } + if let Some(current_key) = new_pre_edit { + focused_window + .handle_ime(ImeInput::SetMarkedText(current_key)); + } + compose.feed(keysym); + state = client.borrow_mut(); + } + _ => {} + } + state.compose_state = Some(compose); + } + let input = PlatformInput::KeyDown(KeyDownEvent { + keystroke: keystroke.clone(), + is_held: false, + }); + + state.repeat.current_id += 1; + state.repeat.current_keycode = Some(keycode); + + let rate = state.repeat.characters_per_second; + let id = state.repeat.current_id; + state + .loop_handle + .insert_source(Timer::from_duration(state.repeat.delay), { + let input = PlatformInput::KeyDown(KeyDownEvent { + keystroke, + is_held: true, + }); + move |_event, _metadata, this| { + let mut client = this.get_client(); + let mut state = client.borrow_mut(); + let is_repeating = id == state.repeat.current_id + && state.repeat.current_keycode.is_some() + && state.keyboard_focused_window.is_some(); + + if !is_repeating || rate == 0 { + return TimeoutAction::Drop; + } + + let focused_window = + state.keyboard_focused_window.as_ref().unwrap().clone(); + + drop(state); + focused_window.handle_input(input.clone()); + + TimeoutAction::ToDuration(Duration::from_secs(1) / rate) + } + }) + .unwrap(); + + drop(state); + focused_window.handle_input(input); + } + wl_keyboard::KeyState::Released if !keysym.is_modifier_key() => { + let input = PlatformInput::KeyUp(KeyUpEvent { + keystroke: Keystroke::from_xkb(keymap_state, state.modifiers, keycode), + }); + + if state.repeat.current_keycode == Some(keycode) { + state.repeat.current_keycode = None; + } + + drop(state); + focused_window.handle_input(input); + } + _ => {} + } + } + _ => {} + } + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + text_input: &zwp_text_input_v3::ZwpTextInputV3, + event: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + match event { + zwp_text_input_v3::Event::Enter { .. } => { + drop(state); + this.enable_ime(); + } + zwp_text_input_v3::Event::Leave { .. } => { + drop(state); + this.disable_ime(); + } + zwp_text_input_v3::Event::CommitString { text } => { + state.composing = false; + let Some(window) = state.keyboard_focused_window.clone() else { + return; + }; + + if let Some(commit_text) = text { + drop(state); + // IBus Intercepts keys like `a`, `b`, but those keys are needed for vim mode. + // We should only send ASCII characters to Zed, otherwise a user could remap a letter like `か` or `相`. + if commit_text.len() == 1 { + window.handle_input(PlatformInput::KeyDown(KeyDownEvent { + keystroke: Keystroke { + modifiers: Modifiers::default(), + key: commit_text.clone(), + key_char: Some(commit_text), + }, + is_held: false, + })); + } else { + window.handle_ime(ImeInput::InsertText(commit_text)); + } + } + } + zwp_text_input_v3::Event::PreeditString { text, .. } => { + state.composing = true; + state.ime_pre_edit = text; + } + zwp_text_input_v3::Event::Done { serial } => { + let last_serial = state.serial_tracker.get(SerialKind::InputMethod); + state.serial_tracker.update(SerialKind::InputMethod, serial); + let Some(window) = state.keyboard_focused_window.clone() else { + return; + }; + + if let Some(text) = state.ime_pre_edit.take() { + drop(state); + window.handle_ime(ImeInput::SetMarkedText(text)); + if let Some(area) = window.get_ime_area() { + text_input.set_cursor_rectangle( + area.origin.x.0 as i32, + area.origin.y.0 as i32, + area.size.width.0 as i32, + area.size.height.0 as i32, + ); + if last_serial == serial { + text_input.commit(); + } + } + } else { + state.composing = false; + drop(state); + window.handle_ime(ImeInput::DeleteText); + } + } + _ => {} + } + } +} + +fn linux_button_to_gpui(button: u32) -> Option { + // These values are coming from . + const BTN_LEFT: u32 = 0x110; + const BTN_RIGHT: u32 = 0x111; + const BTN_MIDDLE: u32 = 0x112; + const BTN_SIDE: u32 = 0x113; + const BTN_EXTRA: u32 = 0x114; + const BTN_FORWARD: u32 = 0x115; + const BTN_BACK: u32 = 0x116; + + Some(match button { + BTN_LEFT => MouseButton::Left, + BTN_RIGHT => MouseButton::Right, + BTN_MIDDLE => MouseButton::Middle, + BTN_BACK | BTN_SIDE => MouseButton::Navigate(NavigationDirection::Back), + BTN_FORWARD | BTN_EXTRA => MouseButton::Navigate(NavigationDirection::Forward), + _ => return None, + }) +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + wl_pointer: &wl_pointer::WlPointer, + event: wl_pointer::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let mut client = this.get_client(); + let mut state = client.borrow_mut(); + + match event { + wl_pointer::Event::Enter { + serial, + surface, + surface_x, + surface_y, + .. + } => { + state.serial_tracker.update(SerialKind::MouseEnter, serial); + state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32))); + state.button_pressed = None; + + if let Some(window) = get_window(&mut state, &surface.id()) { + state.mouse_focused_window = Some(window.clone()); + + if state.enter_token.is_some() { + state.enter_token = None; + } + if let Some(style) = state.cursor_style { + if let CursorStyle::None = style { + let wl_pointer = state + .wl_pointer + .clone() + .expect("window is focused by pointer"); + wl_pointer.set_cursor(serial, None, 0, 0); + } else if let Some(cursor_shape_device) = &state.cursor_shape_device { + cursor_shape_device.set_shape(serial, style.to_shape()); + } else { + let scale = window.primary_output_scale(); + state + .cursor + .set_icon(wl_pointer, serial, style.to_icon_names(), scale); + } + } + drop(state); + window.set_hovered(true); + } + } + wl_pointer::Event::Leave { .. } => { + if let Some(focused_window) = state.mouse_focused_window.clone() { + let input = PlatformInput::MouseExited(MouseExitEvent { + position: state.mouse_location.unwrap(), + pressed_button: state.button_pressed, + modifiers: state.modifiers, + }); + state.mouse_focused_window = None; + state.mouse_location = None; + state.button_pressed = None; + + drop(state); + focused_window.handle_input(input); + focused_window.set_hovered(false); + } + } + wl_pointer::Event::Motion { + surface_x, + surface_y, + .. + } => { + if state.mouse_focused_window.is_none() { + return; + } + state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32))); + + if let Some(window) = state.mouse_focused_window.clone() { + if state + .keyboard_focused_window + .as_ref() + .is_some_and(|keyboard_window| window.ptr_eq(keyboard_window)) + { + state.enter_token = None; + } + let input = PlatformInput::MouseMove(MouseMoveEvent { + position: state.mouse_location.unwrap(), + pressed_button: state.button_pressed, + modifiers: state.modifiers, + }); + drop(state); + window.handle_input(input); + } + } + wl_pointer::Event::Button { + serial, + button, + state: WEnum::Value(button_state), + .. + } => { + state.serial_tracker.update(SerialKind::MousePress, serial); + let button = linux_button_to_gpui(button); + let Some(button) = button else { return }; + if state.mouse_focused_window.is_none() { + return; + } + match button_state { + wl_pointer::ButtonState::Pressed => { + if let Some(window) = state.keyboard_focused_window.clone() { + if state.composing && state.text_input.is_some() { + drop(state); + // text_input_v3 don't have something like a reset function + this.disable_ime(); + this.enable_ime(); + window.handle_ime(ImeInput::UnmarkText); + state = client.borrow_mut(); + } else if let (Some(text), Some(compose)) = + (state.pre_edit_text.take(), state.compose_state.as_mut()) + { + compose.reset(); + drop(state); + window.handle_ime(ImeInput::InsertText(text)); + state = client.borrow_mut(); + } + } + let click_elapsed = state.click.last_click.elapsed(); + + if click_elapsed < DOUBLE_CLICK_INTERVAL + && state + .click + .last_mouse_button + .is_some_and(|prev_button| prev_button == button) + && is_within_click_distance( + state.click.last_location, + state.mouse_location.unwrap(), + ) + { + state.click.current_count += 1; + } else { + state.click.current_count = 1; + } + + state.click.last_click = Instant::now(); + state.click.last_mouse_button = Some(button); + state.click.last_location = state.mouse_location.unwrap(); + + state.button_pressed = Some(button); + + if let Some(window) = state.mouse_focused_window.clone() { + let input = PlatformInput::MouseDown(MouseDownEvent { + button, + position: state.mouse_location.unwrap(), + modifiers: state.modifiers, + click_count: state.click.current_count, + first_mouse: state.enter_token.take().is_some(), + }); + drop(state); + window.handle_input(input); + } + } + wl_pointer::ButtonState::Released => { + state.button_pressed = None; + + if let Some(window) = state.mouse_focused_window.clone() { + let input = PlatformInput::MouseUp(MouseUpEvent { + button, + position: state.mouse_location.unwrap(), + modifiers: state.modifiers, + click_count: state.click.current_count, + }); + drop(state); + window.handle_input(input); + } + } + _ => {} + } + } + + // Axis Events + wl_pointer::Event::AxisSource { + axis_source: WEnum::Value(axis_source), + } => { + state.axis_source = axis_source; + } + wl_pointer::Event::Axis { + axis: WEnum::Value(axis), + value, + .. + } => { + if state.axis_source == AxisSource::Wheel { + return; + } + let axis = if state.modifiers.shift { + wl_pointer::Axis::HorizontalScroll + } else { + axis + }; + let axis_modifier = match axis { + wl_pointer::Axis::VerticalScroll => state.vertical_modifier, + wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier, + _ => 1.0, + }; + state.scroll_event_received = true; + let scroll_delta = state + .continuous_scroll_delta + .get_or_insert(point(px(0.0), px(0.0))); + let modifier = 3.0; + match axis { + wl_pointer::Axis::VerticalScroll => { + scroll_delta.y += px(value as f32 * modifier * axis_modifier); + } + wl_pointer::Axis::HorizontalScroll => { + scroll_delta.x += px(value as f32 * modifier * axis_modifier); + } + _ => unreachable!(), + } + } + wl_pointer::Event::AxisDiscrete { + axis: WEnum::Value(axis), + discrete, + } => { + state.scroll_event_received = true; + let axis = if state.modifiers.shift { + wl_pointer::Axis::HorizontalScroll + } else { + axis + }; + let axis_modifier = match axis { + wl_pointer::Axis::VerticalScroll => state.vertical_modifier, + wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier, + _ => 1.0, + }; + + let scroll_delta = state.discrete_scroll_delta.get_or_insert(point(0.0, 0.0)); + match axis { + wl_pointer::Axis::VerticalScroll => { + scroll_delta.y += discrete as f32 * axis_modifier * SCROLL_LINES; + } + wl_pointer::Axis::HorizontalScroll => { + scroll_delta.x += discrete as f32 * axis_modifier * SCROLL_LINES; + } + _ => unreachable!(), + } + } + wl_pointer::Event::AxisValue120 { + axis: WEnum::Value(axis), + value120, + } => { + state.scroll_event_received = true; + let axis = if state.modifiers.shift { + wl_pointer::Axis::HorizontalScroll + } else { + axis + }; + let axis_modifier = match axis { + wl_pointer::Axis::VerticalScroll => state.vertical_modifier, + wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier, + _ => unreachable!(), + }; + + let scroll_delta = state.discrete_scroll_delta.get_or_insert(point(0.0, 0.0)); + let wheel_percent = value120 as f32 / 120.0; + match axis { + wl_pointer::Axis::VerticalScroll => { + scroll_delta.y += wheel_percent * axis_modifier * SCROLL_LINES; + } + wl_pointer::Axis::HorizontalScroll => { + scroll_delta.x += wheel_percent * axis_modifier * SCROLL_LINES; + } + _ => unreachable!(), + } + } + wl_pointer::Event::Frame => { + if state.scroll_event_received { + state.scroll_event_received = false; + let continuous = state.continuous_scroll_delta.take(); + let discrete = state.discrete_scroll_delta.take(); + if let Some(continuous) = continuous { + if let Some(window) = state.mouse_focused_window.clone() { + let input = PlatformInput::ScrollWheel(ScrollWheelEvent { + position: state.mouse_location.unwrap(), + delta: ScrollDelta::Pixels(continuous), + modifiers: state.modifiers, + touch_phase: TouchPhase::Moved, + }); + drop(state); + window.handle_input(input); + } + } else if let Some(discrete) = discrete + && let Some(window) = state.mouse_focused_window.clone() + { + let input = PlatformInput::ScrollWheel(ScrollWheelEvent { + position: state.mouse_location.unwrap(), + delta: ScrollDelta::Lines(discrete), + modifiers: state.modifiers, + touch_phase: TouchPhase::Moved, + }); + drop(state); + window.handle_input(input); + } + } + } + _ => {} + } + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + _: &wp_fractional_scale_v1::WpFractionalScaleV1, + event: ::Event, + surface_id: &ObjectId, + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + let Some(window) = get_window(&mut state, surface_id) else { + return; + }; + + drop(state); + window.handle_fractional_scale_event(event); + } +} + +impl Dispatch + for WaylandClientStatePtr +{ + fn event( + this: &mut Self, + _: &zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1, + event: zxdg_toplevel_decoration_v1::Event, + surface_id: &ObjectId, + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + let Some(window) = get_window(&mut state, surface_id) else { + return; + }; + + drop(state); + window.handle_toplevel_decoration_event(event); + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + _: &wl_data_device::WlDataDevice, + event: wl_data_device::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + match event { + // Clipboard + wl_data_device::Event::DataOffer { id: data_offer } => { + state.data_offers.push(DataOffer::new(data_offer)); + if state.data_offers.len() > 2 { + // At most we store a clipboard offer and a drag and drop offer. + state.data_offers.remove(0).inner.destroy(); + } + } + wl_data_device::Event::Selection { id: data_offer } => { + if let Some(offer) = data_offer { + let offer = state + .data_offers + .iter() + .find(|wrapper| wrapper.inner.id() == offer.id()); + let offer = offer.cloned(); + state.clipboard.set_offer(offer); + } else { + state.clipboard.set_offer(None); + } + } + + // Drag and drop + wl_data_device::Event::Enter { + serial, + surface, + x, + y, + id: data_offer, + } => { + state.serial_tracker.update(SerialKind::DataDevice, serial); + if let Some(data_offer) = data_offer { + let Some(drag_window) = get_window(&mut state, &surface.id()) else { + return; + }; + + const ACTIONS: DndAction = DndAction::Copy; + data_offer.set_actions(ACTIONS, ACTIONS); + + let pipe = Pipe::new().unwrap(); + data_offer.receive(FILE_LIST_MIME_TYPE.to_string(), unsafe { + BorrowedFd::borrow_raw(pipe.write.as_raw_fd()) + }); + let fd = pipe.read; + drop(pipe.write); + + let read_task = state.common.background_executor.spawn(async { + let buffer = unsafe { read_fd(fd)? }; + let text = String::from_utf8(buffer)?; + anyhow::Ok(text) + }); + + let this = this.clone(); + state + .common + .foreground_executor + .spawn(async move { + let file_list = match read_task.await { + Ok(list) => list, + Err(err) => { + log::error!("error reading drag and drop pipe: {err:?}"); + return; + } + }; + + let paths: SmallVec<[_; 2]> = file_list + .lines() + .filter_map(|path| Url::parse(path).log_err()) + .filter_map(|url| url.to_file_path().log_err()) + .collect(); + let position = Point::new(x.into(), y.into()); + + // Prevent dropping text from other programs. + if paths.is_empty() { + data_offer.destroy(); + return; + } + + let input = PlatformInput::FileDrop(FileDropEvent::Entered { + position, + paths: crate::ExternalPaths(paths), + }); + + let client = this.get_client(); + let mut state = client.borrow_mut(); + state.drag.data_offer = Some(data_offer); + state.drag.window = Some(drag_window.clone()); + state.drag.position = position; + + drop(state); + drag_window.handle_input(input); + }) + .detach(); + } + } + wl_data_device::Event::Motion { x, y, .. } => { + let Some(drag_window) = state.drag.window.clone() else { + return; + }; + let position = Point::new(x.into(), y.into()); + state.drag.position = position; + + let input = PlatformInput::FileDrop(FileDropEvent::Pending { position }); + drop(state); + drag_window.handle_input(input); + } + wl_data_device::Event::Leave => { + let Some(drag_window) = state.drag.window.clone() else { + return; + }; + let data_offer = state.drag.data_offer.clone().unwrap(); + data_offer.destroy(); + + state.drag.data_offer = None; + state.drag.window = None; + + let input = PlatformInput::FileDrop(FileDropEvent::Exited {}); + drop(state); + drag_window.handle_input(input); + } + wl_data_device::Event::Drop => { + let Some(drag_window) = state.drag.window.clone() else { + return; + }; + let data_offer = state.drag.data_offer.clone().unwrap(); + data_offer.finish(); + data_offer.destroy(); + + state.drag.data_offer = None; + state.drag.window = None; + + let input = PlatformInput::FileDrop(FileDropEvent::Submit { + position: state.drag.position, + }); + drop(state); + drag_window.handle_input(input); + } + _ => {} + } + } + + event_created_child!(WaylandClientStatePtr, wl_data_device::WlDataDevice, [ + wl_data_device::EVT_DATA_OFFER_OPCODE => (wl_data_offer::WlDataOffer, ()), + ]); +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + data_offer: &wl_data_offer::WlDataOffer, + event: wl_data_offer::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + if let wl_data_offer::Event::Offer { mime_type } = event { + // Drag and drop + if mime_type == FILE_LIST_MIME_TYPE { + let serial = state.serial_tracker.get(SerialKind::DataDevice); + let mime_type = mime_type.clone(); + data_offer.accept(serial, Some(mime_type)); + } + + // Clipboard + if let Some(offer) = state + .data_offers + .iter_mut() + .find(|wrapper| wrapper.inner.id() == data_offer.id()) + { + offer.add_mime_type(mime_type); + } + } + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + data_source: &wl_data_source::WlDataSource, + event: wl_data_source::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + match event { + wl_data_source::Event::Send { mime_type, fd } => { + state.clipboard.send(mime_type, fd); + } + wl_data_source::Event::Cancelled => { + data_source.destroy(); + } + _ => {} + } + } +} + +impl Dispatch + for WaylandClientStatePtr +{ + fn event( + this: &mut Self, + _: &zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1, + event: zwp_primary_selection_device_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + match event { + zwp_primary_selection_device_v1::Event::DataOffer { offer } => { + let old_offer = state.primary_data_offer.replace(DataOffer::new(offer)); + if let Some(old_offer) = old_offer { + old_offer.inner.destroy(); + } + } + zwp_primary_selection_device_v1::Event::Selection { id: data_offer } => { + if data_offer.is_some() { + let offer = state.primary_data_offer.clone(); + state.clipboard.set_primary_offer(offer); + } else { + state.clipboard.set_primary_offer(None); + } + } + _ => {} + } + } + + event_created_child!(WaylandClientStatePtr, zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1, [ + zwp_primary_selection_device_v1::EVT_DATA_OFFER_OPCODE => (zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1, ()), + ]); +} + +impl Dispatch + for WaylandClientStatePtr +{ + fn event( + this: &mut Self, + _data_offer: &zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1, + event: zwp_primary_selection_offer_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + if let zwp_primary_selection_offer_v1::Event::Offer { mime_type } = event + && let Some(offer) = state.primary_data_offer.as_mut() + { + offer.add_mime_type(mime_type); + } + } +} + +impl Dispatch + for WaylandClientStatePtr +{ + fn event( + this: &mut Self, + selection_source: &zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1, + event: zwp_primary_selection_source_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + match event { + zwp_primary_selection_source_v1::Event::Send { mime_type, fd } => { + state.clipboard.send_primary(mime_type, fd); + } + zwp_primary_selection_source_v1::Event::Cancelled => { + selection_source.destroy(); + } + _ => {} + } + } +} diff --git a/third_party/gpui/src/platform/linux/wayland/clipboard.rs b/third_party/gpui/src/platform/linux/wayland/clipboard.rs new file mode 100644 index 0000000..9d58ad7 --- /dev/null +++ b/third_party/gpui/src/platform/linux/wayland/clipboard.rs @@ -0,0 +1,262 @@ +use std::{ + fs::File, + io::{ErrorKind, Write}, + os::fd::{AsRawFd, BorrowedFd, OwnedFd}, +}; + +use calloop::{LoopHandle, PostAction}; +use filedescriptor::Pipe; +use strum::IntoEnumIterator; +use wayland_client::{Connection, protocol::wl_data_offer::WlDataOffer}; +use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1; + +use crate::{ + ClipboardEntry, ClipboardItem, Image, ImageFormat, WaylandClientStatePtr, hash, + platform::linux::platform::read_fd, +}; + +/// Text mime types that we'll offer to other programs. +pub(crate) const TEXT_MIME_TYPES: [&str; 3] = + ["text/plain;charset=utf-8", "UTF8_STRING", "text/plain"]; +pub(crate) const FILE_LIST_MIME_TYPE: &str = "text/uri-list"; + +/// Text mime types that we'll accept from other programs. +pub(crate) const ALLOWED_TEXT_MIME_TYPES: [&str; 2] = ["text/plain;charset=utf-8", "UTF8_STRING"]; + +pub(crate) struct Clipboard { + connection: Connection, + loop_handle: LoopHandle<'static, WaylandClientStatePtr>, + self_mime: String, + + // Internal clipboard + contents: Option, + primary_contents: Option, + + // External clipboard + cached_read: Option, + current_offer: Option>, + cached_primary_read: Option, + current_primary_offer: Option>, +} + +pub(crate) trait ReceiveData { + fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>); +} + +impl ReceiveData for WlDataOffer { + fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>) { + self.receive(mime_type, fd); + } +} + +impl ReceiveData for ZwpPrimarySelectionOfferV1 { + fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>) { + self.receive(mime_type, fd); + } +} + +#[derive(Clone, Debug)] +/// Wrapper for `WlDataOffer` and `ZwpPrimarySelectionOfferV1`, used to help track mime types. +pub(crate) struct DataOffer { + pub inner: T, + mime_types: Vec, +} + +impl DataOffer { + pub fn new(offer: T) -> Self { + Self { + inner: offer, + mime_types: Vec::new(), + } + } + + pub fn add_mime_type(&mut self, mime_type: String) { + self.mime_types.push(mime_type) + } + + fn has_mime_type(&self, mime_type: &str) -> bool { + self.mime_types.iter().any(|t| t == mime_type) + } + + fn read_bytes(&self, connection: &Connection, mime_type: &str) -> Option> { + let pipe = Pipe::new().unwrap(); + self.inner.receive_data(mime_type.to_string(), unsafe { + BorrowedFd::borrow_raw(pipe.write.as_raw_fd()) + }); + let fd = pipe.read; + drop(pipe.write); + + connection.flush().unwrap(); + + match unsafe { read_fd(fd) } { + Ok(bytes) => Some(bytes), + Err(err) => { + log::error!("error reading clipboard pipe: {err:?}"); + None + } + } + } + + fn read_text(&self, connection: &Connection) -> Option { + let mime_type = self.mime_types.iter().find(|&mime_type| { + ALLOWED_TEXT_MIME_TYPES + .iter() + .any(|&allowed| allowed == mime_type) + })?; + let bytes = self.read_bytes(connection, mime_type)?; + let text_content = match String::from_utf8(bytes) { + Ok(content) => content, + Err(e) => { + log::error!("Failed to convert clipboard content to UTF-8: {}", e); + return None; + } + }; + + // Normalize the text to unix line endings, otherwise + // copying from eg: firefox inserts a lot of blank + // lines, and that is super annoying. + let result = text_content.replace("\r\n", "\n"); + Some(ClipboardItem::new_string(result)) + } + + fn read_image(&self, connection: &Connection) -> Option { + for format in ImageFormat::iter() { + let mime_type = format.mime_type(); + if !self.has_mime_type(mime_type) { + continue; + } + + if let Some(bytes) = self.read_bytes(connection, mime_type) { + let id = hash(&bytes); + return Some(ClipboardItem { + entries: vec![ClipboardEntry::Image(Image { format, bytes, id })], + }); + } + } + None + } +} + +impl Clipboard { + pub fn new( + connection: Connection, + loop_handle: LoopHandle<'static, WaylandClientStatePtr>, + ) -> Self { + Self { + connection, + loop_handle, + self_mime: format!("pid/{}", std::process::id()), + + contents: None, + primary_contents: None, + + cached_read: None, + current_offer: None, + cached_primary_read: None, + current_primary_offer: None, + } + } + + pub fn set(&mut self, item: ClipboardItem) { + self.contents = Some(item); + } + + pub fn set_primary(&mut self, item: ClipboardItem) { + self.primary_contents = Some(item); + } + + pub fn set_offer(&mut self, data_offer: Option>) { + self.cached_read = None; + self.current_offer = data_offer; + } + + pub fn set_primary_offer(&mut self, data_offer: Option>) { + self.cached_primary_read = None; + self.current_primary_offer = data_offer; + } + + pub fn self_mime(&self) -> String { + self.self_mime.clone() + } + + pub fn send(&self, _mime_type: String, fd: OwnedFd) { + if let Some(text) = self.contents.as_ref().and_then(|contents| contents.text()) { + self.send_internal(fd, text.as_bytes().to_owned()); + } + } + + pub fn send_primary(&self, _mime_type: String, fd: OwnedFd) { + if let Some(text) = self + .primary_contents + .as_ref() + .and_then(|contents| contents.text()) + { + self.send_internal(fd, text.as_bytes().to_owned()); + } + } + + pub fn read(&mut self) -> Option { + let offer = self.current_offer.as_ref()?; + if let Some(cached) = self.cached_read.clone() { + return Some(cached); + } + + if offer.has_mime_type(&self.self_mime) { + return self.contents.clone(); + } + + let item = offer + .read_text(&self.connection) + .or_else(|| offer.read_image(&self.connection))?; + + self.cached_read = Some(item.clone()); + Some(item) + } + + pub fn read_primary(&mut self) -> Option { + let offer = self.current_primary_offer.as_ref()?; + if let Some(cached) = self.cached_primary_read.clone() { + return Some(cached); + } + + if offer.has_mime_type(&self.self_mime) { + return self.primary_contents.clone(); + } + + let item = offer + .read_text(&self.connection) + .or_else(|| offer.read_image(&self.connection))?; + + self.cached_primary_read = Some(item.clone()); + Some(item) + } + + fn send_internal(&self, fd: OwnedFd, bytes: Vec) { + let mut written = 0; + self.loop_handle + .insert_source( + calloop::generic::Generic::new( + File::from(fd), + calloop::Interest::WRITE, + calloop::Mode::Level, + ), + move |_, file, _| { + let mut file = unsafe { file.get_mut() }; + loop { + match file.write(&bytes[written..]) { + Ok(n) if written + n == bytes.len() => { + written += n; + break Ok(PostAction::Remove); + } + Ok(n) => written += n, + Err(err) if err.kind() == ErrorKind::WouldBlock => { + break Ok(PostAction::Continue); + } + Err(_) => break Ok(PostAction::Remove), + } + } + }, + ) + .unwrap(); + } +} diff --git a/third_party/gpui/src/platform/linux/wayland/cursor.rs b/third_party/gpui/src/platform/linux/wayland/cursor.rs new file mode 100644 index 0000000..c7c9139 --- /dev/null +++ b/third_party/gpui/src/platform/linux/wayland/cursor.rs @@ -0,0 +1,152 @@ +use crate::Globals; +use crate::platform::linux::{DEFAULT_CURSOR_ICON_NAME, log_cursor_icon_warning}; +use anyhow::{Context as _, anyhow}; +use util::ResultExt; + +use wayland_client::Connection; +use wayland_client::protocol::wl_surface::WlSurface; +use wayland_client::protocol::{wl_pointer::WlPointer, wl_shm::WlShm}; +use wayland_cursor::{CursorImageBuffer, CursorTheme}; + +pub(crate) struct Cursor { + loaded_theme: Option, + size: u32, + scaled_size: u32, + surface: WlSurface, + shm: WlShm, + connection: Connection, +} + +pub(crate) struct LoadedTheme { + theme: CursorTheme, + name: Option, + scaled_size: u32, +} + +impl Drop for Cursor { + fn drop(&mut self) { + self.loaded_theme.take(); + self.surface.destroy(); + } +} + +impl Cursor { + pub fn new(connection: &Connection, globals: &Globals, size: u32) -> Self { + let mut this = Self { + loaded_theme: None, + size, + scaled_size: size, + surface: globals.compositor.create_surface(&globals.qh, ()), + shm: globals.shm.clone(), + connection: connection.clone(), + }; + this.set_theme_internal(None); + this + } + + fn set_theme_internal(&mut self, theme_name: Option) { + if let Some(loaded_theme) = self.loaded_theme.as_ref() + && loaded_theme.name == theme_name + && loaded_theme.scaled_size == self.scaled_size + { + return; + } + let result = if let Some(theme_name) = theme_name.as_ref() { + CursorTheme::load_from_name( + &self.connection, + self.shm.clone(), + theme_name, + self.scaled_size, + ) + } else { + CursorTheme::load(&self.connection, self.shm.clone(), self.scaled_size) + }; + if let Some(theme) = result + .context("Wayland: Failed to load cursor theme") + .log_err() + { + self.loaded_theme = Some(LoadedTheme { + theme, + name: theme_name, + scaled_size: self.scaled_size, + }); + } + } + + pub fn set_theme(&mut self, theme_name: String) { + self.set_theme_internal(Some(theme_name)); + } + + fn set_scaled_size(&mut self, scaled_size: u32) { + self.scaled_size = scaled_size; + let theme_name = self + .loaded_theme + .as_ref() + .and_then(|loaded_theme| loaded_theme.name.clone()); + self.set_theme_internal(theme_name); + } + + pub fn set_size(&mut self, size: u32) { + self.size = size; + self.set_scaled_size(size); + } + + pub fn set_icon( + &mut self, + wl_pointer: &WlPointer, + serial_id: u32, + mut cursor_icon_names: &[&str], + scale: i32, + ) { + self.set_scaled_size(self.size * scale as u32); + + let Some(loaded_theme) = &mut self.loaded_theme else { + log::warn!("Wayland: Unable to load cursor themes"); + return; + }; + let mut theme = &mut loaded_theme.theme; + + let mut buffer: &CursorImageBuffer; + 'outer: { + for cursor_icon_name in cursor_icon_names { + if let Some(cursor) = theme.get_cursor(cursor_icon_name) { + buffer = &cursor[0]; + break 'outer; + } + } + + if let Some(cursor) = theme.get_cursor(DEFAULT_CURSOR_ICON_NAME) { + buffer = &cursor[0]; + log_cursor_icon_warning(anyhow!( + "wayland: Unable to get cursor icon {:?}. \ + Using default cursor icon: '{}'", + cursor_icon_names, + DEFAULT_CURSOR_ICON_NAME + )); + } else { + log_cursor_icon_warning(anyhow!( + "wayland: Unable to fallback on default cursor icon '{}' for theme '{}'", + DEFAULT_CURSOR_ICON_NAME, + loaded_theme.name.as_deref().unwrap_or("default") + )); + return; + } + } + + let (width, height) = buffer.dimensions(); + let (hot_x, hot_y) = buffer.hotspot(); + + self.surface.set_buffer_scale(scale); + + wl_pointer.set_cursor( + serial_id, + Some(&self.surface), + hot_x as i32 / scale, + hot_y as i32 / scale, + ); + + self.surface.attach(Some(buffer), 0, 0); + self.surface.damage(0, 0, width as i32, height as i32); + self.surface.commit(); + } +} diff --git a/third_party/gpui/src/platform/linux/wayland/display.rs b/third_party/gpui/src/platform/linux/wayland/display.rs new file mode 100644 index 0000000..c3d2fc9 --- /dev/null +++ b/third_party/gpui/src/platform/linux/wayland/display.rs @@ -0,0 +1,42 @@ +use std::{ + fmt::Debug, + hash::{Hash, Hasher}, +}; + +use anyhow::Context as _; +use uuid::Uuid; +use wayland_backend::client::ObjectId; + +use crate::{Bounds, DisplayId, Pixels, PlatformDisplay}; + +#[derive(Debug, Clone)] +pub(crate) struct WaylandDisplay { + /// The ID of the wl_output object + pub id: ObjectId, + pub name: Option, + pub bounds: Bounds, +} + +impl Hash for WaylandDisplay { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +impl PlatformDisplay for WaylandDisplay { + fn id(&self) -> DisplayId { + DisplayId(self.id.protocol_id()) + } + + fn uuid(&self) -> anyhow::Result { + let name = self + .name + .as_ref() + .context("Wayland display does not have a name")?; + Ok(Uuid::new_v5(&Uuid::NAMESPACE_DNS, name.as_bytes())) + } + + fn bounds(&self) -> Bounds { + self.bounds + } +} diff --git a/third_party/gpui/src/platform/linux/wayland/serial.rs b/third_party/gpui/src/platform/linux/wayland/serial.rs new file mode 100644 index 0000000..eadc7a9 --- /dev/null +++ b/third_party/gpui/src/platform/linux/wayland/serial.rs @@ -0,0 +1,49 @@ +use collections::HashMap; + +#[derive(Debug, Hash, PartialEq, Eq)] +pub(crate) enum SerialKind { + DataDevice, + InputMethod, + MouseEnter, + MousePress, + KeyPress, +} + +#[derive(Debug)] +struct SerialData { + serial: u32, +} + +impl SerialData { + fn new(value: u32) -> Self { + Self { serial: value } + } +} + +#[derive(Debug)] +/// Helper for tracking of different serial kinds. +pub(crate) struct SerialTracker { + serials: HashMap, +} + +impl SerialTracker { + pub fn new() -> Self { + Self { + serials: HashMap::default(), + } + } + + pub fn update(&mut self, kind: SerialKind, value: u32) { + self.serials.insert(kind, SerialData::new(value)); + } + + /// Returns the latest tracked serial of the provided [`SerialKind`] + /// + /// Will return 0 if not tracked. + pub fn get(&self, kind: SerialKind) -> u32 { + self.serials + .get(&kind) + .map(|serial_data| serial_data.serial) + .unwrap_or(0) + } +} diff --git a/third_party/gpui/src/platform/linux/wayland/window.rs b/third_party/gpui/src/platform/linux/wayland/window.rs new file mode 100644 index 0000000..aa3b714 --- /dev/null +++ b/third_party/gpui/src/platform/linux/wayland/window.rs @@ -0,0 +1,1219 @@ +use std::{ + cell::{Ref, RefCell, RefMut}, + ffi::c_void, + ptr::NonNull, + rc::Rc, + sync::Arc, +}; + +use blade_graphics as gpu; +use collections::HashMap; +use futures::channel::oneshot::Receiver; + +use raw_window_handle as rwh; +use wayland_backend::client::ObjectId; +use wayland_client::WEnum; +use wayland_client::{Proxy, protocol::wl_surface}; +use wayland_protocols::wp::viewporter::client::wp_viewport; +use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1; +use wayland_protocols::xdg::shell::client::xdg_surface; +use wayland_protocols::xdg::shell::client::xdg_toplevel::{self}; +use wayland_protocols::{ + wp::fractional_scale::v1::client::wp_fractional_scale_v1, + xdg::shell::client::xdg_toplevel::XdgToplevel, +}; +use wayland_protocols_plasma::blur::client::org_kde_kwin_blur; + +use crate::{ + AnyWindowHandle, Bounds, Decorations, Globals, GpuSpecs, Modifiers, Output, Pixels, + PlatformDisplay, PlatformInput, Point, PromptButton, PromptLevel, RequestFrameOptions, + ResizeEdge, Size, Tiling, WaylandClientStatePtr, WindowAppearance, WindowBackgroundAppearance, + WindowBounds, WindowControlArea, WindowControls, WindowDecorations, WindowParams, px, size, +}; +use crate::{ + Capslock, + platform::{ + PlatformAtlas, PlatformInputHandler, PlatformWindow, + blade::{BladeContext, BladeRenderer, BladeSurfaceConfig}, + linux::wayland::{display::WaylandDisplay, serial::SerialKind}, + }, +}; +use crate::{WindowKind, scene::Scene}; + +#[derive(Default)] +pub(crate) struct Callbacks { + request_frame: Option>, + input: Option crate::DispatchEventResult>>, + active_status_change: Option>, + hover_status_change: Option>, + resize: Option, f32)>>, + moved: Option>, + should_close: Option bool>>, + close: Option>, + appearance_changed: Option>, +} + +struct RawWindow { + window: *mut c_void, + display: *mut c_void, +} + +impl rwh::HasWindowHandle for RawWindow { + fn window_handle(&self) -> Result, rwh::HandleError> { + let window = NonNull::new(self.window).unwrap(); + let handle = rwh::WaylandWindowHandle::new(window); + Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) }) + } +} +impl rwh::HasDisplayHandle for RawWindow { + fn display_handle(&self) -> Result, rwh::HandleError> { + let display = NonNull::new(self.display).unwrap(); + let handle = rwh::WaylandDisplayHandle::new(display); + Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) }) + } +} + +#[derive(Debug)] +struct InProgressConfigure { + size: Option>, + fullscreen: bool, + maximized: bool, + resizing: bool, + tiling: Tiling, +} + +pub struct WaylandWindowState { + xdg_surface: xdg_surface::XdgSurface, + acknowledged_first_configure: bool, + pub surface: wl_surface::WlSurface, + decoration: Option, + app_id: Option, + appearance: WindowAppearance, + blur: Option, + toplevel: xdg_toplevel::XdgToplevel, + viewport: Option, + outputs: HashMap, + display: Option<(ObjectId, Output)>, + globals: Globals, + renderer: BladeRenderer, + bounds: Bounds, + scale: f32, + input_handler: Option, + decorations: WindowDecorations, + background_appearance: WindowBackgroundAppearance, + fullscreen: bool, + maximized: bool, + tiling: Tiling, + window_bounds: Bounds, + client: WaylandClientStatePtr, + handle: AnyWindowHandle, + active: bool, + hovered: bool, + in_progress_configure: Option, + resize_throttle: bool, + in_progress_window_controls: Option, + window_controls: WindowControls, + client_inset: Option, +} + +#[derive(Clone)] +pub struct WaylandWindowStatePtr { + state: Rc>, + callbacks: Rc>, +} + +impl WaylandWindowState { + pub(crate) fn new( + handle: AnyWindowHandle, + surface: wl_surface::WlSurface, + xdg_surface: xdg_surface::XdgSurface, + toplevel: xdg_toplevel::XdgToplevel, + decoration: Option, + appearance: WindowAppearance, + viewport: Option, + client: WaylandClientStatePtr, + globals: Globals, + gpu_context: &BladeContext, + options: WindowParams, + ) -> anyhow::Result { + let renderer = { + let raw_window = RawWindow { + window: surface.id().as_ptr().cast::(), + display: surface + .backend() + .upgrade() + .unwrap() + .display_ptr() + .cast::(), + }; + let config = BladeSurfaceConfig { + size: gpu::Extent { + width: options.bounds.size.width.0 as u32, + height: options.bounds.size.height.0 as u32, + depth: 1, + }, + transparent: true, + }; + BladeRenderer::new(gpu_context, &raw_window, config)? + }; + + Ok(Self { + xdg_surface, + acknowledged_first_configure: false, + surface, + decoration, + app_id: None, + blur: None, + toplevel, + viewport, + globals, + outputs: HashMap::default(), + display: None, + renderer, + bounds: options.bounds, + scale: 1.0, + input_handler: None, + decorations: WindowDecorations::Client, + background_appearance: WindowBackgroundAppearance::Opaque, + fullscreen: false, + maximized: false, + tiling: Tiling::default(), + window_bounds: options.bounds, + in_progress_configure: None, + resize_throttle: false, + client, + appearance, + handle, + active: false, + hovered: false, + in_progress_window_controls: None, + window_controls: WindowControls::default(), + client_inset: None, + }) + } + + pub fn is_transparent(&self) -> bool { + self.decorations == WindowDecorations::Client + || self.background_appearance != WindowBackgroundAppearance::Opaque + } + + pub fn primary_output_scale(&mut self) -> i32 { + let mut scale = 1; + let mut current_output = self.display.take(); + for (id, output) in self.outputs.iter() { + if let Some((_, output_data)) = ¤t_output { + if output.scale > output_data.scale { + current_output = Some((id.clone(), output.clone())); + } + } else { + current_output = Some((id.clone(), output.clone())); + } + scale = scale.max(output.scale); + } + self.display = current_output; + scale + } + + pub fn inset(&self) -> Pixels { + match self.decorations { + WindowDecorations::Server => px(0.0), + WindowDecorations::Client => self.client_inset.unwrap_or(px(0.0)), + } + } +} + +pub(crate) struct WaylandWindow(pub WaylandWindowStatePtr); +pub enum ImeInput { + InsertText(String), + SetMarkedText(String), + UnmarkText, + DeleteText, +} + +impl Drop for WaylandWindow { + fn drop(&mut self) { + let mut state = self.0.state.borrow_mut(); + let surface_id = state.surface.id(); + let client = state.client.clone(); + + state.renderer.destroy(); + if let Some(decoration) = &state.decoration { + decoration.destroy(); + } + if let Some(blur) = &state.blur { + blur.release(); + } + state.toplevel.destroy(); + if let Some(viewport) = &state.viewport { + viewport.destroy(); + } + state.xdg_surface.destroy(); + state.surface.destroy(); + + let state_ptr = self.0.clone(); + state + .globals + .executor + .spawn(async move { + state_ptr.close(); + client.drop_window(&surface_id) + }) + .detach(); + drop(state); + } +} + +impl WaylandWindow { + fn borrow(&self) -> Ref<'_, WaylandWindowState> { + self.0.state.borrow() + } + + fn borrow_mut(&self) -> RefMut<'_, WaylandWindowState> { + self.0.state.borrow_mut() + } + + pub fn new( + handle: AnyWindowHandle, + globals: Globals, + gpu_context: &BladeContext, + client: WaylandClientStatePtr, + params: WindowParams, + appearance: WindowAppearance, + parent: Option, + ) -> anyhow::Result<(Self, ObjectId)> { + let surface = globals.compositor.create_surface(&globals.qh, ()); + let xdg_surface = globals + .wm_base + .get_xdg_surface(&surface, &globals.qh, surface.id()); + let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id()); + + if params.kind == WindowKind::Floating { + toplevel.set_parent(parent.as_ref()); + } + + if let Some(size) = params.window_min_size { + toplevel.set_min_size(size.width.0 as i32, size.height.0 as i32); + } + + if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() { + fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id()); + } + + // Attempt to set up window decorations based on the requested configuration + let decoration = globals + .decoration_manager + .as_ref() + .map(|decoration_manager| { + decoration_manager.get_toplevel_decoration(&toplevel, &globals.qh, surface.id()) + }); + + let viewport = globals + .viewporter + .as_ref() + .map(|viewporter| viewporter.get_viewport(&surface, &globals.qh, ())); + + let this = Self(WaylandWindowStatePtr { + state: Rc::new(RefCell::new(WaylandWindowState::new( + handle, + surface.clone(), + xdg_surface, + toplevel, + decoration, + appearance, + viewport, + client, + globals, + gpu_context, + params, + )?)), + callbacks: Rc::new(RefCell::new(Callbacks::default())), + }); + + // Kick things off + surface.commit(); + + Ok((this, surface.id())) + } +} + +impl WaylandWindowStatePtr { + pub fn handle(&self) -> AnyWindowHandle { + self.state.borrow().handle + } + + pub fn surface(&self) -> wl_surface::WlSurface { + self.state.borrow().surface.clone() + } + + pub fn toplevel(&self) -> xdg_toplevel::XdgToplevel { + self.state.borrow().toplevel.clone() + } + + pub fn ptr_eq(&self, other: &Self) -> bool { + Rc::ptr_eq(&self.state, &other.state) + } + + pub fn frame(&self) { + let mut state = self.state.borrow_mut(); + state.surface.frame(&state.globals.qh, state.surface.id()); + state.resize_throttle = false; + drop(state); + + let mut cb = self.callbacks.borrow_mut(); + if let Some(fun) = cb.request_frame.as_mut() { + fun(Default::default()); + } + } + + pub fn handle_xdg_surface_event(&self, event: xdg_surface::Event) { + if let xdg_surface::Event::Configure { serial } = event { + { + let mut state = self.state.borrow_mut(); + if let Some(window_controls) = state.in_progress_window_controls.take() { + state.window_controls = window_controls; + + drop(state); + let mut callbacks = self.callbacks.borrow_mut(); + if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() { + appearance_changed(); + } + } + } + { + let mut state = self.state.borrow_mut(); + + if let Some(mut configure) = state.in_progress_configure.take() { + let got_unmaximized = state.maximized && !configure.maximized; + state.fullscreen = configure.fullscreen; + state.maximized = configure.maximized; + state.tiling = configure.tiling; + // Limit interactive resizes to once per vblank + if configure.resizing && state.resize_throttle { + return; + } else if configure.resizing { + state.resize_throttle = true; + } + if !configure.fullscreen && !configure.maximized { + configure.size = if got_unmaximized { + Some(state.window_bounds.size) + } else { + compute_outer_size(state.inset(), configure.size, state.tiling) + }; + if let Some(size) = configure.size { + state.window_bounds = Bounds { + origin: Point::default(), + size, + }; + } + } + drop(state); + if let Some(size) = configure.size { + self.resize(size); + } + } + } + let mut state = self.state.borrow_mut(); + state.xdg_surface.ack_configure(serial); + + let window_geometry = inset_by_tiling( + state.bounds.map_origin(|_| px(0.0)), + state.inset(), + state.tiling, + ) + .map(|v| v.0 as i32) + .map_size(|v| if v <= 0 { 1 } else { v }); + + state.xdg_surface.set_window_geometry( + window_geometry.origin.x, + window_geometry.origin.y, + window_geometry.size.width, + window_geometry.size.height, + ); + + let request_frame_callback = !state.acknowledged_first_configure; + if request_frame_callback { + state.acknowledged_first_configure = true; + drop(state); + self.frame(); + } + } + } + + pub fn handle_toplevel_decoration_event(&self, event: zxdg_toplevel_decoration_v1::Event) { + if let zxdg_toplevel_decoration_v1::Event::Configure { mode } = event { + match mode { + WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ServerSide) => { + self.state.borrow_mut().decorations = WindowDecorations::Server; + if let Some(mut appearance_changed) = + self.callbacks.borrow_mut().appearance_changed.as_mut() + { + appearance_changed(); + } + } + WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ClientSide) => { + self.state.borrow_mut().decorations = WindowDecorations::Client; + // Update background to be transparent + if let Some(mut appearance_changed) = + self.callbacks.borrow_mut().appearance_changed.as_mut() + { + appearance_changed(); + } + } + WEnum::Value(_) => { + log::warn!("Unknown decoration mode"); + } + WEnum::Unknown(v) => { + log::warn!("Unknown decoration mode: {}", v); + } + } + } + } + + pub fn handle_fractional_scale_event(&self, event: wp_fractional_scale_v1::Event) { + if let wp_fractional_scale_v1::Event::PreferredScale { scale } = event { + self.rescale(scale as f32 / 120.0); + } + } + + pub fn handle_toplevel_event(&self, event: xdg_toplevel::Event) -> bool { + match event { + xdg_toplevel::Event::Configure { + width, + height, + states, + } => { + let mut size = if width == 0 || height == 0 { + None + } else { + Some(size(px(width as f32), px(height as f32))) + }; + + let states = extract_states::(&states); + + let mut tiling = Tiling::default(); + let mut fullscreen = false; + let mut maximized = false; + let mut resizing = false; + + for state in states { + match state { + xdg_toplevel::State::Maximized => { + maximized = true; + } + xdg_toplevel::State::Fullscreen => { + fullscreen = true; + } + xdg_toplevel::State::Resizing => resizing = true, + xdg_toplevel::State::TiledTop => { + tiling.top = true; + } + xdg_toplevel::State::TiledLeft => { + tiling.left = true; + } + xdg_toplevel::State::TiledRight => { + tiling.right = true; + } + xdg_toplevel::State::TiledBottom => { + tiling.bottom = true; + } + _ => { + // noop + } + } + } + + if fullscreen || maximized { + tiling = Tiling::tiled(); + } + + let mut state = self.state.borrow_mut(); + state.in_progress_configure = Some(InProgressConfigure { + size, + fullscreen, + maximized, + resizing, + tiling, + }); + + false + } + xdg_toplevel::Event::Close => { + let mut cb = self.callbacks.borrow_mut(); + if let Some(mut should_close) = cb.should_close.take() { + let result = (should_close)(); + cb.should_close = Some(should_close); + if result { + drop(cb); + self.close(); + } + result + } else { + true + } + } + xdg_toplevel::Event::WmCapabilities { capabilities } => { + let mut window_controls = WindowControls::default(); + + let states = extract_states::(&capabilities); + + for state in states { + match state { + xdg_toplevel::WmCapabilities::Maximize => { + window_controls.maximize = true; + } + xdg_toplevel::WmCapabilities::Minimize => { + window_controls.minimize = true; + } + xdg_toplevel::WmCapabilities::Fullscreen => { + window_controls.fullscreen = true; + } + xdg_toplevel::WmCapabilities::WindowMenu => { + window_controls.window_menu = true; + } + _ => {} + } + } + + let mut state = self.state.borrow_mut(); + state.in_progress_window_controls = Some(window_controls); + false + } + _ => false, + } + } + + #[allow(clippy::mutable_key_type)] + pub fn handle_surface_event( + &self, + event: wl_surface::Event, + outputs: HashMap, + ) { + let mut state = self.state.borrow_mut(); + + match event { + wl_surface::Event::Enter { output } => { + let id = output.id(); + + let Some(output) = outputs.get(&id) else { + return; + }; + + state.outputs.insert(id, output.clone()); + + let scale = state.primary_output_scale(); + + // We use `PreferredBufferScale` instead to set the scale if it's available + if state.surface.version() < wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE { + state.surface.set_buffer_scale(scale); + drop(state); + self.rescale(scale as f32); + } + } + wl_surface::Event::Leave { output } => { + state.outputs.remove(&output.id()); + + let scale = state.primary_output_scale(); + + // We use `PreferredBufferScale` instead to set the scale if it's available + if state.surface.version() < wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE { + state.surface.set_buffer_scale(scale); + drop(state); + self.rescale(scale as f32); + } + } + wl_surface::Event::PreferredBufferScale { factor } => { + // We use `WpFractionalScale` instead to set the scale if it's available + if state.globals.fractional_scale_manager.is_none() { + state.surface.set_buffer_scale(factor); + drop(state); + self.rescale(factor as f32); + } + } + _ => {} + } + } + + pub fn handle_ime(&self, ime: ImeInput) { + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + match ime { + ImeInput::InsertText(text) => { + input_handler.replace_text_in_range(None, &text); + } + ImeInput::SetMarkedText(text) => { + input_handler.replace_and_mark_text_in_range(None, &text, None); + } + ImeInput::UnmarkText => { + input_handler.unmark_text(); + } + ImeInput::DeleteText => { + if let Some(marked) = input_handler.marked_text_range() { + input_handler.replace_text_in_range(Some(marked), ""); + } + } + } + self.state.borrow_mut().input_handler = Some(input_handler); + } + } + + pub fn get_ime_area(&self) -> Option> { + let mut state = self.state.borrow_mut(); + let mut bounds: Option> = None; + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + if let Some(selection) = input_handler.marked_text_range() { + bounds = input_handler.bounds_for_range(selection.start..selection.start); + } + self.state.borrow_mut().input_handler = Some(input_handler); + } + bounds + } + + pub fn set_size_and_scale(&self, size: Option>, scale: Option) { + let (size, scale) = { + let mut state = self.state.borrow_mut(); + if size.is_none_or(|size| size == state.bounds.size) + && scale.is_none_or(|scale| scale == state.scale) + { + return; + } + if let Some(size) = size { + state.bounds.size = size; + } + if let Some(scale) = scale { + state.scale = scale; + } + let device_bounds = state.bounds.to_device_pixels(state.scale); + state.renderer.update_drawable_size(device_bounds.size); + (state.bounds.size, state.scale) + }; + + if let Some(ref mut fun) = self.callbacks.borrow_mut().resize { + fun(size, scale); + } + + { + let state = self.state.borrow(); + if let Some(viewport) = &state.viewport { + viewport.set_destination(size.width.0 as i32, size.height.0 as i32); + } + } + } + + pub fn resize(&self, size: Size) { + self.set_size_and_scale(Some(size), None); + } + + pub fn rescale(&self, scale: f32) { + self.set_size_and_scale(None, Some(scale)); + } + + pub fn close(&self) { + let mut callbacks = self.callbacks.borrow_mut(); + if let Some(fun) = callbacks.close.take() { + fun() + } + } + + pub fn handle_input(&self, input: PlatformInput) { + if let Some(ref mut fun) = self.callbacks.borrow_mut().input + && !fun(input.clone()).propagate + { + return; + } + if let PlatformInput::KeyDown(event) = input + && event.keystroke.modifiers.is_subset_of(&Modifiers::shift()) + && let Some(key_char) = &event.keystroke.key_char + { + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + input_handler.replace_text_in_range(None, key_char); + self.state.borrow_mut().input_handler = Some(input_handler); + } + } + } + + pub fn set_focused(&self, focus: bool) { + self.state.borrow_mut().active = focus; + if let Some(ref mut fun) = self.callbacks.borrow_mut().active_status_change { + fun(focus); + } + } + + pub fn set_hovered(&self, focus: bool) { + if let Some(ref mut fun) = self.callbacks.borrow_mut().hover_status_change { + fun(focus); + } + } + + pub fn set_appearance(&mut self, appearance: WindowAppearance) { + self.state.borrow_mut().appearance = appearance; + + let mut callbacks = self.callbacks.borrow_mut(); + if let Some(ref mut fun) = callbacks.appearance_changed { + (fun)() + } + } + + pub fn primary_output_scale(&self) -> i32 { + self.state.borrow_mut().primary_output_scale() + } +} + +fn extract_states<'a, S: TryFrom + 'a>(states: &'a [u8]) -> impl Iterator + 'a +where + >::Error: 'a, +{ + states + .chunks_exact(4) + .flat_map(TryInto::<[u8; 4]>::try_into) + .map(u32::from_ne_bytes) + .flat_map(S::try_from) +} + +impl rwh::HasWindowHandle for WaylandWindow { + fn window_handle(&self) -> Result, rwh::HandleError> { + let surface = self.0.surface().id().as_ptr() as *mut libc::c_void; + let c_ptr = NonNull::new(surface).ok_or(rwh::HandleError::Unavailable)?; + let handle = rwh::WaylandWindowHandle::new(c_ptr); + let raw_handle = rwh::RawWindowHandle::Wayland(handle); + Ok(unsafe { rwh::WindowHandle::borrow_raw(raw_handle) }) + } +} + +impl rwh::HasDisplayHandle for WaylandWindow { + fn display_handle(&self) -> Result, rwh::HandleError> { + let display = self + .0 + .surface() + .backend() + .upgrade() + .ok_or(rwh::HandleError::Unavailable)? + .display_ptr() as *mut libc::c_void; + + let c_ptr = NonNull::new(display).ok_or(rwh::HandleError::Unavailable)?; + let handle = rwh::WaylandDisplayHandle::new(c_ptr); + let raw_handle = rwh::RawDisplayHandle::Wayland(handle); + Ok(unsafe { rwh::DisplayHandle::borrow_raw(raw_handle) }) + } +} + +impl PlatformWindow for WaylandWindow { + fn bounds(&self) -> Bounds { + self.borrow().bounds + } + + fn is_maximized(&self) -> bool { + self.borrow().maximized + } + + fn window_bounds(&self) -> WindowBounds { + let state = self.borrow(); + if state.fullscreen { + WindowBounds::Fullscreen(state.window_bounds) + } else if state.maximized { + WindowBounds::Maximized(state.window_bounds) + } else { + drop(state); + WindowBounds::Windowed(self.bounds()) + } + } + + fn inner_window_bounds(&self) -> WindowBounds { + let state = self.borrow(); + if state.fullscreen { + WindowBounds::Fullscreen(state.window_bounds) + } else if state.maximized { + WindowBounds::Maximized(state.window_bounds) + } else { + let inset = state.inset(); + drop(state); + WindowBounds::Windowed(self.bounds().inset(inset)) + } + } + + fn content_size(&self) -> Size { + self.borrow().bounds.size + } + + fn resize(&mut self, size: Size) { + let state = self.borrow(); + let state_ptr = self.0.clone(); + let dp_size = size.to_device_pixels(self.scale_factor()); + + state.xdg_surface.set_window_geometry( + state.bounds.origin.x.0 as i32, + state.bounds.origin.y.0 as i32, + dp_size.width.0, + dp_size.height.0, + ); + + state + .globals + .executor + .spawn(async move { state_ptr.resize(size) }) + .detach(); + } + + fn scale_factor(&self) -> f32 { + self.borrow().scale + } + + fn appearance(&self) -> WindowAppearance { + self.borrow().appearance + } + + fn display(&self) -> Option> { + let state = self.borrow(); + state.display.as_ref().map(|(id, display)| { + Rc::new(WaylandDisplay { + id: id.clone(), + name: display.name.clone(), + bounds: display.bounds.to_pixels(state.scale), + }) as Rc + }) + } + + fn mouse_position(&self) -> Point { + self.borrow() + .client + .get_client() + .borrow() + .mouse_location + .unwrap_or_default() + } + + fn modifiers(&self) -> Modifiers { + self.borrow().client.get_client().borrow().modifiers + } + + fn capslock(&self) -> Capslock { + self.borrow().client.get_client().borrow().capslock + } + + fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { + self.borrow_mut().input_handler = Some(input_handler); + } + + fn take_input_handler(&mut self) -> Option { + self.borrow_mut().input_handler.take() + } + + fn prompt( + &self, + _level: PromptLevel, + _msg: &str, + _detail: Option<&str>, + _answers: &[PromptButton], + ) -> Option> { + None + } + + fn activate(&self) { + // Try to request an activation token. Even though the activation is likely going to be rejected, + // KWin and Mutter can use the app_id to visually indicate we're requesting attention. + let state = self.borrow(); + if let (Some(activation), Some(app_id)) = (&state.globals.activation, state.app_id.clone()) + { + state.client.set_pending_activation(state.surface.id()); + let token = activation.get_activation_token(&state.globals.qh, ()); + // The serial isn't exactly important here, since the activation is probably going to be rejected anyway. + let serial = state.client.get_serial(SerialKind::MousePress); + token.set_app_id(app_id); + token.set_serial(serial, &state.globals.seat); + token.set_surface(&state.surface); + token.commit(); + } + } + + fn is_active(&self) -> bool { + self.borrow().active + } + + fn is_hovered(&self) -> bool { + self.borrow().hovered + } + + fn set_title(&mut self, title: &str) { + self.borrow().toplevel.set_title(title.to_string()); + } + + fn set_app_id(&mut self, app_id: &str) { + let mut state = self.borrow_mut(); + state.toplevel.set_app_id(app_id.to_owned()); + state.app_id = Some(app_id.to_owned()); + } + + fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { + let mut state = self.borrow_mut(); + state.background_appearance = background_appearance; + update_window(state); + } + + fn minimize(&self) { + self.borrow().toplevel.set_minimized(); + } + + fn zoom(&self) { + let state = self.borrow(); + if !state.maximized { + state.toplevel.set_maximized(); + } else { + state.toplevel.unset_maximized(); + } + } + + fn toggle_fullscreen(&self) { + let mut state = self.borrow_mut(); + if !state.fullscreen { + state.toplevel.set_fullscreen(None); + } else { + state.toplevel.unset_fullscreen(); + } + } + + fn is_fullscreen(&self) -> bool { + self.borrow().fullscreen + } + + fn on_request_frame(&self, callback: Box) { + self.0.callbacks.borrow_mut().request_frame = Some(callback); + } + + fn on_input(&self, callback: Box crate::DispatchEventResult>) { + self.0.callbacks.borrow_mut().input = Some(callback); + } + + fn on_active_status_change(&self, callback: Box) { + self.0.callbacks.borrow_mut().active_status_change = Some(callback); + } + + fn on_hover_status_change(&self, callback: Box) { + self.0.callbacks.borrow_mut().hover_status_change = Some(callback); + } + + fn on_resize(&self, callback: Box, f32)>) { + self.0.callbacks.borrow_mut().resize = Some(callback); + } + + fn on_moved(&self, callback: Box) { + self.0.callbacks.borrow_mut().moved = Some(callback); + } + + fn on_should_close(&self, callback: Box bool>) { + self.0.callbacks.borrow_mut().should_close = Some(callback); + } + + fn on_close(&self, callback: Box) { + self.0.callbacks.borrow_mut().close = Some(callback); + } + + fn on_hit_test_window_control(&self, _callback: Box Option>) { + } + + fn on_appearance_changed(&self, callback: Box) { + self.0.callbacks.borrow_mut().appearance_changed = Some(callback); + } + + fn draw(&self, scene: &Scene) { + let mut state = self.borrow_mut(); + state.renderer.draw(scene); + } + + fn completed_frame(&self) { + let state = self.borrow(); + state.surface.commit(); + } + + fn sprite_atlas(&self) -> Arc { + let state = self.borrow(); + state.renderer.sprite_atlas().clone() + } + + fn show_window_menu(&self, position: Point) { + let state = self.borrow(); + let serial = state.client.get_serial(SerialKind::MousePress); + state.toplevel.show_window_menu( + &state.globals.seat, + serial, + position.x.0 as i32, + position.y.0 as i32, + ); + } + + fn start_window_move(&self) { + let state = self.borrow(); + let serial = state.client.get_serial(SerialKind::MousePress); + state.toplevel._move(&state.globals.seat, serial); + } + + fn start_window_resize(&self, edge: crate::ResizeEdge) { + let state = self.borrow(); + state.toplevel.resize( + &state.globals.seat, + state.client.get_serial(SerialKind::MousePress), + edge.to_xdg(), + ) + } + + fn window_decorations(&self) -> Decorations { + let state = self.borrow(); + match state.decorations { + WindowDecorations::Server => Decorations::Server, + WindowDecorations::Client => Decorations::Client { + tiling: state.tiling, + }, + } + } + + fn request_decorations(&self, decorations: WindowDecorations) { + let mut state = self.borrow_mut(); + state.decorations = decorations; + if let Some(decoration) = state.decoration.as_ref() { + decoration.set_mode(decorations.to_xdg()); + update_window(state); + } + } + + fn window_controls(&self) -> WindowControls { + self.borrow().window_controls + } + + fn set_client_inset(&self, inset: Pixels) { + let mut state = self.borrow_mut(); + if Some(inset) != state.client_inset { + state.client_inset = Some(inset); + update_window(state); + } + } + + fn update_ime_position(&self, bounds: Bounds) { + let state = self.borrow(); + state.client.update_ime_position(bounds); + } + + fn gpu_specs(&self) -> Option { + self.borrow().renderer.gpu_specs().into() + } +} + +fn update_window(mut state: RefMut) { + let opaque = !state.is_transparent(); + + state.renderer.update_transparency(!opaque); + let mut opaque_area = state.window_bounds.map(|v| v.0 as i32); + opaque_area.inset(state.inset().0 as i32); + + let region = state + .globals + .compositor + .create_region(&state.globals.qh, ()); + region.add( + opaque_area.origin.x, + opaque_area.origin.y, + opaque_area.size.width, + opaque_area.size.height, + ); + + // Note that rounded corners make this rectangle API hard to work with. + // As this is common when using CSD, let's just disable this API. + if state.background_appearance == WindowBackgroundAppearance::Opaque + && state.decorations == WindowDecorations::Server + { + // Promise the compositor that this region of the window surface + // contains no transparent pixels. This allows the compositor to skip + // updating whatever is behind the surface for better performance. + state.surface.set_opaque_region(Some(®ion)); + } else { + state.surface.set_opaque_region(None); + } + + if let Some(ref blur_manager) = state.globals.blur_manager { + if state.background_appearance == WindowBackgroundAppearance::Blurred { + if state.blur.is_none() { + let blur = blur_manager.create(&state.surface, &state.globals.qh, ()); + state.blur = Some(blur); + } + state.blur.as_ref().unwrap().commit(); + } else { + // It probably doesn't hurt to clear the blur for opaque windows + blur_manager.unset(&state.surface); + if let Some(b) = state.blur.take() { + b.release() + } + } + } + + region.destroy(); +} + +impl WindowDecorations { + fn to_xdg(self) -> zxdg_toplevel_decoration_v1::Mode { + match self { + WindowDecorations::Client => zxdg_toplevel_decoration_v1::Mode::ClientSide, + WindowDecorations::Server => zxdg_toplevel_decoration_v1::Mode::ServerSide, + } + } +} + +impl ResizeEdge { + fn to_xdg(self) -> xdg_toplevel::ResizeEdge { + match self { + ResizeEdge::Top => xdg_toplevel::ResizeEdge::Top, + ResizeEdge::TopRight => xdg_toplevel::ResizeEdge::TopRight, + ResizeEdge::Right => xdg_toplevel::ResizeEdge::Right, + ResizeEdge::BottomRight => xdg_toplevel::ResizeEdge::BottomRight, + ResizeEdge::Bottom => xdg_toplevel::ResizeEdge::Bottom, + ResizeEdge::BottomLeft => xdg_toplevel::ResizeEdge::BottomLeft, + ResizeEdge::Left => xdg_toplevel::ResizeEdge::Left, + ResizeEdge::TopLeft => xdg_toplevel::ResizeEdge::TopLeft, + } + } +} + +/// The configuration event is in terms of the window geometry, which we are constantly +/// updating to account for the client decorations. But that's not the area we want to render +/// to, due to our intrusize CSD. So, here we calculate the 'actual' size, by adding back in the insets +fn compute_outer_size( + inset: Pixels, + new_size: Option>, + tiling: Tiling, +) -> Option> { + new_size.map(|mut new_size| { + if !tiling.top { + new_size.height += inset; + } + if !tiling.bottom { + new_size.height += inset; + } + if !tiling.left { + new_size.width += inset; + } + if !tiling.right { + new_size.width += inset; + } + + new_size + }) +} + +fn inset_by_tiling(mut bounds: Bounds, inset: Pixels, tiling: Tiling) -> Bounds { + if !tiling.top { + bounds.origin.y += inset; + bounds.size.height -= inset; + } + if !tiling.bottom { + bounds.size.height -= inset; + } + if !tiling.left { + bounds.origin.x += inset; + bounds.size.width -= inset; + } + if !tiling.right { + bounds.size.width -= inset; + } + + bounds +} diff --git a/third_party/gpui/src/platform/linux/x11.rs b/third_party/gpui/src/platform/linux/x11.rs new file mode 100644 index 0000000..5c7a0c2 --- /dev/null +++ b/third_party/gpui/src/platform/linux/x11.rs @@ -0,0 +1,12 @@ +mod client; +mod clipboard; +mod display; +mod event; +mod window; +mod xim_handler; + +pub(crate) use client::*; +pub(crate) use display::*; +pub(crate) use event::*; +pub(crate) use window::*; +pub(crate) use xim_handler::*; diff --git a/third_party/gpui/src/platform/linux/x11/client.rs b/third_party/gpui/src/platform/linux/x11/client.rs new file mode 100644 index 0000000..fa9d018 --- /dev/null +++ b/third_party/gpui/src/platform/linux/x11/client.rs @@ -0,0 +1,2491 @@ +use crate::{Capslock, xcb_flush}; +use anyhow::{Context as _, anyhow}; +use ashpd::WindowIdentifier; +use calloop::{ + EventLoop, LoopHandle, RegistrationToken, + generic::{FdWrapper, Generic}, +}; +use collections::HashMap; +use core::str; +use http_client::Url; +use log::Level; +use smallvec::SmallVec; +use std::{ + cell::RefCell, + collections::{BTreeMap, HashSet}, + ops::Deref, + path::PathBuf, + rc::{Rc, Weak}, + time::{Duration, Instant}, +}; +use util::ResultExt; + +use x11rb::{ + connection::{Connection, RequestConnection}, + cursor, + errors::ConnectionError, + protocol::randr::ConnectionExt as _, + protocol::xinput::ConnectionExt, + protocol::xkb::ConnectionExt as _, + protocol::xproto::{ + AtomEnum, ChangeWindowAttributesAux, ClientMessageData, ClientMessageEvent, + ConnectionExt as _, EventMask, Visibility, + }, + protocol::{Event, randr, render, xinput, xkb, xproto}, + resource_manager::Database, + wrapper::ConnectionExt as _, + xcb_ffi::XCBConnection, +}; +use xim::{AttributeName, Client, InputStyle, x11rb::X11rbClient}; +use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION}; +use xkbcommon::xkb::{self as xkbc, STATE_LAYOUT_EFFECTIVE}; + +use super::{ + ButtonOrScroll, ScrollDirection, X11Display, X11WindowStatePtr, XcbAtoms, XimCallbackEvent, + XimHandler, button_or_scroll_from_event_detail, check_reply, + clipboard::{self, Clipboard}, + get_reply, get_valuator_axis_index, handle_connection_error, modifiers_from_state, + pressed_button_from_mask, +}; + +use crate::platform::{ + LinuxCommon, PlatformWindow, + blade::BladeContext, + linux::{ + DEFAULT_CURSOR_ICON_NAME, LinuxClient, get_xkb_compose_state, is_within_click_distance, + log_cursor_icon_warning, open_uri_internal, + platform::{DOUBLE_CLICK_INTERVAL, SCROLL_LINES}, + reveal_path_internal, + xdg_desktop_portal::{Event as XDPEvent, XDPEventSource}, + }, +}; +use crate::{ + AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, DisplayId, FileDropEvent, Keystroke, + LinuxKeyboardLayout, Modifiers, ModifiersChangedEvent, MouseButton, Pixels, Platform, + PlatformDisplay, PlatformInput, PlatformKeyboardLayout, Point, RequestFrameOptions, + ScrollDelta, Size, TouchPhase, WindowParams, X11Window, modifiers_from_xinput_info, point, px, +}; + +/// Value for DeviceId parameters which selects all devices. +pub(crate) const XINPUT_ALL_DEVICES: xinput::DeviceId = 0; + +/// Value for DeviceId parameters which selects all device groups. Events that +/// occur within the group are emitted by the group itself. +/// +/// In XInput 2's interface, these are referred to as "master devices", but that +/// terminology is both archaic and unclear. +pub(crate) const XINPUT_ALL_DEVICE_GROUPS: xinput::DeviceId = 1; + +const GPUI_X11_SCALE_FACTOR_ENV: &str = "GPUI_X11_SCALE_FACTOR"; + +pub(crate) struct WindowRef { + window: X11WindowStatePtr, + refresh_state: Option, + expose_event_received: bool, + last_visibility: Visibility, + is_mapped: bool, +} + +impl WindowRef { + pub fn handle(&self) -> AnyWindowHandle { + self.window.state.borrow().handle + } +} + +impl Deref for WindowRef { + type Target = X11WindowStatePtr; + + fn deref(&self) -> &Self::Target { + &self.window + } +} + +enum RefreshState { + Hidden { + refresh_rate: Duration, + }, + PeriodicRefresh { + refresh_rate: Duration, + event_loop_token: RegistrationToken, + }, +} + +#[derive(Debug)] +#[non_exhaustive] +pub enum EventHandlerError { + XCBConnectionError(ConnectionError), + XIMClientError(xim::ClientError), +} + +impl std::error::Error for EventHandlerError {} + +impl std::fmt::Display for EventHandlerError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + EventHandlerError::XCBConnectionError(err) => err.fmt(f), + EventHandlerError::XIMClientError(err) => err.fmt(f), + } + } +} + +impl From for EventHandlerError { + fn from(err: ConnectionError) -> Self { + EventHandlerError::XCBConnectionError(err) + } +} + +impl From for EventHandlerError { + fn from(err: xim::ClientError) -> Self { + EventHandlerError::XIMClientError(err) + } +} + +#[derive(Debug, Default)] +pub struct Xdnd { + other_window: xproto::Window, + drag_type: u32, + retrieved: bool, + position: Point, +} + +#[derive(Debug)] +struct PointerDeviceState { + horizontal: ScrollAxisState, + vertical: ScrollAxisState, +} + +#[derive(Debug, Default)] +struct ScrollAxisState { + /// Valuator number for looking up this axis's scroll value. + valuator_number: Option, + /// Conversion factor from scroll units to lines. + multiplier: f32, + /// Last scroll value for calculating scroll delta. + /// + /// This gets set to `None` whenever it might be invalid - when devices change or when window focus changes. + /// The logic errs on the side of invalidating this, since the consequence is just skipping the delta of one scroll event. + /// The consequence of not invalidating it can be large invalid deltas, which are much more user visible. + scroll_value: Option, +} + +pub struct X11ClientState { + pub(crate) loop_handle: LoopHandle<'static, X11Client>, + pub(crate) event_loop: Option>, + + pub(crate) last_click: Instant, + pub(crate) last_mouse_button: Option, + pub(crate) last_location: Point, + pub(crate) current_count: usize, + + gpu_context: BladeContext, + + pub(crate) scale_factor: f32, + + xkb_context: xkbc::Context, + pub(crate) xcb_connection: Rc, + xkb_device_id: i32, + client_side_decorations_supported: bool, + pub(crate) x_root_index: usize, + pub(crate) _resource_database: Database, + pub(crate) atoms: XcbAtoms, + pub(crate) windows: HashMap, + pub(crate) mouse_focused_window: Option, + pub(crate) keyboard_focused_window: Option, + pub(crate) xkb: xkbc::State, + keyboard_layout: LinuxKeyboardLayout, + pub(crate) ximc: Option>>, + pub(crate) xim_handler: Option, + pub modifiers: Modifiers, + pub capslock: Capslock, + // TODO: Can the other updates to `modifiers` be removed so that this is unnecessary? + // capslock logic was done analog to modifiers + pub last_modifiers_changed_event: Modifiers, + pub last_capslock_changed_event: Capslock, + + pub(crate) compose_state: Option, + pub(crate) pre_edit_text: Option, + pub(crate) composing: bool, + pub(crate) pre_key_char_down: Option, + pub(crate) cursor_handle: cursor::Handle, + pub(crate) cursor_styles: HashMap, + pub(crate) cursor_cache: HashMap>, + + pointer_device_states: BTreeMap, + + pub(crate) common: LinuxCommon, + pub(crate) clipboard: Clipboard, + pub(crate) clipboard_item: Option, + pub(crate) xdnd_state: Xdnd, +} + +#[derive(Clone)] +pub struct X11ClientStatePtr(pub Weak>); + +impl X11ClientStatePtr { + fn get_client(&self) -> Option { + self.0.upgrade().map(X11Client) + } + + pub fn drop_window(&self, x_window: u32) { + let Some(client) = self.get_client() else { + return; + }; + let mut state = client.0.borrow_mut(); + + if let Some(window_ref) = state.windows.remove(&x_window) + && let Some(RefreshState::PeriodicRefresh { + event_loop_token, .. + }) = window_ref.refresh_state + { + state.loop_handle.remove(event_loop_token); + } + if state.mouse_focused_window == Some(x_window) { + state.mouse_focused_window = None; + } + if state.keyboard_focused_window == Some(x_window) { + state.keyboard_focused_window = None; + } + state.cursor_styles.remove(&x_window); + + if state.windows.is_empty() { + state.common.signal.stop(); + } + } + + pub fn update_ime_position(&self, bounds: Bounds) { + let Some(client) = self.get_client() else { + return; + }; + let mut state = client.0.borrow_mut(); + if state.composing || state.ximc.is_none() { + return; + } + + let Some(mut ximc) = state.ximc.take() else { + log::error!("bug: xim connection not set"); + return; + }; + let Some(xim_handler) = state.xim_handler.take() else { + log::error!("bug: xim handler not set"); + state.ximc = Some(ximc); + return; + }; + let scaled_bounds = bounds.scale(state.scale_factor); + let ic_attributes = ximc + .build_ic_attributes() + .push( + xim::AttributeName::InputStyle, + xim::InputStyle::PREEDIT_CALLBACKS, + ) + .push(xim::AttributeName::ClientWindow, xim_handler.window) + .push(xim::AttributeName::FocusWindow, xim_handler.window) + .nested_list(xim::AttributeName::PreeditAttributes, |b| { + b.push( + xim::AttributeName::SpotLocation, + xim::Point { + x: u32::from(scaled_bounds.origin.x + scaled_bounds.size.width) as i16, + y: u32::from(scaled_bounds.origin.y + scaled_bounds.size.height) as i16, + }, + ); + }) + .build(); + let _ = ximc + .set_ic_values(xim_handler.im_id, xim_handler.ic_id, ic_attributes) + .log_err(); + state.ximc = Some(ximc); + state.xim_handler = Some(xim_handler); + } +} + +#[derive(Clone)] +pub(crate) struct X11Client(Rc>); + +impl X11Client { + pub(crate) fn new() -> anyhow::Result { + let event_loop = EventLoop::try_new()?; + + let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal()); + + let handle = event_loop.handle(); + + handle + .insert_source(main_receiver, { + let handle = handle.clone(); + move |event, _, _: &mut X11Client| { + if let calloop::channel::Event::Msg(runnable) = event { + // Insert the runnables as idle callbacks, so we make sure that user-input and X11 + // events have higher priority and runnables are only worked off after the event + // callbacks. + handle.insert_idle(|_| { + runnable.run(); + }); + } + } + }) + .map_err(|err| { + anyhow!("Failed to initialize event loop handling of foreground tasks: {err:?}") + })?; + + let (xcb_connection, x_root_index) = XCBConnection::connect(None)?; + xcb_connection.prefetch_extension_information(xkb::X11_EXTENSION_NAME)?; + xcb_connection.prefetch_extension_information(randr::X11_EXTENSION_NAME)?; + xcb_connection.prefetch_extension_information(render::X11_EXTENSION_NAME)?; + xcb_connection.prefetch_extension_information(xinput::X11_EXTENSION_NAME)?; + + // Announce to X server that XInput up to 2.1 is supported. To increase this to 2.2 and + // beyond, support for touch events would need to be added. + let xinput_version = get_reply( + || "XInput XiQueryVersion failed", + xcb_connection.xinput_xi_query_version(2, 1), + )?; + assert!( + xinput_version.major_version >= 2, + "XInput version >= 2 required." + ); + + let pointer_device_states = + current_pointer_device_states(&xcb_connection, &BTreeMap::new()).unwrap_or_default(); + + let atoms = XcbAtoms::new(&xcb_connection) + .context("Failed to get XCB atoms")? + .reply() + .context("Failed to get XCB atoms")?; + + let root = xcb_connection.setup().roots[0].root; + let compositor_present = check_compositor_present(&xcb_connection, root); + let gtk_frame_extents_supported = + check_gtk_frame_extents_supported(&xcb_connection, &atoms, root); + let client_side_decorations_supported = compositor_present && gtk_frame_extents_supported; + log::info!( + "x11: compositor present: {}, gtk_frame_extents_supported: {}", + compositor_present, + gtk_frame_extents_supported + ); + + let xkb = get_reply( + || "Failed to initialize XKB extension", + xcb_connection + .xkb_use_extension(XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION), + )?; + assert!(xkb.supported); + + let events = xkb::EventType::STATE_NOTIFY + | xkb::EventType::MAP_NOTIFY + | xkb::EventType::NEW_KEYBOARD_NOTIFY; + let map_notify_parts = xkb::MapPart::KEY_TYPES + | xkb::MapPart::KEY_SYMS + | xkb::MapPart::MODIFIER_MAP + | xkb::MapPart::EXPLICIT_COMPONENTS + | xkb::MapPart::KEY_ACTIONS + | xkb::MapPart::KEY_BEHAVIORS + | xkb::MapPart::VIRTUAL_MODS + | xkb::MapPart::VIRTUAL_MOD_MAP; + check_reply( + || "Failed to select XKB events", + xcb_connection.xkb_select_events( + xkb::ID::USE_CORE_KBD.into(), + 0u8.into(), + events, + map_notify_parts, + map_notify_parts, + &xkb::SelectEventsAux::new(), + ), + )?; + + let xkb_context = xkbc::Context::new(xkbc::CONTEXT_NO_FLAGS); + let xkb_device_id = xkbc::x11::get_core_keyboard_device_id(&xcb_connection); + let xkb_state = { + let xkb_keymap = xkbc::x11::keymap_new_from_device( + &xkb_context, + &xcb_connection, + xkb_device_id, + xkbc::KEYMAP_COMPILE_NO_FLAGS, + ); + xkbc::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id) + }; + let compose_state = get_xkb_compose_state(&xkb_context); + let layout_idx = xkb_state.serialize_layout(STATE_LAYOUT_EFFECTIVE); + let layout_name = xkb_state + .get_keymap() + .layout_get_name(layout_idx) + .to_string(); + let keyboard_layout = LinuxKeyboardLayout::new(layout_name.into()); + + let gpu_context = BladeContext::new().context("Unable to init GPU context")?; + + let resource_database = x11rb::resource_manager::new_from_default(&xcb_connection) + .context("Failed to create resource database")?; + let scale_factor = get_scale_factor(&xcb_connection, &resource_database, x_root_index); + let cursor_handle = cursor::Handle::new(&xcb_connection, x_root_index, &resource_database) + .context("Failed to initialize cursor theme handler")? + .reply() + .context("Failed to initialize cursor theme handler")?; + + let clipboard = Clipboard::new().context("Failed to initialize clipboard")?; + + let xcb_connection = Rc::new(xcb_connection); + + let ximc = X11rbClient::init(Rc::clone(&xcb_connection), x_root_index, None).ok(); + let xim_handler = if ximc.is_some() { + Some(XimHandler::new()) + } else { + None + }; + + // Safety: Safe if xcb::Connection always returns a valid fd + let fd = unsafe { FdWrapper::new(Rc::clone(&xcb_connection)) }; + + handle + .insert_source( + Generic::new_with_error::( + fd, + calloop::Interest::READ, + calloop::Mode::Level, + ), + { + let xcb_connection = xcb_connection.clone(); + move |_readiness, _, client| { + client.process_x11_events(&xcb_connection)?; + Ok(calloop::PostAction::Continue) + } + }, + ) + .map_err(|err| anyhow!("Failed to initialize X11 event source: {err:?}"))?; + + handle + .insert_source(XDPEventSource::new(&common.background_executor), { + move |event, _, client| match event { + XDPEvent::WindowAppearance(appearance) => { + client.with_common(|common| common.appearance = appearance); + for window in client.0.borrow_mut().windows.values_mut() { + window.window.set_appearance(appearance); + } + } + XDPEvent::CursorTheme(_) | XDPEvent::CursorSize(_) => { + // noop, X11 manages this for us. + } + } + }) + .map_err(|err| anyhow!("Failed to initialize XDP event source: {err:?}"))?; + + xcb_flush(&xcb_connection); + + Ok(X11Client(Rc::new(RefCell::new(X11ClientState { + modifiers: Modifiers::default(), + capslock: Capslock::default(), + last_modifiers_changed_event: Modifiers::default(), + last_capslock_changed_event: Capslock::default(), + event_loop: Some(event_loop), + loop_handle: handle, + common, + last_click: Instant::now(), + last_mouse_button: None, + last_location: Point::new(px(0.0), px(0.0)), + current_count: 0, + gpu_context, + scale_factor, + + xkb_context, + xcb_connection, + xkb_device_id, + client_side_decorations_supported, + x_root_index, + _resource_database: resource_database, + atoms, + windows: HashMap::default(), + mouse_focused_window: None, + keyboard_focused_window: None, + xkb: xkb_state, + keyboard_layout, + ximc, + xim_handler, + + compose_state, + pre_edit_text: None, + pre_key_char_down: None, + composing: false, + + cursor_handle, + cursor_styles: HashMap::default(), + cursor_cache: HashMap::default(), + + pointer_device_states, + + clipboard, + clipboard_item: None, + xdnd_state: Xdnd::default(), + })))) + } + + pub fn process_x11_events( + &self, + xcb_connection: &XCBConnection, + ) -> Result<(), EventHandlerError> { + loop { + let mut events = Vec::new(); + let mut windows_to_refresh = HashSet::new(); + + let mut last_key_release = None; + + // event handlers for new keyboard / remapping refresh the state without using event + // details, this deduplicates them. + let mut last_keymap_change_event: Option = None; + + loop { + match xcb_connection.poll_for_event() { + Ok(Some(event)) => { + match event { + Event::Expose(expose_event) => { + windows_to_refresh.insert(expose_event.window); + } + Event::KeyRelease(_) => { + if let Some(last_keymap_change_event) = + last_keymap_change_event.take() + { + if let Some(last_key_release) = last_key_release.take() { + events.push(last_key_release); + } + events.push(last_keymap_change_event); + } + + last_key_release = Some(event); + } + Event::KeyPress(key_press) => { + if let Some(last_keymap_change_event) = + last_keymap_change_event.take() + { + if let Some(last_key_release) = last_key_release.take() { + events.push(last_key_release); + } + events.push(last_keymap_change_event); + } + + if let Some(Event::KeyRelease(key_release)) = + last_key_release.take() + { + // We ignore that last KeyRelease if it's too close to this KeyPress, + // suggesting that it's auto-generated by X11 as a key-repeat event. + if key_release.detail != key_press.detail + || key_press.time.saturating_sub(key_release.time) > 20 + { + events.push(Event::KeyRelease(key_release)); + } + } + events.push(Event::KeyPress(key_press)); + } + Event::XkbNewKeyboardNotify(_) | Event::XkbMapNotify(_) => { + if let Some(release_event) = last_key_release.take() { + events.push(release_event); + } + last_keymap_change_event = Some(event); + } + _ => { + if let Some(release_event) = last_key_release.take() { + events.push(release_event); + } + events.push(event); + } + } + } + Ok(None) => { + break; + } + Err(err) => { + let err = handle_connection_error(err); + log::warn!("error while polling for X11 events: {err:?}"); + break; + } + } + } + + if let Some(release_event) = last_key_release.take() { + events.push(release_event); + } + if let Some(keymap_change_event) = last_keymap_change_event.take() { + events.push(keymap_change_event); + } + + if events.is_empty() && windows_to_refresh.is_empty() { + break; + } + + for window in windows_to_refresh.into_iter() { + let mut state = self.0.borrow_mut(); + if let Some(window) = state.windows.get_mut(&window) { + window.expose_event_received = true; + } + } + + for event in events.into_iter() { + let mut state = self.0.borrow_mut(); + if !state.has_xim() { + drop(state); + self.handle_event(event); + continue; + } + + let Some((mut ximc, mut xim_handler)) = state.take_xim() else { + continue; + }; + let xim_connected = xim_handler.connected; + drop(state); + + let xim_filtered = ximc.filter_event(&event, &mut xim_handler); + let xim_callback_event = xim_handler.last_callback_event.take(); + + let mut state = self.0.borrow_mut(); + state.restore_xim(ximc, xim_handler); + drop(state); + + if let Some(event) = xim_callback_event { + self.handle_xim_callback_event(event); + } + + match xim_filtered { + Ok(handled) => { + if handled { + continue; + } + if xim_connected { + self.xim_handle_event(event); + } else { + self.handle_event(event); + } + } + Err(err) => { + // this might happen when xim server crashes on one of the events + // we do lose 1-2 keys when crash happens since there is no reliable way to get that info + // luckily, x11 sends us window not found error when xim server crashes upon further key press + // hence we fall back to handle_event + log::error!("XIMClientError: {}", err); + let mut state = self.0.borrow_mut(); + state.take_xim(); + drop(state); + self.handle_event(event); + } + } + } + } + Ok(()) + } + + pub fn enable_ime(&self) { + let mut state = self.0.borrow_mut(); + if !state.has_xim() { + return; + } + + let Some((mut ximc, mut xim_handler)) = state.take_xim() else { + return; + }; + let mut ic_attributes = ximc + .build_ic_attributes() + .push(AttributeName::InputStyle, InputStyle::PREEDIT_CALLBACKS) + .push(AttributeName::ClientWindow, xim_handler.window) + .push(AttributeName::FocusWindow, xim_handler.window); + + let window_id = state.keyboard_focused_window; + drop(state); + if let Some(window_id) = window_id { + let Some(window) = self.get_window(window_id) else { + log::error!("Failed to get window for IME positioning"); + let mut state = self.0.borrow_mut(); + state.ximc = Some(ximc); + state.xim_handler = Some(xim_handler); + return; + }; + if let Some(scaled_area) = window.get_ime_area() { + ic_attributes = + ic_attributes.nested_list(xim::AttributeName::PreeditAttributes, |b| { + b.push( + xim::AttributeName::SpotLocation, + xim::Point { + x: u32::from(scaled_area.origin.x + scaled_area.size.width) as i16, + y: u32::from(scaled_area.origin.y + scaled_area.size.height) as i16, + }, + ); + }); + } + } + ximc.create_ic(xim_handler.im_id, ic_attributes.build()) + .ok(); + let mut state = self.0.borrow_mut(); + state.restore_xim(ximc, xim_handler); + } + + pub fn reset_ime(&self) { + let mut state = self.0.borrow_mut(); + state.composing = false; + if let Some(mut ximc) = state.ximc.take() { + if let Some(xim_handler) = state.xim_handler.as_ref() { + ximc.reset_ic(xim_handler.im_id, xim_handler.ic_id).ok(); + } else { + log::error!("bug: xim handler not set in reset_ime"); + } + state.ximc = Some(ximc); + } + } + + fn get_window(&self, win: xproto::Window) -> Option { + let state = self.0.borrow(); + state + .windows + .get(&win) + .filter(|window_reference| !window_reference.window.state.borrow().destroyed) + .map(|window_reference| window_reference.window.clone()) + } + + fn handle_event(&self, event: Event) -> Option<()> { + match event { + Event::UnmapNotify(event) => { + let mut state = self.0.borrow_mut(); + if let Some(window_ref) = state.windows.get_mut(&event.window) { + window_ref.is_mapped = false; + } + state.update_refresh_loop(event.window); + } + Event::MapNotify(event) => { + let mut state = self.0.borrow_mut(); + if let Some(window_ref) = state.windows.get_mut(&event.window) { + window_ref.is_mapped = true; + } + state.update_refresh_loop(event.window); + } + Event::VisibilityNotify(event) => { + let mut state = self.0.borrow_mut(); + if let Some(window_ref) = state.windows.get_mut(&event.window) { + window_ref.last_visibility = event.state; + } + state.update_refresh_loop(event.window); + } + Event::ClientMessage(event) => { + let window = self.get_window(event.window)?; + let [atom, arg1, arg2, arg3, arg4] = event.data.as_data32(); + let mut state = self.0.borrow_mut(); + + if atom == state.atoms.WM_DELETE_WINDOW { + // window "x" button clicked by user + if window.should_close() { + // Rest of the close logic is handled in drop_window() + window.close(); + } + } else if atom == state.atoms._NET_WM_SYNC_REQUEST { + window.state.borrow_mut().last_sync_counter = + Some(x11rb::protocol::sync::Int64 { + lo: arg2, + hi: arg3 as i32, + }) + } + + if event.type_ == state.atoms.XdndEnter { + state.xdnd_state.other_window = atom; + if (arg1 & 0x1) == 0x1 { + state.xdnd_state.drag_type = xdnd_get_supported_atom( + &state.xcb_connection, + &state.atoms, + state.xdnd_state.other_window, + ); + } else { + if let Some(atom) = [arg2, arg3, arg4] + .into_iter() + .find(|atom| xdnd_is_atom_supported(*atom, &state.atoms)) + { + state.xdnd_state.drag_type = atom; + } + } + } else if event.type_ == state.atoms.XdndLeave { + let position = state.xdnd_state.position; + drop(state); + window + .handle_input(PlatformInput::FileDrop(FileDropEvent::Pending { position })); + window.handle_input(PlatformInput::FileDrop(FileDropEvent::Exited {})); + self.0.borrow_mut().xdnd_state = Xdnd::default(); + } else if event.type_ == state.atoms.XdndPosition { + if let Ok(pos) = get_reply( + || "Failed to query pointer position", + state.xcb_connection.query_pointer(event.window), + ) { + state.xdnd_state.position = + Point::new(Pixels(pos.win_x as f32), Pixels(pos.win_y as f32)); + } + if !state.xdnd_state.retrieved { + check_reply( + || "Failed to convert selection for drag and drop", + state.xcb_connection.convert_selection( + event.window, + state.atoms.XdndSelection, + state.xdnd_state.drag_type, + state.atoms.XDND_DATA, + arg3, + ), + ) + .log_err(); + } + xdnd_send_status( + &state.xcb_connection, + &state.atoms, + event.window, + state.xdnd_state.other_window, + arg4, + ); + let position = state.xdnd_state.position; + drop(state); + window + .handle_input(PlatformInput::FileDrop(FileDropEvent::Pending { position })); + } else if event.type_ == state.atoms.XdndDrop { + xdnd_send_finished( + &state.xcb_connection, + &state.atoms, + event.window, + state.xdnd_state.other_window, + ); + let position = state.xdnd_state.position; + drop(state); + window + .handle_input(PlatformInput::FileDrop(FileDropEvent::Submit { position })); + self.0.borrow_mut().xdnd_state = Xdnd::default(); + } + } + Event::SelectionNotify(event) => { + let window = self.get_window(event.requestor)?; + let mut state = self.0.borrow_mut(); + let reply = get_reply( + || "Failed to get XDND_DATA", + state.xcb_connection.get_property( + false, + event.requestor, + state.atoms.XDND_DATA, + AtomEnum::ANY, + 0, + 1024, + ), + ) + .log_err(); + let Some(reply) = reply else { + return Some(()); + }; + if let Ok(file_list) = str::from_utf8(&reply.value) { + let paths: SmallVec<[_; 2]> = file_list + .lines() + .filter_map(|path| Url::parse(path).log_err()) + .filter_map(|url| url.to_file_path().log_err()) + .collect(); + let input = PlatformInput::FileDrop(FileDropEvent::Entered { + position: state.xdnd_state.position, + paths: crate::ExternalPaths(paths), + }); + drop(state); + window.handle_input(input); + self.0.borrow_mut().xdnd_state.retrieved = true; + } + } + Event::ConfigureNotify(event) => { + let bounds = Bounds { + origin: Point { + x: event.x.into(), + y: event.y.into(), + }, + size: Size { + width: event.width.into(), + height: event.height.into(), + }, + }; + let window = self.get_window(event.window)?; + window + .set_bounds(bounds) + .context("X11: Failed to set window bounds") + .log_err(); + } + Event::PropertyNotify(event) => { + let window = self.get_window(event.window)?; + window + .property_notify(event) + .context("X11: Failed to handle property notify") + .log_err(); + } + Event::FocusIn(event) => { + let window = self.get_window(event.event)?; + window.set_active(true); + let mut state = self.0.borrow_mut(); + state.keyboard_focused_window = Some(event.event); + if let Some(handler) = state.xim_handler.as_mut() { + handler.window = event.event; + } + drop(state); + self.enable_ime(); + } + Event::FocusOut(event) => { + let window = self.get_window(event.event)?; + window.set_active(false); + let mut state = self.0.borrow_mut(); + state.keyboard_focused_window = None; + if let Some(compose_state) = state.compose_state.as_mut() { + compose_state.reset(); + } + state.pre_edit_text.take(); + drop(state); + self.reset_ime(); + window.handle_ime_delete(); + } + Event::XkbNewKeyboardNotify(_) | Event::XkbMapNotify(_) => { + let mut state = self.0.borrow_mut(); + let xkb_state = { + let xkb_keymap = xkbc::x11::keymap_new_from_device( + &state.xkb_context, + &state.xcb_connection, + state.xkb_device_id, + xkbc::KEYMAP_COMPILE_NO_FLAGS, + ); + xkbc::x11::state_new_from_device( + &xkb_keymap, + &state.xcb_connection, + state.xkb_device_id, + ) + }; + state.xkb = xkb_state; + drop(state); + self.handle_keyboard_layout_change(); + } + Event::XkbStateNotify(event) => { + let mut state = self.0.borrow_mut(); + let old_layout = state.xkb.serialize_layout(STATE_LAYOUT_EFFECTIVE); + let new_layout = u32::from(event.group); + state.xkb.update_mask( + event.base_mods.into(), + event.latched_mods.into(), + event.locked_mods.into(), + event.base_group as u32, + event.latched_group as u32, + event.locked_group.into(), + ); + let modifiers = Modifiers::from_xkb(&state.xkb); + let capslock = Capslock::from_xkb(&state.xkb); + if state.last_modifiers_changed_event == modifiers + && state.last_capslock_changed_event == capslock + { + drop(state); + } else { + let focused_window_id = state.keyboard_focused_window?; + state.modifiers = modifiers; + state.last_modifiers_changed_event = modifiers; + state.capslock = capslock; + state.last_capslock_changed_event = capslock; + drop(state); + + let focused_window = self.get_window(focused_window_id)?; + focused_window.handle_input(PlatformInput::ModifiersChanged( + ModifiersChangedEvent { + modifiers, + capslock, + }, + )); + } + + if new_layout != old_layout { + self.handle_keyboard_layout_change(); + } + } + Event::KeyPress(event) => { + let window = self.get_window(event.event)?; + let mut state = self.0.borrow_mut(); + + let modifiers = modifiers_from_state(event.state); + state.modifiers = modifiers; + state.pre_key_char_down.take(); + let keystroke = { + let code = event.detail.into(); + let mut keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code); + let keysym = state.xkb.key_get_one_sym(code); + + if keysym.is_modifier_key() { + return Some(()); + } + + // should be called after key_get_one_sym + state.xkb.update_key(code, xkbc::KeyDirection::Down); + + if let Some(mut compose_state) = state.compose_state.take() { + compose_state.feed(keysym); + match compose_state.status() { + xkbc::Status::Composed => { + state.pre_edit_text.take(); + keystroke.key_char = compose_state.utf8(); + if let Some(keysym) = compose_state.keysym() { + keystroke.key = xkbc::keysym_get_name(keysym); + } + } + xkbc::Status::Composing => { + keystroke.key_char = None; + state.pre_edit_text = compose_state + .utf8() + .or(crate::Keystroke::underlying_dead_key(keysym)); + let pre_edit = + state.pre_edit_text.clone().unwrap_or(String::default()); + drop(state); + window.handle_ime_preedit(pre_edit); + state = self.0.borrow_mut(); + } + xkbc::Status::Cancelled => { + let pre_edit = state.pre_edit_text.take(); + drop(state); + if let Some(pre_edit) = pre_edit { + window.handle_ime_commit(pre_edit); + } + if let Some(current_key) = Keystroke::underlying_dead_key(keysym) { + window.handle_ime_preedit(current_key); + } + state = self.0.borrow_mut(); + compose_state.feed(keysym); + } + _ => {} + } + state.compose_state = Some(compose_state); + } + keystroke + }; + drop(state); + window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent { + keystroke, + is_held: false, + })); + } + Event::KeyRelease(event) => { + let window = self.get_window(event.event)?; + let mut state = self.0.borrow_mut(); + + let modifiers = modifiers_from_state(event.state); + state.modifiers = modifiers; + + let keystroke = { + let code = event.detail.into(); + let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code); + let keysym = state.xkb.key_get_one_sym(code); + + if keysym.is_modifier_key() { + return Some(()); + } + + // should be called after key_get_one_sym + state.xkb.update_key(code, xkbc::KeyDirection::Up); + + keystroke + }; + drop(state); + window.handle_input(PlatformInput::KeyUp(crate::KeyUpEvent { keystroke })); + } + Event::XinputButtonPress(event) => { + let window = self.get_window(event.event)?; + let mut state = self.0.borrow_mut(); + + let modifiers = modifiers_from_xinput_info(event.mods); + state.modifiers = modifiers; + + let position = point( + px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), + px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), + ); + + if state.composing && state.ximc.is_some() { + drop(state); + self.reset_ime(); + window.handle_ime_unmark(); + state = self.0.borrow_mut(); + } else if let Some(text) = state.pre_edit_text.take() { + if let Some(compose_state) = state.compose_state.as_mut() { + compose_state.reset(); + } + drop(state); + window.handle_ime_commit(text); + state = self.0.borrow_mut(); + } + match button_or_scroll_from_event_detail(event.detail) { + Some(ButtonOrScroll::Button(button)) => { + let click_elapsed = state.last_click.elapsed(); + if click_elapsed < DOUBLE_CLICK_INTERVAL + && state + .last_mouse_button + .is_some_and(|prev_button| prev_button == button) + && is_within_click_distance(state.last_location, position) + { + state.current_count += 1; + } else { + state.current_count = 1; + } + + state.last_click = Instant::now(); + state.last_mouse_button = Some(button); + state.last_location = position; + let current_count = state.current_count; + + drop(state); + window.handle_input(PlatformInput::MouseDown(crate::MouseDownEvent { + button, + position, + modifiers, + click_count: current_count, + first_mouse: false, + })); + } + Some(ButtonOrScroll::Scroll(direction)) => { + drop(state); + // Emulated scroll button presses are sent simultaneously with smooth scrolling XinputMotion events. + // Since handling those events does the scrolling, they are skipped here. + if !event + .flags + .contains(xinput::PointerEventFlags::POINTER_EMULATED) + { + let scroll_delta = match direction { + ScrollDirection::Up => Point::new(0.0, SCROLL_LINES), + ScrollDirection::Down => Point::new(0.0, -SCROLL_LINES), + ScrollDirection::Left => Point::new(SCROLL_LINES, 0.0), + ScrollDirection::Right => Point::new(-SCROLL_LINES, 0.0), + }; + window.handle_input(PlatformInput::ScrollWheel( + make_scroll_wheel_event(position, scroll_delta, modifiers), + )); + } + } + None => { + log::error!("Unknown x11 button: {}", event.detail); + } + } + } + Event::XinputButtonRelease(event) => { + let window = self.get_window(event.event)?; + let mut state = self.0.borrow_mut(); + let modifiers = modifiers_from_xinput_info(event.mods); + state.modifiers = modifiers; + + let position = point( + px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), + px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), + ); + match button_or_scroll_from_event_detail(event.detail) { + Some(ButtonOrScroll::Button(button)) => { + let click_count = state.current_count; + drop(state); + window.handle_input(PlatformInput::MouseUp(crate::MouseUpEvent { + button, + position, + modifiers, + click_count, + })); + } + Some(ButtonOrScroll::Scroll(_)) => {} + None => {} + } + } + Event::XinputMotion(event) => { + let window = self.get_window(event.event)?; + let mut state = self.0.borrow_mut(); + let pressed_button = pressed_button_from_mask(event.button_mask[0]); + let position = point( + px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), + px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), + ); + let modifiers = modifiers_from_xinput_info(event.mods); + state.modifiers = modifiers; + drop(state); + + if event.valuator_mask[0] & 3 != 0 { + window.handle_input(PlatformInput::MouseMove(crate::MouseMoveEvent { + position, + pressed_button, + modifiers, + })); + } + + state = self.0.borrow_mut(); + if let Some(mut pointer) = state.pointer_device_states.get_mut(&event.sourceid) { + let scroll_delta = get_scroll_delta_and_update_state(pointer, &event); + drop(state); + if let Some(scroll_delta) = scroll_delta { + window.handle_input(PlatformInput::ScrollWheel(make_scroll_wheel_event( + position, + scroll_delta, + modifiers, + ))); + } + } + } + Event::XinputEnter(event) if event.mode == xinput::NotifyMode::NORMAL => { + let window = self.get_window(event.event)?; + window.set_hovered(true); + let mut state = self.0.borrow_mut(); + state.mouse_focused_window = Some(event.event); + } + Event::XinputLeave(event) if event.mode == xinput::NotifyMode::NORMAL => { + let mut state = self.0.borrow_mut(); + + // Set last scroll values to `None` so that a large delta isn't created if scrolling is done outside the window (the valuator is global) + reset_all_pointer_device_scroll_positions(&mut state.pointer_device_states); + state.mouse_focused_window = None; + let pressed_button = pressed_button_from_mask(event.buttons[0]); + let position = point( + px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), + px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), + ); + let modifiers = modifiers_from_xinput_info(event.mods); + state.modifiers = modifiers; + drop(state); + + let window = self.get_window(event.event)?; + window.handle_input(PlatformInput::MouseExited(crate::MouseExitEvent { + pressed_button, + position, + modifiers, + })); + window.set_hovered(false); + } + Event::XinputHierarchy(event) => { + let mut state = self.0.borrow_mut(); + // Temporarily use `state.pointer_device_states` to only store pointers that still have valid scroll values. + // Any change to a device invalidates its scroll values. + for info in event.infos { + if is_pointer_device(info.type_) { + state.pointer_device_states.remove(&info.deviceid); + } + } + if let Some(pointer_device_states) = current_pointer_device_states( + &state.xcb_connection, + &state.pointer_device_states, + ) { + state.pointer_device_states = pointer_device_states; + } + } + Event::XinputDeviceChanged(event) => { + let mut state = self.0.borrow_mut(); + if let Some(mut pointer) = state.pointer_device_states.get_mut(&event.sourceid) { + reset_pointer_device_scroll_positions(pointer); + } + } + _ => {} + }; + + Some(()) + } + + fn handle_xim_callback_event(&self, event: XimCallbackEvent) { + match event { + XimCallbackEvent::XimXEvent(event) => { + self.handle_event(event); + } + XimCallbackEvent::XimCommitEvent(window, text) => { + self.xim_handle_commit(window, text); + } + XimCallbackEvent::XimPreeditEvent(window, text) => { + self.xim_handle_preedit(window, text); + } + }; + } + + fn xim_handle_event(&self, event: Event) -> Option<()> { + match event { + Event::KeyPress(event) | Event::KeyRelease(event) => { + let mut state = self.0.borrow_mut(); + state.pre_key_char_down = Some(Keystroke::from_xkb( + &state.xkb, + state.modifiers, + event.detail.into(), + )); + let (mut ximc, mut xim_handler) = state.take_xim()?; + drop(state); + xim_handler.window = event.event; + ximc.forward_event( + xim_handler.im_id, + xim_handler.ic_id, + xim::ForwardEventFlag::empty(), + &event, + ) + .context("X11: Failed to forward XIM event") + .log_err(); + let mut state = self.0.borrow_mut(); + state.restore_xim(ximc, xim_handler); + drop(state); + } + event => { + self.handle_event(event); + } + } + Some(()) + } + + fn xim_handle_commit(&self, window: xproto::Window, text: String) -> Option<()> { + let Some(window) = self.get_window(window) else { + log::error!("bug: Failed to get window for XIM commit"); + return None; + }; + let mut state = self.0.borrow_mut(); + state.composing = false; + drop(state); + window.handle_ime_commit(text); + Some(()) + } + + fn xim_handle_preedit(&self, window: xproto::Window, text: String) -> Option<()> { + let Some(window) = self.get_window(window) else { + log::error!("bug: Failed to get window for XIM preedit"); + return None; + }; + + let mut state = self.0.borrow_mut(); + let (mut ximc, mut xim_handler) = state.take_xim()?; + state.composing = !text.is_empty(); + drop(state); + window.handle_ime_preedit(text); + + if let Some(scaled_area) = window.get_ime_area() { + let ic_attributes = ximc + .build_ic_attributes() + .push( + xim::AttributeName::InputStyle, + xim::InputStyle::PREEDIT_CALLBACKS, + ) + .push(xim::AttributeName::ClientWindow, xim_handler.window) + .push(xim::AttributeName::FocusWindow, xim_handler.window) + .nested_list(xim::AttributeName::PreeditAttributes, |b| { + b.push( + xim::AttributeName::SpotLocation, + xim::Point { + x: u32::from(scaled_area.origin.x + scaled_area.size.width) as i16, + y: u32::from(scaled_area.origin.y + scaled_area.size.height) as i16, + }, + ); + }) + .build(); + ximc.set_ic_values(xim_handler.im_id, xim_handler.ic_id, ic_attributes) + .ok(); + } + let mut state = self.0.borrow_mut(); + state.restore_xim(ximc, xim_handler); + drop(state); + Some(()) + } + + fn handle_keyboard_layout_change(&self) { + let mut state = self.0.borrow_mut(); + let layout_idx = state.xkb.serialize_layout(STATE_LAYOUT_EFFECTIVE); + let keymap = state.xkb.get_keymap(); + let layout_name = keymap.layout_get_name(layout_idx); + if layout_name != state.keyboard_layout.name() { + state.keyboard_layout = LinuxKeyboardLayout::new(layout_name.to_string().into()); + if let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() { + drop(state); + callback(); + state = self.0.borrow_mut(); + state.common.callbacks.keyboard_layout_change = Some(callback); + } + } + } +} + +impl LinuxClient for X11Client { + fn compositor_name(&self) -> &'static str { + "X11" + } + + fn with_common(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R { + f(&mut self.0.borrow_mut().common) + } + + fn keyboard_layout(&self) -> Box { + let state = self.0.borrow(); + Box::new(state.keyboard_layout.clone()) + } + + fn displays(&self) -> Vec> { + let state = self.0.borrow(); + let setup = state.xcb_connection.setup(); + setup + .roots + .iter() + .enumerate() + .filter_map(|(root_id, _)| { + Some(Rc::new( + X11Display::new(&state.xcb_connection, state.scale_factor, root_id).ok()?, + ) as Rc) + }) + .collect() + } + + fn primary_display(&self) -> Option> { + let state = self.0.borrow(); + X11Display::new( + &state.xcb_connection, + state.scale_factor, + state.x_root_index, + ) + .log_err() + .map(|display| Rc::new(display) as Rc) + } + + fn display(&self, id: DisplayId) -> Option> { + let state = self.0.borrow(); + + Some(Rc::new( + X11Display::new(&state.xcb_connection, state.scale_factor, id.0 as usize).ok()?, + )) + } + + #[cfg(feature = "screen-capture")] + fn is_screen_capture_supported(&self) -> bool { + true + } + + #[cfg(feature = "screen-capture")] + fn screen_capture_sources( + &self, + ) -> futures::channel::oneshot::Receiver>>> + { + crate::platform::scap_screen_capture::scap_screen_sources( + &self.0.borrow().common.foreground_executor, + ) + } + + fn open_window( + &self, + handle: AnyWindowHandle, + params: WindowParams, + ) -> anyhow::Result> { + let mut state = self.0.borrow_mut(); + let parent_window = state + .keyboard_focused_window + .and_then(|focused_window| state.windows.get(&focused_window)) + .map(|window| window.window.x_window); + let x_window = state + .xcb_connection + .generate_id() + .context("X11: Failed to generate window ID")?; + + let window = X11Window::new( + handle, + X11ClientStatePtr(Rc::downgrade(&self.0)), + state.common.foreground_executor.clone(), + &state.gpu_context, + params, + &state.xcb_connection, + state.client_side_decorations_supported, + state.x_root_index, + x_window, + &state.atoms, + state.scale_factor, + state.common.appearance, + parent_window, + )?; + check_reply( + || "Failed to set XdndAware property", + state.xcb_connection.change_property32( + xproto::PropMode::REPLACE, + x_window, + state.atoms.XdndAware, + state.atoms.XA_ATOM, + &[5], + ), + ) + .log_err(); + xcb_flush(&state.xcb_connection); + + let window_ref = WindowRef { + window: window.0.clone(), + refresh_state: None, + expose_event_received: false, + last_visibility: Visibility::UNOBSCURED, + is_mapped: false, + }; + + state.windows.insert(x_window, window_ref); + Ok(Box::new(window)) + } + + fn set_cursor_style(&self, style: CursorStyle) { + let mut state = self.0.borrow_mut(); + let Some(focused_window) = state.mouse_focused_window else { + return; + }; + let current_style = state + .cursor_styles + .get(&focused_window) + .unwrap_or(&CursorStyle::Arrow); + if *current_style == style { + return; + } + + let Some(cursor) = state.get_cursor_icon(style) else { + return; + }; + + state.cursor_styles.insert(focused_window, style); + check_reply( + || "Failed to set cursor style", + state.xcb_connection.change_window_attributes( + focused_window, + &ChangeWindowAttributesAux { + cursor: Some(cursor), + ..Default::default() + }, + ), + ) + .log_err(); + state.xcb_connection.flush().log_err(); + } + + fn open_uri(&self, uri: &str) { + #[cfg(any(feature = "wayland", feature = "x11"))] + open_uri_internal(self.background_executor(), uri, None); + } + + fn reveal_path(&self, path: PathBuf) { + #[cfg(any(feature = "x11", feature = "wayland"))] + reveal_path_internal(self.background_executor(), path, None); + } + + fn write_to_primary(&self, item: crate::ClipboardItem) { + let state = self.0.borrow_mut(); + state + .clipboard + .set_text( + std::borrow::Cow::Owned(item.text().unwrap_or_default()), + clipboard::ClipboardKind::Primary, + clipboard::WaitConfig::None, + ) + .context("X11 Failed to write to clipboard (primary)") + .log_with_level(log::Level::Debug); + } + + fn write_to_clipboard(&self, item: crate::ClipboardItem) { + let mut state = self.0.borrow_mut(); + state + .clipboard + .set_text( + std::borrow::Cow::Owned(item.text().unwrap_or_default()), + clipboard::ClipboardKind::Clipboard, + clipboard::WaitConfig::None, + ) + .context("X11: Failed to write to clipboard (clipboard)") + .log_with_level(log::Level::Debug); + state.clipboard_item.replace(item); + } + + fn read_from_primary(&self) -> Option { + let state = self.0.borrow_mut(); + state + .clipboard + .get_any(clipboard::ClipboardKind::Primary) + .context("X11: Failed to read from clipboard (primary)") + .log_with_level(log::Level::Debug) + } + + fn read_from_clipboard(&self) -> Option { + let state = self.0.borrow_mut(); + // if the last copy was from this app, return our cached item + // which has metadata attached. + if state + .clipboard + .is_owner(clipboard::ClipboardKind::Clipboard) + { + return state.clipboard_item.clone(); + } + state + .clipboard + .get_any(clipboard::ClipboardKind::Clipboard) + .context("X11: Failed to read from clipboard (clipboard)") + .log_with_level(log::Level::Debug) + } + + fn run(&self) { + let Some(mut event_loop) = self + .0 + .borrow_mut() + .event_loop + .take() + .context("X11Client::run called but it's already running") + .log_err() + else { + return; + }; + + event_loop.run(None, &mut self.clone(), |_| {}).log_err(); + } + + fn active_window(&self) -> Option { + let state = self.0.borrow(); + state.keyboard_focused_window.and_then(|focused_window| { + state + .windows + .get(&focused_window) + .map(|window| window.handle()) + }) + } + + fn window_stack(&self) -> Option> { + let state = self.0.borrow(); + let root = state.xcb_connection.setup().roots[state.x_root_index].root; + + let reply = state + .xcb_connection + .get_property( + false, + root, + state.atoms._NET_CLIENT_LIST_STACKING, + xproto::AtomEnum::WINDOW, + 0, + u32::MAX, + ) + .ok()? + .reply() + .ok()?; + + let window_ids = reply + .value + .chunks_exact(4) + .filter_map(|chunk| chunk.try_into().ok().map(u32::from_ne_bytes)) + .collect::>(); + + let mut handles = Vec::new(); + + // We need to reverse, since _NET_CLIENT_LIST_STACKING has + // a back-to-front order. + // See: https://specifications.freedesktop.org/wm-spec/1.3/ar01s03.html + for window_ref in window_ids + .iter() + .rev() + .filter_map(|&win| state.windows.get(&win)) + { + if !window_ref.window.state.borrow().destroyed { + handles.push(window_ref.handle()); + } + } + + Some(handles) + } + + fn window_identifier(&self) -> impl Future> + Send + 'static { + let state = self.0.borrow(); + state + .keyboard_focused_window + .and_then(|focused_window| state.windows.get(&focused_window)) + .map(|window| window.window.x_window as u64) + .map(|x_window| std::future::ready(Some(WindowIdentifier::from_xid(x_window)))) + .unwrap_or(std::future::ready(None)) + } +} + +impl X11ClientState { + fn has_xim(&self) -> bool { + self.ximc.is_some() && self.xim_handler.is_some() + } + + fn take_xim(&mut self) -> Option<(X11rbClient>, XimHandler)> { + let ximc = self + .ximc + .take() + .ok_or(anyhow!("bug: XIM connection not set")) + .log_err()?; + if let Some(xim_handler) = self.xim_handler.take() { + Some((ximc, xim_handler)) + } else { + self.ximc = Some(ximc); + log::error!("bug: XIM handler not set"); + None + } + } + + fn restore_xim(&mut self, ximc: X11rbClient>, xim_handler: XimHandler) { + self.ximc = Some(ximc); + self.xim_handler = Some(xim_handler); + } + + fn update_refresh_loop(&mut self, x_window: xproto::Window) { + let Some(window_ref) = self.windows.get_mut(&x_window) else { + return; + }; + let is_visible = window_ref.is_mapped + && !matches!(window_ref.last_visibility, Visibility::FULLY_OBSCURED); + match (is_visible, window_ref.refresh_state.take()) { + (false, refresh_state @ Some(RefreshState::Hidden { .. })) + | (false, refresh_state @ None) + | (true, refresh_state @ Some(RefreshState::PeriodicRefresh { .. })) => { + window_ref.refresh_state = refresh_state; + } + ( + false, + Some(RefreshState::PeriodicRefresh { + refresh_rate, + event_loop_token, + }), + ) => { + self.loop_handle.remove(event_loop_token); + window_ref.refresh_state = Some(RefreshState::Hidden { refresh_rate }); + } + (true, Some(RefreshState::Hidden { refresh_rate })) => { + let event_loop_token = self.start_refresh_loop(x_window, refresh_rate); + let Some(window_ref) = self.windows.get_mut(&x_window) else { + return; + }; + window_ref.refresh_state = Some(RefreshState::PeriodicRefresh { + refresh_rate, + event_loop_token, + }); + } + (true, None) => { + let Some(screen_resources) = get_reply( + || "Failed to get screen resources", + self.xcb_connection + .randr_get_screen_resources_current(x_window), + ) + .log_err() else { + return; + }; + + // Ideally this would be re-queried when the window changes screens, but there + // doesn't seem to be an efficient / straightforward way to do this. Should also be + // updated when screen configurations change. + let mode_info = screen_resources.crtcs.iter().find_map(|crtc| { + let crtc_info = self + .xcb_connection + .randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME) + .ok()? + .reply() + .ok()?; + + screen_resources + .modes + .iter() + .find(|m| m.id == crtc_info.mode) + }); + let refresh_rate = match mode_info { + Some(mode_info) => mode_refresh_rate(mode_info), + None => { + log::error!( + "Failed to get screen mode info from xrandr, \ + defaulting to 60hz refresh rate." + ); + Duration::from_micros(1_000_000 / 60) + } + }; + + let event_loop_token = self.start_refresh_loop(x_window, refresh_rate); + let Some(window_ref) = self.windows.get_mut(&x_window) else { + return; + }; + window_ref.refresh_state = Some(RefreshState::PeriodicRefresh { + refresh_rate, + event_loop_token, + }); + } + } + } + + #[must_use] + fn start_refresh_loop( + &self, + x_window: xproto::Window, + refresh_rate: Duration, + ) -> RegistrationToken { + self.loop_handle + .insert_source(calloop::timer::Timer::immediate(), { + move |mut instant, (), client| { + let xcb_connection = { + let mut state = client.0.borrow_mut(); + let xcb_connection = state.xcb_connection.clone(); + if let Some(window) = state.windows.get_mut(&x_window) { + let expose_event_received = window.expose_event_received; + window.expose_event_received = false; + let window = window.window.clone(); + drop(state); + window.refresh(RequestFrameOptions { + require_presentation: expose_event_received, + force_render: false, + }); + } + xcb_connection + }; + client.process_x11_events(&xcb_connection).log_err(); + + // Take into account that some frames have been skipped + let now = Instant::now(); + while instant < now { + instant += refresh_rate; + } + calloop::timer::TimeoutAction::ToInstant(instant) + } + }) + .expect("Failed to initialize window refresh timer") + } + + fn get_cursor_icon(&mut self, style: CursorStyle) -> Option { + if let Some(cursor) = self.cursor_cache.get(&style) { + return *cursor; + } + + let mut result; + match style { + CursorStyle::None => match create_invisible_cursor(&self.xcb_connection) { + Ok(loaded_cursor) => result = Ok(loaded_cursor), + Err(err) => result = Err(err.context("X11: error while creating invisible cursor")), + }, + _ => 'outer: { + let mut errors = String::new(); + let cursor_icon_names = style.to_icon_names(); + for cursor_icon_name in cursor_icon_names { + match self + .cursor_handle + .load_cursor(&self.xcb_connection, cursor_icon_name) + { + Ok(loaded_cursor) => { + if loaded_cursor != x11rb::NONE { + result = Ok(loaded_cursor); + break 'outer; + } + } + Err(err) => { + errors.push_str(&err.to_string()); + errors.push('\n'); + } + } + } + if errors.is_empty() { + result = Err(anyhow!( + "errors while loading cursor icons {:?}:\n{}", + cursor_icon_names, + errors + )); + } else { + result = Err(anyhow!("did not find cursor icons {:?}", cursor_icon_names)); + } + } + }; + + let cursor = match result { + Ok(cursor) => Some(cursor), + Err(err) => { + match self + .cursor_handle + .load_cursor(&self.xcb_connection, DEFAULT_CURSOR_ICON_NAME) + { + Ok(default) => { + log_cursor_icon_warning(err.context(format!( + "X11: error loading cursor icon, falling back on default icon '{}'", + DEFAULT_CURSOR_ICON_NAME + ))); + Some(default) + } + Err(default_err) => { + log_cursor_icon_warning(err.context(default_err).context(format!( + "X11: error loading default cursor fallback '{}'", + DEFAULT_CURSOR_ICON_NAME + ))); + None + } + } + } + }; + + self.cursor_cache.insert(style, cursor); + cursor + } +} + +// Adapted from: +// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111 +pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration { + if mode.dot_clock == 0 || mode.htotal == 0 || mode.vtotal == 0 { + return Duration::from_millis(16); + } + + let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64); + let micros = 1_000_000_000 / millihertz; + log::info!("Refreshing every {}ms", micros / 1_000); + Duration::from_micros(micros) +} + +fn fp3232_to_f32(value: xinput::Fp3232) -> f32 { + value.integral as f32 + value.frac as f32 / u32::MAX as f32 +} + +fn check_compositor_present(xcb_connection: &XCBConnection, root: u32) -> bool { + // Method 1: Check for _NET_WM_CM_S{root} + let atom_name = format!("_NET_WM_CM_S{}", root); + let atom1 = get_reply( + || format!("Failed to intern {atom_name}"), + xcb_connection.intern_atom(false, atom_name.as_bytes()), + ); + let method1 = match atom1.log_with_level(Level::Debug) { + Some(reply) if reply.atom != x11rb::NONE => { + let atom = reply.atom; + get_reply( + || format!("Failed to get {atom_name} owner"), + xcb_connection.get_selection_owner(atom), + ) + .map(|reply| reply.owner != 0) + .log_with_level(Level::Debug) + .unwrap_or(false) + } + _ => false, + }; + + // Method 2: Check for _NET_WM_CM_OWNER + let atom_name = "_NET_WM_CM_OWNER"; + let atom2 = get_reply( + || format!("Failed to intern {atom_name}"), + xcb_connection.intern_atom(false, atom_name.as_bytes()), + ); + let method2 = match atom2.log_with_level(Level::Debug) { + Some(reply) if reply.atom != x11rb::NONE => { + let atom = reply.atom; + get_reply( + || format!("Failed to get {atom_name}"), + xcb_connection.get_property(false, root, atom, xproto::AtomEnum::WINDOW, 0, 1), + ) + .map(|reply| reply.value_len > 0) + .unwrap_or(false) + } + _ => return false, + }; + + // Method 3: Check for _NET_SUPPORTING_WM_CHECK + let atom_name = "_NET_SUPPORTING_WM_CHECK"; + let atom3 = get_reply( + || format!("Failed to intern {atom_name}"), + xcb_connection.intern_atom(false, atom_name.as_bytes()), + ); + let method3 = match atom3.log_with_level(Level::Debug) { + Some(reply) if reply.atom != x11rb::NONE => { + let atom = reply.atom; + get_reply( + || format!("Failed to get {atom_name}"), + xcb_connection.get_property(false, root, atom, xproto::AtomEnum::WINDOW, 0, 1), + ) + .map(|reply| reply.value_len > 0) + .unwrap_or(false) + } + _ => return false, + }; + + log::debug!( + "Compositor detection: _NET_WM_CM_S?={}, _NET_WM_CM_OWNER={}, _NET_SUPPORTING_WM_CHECK={}", + method1, + method2, + method3 + ); + + method1 || method2 || method3 +} + +fn check_gtk_frame_extents_supported( + xcb_connection: &XCBConnection, + atoms: &XcbAtoms, + root: xproto::Window, +) -> bool { + let Some(supported_atoms) = get_reply( + || "Failed to get _NET_SUPPORTED", + xcb_connection.get_property( + false, + root, + atoms._NET_SUPPORTED, + xproto::AtomEnum::ATOM, + 0, + 1024, + ), + ) + .log_with_level(Level::Debug) else { + return false; + }; + + let supported_atom_ids: Vec = supported_atoms + .value + .chunks_exact(4) + .filter_map(|chunk| chunk.try_into().ok().map(u32::from_ne_bytes)) + .collect(); + + supported_atom_ids.contains(&atoms._GTK_FRAME_EXTENTS) +} + +fn xdnd_is_atom_supported(atom: u32, atoms: &XcbAtoms) -> bool { + atom == atoms.TEXT + || atom == atoms.STRING + || atom == atoms.UTF8_STRING + || atom == atoms.TEXT_PLAIN + || atom == atoms.TEXT_PLAIN_UTF8 + || atom == atoms.TextUriList +} + +fn xdnd_get_supported_atom( + xcb_connection: &XCBConnection, + supported_atoms: &XcbAtoms, + target: xproto::Window, +) -> u32 { + if let Some(reply) = get_reply( + || "Failed to get XDnD supported atoms", + xcb_connection.get_property( + false, + target, + supported_atoms.XdndTypeList, + AtomEnum::ANY, + 0, + 1024, + ), + ) + .log_with_level(Level::Warn) + && let Some(atoms) = reply.value32() + { + for atom in atoms { + if xdnd_is_atom_supported(atom, supported_atoms) { + return atom; + } + } + } + 0 +} + +fn xdnd_send_finished( + xcb_connection: &XCBConnection, + atoms: &XcbAtoms, + source: xproto::Window, + target: xproto::Window, +) { + let message = ClientMessageEvent { + format: 32, + window: target, + type_: atoms.XdndFinished, + data: ClientMessageData::from([source, 1, atoms.XdndActionCopy, 0, 0]), + sequence: 0, + response_type: xproto::CLIENT_MESSAGE_EVENT, + }; + check_reply( + || "Failed to send XDnD finished event", + xcb_connection.send_event(false, target, EventMask::default(), message), + ) + .log_err(); + xcb_connection.flush().log_err(); +} + +fn xdnd_send_status( + xcb_connection: &XCBConnection, + atoms: &XcbAtoms, + source: xproto::Window, + target: xproto::Window, + action: u32, +) { + let message = ClientMessageEvent { + format: 32, + window: target, + type_: atoms.XdndStatus, + data: ClientMessageData::from([source, 1, 0, 0, action]), + sequence: 0, + response_type: xproto::CLIENT_MESSAGE_EVENT, + }; + check_reply( + || "Failed to send XDnD status event", + xcb_connection.send_event(false, target, EventMask::default(), message), + ) + .log_err(); + xcb_connection.flush().log_err(); +} + +/// Recomputes `pointer_device_states` by querying all pointer devices. +/// When a device is present in `scroll_values_to_preserve`, its value for `ScrollAxisState.scroll_value` is used. +fn current_pointer_device_states( + xcb_connection: &XCBConnection, + scroll_values_to_preserve: &BTreeMap, +) -> Option> { + let devices_query_result = get_reply( + || "Failed to query XInput devices", + xcb_connection.xinput_xi_query_device(XINPUT_ALL_DEVICES), + ) + .log_err()?; + + let mut pointer_device_states = BTreeMap::new(); + pointer_device_states.extend( + devices_query_result + .infos + .iter() + .filter(|info| is_pointer_device(info.type_)) + .filter_map(|info| { + let scroll_data = info + .classes + .iter() + .filter_map(|class| class.data.as_scroll()) + .copied() + .rev() + .collect::>(); + let old_state = scroll_values_to_preserve.get(&info.deviceid); + let old_horizontal = old_state.map(|state| &state.horizontal); + let old_vertical = old_state.map(|state| &state.vertical); + let horizontal = scroll_data + .iter() + .find(|data| data.scroll_type == xinput::ScrollType::HORIZONTAL) + .map(|data| scroll_data_to_axis_state(data, old_horizontal)); + let vertical = scroll_data + .iter() + .find(|data| data.scroll_type == xinput::ScrollType::VERTICAL) + .map(|data| scroll_data_to_axis_state(data, old_vertical)); + if horizontal.is_none() && vertical.is_none() { + None + } else { + Some(( + info.deviceid, + PointerDeviceState { + horizontal: horizontal.unwrap_or_else(Default::default), + vertical: vertical.unwrap_or_else(Default::default), + }, + )) + } + }), + ); + if pointer_device_states.is_empty() { + log::error!("Found no xinput mouse pointers."); + } + Some(pointer_device_states) +} + +/// Returns true if the device is a pointer device. Does not include pointer device groups. +fn is_pointer_device(type_: xinput::DeviceType) -> bool { + type_ == xinput::DeviceType::SLAVE_POINTER +} + +fn scroll_data_to_axis_state( + data: &xinput::DeviceClassDataScroll, + old_axis_state_with_valid_scroll_value: Option<&ScrollAxisState>, +) -> ScrollAxisState { + ScrollAxisState { + valuator_number: Some(data.number), + multiplier: SCROLL_LINES / fp3232_to_f32(data.increment), + scroll_value: old_axis_state_with_valid_scroll_value.and_then(|state| state.scroll_value), + } +} + +fn reset_all_pointer_device_scroll_positions( + pointer_device_states: &mut BTreeMap, +) { + pointer_device_states + .iter_mut() + .for_each(|(_, device_state)| reset_pointer_device_scroll_positions(device_state)); +} + +fn reset_pointer_device_scroll_positions(pointer: &mut PointerDeviceState) { + pointer.horizontal.scroll_value = None; + pointer.vertical.scroll_value = None; +} + +/// Returns the scroll delta for a smooth scrolling motion event, or `None` if no scroll data is present. +fn get_scroll_delta_and_update_state( + pointer: &mut PointerDeviceState, + event: &xinput::MotionEvent, +) -> Option> { + let delta_x = get_axis_scroll_delta_and_update_state(event, &mut pointer.horizontal); + let delta_y = get_axis_scroll_delta_and_update_state(event, &mut pointer.vertical); + if delta_x.is_some() || delta_y.is_some() { + Some(Point::new(delta_x.unwrap_or(0.0), delta_y.unwrap_or(0.0))) + } else { + None + } +} + +fn get_axis_scroll_delta_and_update_state( + event: &xinput::MotionEvent, + axis: &mut ScrollAxisState, +) -> Option { + let axis_index = get_valuator_axis_index(&event.valuator_mask, axis.valuator_number?)?; + if let Some(axis_value) = event.axisvalues.get(axis_index) { + let new_scroll = fp3232_to_f32(*axis_value); + let delta_scroll = axis + .scroll_value + .map(|old_scroll| (old_scroll - new_scroll) * axis.multiplier); + axis.scroll_value = Some(new_scroll); + delta_scroll + } else { + log::error!("Encountered invalid XInput valuator_mask, scrolling may not work properly."); + None + } +} + +fn make_scroll_wheel_event( + position: Point, + scroll_delta: Point, + modifiers: Modifiers, +) -> crate::ScrollWheelEvent { + // When shift is held down, vertical scrolling turns into horizontal scrolling. + let delta = if modifiers.shift { + Point { + x: scroll_delta.y, + y: 0.0, + } + } else { + scroll_delta + }; + crate::ScrollWheelEvent { + position, + delta: ScrollDelta::Lines(delta), + modifiers, + touch_phase: TouchPhase::default(), + } +} + +fn create_invisible_cursor( + connection: &XCBConnection, +) -> anyhow::Result { + let empty_pixmap = connection.generate_id()?; + let root = connection.setup().roots[0].root; + connection.create_pixmap(1, empty_pixmap, root, 1, 1)?; + + let cursor = connection.generate_id()?; + connection.create_cursor(cursor, empty_pixmap, empty_pixmap, 0, 0, 0, 0, 0, 0, 0, 0)?; + + connection.free_pixmap(empty_pixmap)?; + + xcb_flush(connection); + Ok(cursor) +} + +enum DpiMode { + Randr, + Scale(f32), + NotSet, +} + +fn get_scale_factor( + connection: &XCBConnection, + resource_database: &Database, + screen_index: usize, +) -> f32 { + let env_dpi = std::env::var(GPUI_X11_SCALE_FACTOR_ENV) + .ok() + .map(|var| { + if var.to_lowercase() == "randr" { + DpiMode::Randr + } else if let Ok(scale) = var.parse::() { + if valid_scale_factor(scale) { + DpiMode::Scale(scale) + } else { + panic!( + "`{}` must be a positive normal number or `randr`. Got `{}`", + GPUI_X11_SCALE_FACTOR_ENV, var + ); + } + } else if var.is_empty() { + DpiMode::NotSet + } else { + panic!( + "`{}` must be a positive number or `randr`. Got `{}`", + GPUI_X11_SCALE_FACTOR_ENV, var + ); + } + }) + .unwrap_or(DpiMode::NotSet); + + match env_dpi { + DpiMode::Scale(scale) => { + log::info!( + "Using scale factor from {}: {}", + GPUI_X11_SCALE_FACTOR_ENV, + scale + ); + return scale; + } + DpiMode::Randr => { + if let Some(scale) = get_randr_scale_factor(connection, screen_index) { + log::info!( + "Using RandR scale factor from {}=randr: {}", + GPUI_X11_SCALE_FACTOR_ENV, + scale + ); + return scale; + } + log::warn!("Failed to calculate RandR scale factor, falling back to default"); + return 1.0; + } + DpiMode::NotSet => {} + } + + // TODO: Use scale factor from XSettings here + + if let Some(dpi) = resource_database + .get_value::("Xft.dpi", "Xft.dpi") + .ok() + .flatten() + { + let scale = dpi / 96.0; // base dpi + log::info!("Using scale factor from Xft.dpi: {}", scale); + return scale; + } + + if let Some(scale) = get_randr_scale_factor(connection, screen_index) { + log::info!("Using RandR scale factor: {}", scale); + return scale; + } + + log::info!("Using default scale factor: 1.0"); + 1.0 +} + +fn get_randr_scale_factor(connection: &XCBConnection, screen_index: usize) -> Option { + let root = connection.setup().roots.get(screen_index)?.root; + + let version_cookie = connection.randr_query_version(1, 6).ok()?; + let version_reply = version_cookie.reply().ok()?; + if version_reply.major_version < 1 + || (version_reply.major_version == 1 && version_reply.minor_version < 5) + { + return legacy_get_randr_scale_factor(connection, root); // for randr <1.5 + } + + let monitors_cookie = connection.randr_get_monitors(root, true).ok()?; // true for active only + let monitors_reply = monitors_cookie.reply().ok()?; + + let mut fallback_scale: Option = None; + for monitor in monitors_reply.monitors { + if monitor.width_in_millimeters == 0 || monitor.height_in_millimeters == 0 { + continue; + } + let scale_factor = get_dpi_factor( + (monitor.width as u32, monitor.height as u32), + ( + monitor.width_in_millimeters as u64, + monitor.height_in_millimeters as u64, + ), + ); + if monitor.primary { + return Some(scale_factor); + } else if fallback_scale.is_none() { + fallback_scale = Some(scale_factor); + } + } + + fallback_scale +} + +fn legacy_get_randr_scale_factor(connection: &XCBConnection, root: u32) -> Option { + let primary_cookie = connection.randr_get_output_primary(root).ok()?; + let primary_reply = primary_cookie.reply().ok()?; + let primary_output = primary_reply.output; + + let primary_output_cookie = connection + .randr_get_output_info(primary_output, x11rb::CURRENT_TIME) + .ok()?; + let primary_output_info = primary_output_cookie.reply().ok()?; + + // try primary + if primary_output_info.connection == randr::Connection::CONNECTED + && primary_output_info.mm_width > 0 + && primary_output_info.mm_height > 0 + && primary_output_info.crtc != 0 + { + let crtc_cookie = connection + .randr_get_crtc_info(primary_output_info.crtc, x11rb::CURRENT_TIME) + .ok()?; + let crtc_info = crtc_cookie.reply().ok()?; + + if crtc_info.width > 0 && crtc_info.height > 0 { + let scale_factor = get_dpi_factor( + (crtc_info.width as u32, crtc_info.height as u32), + ( + primary_output_info.mm_width as u64, + primary_output_info.mm_height as u64, + ), + ); + return Some(scale_factor); + } + } + + // fallback: full scan + let resources_cookie = connection.randr_get_screen_resources_current(root).ok()?; + let screen_resources = resources_cookie.reply().ok()?; + + let mut crtc_cookies = Vec::with_capacity(screen_resources.crtcs.len()); + for &crtc in &screen_resources.crtcs { + if let Ok(cookie) = connection.randr_get_crtc_info(crtc, x11rb::CURRENT_TIME) { + crtc_cookies.push((crtc, cookie)); + } + } + + let mut crtc_infos: HashMap = HashMap::default(); + let mut valid_outputs: HashSet = HashSet::new(); + for (crtc, cookie) in crtc_cookies { + if let Ok(reply) = cookie.reply() + && reply.width > 0 + && reply.height > 0 + && !reply.outputs.is_empty() + { + crtc_infos.insert(crtc, reply.clone()); + valid_outputs.extend(&reply.outputs); + } + } + + if valid_outputs.is_empty() { + return None; + } + + let mut output_cookies = Vec::with_capacity(valid_outputs.len()); + for &output in &valid_outputs { + if let Ok(cookie) = connection.randr_get_output_info(output, x11rb::CURRENT_TIME) { + output_cookies.push((output, cookie)); + } + } + let mut output_infos: HashMap = HashMap::default(); + for (output, cookie) in output_cookies { + if let Ok(reply) = cookie.reply() { + output_infos.insert(output, reply); + } + } + + let mut fallback_scale: Option = None; + for crtc_info in crtc_infos.values() { + for &output in &crtc_info.outputs { + if let Some(output_info) = output_infos.get(&output) { + if output_info.connection != randr::Connection::CONNECTED { + continue; + } + + if output_info.mm_width == 0 || output_info.mm_height == 0 { + continue; + } + + let scale_factor = get_dpi_factor( + (crtc_info.width as u32, crtc_info.height as u32), + (output_info.mm_width as u64, output_info.mm_height as u64), + ); + + if output != primary_output && fallback_scale.is_none() { + fallback_scale = Some(scale_factor); + } + } + } + } + + fallback_scale +} + +fn get_dpi_factor((width_px, height_px): (u32, u32), (width_mm, height_mm): (u64, u64)) -> f32 { + let ppmm = ((width_px as f64 * height_px as f64) / (width_mm as f64 * height_mm as f64)).sqrt(); // pixels per mm + + const MM_PER_INCH: f64 = 25.4; + const BASE_DPI: f64 = 96.0; + const QUANTIZE_STEP: f64 = 12.0; // e.g. 1.25 = 15/12, 1.5 = 18/12, 1.75 = 21/12, 2.0 = 24/12 + const MIN_SCALE: f64 = 1.0; + const MAX_SCALE: f64 = 20.0; + + let dpi_factor = + ((ppmm * (QUANTIZE_STEP * MM_PER_INCH / BASE_DPI)).round() / QUANTIZE_STEP).max(MIN_SCALE); + + let validated_factor = if dpi_factor <= MAX_SCALE { + dpi_factor + } else { + MIN_SCALE + }; + + if valid_scale_factor(validated_factor as f32) { + validated_factor as f32 + } else { + log::warn!( + "Calculated DPI factor {} is invalid, using 1.0", + validated_factor + ); + 1.0 + } +} + +#[inline] +fn valid_scale_factor(scale_factor: f32) -> bool { + scale_factor.is_sign_positive() && scale_factor.is_normal() +} diff --git a/third_party/gpui/src/platform/linux/x11/clipboard.rs b/third_party/gpui/src/platform/linux/x11/clipboard.rs new file mode 100644 index 0000000..65ad16e --- /dev/null +++ b/third_party/gpui/src/platform/linux/x11/clipboard.rs @@ -0,0 +1,1270 @@ +/* + * Copyright 2022 - 2025 Zed Industries, Inc. + * License: Apache-2.0 + * See LICENSE-APACHE for complete license terms + * + * Adapted from the x11 submodule of the arboard project https://github.com/1Password/arboard + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + * Copyright 2022 The Arboard contributors + * + * The project to which this file belongs is licensed under either of + * the Apache 2.0 or the MIT license at the licensee's choice. The terms + * and conditions of the chosen license apply to this file. +*/ + +// More info about using the clipboard on X11: +// https://tronche.com/gui/x/icccm/sec-2.html#s-2.6 +// https://freedesktop.org/wiki/ClipboardManager/ + +use std::{ + borrow::Cow, + cell::RefCell, + collections::{HashMap, hash_map::Entry}, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, + thread::JoinHandle, + thread_local, + time::{Duration, Instant}, +}; + +use parking_lot::{Condvar, Mutex, MutexGuard, RwLock}; +use x11rb::{ + COPY_DEPTH_FROM_PARENT, COPY_FROM_PARENT, NONE, + connection::Connection, + protocol::{ + Event, + xproto::{ + Atom, AtomEnum, ConnectionExt as _, CreateWindowAux, EventMask, PropMode, Property, + PropertyNotifyEvent, SELECTION_NOTIFY_EVENT, SelectionNotifyEvent, + SelectionRequestEvent, Time, WindowClass, + }, + }, + rust_connection::RustConnection, + wrapper::ConnectionExt as _, +}; + +use crate::{ClipboardItem, Image, ImageFormat, hash}; + +type Result = std::result::Result; + +static CLIPBOARD: Mutex> = parking_lot::const_mutex(None); + +x11rb::atom_manager! { + pub Atoms: AtomCookies { + CLIPBOARD, + PRIMARY, + SECONDARY, + + CLIPBOARD_MANAGER, + SAVE_TARGETS, + TARGETS, + ATOM, + INCR, + + UTF8_STRING, + UTF8_MIME_0: b"text/plain;charset=utf-8", + UTF8_MIME_1: b"text/plain;charset=UTF-8", + // Text in ISO Latin-1 encoding + // See: https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2 + STRING, + // Text in unknown encoding + // See: https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2 + TEXT, + TEXT_MIME_UNKNOWN: b"text/plain", + + // HTML: b"text/html", + // URI_LIST: b"text/uri-list", + + PNG__MIME: ImageFormat::mime_type(ImageFormat::Png ).as_bytes(), + JPEG_MIME: ImageFormat::mime_type(ImageFormat::Jpeg).as_bytes(), + WEBP_MIME: ImageFormat::mime_type(ImageFormat::Webp).as_bytes(), + GIF__MIME: ImageFormat::mime_type(ImageFormat::Gif ).as_bytes(), + SVG__MIME: ImageFormat::mime_type(ImageFormat::Svg ).as_bytes(), + BMP__MIME: ImageFormat::mime_type(ImageFormat::Bmp ).as_bytes(), + TIFF_MIME: ImageFormat::mime_type(ImageFormat::Tiff).as_bytes(), + + // This is just some random name for the property on our window, into which + // the clipboard owner writes the data we requested. + ARBOARD_CLIPBOARD, + } +} + +thread_local! { + static ATOM_NAME_CACHE: RefCell> = Default::default(); +} + +// Some clipboard items, like images, may take a very long time to produce a +// `SelectionNotify`. Multiple seconds long. +const LONG_TIMEOUT_DUR: Duration = Duration::from_millis(4000); +const SHORT_TIMEOUT_DUR: Duration = Duration::from_millis(10); + +#[derive(Debug, PartialEq, Eq)] +enum ManagerHandoverState { + Idle, + InProgress, + Finished, +} + +struct GlobalClipboard { + inner: Arc, + + /// Join handle to the thread which serves selection requests. + server_handle: JoinHandle<()>, +} + +struct XContext { + conn: RustConnection, + win_id: u32, +} + +struct Inner { + /// The context for the thread which serves clipboard read + /// requests coming to us. + server: XContext, + atoms: Atoms, + + clipboard: Selection, + primary: Selection, + secondary: Selection, + + handover_state: Mutex, + handover_cv: Condvar, + + serve_stopped: AtomicBool, +} + +impl XContext { + fn new() -> Result { + // create a new connection to an X11 server + let (conn, screen_num): (RustConnection, _) = + RustConnection::connect(None).map_err(|_| { + Error::unknown("X11 server connection timed out because it was unreachable") + })?; + let screen = conn + .setup() + .roots + .get(screen_num) + .ok_or(Error::unknown("no screen found"))?; + let win_id = conn.generate_id().map_err(into_unknown)?; + + let event_mask = + // Just in case that some program reports SelectionNotify events + // with XCB_EVENT_MASK_PROPERTY_CHANGE mask. + EventMask::PROPERTY_CHANGE | + // To receive DestroyNotify event and stop the message loop. + EventMask::STRUCTURE_NOTIFY; + // create the window + conn.create_window( + // copy as much as possible from the parent, because no other specific input is needed + COPY_DEPTH_FROM_PARENT, + win_id, + screen.root, + 0, + 0, + 1, + 1, + 0, + WindowClass::COPY_FROM_PARENT, + COPY_FROM_PARENT, + // don't subscribe to any special events because we are requesting everything we need ourselves + &CreateWindowAux::new().event_mask(event_mask), + ) + .map_err(into_unknown)?; + conn.flush().map_err(into_unknown)?; + + Ok(Self { conn, win_id }) + } +} + +#[derive(Default)] +struct Selection { + data: RwLock>>, + /// Mutex around nothing to use with the below condvar. + mutex: Mutex<()>, + /// A condvar that is notified when the contents of this clipboard are changed. + /// + /// This is associated with `Self::mutex`. + data_changed: Condvar, +} + +#[derive(Debug, Clone)] +struct ClipboardData { + bytes: Vec, + + /// The atom representing the format in which the data is encoded. + format: Atom, +} + +enum ReadSelNotifyResult { + GotData(ClipboardData), + IncrStarted, + EventNotRecognized, +} + +impl Inner { + fn new() -> Result { + let server = XContext::new()?; + let atoms = Atoms::new(&server.conn) + .map_err(into_unknown)? + .reply() + .map_err(into_unknown)?; + + Ok(Self { + server, + atoms, + clipboard: Selection::default(), + primary: Selection::default(), + secondary: Selection::default(), + handover_state: Mutex::new(ManagerHandoverState::Idle), + handover_cv: Condvar::new(), + serve_stopped: AtomicBool::new(false), + }) + } + + fn write( + &self, + data: Vec, + selection: ClipboardKind, + wait: WaitConfig, + ) -> Result<()> { + if self.serve_stopped.load(Ordering::Relaxed) { + return Err(Error::unknown( + "The clipboard handler thread seems to have stopped. Logging messages may reveal the cause. (See the `log` crate.)", + )); + } + + let server_win = self.server.win_id; + + // ICCCM version 2, section 2.6.1.3 states that we should re-assert ownership whenever data + // changes. + self.server + .conn + .set_selection_owner(server_win, self.atom_of(selection), Time::CURRENT_TIME) + .map_err(|_| Error::ClipboardOccupied)?; + + self.server.conn.flush().map_err(into_unknown)?; + + // Just setting the data, and the `serve_requests` will take care of the rest. + let selection = self.selection_of(selection); + let mut data_guard = selection.data.write(); + *data_guard = Some(data); + + // Lock the mutex to both ensure that no wakers of `data_changed` can wake us between + // dropping the `data_guard` and calling `wait[_for]` and that we don't we wake other + // threads in that position. + let mut guard = selection.mutex.lock(); + + // Notify any existing waiting threads that we have changed the data in the selection. + // It is important that the mutex is locked to prevent this notification getting lost. + selection.data_changed.notify_all(); + + match wait { + WaitConfig::None => {} + WaitConfig::Forever => { + drop(data_guard); + selection.data_changed.wait(&mut guard); + } + WaitConfig::Until(deadline) => { + drop(data_guard); + selection.data_changed.wait_until(&mut guard, deadline); + } + } + + Ok(()) + } + + /// `formats` must be a slice of atoms, where each atom represents a target format. + /// The first format from `formats`, which the clipboard owner supports will be the + /// format of the return value. + fn read(&self, formats: &[Atom], selection: ClipboardKind) -> Result { + // if we are the current owner, we can get the current clipboard ourselves + if self.is_owner(selection)? { + let data = self.selection_of(selection).data.read(); + if let Some(data_list) = &*data { + for data in data_list { + for format in formats { + if *format == data.format { + return Ok(data.clone()); + } + } + } + } + return Err(Error::ContentNotAvailable); + } + let reader = XContext::new()?; + + let highest_precedence_format = + match self.read_single(&reader, selection, self.atoms.TARGETS) { + Err(err) => { + log::trace!("Clipboard TARGETS query failed with {err:?}"); + None + } + Ok(ClipboardData { bytes, format }) => { + if format == self.atoms.ATOM { + let available_formats = Self::parse_formats(&bytes); + formats + .iter() + .find(|format| available_formats.contains(format)) + } else { + log::trace!( + "Unexpected clipboard TARGETS format {}", + self.atom_name(format) + ); + None + } + } + }; + + if let Some(&format) = highest_precedence_format { + let data = self.read_single(&reader, selection, format)?; + if !formats.contains(&data.format) { + // This shouldn't happen since the format is from the TARGETS list. + log::trace!( + "Conversion to {} responded with {} which is not supported", + self.atom_name(format), + self.atom_name(data.format), + ); + return Err(Error::ConversionFailure); + } + return Ok(data); + } + + log::trace!("Falling back on attempting to convert clipboard to each format."); + for format in formats { + match self.read_single(&reader, selection, *format) { + Ok(data) => { + if formats.contains(&data.format) { + return Ok(data); + } else { + log::trace!( + "Conversion to {} responded with {} which is not supported", + self.atom_name(*format), + self.atom_name(data.format), + ); + continue; + } + } + Err(Error::ContentNotAvailable) => { + continue; + } + Err(e) => { + log::trace!("Conversion to {} failed: {}", self.atom_name(*format), e); + return Err(e); + } + } + } + log::trace!("All conversions to supported formats failed."); + Err(Error::ContentNotAvailable) + } + + fn parse_formats(bytes: &[u8]) -> Vec { + bytes + .chunks_exact(4) + .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) + .collect() + } + + fn read_single( + &self, + reader: &XContext, + selection: ClipboardKind, + target_format: Atom, + ) -> Result { + // Delete the property so that we can detect (using property notify) + // when the selection owner receives our request. + reader + .conn + .delete_property(reader.win_id, self.atoms.ARBOARD_CLIPBOARD) + .map_err(into_unknown)?; + + // request to convert the clipboard selection to our data type(s) + reader + .conn + .convert_selection( + reader.win_id, + self.atom_of(selection), + target_format, + self.atoms.ARBOARD_CLIPBOARD, + Time::CURRENT_TIME, + ) + .map_err(into_unknown)?; + reader.conn.sync().map_err(into_unknown)?; + + log::trace!("Finished `convert_selection`"); + + let mut incr_data: Vec = Vec::new(); + let mut using_incr = false; + + let mut timeout_end = Instant::now() + LONG_TIMEOUT_DUR; + + while Instant::now() < timeout_end { + let event = reader.conn.poll_for_event().map_err(into_unknown)?; + let event = match event { + Some(e) => e, + None => { + std::thread::sleep(Duration::from_millis(1)); + continue; + } + }; + match event { + // The first response after requesting a selection. + Event::SelectionNotify(event) => { + log::trace!("Read SelectionNotify"); + let result = self.handle_read_selection_notify( + reader, + target_format, + &mut using_incr, + &mut incr_data, + event, + )?; + match result { + ReadSelNotifyResult::GotData(data) => return Ok(data), + ReadSelNotifyResult::IncrStarted => { + // This means we received an indication that an the + // data is going to be sent INCRementally. Let's + // reset our timeout. + timeout_end += SHORT_TIMEOUT_DUR; + } + ReadSelNotifyResult::EventNotRecognized => (), + } + } + // If the previous SelectionNotify event specified that the data + // will be sent in INCR segments, each segment is transferred in + // a PropertyNotify event. + Event::PropertyNotify(event) => { + let result = self.handle_read_property_notify( + reader, + target_format, + using_incr, + &mut incr_data, + &mut timeout_end, + event, + )?; + if result { + return Ok(ClipboardData { + bytes: incr_data, + format: target_format, + }); + } + } + _ => log::trace!( + "An unexpected event arrived while reading the clipboard: {:?}", + event + ), + } + } + log::info!("Time-out hit while reading the clipboard."); + Err(Error::ContentNotAvailable) + } + + fn atom_of(&self, selection: ClipboardKind) -> Atom { + match selection { + ClipboardKind::Clipboard => self.atoms.CLIPBOARD, + ClipboardKind::Primary => self.atoms.PRIMARY, + ClipboardKind::Secondary => self.atoms.SECONDARY, + } + } + + fn selection_of(&self, selection: ClipboardKind) -> &Selection { + match selection { + ClipboardKind::Clipboard => &self.clipboard, + ClipboardKind::Primary => &self.primary, + ClipboardKind::Secondary => &self.secondary, + } + } + + fn kind_of(&self, atom: Atom) -> Option { + match atom { + a if a == self.atoms.CLIPBOARD => Some(ClipboardKind::Clipboard), + a if a == self.atoms.PRIMARY => Some(ClipboardKind::Primary), + a if a == self.atoms.SECONDARY => Some(ClipboardKind::Secondary), + _ => None, + } + } + + fn is_owner(&self, selection: ClipboardKind) -> Result { + let current = self + .server + .conn + .get_selection_owner(self.atom_of(selection)) + .map_err(into_unknown)? + .reply() + .map_err(into_unknown)? + .owner; + + Ok(current == self.server.win_id) + } + + fn query_atom_name(&self, atom: x11rb::protocol::xproto::Atom) -> Result { + String::from_utf8( + self.server + .conn + .get_atom_name(atom) + .map_err(into_unknown)? + .reply() + .map_err(into_unknown)? + .name, + ) + .map_err(into_unknown) + } + + fn atom_name(&self, atom: x11rb::protocol::xproto::Atom) -> &'static str { + ATOM_NAME_CACHE.with(|cache| { + let mut cache = cache.borrow_mut(); + match cache.entry(atom) { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => { + let s = self + .query_atom_name(atom) + .map(|s| Box::leak(s.into_boxed_str()) as &str) + .unwrap_or("FAILED-TO-GET-THE-ATOM-NAME"); + entry.insert(s); + s + } + } + }) + } + + fn handle_read_selection_notify( + &self, + reader: &XContext, + target_format: u32, + using_incr: &mut bool, + incr_data: &mut Vec, + event: SelectionNotifyEvent, + ) -> Result { + // The property being set to NONE means that the `convert_selection` + // failed. + + // According to: https://tronche.com/gui/x/icccm/sec-2.html#s-2.4 + // the target must be set to the same as what we requested. + if event.property == NONE || event.target != target_format { + return Err(Error::ContentNotAvailable); + } + if self.kind_of(event.selection).is_none() { + log::info!( + "Received a SelectionNotify for a selection other than CLIPBOARD, PRIMARY or SECONDARY. This is unexpected." + ); + return Ok(ReadSelNotifyResult::EventNotRecognized); + } + if *using_incr { + log::warn!("Received a SelectionNotify while already expecting INCR segments."); + return Ok(ReadSelNotifyResult::EventNotRecognized); + } + // Accept any property type. The property type will typically match the format type except + // when it is `TARGETS` in which case it is `ATOM`. `ANY` is provided to handle the case + // where the clipboard is not convertible to the requested format. In this case + // `reply.type_` will have format information, but `bytes` will only be non-empty if `ANY` + // is provided. + let property_type = AtomEnum::ANY; + // request the selection + let mut reply = reader + .conn + .get_property( + true, + event.requestor, + event.property, + property_type, + 0, + u32::MAX / 4, + ) + .map_err(into_unknown)? + .reply() + .map_err(into_unknown)?; + + // we found something + if reply.type_ == self.atoms.INCR { + // Note that we call the get_property again because we are + // indicating that we are ready to receive the data by deleting the + // property, however deleting only works if the type matches the + // property type. But the type didn't match in the previous call. + reply = reader + .conn + .get_property( + true, + event.requestor, + event.property, + self.atoms.INCR, + 0, + u32::MAX / 4, + ) + .map_err(into_unknown)? + .reply() + .map_err(into_unknown)?; + log::trace!("Receiving INCR segments"); + *using_incr = true; + if reply.value_len == 4 { + let min_data_len = reply + .value32() + .and_then(|mut vals| vals.next()) + .unwrap_or(0); + incr_data.reserve(min_data_len as usize); + } + Ok(ReadSelNotifyResult::IncrStarted) + } else { + Ok(ReadSelNotifyResult::GotData(ClipboardData { + bytes: reply.value, + format: reply.type_, + })) + } + } + + /// Returns Ok(true) when the incr_data is ready + fn handle_read_property_notify( + &self, + reader: &XContext, + target_format: u32, + using_incr: bool, + incr_data: &mut Vec, + timeout_end: &mut Instant, + event: PropertyNotifyEvent, + ) -> Result { + if event.atom != self.atoms.ARBOARD_CLIPBOARD || event.state != Property::NEW_VALUE { + return Ok(false); + } + if !using_incr { + // This must mean the selection owner received our request, and is + // now preparing the data + return Ok(false); + } + let reply = reader + .conn + .get_property( + true, + event.window, + event.atom, + if target_format == self.atoms.TARGETS { + self.atoms.ATOM + } else { + target_format + }, + 0, + u32::MAX / 4, + ) + .map_err(into_unknown)? + .reply() + .map_err(into_unknown)?; + + // log::trace!("Received segment. value_len {}", reply.value_len,); + if reply.value_len == 0 { + // This indicates that all the data has been sent. + return Ok(true); + } + incr_data.extend(reply.value); + + // Let's reset our timeout, since we received a valid chunk. + *timeout_end = Instant::now() + SHORT_TIMEOUT_DUR; + + // Not yet complete + Ok(false) + } + + fn handle_selection_request(&self, event: SelectionRequestEvent) -> Result<()> { + let selection = match self.kind_of(event.selection) { + Some(kind) => kind, + None => { + log::warn!( + "Received a selection request to a selection other than the CLIPBOARD, PRIMARY or SECONDARY. This is unexpected." + ); + return Ok(()); + } + }; + + let success; + // we are asked for a list of supported conversion targets + if event.target == self.atoms.TARGETS { + log::trace!( + "Handling TARGETS, dst property is {}", + self.atom_name(event.property) + ); + let mut targets = Vec::with_capacity(10); + targets.push(self.atoms.TARGETS); + targets.push(self.atoms.SAVE_TARGETS); + let data = self.selection_of(selection).data.read(); + if let Some(data_list) = &*data { + for data in data_list { + targets.push(data.format); + if data.format == self.atoms.UTF8_STRING { + // When we are storing a UTF8 string, + // add all equivalent formats to the supported targets + targets.push(self.atoms.UTF8_MIME_0); + targets.push(self.atoms.UTF8_MIME_1); + } + } + } + self.server + .conn + .change_property32( + PropMode::REPLACE, + event.requestor, + event.property, + // TODO: change to `AtomEnum::ATOM` + self.atoms.ATOM, + &targets, + ) + .map_err(into_unknown)?; + self.server.conn.flush().map_err(into_unknown)?; + success = true; + } else { + log::trace!("Handling request for (probably) the clipboard contents."); + let data = self.selection_of(selection).data.read(); + if let Some(data_list) = &*data { + success = match data_list.iter().find(|d| d.format == event.target) { + Some(data) => { + self.server + .conn + .change_property8( + PropMode::REPLACE, + event.requestor, + event.property, + event.target, + &data.bytes, + ) + .map_err(into_unknown)?; + self.server.conn.flush().map_err(into_unknown)?; + true + } + None => false, + }; + } else { + // This must mean that we lost ownership of the data + // since the other side requested the selection. + // Let's respond with the property set to none. + success = false; + } + } + // on failure we notify the requester of it + let property = if success { + event.property + } else { + AtomEnum::NONE.into() + }; + // tell the requestor that we finished sending data + self.server + .conn + .send_event( + false, + event.requestor, + EventMask::NO_EVENT, + SelectionNotifyEvent { + response_type: SELECTION_NOTIFY_EVENT, + sequence: event.sequence, + time: event.time, + requestor: event.requestor, + selection: event.selection, + target: event.target, + property, + }, + ) + .map_err(into_unknown)?; + + self.server.conn.flush().map_err(into_unknown) + } + + fn ask_clipboard_manager_to_request_our_data(&self) -> Result<()> { + if self.server.win_id == 0 { + // This shouldn't really ever happen but let's just check. + log::error!("The server's window id was 0. This is unexpected"); + return Ok(()); + } + + if !self.is_owner(ClipboardKind::Clipboard)? { + // We are not owning the clipboard, nothing to do. + return Ok(()); + } + if self + .selection_of(ClipboardKind::Clipboard) + .data + .read() + .is_none() + { + // If we don't have any data, there's nothing to do. + return Ok(()); + } + + // It's important that we lock the state before sending the request + // because we don't want the request server thread to lock the state + // after the request but before we can lock it here. + let mut handover_state = self.handover_state.lock(); + + log::trace!("Sending the data to the clipboard manager"); + self.server + .conn + .convert_selection( + self.server.win_id, + self.atoms.CLIPBOARD_MANAGER, + self.atoms.SAVE_TARGETS, + self.atoms.ARBOARD_CLIPBOARD, + Time::CURRENT_TIME, + ) + .map_err(into_unknown)?; + self.server.conn.flush().map_err(into_unknown)?; + + *handover_state = ManagerHandoverState::InProgress; + let max_handover_duration = Duration::from_millis(100); + + // Note that we are using a parking_lot condvar here, which doesn't wake up + // spuriously + let result = self + .handover_cv + .wait_for(&mut handover_state, max_handover_duration); + + if *handover_state == ManagerHandoverState::Finished { + return Ok(()); + } + if result.timed_out() { + log::warn!( + "Could not hand the clipboard contents over to the clipboard manager. The request timed out." + ); + return Ok(()); + } + + Err(Error::unknown( + "The handover was not finished and the condvar didn't time out, yet the condvar wait ended. This should be unreachable.", + )) + } +} + +fn serve_requests(context: Arc) -> Result<(), Box> { + fn handover_finished(clip: &Arc, mut handover_state: MutexGuard) { + log::trace!("Finishing clipboard manager handover."); + *handover_state = ManagerHandoverState::Finished; + + // Not sure if unlocking the mutex is necessary here but better safe than sorry. + drop(handover_state); + + clip.handover_cv.notify_all(); + } + + log::trace!("Started serve requests thread."); + + let _guard = util::defer(|| { + context.serve_stopped.store(true, Ordering::Relaxed); + }); + + let mut written = false; + let mut notified = false; + + loop { + match context.server.conn.wait_for_event().map_err(into_unknown)? { + Event::DestroyNotify(_) => { + // This window is being destroyed. + log::trace!("Clipboard server window is being destroyed x_x"); + return Ok(()); + } + Event::SelectionClear(event) => { + // TODO: check if this works + // Someone else has new content in the clipboard, so it is + // notifying us that we should delete our data now. + log::trace!("Somebody else owns the clipboard now"); + + if let Some(selection) = context.kind_of(event.selection) { + let selection = context.selection_of(selection); + let mut data_guard = selection.data.write(); + *data_guard = None; + + // It is important that this mutex is locked at the time of calling + // `notify_all` to prevent notifications getting lost in case the sleeping + // thread has unlocked its `data_guard` and is just about to sleep. + // It is also important that the RwLock is kept write-locked for the same + // reason. + let _guard = selection.mutex.lock(); + selection.data_changed.notify_all(); + } + } + Event::SelectionRequest(event) => { + log::trace!( + "SelectionRequest - selection is: {}, target is {}", + context.atom_name(event.selection), + context.atom_name(event.target), + ); + // Someone is requesting the clipboard content from us. + context + .handle_selection_request(event) + .map_err(into_unknown)?; + + // if we are in the progress of saving to the clipboard manager + // make sure we save that we have finished writing + let handover_state = context.handover_state.lock(); + if *handover_state == ManagerHandoverState::InProgress { + // Only set written, when the actual contents were written, + // not just a response to what TARGETS we have. + if event.target != context.atoms.TARGETS { + log::trace!("The contents were written to the clipboard manager."); + written = true; + // if we have written and notified, make sure to notify that we are done + if notified { + handover_finished(&context, handover_state); + } + } + } + } + Event::SelectionNotify(event) => { + // We've requested the clipboard content and this is the answer. + // Considering that this thread is not responsible for reading + // clipboard contents, this must come from the clipboard manager + // signaling that the data was handed over successfully. + if event.selection != context.atoms.CLIPBOARD_MANAGER { + log::error!( + "Received a `SelectionNotify` from a selection other than the CLIPBOARD_MANAGER. This is unexpected in this thread." + ); + continue; + } + let handover_state = context.handover_state.lock(); + if *handover_state == ManagerHandoverState::InProgress { + // Note that some clipboard managers send a selection notify + // before even sending a request for the actual contents. + // (That's why we use the "notified" & "written" flags) + log::trace!( + "The clipboard manager indicated that it's done requesting the contents from us." + ); + notified = true; + + // One would think that we could also finish if the property + // here is set 0, because that indicates failure. However + // this is not the case; for example on KDE plasma 5.18, we + // immediately get a SelectionNotify with property set to 0, + // but following that, we also get a valid SelectionRequest + // from the clipboard manager. + if written { + handover_finished(&context, handover_state); + } + } + } + _event => { + // May be useful for debugging but nothing else really. + //log::trace!("Received unwanted event: {:?}", event); + } + } + } +} + +pub(crate) struct Clipboard { + inner: Arc, +} + +impl Clipboard { + pub(crate) fn new() -> Result { + let mut global_cb = CLIPBOARD.lock(); + if let Some(global_cb) = &*global_cb { + return Ok(Self { + inner: Arc::clone(&global_cb.inner), + }); + } + // At this point we know that the clipboard does not exist. + let ctx = Arc::new(Inner::new()?); + let join_handle = std::thread::Builder::new() + .name("Clipboard".to_owned()) + .spawn({ + let ctx = Arc::clone(&ctx); + move || { + if let Err(error) = serve_requests(ctx) { + log::error!("Worker thread errored with: {}", error); + } + } + }) + .unwrap(); + *global_cb = Some(GlobalClipboard { + inner: Arc::clone(&ctx), + server_handle: join_handle, + }); + Ok(Self { inner: ctx }) + } + + pub(crate) fn set_text( + &self, + message: Cow<'_, str>, + selection: ClipboardKind, + wait: WaitConfig, + ) -> Result<()> { + let data = vec![ClipboardData { + bytes: message.into_owned().into_bytes(), + format: self.inner.atoms.UTF8_STRING, + }]; + self.inner.write(data, selection, wait) + } + + #[allow(unused)] + pub(crate) fn set_image( + &self, + image: Image, + selection: ClipboardKind, + wait: WaitConfig, + ) -> Result<()> { + let format = match image.format { + ImageFormat::Png => self.inner.atoms.PNG__MIME, + ImageFormat::Jpeg => self.inner.atoms.JPEG_MIME, + ImageFormat::Webp => self.inner.atoms.WEBP_MIME, + ImageFormat::Gif => self.inner.atoms.GIF__MIME, + ImageFormat::Svg => self.inner.atoms.SVG__MIME, + ImageFormat::Bmp => self.inner.atoms.BMP__MIME, + ImageFormat::Tiff => self.inner.atoms.TIFF_MIME, + }; + let data = vec![ClipboardData { + bytes: image.bytes, + format: self.inner.atoms.PNG__MIME, + }]; + self.inner.write(data, selection, wait) + } + + pub(crate) fn get_any(&self, selection: ClipboardKind) -> Result { + const IMAGE_FORMAT_COUNT: usize = 7; + let image_format_atoms: [Atom; IMAGE_FORMAT_COUNT] = [ + self.inner.atoms.PNG__MIME, + self.inner.atoms.JPEG_MIME, + self.inner.atoms.WEBP_MIME, + self.inner.atoms.GIF__MIME, + self.inner.atoms.SVG__MIME, + self.inner.atoms.BMP__MIME, + self.inner.atoms.TIFF_MIME, + ]; + let image_formats: [ImageFormat; IMAGE_FORMAT_COUNT] = [ + ImageFormat::Png, + ImageFormat::Jpeg, + ImageFormat::Webp, + ImageFormat::Gif, + ImageFormat::Svg, + ImageFormat::Bmp, + ImageFormat::Tiff, + ]; + + const TEXT_FORMAT_COUNT: usize = 6; + let text_format_atoms: [Atom; TEXT_FORMAT_COUNT] = [ + self.inner.atoms.UTF8_STRING, + self.inner.atoms.UTF8_MIME_0, + self.inner.atoms.UTF8_MIME_1, + self.inner.atoms.STRING, + self.inner.atoms.TEXT, + self.inner.atoms.TEXT_MIME_UNKNOWN, + ]; + + let atom_none: Atom = AtomEnum::NONE.into(); + + const FORMAT_ATOM_COUNT: usize = TEXT_FORMAT_COUNT + IMAGE_FORMAT_COUNT; + + let mut format_atoms: [Atom; FORMAT_ATOM_COUNT] = [atom_none; FORMAT_ATOM_COUNT]; + + // image formats first, as they are more specific, and read will return the first + // format that the contents can be converted to + format_atoms[0..IMAGE_FORMAT_COUNT].copy_from_slice(&image_format_atoms); + format_atoms[IMAGE_FORMAT_COUNT..].copy_from_slice(&text_format_atoms); + debug_assert!(!format_atoms.contains(&atom_none)); + + let result = self.inner.read(&format_atoms, selection)?; + + log::trace!( + "read clipboard as format {:?}", + self.inner.atom_name(result.format) + ); + + for (format_atom, image_format) in image_format_atoms.into_iter().zip(image_formats) { + if result.format == format_atom { + let bytes = result.bytes; + let id = hash(&bytes); + return Ok(ClipboardItem::new_image(&Image { + id, + format: image_format, + bytes, + })); + } + } + + let text = if result.format == self.inner.atoms.STRING { + // ISO Latin-1 + // See: https://stackoverflow.com/questions/28169745/what-are-the-options-to-convert-iso-8859-1-latin-1-to-a-string-utf-8 + result.bytes.into_iter().map(|c| c as char).collect() + } else { + String::from_utf8(result.bytes).map_err(|_| Error::ConversionFailure)? + }; + Ok(ClipboardItem::new_string(text)) + } + + pub fn is_owner(&self, selection: ClipboardKind) -> bool { + self.inner.is_owner(selection).unwrap_or(false) + } +} + +impl Drop for Clipboard { + fn drop(&mut self) { + // There are always at least 3 owners: + // the global, the server thread, and one `Clipboard::inner` + const MIN_OWNERS: usize = 3; + + // We start with locking the global guard to prevent race + // conditions below. + let mut global_cb = CLIPBOARD.lock(); + if Arc::strong_count(&self.inner) == MIN_OWNERS { + // If the are the only owners of the clipboard are ourselves and + // the global object, then we should destroy the global object, + // and send the data to the clipboard manager + + if let Err(e) = self.inner.ask_clipboard_manager_to_request_our_data() { + log::error!( + "Could not hand the clipboard data over to the clipboard manager: {}", + e + ); + } + let global_cb = global_cb.take(); + if let Err(e) = self + .inner + .server + .conn + .destroy_window(self.inner.server.win_id) + { + log::error!("Failed to destroy the clipboard window. Error: {}", e); + return; + } + if let Err(e) = self.inner.server.conn.flush() { + log::error!("Failed to flush the clipboard window. Error: {}", e); + return; + } + if let Some(global_cb) = global_cb + && let Err(e) = global_cb.server_handle.join() + { + // Let's try extracting the error message + let message; + if let Some(msg) = e.downcast_ref::<&'static str>() { + message = Some((*msg).to_string()); + } else if let Some(msg) = e.downcast_ref::() { + message = Some(msg.clone()); + } else { + message = None; + } + if let Some(message) = message { + log::error!( + "The clipboard server thread panicked. Panic message: '{}'", + message, + ); + } else { + log::error!("The clipboard server thread panicked."); + } + } + } + } +} + +fn into_unknown(error: E) -> Error { + Error::Unknown { + description: error.to_string(), + } +} + +/// Clipboard selection +/// +/// Linux has a concept of clipboard "selections" which tend to be used in different contexts. This +/// enum provides a way to get/set to a specific clipboard +/// +/// See for a better +/// description of the different clipboards. +#[derive(Copy, Clone, Debug)] +pub enum ClipboardKind { + /// Typically used selection for explicit cut/copy/paste actions (ie. windows/macos like + /// clipboard behavior) + Clipboard, + + /// Typically used for mouse selections and/or currently selected text. Accessible via middle + /// mouse click. + Primary, + + /// The secondary clipboard is rarely used but theoretically available on X11. + Secondary, +} + +/// Configuration on how long to wait for a new X11 copy event is emitted. +#[derive(Default)] +pub(crate) enum WaitConfig { + /// Waits until the given [`Instant`] has reached. + #[allow( + unused, + reason = "Right now we don't wait for clipboard contents to sync on app close, but we may in the future" + )] + Until(Instant), + + /// Waits forever until a new event is reached. + #[allow(unused)] + #[allow( + unused, + reason = "Right now we don't wait for clipboard contents to sync on app close, but we may in the future" + )] + Forever, + + /// It shouldn't wait. + #[default] + None, +} + +#[non_exhaustive] +pub enum Error { + /// The clipboard contents were not available in the requested format. + /// This could either be due to the clipboard being empty or the clipboard contents having + /// an incompatible format to the requested one (eg when calling `get_image` on text) + ContentNotAvailable, + + /// The native clipboard is not accessible due to being held by an other party. + /// + /// This "other party" could be a different process or it could be within + /// the same program. So for example you may get this error when trying + /// to interact with the clipboard from multiple threads at once. + /// + /// Note that it's OK to have multiple `Clipboard` instances. The underlying + /// implementation will make sure that the native clipboard is only + /// opened for transferring data and then closed as soon as possible. + ClipboardOccupied, + + /// The image or the text that was about the be transferred to/from the clipboard could not be + /// converted to the appropriate format. + ConversionFailure, + + /// Any error that doesn't fit the other error types. + /// + /// The `description` field is only meant to help the developer and should not be relied on as a + /// means to identify an error case during runtime. + Unknown { description: String }, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::ContentNotAvailable => f.write_str("The clipboard contents were not available in the requested format or the clipboard is empty."), + Error::ClipboardOccupied => f.write_str("The native clipboard is not accessible due to being held by an other party."), + Error::ConversionFailure => f.write_str("The image or the text that was about the be transferred to/from the clipboard could not be converted to the appropriate format."), + Error::Unknown { description } => f.write_fmt(format_args!("Unknown error while interacting with the clipboard: {description}")), + } + } +} + +impl std::error::Error for Error {} + +impl std::fmt::Debug for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use Error::*; + macro_rules! kind_to_str { + ($( $e: pat ),*) => { + match self { + $( + $e => stringify!($e), + )* + } + } + } + let name = kind_to_str!( + ContentNotAvailable, + ClipboardOccupied, + ConversionFailure, + Unknown { .. } + ); + f.write_fmt(format_args!("{name} - \"{self}\"")) + } +} + +impl Error { + pub(crate) fn unknown>(message: M) -> Self { + Error::Unknown { + description: message.into(), + } + } +} diff --git a/third_party/gpui/src/platform/linux/x11/display.rs b/third_party/gpui/src/platform/linux/x11/display.rs new file mode 100644 index 0000000..ea2f8bb --- /dev/null +++ b/third_party/gpui/src/platform/linux/x11/display.rs @@ -0,0 +1,51 @@ +use anyhow::Context as _; +use uuid::Uuid; +use x11rb::{connection::Connection as _, xcb_ffi::XCBConnection}; + +use crate::{Bounds, DisplayId, Pixels, PlatformDisplay, Size, px}; + +#[derive(Debug)] +pub(crate) struct X11Display { + x_screen_index: usize, + bounds: Bounds, + uuid: Uuid, +} + +impl X11Display { + pub(crate) fn new( + xcb: &XCBConnection, + scale_factor: f32, + x_screen_index: usize, + ) -> anyhow::Result { + let screen = xcb + .setup() + .roots + .get(x_screen_index) + .with_context(|| format!("No screen found with index {x_screen_index}"))?; + Ok(Self { + x_screen_index, + bounds: Bounds { + origin: Default::default(), + size: Size { + width: px(screen.width_in_pixels as f32 / scale_factor), + height: px(screen.height_in_pixels as f32 / scale_factor), + }, + }, + uuid: Uuid::from_bytes([0; 16]), + }) + } +} + +impl PlatformDisplay for X11Display { + fn id(&self) -> DisplayId { + DisplayId(self.x_screen_index as u32) + } + + fn uuid(&self) -> anyhow::Result { + Ok(self.uuid) + } + + fn bounds(&self) -> Bounds { + self.bounds + } +} diff --git a/third_party/gpui/src/platform/linux/x11/event.rs b/third_party/gpui/src/platform/linux/x11/event.rs new file mode 100644 index 0000000..17bcc90 --- /dev/null +++ b/third_party/gpui/src/platform/linux/x11/event.rs @@ -0,0 +1,154 @@ +use x11rb::protocol::{ + xinput, + xproto::{self, ModMask}, +}; + +use crate::{Modifiers, MouseButton, NavigationDirection}; + +pub(crate) enum ButtonOrScroll { + Button(MouseButton), + Scroll(ScrollDirection), +} + +pub(crate) enum ScrollDirection { + Up, + Down, + Left, + Right, +} + +pub(crate) fn button_or_scroll_from_event_detail(detail: u32) -> Option { + Some(match detail { + 1 => ButtonOrScroll::Button(MouseButton::Left), + 2 => ButtonOrScroll::Button(MouseButton::Middle), + 3 => ButtonOrScroll::Button(MouseButton::Right), + 4 => ButtonOrScroll::Scroll(ScrollDirection::Up), + 5 => ButtonOrScroll::Scroll(ScrollDirection::Down), + 6 => ButtonOrScroll::Scroll(ScrollDirection::Left), + 7 => ButtonOrScroll::Scroll(ScrollDirection::Right), + 8 => ButtonOrScroll::Button(MouseButton::Navigate(NavigationDirection::Back)), + 9 => ButtonOrScroll::Button(MouseButton::Navigate(NavigationDirection::Forward)), + _ => return None, + }) +} + +pub(crate) fn modifiers_from_state(state: xproto::KeyButMask) -> Modifiers { + Modifiers { + control: state.contains(xproto::KeyButMask::CONTROL), + alt: state.contains(xproto::KeyButMask::MOD1), + shift: state.contains(xproto::KeyButMask::SHIFT), + platform: state.contains(xproto::KeyButMask::MOD4), + function: false, + } +} + +pub(crate) fn modifiers_from_xinput_info(modifier_info: xinput::ModifierInfo) -> Modifiers { + Modifiers { + control: modifier_info.effective as u16 & ModMask::CONTROL.bits() + == ModMask::CONTROL.bits(), + alt: modifier_info.effective as u16 & ModMask::M1.bits() == ModMask::M1.bits(), + shift: modifier_info.effective as u16 & ModMask::SHIFT.bits() == ModMask::SHIFT.bits(), + platform: modifier_info.effective as u16 & ModMask::M4.bits() == ModMask::M4.bits(), + function: false, + } +} + +pub(crate) fn pressed_button_from_mask(button_mask: u32) -> Option { + Some(if button_mask & 2 == 2 { + MouseButton::Left + } else if button_mask & 4 == 4 { + MouseButton::Middle + } else if button_mask & 8 == 8 { + MouseButton::Right + } else { + return None; + }) +} + +pub(crate) fn get_valuator_axis_index( + valuator_mask: &Vec, + valuator_number: u16, +) -> Option { + // XInput valuator masks have a 1 at the bit indexes corresponding to each + // valuator present in this event's axisvalues. Axisvalues is ordered from + // lowest valuator number to highest, so counting bits before the 1 bit for + // this valuator yields the index in axisvalues. + if bit_is_set_in_vec(valuator_mask, valuator_number) { + Some(popcount_upto_bit_index(valuator_mask, valuator_number) as usize) + } else { + None + } +} + +/// Returns the number of 1 bits in `bit_vec` for all bits where `i < bit_index`. +fn popcount_upto_bit_index(bit_vec: &Vec, bit_index: u16) -> u32 { + let array_index = bit_index as usize / 32; + let popcount: u32 = bit_vec + .get(array_index) + .map_or(0, |bits| keep_bits_upto(*bits, bit_index % 32).count_ones()); + if array_index == 0 { + popcount + } else { + // Valuator numbers over 32 probably never occur for scroll position, but may as well + // support it. + let leading_popcount: u32 = bit_vec + .iter() + .take(array_index) + .map(|bits| bits.count_ones()) + .sum(); + popcount + leading_popcount + } +} + +fn bit_is_set_in_vec(bit_vec: &Vec, bit_index: u16) -> bool { + let array_index = bit_index as usize / 32; + bit_vec + .get(array_index) + .is_some_and(|bits| bit_is_set(*bits, bit_index % 32)) +} + +fn bit_is_set(bits: u32, bit_index: u16) -> bool { + bits & (1 << bit_index) != 0 +} + +/// Sets every bit with `i >= bit_index` to 0. +fn keep_bits_upto(bits: u32, bit_index: u16) -> u32 { + if bit_index == 0 { + 0 + } else if bit_index >= 32 { + u32::MAX + } else { + bits & ((1 << bit_index) - 1) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_valuator_axis_index() { + assert!(get_valuator_axis_index(&vec![0b11], 0) == Some(0)); + assert!(get_valuator_axis_index(&vec![0b11], 1) == Some(1)); + assert!(get_valuator_axis_index(&vec![0b11], 2) == None); + + assert!(get_valuator_axis_index(&vec![0b100], 0) == None); + assert!(get_valuator_axis_index(&vec![0b100], 1) == None); + assert!(get_valuator_axis_index(&vec![0b100], 2) == Some(0)); + assert!(get_valuator_axis_index(&vec![0b100], 3) == None); + + assert!(get_valuator_axis_index(&vec![0b1010, 0], 0) == None); + assert!(get_valuator_axis_index(&vec![0b1010, 0], 1) == Some(0)); + assert!(get_valuator_axis_index(&vec![0b1010, 0], 2) == None); + assert!(get_valuator_axis_index(&vec![0b1010, 0], 3) == Some(1)); + + assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 0) == None); + assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 1) == Some(0)); + assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 2) == None); + assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 3) == Some(1)); + assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 32) == Some(2)); + assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 33) == None); + + assert!(get_valuator_axis_index(&vec![0b1010, 0b101], 34) == Some(3)); + } +} diff --git a/third_party/gpui/src/platform/linux/x11/window.rs b/third_party/gpui/src/platform/linux/x11/window.rs new file mode 100644 index 0000000..fe197a6 --- /dev/null +++ b/third_party/gpui/src/platform/linux/x11/window.rs @@ -0,0 +1,1670 @@ +use anyhow::{Context as _, anyhow}; +use x11rb::connection::RequestConnection; + +use crate::platform::blade::{BladeContext, BladeRenderer, BladeSurfaceConfig}; +use crate::{ + AnyWindowHandle, Bounds, Decorations, DevicePixels, ForegroundExecutor, GpuSpecs, Modifiers, + Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, + Point, PromptButton, PromptLevel, RequestFrameOptions, ResizeEdge, ScaledPixels, Scene, Size, + Tiling, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControlArea, + WindowDecorations, WindowKind, WindowParams, X11ClientStatePtr, px, size, +}; + +use blade_graphics as gpu; +use raw_window_handle as rwh; +use util::{ResultExt, maybe}; +use x11rb::{ + connection::Connection, + cookie::{Cookie, VoidCookie}, + errors::ConnectionError, + properties::WmSizeHints, + protocol::{ + sync, + xinput::{self, ConnectionExt as _}, + xproto::{self, ClientMessageEvent, ConnectionExt, TranslateCoordinatesReply}, + }, + wrapper::ConnectionExt as _, + xcb_ffi::XCBConnection, +}; + +use std::{ + cell::RefCell, ffi::c_void, fmt::Display, num::NonZeroU32, ops::Div, ptr::NonNull, rc::Rc, + sync::Arc, +}; + +use super::{X11Display, XINPUT_ALL_DEVICE_GROUPS, XINPUT_ALL_DEVICES}; + +x11rb::atom_manager! { + pub XcbAtoms: AtomsCookie { + XA_ATOM, + XdndAware, + XdndStatus, + XdndEnter, + XdndLeave, + XdndPosition, + XdndSelection, + XdndDrop, + XdndFinished, + XdndTypeList, + XdndActionCopy, + TextUriList: b"text/uri-list", + UTF8_STRING, + TEXT, + STRING, + TEXT_PLAIN_UTF8: b"text/plain;charset=utf-8", + TEXT_PLAIN: b"text/plain", + XDND_DATA, + WM_PROTOCOLS, + WM_DELETE_WINDOW, + WM_CHANGE_STATE, + WM_TRANSIENT_FOR, + _NET_WM_PID, + _NET_WM_NAME, + _NET_WM_STATE, + _NET_WM_STATE_MAXIMIZED_VERT, + _NET_WM_STATE_MAXIMIZED_HORZ, + _NET_WM_STATE_FULLSCREEN, + _NET_WM_STATE_HIDDEN, + _NET_WM_STATE_FOCUSED, + _NET_ACTIVE_WINDOW, + _NET_WM_SYNC_REQUEST, + _NET_WM_SYNC_REQUEST_COUNTER, + _NET_WM_BYPASS_COMPOSITOR, + _NET_WM_MOVERESIZE, + _NET_WM_WINDOW_TYPE, + _NET_WM_WINDOW_TYPE_NOTIFICATION, + _NET_WM_WINDOW_TYPE_DIALOG, + _NET_WM_SYNC, + _NET_SUPPORTED, + _MOTIF_WM_HINTS, + _GTK_SHOW_WINDOW_MENU, + _GTK_FRAME_EXTENTS, + _GTK_EDGE_CONSTRAINTS, + _NET_CLIENT_LIST_STACKING, + } +} + +fn query_render_extent( + xcb: &Rc, + x_window: xproto::Window, +) -> anyhow::Result { + let reply = get_reply(|| "X11 GetGeometry failed.", xcb.get_geometry(x_window))?; + Ok(gpu::Extent { + width: reply.width as u32, + height: reply.height as u32, + depth: 1, + }) +} + +impl ResizeEdge { + fn to_moveresize(self) -> u32 { + match self { + ResizeEdge::TopLeft => 0, + ResizeEdge::Top => 1, + ResizeEdge::TopRight => 2, + ResizeEdge::Right => 3, + ResizeEdge::BottomRight => 4, + ResizeEdge::Bottom => 5, + ResizeEdge::BottomLeft => 6, + ResizeEdge::Left => 7, + } + } +} + +#[derive(Debug)] +struct EdgeConstraints { + top_tiled: bool, + #[allow(dead_code)] + top_resizable: bool, + + right_tiled: bool, + #[allow(dead_code)] + right_resizable: bool, + + bottom_tiled: bool, + #[allow(dead_code)] + bottom_resizable: bool, + + left_tiled: bool, + #[allow(dead_code)] + left_resizable: bool, +} + +impl EdgeConstraints { + fn from_atom(atom: u32) -> Self { + EdgeConstraints { + top_tiled: (atom & (1 << 0)) != 0, + top_resizable: (atom & (1 << 1)) != 0, + right_tiled: (atom & (1 << 2)) != 0, + right_resizable: (atom & (1 << 3)) != 0, + bottom_tiled: (atom & (1 << 4)) != 0, + bottom_resizable: (atom & (1 << 5)) != 0, + left_tiled: (atom & (1 << 6)) != 0, + left_resizable: (atom & (1 << 7)) != 0, + } + } + + fn to_tiling(&self) -> Tiling { + Tiling { + top: self.top_tiled, + right: self.right_tiled, + bottom: self.bottom_tiled, + left: self.left_tiled, + } + } +} + +#[derive(Copy, Clone, Debug)] +struct Visual { + id: xproto::Visualid, + colormap: u32, + depth: u8, +} + +struct VisualSet { + inherit: Visual, + opaque: Option, + transparent: Option, + root: u32, + black_pixel: u32, +} + +fn find_visuals(xcb: &XCBConnection, screen_index: usize) -> VisualSet { + let screen = &xcb.setup().roots[screen_index]; + let mut set = VisualSet { + inherit: Visual { + id: screen.root_visual, + colormap: screen.default_colormap, + depth: screen.root_depth, + }, + opaque: None, + transparent: None, + root: screen.root, + black_pixel: screen.black_pixel, + }; + + for depth_info in screen.allowed_depths.iter() { + for visual_type in depth_info.visuals.iter() { + let visual = Visual { + id: visual_type.visual_id, + colormap: 0, + depth: depth_info.depth, + }; + log::debug!( + "Visual id: {}, class: {:?}, depth: {}, bits_per_value: {}, masks: 0x{:x} 0x{:x} 0x{:x}", + visual_type.visual_id, + visual_type.class, + depth_info.depth, + visual_type.bits_per_rgb_value, + visual_type.red_mask, + visual_type.green_mask, + visual_type.blue_mask, + ); + + if ( + visual_type.red_mask, + visual_type.green_mask, + visual_type.blue_mask, + ) != (0xFF0000, 0xFF00, 0xFF) + { + continue; + } + let color_mask = visual_type.red_mask | visual_type.green_mask | visual_type.blue_mask; + let alpha_mask = color_mask as usize ^ ((1usize << depth_info.depth) - 1); + + if alpha_mask == 0 { + if set.opaque.is_none() { + set.opaque = Some(visual); + } + } else { + if set.transparent.is_none() { + set.transparent = Some(visual); + } + } + } + } + + set +} + +struct RawWindow { + connection: *mut c_void, + screen_id: usize, + window_id: u32, + visual_id: u32, +} + +#[derive(Default)] +pub struct Callbacks { + request_frame: Option>, + input: Option crate::DispatchEventResult>>, + active_status_change: Option>, + hovered_status_change: Option>, + resize: Option, f32)>>, + moved: Option>, + should_close: Option bool>>, + close: Option>, + appearance_changed: Option>, +} + +pub struct X11WindowState { + pub destroyed: bool, + client: X11ClientStatePtr, + executor: ForegroundExecutor, + atoms: XcbAtoms, + x_root_window: xproto::Window, + pub(crate) counter_id: sync::Counter, + pub(crate) last_sync_counter: Option, + bounds: Bounds, + scale_factor: f32, + renderer: BladeRenderer, + display: Rc, + input_handler: Option, + appearance: WindowAppearance, + background_appearance: WindowBackgroundAppearance, + maximized_vertical: bool, + maximized_horizontal: bool, + hidden: bool, + active: bool, + hovered: bool, + fullscreen: bool, + client_side_decorations_supported: bool, + decorations: WindowDecorations, + edge_constraints: Option, + pub handle: AnyWindowHandle, + last_insets: [u32; 4], +} + +impl X11WindowState { + fn is_transparent(&self) -> bool { + self.background_appearance != WindowBackgroundAppearance::Opaque + } +} + +#[derive(Clone)] +pub(crate) struct X11WindowStatePtr { + pub state: Rc>, + pub(crate) callbacks: Rc>, + xcb: Rc, + pub(crate) x_window: xproto::Window, +} + +impl rwh::HasWindowHandle for RawWindow { + fn window_handle(&self) -> Result, rwh::HandleError> { + let Some(non_zero) = NonZeroU32::new(self.window_id) else { + log::error!("RawWindow.window_id zero when getting window handle."); + return Err(rwh::HandleError::Unavailable); + }; + let mut handle = rwh::XcbWindowHandle::new(non_zero); + handle.visual_id = NonZeroU32::new(self.visual_id); + Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) }) + } +} +impl rwh::HasDisplayHandle for RawWindow { + fn display_handle(&self) -> Result, rwh::HandleError> { + let Some(non_zero) = NonNull::new(self.connection) else { + log::error!("Null RawWindow.connection when getting display handle."); + return Err(rwh::HandleError::Unavailable); + }; + let handle = rwh::XcbDisplayHandle::new(Some(non_zero), self.screen_id as i32); + Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) }) + } +} + +impl rwh::HasWindowHandle for X11Window { + fn window_handle(&self) -> Result, rwh::HandleError> { + unimplemented!() + } +} +impl rwh::HasDisplayHandle for X11Window { + fn display_handle(&self) -> Result, rwh::HandleError> { + unimplemented!() + } +} + +pub(crate) fn xcb_flush(xcb: &XCBConnection) { + xcb.flush() + .map_err(handle_connection_error) + .context("X11 flush failed") + .log_err(); +} + +pub(crate) fn check_reply( + failure_context: F, + result: Result, ConnectionError>, +) -> anyhow::Result<()> +where + E: Display + Send + Sync + 'static, + F: FnOnce() -> E, + C: RequestConnection, +{ + result + .map_err(handle_connection_error) + .and_then(|response| response.check().map_err(|reply_error| anyhow!(reply_error))) + .with_context(failure_context) +} + +pub(crate) fn get_reply( + failure_context: F, + result: Result, ConnectionError>, +) -> anyhow::Result +where + E: Display + Send + Sync + 'static, + F: FnOnce() -> E, + C: RequestConnection, + O: x11rb::x11_utils::TryParse, +{ + result + .map_err(handle_connection_error) + .and_then(|response| response.reply().map_err(|reply_error| anyhow!(reply_error))) + .with_context(failure_context) +} + +/// Convert X11 connection errors to `anyhow::Error` and panic for unrecoverable errors. +pub(crate) fn handle_connection_error(err: ConnectionError) -> anyhow::Error { + match err { + ConnectionError::UnknownError => anyhow!("X11 connection: Unknown error"), + ConnectionError::UnsupportedExtension => anyhow!("X11 connection: Unsupported extension"), + ConnectionError::MaximumRequestLengthExceeded => { + anyhow!("X11 connection: Maximum request length exceeded") + } + ConnectionError::FdPassingFailed => { + panic!("X11 connection: File descriptor passing failed") + } + ConnectionError::ParseError(parse_error) => { + anyhow!(parse_error).context("Parse error in X11 response") + } + ConnectionError::InsufficientMemory => panic!("X11 connection: Insufficient memory"), + ConnectionError::IoError(err) => anyhow!(err).context("X11 connection: IOError"), + _ => anyhow!(err), + } +} + +impl X11WindowState { + pub fn new( + handle: AnyWindowHandle, + client: X11ClientStatePtr, + executor: ForegroundExecutor, + gpu_context: &BladeContext, + params: WindowParams, + xcb: &Rc, + client_side_decorations_supported: bool, + x_main_screen_index: usize, + x_window: xproto::Window, + atoms: &XcbAtoms, + scale_factor: f32, + appearance: WindowAppearance, + parent_window: Option, + ) -> anyhow::Result { + let x_screen_index = params + .display_id + .map_or(x_main_screen_index, |did| did.0 as usize); + + let visual_set = find_visuals(xcb, x_screen_index); + + let visual = match visual_set.transparent { + Some(visual) => visual, + None => { + log::warn!("Unable to find a transparent visual",); + visual_set.inherit + } + }; + log::info!("Using {:?}", visual); + + let colormap = if visual.colormap != 0 { + visual.colormap + } else { + let id = xcb.generate_id()?; + log::info!("Creating colormap {}", id); + check_reply( + || format!("X11 CreateColormap failed. id: {}", id), + xcb.create_colormap(xproto::ColormapAlloc::NONE, id, visual_set.root, visual.id), + )?; + id + }; + + let win_aux = xproto::CreateWindowAux::new() + // https://stackoverflow.com/questions/43218127/x11-xlib-xcb-creating-a-window-requires-border-pixel-if-specifying-colormap-wh + .border_pixel(visual_set.black_pixel) + .colormap(colormap) + .event_mask( + xproto::EventMask::EXPOSURE + | xproto::EventMask::STRUCTURE_NOTIFY + | xproto::EventMask::FOCUS_CHANGE + | xproto::EventMask::KEY_PRESS + | xproto::EventMask::KEY_RELEASE + | xproto::EventMask::PROPERTY_CHANGE + | xproto::EventMask::VISIBILITY_CHANGE, + ); + + let mut bounds = params.bounds.to_device_pixels(scale_factor); + if bounds.size.width.0 == 0 || bounds.size.height.0 == 0 { + log::warn!( + "Window bounds contain a zero value. height={}, width={}. Falling back to defaults.", + bounds.size.height.0, + bounds.size.width.0 + ); + bounds.size.width = 800.into(); + bounds.size.height = 600.into(); + } + + check_reply( + || { + format!( + "X11 CreateWindow failed. depth: {}, x_window: {}, visual_set.root: {}, bounds.origin.x.0: {}, bounds.origin.y.0: {}, bounds.size.width.0: {}, bounds.size.height.0: {}", + visual.depth, + x_window, + visual_set.root, + bounds.origin.x.0 + 2, + bounds.origin.y.0, + bounds.size.width.0, + bounds.size.height.0 + ) + }, + xcb.create_window( + visual.depth, + x_window, + visual_set.root, + (bounds.origin.x.0 + 2) as i16, + bounds.origin.y.0 as i16, + bounds.size.width.0 as u16, + bounds.size.height.0 as u16, + 0, + xproto::WindowClass::INPUT_OUTPUT, + visual.id, + &win_aux, + ), + )?; + + // Collect errors during setup, so that window can be destroyed on failure. + let setup_result = maybe!({ + let pid = std::process::id(); + check_reply( + || "X11 ChangeProperty for _NET_WM_PID failed.", + xcb.change_property32( + xproto::PropMode::REPLACE, + x_window, + atoms._NET_WM_PID, + xproto::AtomEnum::CARDINAL, + &[pid], + ), + )?; + + if let Some(size) = params.window_min_size { + let mut size_hints = WmSizeHints::new(); + let min_size = (size.width.0 as i32, size.height.0 as i32); + size_hints.min_size = Some(min_size); + check_reply( + || { + format!( + "X11 change of WM_SIZE_HINTS failed. min_size: {:?}", + min_size + ) + }, + size_hints.set_normal_hints(xcb, x_window), + )?; + } + + let reply = get_reply(|| "X11 GetGeometry failed.", xcb.get_geometry(x_window))?; + if reply.x == 0 && reply.y == 0 { + bounds.origin.x.0 += 2; + // Work around a bug where our rendered content appears + // outside the window bounds when opened at the default position + // (14px, 49px on X + Gnome + Ubuntu 22). + let x = bounds.origin.x.0; + let y = bounds.origin.y.0; + check_reply( + || format!("X11 ConfigureWindow failed. x: {}, y: {}", x, y), + xcb.configure_window(x_window, &xproto::ConfigureWindowAux::new().x(x).y(y)), + )?; + } + if let Some(titlebar) = params.titlebar + && let Some(title) = titlebar.title + { + check_reply( + || "X11 ChangeProperty8 on window title failed.", + xcb.change_property8( + xproto::PropMode::REPLACE, + x_window, + xproto::AtomEnum::WM_NAME, + xproto::AtomEnum::STRING, + title.as_bytes(), + ), + )?; + } + + if params.kind == WindowKind::PopUp { + check_reply( + || "X11 ChangeProperty32 setting window type for pop-up failed.", + xcb.change_property32( + xproto::PropMode::REPLACE, + x_window, + atoms._NET_WM_WINDOW_TYPE, + xproto::AtomEnum::ATOM, + &[atoms._NET_WM_WINDOW_TYPE_NOTIFICATION], + ), + )?; + } + + if params.kind == WindowKind::Floating { + if let Some(parent_window) = parent_window { + // WM_TRANSIENT_FOR hint indicating the main application window. For floating windows, we set + // a parent window (WM_TRANSIENT_FOR) such that the window manager knows where to + // place the floating window in relation to the main window. + // https://specifications.freedesktop.org/wm-spec/1.4/ar01s05.html + check_reply( + || "X11 ChangeProperty32 setting WM_TRANSIENT_FOR for floating window failed.", + xcb.change_property32( + xproto::PropMode::REPLACE, + x_window, + atoms.WM_TRANSIENT_FOR, + xproto::AtomEnum::WINDOW, + &[parent_window], + ), + )?; + } + + // _NET_WM_WINDOW_TYPE_DIALOG indicates that this is a dialog (floating) window + // https://specifications.freedesktop.org/wm-spec/1.4/ar01s05.html + check_reply( + || "X11 ChangeProperty32 setting window type for floating window failed.", + xcb.change_property32( + xproto::PropMode::REPLACE, + x_window, + atoms._NET_WM_WINDOW_TYPE, + xproto::AtomEnum::ATOM, + &[atoms._NET_WM_WINDOW_TYPE_DIALOG], + ), + )?; + } + + check_reply( + || "X11 ChangeProperty32 setting protocols failed.", + xcb.change_property32( + xproto::PropMode::REPLACE, + x_window, + atoms.WM_PROTOCOLS, + xproto::AtomEnum::ATOM, + &[atoms.WM_DELETE_WINDOW, atoms._NET_WM_SYNC_REQUEST], + ), + )?; + + get_reply( + || "X11 sync protocol initialize failed.", + sync::initialize(xcb, 3, 1), + )?; + let sync_request_counter = xcb.generate_id()?; + check_reply( + || "X11 sync CreateCounter failed.", + sync::create_counter(xcb, sync_request_counter, sync::Int64 { lo: 0, hi: 0 }), + )?; + + check_reply( + || "X11 ChangeProperty32 setting sync request counter failed.", + xcb.change_property32( + xproto::PropMode::REPLACE, + x_window, + atoms._NET_WM_SYNC_REQUEST_COUNTER, + xproto::AtomEnum::CARDINAL, + &[sync_request_counter], + ), + )?; + + check_reply( + || "X11 XiSelectEvents failed.", + xcb.xinput_xi_select_events( + x_window, + &[xinput::EventMask { + deviceid: XINPUT_ALL_DEVICE_GROUPS, + mask: vec![ + xinput::XIEventMask::MOTION + | xinput::XIEventMask::BUTTON_PRESS + | xinput::XIEventMask::BUTTON_RELEASE + | xinput::XIEventMask::ENTER + | xinput::XIEventMask::LEAVE, + ], + }], + ), + )?; + + check_reply( + || "X11 XiSelectEvents for device changes failed.", + xcb.xinput_xi_select_events( + x_window, + &[xinput::EventMask { + deviceid: XINPUT_ALL_DEVICES, + mask: vec![ + xinput::XIEventMask::HIERARCHY | xinput::XIEventMask::DEVICE_CHANGED, + ], + }], + ), + )?; + + xcb_flush(xcb); + + let renderer = { + let raw_window = RawWindow { + connection: as_raw_xcb_connection::AsRawXcbConnection::as_raw_xcb_connection( + xcb, + ) as *mut _, + screen_id: x_screen_index, + window_id: x_window, + visual_id: visual.id, + }; + let config = BladeSurfaceConfig { + // Note: this has to be done after the GPU init, or otherwise + // the sizes are immediately invalidated. + size: query_render_extent(xcb, x_window)?, + // We set it to transparent by default, even if we have client-side + // decorations, since those seem to work on X11 even without `true` here. + // If the window appearance changes, then the renderer will get updated + // too + transparent: false, + }; + BladeRenderer::new(gpu_context, &raw_window, config)? + }; + + let display = Rc::new(X11Display::new(xcb, scale_factor, x_screen_index)?); + + Ok(Self { + client, + executor, + display, + x_root_window: visual_set.root, + bounds: bounds.to_pixels(scale_factor), + scale_factor, + renderer, + atoms: *atoms, + input_handler: None, + active: false, + hovered: false, + fullscreen: false, + maximized_vertical: false, + maximized_horizontal: false, + hidden: false, + appearance, + handle, + background_appearance: WindowBackgroundAppearance::Opaque, + destroyed: false, + client_side_decorations_supported, + decorations: WindowDecorations::Server, + last_insets: [0, 0, 0, 0], + edge_constraints: None, + counter_id: sync_request_counter, + last_sync_counter: None, + }) + }); + + if setup_result.is_err() { + check_reply( + || "X11 DestroyWindow failed while cleaning it up after setup failure.", + xcb.destroy_window(x_window), + )?; + xcb_flush(xcb); + } + + setup_result + } + + fn content_size(&self) -> Size { + let size = self.renderer.viewport_size(); + Size { + width: size.width.into(), + height: size.height.into(), + } + } +} + +pub(crate) struct X11Window(pub X11WindowStatePtr); + +impl Drop for X11Window { + fn drop(&mut self) { + let mut state = self.0.state.borrow_mut(); + state.renderer.destroy(); + + let destroy_x_window = maybe!({ + check_reply( + || "X11 DestroyWindow failure.", + self.0.xcb.destroy_window(self.0.x_window), + )?; + xcb_flush(&self.0.xcb); + + anyhow::Ok(()) + }) + .log_err(); + + if destroy_x_window.is_some() { + // Mark window as destroyed so that we can filter out when X11 events + // for it still come in. + state.destroyed = true; + + let this_ptr = self.0.clone(); + let client_ptr = state.client.clone(); + state + .executor + .spawn(async move { + this_ptr.close(); + client_ptr.drop_window(this_ptr.x_window); + }) + .detach(); + } + + drop(state); + } +} + +enum WmHintPropertyState { + // Remove = 0, + // Add = 1, + Toggle = 2, +} + +impl X11Window { + pub fn new( + handle: AnyWindowHandle, + client: X11ClientStatePtr, + executor: ForegroundExecutor, + gpu_context: &BladeContext, + params: WindowParams, + xcb: &Rc, + client_side_decorations_supported: bool, + x_main_screen_index: usize, + x_window: xproto::Window, + atoms: &XcbAtoms, + scale_factor: f32, + appearance: WindowAppearance, + parent_window: Option, + ) -> anyhow::Result { + let ptr = X11WindowStatePtr { + state: Rc::new(RefCell::new(X11WindowState::new( + handle, + client, + executor, + gpu_context, + params, + xcb, + client_side_decorations_supported, + x_main_screen_index, + x_window, + atoms, + scale_factor, + appearance, + parent_window, + )?)), + callbacks: Rc::new(RefCell::new(Callbacks::default())), + xcb: xcb.clone(), + x_window, + }; + + let state = ptr.state.borrow_mut(); + ptr.set_wm_properties(state)?; + + Ok(Self(ptr)) + } + + fn set_wm_hints C>( + &self, + failure_context: F, + wm_hint_property_state: WmHintPropertyState, + prop1: u32, + prop2: u32, + ) -> anyhow::Result<()> { + let state = self.0.state.borrow(); + let message = ClientMessageEvent::new( + 32, + self.0.x_window, + state.atoms._NET_WM_STATE, + [wm_hint_property_state as u32, prop1, prop2, 1, 0], + ); + check_reply( + failure_context, + self.0.xcb.send_event( + false, + state.x_root_window, + xproto::EventMask::SUBSTRUCTURE_REDIRECT | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + message, + ), + )?; + xcb_flush(&self.0.xcb); + Ok(()) + } + + fn get_root_position( + &self, + position: Point, + ) -> anyhow::Result { + let state = self.0.state.borrow(); + get_reply( + || "X11 TranslateCoordinates failed.", + self.0.xcb.translate_coordinates( + self.0.x_window, + state.x_root_window, + (position.x.0 * state.scale_factor) as i16, + (position.y.0 * state.scale_factor) as i16, + ), + ) + } + + fn send_moveresize(&self, flag: u32) -> anyhow::Result<()> { + let state = self.0.state.borrow(); + + check_reply( + || "X11 UngrabPointer before move/resize of window failed.", + self.0.xcb.ungrab_pointer(x11rb::CURRENT_TIME), + )?; + + let pointer = get_reply( + || "X11 QueryPointer before move/resize of window failed.", + self.0.xcb.query_pointer(self.0.x_window), + )?; + let message = ClientMessageEvent::new( + 32, + self.0.x_window, + state.atoms._NET_WM_MOVERESIZE, + [ + pointer.root_x as u32, + pointer.root_y as u32, + flag, + 0, // Left mouse button + 0, + ], + ); + check_reply( + || "X11 SendEvent to move/resize window failed.", + self.0.xcb.send_event( + false, + state.x_root_window, + xproto::EventMask::SUBSTRUCTURE_REDIRECT | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + message, + ), + )?; + + xcb_flush(&self.0.xcb); + Ok(()) + } +} + +impl X11WindowStatePtr { + pub fn should_close(&self) -> bool { + let mut cb = self.callbacks.borrow_mut(); + if let Some(mut should_close) = cb.should_close.take() { + let result = (should_close)(); + cb.should_close = Some(should_close); + result + } else { + true + } + } + + pub fn property_notify(&self, event: xproto::PropertyNotifyEvent) -> anyhow::Result<()> { + let mut state = self.state.borrow_mut(); + if event.atom == state.atoms._NET_WM_STATE { + self.set_wm_properties(state)?; + } else if event.atom == state.atoms._GTK_EDGE_CONSTRAINTS { + self.set_edge_constraints(state)?; + } + Ok(()) + } + + fn set_edge_constraints( + &self, + mut state: std::cell::RefMut, + ) -> anyhow::Result<()> { + let reply = get_reply( + || "X11 GetProperty for _GTK_EDGE_CONSTRAINTS failed.", + self.xcb.get_property( + false, + self.x_window, + state.atoms._GTK_EDGE_CONSTRAINTS, + xproto::AtomEnum::CARDINAL, + 0, + 4, + ), + )?; + + if reply.value_len != 0 { + if let Ok(bytes) = reply.value[0..4].try_into() { + let atom = u32::from_ne_bytes(bytes); + let edge_constraints = EdgeConstraints::from_atom(atom); + state.edge_constraints.replace(edge_constraints); + } else { + log::error!("Failed to parse GTK_EDGE_CONSTRAINTS"); + } + } + + Ok(()) + } + + fn set_wm_properties( + &self, + mut state: std::cell::RefMut, + ) -> anyhow::Result<()> { + let reply = get_reply( + || "X11 GetProperty for _NET_WM_STATE failed.", + self.xcb.get_property( + false, + self.x_window, + state.atoms._NET_WM_STATE, + xproto::AtomEnum::ATOM, + 0, + u32::MAX, + ), + )?; + + let atoms = reply + .value + .chunks_exact(4) + .map(|chunk| u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])); + + state.active = false; + state.fullscreen = false; + state.maximized_vertical = false; + state.maximized_horizontal = false; + state.hidden = false; + + for atom in atoms { + if atom == state.atoms._NET_WM_STATE_FOCUSED { + state.active = true; + } else if atom == state.atoms._NET_WM_STATE_FULLSCREEN { + state.fullscreen = true; + } else if atom == state.atoms._NET_WM_STATE_MAXIMIZED_VERT { + state.maximized_vertical = true; + } else if atom == state.atoms._NET_WM_STATE_MAXIMIZED_HORZ { + state.maximized_horizontal = true; + } else if atom == state.atoms._NET_WM_STATE_HIDDEN { + state.hidden = true; + } + } + + Ok(()) + } + + pub fn close(&self) { + let mut callbacks = self.callbacks.borrow_mut(); + if let Some(fun) = callbacks.close.take() { + fun() + } + } + + pub fn refresh(&self, request_frame_options: RequestFrameOptions) { + let mut cb = self.callbacks.borrow_mut(); + if let Some(ref mut fun) = cb.request_frame { + fun(request_frame_options); + } + } + + pub fn handle_input(&self, input: PlatformInput) { + if let Some(ref mut fun) = self.callbacks.borrow_mut().input + && !fun(input.clone()).propagate + { + return; + } + if let PlatformInput::KeyDown(event) = input { + // only allow shift modifier when inserting text + if event.keystroke.modifiers.is_subset_of(&Modifiers::shift()) { + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + if let Some(key_char) = &event.keystroke.key_char { + drop(state); + input_handler.replace_text_in_range(None, key_char); + state = self.state.borrow_mut(); + } + state.input_handler = Some(input_handler); + } + } + } + } + + pub fn handle_ime_commit(&self, text: String) { + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + input_handler.replace_text_in_range(None, &text); + let mut state = self.state.borrow_mut(); + state.input_handler = Some(input_handler); + } + } + + pub fn handle_ime_preedit(&self, text: String) { + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + input_handler.replace_and_mark_text_in_range(None, &text, None); + let mut state = self.state.borrow_mut(); + state.input_handler = Some(input_handler); + } + } + + pub fn handle_ime_unmark(&self) { + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + input_handler.unmark_text(); + let mut state = self.state.borrow_mut(); + state.input_handler = Some(input_handler); + } + } + + pub fn handle_ime_delete(&self) { + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + if let Some(marked) = input_handler.marked_text_range() { + input_handler.replace_text_in_range(Some(marked), ""); + } + let mut state = self.state.borrow_mut(); + state.input_handler = Some(input_handler); + } + } + + pub fn get_ime_area(&self) -> Option> { + let mut state = self.state.borrow_mut(); + let scale_factor = state.scale_factor; + let mut bounds: Option> = None; + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + if let Some(selection) = input_handler.selected_text_range(true) { + bounds = input_handler.bounds_for_range(selection.range); + } + let mut state = self.state.borrow_mut(); + state.input_handler = Some(input_handler); + }; + bounds.map(|b| b.scale(scale_factor)) + } + + pub fn set_bounds(&self, bounds: Bounds) -> anyhow::Result<()> { + let mut resize_args = None; + let is_resize; + { + let mut state = self.state.borrow_mut(); + let bounds = bounds.map(|f| px(f as f32 / state.scale_factor)); + + is_resize = bounds.size.width != state.bounds.size.width + || bounds.size.height != state.bounds.size.height; + + // If it's a resize event (only width/height changed), we ignore `bounds.origin` + // because it contains wrong values. + if is_resize { + state.bounds.size = bounds.size; + } else { + state.bounds = bounds; + } + + let gpu_size = query_render_extent(&self.xcb, self.x_window)?; + if true { + state.renderer.update_drawable_size(size( + DevicePixels(gpu_size.width as i32), + DevicePixels(gpu_size.height as i32), + )); + resize_args = Some((state.content_size(), state.scale_factor)); + } + if let Some(value) = state.last_sync_counter.take() { + check_reply( + || "X11 sync SetCounter failed.", + sync::set_counter(&self.xcb, state.counter_id, value), + )?; + } + } + + let mut callbacks = self.callbacks.borrow_mut(); + if let Some((content_size, scale_factor)) = resize_args + && let Some(ref mut fun) = callbacks.resize + { + fun(content_size, scale_factor) + } + + if !is_resize && let Some(ref mut fun) = callbacks.moved { + fun(); + } + + Ok(()) + } + + pub fn set_active(&self, focus: bool) { + if let Some(ref mut fun) = self.callbacks.borrow_mut().active_status_change { + fun(focus); + } + } + + pub fn set_hovered(&self, focus: bool) { + if let Some(ref mut fun) = self.callbacks.borrow_mut().hovered_status_change { + fun(focus); + } + } + + pub fn set_appearance(&mut self, appearance: WindowAppearance) { + let mut state = self.state.borrow_mut(); + state.appearance = appearance; + let is_transparent = state.is_transparent(); + state.renderer.update_transparency(is_transparent); + state.appearance = appearance; + drop(state); + let mut callbacks = self.callbacks.borrow_mut(); + if let Some(ref mut fun) = callbacks.appearance_changed { + (fun)() + } + } +} + +impl PlatformWindow for X11Window { + fn bounds(&self) -> Bounds { + self.0.state.borrow().bounds + } + + fn is_maximized(&self) -> bool { + let state = self.0.state.borrow(); + + // A maximized window that gets minimized will still retain its maximized state. + !state.hidden && state.maximized_vertical && state.maximized_horizontal + } + + fn window_bounds(&self) -> WindowBounds { + let state = self.0.state.borrow(); + if self.is_maximized() { + WindowBounds::Maximized(state.bounds) + } else { + WindowBounds::Windowed(state.bounds) + } + } + + fn inner_window_bounds(&self) -> WindowBounds { + let state = self.0.state.borrow(); + if self.is_maximized() { + WindowBounds::Maximized(state.bounds) + } else { + let mut bounds = state.bounds; + let [left, right, top, bottom] = state.last_insets; + + let [left, right, top, bottom] = [ + Pixels((left as f32) / state.scale_factor), + Pixels((right as f32) / state.scale_factor), + Pixels((top as f32) / state.scale_factor), + Pixels((bottom as f32) / state.scale_factor), + ]; + + bounds.origin.x += left; + bounds.origin.y += top; + bounds.size.width -= left + right; + bounds.size.height -= top + bottom; + + WindowBounds::Windowed(bounds) + } + } + + fn content_size(&self) -> Size { + // We divide by the scale factor here because this value is queried to determine how much to draw, + // but it will be multiplied later by the scale to adjust for scaling. + let state = self.0.state.borrow(); + state + .content_size() + .map(|size| size.div(state.scale_factor)) + } + + fn resize(&mut self, size: Size) { + let state = self.0.state.borrow(); + let size = size.to_device_pixels(state.scale_factor); + let width = size.width.0 as u32; + let height = size.height.0 as u32; + + check_reply( + || { + format!( + "X11 ConfigureWindow failed. width: {}, height: {}", + width, height + ) + }, + self.0.xcb.configure_window( + self.0.x_window, + &xproto::ConfigureWindowAux::new() + .width(width) + .height(height), + ), + ) + .log_err(); + xcb_flush(&self.0.xcb); + } + + fn scale_factor(&self) -> f32 { + self.0.state.borrow().scale_factor + } + + fn appearance(&self) -> WindowAppearance { + self.0.state.borrow().appearance + } + + fn display(&self) -> Option> { + Some(self.0.state.borrow().display.clone()) + } + + fn mouse_position(&self) -> Point { + get_reply( + || "X11 QueryPointer failed.", + self.0.xcb.query_pointer(self.0.x_window), + ) + .log_err() + .map_or(Point::new(Pixels::ZERO, Pixels::ZERO), |reply| { + Point::new((reply.root_x as u32).into(), (reply.root_y as u32).into()) + }) + } + + fn modifiers(&self) -> Modifiers { + self.0 + .state + .borrow() + .client + .0 + .upgrade() + .map(|ref_cell| ref_cell.borrow().modifiers) + .unwrap_or_default() + } + + fn capslock(&self) -> crate::Capslock { + self.0 + .state + .borrow() + .client + .0 + .upgrade() + .map(|ref_cell| ref_cell.borrow().capslock) + .unwrap_or_default() + } + + fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { + self.0.state.borrow_mut().input_handler = Some(input_handler); + } + + fn take_input_handler(&mut self) -> Option { + self.0.state.borrow_mut().input_handler.take() + } + + fn prompt( + &self, + _level: PromptLevel, + _msg: &str, + _detail: Option<&str>, + _answers: &[PromptButton], + ) -> Option> { + None + } + + fn activate(&self) { + let data = [1, xproto::Time::CURRENT_TIME.into(), 0, 0, 0]; + let message = xproto::ClientMessageEvent::new( + 32, + self.0.x_window, + self.0.state.borrow().atoms._NET_ACTIVE_WINDOW, + data, + ); + self.0 + .xcb + .send_event( + false, + self.0.state.borrow().x_root_window, + xproto::EventMask::SUBSTRUCTURE_REDIRECT | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + message, + ) + .log_err(); + self.0 + .xcb + .set_input_focus( + xproto::InputFocus::POINTER_ROOT, + self.0.x_window, + xproto::Time::CURRENT_TIME, + ) + .log_err(); + xcb_flush(&self.0.xcb); + } + + fn is_active(&self) -> bool { + self.0.state.borrow().active + } + + fn is_hovered(&self) -> bool { + self.0.state.borrow().hovered + } + + fn set_title(&mut self, title: &str) { + check_reply( + || "X11 ChangeProperty8 on WM_NAME failed.", + self.0.xcb.change_property8( + xproto::PropMode::REPLACE, + self.0.x_window, + xproto::AtomEnum::WM_NAME, + xproto::AtomEnum::STRING, + title.as_bytes(), + ), + ) + .log_err(); + + check_reply( + || "X11 ChangeProperty8 on _NET_WM_NAME failed.", + self.0.xcb.change_property8( + xproto::PropMode::REPLACE, + self.0.x_window, + self.0.state.borrow().atoms._NET_WM_NAME, + self.0.state.borrow().atoms.UTF8_STRING, + title.as_bytes(), + ), + ) + .log_err(); + xcb_flush(&self.0.xcb); + } + + fn set_app_id(&mut self, app_id: &str) { + let mut data = Vec::with_capacity(app_id.len() * 2 + 1); + data.extend(app_id.bytes()); // instance https://unix.stackexchange.com/a/494170 + data.push(b'\0'); + data.extend(app_id.bytes()); // class + + check_reply( + || "X11 ChangeProperty8 for WM_CLASS failed.", + self.0.xcb.change_property8( + xproto::PropMode::REPLACE, + self.0.x_window, + xproto::AtomEnum::WM_CLASS, + xproto::AtomEnum::STRING, + &data, + ), + ) + .log_err(); + } + + fn map_window(&mut self) -> anyhow::Result<()> { + check_reply( + || "X11 MapWindow failed.", + self.0.xcb.map_window(self.0.x_window), + )?; + Ok(()) + } + + fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { + let mut state = self.0.state.borrow_mut(); + state.background_appearance = background_appearance; + let transparent = state.is_transparent(); + state.renderer.update_transparency(transparent); + } + + fn minimize(&self) { + let state = self.0.state.borrow(); + const WINDOW_ICONIC_STATE: u32 = 3; + let message = ClientMessageEvent::new( + 32, + self.0.x_window, + state.atoms.WM_CHANGE_STATE, + [WINDOW_ICONIC_STATE, 0, 0, 0, 0], + ); + check_reply( + || "X11 SendEvent to minimize window failed.", + self.0.xcb.send_event( + false, + state.x_root_window, + xproto::EventMask::SUBSTRUCTURE_REDIRECT | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + message, + ), + ) + .log_err(); + } + + fn zoom(&self) { + let state = self.0.state.borrow(); + self.set_wm_hints( + || "X11 SendEvent to maximize a window failed.", + WmHintPropertyState::Toggle, + state.atoms._NET_WM_STATE_MAXIMIZED_VERT, + state.atoms._NET_WM_STATE_MAXIMIZED_HORZ, + ) + .log_err(); + } + + fn toggle_fullscreen(&self) { + let state = self.0.state.borrow(); + self.set_wm_hints( + || "X11 SendEvent to fullscreen a window failed.", + WmHintPropertyState::Toggle, + state.atoms._NET_WM_STATE_FULLSCREEN, + xproto::AtomEnum::NONE.into(), + ) + .log_err(); + } + + fn is_fullscreen(&self) -> bool { + self.0.state.borrow().fullscreen + } + + fn on_request_frame(&self, callback: Box) { + self.0.callbacks.borrow_mut().request_frame = Some(callback); + } + + fn on_input(&self, callback: Box crate::DispatchEventResult>) { + self.0.callbacks.borrow_mut().input = Some(callback); + } + + fn on_active_status_change(&self, callback: Box) { + self.0.callbacks.borrow_mut().active_status_change = Some(callback); + } + + fn on_hover_status_change(&self, callback: Box) { + self.0.callbacks.borrow_mut().hovered_status_change = Some(callback); + } + + fn on_resize(&self, callback: Box, f32)>) { + self.0.callbacks.borrow_mut().resize = Some(callback); + } + + fn on_moved(&self, callback: Box) { + self.0.callbacks.borrow_mut().moved = Some(callback); + } + + fn on_should_close(&self, callback: Box bool>) { + self.0.callbacks.borrow_mut().should_close = Some(callback); + } + + fn on_close(&self, callback: Box) { + self.0.callbacks.borrow_mut().close = Some(callback); + } + + fn on_hit_test_window_control(&self, _callback: Box Option>) { + } + + fn on_appearance_changed(&self, callback: Box) { + self.0.callbacks.borrow_mut().appearance_changed = Some(callback); + } + + fn draw(&self, scene: &Scene) { + let mut inner = self.0.state.borrow_mut(); + inner.renderer.draw(scene); + } + + fn sprite_atlas(&self) -> Arc { + let inner = self.0.state.borrow(); + inner.renderer.sprite_atlas().clone() + } + + fn show_window_menu(&self, position: Point) { + let state = self.0.state.borrow(); + + check_reply( + || "X11 UngrabPointer failed.", + self.0.xcb.ungrab_pointer(x11rb::CURRENT_TIME), + ) + .log_err(); + + let Some(coords) = self.get_root_position(position).log_err() else { + return; + }; + let message = ClientMessageEvent::new( + 32, + self.0.x_window, + state.atoms._GTK_SHOW_WINDOW_MENU, + [ + XINPUT_ALL_DEVICE_GROUPS as u32, + coords.dst_x as u32, + coords.dst_y as u32, + 0, + 0, + ], + ); + check_reply( + || "X11 SendEvent to show window menu failed.", + self.0.xcb.send_event( + false, + state.x_root_window, + xproto::EventMask::SUBSTRUCTURE_REDIRECT | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + message, + ), + ) + .log_err(); + } + + fn start_window_move(&self) { + const MOVERESIZE_MOVE: u32 = 8; + self.send_moveresize(MOVERESIZE_MOVE).log_err(); + } + + fn start_window_resize(&self, edge: ResizeEdge) { + self.send_moveresize(edge.to_moveresize()).log_err(); + } + + fn window_decorations(&self) -> crate::Decorations { + let state = self.0.state.borrow(); + + // Client window decorations require compositor support + if !state.client_side_decorations_supported { + return Decorations::Server; + } + + match state.decorations { + WindowDecorations::Server => Decorations::Server, + WindowDecorations::Client => { + let tiling = if state.fullscreen { + Tiling::tiled() + } else if let Some(edge_constraints) = &state.edge_constraints { + edge_constraints.to_tiling() + } else { + // https://source.chromium.org/chromium/chromium/src/+/main:ui/ozone/platform/x11/x11_window.cc;l=2519;drc=1f14cc876cc5bf899d13284a12c451498219bb2d + Tiling { + top: state.maximized_vertical, + bottom: state.maximized_vertical, + left: state.maximized_horizontal, + right: state.maximized_horizontal, + } + }; + Decorations::Client { tiling } + } + } + } + + fn set_client_inset(&self, inset: Pixels) { + let mut state = self.0.state.borrow_mut(); + + let dp = (inset.0 * state.scale_factor) as u32; + + let insets = if state.fullscreen { + [0, 0, 0, 0] + } else if let Some(edge_constraints) = &state.edge_constraints { + let left = if edge_constraints.left_tiled { 0 } else { dp }; + let top = if edge_constraints.top_tiled { 0 } else { dp }; + let right = if edge_constraints.right_tiled { 0 } else { dp }; + let bottom = if edge_constraints.bottom_tiled { 0 } else { dp }; + + [left, right, top, bottom] + } else { + let (left, right) = if state.maximized_horizontal { + (0, 0) + } else { + (dp, dp) + }; + let (top, bottom) = if state.maximized_vertical { + (0, 0) + } else { + (dp, dp) + }; + [left, right, top, bottom] + }; + + if state.last_insets != insets { + state.last_insets = insets; + + check_reply( + || "X11 ChangeProperty for _GTK_FRAME_EXTENTS failed.", + self.0.xcb.change_property( + xproto::PropMode::REPLACE, + self.0.x_window, + state.atoms._GTK_FRAME_EXTENTS, + xproto::AtomEnum::CARDINAL, + size_of::() as u8 * 8, + 4, + bytemuck::cast_slice::(&insets), + ), + ) + .log_err(); + } + } + + fn request_decorations(&self, mut decorations: crate::WindowDecorations) { + let mut state = self.0.state.borrow_mut(); + + if matches!(decorations, crate::WindowDecorations::Client) + && !state.client_side_decorations_supported + { + log::info!( + "x11: no compositor present, falling back to server-side window decorations" + ); + decorations = crate::WindowDecorations::Server; + } + + // https://github.com/rust-windowing/winit/blob/master/src/platform_impl/linux/x11/util/hint.rs#L53-L87 + let hints_data: [u32; 5] = match decorations { + WindowDecorations::Server => [1 << 1, 0, 1, 0, 0], + WindowDecorations::Client => [1 << 1, 0, 0, 0, 0], + }; + + let success = check_reply( + || "X11 ChangeProperty for _MOTIF_WM_HINTS failed.", + self.0.xcb.change_property( + xproto::PropMode::REPLACE, + self.0.x_window, + state.atoms._MOTIF_WM_HINTS, + state.atoms._MOTIF_WM_HINTS, + size_of::() as u8 * 8, + 5, + bytemuck::cast_slice::(&hints_data), + ), + ) + .log_err(); + + let Some(()) = success else { + return; + }; + + match decorations { + WindowDecorations::Server => { + state.decorations = WindowDecorations::Server; + let is_transparent = state.is_transparent(); + state.renderer.update_transparency(is_transparent); + } + WindowDecorations::Client => { + state.decorations = WindowDecorations::Client; + let is_transparent = state.is_transparent(); + state.renderer.update_transparency(is_transparent); + } + } + + drop(state); + let mut callbacks = self.0.callbacks.borrow_mut(); + if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() { + appearance_changed(); + } + } + + fn update_ime_position(&self, bounds: Bounds) { + let mut state = self.0.state.borrow_mut(); + let client = state.client.clone(); + drop(state); + client.update_ime_position(bounds); + } + + fn gpu_specs(&self) -> Option { + self.0.state.borrow().renderer.gpu_specs().into() + } +} diff --git a/third_party/gpui/src/platform/linux/x11/xim_handler.rs b/third_party/gpui/src/platform/linux/x11/xim_handler.rs new file mode 100644 index 0000000..82b7c96 --- /dev/null +++ b/third_party/gpui/src/platform/linux/x11/xim_handler.rs @@ -0,0 +1,133 @@ +use std::default::Default; + +use x11rb::protocol::{Event, xproto}; +use xim::{AHashMap, AttributeName, Client, ClientError, ClientHandler, InputStyle}; + +pub enum XimCallbackEvent { + XimXEvent(x11rb::protocol::Event), + XimPreeditEvent(xproto::Window, String), + XimCommitEvent(xproto::Window, String), +} + +pub struct XimHandler { + pub im_id: u16, + pub ic_id: u16, + pub connected: bool, + pub window: xproto::Window, + pub last_callback_event: Option, +} + +impl XimHandler { + pub fn new() -> Self { + Self { + im_id: Default::default(), + ic_id: Default::default(), + connected: false, + window: Default::default(), + last_callback_event: None, + } + } +} + +impl> ClientHandler for XimHandler { + fn handle_connect(&mut self, client: &mut C) -> Result<(), ClientError> { + client.open("C") + } + + fn handle_open(&mut self, client: &mut C, input_method_id: u16) -> Result<(), ClientError> { + self.im_id = input_method_id; + + client.get_im_values(input_method_id, &[AttributeName::QueryInputStyle]) + } + + fn handle_get_im_values( + &mut self, + client: &mut C, + input_method_id: u16, + _attributes: AHashMap>, + ) -> Result<(), ClientError> { + let ic_attributes = client + .build_ic_attributes() + .push(AttributeName::InputStyle, InputStyle::PREEDIT_CALLBACKS) + .push(AttributeName::ClientWindow, self.window) + .push(AttributeName::FocusWindow, self.window) + .build(); + client.create_ic(input_method_id, ic_attributes) + } + + fn handle_create_ic( + &mut self, + _client: &mut C, + _input_method_id: u16, + input_context_id: u16, + ) -> Result<(), ClientError> { + self.connected = true; + self.ic_id = input_context_id; + Ok(()) + } + + fn handle_commit( + &mut self, + _client: &mut C, + _input_method_id: u16, + _input_context_id: u16, + text: &str, + ) -> Result<(), ClientError> { + self.last_callback_event = Some(XimCallbackEvent::XimCommitEvent( + self.window, + String::from(text), + )); + Ok(()) + } + + fn handle_forward_event( + &mut self, + _client: &mut C, + _input_method_id: u16, + _input_context_id: u16, + _flag: xim::ForwardEventFlag, + xev: C::XEvent, + ) -> Result<(), ClientError> { + match xev.response_type { + x11rb::protocol::xproto::KEY_PRESS_EVENT => { + self.last_callback_event = Some(XimCallbackEvent::XimXEvent(Event::KeyPress(xev))); + } + x11rb::protocol::xproto::KEY_RELEASE_EVENT => { + self.last_callback_event = + Some(XimCallbackEvent::XimXEvent(Event::KeyRelease(xev))); + } + _ => {} + } + Ok(()) + } + + fn handle_close(&mut self, client: &mut C, _input_method_id: u16) -> Result<(), ClientError> { + client.disconnect() + } + + fn handle_preedit_draw( + &mut self, + _client: &mut C, + _input_method_id: u16, + _input_context_id: u16, + _caret: i32, + _chg_first: i32, + _chg_len: i32, + _status: xim::PreeditDrawStatus, + preedit_string: &str, + _feedbacks: Vec, + ) -> Result<(), ClientError> { + // XIMReverse: 1, XIMPrimary: 8, XIMTertiary: 32: selected text + // XIMUnderline: 2, XIMSecondary: 16: underlined text + // XIMHighlight: 4: normal text + // XIMVisibleToForward: 64, XIMVisibleToBackward: 128, XIMVisibleCenter: 256: text align position + // XIMPrimary, XIMHighlight, XIMSecondary, XIMTertiary are not specified, + // but interchangeable as above + // Currently there's no way to support these. + self.last_callback_event = Some(XimCallbackEvent::XimPreeditEvent( + self.window, + String::from(preedit_string), + )); + Ok(()) + } +} diff --git a/third_party/gpui/src/platform/linux/xdg_desktop_portal.rs b/third_party/gpui/src/platform/linux/xdg_desktop_portal.rs new file mode 100644 index 0000000..722947a --- /dev/null +++ b/third_party/gpui/src/platform/linux/xdg_desktop_portal.rs @@ -0,0 +1,171 @@ +//! Provides a [calloop] event source from [XDG Desktop Portal] events +//! +//! This module uses the [ashpd] crate + +use ashpd::desktop::settings::{ColorScheme, Settings}; +use calloop::channel::Channel; +use calloop::{EventSource, Poll, PostAction, Readiness, Token, TokenFactory}; +use smol::stream::StreamExt; + +use crate::{BackgroundExecutor, WindowAppearance}; + +pub enum Event { + WindowAppearance(WindowAppearance), + #[cfg_attr(feature = "x11", allow(dead_code))] + CursorTheme(String), + #[cfg_attr(feature = "x11", allow(dead_code))] + CursorSize(u32), +} + +pub struct XDPEventSource { + channel: Channel, +} + +impl XDPEventSource { + pub fn new(executor: &BackgroundExecutor) -> Self { + let (sender, channel) = calloop::channel::channel(); + + let background = executor.clone(); + + executor + .spawn(async move { + let settings = Settings::new().await?; + + if let Ok(initial_appearance) = settings.color_scheme().await { + sender.send(Event::WindowAppearance(WindowAppearance::from_native( + initial_appearance, + )))?; + } + if let Ok(initial_theme) = settings + .read::("org.gnome.desktop.interface", "cursor-theme") + .await + { + sender.send(Event::CursorTheme(initial_theme))?; + } + + // If u32 is used here, it throws invalid type error + if let Ok(initial_size) = settings + .read::("org.gnome.desktop.interface", "cursor-size") + .await + { + sender.send(Event::CursorSize(initial_size as u32))?; + } + + if let Ok(mut cursor_theme_changed) = settings + .receive_setting_changed_with_args( + "org.gnome.desktop.interface", + "cursor-theme", + ) + .await + { + let sender = sender.clone(); + background + .spawn(async move { + while let Some(theme) = cursor_theme_changed.next().await { + let theme = theme?; + sender.send(Event::CursorTheme(theme))?; + } + anyhow::Ok(()) + }) + .detach(); + } + + if let Ok(mut cursor_size_changed) = settings + .receive_setting_changed_with_args::( + "org.gnome.desktop.interface", + "cursor-size", + ) + .await + { + let sender = sender.clone(); + background + .spawn(async move { + while let Some(size) = cursor_size_changed.next().await { + let size = size?; + sender.send(Event::CursorSize(size as u32))?; + } + anyhow::Ok(()) + }) + .detach(); + } + + let mut appearance_changed = settings.receive_color_scheme_changed().await?; + while let Some(scheme) = appearance_changed.next().await { + sender.send(Event::WindowAppearance(WindowAppearance::from_native( + scheme, + )))?; + } + + anyhow::Ok(()) + }) + .detach(); + + Self { channel } + } +} + +impl EventSource for XDPEventSource { + type Event = Event; + type Metadata = (); + type Ret = (); + type Error = anyhow::Error; + + fn process_events( + &mut self, + readiness: Readiness, + token: Token, + mut callback: F, + ) -> Result + where + F: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret, + { + self.channel.process_events(readiness, token, |evt, _| { + if let calloop::channel::Event::Msg(msg) = evt { + (callback)(msg, &mut ()) + } + })?; + + Ok(PostAction::Continue) + } + + fn register( + &mut self, + poll: &mut Poll, + token_factory: &mut TokenFactory, + ) -> calloop::Result<()> { + self.channel.register(poll, token_factory)?; + + Ok(()) + } + + fn reregister( + &mut self, + poll: &mut Poll, + token_factory: &mut TokenFactory, + ) -> calloop::Result<()> { + self.channel.reregister(poll, token_factory)?; + + Ok(()) + } + + fn unregister(&mut self, poll: &mut Poll) -> calloop::Result<()> { + self.channel.unregister(poll)?; + + Ok(()) + } +} + +impl WindowAppearance { + fn from_native(cs: ColorScheme) -> WindowAppearance { + match cs { + ColorScheme::PreferDark => WindowAppearance::Dark, + ColorScheme::PreferLight => WindowAppearance::Light, + ColorScheme::NoPreference => WindowAppearance::Light, + } + } + + #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] + fn set_native(&mut self, cs: ColorScheme) { + *self = Self::from_native(cs); + } +} diff --git a/third_party/gpui/src/platform/mac.rs b/third_party/gpui/src/platform/mac.rs new file mode 100644 index 0000000..76d636b --- /dev/null +++ b/third_party/gpui/src/platform/mac.rs @@ -0,0 +1,163 @@ +//! Macos screen have a y axis that goings up from the bottom of the screen and +//! an origin at the bottom left of the main display. +mod dispatcher; +mod display; +mod display_link; +mod events; +mod keyboard; + +#[cfg(feature = "screen-capture")] +mod screen_capture; + +#[cfg(not(feature = "macos-blade"))] +mod metal_atlas; +#[cfg(not(feature = "macos-blade"))] +pub mod metal_renderer; + +use core_video::image_buffer::CVImageBuffer; +#[cfg(not(feature = "macos-blade"))] +use metal_renderer as renderer; + +#[cfg(feature = "macos-blade")] +use crate::platform::blade as renderer; + +mod attributed_string; + +#[cfg(feature = "font-kit")] +mod open_type; + +#[cfg(feature = "font-kit")] +mod text_system; + +mod platform; +mod window; +mod window_appearance; + +use crate::{DevicePixels, Pixels, Size, px, size}; +use cocoa::{ + base::{id, nil}, + foundation::{NSAutoreleasePool, NSNotFound, NSRect, NSSize, NSString, NSUInteger}, +}; + +use objc::runtime::{BOOL, NO, YES}; +use std::{ + ffi::{CStr, c_char}, + ops::Range, +}; + +pub(crate) use dispatcher::*; +pub(crate) use display::*; +pub(crate) use display_link::*; +pub(crate) use keyboard::*; +pub(crate) use platform::*; +pub(crate) use window::*; + +#[cfg(feature = "font-kit")] +pub(crate) use text_system::*; + +/// A frame of video captured from a screen. +pub(crate) type PlatformScreenCaptureFrame = CVImageBuffer; + +trait BoolExt { + fn to_objc(self) -> BOOL; +} + +impl BoolExt for bool { + fn to_objc(self) -> BOOL { + if self { YES } else { NO } + } +} + +trait NSStringExt { + unsafe fn to_str(&self) -> &str; +} + +impl NSStringExt for id { + unsafe fn to_str(&self) -> &str { + unsafe { + let cstr = self.UTF8String(); + if cstr.is_null() { + "" + } else { + CStr::from_ptr(cstr as *mut c_char).to_str().unwrap() + } + } + } +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct NSRange { + pub location: NSUInteger, + pub length: NSUInteger, +} + +impl NSRange { + fn invalid() -> Self { + Self { + location: NSNotFound as NSUInteger, + length: 0, + } + } + + fn is_valid(&self) -> bool { + self.location != NSNotFound as NSUInteger + } + + fn to_range(self) -> Option> { + if self.is_valid() { + let start = self.location as usize; + let end = start + self.length as usize; + Some(start..end) + } else { + None + } + } +} + +impl From> for NSRange { + fn from(range: Range) -> Self { + NSRange { + location: range.start as NSUInteger, + length: range.len() as NSUInteger, + } + } +} + +unsafe impl objc::Encode for NSRange { + fn encode() -> objc::Encoding { + let encoding = format!( + "{{NSRange={}{}}}", + NSUInteger::encode().as_str(), + NSUInteger::encode().as_str() + ); + unsafe { objc::Encoding::from_str(&encoding) } + } +} + +unsafe fn ns_string(string: &str) -> id { + unsafe { NSString::alloc(nil).init_str(string).autorelease() } +} + +impl From for Size { + fn from(value: NSSize) -> Self { + Size { + width: px(value.width as f32), + height: px(value.height as f32), + } + } +} + +impl From for Size { + fn from(rect: NSRect) -> Self { + let NSSize { width, height } = rect.size; + size(width.into(), height.into()) + } +} + +impl From for Size { + fn from(rect: NSRect) -> Self { + let NSSize { width, height } = rect.size; + size(DevicePixels(width as i32), DevicePixels(height as i32)) + } +} diff --git a/third_party/gpui/src/platform/mac/attributed_string.rs b/third_party/gpui/src/platform/mac/attributed_string.rs new file mode 100644 index 0000000..5f313ac --- /dev/null +++ b/third_party/gpui/src/platform/mac/attributed_string.rs @@ -0,0 +1,119 @@ +use cocoa::base::id; +use cocoa::foundation::NSRange; +use objc::{class, msg_send, sel, sel_impl}; + +/// The `cocoa` crate does not define NSAttributedString (and related Cocoa classes), +/// which are needed for copying rich text (that is, text intermingled with images) +/// to the clipboard. This adds access to those APIs. +#[allow(non_snake_case)] +pub trait NSAttributedString: Sized { + unsafe fn alloc(_: Self) -> id { + msg_send![class!(NSAttributedString), alloc] + } + + unsafe fn init_attributed_string(self, string: id) -> id; + unsafe fn appendAttributedString_(self, attr_string: id); + unsafe fn RTFDFromRange_documentAttributes_(self, range: NSRange, attrs: id) -> id; + unsafe fn RTFFromRange_documentAttributes_(self, range: NSRange, attrs: id) -> id; + unsafe fn string(self) -> id; +} + +impl NSAttributedString for id { + unsafe fn init_attributed_string(self, string: id) -> id { + msg_send![self, initWithString: string] + } + + unsafe fn appendAttributedString_(self, attr_string: id) { + let _: () = msg_send![self, appendAttributedString: attr_string]; + } + + unsafe fn RTFDFromRange_documentAttributes_(self, range: NSRange, attrs: id) -> id { + msg_send![self, RTFDFromRange: range documentAttributes: attrs] + } + + unsafe fn RTFFromRange_documentAttributes_(self, range: NSRange, attrs: id) -> id { + msg_send![self, RTFFromRange: range documentAttributes: attrs] + } + + unsafe fn string(self) -> id { + msg_send![self, string] + } +} + +pub trait NSMutableAttributedString: NSAttributedString { + unsafe fn alloc(_: Self) -> id { + msg_send![class!(NSMutableAttributedString), alloc] + } +} + +impl NSMutableAttributedString for id {} + +#[cfg(test)] +mod tests { + use super::*; + use cocoa::appkit::NSImage; + use cocoa::base::nil; + use cocoa::foundation::NSString; + #[test] + #[ignore] // This was SIGSEGV-ing on CI but not locally; need to investigate https://github.com/zed-industries/zed/actions/runs/10362363230/job/28684225486?pr=15782#step:4:1348 + fn test_nsattributed_string() { + // TODO move these to parent module once it's actually ready to be used + #[allow(non_snake_case)] + pub trait NSTextAttachment: Sized { + unsafe fn alloc(_: Self) -> id { + msg_send![class!(NSTextAttachment), alloc] + } + } + + impl NSTextAttachment for id {} + + unsafe { + let image: id = msg_send![class!(NSImage), alloc]; + image.initWithContentsOfFile_(NSString::alloc(nil).init_str("test.jpeg")); + let _size = image.size(); + + let string = NSString::alloc(nil).init_str("Test String"); + let attr_string = NSMutableAttributedString::alloc(nil).init_attributed_string(string); + let hello_string = NSString::alloc(nil).init_str("Hello World"); + let hello_attr_string = + NSAttributedString::alloc(nil).init_attributed_string(hello_string); + attr_string.appendAttributedString_(hello_attr_string); + + let attachment = NSTextAttachment::alloc(nil); + let _: () = msg_send![attachment, setImage: image]; + let image_attr_string = + msg_send![class!(NSAttributedString), attributedStringWithAttachment: attachment]; + attr_string.appendAttributedString_(image_attr_string); + + let another_string = NSString::alloc(nil).init_str("Another String"); + let another_attr_string = + NSAttributedString::alloc(nil).init_attributed_string(another_string); + attr_string.appendAttributedString_(another_attr_string); + + let _len: cocoa::foundation::NSUInteger = msg_send![attr_string, length]; + + /////////////////////////////////////////////////// + // pasteboard.clearContents(); + + let rtfd_data = attr_string.RTFDFromRange_documentAttributes_( + NSRange::new(0, msg_send![attr_string, length]), + nil, + ); + assert_ne!(rtfd_data, nil); + // if rtfd_data != nil { + // pasteboard.setData_forType(rtfd_data, NSPasteboardTypeRTFD); + // } + + // let rtf_data = attributed_string.RTFFromRange_documentAttributes_( + // NSRange::new(0, attributed_string.length()), + // nil, + // ); + // if rtf_data != nil { + // pasteboard.setData_forType(rtf_data, NSPasteboardTypeRTF); + // } + + // let plain_text = attributed_string.string(); + // pasteboard.setString_forType(plain_text, NSPasteboardTypeString); + } + } +} diff --git a/third_party/gpui/src/platform/mac/dispatch.h b/third_party/gpui/src/platform/mac/dispatch.h new file mode 100644 index 0000000..54f3818 --- /dev/null +++ b/third_party/gpui/src/platform/mac/dispatch.h @@ -0,0 +1,2 @@ +#include +#include diff --git a/third_party/gpui/src/platform/mac/dispatcher.rs b/third_party/gpui/src/platform/mac/dispatcher.rs new file mode 100644 index 0000000..c72f791 --- /dev/null +++ b/third_party/gpui/src/platform/mac/dispatcher.rs @@ -0,0 +1,75 @@ +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] + +use crate::{PlatformDispatcher, TaskLabel}; +use async_task::Runnable; +use objc::{ + class, msg_send, + runtime::{BOOL, YES}, + sel, sel_impl, +}; +use std::{ + ffi::c_void, + ptr::{NonNull, addr_of}, + time::Duration, +}; + +/// All items in the generated file are marked as pub, so we're gonna wrap it in a separate mod to prevent +/// these pub items from leaking into public API. +pub(crate) mod dispatch_sys { + include!(concat!(env!("OUT_DIR"), "/dispatch_sys.rs")); +} + +use dispatch_sys::*; +pub(crate) fn dispatch_get_main_queue() -> dispatch_queue_t { + addr_of!(_dispatch_main_q) as *const _ as dispatch_queue_t +} + +pub(crate) struct MacDispatcher; + +impl PlatformDispatcher for MacDispatcher { + fn is_main_thread(&self) -> bool { + let is_main_thread: BOOL = unsafe { msg_send![class!(NSThread), isMainThread] }; + is_main_thread == YES + } + + fn dispatch(&self, runnable: Runnable, _: Option) { + unsafe { + dispatch_async_f( + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH.try_into().unwrap(), 0), + runnable.into_raw().as_ptr() as *mut c_void, + Some(trampoline), + ); + } + } + + fn dispatch_on_main_thread(&self, runnable: Runnable) { + unsafe { + dispatch_async_f( + dispatch_get_main_queue(), + runnable.into_raw().as_ptr() as *mut c_void, + Some(trampoline), + ); + } + } + + fn dispatch_after(&self, duration: Duration, runnable: Runnable) { + unsafe { + let queue = + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH.try_into().unwrap(), 0); + let when = dispatch_time(DISPATCH_TIME_NOW as u64, duration.as_nanos() as i64); + dispatch_after_f( + when, + queue, + runnable.into_raw().as_ptr() as *mut c_void, + Some(trampoline), + ); + } + } +} + +extern "C" fn trampoline(runnable: *mut c_void) { + let task = unsafe { Runnable::<()>::from_raw(NonNull::new_unchecked(runnable as *mut ())) }; + task.run(); +} diff --git a/third_party/gpui/src/platform/mac/display.rs b/third_party/gpui/src/platform/mac/display.rs new file mode 100644 index 0000000..4ee2702 --- /dev/null +++ b/third_party/gpui/src/platform/mac/display.rs @@ -0,0 +1,117 @@ +use crate::{Bounds, DisplayId, Pixels, PlatformDisplay, px, size}; +use anyhow::Result; +use cocoa::{ + appkit::NSScreen, + base::{id, nil}, + foundation::{NSDictionary, NSString}, +}; +use core_foundation::uuid::{CFUUIDGetUUIDBytes, CFUUIDRef}; +use core_graphics::display::{CGDirectDisplayID, CGDisplayBounds, CGGetActiveDisplayList}; +use objc::{msg_send, sel, sel_impl}; +use uuid::Uuid; + +#[derive(Debug)] +pub(crate) struct MacDisplay(pub(crate) CGDirectDisplayID); + +unsafe impl Send for MacDisplay {} + +impl MacDisplay { + /// Get the screen with the given [`DisplayId`]. + pub fn find_by_id(id: DisplayId) -> Option { + Self::all().find(|screen| screen.id() == id) + } + + /// Get the primary screen - the one with the menu bar, and whose bottom left + /// corner is at the origin of the AppKit coordinate system. + pub fn primary() -> Self { + // Instead of iterating through all active systems displays via `all()` we use the first + // NSScreen and gets its CGDirectDisplayID, because we can't be sure that `CGGetActiveDisplayList` + // will always return a list of active displays (machine might be sleeping). + // + // The following is what Chromium does too: + // + // https://chromium.googlesource.com/chromium/src/+/66.0.3359.158/ui/display/mac/screen_mac.mm#56 + unsafe { + let screens = NSScreen::screens(nil); + let screen = cocoa::foundation::NSArray::objectAtIndex(screens, 0); + let device_description = NSScreen::deviceDescription(screen); + let screen_number_key: id = NSString::alloc(nil).init_str("NSScreenNumber"); + let screen_number = device_description.objectForKey_(screen_number_key); + let screen_number: CGDirectDisplayID = msg_send![screen_number, unsignedIntegerValue]; + Self(screen_number) + } + } + + /// Obtains an iterator over all currently active system displays. + pub fn all() -> impl Iterator { + unsafe { + // We're assuming there aren't more than 32 displays connected to the system. + let mut displays = Vec::with_capacity(32); + let mut display_count = 0; + let result = CGGetActiveDisplayList( + displays.capacity() as u32, + displays.as_mut_ptr(), + &mut display_count, + ); + + if result == 0 { + displays.set_len(display_count as usize); + displays.into_iter().map(MacDisplay) + } else { + panic!("Failed to get active display list. Result: {result}"); + } + } + } +} + +#[link(name = "ApplicationServices", kind = "framework")] +unsafe extern "C" { + fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef; +} + +impl PlatformDisplay for MacDisplay { + fn id(&self) -> DisplayId { + DisplayId(self.0) + } + + fn uuid(&self) -> Result { + let cfuuid = unsafe { CGDisplayCreateUUIDFromDisplayID(self.0 as CGDirectDisplayID) }; + anyhow::ensure!( + !cfuuid.is_null(), + "AppKit returned a null from CGDisplayCreateUUIDFromDisplayID" + ); + + let bytes = unsafe { CFUUIDGetUUIDBytes(cfuuid) }; + Ok(Uuid::from_bytes([ + bytes.byte0, + bytes.byte1, + bytes.byte2, + bytes.byte3, + bytes.byte4, + bytes.byte5, + bytes.byte6, + bytes.byte7, + bytes.byte8, + bytes.byte9, + bytes.byte10, + bytes.byte11, + bytes.byte12, + bytes.byte13, + bytes.byte14, + bytes.byte15, + ])) + } + + fn bounds(&self) -> Bounds { + unsafe { + // CGDisplayBounds is in "global display" coordinates, where 0 is + // the top left of the primary display. + let bounds = CGDisplayBounds(self.0); + + Bounds { + origin: Default::default(), + size: size(px(bounds.size.width as f32), px(bounds.size.height as f32)), + } + } + } +} diff --git a/third_party/gpui/src/platform/mac/display_link.rs b/third_party/gpui/src/platform/mac/display_link.rs new file mode 100644 index 0000000..ce39b41 --- /dev/null +++ b/third_party/gpui/src/platform/mac/display_link.rs @@ -0,0 +1,283 @@ +use crate::{ + dispatch_get_main_queue, + dispatch_sys::{ + _dispatch_source_type_data_add, dispatch_resume, dispatch_set_context, + dispatch_source_cancel, dispatch_source_create, dispatch_source_merge_data, + dispatch_source_set_event_handler_f, dispatch_source_t, dispatch_suspend, + }, +}; +use anyhow::Result; +use core_graphics::display::CGDirectDisplayID; +use std::ffi::c_void; +use util::ResultExt; + +pub struct DisplayLink { + display_link: Option, + frame_requests: dispatch_source_t, +} + +impl DisplayLink { + pub fn new( + display_id: CGDirectDisplayID, + data: *mut c_void, + callback: unsafe extern "C" fn(*mut c_void), + ) -> Result { + unsafe extern "C" fn display_link_callback( + _display_link_out: *mut sys::CVDisplayLink, + _current_time: *const sys::CVTimeStamp, + _output_time: *const sys::CVTimeStamp, + _flags_in: i64, + _flags_out: *mut i64, + frame_requests: *mut c_void, + ) -> i32 { + unsafe { + let frame_requests = frame_requests as dispatch_source_t; + dispatch_source_merge_data(frame_requests, 1); + 0 + } + } + + unsafe { + let frame_requests = dispatch_source_create( + &_dispatch_source_type_data_add, + 0, + 0, + dispatch_get_main_queue(), + ); + dispatch_set_context( + crate::dispatch_sys::dispatch_object_t { + _ds: frame_requests, + }, + data, + ); + dispatch_source_set_event_handler_f(frame_requests, Some(callback)); + + let display_link = sys::DisplayLink::new( + display_id, + display_link_callback, + frame_requests as *mut c_void, + )?; + + Ok(Self { + display_link: Some(display_link), + frame_requests, + }) + } + } + + pub fn start(&mut self) -> Result<()> { + unsafe { + dispatch_resume(crate::dispatch_sys::dispatch_object_t { + _ds: self.frame_requests, + }); + self.display_link.as_mut().unwrap().start()?; + } + Ok(()) + } + + pub fn stop(&mut self) -> Result<()> { + unsafe { + dispatch_suspend(crate::dispatch_sys::dispatch_object_t { + _ds: self.frame_requests, + }); + self.display_link.as_mut().unwrap().stop()?; + } + Ok(()) + } +} + +impl Drop for DisplayLink { + fn drop(&mut self) { + self.stop().log_err(); + // We see occasional segfaults on the CVDisplayLink thread. + // + // It seems possible that this happens because CVDisplayLinkRelease releases the CVDisplayLink + // on the main thread immediately, but the background thread that CVDisplayLink uses for timers + // is still accessing it. + // + // We might also want to upgrade to CADisplayLink, but that requires dropping old macOS support. + std::mem::forget(self.display_link.take()); + unsafe { + dispatch_source_cancel(self.frame_requests); + } + } +} + +mod sys { + //! Derived from display-link crate under the following license: + //! + //! Apple docs: [CVDisplayLink](https://developer.apple.com/documentation/corevideo/cvdisplaylinkoutputcallback?language=objc) + #![allow(dead_code, non_upper_case_globals)] + + use anyhow::Result; + use core_graphics::display::CGDirectDisplayID; + use foreign_types::{ForeignType, foreign_type}; + use std::{ + ffi::c_void, + fmt::{self, Debug, Formatter}, + }; + + #[derive(Debug)] + pub enum CVDisplayLink {} + + foreign_type! { + pub unsafe type DisplayLink { + type CType = CVDisplayLink; + fn drop = CVDisplayLinkRelease; + fn clone = CVDisplayLinkRetain; + } + } + + impl Debug for DisplayLink { + fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { + formatter + .debug_tuple("DisplayLink") + .field(&self.as_ptr()) + .finish() + } + } + + #[repr(C)] + #[derive(Clone, Copy)] + pub(crate) struct CVTimeStamp { + pub version: u32, + pub video_time_scale: i32, + pub video_time: i64, + pub host_time: u64, + pub rate_scalar: f64, + pub video_refresh_period: i64, + pub smpte_time: CVSMPTETime, + pub flags: u64, + pub reserved: u64, + } + + pub type CVTimeStampFlags = u64; + + pub const kCVTimeStampVideoTimeValid: CVTimeStampFlags = 1 << 0; + pub const kCVTimeStampHostTimeValid: CVTimeStampFlags = 1 << 1; + pub const kCVTimeStampSMPTETimeValid: CVTimeStampFlags = 1 << 2; + pub const kCVTimeStampVideoRefreshPeriodValid: CVTimeStampFlags = 1 << 3; + pub const kCVTimeStampRateScalarValid: CVTimeStampFlags = 1 << 4; + pub const kCVTimeStampTopField: CVTimeStampFlags = 1 << 16; + pub const kCVTimeStampBottomField: CVTimeStampFlags = 1 << 17; + pub const kCVTimeStampVideoHostTimeValid: CVTimeStampFlags = + kCVTimeStampVideoTimeValid | kCVTimeStampHostTimeValid; + pub const kCVTimeStampIsInterlaced: CVTimeStampFlags = + kCVTimeStampTopField | kCVTimeStampBottomField; + + #[repr(C)] + #[derive(Clone, Copy, Default)] + pub(crate) struct CVSMPTETime { + pub subframes: i16, + pub subframe_divisor: i16, + pub counter: u32, + pub time_type: u32, + pub flags: u32, + pub hours: i16, + pub minutes: i16, + pub seconds: i16, + pub frames: i16, + } + + pub type CVSMPTETimeType = u32; + + pub const kCVSMPTETimeType24: CVSMPTETimeType = 0; + pub const kCVSMPTETimeType25: CVSMPTETimeType = 1; + pub const kCVSMPTETimeType30Drop: CVSMPTETimeType = 2; + pub const kCVSMPTETimeType30: CVSMPTETimeType = 3; + pub const kCVSMPTETimeType2997: CVSMPTETimeType = 4; + pub const kCVSMPTETimeType2997Drop: CVSMPTETimeType = 5; + pub const kCVSMPTETimeType60: CVSMPTETimeType = 6; + pub const kCVSMPTETimeType5994: CVSMPTETimeType = 7; + + pub type CVSMPTETimeFlags = u32; + + pub const kCVSMPTETimeValid: CVSMPTETimeFlags = 1 << 0; + pub const kCVSMPTETimeRunning: CVSMPTETimeFlags = 1 << 1; + + pub type CVDisplayLinkOutputCallback = unsafe extern "C" fn( + display_link_out: *mut CVDisplayLink, + // A pointer to the current timestamp. This represents the timestamp when the callback is called. + current_time: *const CVTimeStamp, + // A pointer to the output timestamp. This represents the timestamp for when the frame will be displayed. + output_time: *const CVTimeStamp, + // Unused + flags_in: i64, + // Unused + flags_out: *mut i64, + // A pointer to app-defined data. + display_link_context: *mut c_void, + ) -> i32; + + #[link(name = "CoreFoundation", kind = "framework")] + #[link(name = "CoreVideo", kind = "framework")] + #[allow(improper_ctypes, unknown_lints, clippy::duplicated_attributes)] + unsafe extern "C" { + pub fn CVDisplayLinkCreateWithActiveCGDisplays( + display_link_out: *mut *mut CVDisplayLink, + ) -> i32; + pub fn CVDisplayLinkSetCurrentCGDisplay( + display_link: &mut DisplayLinkRef, + display_id: u32, + ) -> i32; + pub fn CVDisplayLinkSetOutputCallback( + display_link: &mut DisplayLinkRef, + callback: CVDisplayLinkOutputCallback, + user_info: *mut c_void, + ) -> i32; + pub fn CVDisplayLinkStart(display_link: &mut DisplayLinkRef) -> i32; + pub fn CVDisplayLinkStop(display_link: &mut DisplayLinkRef) -> i32; + pub fn CVDisplayLinkRelease(display_link: *mut CVDisplayLink); + pub fn CVDisplayLinkRetain(display_link: *mut CVDisplayLink) -> *mut CVDisplayLink; + } + + impl DisplayLink { + /// Apple docs: [CVDisplayLinkCreateWithCGDisplay](https://developer.apple.com/documentation/corevideo/1456981-cvdisplaylinkcreatewithcgdisplay?language=objc) + pub unsafe fn new( + display_id: CGDirectDisplayID, + callback: CVDisplayLinkOutputCallback, + user_info: *mut c_void, + ) -> Result { + unsafe { + let mut display_link: *mut CVDisplayLink = 0 as _; + + let code = CVDisplayLinkCreateWithActiveCGDisplays(&mut display_link); + anyhow::ensure!(code == 0, "could not create display link, code: {}", code); + + let mut display_link = DisplayLink::from_ptr(display_link); + + let code = CVDisplayLinkSetOutputCallback(&mut display_link, callback, user_info); + anyhow::ensure!(code == 0, "could not set output callback, code: {}", code); + + let code = CVDisplayLinkSetCurrentCGDisplay(&mut display_link, display_id); + anyhow::ensure!( + code == 0, + "could not assign display to display link, code: {}", + code + ); + + Ok(display_link) + } + } + } + + impl DisplayLinkRef { + /// Apple docs: [CVDisplayLinkStart](https://developer.apple.com/documentation/corevideo/1457193-cvdisplaylinkstart?language=objc) + pub unsafe fn start(&mut self) -> Result<()> { + unsafe { + let code = CVDisplayLinkStart(self); + anyhow::ensure!(code == 0, "could not start display link, code: {}", code); + Ok(()) + } + } + + /// Apple docs: [CVDisplayLinkStop](https://developer.apple.com/documentation/corevideo/1457281-cvdisplaylinkstop?language=objc) + pub unsafe fn stop(&mut self) -> Result<()> { + unsafe { + let code = CVDisplayLinkStop(self); + anyhow::ensure!(code == 0, "could not stop display link, code: {}", code); + Ok(()) + } + } + } +} diff --git a/third_party/gpui/src/platform/mac/events.rs b/third_party/gpui/src/platform/mac/events.rs new file mode 100644 index 0000000..938db4b --- /dev/null +++ b/third_party/gpui/src/platform/mac/events.rs @@ -0,0 +1,533 @@ +use crate::{ + Capslock, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, + MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, + PlatformInput, ScrollDelta, ScrollWheelEvent, TouchPhase, + platform::mac::{ + LMGetKbdType, NSStringExt, TISCopyCurrentKeyboardLayoutInputSource, + TISGetInputSourceProperty, UCKeyTranslate, kTISPropertyUnicodeKeyLayoutData, + }, + point, px, +}; +use cocoa::{ + appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType}, + base::{YES, id}, +}; +use core_foundation::data::{CFDataGetBytePtr, CFDataRef}; +use core_graphics::event::CGKeyCode; +use objc::{msg_send, sel, sel_impl}; +use std::{borrow::Cow, ffi::c_void}; + +const BACKSPACE_KEY: u16 = 0x7f; +const SPACE_KEY: u16 = b' ' as u16; +const ENTER_KEY: u16 = 0x0d; +const NUMPAD_ENTER_KEY: u16 = 0x03; +pub(crate) const ESCAPE_KEY: u16 = 0x1b; +const TAB_KEY: u16 = 0x09; +const SHIFT_TAB_KEY: u16 = 0x19; + +pub fn key_to_native(key: &str) -> Cow<'_, str> { + use cocoa::appkit::*; + let code = match key { + "space" => SPACE_KEY, + "backspace" => BACKSPACE_KEY, + "escape" => ESCAPE_KEY, + "up" => NSUpArrowFunctionKey, + "down" => NSDownArrowFunctionKey, + "left" => NSLeftArrowFunctionKey, + "right" => NSRightArrowFunctionKey, + "pageup" => NSPageUpFunctionKey, + "pagedown" => NSPageDownFunctionKey, + "home" => NSHomeFunctionKey, + "end" => NSEndFunctionKey, + "delete" => NSDeleteFunctionKey, + "insert" => NSHelpFunctionKey, + "f1" => NSF1FunctionKey, + "f2" => NSF2FunctionKey, + "f3" => NSF3FunctionKey, + "f4" => NSF4FunctionKey, + "f5" => NSF5FunctionKey, + "f6" => NSF6FunctionKey, + "f7" => NSF7FunctionKey, + "f8" => NSF8FunctionKey, + "f9" => NSF9FunctionKey, + "f10" => NSF10FunctionKey, + "f11" => NSF11FunctionKey, + "f12" => NSF12FunctionKey, + "f13" => NSF13FunctionKey, + "f14" => NSF14FunctionKey, + "f15" => NSF15FunctionKey, + "f16" => NSF16FunctionKey, + "f17" => NSF17FunctionKey, + "f18" => NSF18FunctionKey, + "f19" => NSF19FunctionKey, + "f20" => NSF20FunctionKey, + "f21" => NSF21FunctionKey, + "f22" => NSF22FunctionKey, + "f23" => NSF23FunctionKey, + "f24" => NSF24FunctionKey, + "f25" => NSF25FunctionKey, + "f26" => NSF26FunctionKey, + "f27" => NSF27FunctionKey, + "f28" => NSF28FunctionKey, + "f29" => NSF29FunctionKey, + "f30" => NSF30FunctionKey, + "f31" => NSF31FunctionKey, + "f32" => NSF32FunctionKey, + "f33" => NSF33FunctionKey, + "f34" => NSF34FunctionKey, + "f35" => NSF35FunctionKey, + _ => return Cow::Borrowed(key), + }; + Cow::Owned(String::from_utf16(&[code]).unwrap()) +} + +unsafe fn read_modifiers(native_event: id) -> Modifiers { + unsafe { + let modifiers = native_event.modifierFlags(); + let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask); + let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); + let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); + let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); + let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask); + + Modifiers { + control, + alt, + shift, + platform: command, + function, + } + } +} + +impl PlatformInput { + pub(crate) unsafe fn from_native( + native_event: id, + window_height: Option, + ) -> Option { + unsafe { + let event_type = native_event.eventType(); + + // Filter out event types that aren't in the NSEventType enum. + // See https://github.com/servo/cocoa-rs/issues/155#issuecomment-323482792 for details. + match event_type as u64 { + 0 | 21 | 32 | 33 | 35 | 36 | 37 => { + return None; + } + _ => {} + } + + match event_type { + NSEventType::NSFlagsChanged => { + Some(Self::ModifiersChanged(ModifiersChangedEvent { + modifiers: read_modifiers(native_event), + capslock: Capslock { + on: native_event + .modifierFlags() + .contains(NSEventModifierFlags::NSAlphaShiftKeyMask), + }, + })) + } + NSEventType::NSKeyDown => Some(Self::KeyDown(KeyDownEvent { + keystroke: parse_keystroke(native_event), + is_held: native_event.isARepeat() == YES, + })), + NSEventType::NSKeyUp => Some(Self::KeyUp(KeyUpEvent { + keystroke: parse_keystroke(native_event), + })), + NSEventType::NSLeftMouseDown + | NSEventType::NSRightMouseDown + | NSEventType::NSOtherMouseDown => { + let button = match native_event.buttonNumber() { + 0 => MouseButton::Left, + 1 => MouseButton::Right, + 2 => MouseButton::Middle, + 3 => MouseButton::Navigate(NavigationDirection::Back), + 4 => MouseButton::Navigate(NavigationDirection::Forward), + // Other mouse buttons aren't tracked currently + _ => return None, + }; + window_height.map(|window_height| { + Self::MouseDown(MouseDownEvent { + button, + position: point( + px(native_event.locationInWindow().x as f32), + // MacOS screen coordinates are relative to bottom left + window_height - px(native_event.locationInWindow().y as f32), + ), + modifiers: read_modifiers(native_event), + click_count: native_event.clickCount() as usize, + first_mouse: false, + }) + }) + } + NSEventType::NSLeftMouseUp + | NSEventType::NSRightMouseUp + | NSEventType::NSOtherMouseUp => { + let button = match native_event.buttonNumber() { + 0 => MouseButton::Left, + 1 => MouseButton::Right, + 2 => MouseButton::Middle, + 3 => MouseButton::Navigate(NavigationDirection::Back), + 4 => MouseButton::Navigate(NavigationDirection::Forward), + // Other mouse buttons aren't tracked currently + _ => return None, + }; + + window_height.map(|window_height| { + Self::MouseUp(MouseUpEvent { + button, + position: point( + px(native_event.locationInWindow().x as f32), + window_height - px(native_event.locationInWindow().y as f32), + ), + modifiers: read_modifiers(native_event), + click_count: native_event.clickCount() as usize, + }) + }) + } + // Some mice (like Logitech MX Master) send navigation buttons as swipe events + NSEventType::NSEventTypeSwipe => { + let navigation_direction = match native_event.phase() { + NSEventPhase::NSEventPhaseEnded => match native_event.deltaX() { + x if x > 0.0 => Some(NavigationDirection::Back), + x if x < 0.0 => Some(NavigationDirection::Forward), + _ => return None, + }, + _ => return None, + }; + + match navigation_direction { + Some(direction) => window_height.map(|window_height| { + Self::MouseDown(MouseDownEvent { + button: MouseButton::Navigate(direction), + position: point( + px(native_event.locationInWindow().x as f32), + window_height - px(native_event.locationInWindow().y as f32), + ), + modifiers: read_modifiers(native_event), + click_count: 1, + first_mouse: false, + }) + }), + _ => None, + } + } + NSEventType::NSScrollWheel => window_height.map(|window_height| { + let phase = match native_event.phase() { + NSEventPhase::NSEventPhaseMayBegin | NSEventPhase::NSEventPhaseBegan => { + TouchPhase::Started + } + NSEventPhase::NSEventPhaseEnded => TouchPhase::Ended, + _ => TouchPhase::Moved, + }; + + let raw_data = point( + native_event.scrollingDeltaX() as f32, + native_event.scrollingDeltaY() as f32, + ); + + let delta = if native_event.hasPreciseScrollingDeltas() == YES { + ScrollDelta::Pixels(raw_data.map(px)) + } else { + ScrollDelta::Lines(raw_data) + }; + + Self::ScrollWheel(ScrollWheelEvent { + position: point( + px(native_event.locationInWindow().x as f32), + window_height - px(native_event.locationInWindow().y as f32), + ), + delta, + touch_phase: phase, + modifiers: read_modifiers(native_event), + }) + }), + NSEventType::NSLeftMouseDragged + | NSEventType::NSRightMouseDragged + | NSEventType::NSOtherMouseDragged => { + let pressed_button = match native_event.buttonNumber() { + 0 => MouseButton::Left, + 1 => MouseButton::Right, + 2 => MouseButton::Middle, + 3 => MouseButton::Navigate(NavigationDirection::Back), + 4 => MouseButton::Navigate(NavigationDirection::Forward), + // Other mouse buttons aren't tracked currently + _ => return None, + }; + + window_height.map(|window_height| { + Self::MouseMove(MouseMoveEvent { + pressed_button: Some(pressed_button), + position: point( + px(native_event.locationInWindow().x as f32), + window_height - px(native_event.locationInWindow().y as f32), + ), + modifiers: read_modifiers(native_event), + }) + }) + } + NSEventType::NSMouseMoved => window_height.map(|window_height| { + Self::MouseMove(MouseMoveEvent { + position: point( + px(native_event.locationInWindow().x as f32), + window_height - px(native_event.locationInWindow().y as f32), + ), + pressed_button: None, + modifiers: read_modifiers(native_event), + }) + }), + NSEventType::NSMouseExited => window_height.map(|window_height| { + Self::MouseExited(MouseExitEvent { + position: point( + px(native_event.locationInWindow().x as f32), + window_height - px(native_event.locationInWindow().y as f32), + ), + + pressed_button: None, + modifiers: read_modifiers(native_event), + }) + }), + _ => None, + } + } + } +} + +unsafe fn parse_keystroke(native_event: id) -> Keystroke { + unsafe { + use cocoa::appkit::*; + + let mut characters = native_event + .charactersIgnoringModifiers() + .to_str() + .to_string(); + let mut key_char = None; + let first_char = characters.chars().next().map(|ch| ch as u16); + let modifiers = native_event.modifierFlags(); + + let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask); + let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); + let mut shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); + let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); + let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask) + && first_char + .is_none_or(|ch| !(NSUpArrowFunctionKey..=NSModeSwitchFunctionKey).contains(&ch)); + + #[allow(non_upper_case_globals)] + let key = match first_char { + Some(SPACE_KEY) => { + key_char = Some(" ".to_string()); + "space".to_string() + } + Some(TAB_KEY) => { + key_char = Some("\t".to_string()); + "tab".to_string() + } + Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => { + key_char = Some("\n".to_string()); + "enter".to_string() + } + Some(BACKSPACE_KEY) => "backspace".to_string(), + Some(ESCAPE_KEY) => "escape".to_string(), + Some(SHIFT_TAB_KEY) => "tab".to_string(), + Some(NSUpArrowFunctionKey) => "up".to_string(), + Some(NSDownArrowFunctionKey) => "down".to_string(), + Some(NSLeftArrowFunctionKey) => "left".to_string(), + Some(NSRightArrowFunctionKey) => "right".to_string(), + Some(NSPageUpFunctionKey) => "pageup".to_string(), + Some(NSPageDownFunctionKey) => "pagedown".to_string(), + Some(NSHomeFunctionKey) => "home".to_string(), + Some(NSEndFunctionKey) => "end".to_string(), + Some(NSDeleteFunctionKey) => "delete".to_string(), + // Observed Insert==NSHelpFunctionKey not NSInsertFunctionKey. + Some(NSHelpFunctionKey) => "insert".to_string(), + Some(NSF1FunctionKey) => "f1".to_string(), + Some(NSF2FunctionKey) => "f2".to_string(), + Some(NSF3FunctionKey) => "f3".to_string(), + Some(NSF4FunctionKey) => "f4".to_string(), + Some(NSF5FunctionKey) => "f5".to_string(), + Some(NSF6FunctionKey) => "f6".to_string(), + Some(NSF7FunctionKey) => "f7".to_string(), + Some(NSF8FunctionKey) => "f8".to_string(), + Some(NSF9FunctionKey) => "f9".to_string(), + Some(NSF10FunctionKey) => "f10".to_string(), + Some(NSF11FunctionKey) => "f11".to_string(), + Some(NSF12FunctionKey) => "f12".to_string(), + Some(NSF13FunctionKey) => "f13".to_string(), + Some(NSF14FunctionKey) => "f14".to_string(), + Some(NSF15FunctionKey) => "f15".to_string(), + Some(NSF16FunctionKey) => "f16".to_string(), + Some(NSF17FunctionKey) => "f17".to_string(), + Some(NSF18FunctionKey) => "f18".to_string(), + Some(NSF19FunctionKey) => "f19".to_string(), + Some(NSF20FunctionKey) => "f20".to_string(), + Some(NSF21FunctionKey) => "f21".to_string(), + Some(NSF22FunctionKey) => "f22".to_string(), + Some(NSF23FunctionKey) => "f23".to_string(), + Some(NSF24FunctionKey) => "f24".to_string(), + Some(NSF25FunctionKey) => "f25".to_string(), + Some(NSF26FunctionKey) => "f26".to_string(), + Some(NSF27FunctionKey) => "f27".to_string(), + Some(NSF28FunctionKey) => "f28".to_string(), + Some(NSF29FunctionKey) => "f29".to_string(), + Some(NSF30FunctionKey) => "f30".to_string(), + Some(NSF31FunctionKey) => "f31".to_string(), + Some(NSF32FunctionKey) => "f32".to_string(), + Some(NSF33FunctionKey) => "f33".to_string(), + Some(NSF34FunctionKey) => "f34".to_string(), + Some(NSF35FunctionKey) => "f35".to_string(), + _ => { + // Cases to test when modifying this: + // + // qwerty key | none | cmd | cmd-shift + // * Armenian s | ս | cmd-s | cmd-shift-s (layout is non-ASCII, so we use cmd layout) + // * Dvorak+QWERTY s | o | cmd-s | cmd-shift-s (layout switches on cmd) + // * Ukrainian+QWERTY s | с | cmd-s | cmd-shift-s (macOS reports cmd-s instead of cmd-S) + // * Czech 7 | ý | cmd-ý | cmd-7 (layout has shifted numbers) + // * Norwegian 7 | 7 | cmd-7 | cmd-/ (macOS reports cmd-shift-7 instead of cmd-/) + // * Russian 7 | 7 | cmd-7 | cmd-& (shift-7 is . but when cmd is down, should use cmd layout) + // * German QWERTZ ; | ö | cmd-ö | cmd-Ö (Zed's shift special case only applies to a-z) + // + let mut chars_ignoring_modifiers = + chars_for_modified_key(native_event.keyCode(), NO_MOD); + let mut chars_with_shift = + chars_for_modified_key(native_event.keyCode(), SHIFT_MOD); + let always_use_cmd_layout = always_use_command_layout(); + + // Handle Dvorak+QWERTY / Russian / Armenian + if command || always_use_cmd_layout { + let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), CMD_MOD); + let chars_with_both = + chars_for_modified_key(native_event.keyCode(), CMD_MOD | SHIFT_MOD); + + // We don't do this in the case that the shifted command key generates + // the same character as the unshifted command key (Norwegian, e.g.) + if chars_with_both != chars_with_cmd { + chars_with_shift = chars_with_both; + + // Handle edge-case where cmd-shift-s reports cmd-s instead of + // cmd-shift-s (Ukrainian, etc.) + } else if chars_with_cmd.to_ascii_uppercase() != chars_with_cmd { + chars_with_shift = chars_with_cmd.to_ascii_uppercase(); + } + chars_ignoring_modifiers = chars_with_cmd; + } + + if !control && !command && !function { + let mut mods = NO_MOD; + if shift { + mods |= SHIFT_MOD; + } + if alt { + mods |= OPTION_MOD; + } + + key_char = Some(chars_for_modified_key(native_event.keyCode(), mods)); + } + + if shift + && chars_ignoring_modifiers + .chars() + .all(|c| c.is_ascii_lowercase()) + { + chars_ignoring_modifiers + } else if shift { + shift = false; + chars_with_shift + } else { + chars_ignoring_modifiers + } + } + }; + + Keystroke { + modifiers: Modifiers { + control, + alt, + shift, + platform: command, + function, + }, + key, + key_char, + } + } +} + +fn always_use_command_layout() -> bool { + if chars_for_modified_key(0, NO_MOD).is_ascii() { + return false; + } + + chars_for_modified_key(0, CMD_MOD).is_ascii() +} + +const NO_MOD: u32 = 0; +const CMD_MOD: u32 = 1; +const SHIFT_MOD: u32 = 2; +const OPTION_MOD: u32 = 8; + +fn chars_for_modified_key(code: CGKeyCode, modifiers: u32) -> String { + // Values from: https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h#L126 + // shifted >> 8 for UCKeyTranslate + const CG_SPACE_KEY: u16 = 49; + // https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/Headers/UnicodeUtilities.h#L278 + #[allow(non_upper_case_globals)] + const kUCKeyActionDown: u16 = 0; + #[allow(non_upper_case_globals)] + const kUCKeyTranslateNoDeadKeysMask: u32 = 0; + + let keyboard_type = unsafe { LMGetKbdType() as u32 }; + const BUFFER_SIZE: usize = 4; + let mut dead_key_state = 0; + let mut buffer: [u16; BUFFER_SIZE] = [0; BUFFER_SIZE]; + let mut buffer_size: usize = 0; + + let keyboard = unsafe { TISCopyCurrentKeyboardLayoutInputSource() }; + if keyboard.is_null() { + return "".to_string(); + } + let layout_data = unsafe { + TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData as *const c_void) + as CFDataRef + }; + if layout_data.is_null() { + unsafe { + let _: () = msg_send![keyboard, release]; + } + return "".to_string(); + } + let keyboard_layout = unsafe { CFDataGetBytePtr(layout_data) }; + + unsafe { + UCKeyTranslate( + keyboard_layout as *const c_void, + code, + kUCKeyActionDown, + modifiers, + keyboard_type, + kUCKeyTranslateNoDeadKeysMask, + &mut dead_key_state, + BUFFER_SIZE, + &mut buffer_size as *mut usize, + &mut buffer as *mut u16, + ); + if dead_key_state != 0 { + UCKeyTranslate( + keyboard_layout as *const c_void, + CG_SPACE_KEY, + kUCKeyActionDown, + modifiers, + keyboard_type, + kUCKeyTranslateNoDeadKeysMask, + &mut dead_key_state, + BUFFER_SIZE, + &mut buffer_size as *mut usize, + &mut buffer as *mut u16, + ); + } + let _: () = msg_send![keyboard, release]; + } + String::from_utf16(&buffer[..buffer_size]).unwrap_or_default() +} diff --git a/third_party/gpui/src/platform/mac/keyboard.rs b/third_party/gpui/src/platform/mac/keyboard.rs new file mode 100644 index 0000000..1409731 --- /dev/null +++ b/third_party/gpui/src/platform/mac/keyboard.rs @@ -0,0 +1,1500 @@ +use collections::HashMap; +use std::ffi::{CStr, c_void}; + +use objc::{msg_send, runtime::Object, sel, sel_impl}; + +use crate::{KeybindingKeystroke, Keystroke, PlatformKeyboardLayout, PlatformKeyboardMapper}; + +use super::{ + TISCopyCurrentKeyboardLayoutInputSource, TISGetInputSourceProperty, kTISPropertyInputSourceID, + kTISPropertyLocalizedName, +}; + +pub(crate) struct MacKeyboardLayout { + id: String, + name: String, +} + +pub(crate) struct MacKeyboardMapper { + key_equivalents: Option>, +} + +impl PlatformKeyboardLayout for MacKeyboardLayout { + fn id(&self) -> &str { + &self.id + } + + fn name(&self) -> &str { + &self.name + } +} + +impl PlatformKeyboardMapper for MacKeyboardMapper { + fn map_key_equivalent( + &self, + mut keystroke: Keystroke, + use_key_equivalents: bool, + ) -> KeybindingKeystroke { + if use_key_equivalents && let Some(key_equivalents) = &self.key_equivalents { + if keystroke.key.chars().count() == 1 + && let Some(key) = key_equivalents.get(&keystroke.key.chars().next().unwrap()) + { + keystroke.key = key.to_string(); + } + } + KeybindingKeystroke::from_keystroke(keystroke) + } + + fn get_key_equivalents(&self) -> Option<&HashMap> { + self.key_equivalents.as_ref() + } +} + +impl MacKeyboardLayout { + pub(crate) fn new() -> Self { + unsafe { + let current_keyboard = TISCopyCurrentKeyboardLayoutInputSource(); + + let id: *mut Object = TISGetInputSourceProperty( + current_keyboard, + kTISPropertyInputSourceID as *const c_void, + ); + let id: *const std::os::raw::c_char = msg_send![id, UTF8String]; + let id = CStr::from_ptr(id).to_str().unwrap().to_string(); + + let name: *mut Object = TISGetInputSourceProperty( + current_keyboard, + kTISPropertyLocalizedName as *const c_void, + ); + let name: *const std::os::raw::c_char = msg_send![name, UTF8String]; + let name = CStr::from_ptr(name).to_str().unwrap().to_string(); + + Self { id, name } + } + } +} + +impl MacKeyboardMapper { + pub(crate) fn new(layout_id: &str) -> Self { + let key_equivalents = get_key_equivalents(layout_id); + + Self { key_equivalents } + } +} + +// On some keyboards (e.g. German QWERTZ) it is not possible to type the full ASCII range +// without using option. This means that some of our built in keyboard shortcuts do not work +// for those users. +// +// The way macOS solves this problem is to move shortcuts around so that they are all reachable, +// even if the mnemonic changes. https://developer.apple.com/documentation/swiftui/keyboardshortcut/localization-swift.struct +// +// For example, cmd-> is the "switch window" shortcut because the > key is right above tab. +// To ensure this doesn't cause problems for shortcuts defined for a QWERTY layout, apple moves +// any shortcuts defined as cmd-> to cmd-:. Coincidentally this s also the same keyboard position +// as cmd-> on a QWERTY layout. +// +// Another example is cmd-[ and cmd-], as they cannot be typed without option, those keys are remapped to cmd-ö +// and cmd-ä. These shortcuts are not in the same position as a QWERTY keyboard, because on a QWERTZ keyboard +// the + key is in the way; and shortcuts bound to cmd-+ are still typed as cmd-+ on either keyboard (though the +// specific key moves) +// +// As far as I can tell, there's no way to query the mappings Apple uses except by rendering a menu with every +// possible key combination, and inspecting the UI to see what it rendered. So that's what we did... +// +// These mappings were generated by running https://github.com/ConradIrwin/keyboard-inspector, tidying up the +// output to remove languages with no mappings and other oddities, and converting it to a less verbose representation with: +// jq -s 'map(to_entries | map({key: .key, value: [(.value | to_entries | map(.key) | join("")), (.value | to_entries | map(.value) | join(""))]}) | from_entries) | add' +// From there I used multi-cursor to produce this match statement. +fn get_key_equivalents(layout_id: &str) -> Option> { + let mappings: &[(char, char)] = match layout_id { + "com.apple.keylayout.ABC-AZERTY" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.ABC-QWERTZ" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Albanian" => &[ + ('"', '\''), + (':', 'Ç'), + (';', 'ç'), + ('<', ';'), + ('>', ':'), + ('@', '"'), + ('\'', '@'), + ('\\', 'ë'), + ('`', '<'), + ('|', 'Ë'), + ('~', '>'), + ], + "com.apple.keylayout.Austrian" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Azeri" => &[ + ('"', 'Ə'), + (',', 'ç'), + ('.', 'ş'), + ('/', '.'), + (':', 'I'), + (';', 'ı'), + ('<', 'Ç'), + ('>', 'Ş'), + ('?', ','), + ('W', 'Ü'), + ('[', 'ö'), + ('\'', 'ə'), + (']', 'ğ'), + ('w', 'ü'), + ('{', 'Ö'), + ('|', '/'), + ('}', 'Ğ'), + ], + "com.apple.keylayout.Belgian" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.Brazilian-ABNT2" => &[ + ('"', '`'), + ('/', 'ç'), + ('?', 'Ç'), + ('\'', '´'), + ('\\', '~'), + ('^', '¨'), + ('`', '\''), + ('|', '^'), + ('~', '"'), + ], + "com.apple.keylayout.Brazilian-Pro" => &[('^', 'ˆ'), ('~', '˜')], + "com.apple.keylayout.British" => &[('#', '£')], + "com.apple.keylayout.Canadian-CSA" => &[ + ('"', 'È'), + ('/', 'é'), + ('<', '\''), + ('>', '"'), + ('?', 'É'), + ('[', '^'), + ('\'', 'è'), + ('\\', 'à'), + (']', 'ç'), + ('`', 'ù'), + ('{', '¨'), + ('|', 'À'), + ('}', 'Ç'), + ('~', 'Ù'), + ], + "com.apple.keylayout.Croatian" => &[ + ('"', 'Ć'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Croatian-PC" => &[ + ('"', 'Ć'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Czech" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ě'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ř'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ů'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', ')'), + ('^', '6'), + ('`', '¨'), + ('{', 'Ú'), + ('}', '('), + ('~', '`'), + ], + "com.apple.keylayout.Czech-QWERTY" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ě'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ř'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ů'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', ')'), + ('^', '6'), + ('`', '¨'), + ('{', 'Ú'), + ('}', '('), + ('~', '`'), + ], + "com.apple.keylayout.Danish" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'æ'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ø'), + ('^', '&'), + ('`', '<'), + ('{', 'Æ'), + ('|', '*'), + ('}', 'Ø'), + ('~', '>'), + ], + "com.apple.keylayout.Faroese" => &[ + ('"', 'Ø'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Æ'), + (';', 'æ'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'å'), + ('\'', 'ø'), + ('\\', '\''), + (']', 'ð'), + ('^', '&'), + ('`', '<'), + ('{', 'Å'), + ('|', '*'), + ('}', 'Ð'), + ('~', '>'), + ], + "com.apple.keylayout.Finnish" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.FinnishExtended" => &[ + ('"', 'ˆ'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.FinnishSami-PC" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '@'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.French" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.French-PC" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('-', ')'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '-'), + ('7', 'è'), + ('8', '_'), + ('9', 'ç'), + (':', '§'), + (';', '!'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '*'), + (']', '$'), + ('^', '6'), + ('_', '°'), + ('`', '<'), + ('{', '¨'), + ('|', 'μ'), + ('}', '£'), + ('~', '>'), + ], + "com.apple.keylayout.French-numerical" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.German" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.German-DIN-2137" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Hawaiian" => &[('\'', 'ʻ')], + "com.apple.keylayout.Hungarian" => &[ + ('!', '\''), + ('"', 'Á'), + ('#', '+'), + ('$', '!'), + ('&', '='), + ('(', ')'), + (')', 'Ö'), + ('*', '('), + ('+', 'Ó'), + ('/', 'ü'), + ('0', 'ö'), + (':', 'É'), + (';', 'é'), + ('<', 'Ü'), + ('=', 'ó'), + ('>', ':'), + ('@', '"'), + ('[', 'ő'), + ('\'', 'á'), + ('\\', 'ű'), + (']', 'ú'), + ('^', '/'), + ('`', 'í'), + ('{', 'Ő'), + ('|', 'Ű'), + ('}', 'Ú'), + ('~', 'Í'), + ], + "com.apple.keylayout.Hungarian-QWERTY" => &[ + ('!', '\''), + ('"', 'Á'), + ('#', '+'), + ('$', '!'), + ('&', '='), + ('(', ')'), + (')', 'Ö'), + ('*', '('), + ('+', 'Ó'), + ('/', 'ü'), + ('0', 'ö'), + (':', 'É'), + (';', 'é'), + ('<', 'Ü'), + ('=', 'ó'), + ('>', ':'), + ('@', '"'), + ('[', 'ő'), + ('\'', 'á'), + ('\\', 'ű'), + (']', 'ú'), + ('^', '/'), + ('`', 'í'), + ('{', 'Ő'), + ('|', 'Ű'), + ('}', 'Ú'), + ('~', 'Í'), + ], + "com.apple.keylayout.Icelandic" => &[ + ('"', 'Ö'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'Ð'), + (';', 'ð'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'æ'), + ('\'', 'ö'), + ('\\', 'þ'), + (']', '´'), + ('^', '&'), + ('`', '<'), + ('{', 'Æ'), + ('|', 'Þ'), + ('}', '´'), + ('~', '>'), + ], + "com.apple.keylayout.Irish" => &[('#', '£')], + "com.apple.keylayout.IrishExtended" => &[('#', '£')], + "com.apple.keylayout.Italian" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + (',', ';'), + ('.', ':'), + ('/', ','), + ('0', 'é'), + ('1', '&'), + ('2', '"'), + ('3', '\''), + ('4', '('), + ('5', 'ç'), + ('6', 'è'), + ('7', ')'), + ('8', '£'), + ('9', 'à'), + (':', '!'), + (';', 'ò'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', 'ì'), + ('\'', 'ù'), + ('\\', '§'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '^'), + ('|', '°'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.Italian-Pro" => &[ + ('"', '^'), + ('#', '£'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'é'), + (';', 'è'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ò'), + ('\'', 'ì'), + ('\\', 'ù'), + (']', 'à'), + ('^', '&'), + ('`', '<'), + ('{', 'ç'), + ('|', '§'), + ('}', '°'), + ('~', '>'), + ], + "com.apple.keylayout.LatinAmerican" => &[ + ('"', '¨'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'Ñ'), + (';', 'ñ'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', '{'), + ('\'', '´'), + ('\\', '¿'), + (']', '}'), + ('^', '&'), + ('`', '<'), + ('{', '['), + ('|', '¡'), + ('}', ']'), + ('~', '>'), + ], + "com.apple.keylayout.Lithuanian" => &[ + ('!', 'Ą'), + ('#', 'Ę'), + ('$', 'Ė'), + ('%', 'Į'), + ('&', 'Ų'), + ('*', 'Ū'), + ('+', 'Ž'), + ('1', 'ą'), + ('2', 'č'), + ('3', 'ę'), + ('4', 'ė'), + ('5', 'į'), + ('6', 'š'), + ('7', 'ų'), + ('8', 'ū'), + ('=', 'ž'), + ('@', 'Č'), + ('^', 'Š'), + ], + "com.apple.keylayout.Maltese" => &[ + ('#', '£'), + ('[', 'ġ'), + (']', 'ħ'), + ('`', 'ż'), + ('{', 'Ġ'), + ('}', 'Ħ'), + ('~', 'Ż'), + ], + "com.apple.keylayout.NorthernSami" => &[ + ('"', 'Ŋ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('Q', 'Á'), + ('W', 'Š'), + ('X', 'Č'), + ('[', 'ø'), + ('\'', 'ŋ'), + ('\\', 'đ'), + (']', 'æ'), + ('^', '&'), + ('`', 'ž'), + ('q', 'á'), + ('w', 'š'), + ('x', 'č'), + ('{', 'Ø'), + ('|', 'Đ'), + ('}', 'Æ'), + ('~', 'Ž'), + ], + "com.apple.keylayout.Norwegian" => &[ + ('"', '^'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ø'), + ('\'', '¨'), + ('\\', '@'), + (']', 'æ'), + ('^', '&'), + ('`', '<'), + ('{', 'Ø'), + ('|', '*'), + ('}', 'Æ'), + ('~', '>'), + ], + "com.apple.keylayout.NorwegianExtended" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ø'), + ('\\', '@'), + (']', 'æ'), + ('`', '<'), + ('}', 'Æ'), + ('~', '>'), + ], + "com.apple.keylayout.NorwegianSami-PC" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ø'), + ('\'', '¨'), + ('\\', '@'), + (']', 'æ'), + ('^', '&'), + ('`', '<'), + ('{', 'Ø'), + ('|', '*'), + ('}', 'Æ'), + ('~', '>'), + ], + "com.apple.keylayout.Polish" => &[ + ('!', '§'), + ('"', 'ę'), + ('#', '!'), + ('$', '?'), + ('%', '+'), + ('&', ':'), + ('(', '/'), + (')', '"'), + ('*', '_'), + ('+', ']'), + (',', '.'), + ('.', ','), + ('/', 'ż'), + (':', 'Ł'), + (';', 'ł'), + ('<', 'ś'), + ('=', '['), + ('>', 'ń'), + ('?', 'Ż'), + ('@', '%'), + ('[', 'ó'), + ('\'', 'ą'), + ('\\', ';'), + (']', '('), + ('^', '='), + ('_', 'ć'), + ('`', '<'), + ('{', 'ź'), + ('|', '$'), + ('}', ')'), + ('~', '>'), + ], + "com.apple.keylayout.Portuguese" => &[ + ('"', '`'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'ª'), + (';', 'º'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ç'), + ('\'', '´'), + (']', '~'), + ('^', '&'), + ('`', '<'), + ('{', 'Ç'), + ('}', '^'), + ('~', '>'), + ], + "com.apple.keylayout.Sami-PC" => &[ + ('"', 'Ŋ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('Q', 'Á'), + ('W', 'Š'), + ('X', 'Č'), + ('[', 'ø'), + ('\'', 'ŋ'), + ('\\', 'đ'), + (']', 'æ'), + ('^', '&'), + ('`', 'ž'), + ('q', 'á'), + ('w', 'š'), + ('x', 'č'), + ('{', 'Ø'), + ('|', 'Đ'), + ('}', 'Æ'), + ('~', 'Ž'), + ], + "com.apple.keylayout.Serbian-Latin" => &[ + ('"', 'Ć'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Slovak" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ľ'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ť'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ô'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', 'ä'), + ('^', '6'), + ('`', 'ň'), + ('{', 'Ú'), + ('}', 'Ä'), + ('~', 'Ň'), + ], + "com.apple.keylayout.Slovak-QWERTY" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ľ'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ť'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ô'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', 'ä'), + ('^', '6'), + ('`', 'ň'), + ('{', 'Ú'), + ('}', 'Ä'), + ('~', 'Ň'), + ], + "com.apple.keylayout.Slovenian" => &[ + ('"', 'Ć'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Spanish" => &[ + ('!', '¡'), + ('"', '¨'), + ('.', 'ç'), + ('/', '.'), + (':', 'º'), + (';', '´'), + ('<', '¿'), + ('>', 'Ç'), + ('@', '!'), + ('[', 'ñ'), + ('\'', '`'), + ('\\', '\''), + (']', ';'), + ('^', '/'), + ('`', '<'), + ('{', 'Ñ'), + ('|', '"'), + ('}', ':'), + ('~', '>'), + ], + "com.apple.keylayout.Spanish-ISO" => &[ + ('"', '¨'), + ('#', '·'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('.', 'ç'), + ('/', '.'), + (':', 'º'), + (';', '´'), + ('<', '¿'), + ('>', 'Ç'), + ('@', '"'), + ('[', 'ñ'), + ('\'', '`'), + ('\\', '\''), + (']', ';'), + ('^', '&'), + ('`', '<'), + ('{', 'Ñ'), + ('|', '"'), + ('}', '`'), + ('~', '>'), + ], + "com.apple.keylayout.Swedish" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Swedish-Pro" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.SwedishSami-PC" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '@'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.SwissFrench" => &[ + ('!', '+'), + ('"', '`'), + ('#', '*'), + ('$', 'ç'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('+', '!'), + ('/', '\''), + (':', 'ü'), + (';', 'è'), + ('<', ';'), + ('=', '¨'), + ('>', ':'), + ('@', '"'), + ('[', 'é'), + ('\'', '^'), + ('\\', '$'), + (']', 'à'), + ('^', '&'), + ('`', '<'), + ('{', 'ö'), + ('|', '£'), + ('}', 'ä'), + ('~', '>'), + ], + "com.apple.keylayout.SwissGerman" => &[ + ('!', '+'), + ('"', '`'), + ('#', '*'), + ('$', 'ç'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('+', '!'), + ('/', '\''), + (':', 'è'), + (';', 'ü'), + ('<', ';'), + ('=', '¨'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '^'), + ('\\', '$'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'é'), + ('|', '£'), + ('}', 'à'), + ('~', '>'), + ], + "com.apple.keylayout.Turkish" => &[ + ('"', '-'), + ('#', '"'), + ('$', '\''), + ('%', '('), + ('&', ')'), + ('(', '%'), + (')', ':'), + ('*', '_'), + (',', 'ö'), + ('-', 'ş'), + ('.', 'ç'), + ('/', '.'), + (':', '$'), + ('<', 'Ö'), + ('>', 'Ç'), + ('@', '*'), + ('[', 'ğ'), + ('\'', ','), + ('\\', 'ü'), + (']', 'ı'), + ('^', '/'), + ('_', 'Ş'), + ('`', '<'), + ('{', 'Ğ'), + ('|', 'Ü'), + ('}', 'I'), + ('~', '>'), + ], + "com.apple.keylayout.Turkish-QWERTY-PC" => &[ + ('"', 'I'), + ('#', '^'), + ('$', '+'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('+', ':'), + (',', 'ö'), + ('.', 'ç'), + ('/', '*'), + (':', 'Ş'), + (';', 'ş'), + ('<', 'Ö'), + ('=', '.'), + ('>', 'Ç'), + ('@', '\''), + ('[', 'ğ'), + ('\'', 'ı'), + ('\\', ','), + (']', 'ü'), + ('^', '&'), + ('`', '<'), + ('{', 'Ğ'), + ('|', ';'), + ('}', 'Ü'), + ('~', '>'), + ], + "com.apple.keylayout.Turkish-Standard" => &[ + ('"', 'Ş'), + ('#', '^'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (',', '.'), + ('.', ','), + (':', 'Ç'), + (';', 'ç'), + ('<', ':'), + ('=', '*'), + ('>', ';'), + ('@', '"'), + ('[', 'ğ'), + ('\'', 'ş'), + ('\\', 'ü'), + (']', 'ı'), + ('^', '&'), + ('`', 'ö'), + ('{', 'Ğ'), + ('|', 'Ü'), + ('}', 'I'), + ('~', 'Ö'), + ], + "com.apple.keylayout.Turkmen" => &[ + ('C', 'Ç'), + ('Q', 'Ä'), + ('V', 'Ý'), + ('X', 'Ü'), + ('[', 'ň'), + ('\\', 'ş'), + (']', 'ö'), + ('^', '№'), + ('`', 'ž'), + ('c', 'ç'), + ('q', 'ä'), + ('v', 'ý'), + ('x', 'ü'), + ('{', 'Ň'), + ('|', 'Ş'), + ('}', 'Ö'), + ('~', 'Ž'), + ], + "com.apple.keylayout.USInternational-PC" => &[('^', 'ˆ'), ('~', '˜')], + "com.apple.keylayout.Welsh" => &[('#', '£')], + + _ => return None, + }; + + Some(HashMap::from_iter(mappings.iter().cloned())) +} diff --git a/third_party/gpui/src/platform/mac/metal_atlas.rs b/third_party/gpui/src/platform/mac/metal_atlas.rs new file mode 100644 index 0000000..8282530 --- /dev/null +++ b/third_party/gpui/src/platform/mac/metal_atlas.rs @@ -0,0 +1,281 @@ +use crate::{ + AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, DevicePixels, PlatformAtlas, + Point, Size, platform::AtlasTextureList, +}; +use anyhow::{Context as _, Result}; +use collections::FxHashMap; +use derive_more::{Deref, DerefMut}; +use etagere::BucketedAtlasAllocator; +use metal::Device; +use parking_lot::Mutex; +use std::borrow::Cow; + +pub(crate) struct MetalAtlas(Mutex); + +impl MetalAtlas { + pub(crate) fn new(device: Device) -> Self { + MetalAtlas(Mutex::new(MetalAtlasState { + device: AssertSend(device), + monochrome_textures: Default::default(), + polychrome_textures: Default::default(), + tiles_by_key: Default::default(), + })) + } + + pub(crate) fn metal_texture(&self, id: AtlasTextureId) -> metal::Texture { + self.0.lock().texture(id).metal_texture.clone() + } +} + +struct MetalAtlasState { + device: AssertSend, + monochrome_textures: AtlasTextureList, + polychrome_textures: AtlasTextureList, + tiles_by_key: FxHashMap, +} + +impl PlatformAtlas for MetalAtlas { + fn get_or_insert_with<'a>( + &self, + key: &AtlasKey, + build: &mut dyn FnMut() -> Result, Cow<'a, [u8]>)>>, + ) -> Result> { + let mut lock = self.0.lock(); + if let Some(tile) = lock.tiles_by_key.get(key) { + Ok(Some(tile.clone())) + } else { + let Some((size, bytes)) = build()? else { + return Ok(None); + }; + let tile = lock + .allocate(size, key.texture_kind()) + .context("failed to allocate")?; + let texture = lock.texture(tile.texture_id); + texture.upload(tile.bounds, &bytes); + lock.tiles_by_key.insert(key.clone(), tile.clone()); + Ok(Some(tile)) + } + } + + fn remove(&self, key: &AtlasKey) { + let mut lock = self.0.lock(); + let Some(id) = lock.tiles_by_key.get(key).map(|v| v.texture_id) else { + return; + }; + + let textures = match id.kind { + AtlasTextureKind::Monochrome => &mut lock.monochrome_textures, + AtlasTextureKind::Polychrome => &mut lock.polychrome_textures, + }; + + let Some(texture_slot) = textures + .textures + .iter_mut() + .find(|texture| texture.as_ref().is_some_and(|v| v.id == id)) + else { + return; + }; + + if let Some(mut texture) = texture_slot.take() { + texture.decrement_ref_count(); + + if texture.is_unreferenced() { + textures.free_list.push(id.index as usize); + lock.tiles_by_key.remove(key); + } else { + *texture_slot = Some(texture); + } + } + } +} + +impl MetalAtlasState { + fn allocate( + &mut self, + size: Size, + texture_kind: AtlasTextureKind, + ) -> Option { + { + let textures = match texture_kind { + AtlasTextureKind::Monochrome => &mut self.monochrome_textures, + AtlasTextureKind::Polychrome => &mut self.polychrome_textures, + }; + + if let Some(tile) = textures + .iter_mut() + .rev() + .find_map(|texture| texture.allocate(size)) + { + return Some(tile); + } + } + + let texture = self.push_texture(size, texture_kind); + texture.allocate(size) + } + + fn push_texture( + &mut self, + min_size: Size, + kind: AtlasTextureKind, + ) -> &mut MetalAtlasTexture { + const DEFAULT_ATLAS_SIZE: Size = Size { + width: DevicePixels(1024), + height: DevicePixels(1024), + }; + // Max texture size on all modern Apple GPUs. Anything bigger than that crashes in validateWithDevice. + const MAX_ATLAS_SIZE: Size = Size { + width: DevicePixels(16384), + height: DevicePixels(16384), + }; + let size = min_size.min(&MAX_ATLAS_SIZE).max(&DEFAULT_ATLAS_SIZE); + let texture_descriptor = metal::TextureDescriptor::new(); + texture_descriptor.set_width(size.width.into()); + texture_descriptor.set_height(size.height.into()); + let pixel_format; + let usage; + match kind { + AtlasTextureKind::Monochrome => { + pixel_format = metal::MTLPixelFormat::A8Unorm; + usage = metal::MTLTextureUsage::ShaderRead; + } + AtlasTextureKind::Polychrome => { + pixel_format = metal::MTLPixelFormat::BGRA8Unorm; + usage = metal::MTLTextureUsage::ShaderRead; + } + } + texture_descriptor.set_pixel_format(pixel_format); + texture_descriptor.set_usage(usage); + let metal_texture = self.device.new_texture(&texture_descriptor); + + let texture_list = match kind { + AtlasTextureKind::Monochrome => &mut self.monochrome_textures, + AtlasTextureKind::Polychrome => &mut self.polychrome_textures, + }; + + let index = texture_list.free_list.pop(); + + let atlas_texture = MetalAtlasTexture { + id: AtlasTextureId { + index: index.unwrap_or(texture_list.textures.len()) as u32, + kind, + }, + allocator: etagere::BucketedAtlasAllocator::new(size.into()), + metal_texture: AssertSend(metal_texture), + live_atlas_keys: 0, + }; + + if let Some(ix) = index { + texture_list.textures[ix] = Some(atlas_texture); + texture_list.textures.get_mut(ix) + } else { + texture_list.textures.push(Some(atlas_texture)); + texture_list.textures.last_mut() + } + .unwrap() + .as_mut() + .unwrap() + } + + fn texture(&self, id: AtlasTextureId) -> &MetalAtlasTexture { + let textures = match id.kind { + crate::AtlasTextureKind::Monochrome => &self.monochrome_textures, + crate::AtlasTextureKind::Polychrome => &self.polychrome_textures, + }; + textures[id.index as usize].as_ref().unwrap() + } +} + +struct MetalAtlasTexture { + id: AtlasTextureId, + allocator: BucketedAtlasAllocator, + metal_texture: AssertSend, + live_atlas_keys: u32, +} + +impl MetalAtlasTexture { + fn allocate(&mut self, size: Size) -> Option { + let allocation = self.allocator.allocate(size.into())?; + let tile = AtlasTile { + texture_id: self.id, + tile_id: allocation.id.into(), + bounds: Bounds { + origin: allocation.rectangle.min.into(), + size, + }, + padding: 0, + }; + self.live_atlas_keys += 1; + Some(tile) + } + + fn upload(&self, bounds: Bounds, bytes: &[u8]) { + let region = metal::MTLRegion::new_2d( + bounds.origin.x.into(), + bounds.origin.y.into(), + bounds.size.width.into(), + bounds.size.height.into(), + ); + self.metal_texture.replace_region( + region, + 0, + bytes.as_ptr() as *const _, + bounds.size.width.to_bytes(self.bytes_per_pixel()) as u64, + ); + } + + fn bytes_per_pixel(&self) -> u8 { + use metal::MTLPixelFormat::*; + match self.metal_texture.pixel_format() { + A8Unorm | R8Unorm => 1, + RGBA8Unorm | BGRA8Unorm => 4, + _ => unimplemented!(), + } + } + + fn decrement_ref_count(&mut self) { + self.live_atlas_keys -= 1; + } + + fn is_unreferenced(&mut self) -> bool { + self.live_atlas_keys == 0 + } +} + +impl From> for etagere::Size { + fn from(size: Size) -> Self { + etagere::Size::new(size.width.into(), size.height.into()) + } +} + +impl From for Point { + fn from(value: etagere::Point) -> Self { + Point { + x: DevicePixels::from(value.x), + y: DevicePixels::from(value.y), + } + } +} + +impl From for Size { + fn from(size: etagere::Size) -> Self { + Size { + width: DevicePixels::from(size.width), + height: DevicePixels::from(size.height), + } + } +} + +impl From for Bounds { + fn from(rectangle: etagere::Rectangle) -> Self { + Bounds { + origin: rectangle.min.into(), + size: rectangle.size().into(), + } + } +} + +#[derive(Deref, DerefMut)] +struct AssertSend(T); + +unsafe impl Send for AssertSend {} diff --git a/third_party/gpui/src/platform/mac/metal_renderer.rs b/third_party/gpui/src/platform/mac/metal_renderer.rs new file mode 100644 index 0000000..edf822e --- /dev/null +++ b/third_party/gpui/src/platform/mac/metal_renderer.rs @@ -0,0 +1,1390 @@ +use super::metal_atlas::MetalAtlas; +use crate::{ + AtlasTextureId, Background, Bounds, ContentMask, DevicePixels, MonochromeSprite, PaintSurface, + Path, Point, PolychromeSprite, PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size, + Surface, Underline, point, size, +}; +use anyhow::Result; +use block::ConcreteBlock; +use cocoa::{ + base::{NO, YES}, + foundation::{NSSize, NSUInteger}, + quartzcore::AutoresizingMask, +}; + +use core_foundation::base::TCFType; +use core_video::{ + metal_texture::CVMetalTextureGetTexture, metal_texture_cache::CVMetalTextureCache, + pixel_buffer::{kCVPixelFormatType_32BGRA, kCVPixelFormatType_420YpCbCr8BiPlanarFullRange}, +}; +use foreign_types::{ForeignType, ForeignTypeRef}; +use metal::{ + CAMetalLayer, CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange, + RenderPassColorAttachmentDescriptorRef, +}; +use objc::{self, msg_send, sel, sel_impl}; +use parking_lot::Mutex; + +use std::{cell::Cell, ffi::c_void, mem, ptr, sync::Arc}; + +// Exported to metal +pub(crate) type PointF = crate::Point; + +#[cfg(not(feature = "runtime_shaders"))] +const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib")); +#[cfg(feature = "runtime_shaders")] +const SHADERS_SOURCE_FILE: &str = include_str!(concat!(env!("OUT_DIR"), "/stitched_shaders.metal")); +// Use 4x MSAA, all devices support it. +// https://developer.apple.com/documentation/metal/mtldevice/1433355-supportstexturesamplecount +const PATH_SAMPLE_COUNT: u32 = 4; + +pub type Context = Arc>; +pub type Renderer = MetalRenderer; + +pub unsafe fn new_renderer( + context: self::Context, + _native_window: *mut c_void, + _native_view: *mut c_void, + _bounds: crate::Size, + _transparent: bool, +) -> Renderer { + MetalRenderer::new(context) +} + +pub(crate) struct InstanceBufferPool { + buffer_size: usize, + buffers: Vec, +} + +impl Default for InstanceBufferPool { + fn default() -> Self { + Self { + buffer_size: 2 * 1024 * 1024, + buffers: Vec::new(), + } + } +} + +pub(crate) struct InstanceBuffer { + metal_buffer: metal::Buffer, + size: usize, +} + +impl InstanceBufferPool { + pub(crate) fn reset(&mut self, buffer_size: usize) { + self.buffer_size = buffer_size; + self.buffers.clear(); + } + + pub(crate) fn acquire(&mut self, device: &metal::Device) -> InstanceBuffer { + let buffer = self.buffers.pop().unwrap_or_else(|| { + device.new_buffer( + self.buffer_size as u64, + MTLResourceOptions::StorageModeManaged, + ) + }); + InstanceBuffer { + metal_buffer: buffer, + size: self.buffer_size, + } + } + + pub(crate) fn release(&mut self, buffer: InstanceBuffer) { + if buffer.size == self.buffer_size { + self.buffers.push(buffer.metal_buffer) + } + } +} + +pub(crate) struct MetalRenderer { + device: metal::Device, + layer: metal::MetalLayer, + presents_with_transaction: bool, + command_queue: CommandQueue, + paths_rasterization_pipeline_state: metal::RenderPipelineState, + path_sprites_pipeline_state: metal::RenderPipelineState, + shadows_pipeline_state: metal::RenderPipelineState, + quads_pipeline_state: metal::RenderPipelineState, + underlines_pipeline_state: metal::RenderPipelineState, + monochrome_sprites_pipeline_state: metal::RenderPipelineState, + polychrome_sprites_pipeline_state: metal::RenderPipelineState, + surfaces_pipeline_state: metal::RenderPipelineState, + surface_bgra_pipeline_state: metal::RenderPipelineState, + unit_vertices: metal::Buffer, + #[allow(clippy::arc_with_non_send_sync)] + instance_buffer_pool: Arc>, + sprite_atlas: Arc, + core_video_texture_cache: core_video::metal_texture_cache::CVMetalTextureCache, + path_intermediate_texture: Option, + path_intermediate_msaa_texture: Option, + path_sample_count: u32, +} + +#[repr(C)] +pub struct PathRasterizationVertex { + pub xy_position: Point, + pub st_position: Point, + pub color: Background, + pub bounds: Bounds, +} + +impl MetalRenderer { + pub fn new(instance_buffer_pool: Arc>) -> Self { + // Prefer low‐power integrated GPUs on Intel Mac. On Apple + // Silicon, there is only ever one GPU, so this is equivalent to + // `metal::Device::system_default()`. + let mut devices = metal::Device::all(); + devices.sort_by_key(|device| (device.is_removable(), device.is_low_power())); + let Some(device) = devices.pop() else { + log::error!("unable to access a compatible graphics device"); + std::process::exit(1); + }; + + let layer = metal::MetalLayer::new(); + layer.set_device(&device); + layer.set_pixel_format(MTLPixelFormat::BGRA8Unorm); + layer.set_opaque(false); + layer.set_maximum_drawable_count(3); + unsafe { + let _: () = msg_send![&*layer, setAllowsNextDrawableTimeout: NO]; + let _: () = msg_send![&*layer, setNeedsDisplayOnBoundsChange: YES]; + let _: () = msg_send![ + &*layer, + setAutoresizingMask: AutoresizingMask::WIDTH_SIZABLE + | AutoresizingMask::HEIGHT_SIZABLE + ]; + } + #[cfg(feature = "runtime_shaders")] + let library = device + .new_library_with_source(&SHADERS_SOURCE_FILE, &metal::CompileOptions::new()) + .expect("error building metal library"); + #[cfg(not(feature = "runtime_shaders"))] + let library = device + .new_library_with_data(SHADERS_METALLIB) + .expect("error building metal library"); + + fn to_float2_bits(point: PointF) -> u64 { + let mut output = point.y.to_bits() as u64; + output <<= 32; + output |= point.x.to_bits() as u64; + output + } + + let unit_vertices = [ + to_float2_bits(point(0., 0.)), + to_float2_bits(point(1., 0.)), + to_float2_bits(point(0., 1.)), + to_float2_bits(point(0., 1.)), + to_float2_bits(point(1., 0.)), + to_float2_bits(point(1., 1.)), + ]; + let unit_vertices = device.new_buffer_with_data( + unit_vertices.as_ptr() as *const c_void, + mem::size_of_val(&unit_vertices) as u64, + MTLResourceOptions::StorageModeManaged, + ); + + let paths_rasterization_pipeline_state = build_path_rasterization_pipeline_state( + &device, + &library, + "paths_rasterization", + "path_rasterization_vertex", + "path_rasterization_fragment", + MTLPixelFormat::BGRA8Unorm, + PATH_SAMPLE_COUNT, + ); + let path_sprites_pipeline_state = build_path_sprite_pipeline_state( + &device, + &library, + "path_sprites", + "path_sprite_vertex", + "path_sprite_fragment", + MTLPixelFormat::BGRA8Unorm, + ); + let shadows_pipeline_state = build_pipeline_state( + &device, + &library, + "shadows", + "shadow_vertex", + "shadow_fragment", + MTLPixelFormat::BGRA8Unorm, + ); + let quads_pipeline_state = build_pipeline_state( + &device, + &library, + "quads", + "quad_vertex", + "quad_fragment", + MTLPixelFormat::BGRA8Unorm, + ); + let underlines_pipeline_state = build_pipeline_state( + &device, + &library, + "underlines", + "underline_vertex", + "underline_fragment", + MTLPixelFormat::BGRA8Unorm, + ); + let monochrome_sprites_pipeline_state = build_pipeline_state( + &device, + &library, + "monochrome_sprites", + "monochrome_sprite_vertex", + "monochrome_sprite_fragment", + MTLPixelFormat::BGRA8Unorm, + ); + let polychrome_sprites_pipeline_state = build_pipeline_state( + &device, + &library, + "polychrome_sprites", + "polychrome_sprite_vertex", + "polychrome_sprite_fragment", + MTLPixelFormat::BGRA8Unorm, + ); + let surfaces_pipeline_state = build_pipeline_state( + &device, + &library, + "surfaces", + "surface_vertex", + "surface_fragment", + MTLPixelFormat::BGRA8Unorm, + ); + // Servo publishes hardware offscreen frames as BGRA IOSurfaces. + // Keep the original NV12 path for video surfaces and use this + // pipeline for browser frames. + let surface_bgra_pipeline_state = build_pipeline_state( + &device, + &library, + "surface_bgra", + "surface_vertex", + "surface_bgra_fragment", + MTLPixelFormat::BGRA8Unorm, + ); + + let command_queue = device.new_command_queue(); + let sprite_atlas = Arc::new(MetalAtlas::new(device.clone())); + let core_video_texture_cache = + CVMetalTextureCache::new(None, device.clone(), None).unwrap(); + + Self { + device, + layer, + presents_with_transaction: false, + command_queue, + paths_rasterization_pipeline_state, + path_sprites_pipeline_state, + shadows_pipeline_state, + quads_pipeline_state, + underlines_pipeline_state, + monochrome_sprites_pipeline_state, + polychrome_sprites_pipeline_state, + surfaces_pipeline_state, + surface_bgra_pipeline_state, + unit_vertices, + instance_buffer_pool, + sprite_atlas, + core_video_texture_cache, + path_intermediate_texture: None, + path_intermediate_msaa_texture: None, + path_sample_count: PATH_SAMPLE_COUNT, + } + } + + pub fn layer(&self) -> &metal::MetalLayerRef { + &self.layer + } + + pub fn layer_ptr(&self) -> *mut CAMetalLayer { + self.layer.as_ptr() + } + + pub fn sprite_atlas(&self) -> &Arc { + &self.sprite_atlas + } + + pub fn set_presents_with_transaction(&mut self, presents_with_transaction: bool) { + self.presents_with_transaction = presents_with_transaction; + self.layer + .set_presents_with_transaction(presents_with_transaction); + } + + pub fn update_drawable_size(&mut self, size: Size) { + let size = NSSize { + width: size.width.0 as f64, + height: size.height.0 as f64, + }; + unsafe { + let _: () = msg_send![ + self.layer(), + setDrawableSize: size + ]; + } + let device_pixels_size = Size { + width: DevicePixels(size.width as i32), + height: DevicePixels(size.height as i32), + }; + self.update_path_intermediate_textures(device_pixels_size); + } + + fn update_path_intermediate_textures(&mut self, size: Size) { + // We are uncertain when this happens, but sometimes size can be 0 here. Most likely before + // the layout pass on window creation. Zero-sized texture creation causes SIGABRT. + // https://github.com/zed-industries/zed/issues/36229 + if size.width.0 <= 0 || size.height.0 <= 0 { + self.path_intermediate_texture = None; + self.path_intermediate_msaa_texture = None; + return; + } + + let texture_descriptor = metal::TextureDescriptor::new(); + texture_descriptor.set_width(size.width.0 as u64); + texture_descriptor.set_height(size.height.0 as u64); + texture_descriptor.set_pixel_format(metal::MTLPixelFormat::BGRA8Unorm); + texture_descriptor + .set_usage(metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead); + self.path_intermediate_texture = Some(self.device.new_texture(&texture_descriptor)); + + if self.path_sample_count > 1 { + let mut msaa_descriptor = texture_descriptor; + msaa_descriptor.set_texture_type(metal::MTLTextureType::D2Multisample); + msaa_descriptor.set_storage_mode(metal::MTLStorageMode::Private); + msaa_descriptor.set_sample_count(self.path_sample_count as _); + self.path_intermediate_msaa_texture = Some(self.device.new_texture(&msaa_descriptor)); + } else { + self.path_intermediate_msaa_texture = None; + } + } + + pub fn update_transparency(&self, _transparent: bool) { + // todo(mac)? + } + + pub fn destroy(&self) { + // nothing to do + } + + pub fn draw(&mut self, scene: &Scene) { + let layer = self.layer.clone(); + let viewport_size = layer.drawable_size(); + let viewport_size: Size = size( + (viewport_size.width.ceil() as i32).into(), + (viewport_size.height.ceil() as i32).into(), + ); + let drawable = if let Some(drawable) = layer.next_drawable() { + drawable + } else { + log::error!( + "failed to retrieve next drawable, drawable size: {:?}", + viewport_size + ); + return; + }; + + loop { + let mut instance_buffer = self.instance_buffer_pool.lock().acquire(&self.device); + + let command_buffer = + self.draw_primitives(scene, &mut instance_buffer, drawable, viewport_size); + + match command_buffer { + Ok(command_buffer) => { + let instance_buffer_pool = self.instance_buffer_pool.clone(); + let instance_buffer = Cell::new(Some(instance_buffer)); + let block = ConcreteBlock::new(move |_| { + if let Some(instance_buffer) = instance_buffer.take() { + instance_buffer_pool.lock().release(instance_buffer); + } + }); + let block = block.copy(); + command_buffer.add_completed_handler(&block); + + if self.presents_with_transaction { + command_buffer.commit(); + command_buffer.wait_until_scheduled(); + drawable.present(); + } else { + command_buffer.present_drawable(drawable); + command_buffer.commit(); + } + return; + } + Err(err) => { + log::error!( + "failed to render: {}. retrying with larger instance buffer size", + err + ); + let mut instance_buffer_pool = self.instance_buffer_pool.lock(); + let buffer_size = instance_buffer_pool.buffer_size; + if buffer_size >= 256 * 1024 * 1024 { + log::error!("instance buffer size grew too large: {}", buffer_size); + break; + } + instance_buffer_pool.reset(buffer_size * 2); + log::info!( + "increased instance buffer size to {}", + instance_buffer_pool.buffer_size + ); + } + } + } + } + + fn draw_primitives( + &mut self, + scene: &Scene, + instance_buffer: &mut InstanceBuffer, + drawable: &metal::MetalDrawableRef, + viewport_size: Size, + ) -> Result { + let command_queue = self.command_queue.clone(); + let command_buffer = command_queue.new_command_buffer(); + let alpha = if self.layer.is_opaque() { 1. } else { 0. }; + let mut instance_offset = 0; + + let mut command_encoder = new_command_encoder( + command_buffer, + drawable, + viewport_size, + |color_attachment| { + color_attachment.set_load_action(metal::MTLLoadAction::Clear); + color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., alpha)); + }, + ); + + for batch in scene.batches() { + let ok = match batch { + PrimitiveBatch::Shadows(shadows) => self.draw_shadows( + shadows, + instance_buffer, + &mut instance_offset, + viewport_size, + command_encoder, + ), + PrimitiveBatch::Quads(quads) => self.draw_quads( + quads, + instance_buffer, + &mut instance_offset, + viewport_size, + command_encoder, + ), + PrimitiveBatch::Paths(paths) => { + command_encoder.end_encoding(); + + let did_draw = self.draw_paths_to_intermediate( + paths, + instance_buffer, + &mut instance_offset, + viewport_size, + command_buffer, + ); + + command_encoder = new_command_encoder( + command_buffer, + drawable, + viewport_size, + |color_attachment| { + color_attachment.set_load_action(metal::MTLLoadAction::Load); + }, + ); + + if did_draw { + self.draw_paths_from_intermediate( + paths, + instance_buffer, + &mut instance_offset, + viewport_size, + command_encoder, + ) + } else { + false + } + } + PrimitiveBatch::Underlines(underlines) => self.draw_underlines( + underlines, + instance_buffer, + &mut instance_offset, + viewport_size, + command_encoder, + ), + PrimitiveBatch::MonochromeSprites { + texture_id, + sprites, + } => self.draw_monochrome_sprites( + texture_id, + sprites, + instance_buffer, + &mut instance_offset, + viewport_size, + command_encoder, + ), + PrimitiveBatch::PolychromeSprites { + texture_id, + sprites, + } => self.draw_polychrome_sprites( + texture_id, + sprites, + instance_buffer, + &mut instance_offset, + viewport_size, + command_encoder, + ), + PrimitiveBatch::Surfaces(surfaces) => self.draw_surfaces( + surfaces, + instance_buffer, + &mut instance_offset, + viewport_size, + command_encoder, + ), + }; + if !ok { + command_encoder.end_encoding(); + anyhow::bail!( + "scene too large: {} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces", + scene.paths.len(), + scene.shadows.len(), + scene.quads.len(), + scene.underlines.len(), + scene.monochrome_sprites.len(), + scene.polychrome_sprites.len(), + scene.surfaces.len(), + ); + } + } + + command_encoder.end_encoding(); + + instance_buffer.metal_buffer.did_modify_range(NSRange { + location: 0, + length: instance_offset as NSUInteger, + }); + Ok(command_buffer.to_owned()) + } + + fn draw_paths_to_intermediate( + &self, + paths: &[Path], + instance_buffer: &mut InstanceBuffer, + instance_offset: &mut usize, + viewport_size: Size, + command_buffer: &metal::CommandBufferRef, + ) -> bool { + if paths.is_empty() { + return true; + } + let Some(intermediate_texture) = &self.path_intermediate_texture else { + return false; + }; + + let render_pass_descriptor = metal::RenderPassDescriptor::new(); + let color_attachment = render_pass_descriptor + .color_attachments() + .object_at(0) + .unwrap(); + color_attachment.set_load_action(metal::MTLLoadAction::Clear); + color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., 0.)); + + if let Some(msaa_texture) = &self.path_intermediate_msaa_texture { + color_attachment.set_texture(Some(msaa_texture)); + color_attachment.set_resolve_texture(Some(intermediate_texture)); + color_attachment.set_store_action(metal::MTLStoreAction::MultisampleResolve); + } else { + color_attachment.set_texture(Some(intermediate_texture)); + color_attachment.set_store_action(metal::MTLStoreAction::Store); + } + + let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor); + command_encoder.set_render_pipeline_state(&self.paths_rasterization_pipeline_state); + + align_offset(instance_offset); + let mut vertices = Vec::new(); + for path in paths { + vertices.extend(path.vertices.iter().map(|v| PathRasterizationVertex { + xy_position: v.xy_position, + st_position: v.st_position, + color: path.color, + bounds: path.bounds.intersect(&path.content_mask.bounds), + })); + } + let vertices_bytes_len = mem::size_of_val(vertices.as_slice()); + let next_offset = *instance_offset + vertices_bytes_len; + if next_offset > instance_buffer.size { + command_encoder.end_encoding(); + return false; + } + command_encoder.set_vertex_buffer( + PathRasterizationInputIndex::Vertices as u64, + Some(&instance_buffer.metal_buffer), + *instance_offset as u64, + ); + command_encoder.set_vertex_bytes( + PathRasterizationInputIndex::ViewportSize as u64, + mem::size_of_val(&viewport_size) as u64, + &viewport_size as *const Size as *const _, + ); + command_encoder.set_fragment_buffer( + PathRasterizationInputIndex::Vertices as u64, + Some(&instance_buffer.metal_buffer), + *instance_offset as u64, + ); + let buffer_contents = + unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) }; + unsafe { + ptr::copy_nonoverlapping( + vertices.as_ptr() as *const u8, + buffer_contents, + vertices_bytes_len, + ); + } + command_encoder.draw_primitives( + metal::MTLPrimitiveType::Triangle, + 0, + vertices.len() as u64, + ); + *instance_offset = next_offset; + + command_encoder.end_encoding(); + true + } + + fn draw_shadows( + &self, + shadows: &[Shadow], + instance_buffer: &mut InstanceBuffer, + instance_offset: &mut usize, + viewport_size: Size, + command_encoder: &metal::RenderCommandEncoderRef, + ) -> bool { + if shadows.is_empty() { + return true; + } + align_offset(instance_offset); + + command_encoder.set_render_pipeline_state(&self.shadows_pipeline_state); + command_encoder.set_vertex_buffer( + ShadowInputIndex::Vertices as u64, + Some(&self.unit_vertices), + 0, + ); + command_encoder.set_vertex_buffer( + ShadowInputIndex::Shadows as u64, + Some(&instance_buffer.metal_buffer), + *instance_offset as u64, + ); + command_encoder.set_fragment_buffer( + ShadowInputIndex::Shadows as u64, + Some(&instance_buffer.metal_buffer), + *instance_offset as u64, + ); + + command_encoder.set_vertex_bytes( + ShadowInputIndex::ViewportSize as u64, + mem::size_of_val(&viewport_size) as u64, + &viewport_size as *const Size as *const _, + ); + + let shadow_bytes_len = mem::size_of_val(shadows); + let buffer_contents = + unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) }; + + let next_offset = *instance_offset + shadow_bytes_len; + if next_offset > instance_buffer.size { + return false; + } + + unsafe { + ptr::copy_nonoverlapping( + shadows.as_ptr() as *const u8, + buffer_contents, + shadow_bytes_len, + ); + } + + command_encoder.draw_primitives_instanced( + metal::MTLPrimitiveType::Triangle, + 0, + 6, + shadows.len() as u64, + ); + *instance_offset = next_offset; + true + } + + fn draw_quads( + &self, + quads: &[Quad], + instance_buffer: &mut InstanceBuffer, + instance_offset: &mut usize, + viewport_size: Size, + command_encoder: &metal::RenderCommandEncoderRef, + ) -> bool { + if quads.is_empty() { + return true; + } + align_offset(instance_offset); + + command_encoder.set_render_pipeline_state(&self.quads_pipeline_state); + command_encoder.set_vertex_buffer( + QuadInputIndex::Vertices as u64, + Some(&self.unit_vertices), + 0, + ); + command_encoder.set_vertex_buffer( + QuadInputIndex::Quads as u64, + Some(&instance_buffer.metal_buffer), + *instance_offset as u64, + ); + command_encoder.set_fragment_buffer( + QuadInputIndex::Quads as u64, + Some(&instance_buffer.metal_buffer), + *instance_offset as u64, + ); + + command_encoder.set_vertex_bytes( + QuadInputIndex::ViewportSize as u64, + mem::size_of_val(&viewport_size) as u64, + &viewport_size as *const Size as *const _, + ); + + let quad_bytes_len = mem::size_of_val(quads); + let buffer_contents = + unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) }; + + let next_offset = *instance_offset + quad_bytes_len; + if next_offset > instance_buffer.size { + return false; + } + + unsafe { + ptr::copy_nonoverlapping(quads.as_ptr() as *const u8, buffer_contents, quad_bytes_len); + } + + command_encoder.draw_primitives_instanced( + metal::MTLPrimitiveType::Triangle, + 0, + 6, + quads.len() as u64, + ); + *instance_offset = next_offset; + true + } + + fn draw_paths_from_intermediate( + &self, + paths: &[Path], + instance_buffer: &mut InstanceBuffer, + instance_offset: &mut usize, + viewport_size: Size, + command_encoder: &metal::RenderCommandEncoderRef, + ) -> bool { + let Some(first_path) = paths.first() else { + return true; + }; + + let Some(ref intermediate_texture) = self.path_intermediate_texture else { + return false; + }; + + command_encoder.set_render_pipeline_state(&self.path_sprites_pipeline_state); + command_encoder.set_vertex_buffer( + SpriteInputIndex::Vertices as u64, + Some(&self.unit_vertices), + 0, + ); + command_encoder.set_vertex_bytes( + SpriteInputIndex::ViewportSize as u64, + mem::size_of_val(&viewport_size) as u64, + &viewport_size as *const Size as *const _, + ); + + command_encoder.set_fragment_texture( + SpriteInputIndex::AtlasTexture as u64, + Some(intermediate_texture), + ); + + // When copying paths from the intermediate texture to the drawable, + // each pixel must only be copied once, in case of transparent paths. + // + // If all paths have the same draw order, then their bounds are all + // disjoint, so we can copy each path's bounds individually. If this + // batch combines different draw orders, we perform a single copy + // for a minimal spanning rect. + let sprites; + if paths.last().unwrap().order == first_path.order { + sprites = paths + .iter() + .map(|path| PathSprite { + bounds: path.clipped_bounds(), + }) + .collect(); + } else { + let mut bounds = first_path.clipped_bounds(); + for path in paths.iter().skip(1) { + bounds = bounds.union(&path.clipped_bounds()); + } + sprites = vec![PathSprite { bounds }]; + } + + align_offset(instance_offset); + let sprite_bytes_len = mem::size_of_val(sprites.as_slice()); + let next_offset = *instance_offset + sprite_bytes_len; + if next_offset > instance_buffer.size { + return false; + } + + command_encoder.set_vertex_buffer( + SpriteInputIndex::Sprites as u64, + Some(&instance_buffer.metal_buffer), + *instance_offset as u64, + ); + + let buffer_contents = + unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) }; + unsafe { + ptr::copy_nonoverlapping( + sprites.as_ptr() as *const u8, + buffer_contents, + sprite_bytes_len, + ); + } + + command_encoder.draw_primitives_instanced( + metal::MTLPrimitiveType::Triangle, + 0, + 6, + sprites.len() as u64, + ); + *instance_offset = next_offset; + + true + } + + fn draw_underlines( + &self, + underlines: &[Underline], + instance_buffer: &mut InstanceBuffer, + instance_offset: &mut usize, + viewport_size: Size, + command_encoder: &metal::RenderCommandEncoderRef, + ) -> bool { + if underlines.is_empty() { + return true; + } + align_offset(instance_offset); + + command_encoder.set_render_pipeline_state(&self.underlines_pipeline_state); + command_encoder.set_vertex_buffer( + UnderlineInputIndex::Vertices as u64, + Some(&self.unit_vertices), + 0, + ); + command_encoder.set_vertex_buffer( + UnderlineInputIndex::Underlines as u64, + Some(&instance_buffer.metal_buffer), + *instance_offset as u64, + ); + command_encoder.set_fragment_buffer( + UnderlineInputIndex::Underlines as u64, + Some(&instance_buffer.metal_buffer), + *instance_offset as u64, + ); + + command_encoder.set_vertex_bytes( + UnderlineInputIndex::ViewportSize as u64, + mem::size_of_val(&viewport_size) as u64, + &viewport_size as *const Size as *const _, + ); + + let underline_bytes_len = mem::size_of_val(underlines); + let buffer_contents = + unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) }; + + let next_offset = *instance_offset + underline_bytes_len; + if next_offset > instance_buffer.size { + return false; + } + + unsafe { + ptr::copy_nonoverlapping( + underlines.as_ptr() as *const u8, + buffer_contents, + underline_bytes_len, + ); + } + + command_encoder.draw_primitives_instanced( + metal::MTLPrimitiveType::Triangle, + 0, + 6, + underlines.len() as u64, + ); + *instance_offset = next_offset; + true + } + + fn draw_monochrome_sprites( + &self, + texture_id: AtlasTextureId, + sprites: &[MonochromeSprite], + instance_buffer: &mut InstanceBuffer, + instance_offset: &mut usize, + viewport_size: Size, + command_encoder: &metal::RenderCommandEncoderRef, + ) -> bool { + if sprites.is_empty() { + return true; + } + align_offset(instance_offset); + + let sprite_bytes_len = mem::size_of_val(sprites); + let buffer_contents = + unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) }; + + let next_offset = *instance_offset + sprite_bytes_len; + if next_offset > instance_buffer.size { + return false; + } + + let texture = self.sprite_atlas.metal_texture(texture_id); + let texture_size = size( + DevicePixels(texture.width() as i32), + DevicePixels(texture.height() as i32), + ); + command_encoder.set_render_pipeline_state(&self.monochrome_sprites_pipeline_state); + command_encoder.set_vertex_buffer( + SpriteInputIndex::Vertices as u64, + Some(&self.unit_vertices), + 0, + ); + command_encoder.set_vertex_buffer( + SpriteInputIndex::Sprites as u64, + Some(&instance_buffer.metal_buffer), + *instance_offset as u64, + ); + command_encoder.set_vertex_bytes( + SpriteInputIndex::ViewportSize as u64, + mem::size_of_val(&viewport_size) as u64, + &viewport_size as *const Size as *const _, + ); + command_encoder.set_vertex_bytes( + SpriteInputIndex::AtlasTextureSize as u64, + mem::size_of_val(&texture_size) as u64, + &texture_size as *const Size as *const _, + ); + command_encoder.set_fragment_buffer( + SpriteInputIndex::Sprites as u64, + Some(&instance_buffer.metal_buffer), + *instance_offset as u64, + ); + command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture)); + + unsafe { + ptr::copy_nonoverlapping( + sprites.as_ptr() as *const u8, + buffer_contents, + sprite_bytes_len, + ); + } + + command_encoder.draw_primitives_instanced( + metal::MTLPrimitiveType::Triangle, + 0, + 6, + sprites.len() as u64, + ); + *instance_offset = next_offset; + true + } + + fn draw_polychrome_sprites( + &self, + texture_id: AtlasTextureId, + sprites: &[PolychromeSprite], + instance_buffer: &mut InstanceBuffer, + instance_offset: &mut usize, + viewport_size: Size, + command_encoder: &metal::RenderCommandEncoderRef, + ) -> bool { + if sprites.is_empty() { + return true; + } + align_offset(instance_offset); + + let texture = self.sprite_atlas.metal_texture(texture_id); + let texture_size = size( + DevicePixels(texture.width() as i32), + DevicePixels(texture.height() as i32), + ); + command_encoder.set_render_pipeline_state(&self.polychrome_sprites_pipeline_state); + command_encoder.set_vertex_buffer( + SpriteInputIndex::Vertices as u64, + Some(&self.unit_vertices), + 0, + ); + command_encoder.set_vertex_buffer( + SpriteInputIndex::Sprites as u64, + Some(&instance_buffer.metal_buffer), + *instance_offset as u64, + ); + command_encoder.set_vertex_bytes( + SpriteInputIndex::ViewportSize as u64, + mem::size_of_val(&viewport_size) as u64, + &viewport_size as *const Size as *const _, + ); + command_encoder.set_vertex_bytes( + SpriteInputIndex::AtlasTextureSize as u64, + mem::size_of_val(&texture_size) as u64, + &texture_size as *const Size as *const _, + ); + command_encoder.set_fragment_buffer( + SpriteInputIndex::Sprites as u64, + Some(&instance_buffer.metal_buffer), + *instance_offset as u64, + ); + command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture)); + + let sprite_bytes_len = mem::size_of_val(sprites); + let buffer_contents = + unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) }; + + let next_offset = *instance_offset + sprite_bytes_len; + if next_offset > instance_buffer.size { + return false; + } + + unsafe { + ptr::copy_nonoverlapping( + sprites.as_ptr() as *const u8, + buffer_contents, + sprite_bytes_len, + ); + } + + command_encoder.draw_primitives_instanced( + metal::MTLPrimitiveType::Triangle, + 0, + 6, + sprites.len() as u64, + ); + *instance_offset = next_offset; + true + } + + fn draw_surfaces( + &mut self, + surfaces: &[PaintSurface], + instance_buffer: &mut InstanceBuffer, + instance_offset: &mut usize, + viewport_size: Size, + command_encoder: &metal::RenderCommandEncoderRef, + ) -> bool { + command_encoder.set_vertex_buffer( + SurfaceInputIndex::Vertices as u64, + Some(&self.unit_vertices), + 0, + ); + command_encoder.set_vertex_bytes( + SurfaceInputIndex::ViewportSize as u64, + mem::size_of_val(&viewport_size) as u64, + &viewport_size as *const Size as *const _, + ); + + for surface in surfaces { + let texture_size = size( + DevicePixels::from(surface.image_buffer.get_width() as i32), + DevicePixels::from(surface.image_buffer.get_height() as i32), + ); + + let pixel_format = surface.image_buffer.get_pixel_format(); + assert!( + pixel_format == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange + || pixel_format == kCVPixelFormatType_32BGRA, + "unsupported CVPixelBuffer surface pixel format: {pixel_format:#x}" + ); + + align_offset(instance_offset); + let next_offset = *instance_offset + mem::size_of::(); + if next_offset > instance_buffer.size { + return false; + } + + command_encoder.set_vertex_buffer( + SurfaceInputIndex::Surfaces as u64, + Some(&instance_buffer.metal_buffer), + *instance_offset as u64, + ); + command_encoder.set_vertex_bytes( + SurfaceInputIndex::TextureSize as u64, + mem::size_of_val(&texture_size) as u64, + &texture_size as *const Size as *const _, + ); + if pixel_format == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange { + command_encoder.set_render_pipeline_state(&self.surfaces_pipeline_state); + let y_texture = self + .core_video_texture_cache + .create_texture_from_image( + surface.image_buffer.as_concrete_TypeRef(), + None, + MTLPixelFormat::R8Unorm, + surface.image_buffer.get_width_of_plane(0), + surface.image_buffer.get_height_of_plane(0), + 0, + ) + .unwrap(); + let cb_cr_texture = self + .core_video_texture_cache + .create_texture_from_image( + surface.image_buffer.as_concrete_TypeRef(), + None, + MTLPixelFormat::RG8Unorm, + surface.image_buffer.get_width_of_plane(1), + surface.image_buffer.get_height_of_plane(1), + 1, + ) + .unwrap(); + + command_encoder.set_fragment_texture(SurfaceInputIndex::YTexture as u64, unsafe { + let texture = CVMetalTextureGetTexture(y_texture.as_concrete_TypeRef()); + Some(metal::TextureRef::from_ptr(texture as *mut _)) + }); + command_encoder.set_fragment_texture(SurfaceInputIndex::CbCrTexture as u64, unsafe { + let texture = CVMetalTextureGetTexture(cb_cr_texture.as_concrete_TypeRef()); + Some(metal::TextureRef::from_ptr(texture as *mut _)) + }); + } else { + command_encoder.set_render_pipeline_state(&self.surface_bgra_pipeline_state); + let bgra_texture = self + .core_video_texture_cache + .create_texture_from_image( + surface.image_buffer.as_concrete_TypeRef(), + None, + MTLPixelFormat::BGRA8Unorm, + surface.image_buffer.get_width(), + surface.image_buffer.get_height(), + 0, + ) + .unwrap(); + + command_encoder.set_fragment_texture(SurfaceInputIndex::YTexture as u64, unsafe { + let texture = CVMetalTextureGetTexture(bgra_texture.as_concrete_TypeRef()); + Some(metal::TextureRef::from_ptr(texture as *mut _)) + }); + command_encoder.set_fragment_texture(SurfaceInputIndex::CbCrTexture as u64, None); + } + + unsafe { + let buffer_contents = (instance_buffer.metal_buffer.contents() as *mut u8) + .add(*instance_offset) + as *mut SurfaceBounds; + ptr::write( + buffer_contents, + SurfaceBounds { + bounds: surface.bounds, + content_mask: surface.content_mask.clone(), + }, + ); + } + + command_encoder.draw_primitives(metal::MTLPrimitiveType::Triangle, 0, 6); + *instance_offset = next_offset; + } + true + } +} + +fn new_command_encoder<'a>( + command_buffer: &'a metal::CommandBufferRef, + drawable: &'a metal::MetalDrawableRef, + viewport_size: Size, + configure_color_attachment: impl Fn(&RenderPassColorAttachmentDescriptorRef), +) -> &'a metal::RenderCommandEncoderRef { + let render_pass_descriptor = metal::RenderPassDescriptor::new(); + let color_attachment = render_pass_descriptor + .color_attachments() + .object_at(0) + .unwrap(); + color_attachment.set_texture(Some(drawable.texture())); + color_attachment.set_store_action(metal::MTLStoreAction::Store); + configure_color_attachment(color_attachment); + + let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor); + command_encoder.set_viewport(metal::MTLViewport { + originX: 0.0, + originY: 0.0, + width: i32::from(viewport_size.width) as f64, + height: i32::from(viewport_size.height) as f64, + znear: 0.0, + zfar: 1.0, + }); + command_encoder +} + +fn build_pipeline_state( + device: &metal::DeviceRef, + library: &metal::LibraryRef, + label: &str, + vertex_fn_name: &str, + fragment_fn_name: &str, + pixel_format: metal::MTLPixelFormat, +) -> metal::RenderPipelineState { + let vertex_fn = library + .get_function(vertex_fn_name, None) + .expect("error locating vertex function"); + let fragment_fn = library + .get_function(fragment_fn_name, None) + .expect("error locating fragment function"); + + let descriptor = metal::RenderPipelineDescriptor::new(); + descriptor.set_label(label); + descriptor.set_vertex_function(Some(vertex_fn.as_ref())); + descriptor.set_fragment_function(Some(fragment_fn.as_ref())); + let color_attachment = descriptor.color_attachments().object_at(0).unwrap(); + color_attachment.set_pixel_format(pixel_format); + color_attachment.set_blending_enabled(true); + color_attachment.set_rgb_blend_operation(metal::MTLBlendOperation::Add); + color_attachment.set_alpha_blend_operation(metal::MTLBlendOperation::Add); + color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::SourceAlpha); + color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One); + color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha); + color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::One); + + device + .new_render_pipeline_state(&descriptor) + .expect("could not create render pipeline state") +} + +fn build_path_sprite_pipeline_state( + device: &metal::DeviceRef, + library: &metal::LibraryRef, + label: &str, + vertex_fn_name: &str, + fragment_fn_name: &str, + pixel_format: metal::MTLPixelFormat, +) -> metal::RenderPipelineState { + let vertex_fn = library + .get_function(vertex_fn_name, None) + .expect("error locating vertex function"); + let fragment_fn = library + .get_function(fragment_fn_name, None) + .expect("error locating fragment function"); + + let descriptor = metal::RenderPipelineDescriptor::new(); + descriptor.set_label(label); + descriptor.set_vertex_function(Some(vertex_fn.as_ref())); + descriptor.set_fragment_function(Some(fragment_fn.as_ref())); + let color_attachment = descriptor.color_attachments().object_at(0).unwrap(); + color_attachment.set_pixel_format(pixel_format); + color_attachment.set_blending_enabled(true); + color_attachment.set_rgb_blend_operation(metal::MTLBlendOperation::Add); + color_attachment.set_alpha_blend_operation(metal::MTLBlendOperation::Add); + color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::One); + color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One); + color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha); + color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::One); + + device + .new_render_pipeline_state(&descriptor) + .expect("could not create render pipeline state") +} + +fn build_path_rasterization_pipeline_state( + device: &metal::DeviceRef, + library: &metal::LibraryRef, + label: &str, + vertex_fn_name: &str, + fragment_fn_name: &str, + pixel_format: metal::MTLPixelFormat, + path_sample_count: u32, +) -> metal::RenderPipelineState { + let vertex_fn = library + .get_function(vertex_fn_name, None) + .expect("error locating vertex function"); + let fragment_fn = library + .get_function(fragment_fn_name, None) + .expect("error locating fragment function"); + + let descriptor = metal::RenderPipelineDescriptor::new(); + descriptor.set_label(label); + descriptor.set_vertex_function(Some(vertex_fn.as_ref())); + descriptor.set_fragment_function(Some(fragment_fn.as_ref())); + if path_sample_count > 1 { + descriptor.set_raster_sample_count(path_sample_count as _); + descriptor.set_alpha_to_coverage_enabled(false); + } + let color_attachment = descriptor.color_attachments().object_at(0).unwrap(); + color_attachment.set_pixel_format(pixel_format); + color_attachment.set_blending_enabled(true); + color_attachment.set_rgb_blend_operation(metal::MTLBlendOperation::Add); + color_attachment.set_alpha_blend_operation(metal::MTLBlendOperation::Add); + color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::One); + color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One); + color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha); + color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha); + + device + .new_render_pipeline_state(&descriptor) + .expect("could not create render pipeline state") +} + +// Align to multiples of 256 make Metal happy. +fn align_offset(offset: &mut usize) { + *offset = (*offset).div_ceil(256) * 256; +} + +#[repr(C)] +enum ShadowInputIndex { + Vertices = 0, + Shadows = 1, + ViewportSize = 2, +} + +#[repr(C)] +enum QuadInputIndex { + Vertices = 0, + Quads = 1, + ViewportSize = 2, +} + +#[repr(C)] +enum UnderlineInputIndex { + Vertices = 0, + Underlines = 1, + ViewportSize = 2, +} + +#[repr(C)] +enum SpriteInputIndex { + Vertices = 0, + Sprites = 1, + ViewportSize = 2, + AtlasTextureSize = 3, + AtlasTexture = 4, +} + +#[repr(C)] +enum SurfaceInputIndex { + Vertices = 0, + Surfaces = 1, + ViewportSize = 2, + TextureSize = 3, + YTexture = 4, + CbCrTexture = 5, +} + +#[repr(C)] +enum PathRasterizationInputIndex { + Vertices = 0, + ViewportSize = 1, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct PathSprite { + pub bounds: Bounds, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct SurfaceBounds { + pub bounds: Bounds, + pub content_mask: ContentMask, +} diff --git a/third_party/gpui/src/platform/mac/open_type.rs b/third_party/gpui/src/platform/mac/open_type.rs new file mode 100644 index 0000000..37a2955 --- /dev/null +++ b/third_party/gpui/src/platform/mac/open_type.rs @@ -0,0 +1,147 @@ +#![allow(unused, non_upper_case_globals)] + +use crate::{FontFallbacks, FontFeatures}; +use cocoa::appkit::CGFloat; +use core_foundation::{ + array::{ + CFArray, CFArrayAppendArray, CFArrayAppendValue, CFArrayCreateMutable, CFArrayGetCount, + CFArrayGetValueAtIndex, CFArrayRef, CFMutableArrayRef, kCFTypeArrayCallBacks, + }, + base::{CFRelease, TCFType, kCFAllocatorDefault}, + dictionary::{ + CFDictionaryCreate, kCFTypeDictionaryKeyCallBacks, kCFTypeDictionaryValueCallBacks, + }, + number::CFNumber, + string::{CFString, CFStringRef}, +}; +use core_foundation_sys::locale::CFLocaleCopyPreferredLanguages; +use core_graphics::{display::CFDictionary, geometry::CGAffineTransform}; +use core_text::{ + font::{CTFont, CTFontRef, cascade_list_for_languages}, + font_descriptor::{ + CTFontDescriptor, CTFontDescriptorCopyAttributes, CTFontDescriptorCreateCopyWithFeature, + CTFontDescriptorCreateWithAttributes, CTFontDescriptorCreateWithNameAndSize, + CTFontDescriptorRef, kCTFontCascadeListAttribute, kCTFontFeatureSettingsAttribute, + }, +}; +use font_kit::font::Font as FontKitFont; +use std::ptr; + +pub fn apply_features_and_fallbacks( + font: &mut FontKitFont, + features: &FontFeatures, + fallbacks: Option<&FontFallbacks>, +) -> anyhow::Result<()> { + unsafe { + let mut keys = vec![kCTFontFeatureSettingsAttribute]; + let mut values = vec![generate_feature_array(features)]; + if let Some(fallbacks) = fallbacks + && !fallbacks.fallback_list().is_empty() + { + keys.push(kCTFontCascadeListAttribute); + values.push(generate_fallback_array( + fallbacks, + font.native_font().as_concrete_TypeRef(), + )); + } + let attrs = CFDictionaryCreate( + kCFAllocatorDefault, + keys.as_ptr() as _, + values.as_ptr() as _, + keys.len() as isize, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks, + ); + let new_descriptor = CTFontDescriptorCreateWithAttributes(attrs); + CFRelease(attrs as _); + let new_descriptor = CTFontDescriptor::wrap_under_create_rule(new_descriptor); + let new_font = CTFontCreateCopyWithAttributes( + font.native_font().as_concrete_TypeRef(), + 0.0, + std::ptr::null(), + new_descriptor.as_concrete_TypeRef(), + ); + let new_font = CTFont::wrap_under_create_rule(new_font); + *font = font_kit::font::Font::from_native_font(&new_font); + + Ok(()) + } +} + +fn generate_feature_array(features: &FontFeatures) -> CFMutableArrayRef { + unsafe { + let feature_array = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks); + for (tag, value) in features.tag_value_list() { + let keys = [kCTFontOpenTypeFeatureTag, kCTFontOpenTypeFeatureValue]; + let values = [ + CFString::new(tag).as_CFTypeRef(), + CFNumber::from(*value as i32).as_CFTypeRef(), + ]; + let dict = CFDictionaryCreate( + kCFAllocatorDefault, + &keys as *const _ as _, + &values as *const _ as _, + 2, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks, + ); + values.into_iter().for_each(|value| CFRelease(value)); + CFArrayAppendValue(feature_array, dict as _); + CFRelease(dict as _); + } + feature_array + } +} + +fn generate_fallback_array(fallbacks: &FontFallbacks, font_ref: CTFontRef) -> CFMutableArrayRef { + unsafe { + let fallback_array = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks); + for user_fallback in fallbacks.fallback_list() { + let name = CFString::from(user_fallback.as_str()); + let fallback_desc = + CTFontDescriptorCreateWithNameAndSize(name.as_concrete_TypeRef(), 0.0); + CFArrayAppendValue(fallback_array, fallback_desc as _); + CFRelease(fallback_desc as _); + } + append_system_fallbacks(fallback_array, font_ref); + fallback_array + } +} + +fn append_system_fallbacks(fallback_array: CFMutableArrayRef, font_ref: CTFontRef) { + unsafe { + let preferred_languages: CFArray = + CFArray::wrap_under_create_rule(CFLocaleCopyPreferredLanguages()); + + let default_fallbacks = CTFontCopyDefaultCascadeListForLanguages( + font_ref, + preferred_languages.as_concrete_TypeRef(), + ); + let default_fallbacks: CFArray = + CFArray::wrap_under_create_rule(default_fallbacks); + + default_fallbacks + .iter() + .filter(|desc| desc.font_path().is_some()) + .map(|desc| { + CFArrayAppendValue(fallback_array, desc.as_concrete_TypeRef() as _); + }); + } +} + +#[link(name = "CoreText", kind = "framework")] +unsafe extern "C" { + static kCTFontOpenTypeFeatureTag: CFStringRef; + static kCTFontOpenTypeFeatureValue: CFStringRef; + + fn CTFontCreateCopyWithAttributes( + font: CTFontRef, + size: CGFloat, + matrix: *const CGAffineTransform, + attributes: CTFontDescriptorRef, + ) -> CTFontRef; + fn CTFontCopyDefaultCascadeListForLanguages( + font: CTFontRef, + languagePrefList: CFArrayRef, + ) -> CFArrayRef; +} diff --git a/third_party/gpui/src/platform/mac/platform.rs b/third_party/gpui/src/platform/mac/platform.rs new file mode 100644 index 0000000..ee393fb --- /dev/null +++ b/third_party/gpui/src/platform/mac/platform.rs @@ -0,0 +1,1709 @@ +use super::{ + BoolExt, MacKeyboardLayout, MacKeyboardMapper, + attributed_string::{NSAttributedString, NSMutableAttributedString}, + events::key_to_native, + renderer, +}; +use crate::{ + Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString, + CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher, + MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform, + PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, + PlatformWindow, Result, SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams, + hash, +}; +use anyhow::{Context as _, anyhow}; +use block::ConcreteBlock; +use cocoa::{ + appkit::{ + NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular, + NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard, + NSPasteboardTypePNG, NSPasteboardTypeRTF, NSPasteboardTypeRTFD, NSPasteboardTypeString, + NSPasteboardTypeTIFF, NSSavePanel, NSWindow, + }, + base::{BOOL, NO, YES, id, nil, selector}, + foundation::{ + NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSProcessInfo, NSRange, NSString, + NSUInteger, NSURL, + }, +}; +use core_foundation::{ + base::{CFRelease, CFType, CFTypeRef, OSStatus, TCFType}, + boolean::CFBoolean, + data::CFData, + dictionary::{CFDictionary, CFDictionaryRef, CFMutableDictionary}, + runloop::CFRunLoopRun, + string::{CFString, CFStringRef}, +}; +use ctor::ctor; +use futures::channel::oneshot; +use itertools::Itertools; +use objc::{ + class, + declare::ClassDecl, + msg_send, + runtime::{Class, Object, Sel}, + sel, sel_impl, +}; +use parking_lot::Mutex; +use ptr::null_mut; +use std::{ + cell::Cell, + convert::TryInto, + ffi::{CStr, OsStr, c_void}, + os::{raw::c_char, unix::ffi::OsStrExt}, + path::{Path, PathBuf}, + process::Command, + ptr, + rc::Rc, + slice, str, + sync::{Arc, OnceLock}, +}; +use strum::IntoEnumIterator; +use util::ResultExt; + +#[allow(non_upper_case_globals)] +const NSUTF8StringEncoding: NSUInteger = 4; + +const MAC_PLATFORM_IVAR: &str = "platform"; +static mut APP_CLASS: *const Class = ptr::null(); +static mut APP_DELEGATE_CLASS: *const Class = ptr::null(); + +#[ctor] +unsafe fn build_classes() { + unsafe { + APP_CLASS = { + let mut decl = ClassDecl::new("GPUIApplication", class!(NSApplication)).unwrap(); + decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR); + decl.register() + } + }; + unsafe { + APP_DELEGATE_CLASS = unsafe { + let mut decl = ClassDecl::new("GPUIApplicationDelegate", class!(NSResponder)).unwrap(); + decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR); + decl.add_method( + sel!(applicationWillFinishLaunching:), + will_finish_launching as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(applicationDidFinishLaunching:), + did_finish_launching as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(applicationShouldHandleReopen:hasVisibleWindows:), + should_handle_reopen as extern "C" fn(&mut Object, Sel, id, bool), + ); + decl.add_method( + sel!(applicationWillTerminate:), + will_terminate as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(handleGPUIMenuItem:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + // Add menu item handlers so that OS save panels have the correct key commands + decl.add_method( + sel!(cut:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(copy:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(paste:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(selectAll:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(undo:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(redo:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(validateMenuItem:), + validate_menu_item as extern "C" fn(&mut Object, Sel, id) -> bool, + ); + decl.add_method( + sel!(menuWillOpen:), + menu_will_open as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(applicationDockMenu:), + handle_dock_menu as extern "C" fn(&mut Object, Sel, id) -> id, + ); + decl.add_method( + sel!(application:openURLs:), + open_urls as extern "C" fn(&mut Object, Sel, id, id), + ); + + decl.add_method( + sel!(onKeyboardLayoutChange:), + on_keyboard_layout_change as extern "C" fn(&mut Object, Sel, id), + ); + + decl.register() + } + } +} + +pub(crate) struct MacPlatform(Mutex); + +pub(crate) struct MacPlatformState { + background_executor: BackgroundExecutor, + foreground_executor: ForegroundExecutor, + text_system: Arc, + renderer_context: renderer::Context, + headless: bool, + pasteboard: id, + text_hash_pasteboard_type: id, + metadata_pasteboard_type: id, + reopen: Option>, + on_keyboard_layout_change: Option>, + quit: Option>, + menu_command: Option>, + validate_menu_command: Option bool>>, + will_open_menu: Option>, + menu_actions: Vec>, + open_urls: Option)>>, + finish_launching: Option>, + dock_menu: Option, + menus: Option>, + keyboard_mapper: Rc, +} + +impl Default for MacPlatform { + fn default() -> Self { + Self::new(false) + } +} + +impl MacPlatform { + pub(crate) fn new(headless: bool) -> Self { + let dispatcher = Arc::new(MacDispatcher); + + #[cfg(feature = "font-kit")] + let text_system = Arc::new(crate::MacTextSystem::new()); + + #[cfg(not(feature = "font-kit"))] + let text_system = Arc::new(crate::NoopTextSystem::new()); + + let keyboard_layout = MacKeyboardLayout::new(); + let keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id())); + + Self(Mutex::new(MacPlatformState { + headless, + text_system, + background_executor: BackgroundExecutor::new(dispatcher.clone()), + foreground_executor: ForegroundExecutor::new(dispatcher), + renderer_context: renderer::Context::default(), + pasteboard: unsafe { NSPasteboard::generalPasteboard(nil) }, + text_hash_pasteboard_type: unsafe { ns_string("zed-text-hash") }, + metadata_pasteboard_type: unsafe { ns_string("zed-metadata") }, + reopen: None, + quit: None, + menu_command: None, + validate_menu_command: None, + will_open_menu: None, + menu_actions: Default::default(), + open_urls: None, + finish_launching: None, + dock_menu: None, + on_keyboard_layout_change: None, + menus: None, + keyboard_mapper, + })) + } + + unsafe fn read_from_pasteboard(&self, pasteboard: *mut Object, kind: id) -> Option<&[u8]> { + unsafe { + let data = pasteboard.dataForType(kind); + if data == nil { + None + } else { + Some(slice::from_raw_parts( + data.bytes() as *mut u8, + data.length() as usize, + )) + } + } + } + + unsafe fn create_menu_bar( + &self, + menus: &Vec, + delegate: id, + actions: &mut Vec>, + keymap: &Keymap, + ) -> id { + unsafe { + let application_menu = NSMenu::new(nil).autorelease(); + application_menu.setDelegate_(delegate); + + for menu_config in menus { + let menu = NSMenu::new(nil).autorelease(); + let menu_title = ns_string(&menu_config.name); + menu.setTitle_(menu_title); + menu.setDelegate_(delegate); + + for item_config in &menu_config.items { + menu.addItem_(Self::create_menu_item( + item_config, + delegate, + actions, + keymap, + )); + } + + let menu_item = NSMenuItem::new(nil).autorelease(); + menu_item.setTitle_(menu_title); + menu_item.setSubmenu_(menu); + application_menu.addItem_(menu_item); + + if menu_config.name == "Window" { + let app: id = msg_send![APP_CLASS, sharedApplication]; + app.setWindowsMenu_(menu); + } + } + + application_menu + } + } + + unsafe fn create_dock_menu( + &self, + menu_items: Vec, + delegate: id, + actions: &mut Vec>, + keymap: &Keymap, + ) -> id { + unsafe { + let dock_menu = NSMenu::new(nil); + dock_menu.setDelegate_(delegate); + for item_config in menu_items { + dock_menu.addItem_(Self::create_menu_item( + &item_config, + delegate, + actions, + keymap, + )); + } + + dock_menu + } + } + + unsafe fn create_menu_item( + item: &MenuItem, + delegate: id, + actions: &mut Vec>, + keymap: &Keymap, + ) -> id { + static DEFAULT_CONTEXT: OnceLock> = OnceLock::new(); + + unsafe { + match item { + MenuItem::Separator => NSMenuItem::separatorItem(nil), + MenuItem::Action { + name, + action, + os_action, + } => { + // Note that this is intentionally using earlier bindings, whereas typically + // later ones take display precedence. See the discussion on + // https://github.com/zed-industries/zed/issues/23621 + let keystrokes = keymap + .bindings_for_action(action.as_ref()) + .find_or_first(|binding| { + binding.predicate().is_none_or(|predicate| { + predicate.eval(DEFAULT_CONTEXT.get_or_init(|| { + let mut workspace_context = KeyContext::new_with_defaults(); + workspace_context.add("Workspace"); + let mut pane_context = KeyContext::new_with_defaults(); + pane_context.add("Pane"); + let mut editor_context = KeyContext::new_with_defaults(); + editor_context.add("Editor"); + + pane_context.extend(&editor_context); + workspace_context.extend(&pane_context); + vec![workspace_context] + })) + }) + }) + .map(|binding| binding.keystrokes()); + + let selector = match os_action { + Some(crate::OsAction::Cut) => selector("cut:"), + Some(crate::OsAction::Copy) => selector("copy:"), + Some(crate::OsAction::Paste) => selector("paste:"), + Some(crate::OsAction::SelectAll) => selector("selectAll:"), + // "undo:" and "redo:" are always disabled in our case, as + // we don't have a NSTextView/NSTextField to enable them on. + Some(crate::OsAction::Undo) => selector("handleGPUIMenuItem:"), + Some(crate::OsAction::Redo) => selector("handleGPUIMenuItem:"), + None => selector("handleGPUIMenuItem:"), + }; + + let item; + if let Some(keystrokes) = keystrokes { + if keystrokes.len() == 1 { + let keystroke = &keystrokes[0]; + let mut mask = NSEventModifierFlags::empty(); + for (modifier, flag) in &[ + ( + keystroke.modifiers().platform, + NSEventModifierFlags::NSCommandKeyMask, + ), + ( + keystroke.modifiers().control, + NSEventModifierFlags::NSControlKeyMask, + ), + ( + keystroke.modifiers().alt, + NSEventModifierFlags::NSAlternateKeyMask, + ), + ( + keystroke.modifiers().shift, + NSEventModifierFlags::NSShiftKeyMask, + ), + ] { + if *modifier { + mask |= *flag; + } + } + + item = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_( + ns_string(name), + selector, + ns_string(key_to_native(keystroke.key()).as_ref()), + ) + .autorelease(); + if Self::os_version() >= SemanticVersion::new(12, 0, 0) { + let _: () = msg_send![item, setAllowsAutomaticKeyEquivalentLocalization: NO]; + } + item.setKeyEquivalentModifierMask_(mask); + } else { + item = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_( + ns_string(name), + selector, + ns_string(""), + ) + .autorelease(); + } + } else { + item = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_( + ns_string(name), + selector, + ns_string(""), + ) + .autorelease(); + } + + let tag = actions.len() as NSInteger; + let _: () = msg_send![item, setTag: tag]; + actions.push(action.boxed_clone()); + item + } + MenuItem::Submenu(Menu { name, items }) => { + let item = NSMenuItem::new(nil).autorelease(); + let submenu = NSMenu::new(nil).autorelease(); + submenu.setDelegate_(delegate); + for item in items { + submenu.addItem_(Self::create_menu_item(item, delegate, actions, keymap)); + } + item.setSubmenu_(submenu); + item.setTitle_(ns_string(name)); + item + } + MenuItem::SystemMenu(OsMenu { name, menu_type }) => { + let item = NSMenuItem::new(nil).autorelease(); + let submenu = NSMenu::new(nil).autorelease(); + submenu.setDelegate_(delegate); + item.setSubmenu_(submenu); + item.setTitle_(ns_string(name)); + + match menu_type { + SystemMenuType::Services => { + let app: id = msg_send![APP_CLASS, sharedApplication]; + app.setServicesMenu_(item); + } + } + + item + } + } + } + } + + fn os_version() -> SemanticVersion { + let version = unsafe { + let process_info = NSProcessInfo::processInfo(nil); + process_info.operatingSystemVersion() + }; + SemanticVersion::new( + version.majorVersion as usize, + version.minorVersion as usize, + version.patchVersion as usize, + ) + } +} + +impl Platform for MacPlatform { + fn background_executor(&self) -> BackgroundExecutor { + self.0.lock().background_executor.clone() + } + + fn foreground_executor(&self) -> crate::ForegroundExecutor { + self.0.lock().foreground_executor.clone() + } + + fn text_system(&self) -> Arc { + self.0.lock().text_system.clone() + } + + fn run(&self, on_finish_launching: Box) { + let mut state = self.0.lock(); + if state.headless { + drop(state); + on_finish_launching(); + unsafe { CFRunLoopRun() }; + } else { + state.finish_launching = Some(on_finish_launching); + drop(state); + } + + unsafe { + let app: id = msg_send![APP_CLASS, sharedApplication]; + let app_delegate: id = msg_send![APP_DELEGATE_CLASS, new]; + app.setDelegate_(app_delegate); + + let self_ptr = self as *const Self as *const c_void; + (*app).set_ivar(MAC_PLATFORM_IVAR, self_ptr); + (*app_delegate).set_ivar(MAC_PLATFORM_IVAR, self_ptr); + + let pool = NSAutoreleasePool::new(nil); + app.run(); + pool.drain(); + + (*app).set_ivar(MAC_PLATFORM_IVAR, null_mut::()); + (*NSWindow::delegate(app)).set_ivar(MAC_PLATFORM_IVAR, null_mut::()); + } + } + + fn quit(&self) { + // Quitting the app causes us to close windows, which invokes `Window::on_close` callbacks + // synchronously before this method terminates. If we call `Platform::quit` while holding a + // borrow of the app state (which most of the time we will do), we will end up + // double-borrowing the app state in the `on_close` callbacks for our open windows. To solve + // this, we make quitting the application asynchronous so that we aren't holding borrows to + // the app state on the stack when we actually terminate the app. + + use super::dispatcher::{dispatch_get_main_queue, dispatch_sys::dispatch_async_f}; + + unsafe { + dispatch_async_f(dispatch_get_main_queue(), ptr::null_mut(), Some(quit)); + } + + unsafe extern "C" fn quit(_: *mut c_void) { + unsafe { + let app = NSApplication::sharedApplication(nil); + let _: () = msg_send![app, terminate: nil]; + } + } + } + + fn restart(&self, _binary_path: Option) { + use std::os::unix::process::CommandExt as _; + + let app_pid = std::process::id().to_string(); + let app_path = self + .app_path() + .ok() + // When the app is not bundled, `app_path` returns the + // directory containing the executable. Disregard this + // and get the path to the executable itself. + .and_then(|path| (path.extension()?.to_str()? == "app").then_some(path)) + .unwrap_or_else(|| std::env::current_exe().unwrap()); + + // Wait until this process has exited and then re-open this path. + let script = r#" + while kill -0 $0 2> /dev/null; do + sleep 0.1 + done + open "$1" + "#; + + #[allow( + clippy::disallowed_methods, + reason = "We are restarting ourselves, using std command thus is fine" + )] + let restart_process = Command::new("/bin/bash") + .arg("-c") + .arg(script) + .arg(app_pid) + .arg(app_path) + .process_group(0) + .spawn(); + + match restart_process { + Ok(_) => self.quit(), + Err(e) => log::error!("failed to spawn restart script: {:?}", e), + } + } + + fn activate(&self, ignoring_other_apps: bool) { + unsafe { + let app = NSApplication::sharedApplication(nil); + app.activateIgnoringOtherApps_(ignoring_other_apps.to_objc()); + } + } + + fn hide(&self) { + unsafe { + let app = NSApplication::sharedApplication(nil); + let _: () = msg_send![app, hide: nil]; + } + } + + fn hide_other_apps(&self) { + unsafe { + let app = NSApplication::sharedApplication(nil); + let _: () = msg_send![app, hideOtherApplications: nil]; + } + } + + fn unhide_other_apps(&self) { + unsafe { + let app = NSApplication::sharedApplication(nil); + let _: () = msg_send![app, unhideAllApplications: nil]; + } + } + + fn primary_display(&self) -> Option> { + Some(Rc::new(MacDisplay::primary())) + } + + fn displays(&self) -> Vec> { + MacDisplay::all() + .map(|screen| Rc::new(screen) as Rc<_>) + .collect() + } + + #[cfg(feature = "screen-capture")] + fn is_screen_capture_supported(&self) -> bool { + let min_version = cocoa::foundation::NSOperatingSystemVersion::new(12, 3, 0); + super::is_macos_version_at_least(min_version) + } + + #[cfg(feature = "screen-capture")] + fn screen_capture_sources( + &self, + ) -> oneshot::Receiver>>> { + super::screen_capture::get_sources() + } + + fn active_window(&self) -> Option { + MacWindow::active_window() + } + + // Returns the windows ordered front-to-back, meaning that the active + // window is the first one in the returned vec. + fn window_stack(&self) -> Option> { + Some(MacWindow::ordered_windows()) + } + + fn open_window( + &self, + handle: AnyWindowHandle, + options: WindowParams, + ) -> Result> { + let renderer_context = self.0.lock().renderer_context.clone(); + Ok(Box::new(MacWindow::open( + handle, + options, + self.foreground_executor(), + renderer_context, + ))) + } + + fn window_appearance(&self) -> WindowAppearance { + unsafe { + let app = NSApplication::sharedApplication(nil); + let appearance: id = msg_send![app, effectiveAppearance]; + WindowAppearance::from_native(appearance) + } + } + + fn open_url(&self, url: &str) { + unsafe { + let url = NSURL::alloc(nil) + .initWithString_(ns_string(url)) + .autorelease(); + let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace]; + msg_send![workspace, openURL: url] + } + } + + fn register_url_scheme(&self, scheme: &str) -> Task> { + // API only available post Monterey + // https://developer.apple.com/documentation/appkit/nsworkspace/3753004-setdefaultapplicationaturl + let (done_tx, done_rx) = oneshot::channel(); + if Self::os_version() < SemanticVersion::new(12, 0, 0) { + return Task::ready(Err(anyhow!( + "macOS 12.0 or later is required to register URL schemes" + ))); + } + + let bundle_id = unsafe { + let bundle: id = msg_send![class!(NSBundle), mainBundle]; + let bundle_id: id = msg_send![bundle, bundleIdentifier]; + if bundle_id == nil { + return Task::ready(Err(anyhow!("Can only register URL scheme in bundled apps"))); + } + bundle_id + }; + + unsafe { + let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace]; + let scheme: id = ns_string(scheme); + let app: id = msg_send![workspace, URLForApplicationWithBundleIdentifier: bundle_id]; + if app == nil { + return Task::ready(Err(anyhow!( + "Cannot register URL scheme until app is installed" + ))); + } + let done_tx = Cell::new(Some(done_tx)); + let block = ConcreteBlock::new(move |error: id| { + let result = if error == nil { + Ok(()) + } else { + let msg: id = msg_send![error, localizedDescription]; + Err(anyhow!("Failed to register: {msg:?}")) + }; + + if let Some(done_tx) = done_tx.take() { + let _ = done_tx.send(result); + } + }); + let block = block.copy(); + let _: () = msg_send![workspace, setDefaultApplicationAtURL: app toOpenURLsWithScheme: scheme completionHandler: block]; + } + + self.background_executor() + .spawn(async { crate::Flatten::flatten(done_rx.await.map_err(|e| anyhow!(e))) }) + } + + fn on_open_urls(&self, callback: Box)>) { + self.0.lock().open_urls = Some(callback); + } + + fn prompt_for_paths( + &self, + options: PathPromptOptions, + ) -> oneshot::Receiver>>> { + let (done_tx, done_rx) = oneshot::channel(); + self.foreground_executor() + .spawn(async move { + unsafe { + let panel = NSOpenPanel::openPanel(nil); + panel.setCanChooseDirectories_(options.directories.to_objc()); + panel.setCanChooseFiles_(options.files.to_objc()); + panel.setAllowsMultipleSelection_(options.multiple.to_objc()); + + panel.setCanCreateDirectories(true.to_objc()); + panel.setResolvesAliases_(false.to_objc()); + let done_tx = Cell::new(Some(done_tx)); + let block = ConcreteBlock::new(move |response: NSModalResponse| { + let result = if response == NSModalResponse::NSModalResponseOk { + let mut result = Vec::new(); + let urls = panel.URLs(); + for i in 0..urls.count() { + let url = urls.objectAtIndex(i); + if url.isFileURL() == YES + && let Ok(path) = ns_url_to_path(url) + { + result.push(path) + } + } + Some(result) + } else { + None + }; + + if let Some(done_tx) = done_tx.take() { + let _ = done_tx.send(Ok(result)); + } + }); + let block = block.copy(); + + if let Some(prompt) = options.prompt { + let _: () = msg_send![panel, setPrompt: ns_string(&prompt)]; + } + + let _: () = msg_send![panel, beginWithCompletionHandler: block]; + } + }) + .detach(); + done_rx + } + + fn prompt_for_new_path( + &self, + directory: &Path, + suggested_name: Option<&str>, + ) -> oneshot::Receiver>> { + let directory = directory.to_owned(); + let suggested_name = suggested_name.map(|s| s.to_owned()); + let (done_tx, done_rx) = oneshot::channel(); + self.foreground_executor() + .spawn(async move { + unsafe { + let panel = NSSavePanel::savePanel(nil); + let path = ns_string(directory.to_string_lossy().as_ref()); + let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc()); + panel.setDirectoryURL(url); + + if let Some(suggested_name) = suggested_name { + let name_string = ns_string(&suggested_name); + let _: () = msg_send![panel, setNameFieldStringValue: name_string]; + } + + let done_tx = Cell::new(Some(done_tx)); + let block = ConcreteBlock::new(move |response: NSModalResponse| { + let mut result = None; + if response == NSModalResponse::NSModalResponseOk { + let url = panel.URL(); + if url.isFileURL() == YES { + result = ns_url_to_path(panel.URL()).ok().map(|mut result| { + let Some(filename) = result.file_name() else { + return result; + }; + let chunks = filename + .as_bytes() + .split(|&b| b == b'.') + .collect::>(); + + // https://github.com/zed-industries/zed/issues/16969 + // Workaround a bug in macOS Sequoia that adds an extra file-extension + // sometimes. e.g. `a.sql` becomes `a.sql.s` or `a.txtx` becomes `a.txtx.txt` + // + // This is conditional on OS version because I'd like to get rid of it, so that + // you can manually create a file called `a.sql.s`. That said it seems better + // to break that use-case than breaking `a.sql`. + if chunks.len() == 3 + && chunks[1].starts_with(chunks[2]) + && Self::os_version() >= SemanticVersion::new(15, 0, 0) + { + let new_filename = OsStr::from_bytes( + &filename.as_bytes() + [..chunks[0].len() + 1 + chunks[1].len()], + ) + .to_owned(); + result.set_file_name(&new_filename); + } + result + }) + } + } + + if let Some(done_tx) = done_tx.take() { + let _ = done_tx.send(Ok(result)); + } + }); + let block = block.copy(); + let _: () = msg_send![panel, beginWithCompletionHandler: block]; + } + }) + .detach(); + + done_rx + } + + fn can_select_mixed_files_and_dirs(&self) -> bool { + true + } + + fn reveal_path(&self, path: &Path) { + unsafe { + let path = path.to_path_buf(); + self.0 + .lock() + .background_executor + .spawn(async move { + let full_path = ns_string(path.to_str().unwrap_or("")); + let root_full_path = ns_string(""); + let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace]; + let _: BOOL = msg_send![ + workspace, + selectFile: full_path + inFileViewerRootedAtPath: root_full_path + ]; + }) + .detach(); + } + } + + fn open_with_system(&self, path: &Path) { + let path = path.to_owned(); + self.0 + .lock() + .background_executor + .spawn(async move { + if let Some(mut child) = smol::process::Command::new("open") + .arg(path) + .spawn() + .context("invoking open command") + .log_err() + { + child.status().await.log_err(); + } + }) + .detach(); + } + + fn on_quit(&self, callback: Box) { + self.0.lock().quit = Some(callback); + } + + fn on_reopen(&self, callback: Box) { + self.0.lock().reopen = Some(callback); + } + + fn on_keyboard_layout_change(&self, callback: Box) { + self.0.lock().on_keyboard_layout_change = Some(callback); + } + + fn on_app_menu_action(&self, callback: Box) { + self.0.lock().menu_command = Some(callback); + } + + fn on_will_open_app_menu(&self, callback: Box) { + self.0.lock().will_open_menu = Some(callback); + } + + fn on_validate_app_menu_command(&self, callback: Box bool>) { + self.0.lock().validate_menu_command = Some(callback); + } + + fn keyboard_layout(&self) -> Box { + Box::new(MacKeyboardLayout::new()) + } + + fn keyboard_mapper(&self) -> Rc { + self.0.lock().keyboard_mapper.clone() + } + + fn app_path(&self) -> Result { + unsafe { + let bundle: id = NSBundle::mainBundle(); + anyhow::ensure!(!bundle.is_null(), "app is not running inside a bundle"); + Ok(path_from_objc(msg_send![bundle, bundlePath])) + } + } + + fn set_menus(&self, menus: Vec, keymap: &Keymap) { + unsafe { + let app: id = msg_send![APP_CLASS, sharedApplication]; + let mut state = self.0.lock(); + let actions = &mut state.menu_actions; + let menu = self.create_menu_bar(&menus, NSWindow::delegate(app), actions, keymap); + drop(state); + app.setMainMenu_(menu); + } + self.0.lock().menus = Some(menus.into_iter().map(|menu| menu.owned()).collect()); + } + + fn get_menus(&self) -> Option> { + self.0.lock().menus.clone() + } + + fn set_dock_menu(&self, menu: Vec, keymap: &Keymap) { + unsafe { + let app: id = msg_send![APP_CLASS, sharedApplication]; + let mut state = self.0.lock(); + let actions = &mut state.menu_actions; + let new = self.create_dock_menu(menu, NSWindow::delegate(app), actions, keymap); + if let Some(old) = state.dock_menu.replace(new) { + CFRelease(old as _) + } + } + } + + fn add_recent_document(&self, path: &Path) { + if let Some(path_str) = path.to_str() { + unsafe { + let document_controller: id = + msg_send![class!(NSDocumentController), sharedDocumentController]; + let url: id = NSURL::fileURLWithPath_(nil, ns_string(path_str)); + let _: () = msg_send![document_controller, noteNewRecentDocumentURL:url]; + } + } + } + + fn path_for_auxiliary_executable(&self, name: &str) -> Result { + unsafe { + let bundle: id = NSBundle::mainBundle(); + anyhow::ensure!(!bundle.is_null(), "app is not running inside a bundle"); + let name = ns_string(name); + let url: id = msg_send![bundle, URLForAuxiliaryExecutable: name]; + anyhow::ensure!(!url.is_null(), "resource not found"); + ns_url_to_path(url) + } + } + + /// Match cursor style to one of the styles available + /// in macOS's [NSCursor](https://developer.apple.com/documentation/appkit/nscursor). + fn set_cursor_style(&self, style: CursorStyle) { + unsafe { + if style == CursorStyle::None { + let _: () = msg_send![class!(NSCursor), setHiddenUntilMouseMoves:YES]; + return; + } + + let new_cursor: id = match style { + CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor], + CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor], + CursorStyle::Crosshair => msg_send![class!(NSCursor), crosshairCursor], + CursorStyle::ClosedHand => msg_send![class!(NSCursor), closedHandCursor], + CursorStyle::OpenHand => msg_send![class!(NSCursor), openHandCursor], + CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor], + CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor], + CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor], + CursorStyle::ResizeLeft => msg_send![class!(NSCursor), resizeLeftCursor], + CursorStyle::ResizeRight => msg_send![class!(NSCursor), resizeRightCursor], + CursorStyle::ResizeColumn => msg_send![class!(NSCursor), resizeLeftRightCursor], + CursorStyle::ResizeRow => msg_send![class!(NSCursor), resizeUpDownCursor], + CursorStyle::ResizeUp => msg_send![class!(NSCursor), resizeUpCursor], + CursorStyle::ResizeDown => msg_send![class!(NSCursor), resizeDownCursor], + + // Undocumented, private class methods: + // https://stackoverflow.com/questions/27242353/cocoa-predefined-resize-mouse-cursor + CursorStyle::ResizeUpLeftDownRight => { + msg_send![class!(NSCursor), _windowResizeNorthWestSouthEastCursor] + } + CursorStyle::ResizeUpRightDownLeft => { + msg_send![class!(NSCursor), _windowResizeNorthEastSouthWestCursor] + } + + CursorStyle::IBeamCursorForVerticalLayout => { + msg_send![class!(NSCursor), IBeamCursorForVerticalLayout] + } + CursorStyle::OperationNotAllowed => { + msg_send![class!(NSCursor), operationNotAllowedCursor] + } + CursorStyle::DragLink => msg_send![class!(NSCursor), dragLinkCursor], + CursorStyle::DragCopy => msg_send![class!(NSCursor), dragCopyCursor], + CursorStyle::ContextualMenu => msg_send![class!(NSCursor), contextualMenuCursor], + CursorStyle::None => unreachable!(), + }; + + let old_cursor: id = msg_send![class!(NSCursor), currentCursor]; + if new_cursor != old_cursor { + let _: () = msg_send![new_cursor, set]; + } + } + } + + fn should_auto_hide_scrollbars(&self) -> bool { + #[allow(non_upper_case_globals)] + const NSScrollerStyleOverlay: NSInteger = 1; + + unsafe { + let style: NSInteger = msg_send![class!(NSScroller), preferredScrollerStyle]; + style == NSScrollerStyleOverlay + } + } + + fn write_to_clipboard(&self, item: ClipboardItem) { + use crate::ClipboardEntry; + + unsafe { + // We only want to use NSAttributedString if there are multiple entries to write. + if item.entries.len() <= 1 { + match item.entries.first() { + Some(entry) => match entry { + ClipboardEntry::String(string) => { + self.write_plaintext_to_clipboard(string); + } + ClipboardEntry::Image(image) => { + self.write_image_to_clipboard(image); + } + }, + None => { + // Writing an empty list of entries just clears the clipboard. + let state = self.0.lock(); + state.pasteboard.clearContents(); + } + } + } else { + let mut any_images = false; + let attributed_string = { + let mut buf = NSMutableAttributedString::alloc(nil) + // TODO can we skip this? Or at least part of it? + .init_attributed_string(NSString::alloc(nil).init_str("")); + + for entry in item.entries { + if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry + { + let to_append = NSAttributedString::alloc(nil) + .init_attributed_string(NSString::alloc(nil).init_str(&text)); + + buf.appendAttributedString_(to_append); + } + } + + buf + }; + + let state = self.0.lock(); + state.pasteboard.clearContents(); + + // Only set rich text clipboard types if we actually have 1+ images to include. + if any_images { + let rtfd_data = attributed_string.RTFDFromRange_documentAttributes_( + NSRange::new(0, msg_send![attributed_string, length]), + nil, + ); + if rtfd_data != nil { + state + .pasteboard + .setData_forType(rtfd_data, NSPasteboardTypeRTFD); + } + + let rtf_data = attributed_string.RTFFromRange_documentAttributes_( + NSRange::new(0, attributed_string.length()), + nil, + ); + if rtf_data != nil { + state + .pasteboard + .setData_forType(rtf_data, NSPasteboardTypeRTF); + } + } + + let plain_text = attributed_string.string(); + state + .pasteboard + .setString_forType(plain_text, NSPasteboardTypeString); + } + } + } + + fn read_from_clipboard(&self) -> Option { + let state = self.0.lock(); + let pasteboard = state.pasteboard; + + // First, see if it's a string. + unsafe { + let types: id = pasteboard.types(); + let string_type: id = ns_string("public.utf8-plain-text"); + + if msg_send![types, containsObject: string_type] { + let data = pasteboard.dataForType(string_type); + if data == nil { + return None; + } else if data.bytes().is_null() { + // https://developer.apple.com/documentation/foundation/nsdata/1410616-bytes?language=objc + // "If the length of the NSData object is 0, this property returns nil." + return Some(self.read_string_from_clipboard(&state, &[])); + } else { + let bytes = + slice::from_raw_parts(data.bytes() as *mut u8, data.length() as usize); + + return Some(self.read_string_from_clipboard(&state, bytes)); + } + } + + // If it wasn't a string, try the various supported image types. + for format in ImageFormat::iter() { + if let Some(item) = try_clipboard_image(pasteboard, format) { + return Some(item); + } + } + } + + // If it wasn't a string or a supported image type, give up. + None + } + + fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task> { + let url = url.to_string(); + let username = username.to_string(); + let password = password.to_vec(); + self.background_executor().spawn(async move { + unsafe { + use security::*; + + let url = CFString::from(url.as_str()); + let username = CFString::from(username.as_str()); + let password = CFData::from_buffer(&password); + + // First, check if there are already credentials for the given server. If so, then + // update the username and password. + let mut verb = "updating"; + let mut query_attrs = CFMutableDictionary::with_capacity(2); + query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); + query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); + + let mut attrs = CFMutableDictionary::with_capacity(4); + attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); + attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); + attrs.set(kSecAttrAccount as *const _, username.as_CFTypeRef()); + attrs.set(kSecValueData as *const _, password.as_CFTypeRef()); + + let mut status = SecItemUpdate( + query_attrs.as_concrete_TypeRef(), + attrs.as_concrete_TypeRef(), + ); + + // If there were no existing credentials for the given server, then create them. + if status == errSecItemNotFound { + verb = "creating"; + status = SecItemAdd(attrs.as_concrete_TypeRef(), ptr::null_mut()); + } + anyhow::ensure!(status == errSecSuccess, "{verb} password failed: {status}"); + } + Ok(()) + }) + } + + fn read_credentials(&self, url: &str) -> Task)>>> { + let url = url.to_string(); + self.background_executor().spawn(async move { + let url = CFString::from(url.as_str()); + let cf_true = CFBoolean::true_value().as_CFTypeRef(); + + unsafe { + use security::*; + + // Find any credentials for the given server URL. + let mut attrs = CFMutableDictionary::with_capacity(5); + attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); + attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); + attrs.set(kSecReturnAttributes as *const _, cf_true); + attrs.set(kSecReturnData as *const _, cf_true); + + let mut result = CFTypeRef::from(ptr::null()); + let status = SecItemCopyMatching(attrs.as_concrete_TypeRef(), &mut result); + match status { + security::errSecSuccess => {} + security::errSecItemNotFound | security::errSecUserCanceled => return Ok(None), + _ => anyhow::bail!("reading password failed: {status}"), + } + + let result = CFType::wrap_under_create_rule(result) + .downcast::() + .context("keychain item was not a dictionary")?; + let username = result + .find(kSecAttrAccount as *const _) + .context("account was missing from keychain item")?; + let username = CFType::wrap_under_get_rule(*username) + .downcast::() + .context("account was not a string")?; + let password = result + .find(kSecValueData as *const _) + .context("password was missing from keychain item")?; + let password = CFType::wrap_under_get_rule(*password) + .downcast::() + .context("password was not a string")?; + + Ok(Some((username.to_string(), password.bytes().to_vec()))) + } + }) + } + + fn delete_credentials(&self, url: &str) -> Task> { + let url = url.to_string(); + + self.background_executor().spawn(async move { + unsafe { + use security::*; + + let url = CFString::from(url.as_str()); + let mut query_attrs = CFMutableDictionary::with_capacity(2); + query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); + query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); + + let status = SecItemDelete(query_attrs.as_concrete_TypeRef()); + anyhow::ensure!(status == errSecSuccess, "delete password failed: {status}"); + } + Ok(()) + }) + } +} + +impl MacPlatform { + unsafe fn read_string_from_clipboard( + &self, + state: &MacPlatformState, + text_bytes: &[u8], + ) -> ClipboardItem { + unsafe { + let text = String::from_utf8_lossy(text_bytes).to_string(); + let metadata = self + .read_from_pasteboard(state.pasteboard, state.text_hash_pasteboard_type) + .and_then(|hash_bytes| { + let hash_bytes = hash_bytes.try_into().ok()?; + let hash = u64::from_be_bytes(hash_bytes); + let metadata = self + .read_from_pasteboard(state.pasteboard, state.metadata_pasteboard_type)?; + + if hash == ClipboardString::text_hash(&text) { + String::from_utf8(metadata.to_vec()).ok() + } else { + None + } + }); + + ClipboardItem { + entries: vec![ClipboardEntry::String(ClipboardString { text, metadata })], + } + } + } + + unsafe fn write_plaintext_to_clipboard(&self, string: &ClipboardString) { + unsafe { + let state = self.0.lock(); + state.pasteboard.clearContents(); + + let text_bytes = NSData::dataWithBytes_length_( + nil, + string.text.as_ptr() as *const c_void, + string.text.len() as u64, + ); + state + .pasteboard + .setData_forType(text_bytes, NSPasteboardTypeString); + + if let Some(metadata) = string.metadata.as_ref() { + let hash_bytes = ClipboardString::text_hash(&string.text).to_be_bytes(); + let hash_bytes = NSData::dataWithBytes_length_( + nil, + hash_bytes.as_ptr() as *const c_void, + hash_bytes.len() as u64, + ); + state + .pasteboard + .setData_forType(hash_bytes, state.text_hash_pasteboard_type); + + let metadata_bytes = NSData::dataWithBytes_length_( + nil, + metadata.as_ptr() as *const c_void, + metadata.len() as u64, + ); + state + .pasteboard + .setData_forType(metadata_bytes, state.metadata_pasteboard_type); + } + } + } + + unsafe fn write_image_to_clipboard(&self, image: &Image) { + unsafe { + let state = self.0.lock(); + state.pasteboard.clearContents(); + + let bytes = NSData::dataWithBytes_length_( + nil, + image.bytes.as_ptr() as *const c_void, + image.bytes.len() as u64, + ); + + state + .pasteboard + .setData_forType(bytes, Into::::into(image.format).inner_mut()); + } + } +} + +fn try_clipboard_image(pasteboard: id, format: ImageFormat) -> Option { + let mut ut_type: UTType = format.into(); + + unsafe { + let types: id = pasteboard.types(); + if msg_send![types, containsObject: ut_type.inner()] { + let data = pasteboard.dataForType(ut_type.inner_mut()); + if data == nil { + None + } else { + let bytes = Vec::from(slice::from_raw_parts( + data.bytes() as *mut u8, + data.length() as usize, + )); + let id = hash(&bytes); + + Some(ClipboardItem { + entries: vec![ClipboardEntry::Image(Image { format, bytes, id })], + }) + } + } else { + None + } + } +} + +unsafe fn path_from_objc(path: id) -> PathBuf { + let len = msg_send![path, lengthOfBytesUsingEncoding: NSUTF8StringEncoding]; + let bytes = unsafe { path.UTF8String() as *const u8 }; + let path = str::from_utf8(unsafe { slice::from_raw_parts(bytes, len) }).unwrap(); + PathBuf::from(path) +} + +unsafe fn get_mac_platform(object: &mut Object) -> &MacPlatform { + unsafe { + let platform_ptr: *mut c_void = *object.get_ivar(MAC_PLATFORM_IVAR); + assert!(!platform_ptr.is_null()); + &*(platform_ptr as *const MacPlatform) + } +} + +extern "C" fn will_finish_launching(_this: &mut Object, _: Sel, _: id) { + unsafe { + let user_defaults: id = msg_send![class!(NSUserDefaults), standardUserDefaults]; + + // The autofill heuristic controller causes slowdown and high CPU usage. + // We don't know exactly why. This disables the full heuristic controller. + // + // Adapted from: https://github.com/ghostty-org/ghostty/pull/8625 + let name = ns_string("NSAutoFillHeuristicControllerEnabled"); + let existing_value: id = msg_send![user_defaults, objectForKey: name]; + if existing_value == nil { + let false_value: id = msg_send![class!(NSNumber), numberWithBool:false]; + let _: () = msg_send![user_defaults, setObject: false_value forKey: name]; + } + } +} + +extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) { + unsafe { + let app: id = msg_send![APP_CLASS, sharedApplication]; + app.setActivationPolicy_(NSApplicationActivationPolicyRegular); + + let notification_center: *mut Object = + msg_send![class!(NSNotificationCenter), defaultCenter]; + let name = ns_string("NSTextInputContextKeyboardSelectionDidChangeNotification"); + let _: () = msg_send![notification_center, addObserver: this as id + selector: sel!(onKeyboardLayoutChange:) + name: name + object: nil + ]; + + let platform = get_mac_platform(this); + let callback = platform.0.lock().finish_launching.take(); + if let Some(callback) = callback { + callback(); + } + } +} + +extern "C" fn should_handle_reopen(this: &mut Object, _: Sel, _: id, has_open_windows: bool) { + if !has_open_windows { + let platform = unsafe { get_mac_platform(this) }; + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.reopen.take() { + drop(lock); + callback(); + platform.0.lock().reopen.get_or_insert(callback); + } + } +} + +extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) { + let platform = unsafe { get_mac_platform(this) }; + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.quit.take() { + drop(lock); + callback(); + platform.0.lock().quit.get_or_insert(callback); + } +} + +extern "C" fn on_keyboard_layout_change(this: &mut Object, _: Sel, _: id) { + let platform = unsafe { get_mac_platform(this) }; + let mut lock = platform.0.lock(); + let keyboard_layout = MacKeyboardLayout::new(); + lock.keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id())); + if let Some(mut callback) = lock.on_keyboard_layout_change.take() { + drop(lock); + callback(); + platform + .0 + .lock() + .on_keyboard_layout_change + .get_or_insert(callback); + } +} + +extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) { + let urls = unsafe { + (0..urls.count()) + .filter_map(|i| { + let url = urls.objectAtIndex(i); + match CStr::from_ptr(url.absoluteString().UTF8String() as *mut c_char).to_str() { + Ok(string) => Some(string.to_string()), + Err(err) => { + log::error!("error converting path to string: {}", err); + None + } + } + }) + .collect::>() + }; + let platform = unsafe { get_mac_platform(this) }; + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.open_urls.take() { + drop(lock); + callback(urls); + platform.0.lock().open_urls.get_or_insert(callback); + } +} + +extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { + unsafe { + let platform = get_mac_platform(this); + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.menu_command.take() { + let tag: NSInteger = msg_send![item, tag]; + let index = tag as usize; + if let Some(action) = lock.menu_actions.get(index) { + let action = action.boxed_clone(); + drop(lock); + callback(&*action); + } + platform.0.lock().menu_command.get_or_insert(callback); + } + } +} + +extern "C" fn validate_menu_item(this: &mut Object, _: Sel, item: id) -> bool { + unsafe { + let mut result = false; + let platform = get_mac_platform(this); + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.validate_menu_command.take() { + let tag: NSInteger = msg_send![item, tag]; + let index = tag as usize; + if let Some(action) = lock.menu_actions.get(index) { + let action = action.boxed_clone(); + drop(lock); + result = callback(action.as_ref()); + } + platform + .0 + .lock() + .validate_menu_command + .get_or_insert(callback); + } + result + } +} + +extern "C" fn menu_will_open(this: &mut Object, _: Sel, _: id) { + unsafe { + let platform = get_mac_platform(this); + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.will_open_menu.take() { + drop(lock); + callback(); + platform.0.lock().will_open_menu.get_or_insert(callback); + } + } +} + +extern "C" fn handle_dock_menu(this: &mut Object, _: Sel, _: id) -> id { + unsafe { + let platform = get_mac_platform(this); + let mut state = platform.0.lock(); + if let Some(id) = state.dock_menu { + id + } else { + nil + } + } +} + +unsafe fn ns_string(string: &str) -> id { + unsafe { NSString::alloc(nil).init_str(string).autorelease() } +} + +unsafe fn ns_url_to_path(url: id) -> Result { + let path: *mut c_char = msg_send![url, fileSystemRepresentation]; + anyhow::ensure!(!path.is_null(), "url is not a file path: {}", unsafe { + CStr::from_ptr(url.absoluteString().UTF8String()).to_string_lossy() + }); + Ok(PathBuf::from(OsStr::from_bytes(unsafe { + CStr::from_ptr(path).to_bytes() + }))) +} + +#[link(name = "Carbon", kind = "framework")] +unsafe extern "C" { + pub(super) fn TISCopyCurrentKeyboardLayoutInputSource() -> *mut Object; + pub(super) fn TISGetInputSourceProperty( + inputSource: *mut Object, + propertyKey: *const c_void, + ) -> *mut Object; + + pub(super) fn UCKeyTranslate( + keyLayoutPtr: *const ::std::os::raw::c_void, + virtualKeyCode: u16, + keyAction: u16, + modifierKeyState: u32, + keyboardType: u32, + keyTranslateOptions: u32, + deadKeyState: *mut u32, + maxStringLength: usize, + actualStringLength: *mut usize, + unicodeString: *mut u16, + ) -> u32; + pub(super) fn LMGetKbdType() -> u16; + pub(super) static kTISPropertyUnicodeKeyLayoutData: CFStringRef; + pub(super) static kTISPropertyInputSourceID: CFStringRef; + pub(super) static kTISPropertyLocalizedName: CFStringRef; +} + +mod security { + #![allow(non_upper_case_globals)] + use super::*; + + #[link(name = "Security", kind = "framework")] + unsafe extern "C" { + pub static kSecClass: CFStringRef; + pub static kSecClassInternetPassword: CFStringRef; + pub static kSecAttrServer: CFStringRef; + pub static kSecAttrAccount: CFStringRef; + pub static kSecValueData: CFStringRef; + pub static kSecReturnAttributes: CFStringRef; + pub static kSecReturnData: CFStringRef; + + pub fn SecItemAdd(attributes: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus; + pub fn SecItemUpdate(query: CFDictionaryRef, attributes: CFDictionaryRef) -> OSStatus; + pub fn SecItemDelete(query: CFDictionaryRef) -> OSStatus; + pub fn SecItemCopyMatching(query: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus; + } + + pub const errSecSuccess: OSStatus = 0; + pub const errSecUserCanceled: OSStatus = -128; + pub const errSecItemNotFound: OSStatus = -25300; +} + +impl From for UTType { + fn from(value: ImageFormat) -> Self { + match value { + ImageFormat::Png => Self::png(), + ImageFormat::Jpeg => Self::jpeg(), + ImageFormat::Tiff => Self::tiff(), + ImageFormat::Webp => Self::webp(), + ImageFormat::Gif => Self::gif(), + ImageFormat::Bmp => Self::bmp(), + ImageFormat::Svg => Self::svg(), + } + } +} + +// See https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/ +struct UTType(id); + +impl UTType { + pub fn png() -> Self { + // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/png + Self(unsafe { NSPasteboardTypePNG }) // This is a rare case where there's a built-in NSPasteboardType + } + + pub fn jpeg() -> Self { + // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/jpeg + Self(unsafe { ns_string("public.jpeg") }) + } + + pub fn gif() -> Self { + // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/gif + Self(unsafe { ns_string("com.compuserve.gif") }) + } + + pub fn webp() -> Self { + // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/webp + Self(unsafe { ns_string("org.webmproject.webp") }) + } + + pub fn bmp() -> Self { + // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/bmp + Self(unsafe { ns_string("com.microsoft.bmp") }) + } + + pub fn svg() -> Self { + // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/svg + Self(unsafe { ns_string("public.svg-image") }) + } + + pub fn tiff() -> Self { + // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/tiff + Self(unsafe { NSPasteboardTypeTIFF }) // This is a rare case where there's a built-in NSPasteboardType + } + + fn inner(&self) -> *const Object { + self.0 + } + + fn inner_mut(&self) -> *mut Object { + self.0 as *mut _ + } +} + +#[cfg(test)] +mod tests { + use crate::ClipboardItem; + + use super::*; + + #[test] + fn test_clipboard() { + let platform = build_platform(); + assert_eq!(platform.read_from_clipboard(), None); + + let item = ClipboardItem::new_string("1".to_string()); + platform.write_to_clipboard(item.clone()); + assert_eq!(platform.read_from_clipboard(), Some(item)); + + let item = ClipboardItem { + entries: vec![ClipboardEntry::String( + ClipboardString::new("2".to_string()).with_json_metadata(vec![3, 4]), + )], + }; + platform.write_to_clipboard(item.clone()); + assert_eq!(platform.read_from_clipboard(), Some(item)); + + let text_from_other_app = "text from other app"; + unsafe { + let bytes = NSData::dataWithBytes_length_( + nil, + text_from_other_app.as_ptr() as *const c_void, + text_from_other_app.len() as u64, + ); + platform + .0 + .lock() + .pasteboard + .setData_forType(bytes, NSPasteboardTypeString); + } + assert_eq!( + platform.read_from_clipboard(), + Some(ClipboardItem::new_string(text_from_other_app.to_string())) + ); + } + + fn build_platform() -> MacPlatform { + let platform = MacPlatform::new(false); + platform.0.lock().pasteboard = unsafe { NSPasteboard::pasteboardWithUniqueName(nil) }; + platform + } +} diff --git a/third_party/gpui/src/platform/mac/screen_capture.rs b/third_party/gpui/src/platform/mac/screen_capture.rs new file mode 100644 index 0000000..4d4ffa6 --- /dev/null +++ b/third_party/gpui/src/platform/mac/screen_capture.rs @@ -0,0 +1,334 @@ +use crate::{ + DevicePixels, ForegroundExecutor, SharedString, SourceMetadata, + platform::{ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream}, + size, +}; +use anyhow::{Result, anyhow}; +use block::ConcreteBlock; +use cocoa::{ + base::{YES, id, nil}, + foundation::{NSArray, NSString}, +}; +use collections::HashMap; +use core_foundation::base::TCFType; +use core_graphics::display::{ + CGDirectDisplayID, CGDisplayCopyDisplayMode, CGDisplayModeGetPixelHeight, + CGDisplayModeGetPixelWidth, CGDisplayModeRelease, +}; +use ctor::ctor; +use futures::channel::oneshot; +use media::core_media::{CMSampleBuffer, CMSampleBufferRef}; +use metal::NSInteger; +use objc::{ + class, + declare::ClassDecl, + msg_send, + runtime::{Class, Object, Sel}, + sel, sel_impl, +}; +use std::{cell::RefCell, ffi::c_void, mem, ptr, rc::Rc}; + +use super::NSStringExt; + +#[derive(Clone)] +pub struct MacScreenCaptureSource { + sc_display: id, + meta: Option, +} + +pub struct MacScreenCaptureStream { + sc_stream: id, + sc_stream_output: id, + meta: SourceMetadata, +} + +static mut DELEGATE_CLASS: *const Class = ptr::null(); +static mut OUTPUT_CLASS: *const Class = ptr::null(); +const FRAME_CALLBACK_IVAR: &str = "frame_callback"; + +#[allow(non_upper_case_globals)] +const SCStreamOutputTypeScreen: NSInteger = 0; + +impl ScreenCaptureSource for MacScreenCaptureSource { + fn metadata(&self) -> Result { + let (display_id, size) = unsafe { + let display_id: CGDirectDisplayID = msg_send![self.sc_display, displayID]; + let display_mode_ref = CGDisplayCopyDisplayMode(display_id); + let width = CGDisplayModeGetPixelWidth(display_mode_ref); + let height = CGDisplayModeGetPixelHeight(display_mode_ref); + CGDisplayModeRelease(display_mode_ref); + + ( + display_id, + size(DevicePixels(width as i32), DevicePixels(height as i32)), + ) + }; + let (label, is_main) = self + .meta + .clone() + .map(|meta| (meta.label, meta.is_main)) + .unzip(); + + Ok(SourceMetadata { + id: display_id as u64, + label, + is_main, + resolution: size, + }) + } + + fn stream( + &self, + _foreground_executor: &ForegroundExecutor, + frame_callback: Box, + ) -> oneshot::Receiver>> { + unsafe { + let stream: id = msg_send![class!(SCStream), alloc]; + let filter: id = msg_send![class!(SCContentFilter), alloc]; + let configuration: id = msg_send![class!(SCStreamConfiguration), alloc]; + let delegate: id = msg_send![DELEGATE_CLASS, alloc]; + let output: id = msg_send![OUTPUT_CLASS, alloc]; + + let excluded_windows = NSArray::array(nil); + let filter: id = msg_send![filter, initWithDisplay:self.sc_display excludingWindows:excluded_windows]; + let configuration: id = msg_send![configuration, init]; + let _: id = msg_send![configuration, setScalesToFit: true]; + let _: id = msg_send![configuration, setPixelFormat: 0x42475241]; + // let _: id = msg_send![configuration, setShowsCursor: false]; + // let _: id = msg_send![configuration, setCaptureResolution: 3]; + let delegate: id = msg_send![delegate, init]; + let output: id = msg_send![output, init]; + + output.as_mut().unwrap().set_ivar( + FRAME_CALLBACK_IVAR, + Box::into_raw(Box::new(frame_callback)) as *mut c_void, + ); + + let meta = self.metadata().unwrap(); + let _: id = msg_send![configuration, setWidth: meta.resolution.width.0 as i64]; + let _: id = msg_send![configuration, setHeight: meta.resolution.height.0 as i64]; + let stream: id = msg_send![stream, initWithFilter:filter configuration:configuration delegate:delegate]; + + let (mut tx, rx) = oneshot::channel(); + + let mut error: id = nil; + let _: () = msg_send![stream, addStreamOutput:output type:SCStreamOutputTypeScreen sampleHandlerQueue:0 error:&mut error as *mut id]; + if error != nil { + let message: id = msg_send![error, localizedDescription]; + tx.send(Err(anyhow!("failed to add stream output {message:?}"))) + .ok(); + return rx; + } + + let tx = Rc::new(RefCell::new(Some(tx))); + let handler = ConcreteBlock::new({ + move |error: id| { + let result = if error == nil { + let stream = MacScreenCaptureStream { + meta: meta.clone(), + sc_stream: stream, + sc_stream_output: output, + }; + Ok(Box::new(stream) as Box) + } else { + let message: id = msg_send![error, localizedDescription]; + Err(anyhow!("failed to stop screen capture stream {message:?}")) + }; + if let Some(tx) = tx.borrow_mut().take() { + tx.send(result).ok(); + } + } + }); + let handler = handler.copy(); + let _: () = msg_send![stream, startCaptureWithCompletionHandler:handler]; + rx + } + } +} + +impl Drop for MacScreenCaptureSource { + fn drop(&mut self) { + unsafe { + let _: () = msg_send![self.sc_display, release]; + } + } +} + +impl ScreenCaptureStream for MacScreenCaptureStream { + fn metadata(&self) -> Result { + Ok(self.meta.clone()) + } +} + +impl Drop for MacScreenCaptureStream { + fn drop(&mut self) { + unsafe { + let mut error: id = nil; + let _: () = msg_send![self.sc_stream, removeStreamOutput:self.sc_stream_output type:SCStreamOutputTypeScreen error:&mut error as *mut _]; + if error != nil { + let message: id = msg_send![error, localizedDescription]; + log::error!("failed to add stream output {message:?}"); + } + + let handler = ConcreteBlock::new(move |error: id| { + if error != nil { + let message: id = msg_send![error, localizedDescription]; + log::error!("failed to stop screen capture stream {message:?}"); + } + }); + let block = handler.copy(); + let _: () = msg_send![self.sc_stream, stopCaptureWithCompletionHandler:block]; + let _: () = msg_send![self.sc_stream, release]; + let _: () = msg_send![self.sc_stream_output, release]; + } + } +} + +#[derive(Clone)] +struct ScreenMeta { + label: SharedString, + // Is this the screen with menu bar? + is_main: bool, +} + +unsafe fn screen_id_to_human_label() -> HashMap { + let screens: id = msg_send![class!(NSScreen), screens]; + let count: usize = msg_send![screens, count]; + let mut map = HashMap::default(); + let screen_number_key = unsafe { NSString::alloc(nil).init_str("NSScreenNumber") }; + for i in 0..count { + let screen: id = msg_send![screens, objectAtIndex: i]; + let device_desc: id = msg_send![screen, deviceDescription]; + if device_desc == nil { + continue; + } + + let nsnumber: id = msg_send![device_desc, objectForKey: screen_number_key]; + if nsnumber == nil { + continue; + } + + let screen_id: u32 = msg_send![nsnumber, unsignedIntValue]; + + let name: id = msg_send![screen, localizedName]; + if name != nil { + let cstr: *const std::os::raw::c_char = msg_send![name, UTF8String]; + let rust_str = unsafe { + std::ffi::CStr::from_ptr(cstr) + .to_string_lossy() + .into_owned() + }; + map.insert( + screen_id, + ScreenMeta { + label: rust_str.into(), + is_main: i == 0, + }, + ); + } + } + map +} + +pub(crate) fn get_sources() -> oneshot::Receiver>>> { + unsafe { + let (mut tx, rx) = oneshot::channel(); + let tx = Rc::new(RefCell::new(Some(tx))); + let screen_id_to_label = screen_id_to_human_label(); + let block = ConcreteBlock::new(move |shareable_content: id, error: id| { + let Some(mut tx) = tx.borrow_mut().take() else { + return; + }; + + let result = if error == nil { + let displays: id = msg_send![shareable_content, displays]; + let mut result = Vec::new(); + for i in 0..displays.count() { + let display = displays.objectAtIndex(i); + let id: CGDirectDisplayID = msg_send![display, displayID]; + let meta = screen_id_to_label.get(&id).cloned(); + let source = MacScreenCaptureSource { + sc_display: msg_send![display, retain], + meta, + }; + result.push(Rc::new(source) as Rc); + } + Ok(result) + } else { + let msg: id = msg_send![error, localizedDescription]; + Err(anyhow!( + "Screen share failed: {:?}", + NSStringExt::to_str(&msg) + )) + }; + tx.send(result).ok(); + }); + let block = block.copy(); + + let _: () = msg_send![ + class!(SCShareableContent), + getShareableContentExcludingDesktopWindows:YES + onScreenWindowsOnly:YES + completionHandler:block]; + rx + } +} + +#[ctor] +unsafe fn build_classes() { + let mut decl = ClassDecl::new("GPUIStreamDelegate", class!(NSObject)).unwrap(); + unsafe { + decl.add_method( + sel!(outputVideoEffectDidStartForStream:), + output_video_effect_did_start_for_stream as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(outputVideoEffectDidStopForStream:), + output_video_effect_did_stop_for_stream as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(stream:didStopWithError:), + stream_did_stop_with_error as extern "C" fn(&Object, Sel, id, id), + ); + DELEGATE_CLASS = decl.register(); + + let mut decl = ClassDecl::new("GPUIStreamOutput", class!(NSObject)).unwrap(); + decl.add_method( + sel!(stream:didOutputSampleBuffer:ofType:), + stream_did_output_sample_buffer_of_type + as extern "C" fn(&Object, Sel, id, id, NSInteger), + ); + decl.add_ivar::<*mut c_void>(FRAME_CALLBACK_IVAR); + + OUTPUT_CLASS = decl.register(); + } +} + +extern "C" fn output_video_effect_did_start_for_stream(_this: &Object, _: Sel, _stream: id) {} + +extern "C" fn output_video_effect_did_stop_for_stream(_this: &Object, _: Sel, _stream: id) {} + +extern "C" fn stream_did_stop_with_error(_this: &Object, _: Sel, _stream: id, _error: id) {} + +extern "C" fn stream_did_output_sample_buffer_of_type( + this: &Object, + _: Sel, + _stream: id, + sample_buffer: id, + buffer_type: NSInteger, +) { + if buffer_type != SCStreamOutputTypeScreen { + return; + } + + unsafe { + let sample_buffer = sample_buffer as CMSampleBufferRef; + let sample_buffer = CMSampleBuffer::wrap_under_get_rule(sample_buffer); + if let Some(buffer) = sample_buffer.image_buffer() { + let callback: Box> = + Box::from_raw(*this.get_ivar::<*mut c_void>(FRAME_CALLBACK_IVAR) as *mut _); + callback(ScreenCaptureFrame(buffer)); + mem::forget(callback); + } + } +} diff --git a/third_party/gpui/src/platform/mac/shaders.metal b/third_party/gpui/src/platform/mac/shaders.metal new file mode 100644 index 0000000..c0e4dea --- /dev/null +++ b/third_party/gpui/src/platform/mac/shaders.metal @@ -0,0 +1,1246 @@ +#include +#include + +using namespace metal; + +float4 hsla_to_rgba(Hsla hsla); +float3 srgb_to_linear(float3 color); +float3 linear_to_srgb(float3 color); +float4 srgb_to_oklab(float4 color); +float4 oklab_to_srgb(float4 color); +float4 to_device_position(float2 unit_vertex, Bounds_ScaledPixels bounds, + constant Size_DevicePixels *viewport_size); +float4 to_device_position_transformed(float2 unit_vertex, Bounds_ScaledPixels bounds, + TransformationMatrix transformation, + constant Size_DevicePixels *input_viewport_size); + +float2 to_tile_position(float2 unit_vertex, AtlasTile tile, + constant Size_DevicePixels *atlas_size); +float4 distance_from_clip_rect(float2 unit_vertex, Bounds_ScaledPixels bounds, + Bounds_ScaledPixels clip_bounds); +float4 distance_from_clip_rect_transformed(float2 unit_vertex, Bounds_ScaledPixels bounds, + Bounds_ScaledPixels clip_bounds, TransformationMatrix transformation); +float corner_dash_velocity(float dv1, float dv2); +float dash_alpha(float t, float period, float length, float dash_velocity, + float antialias_threshold); +float quarter_ellipse_sdf(float2 point, float2 radii); +float pick_corner_radius(float2 center_to_point, Corners_ScaledPixels corner_radii); +float quad_sdf(float2 point, Bounds_ScaledPixels bounds, + Corners_ScaledPixels corner_radii); +float quad_sdf_impl(float2 center_to_point, float corner_radius); +float gaussian(float x, float sigma); +float2 erf(float2 x); +float blur_along_x(float x, float y, float sigma, float corner, + float2 half_size); +float4 over(float4 below, float4 above); +float radians(float degrees); +float4 fill_color(Background background, float2 position, Bounds_ScaledPixels bounds, + float4 solid_color, float4 color0, float4 color1); + +struct GradientColor { + float4 solid; + float4 color0; + float4 color1; +}; +GradientColor prepare_fill_color(uint tag, uint color_space, Hsla solid, Hsla color0, Hsla color1); + +struct QuadVertexOutput { + uint quad_id [[flat]]; + float4 position [[position]]; + float4 border_color [[flat]]; + float4 background_solid [[flat]]; + float4 background_color0 [[flat]]; + float4 background_color1 [[flat]]; + float clip_distance [[clip_distance]][4]; +}; + +struct QuadFragmentInput { + uint quad_id [[flat]]; + float4 position [[position]]; + float4 border_color [[flat]]; + float4 background_solid [[flat]]; + float4 background_color0 [[flat]]; + float4 background_color1 [[flat]]; +}; + +vertex QuadVertexOutput quad_vertex(uint unit_vertex_id [[vertex_id]], + uint quad_id [[instance_id]], + constant float2 *unit_vertices + [[buffer(QuadInputIndex_Vertices)]], + constant Quad *quads + [[buffer(QuadInputIndex_Quads)]], + constant Size_DevicePixels *viewport_size + [[buffer(QuadInputIndex_ViewportSize)]]) { + float2 unit_vertex = unit_vertices[unit_vertex_id]; + Quad quad = quads[quad_id]; + float4 device_position = + to_device_position(unit_vertex, quad.bounds, viewport_size); + float4 clip_distance = distance_from_clip_rect(unit_vertex, quad.bounds, + quad.content_mask.bounds); + float4 border_color = hsla_to_rgba(quad.border_color); + + GradientColor gradient = prepare_fill_color( + quad.background.tag, + quad.background.color_space, + quad.background.solid, + quad.background.colors[0].color, + quad.background.colors[1].color + ); + + return QuadVertexOutput{ + quad_id, + device_position, + border_color, + gradient.solid, + gradient.color0, + gradient.color1, + {clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}}; +} + +fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]], + constant Quad *quads + [[buffer(QuadInputIndex_Quads)]]) { + Quad quad = quads[input.quad_id]; + float4 background_color = fill_color(quad.background, input.position.xy, quad.bounds, + input.background_solid, input.background_color0, input.background_color1); + + bool unrounded = quad.corner_radii.top_left == 0.0 && + quad.corner_radii.bottom_left == 0.0 && + quad.corner_radii.top_right == 0.0 && + quad.corner_radii.bottom_right == 0.0; + + // Fast path when the quad is not rounded and doesn't have any border + if (quad.border_widths.top == 0.0 && + quad.border_widths.left == 0.0 && + quad.border_widths.right == 0.0 && + quad.border_widths.bottom == 0.0 && + unrounded) { + return background_color; + } + + float2 size = float2(quad.bounds.size.width, quad.bounds.size.height); + float2 half_size = size / 2.0; + float2 point = input.position.xy - float2(quad.bounds.origin.x, quad.bounds.origin.y); + float2 center_to_point = point - half_size; + + // Signed distance field threshold for inclusion of pixels. 0.5 is the + // minimum distance between the center of the pixel and the edge. + const float antialias_threshold = 0.5; + + // Radius of the nearest corner + float corner_radius = pick_corner_radius(center_to_point, quad.corner_radii); + + // Width of the nearest borders + float2 border = float2( + center_to_point.x < 0.0 ? quad.border_widths.left : quad.border_widths.right, + center_to_point.y < 0.0 ? quad.border_widths.top : quad.border_widths.bottom + ); + + // 0-width borders are reduced so that `inner_sdf >= antialias_threshold`. + // The purpose of this is to not draw antialiasing pixels in this case. + float2 reduced_border = float2( + border.x == 0.0 ? -antialias_threshold : border.x, + border.y == 0.0 ? -antialias_threshold : border.y); + + // Vector from the corner of the quad bounds to the point, after mirroring + // the point into the bottom right quadrant. Both components are <= 0. + float2 corner_to_point = fabs(center_to_point) - half_size; + + // Vector from the point to the center of the rounded corner's circle, also + // mirrored into bottom right quadrant. + float2 corner_center_to_point = corner_to_point + corner_radius; + + // Whether the nearest point on the border is rounded + bool is_near_rounded_corner = + corner_center_to_point.x >= 0.0 && + corner_center_to_point.y >= 0.0; + + // Vector from straight border inner corner to point. + // + // 0-width borders are turned into width -1 so that inner_sdf is > 1.0 near + // the border. Without this, antialiasing pixels would be drawn. + float2 straight_border_inner_corner_to_point = corner_to_point + reduced_border; + + // Whether the point is beyond the inner edge of the straight border + bool is_beyond_inner_straight_border = + straight_border_inner_corner_to_point.x > 0.0 || + straight_border_inner_corner_to_point.y > 0.0; + + + // Whether the point is far enough inside the quad, such that the pixels are + // not affected by the straight border. + bool is_within_inner_straight_border = + straight_border_inner_corner_to_point.x < -antialias_threshold && + straight_border_inner_corner_to_point.y < -antialias_threshold; + + // Fast path for points that must be part of the background + if (is_within_inner_straight_border && !is_near_rounded_corner) { + return background_color; + } + + // Signed distance of the point to the outside edge of the quad's border + float outer_sdf = quad_sdf_impl(corner_center_to_point, corner_radius); + + // Approximate signed distance of the point to the inside edge of the quad's + // border. It is negative outside this edge (within the border), and + // positive inside. + // + // This is not always an accurate signed distance: + // * The rounded portions with varying border width use an approximation of + // nearest-point-on-ellipse. + // * When it is quickly known to be outside the edge, -1.0 is used. + float inner_sdf = 0.0; + if (corner_center_to_point.x <= 0.0 || corner_center_to_point.y <= 0.0) { + // Fast paths for straight borders + inner_sdf = -max(straight_border_inner_corner_to_point.x, + straight_border_inner_corner_to_point.y); + } else if (is_beyond_inner_straight_border) { + // Fast path for points that must be outside the inner edge + inner_sdf = -1.0; + } else if (reduced_border.x == reduced_border.y) { + // Fast path for circular inner edge. + inner_sdf = -(outer_sdf + reduced_border.x); + } else { + float2 ellipse_radii = max(float2(0.0), float2(corner_radius) - reduced_border); + inner_sdf = quarter_ellipse_sdf(corner_center_to_point, ellipse_radii); + } + + // Negative when inside the border + float border_sdf = max(inner_sdf, outer_sdf); + + float4 color = background_color; + if (border_sdf < antialias_threshold) { + float4 border_color = input.border_color; + + // Dashed border logic when border_style == 1 + if (quad.border_style == 1) { + // Position along the perimeter in "dash space", where each dash + // period has length 1 + float t = 0.0; + + // Total number of dash periods, so that the dash spacing can be + // adjusted to evenly divide it + float max_t = 0.0; + + // Border width is proportional to dash size. This is the behavior + // used by browsers, but also avoids dashes from different segments + // overlapping when dash size is smaller than the border width. + // + // Dash pattern: (2 * border width) dash, (1 * border width) gap + const float dash_length_per_width = 2.0; + const float dash_gap_per_width = 1.0; + const float dash_period_per_width = dash_length_per_width + dash_gap_per_width; + + // Since the dash size is determined by border width, the density of + // dashes varies. Multiplying a pixel distance by this returns a + // position in dash space - it has units (dash period / pixels). So + // a dash velocity of (1 / 10) is 1 dash every 10 pixels. + float dash_velocity = 0.0; + + // Dividing this by the border width gives the dash velocity + const float dv_numerator = 1.0 / dash_period_per_width; + + if (unrounded) { + // When corners aren't rounded, the dashes are separately laid + // out on each straight line, rather than around the whole + // perimeter. This way each line starts and ends with a dash. + bool is_horizontal = corner_center_to_point.x < corner_center_to_point.y; + + // Choosing the right border width for dashed borders. + // TODO: A better solution exists taking a look at the whole file. + // this does not fix single dashed borders at the corners + float2 dashed_border = float2( + fmax(quad.border_widths.bottom, quad.border_widths.top), + fmax(quad.border_widths.right, quad.border_widths.left)); + + float border_width = is_horizontal ? dashed_border.x : dashed_border.y; + dash_velocity = dv_numerator / border_width; + t = is_horizontal ? point.x : point.y; + t *= dash_velocity; + max_t = is_horizontal ? size.x : size.y; + max_t *= dash_velocity; + } else { + // When corners are rounded, the dashes are laid out clockwise + // around the whole perimeter. + + float r_tr = quad.corner_radii.top_right; + float r_br = quad.corner_radii.bottom_right; + float r_bl = quad.corner_radii.bottom_left; + float r_tl = quad.corner_radii.top_left; + + float w_t = quad.border_widths.top; + float w_r = quad.border_widths.right; + float w_b = quad.border_widths.bottom; + float w_l = quad.border_widths.left; + + // Straight side dash velocities + float dv_t = w_t <= 0.0 ? 0.0 : dv_numerator / w_t; + float dv_r = w_r <= 0.0 ? 0.0 : dv_numerator / w_r; + float dv_b = w_b <= 0.0 ? 0.0 : dv_numerator / w_b; + float dv_l = w_l <= 0.0 ? 0.0 : dv_numerator / w_l; + + // Straight side lengths in dash space + float s_t = (size.x - r_tl - r_tr) * dv_t; + float s_r = (size.y - r_tr - r_br) * dv_r; + float s_b = (size.x - r_br - r_bl) * dv_b; + float s_l = (size.y - r_bl - r_tl) * dv_l; + + float corner_dash_velocity_tr = corner_dash_velocity(dv_t, dv_r); + float corner_dash_velocity_br = corner_dash_velocity(dv_b, dv_r); + float corner_dash_velocity_bl = corner_dash_velocity(dv_b, dv_l); + float corner_dash_velocity_tl = corner_dash_velocity(dv_t, dv_l); + + // Corner lengths in dash space + float c_tr = r_tr * (M_PI_F / 2.0) * corner_dash_velocity_tr; + float c_br = r_br * (M_PI_F / 2.0) * corner_dash_velocity_br; + float c_bl = r_bl * (M_PI_F / 2.0) * corner_dash_velocity_bl; + float c_tl = r_tl * (M_PI_F / 2.0) * corner_dash_velocity_tl; + + // Cumulative dash space upto each segment + float upto_tr = s_t; + float upto_r = upto_tr + c_tr; + float upto_br = upto_r + s_r; + float upto_b = upto_br + c_br; + float upto_bl = upto_b + s_b; + float upto_l = upto_bl + c_bl; + float upto_tl = upto_l + s_l; + max_t = upto_tl + c_tl; + + if (is_near_rounded_corner) { + float radians = atan2(corner_center_to_point.y, corner_center_to_point.x); + float corner_t = radians * corner_radius; + + if (center_to_point.x >= 0.0) { + if (center_to_point.y < 0.0) { + dash_velocity = corner_dash_velocity_tr; + // Subtracted because radians is pi/2 to 0 when + // going clockwise around the top right corner, + // since the y axis has been flipped + t = upto_r - corner_t * dash_velocity; + } else { + dash_velocity = corner_dash_velocity_br; + // Added because radians is 0 to pi/2 when going + // clockwise around the bottom-right corner + t = upto_br + corner_t * dash_velocity; + } + } else { + if (center_to_point.y >= 0.0) { + dash_velocity = corner_dash_velocity_bl; + // Subtracted because radians is pi/1 to 0 when + // going clockwise around the bottom-left corner, + // since the x axis has been flipped + t = upto_l - corner_t * dash_velocity; + } else { + dash_velocity = corner_dash_velocity_tl; + // Added because radians is 0 to pi/2 when going + // clockwise around the top-left corner, since both + // axis were flipped + t = upto_tl + corner_t * dash_velocity; + } + } + } else { + // Straight borders + bool is_horizontal = corner_center_to_point.x < corner_center_to_point.y; + if (is_horizontal) { + if (center_to_point.y < 0.0) { + dash_velocity = dv_t; + t = (point.x - r_tl) * dash_velocity; + } else { + dash_velocity = dv_b; + t = upto_bl - (point.x - r_bl) * dash_velocity; + } + } else { + if (center_to_point.x < 0.0) { + dash_velocity = dv_l; + t = upto_tl - (point.y - r_tl) * dash_velocity; + } else { + dash_velocity = dv_r; + t = upto_r + (point.y - r_tr) * dash_velocity; + } + } + } + } + + float dash_length = dash_length_per_width / dash_period_per_width; + float desired_dash_gap = dash_gap_per_width / dash_period_per_width; + + // Straight borders should start and end with a dash, so max_t is + // reduced to cause this. + max_t -= unrounded ? dash_length : 0.0; + if (max_t >= 1.0) { + // Adjust dash gap to evenly divide max_t + float dash_count = floor(max_t); + float dash_period = max_t / dash_count; + border_color.a *= dash_alpha(t, dash_period, dash_length, dash_velocity, + antialias_threshold); + } else if (unrounded) { + // When there isn't enough space for the full gap between the + // two start / end dashes of a straight border, reduce gap to + // make them fit. + float dash_gap = max_t - dash_length; + if (dash_gap > 0.0) { + float dash_period = dash_length + dash_gap; + border_color.a *= dash_alpha(t, dash_period, dash_length, dash_velocity, + antialias_threshold); + } + } + } + + // Blend the border on top of the background and then linearly interpolate + // between the two as we slide inside the background. + float4 blended_border = over(background_color, border_color); + color = mix(background_color, blended_border, + saturate(antialias_threshold - inner_sdf)); + } + + return color * float4(1.0, 1.0, 1.0, saturate(antialias_threshold - outer_sdf)); +} + +// Returns the dash velocity of a corner given the dash velocity of the two +// sides, by returning the slower velocity (larger dashes). +// +// Since 0 is used for dash velocity when the border width is 0 (instead of +// +inf), this returns the other dash velocity in that case. +// +// An alternative to this might be to appropriately interpolate the dash +// velocity around the corner, but that seems overcomplicated. +float corner_dash_velocity(float dv1, float dv2) { + if (dv1 == 0.0) { + return dv2; + } else if (dv2 == 0.0) { + return dv1; + } else { + return min(dv1, dv2); + } +} + +// Returns alpha used to render antialiased dashes. +// `t` is within the dash when `fmod(t, period) < length`. +float dash_alpha( + float t, float period, float length, float dash_velocity, + float antialias_threshold) { + float half_period = period / 2.0; + float half_length = length / 2.0; + // Value in [-half_period, half_period] + // The dash is in [-half_length, half_length] + float centered = fmod(t + half_period - half_length, period) - half_period; + // Signed distance for the dash, negative values are inside the dash + float signed_distance = abs(centered) - half_length; + // Antialiased alpha based on the signed distance + return saturate(antialias_threshold - signed_distance / dash_velocity); +} + +// This approximates distance to the nearest point to a quarter ellipse in a way +// that is sufficient for anti-aliasing when the ellipse is not very eccentric. +// The components of `point` are expected to be positive. +// +// Negative on the outside and positive on the inside. +float quarter_ellipse_sdf(float2 point, float2 radii) { + // Scale the space to treat the ellipse like a unit circle + float2 circle_vec = point / radii; + float unit_circle_sdf = length(circle_vec) - 1.0; + // Approximate up-scaling of the length by using the average of the radii. + // + // TODO: A better solution would be to use the gradient of the implicit + // function for an ellipse to approximate a scaling factor. + return unit_circle_sdf * (radii.x + radii.y) * -0.5; +} + +struct ShadowVertexOutput { + float4 position [[position]]; + float4 color [[flat]]; + uint shadow_id [[flat]]; + float clip_distance [[clip_distance]][4]; +}; + +struct ShadowFragmentInput { + float4 position [[position]]; + float4 color [[flat]]; + uint shadow_id [[flat]]; +}; + +vertex ShadowVertexOutput shadow_vertex( + uint unit_vertex_id [[vertex_id]], uint shadow_id [[instance_id]], + constant float2 *unit_vertices [[buffer(ShadowInputIndex_Vertices)]], + constant Shadow *shadows [[buffer(ShadowInputIndex_Shadows)]], + constant Size_DevicePixels *viewport_size + [[buffer(ShadowInputIndex_ViewportSize)]]) { + float2 unit_vertex = unit_vertices[unit_vertex_id]; + Shadow shadow = shadows[shadow_id]; + + float margin = 3. * shadow.blur_radius; + // Set the bounds of the shadow and adjust its size based on the shadow's + // spread radius to achieve the spreading effect + Bounds_ScaledPixels bounds = shadow.bounds; + bounds.origin.x -= margin; + bounds.origin.y -= margin; + bounds.size.width += 2. * margin; + bounds.size.height += 2. * margin; + + float4 device_position = + to_device_position(unit_vertex, bounds, viewport_size); + float4 clip_distance = + distance_from_clip_rect(unit_vertex, bounds, shadow.content_mask.bounds); + float4 color = hsla_to_rgba(shadow.color); + + return ShadowVertexOutput{ + device_position, + color, + shadow_id, + {clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}}; +} + +fragment float4 shadow_fragment(ShadowFragmentInput input [[stage_in]], + constant Shadow *shadows + [[buffer(ShadowInputIndex_Shadows)]]) { + Shadow shadow = shadows[input.shadow_id]; + + float2 origin = float2(shadow.bounds.origin.x, shadow.bounds.origin.y); + float2 size = float2(shadow.bounds.size.width, shadow.bounds.size.height); + float2 half_size = size / 2.; + float2 center = origin + half_size; + float2 point = input.position.xy - center; + float corner_radius; + if (point.x < 0.) { + if (point.y < 0.) { + corner_radius = shadow.corner_radii.top_left; + } else { + corner_radius = shadow.corner_radii.bottom_left; + } + } else { + if (point.y < 0.) { + corner_radius = shadow.corner_radii.top_right; + } else { + corner_radius = shadow.corner_radii.bottom_right; + } + } + + float alpha; + if (shadow.blur_radius == 0.) { + float distance = quad_sdf(input.position.xy, shadow.bounds, shadow.corner_radii); + alpha = saturate(0.5 - distance); + } else { + // The signal is only non-zero in a limited range, so don't waste samples + float low = point.y - half_size.y; + float high = point.y + half_size.y; + float start = clamp(-3. * shadow.blur_radius, low, high); + float end = clamp(3. * shadow.blur_radius, low, high); + + // Accumulate samples (we can get away with surprisingly few samples) + float step = (end - start) / 4.; + float y = start + step * 0.5; + alpha = 0.; + for (int i = 0; i < 4; i++) { + alpha += blur_along_x(point.x, point.y - y, shadow.blur_radius, + corner_radius, half_size) * + gaussian(y, shadow.blur_radius) * step; + y += step; + } + } + + return input.color * float4(1., 1., 1., alpha); +} + +struct UnderlineVertexOutput { + float4 position [[position]]; + float4 color [[flat]]; + uint underline_id [[flat]]; + float clip_distance [[clip_distance]][4]; +}; + +struct UnderlineFragmentInput { + float4 position [[position]]; + float4 color [[flat]]; + uint underline_id [[flat]]; +}; + +vertex UnderlineVertexOutput underline_vertex( + uint unit_vertex_id [[vertex_id]], uint underline_id [[instance_id]], + constant float2 *unit_vertices [[buffer(UnderlineInputIndex_Vertices)]], + constant Underline *underlines [[buffer(UnderlineInputIndex_Underlines)]], + constant Size_DevicePixels *viewport_size + [[buffer(ShadowInputIndex_ViewportSize)]]) { + float2 unit_vertex = unit_vertices[unit_vertex_id]; + Underline underline = underlines[underline_id]; + float4 device_position = + to_device_position(unit_vertex, underline.bounds, viewport_size); + float4 clip_distance = distance_from_clip_rect(unit_vertex, underline.bounds, + underline.content_mask.bounds); + float4 color = hsla_to_rgba(underline.color); + return UnderlineVertexOutput{ + device_position, + color, + underline_id, + {clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}}; +} + +fragment float4 underline_fragment(UnderlineFragmentInput input [[stage_in]], + constant Underline *underlines + [[buffer(UnderlineInputIndex_Underlines)]]) { + const float WAVE_FREQUENCY = 2.0; + const float WAVE_HEIGHT_RATIO = 0.8; + + Underline underline = underlines[input.underline_id]; + if (underline.wavy) { + float half_thickness = underline.thickness * 0.5; + float2 origin = + float2(underline.bounds.origin.x, underline.bounds.origin.y); + + float2 st = ((input.position.xy - origin) / underline.bounds.size.height) - + float2(0., 0.5); + float frequency = (M_PI_F * WAVE_FREQUENCY * underline.thickness) / underline.bounds.size.height; + float amplitude = (underline.thickness * WAVE_HEIGHT_RATIO) / underline.bounds.size.height; + + float sine = sin(st.x * frequency) * amplitude; + float dSine = cos(st.x * frequency) * amplitude * frequency; + float distance = (st.y - sine) / sqrt(1. + dSine * dSine); + float distance_in_pixels = distance * underline.bounds.size.height; + float distance_from_top_border = distance_in_pixels - half_thickness; + float distance_from_bottom_border = distance_in_pixels + half_thickness; + float alpha = saturate( + 0.5 - max(-distance_from_bottom_border, distance_from_top_border)); + return input.color * float4(1., 1., 1., alpha); + } else { + return input.color; + } +} + +struct MonochromeSpriteVertexOutput { + float4 position [[position]]; + float2 tile_position; + float4 color [[flat]]; + float4 clip_distance; +}; + +struct MonochromeSpriteFragmentInput { + float4 position [[position]]; + float2 tile_position; + float4 color [[flat]]; + float4 clip_distance; +}; + +vertex MonochromeSpriteVertexOutput monochrome_sprite_vertex( + uint unit_vertex_id [[vertex_id]], uint sprite_id [[instance_id]], + constant float2 *unit_vertices [[buffer(SpriteInputIndex_Vertices)]], + constant MonochromeSprite *sprites [[buffer(SpriteInputIndex_Sprites)]], + constant Size_DevicePixels *viewport_size + [[buffer(SpriteInputIndex_ViewportSize)]], + constant Size_DevicePixels *atlas_size + [[buffer(SpriteInputIndex_AtlasTextureSize)]]) { + float2 unit_vertex = unit_vertices[unit_vertex_id]; + MonochromeSprite sprite = sprites[sprite_id]; + float4 device_position = + to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation, viewport_size); + float4 clip_distance = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, + sprite.content_mask.bounds, sprite.transformation); + float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size); + float4 color = hsla_to_rgba(sprite.color); + return MonochromeSpriteVertexOutput{ + device_position, + tile_position, + color, + {clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}}; +} + +fragment float4 monochrome_sprite_fragment( + MonochromeSpriteFragmentInput input [[stage_in]], + constant MonochromeSprite *sprites [[buffer(SpriteInputIndex_Sprites)]], + texture2d atlas_texture [[texture(SpriteInputIndex_AtlasTexture)]]) { + if (any(input.clip_distance < float4(0.0))) { + return float4(0.0); + } + + constexpr sampler atlas_texture_sampler(mag_filter::linear, + min_filter::linear); + float4 sample = + atlas_texture.sample(atlas_texture_sampler, input.tile_position); + float4 color = input.color; + color.a *= sample.a; + return color; +} + +struct PolychromeSpriteVertexOutput { + float4 position [[position]]; + float2 tile_position; + uint sprite_id [[flat]]; + float clip_distance [[clip_distance]][4]; +}; + +struct PolychromeSpriteFragmentInput { + float4 position [[position]]; + float2 tile_position; + uint sprite_id [[flat]]; +}; + +vertex PolychromeSpriteVertexOutput polychrome_sprite_vertex( + uint unit_vertex_id [[vertex_id]], uint sprite_id [[instance_id]], + constant float2 *unit_vertices [[buffer(SpriteInputIndex_Vertices)]], + constant PolychromeSprite *sprites [[buffer(SpriteInputIndex_Sprites)]], + constant Size_DevicePixels *viewport_size + [[buffer(SpriteInputIndex_ViewportSize)]], + constant Size_DevicePixels *atlas_size + [[buffer(SpriteInputIndex_AtlasTextureSize)]]) { + + float2 unit_vertex = unit_vertices[unit_vertex_id]; + PolychromeSprite sprite = sprites[sprite_id]; + float4 device_position = + to_device_position(unit_vertex, sprite.bounds, viewport_size); + float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds, + sprite.content_mask.bounds); + float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size); + return PolychromeSpriteVertexOutput{ + device_position, + tile_position, + sprite_id, + {clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}}; +} + +fragment float4 polychrome_sprite_fragment( + PolychromeSpriteFragmentInput input [[stage_in]], + constant PolychromeSprite *sprites [[buffer(SpriteInputIndex_Sprites)]], + texture2d atlas_texture [[texture(SpriteInputIndex_AtlasTexture)]]) { + PolychromeSprite sprite = sprites[input.sprite_id]; + constexpr sampler atlas_texture_sampler(mag_filter::linear, + min_filter::linear); + float4 sample = + atlas_texture.sample(atlas_texture_sampler, input.tile_position); + float distance = + quad_sdf(input.position.xy, sprite.bounds, sprite.corner_radii); + + float4 color = sample; + if (sprite.grayscale) { + float grayscale = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b; + color.r = grayscale; + color.g = grayscale; + color.b = grayscale; + } + color.a *= sprite.opacity * saturate(0.5 - distance); + return color; +} + +struct PathRasterizationVertexOutput { + float4 position [[position]]; + float2 st_position; + uint vertex_id [[flat]]; + float clip_rect_distance [[clip_distance]][4]; +}; + +struct PathRasterizationFragmentInput { + float4 position [[position]]; + float2 st_position; + uint vertex_id [[flat]]; +}; + +vertex PathRasterizationVertexOutput path_rasterization_vertex( + uint vertex_id [[vertex_id]], + constant PathRasterizationVertex *vertices [[buffer(PathRasterizationInputIndex_Vertices)]], + constant Size_DevicePixels *atlas_size [[buffer(PathRasterizationInputIndex_ViewportSize)]] +) { + PathRasterizationVertex v = vertices[vertex_id]; + float2 vertex_position = float2(v.xy_position.x, v.xy_position.y); + float4 position = float4( + vertex_position * float2(2. / atlas_size->width, -2. / atlas_size->height) + float2(-1., 1.), + 0., + 1. + ); + return PathRasterizationVertexOutput{ + position, + float2(v.st_position.x, v.st_position.y), + vertex_id, + { + v.xy_position.x - v.bounds.origin.x, + v.bounds.origin.x + v.bounds.size.width - v.xy_position.x, + v.xy_position.y - v.bounds.origin.y, + v.bounds.origin.y + v.bounds.size.height - v.xy_position.y + } + }; +} + +fragment float4 path_rasterization_fragment( + PathRasterizationFragmentInput input [[stage_in]], + constant PathRasterizationVertex *vertices [[buffer(PathRasterizationInputIndex_Vertices)]] +) { + float2 dx = dfdx(input.st_position); + float2 dy = dfdy(input.st_position); + + PathRasterizationVertex v = vertices[input.vertex_id]; + Background background = v.color; + Bounds_ScaledPixels path_bounds = v.bounds; + float alpha; + if (length(float2(dx.x, dy.x)) < 0.001) { + alpha = 1.0; + } else { + float2 gradient = float2( + (2. * input.st_position.x) * dx.x - dx.y, + (2. * input.st_position.x) * dy.x - dy.y + ); + float f = (input.st_position.x * input.st_position.x) - input.st_position.y; + float distance = f / length(gradient); + alpha = saturate(0.5 - distance); + } + + GradientColor gradient_color = prepare_fill_color( + background.tag, + background.color_space, + background.solid, + background.colors[0].color, + background.colors[1].color + ); + + float4 color = fill_color( + background, + input.position.xy, + path_bounds, + gradient_color.solid, + gradient_color.color0, + gradient_color.color1 + ); + return float4(color.rgb * color.a * alpha, alpha * color.a); +} + +struct PathSpriteVertexOutput { + float4 position [[position]]; + float2 texture_coords; +}; + +vertex PathSpriteVertexOutput path_sprite_vertex( + uint unit_vertex_id [[vertex_id]], + uint sprite_id [[instance_id]], + constant float2 *unit_vertices [[buffer(SpriteInputIndex_Vertices)]], + constant PathSprite *sprites [[buffer(SpriteInputIndex_Sprites)]], + constant Size_DevicePixels *viewport_size [[buffer(SpriteInputIndex_ViewportSize)]] +) { + float2 unit_vertex = unit_vertices[unit_vertex_id]; + PathSprite sprite = sprites[sprite_id]; + // Don't apply content mask because it was already accounted for when + // rasterizing the path. + float4 device_position = + to_device_position(unit_vertex, sprite.bounds, viewport_size); + + float2 screen_position = float2(sprite.bounds.origin.x, sprite.bounds.origin.y) + unit_vertex * float2(sprite.bounds.size.width, sprite.bounds.size.height); + float2 texture_coords = screen_position / float2(viewport_size->width, viewport_size->height); + + return PathSpriteVertexOutput{ + device_position, + texture_coords + }; +} + +fragment float4 path_sprite_fragment( + PathSpriteVertexOutput input [[stage_in]], + texture2d intermediate_texture [[texture(SpriteInputIndex_AtlasTexture)]] +) { + constexpr sampler intermediate_texture_sampler(mag_filter::linear, min_filter::linear); + return intermediate_texture.sample(intermediate_texture_sampler, input.texture_coords); +} + +struct SurfaceVertexOutput { + float4 position [[position]]; + float2 texture_position; + float clip_distance [[clip_distance]][4]; +}; + +struct SurfaceFragmentInput { + float4 position [[position]]; + float2 texture_position; +}; + +vertex SurfaceVertexOutput surface_vertex( + uint unit_vertex_id [[vertex_id]], uint surface_id [[instance_id]], + constant float2 *unit_vertices [[buffer(SurfaceInputIndex_Vertices)]], + constant SurfaceBounds *surfaces [[buffer(SurfaceInputIndex_Surfaces)]], + constant Size_DevicePixels *viewport_size + [[buffer(SurfaceInputIndex_ViewportSize)]], + constant Size_DevicePixels *texture_size + [[buffer(SurfaceInputIndex_TextureSize)]]) { + float2 unit_vertex = unit_vertices[unit_vertex_id]; + SurfaceBounds surface = surfaces[surface_id]; + float4 device_position = + to_device_position(unit_vertex, surface.bounds, viewport_size); + float4 clip_distance = distance_from_clip_rect(unit_vertex, surface.bounds, + surface.content_mask.bounds); + // We are going to copy the whole texture, so the texture position corresponds + // to the current vertex of the unit triangle. + float2 texture_position = unit_vertex; + return SurfaceVertexOutput{ + device_position, + texture_position, + {clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}}; +} + +fragment float4 surface_fragment(SurfaceFragmentInput input [[stage_in]], + texture2d y_texture + [[texture(SurfaceInputIndex_YTexture)]], + texture2d cb_cr_texture + [[texture(SurfaceInputIndex_CbCrTexture)]]) { + constexpr sampler texture_sampler(mag_filter::linear, min_filter::linear); + const float4x4 ycbcrToRGBTransform = + float4x4(float4(+1.0000f, +1.0000f, +1.0000f, +0.0000f), + float4(+0.0000f, -0.3441f, +1.7720f, +0.0000f), + float4(+1.4020f, -0.7141f, +0.0000f, +0.0000f), + float4(-0.7010f, +0.5291f, -0.8860f, +1.0000f)); + float4 ycbcr = float4( + y_texture.sample(texture_sampler, input.texture_position).r, + cb_cr_texture.sample(texture_sampler, input.texture_position).rg, 1.0); + + return ycbcrToRGBTransform * ycbcr; +} + +fragment float4 surface_bgra_fragment(SurfaceFragmentInput input [[stage_in]], + texture2d bgra_texture + [[texture(SurfaceInputIndex_YTexture)]]) { + constexpr sampler texture_sampler(mag_filter::linear, min_filter::linear); + return bgra_texture.sample(texture_sampler, input.texture_position); +} + +float4 hsla_to_rgba(Hsla hsla) { + float h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range + float s = hsla.s; + float l = hsla.l; + float a = hsla.a; + + float c = (1.0 - fabs(2.0 * l - 1.0)) * s; + float x = c * (1.0 - fabs(fmod(h, 2.0) - 1.0)); + float m = l - c / 2.0; + + float r = 0.0; + float g = 0.0; + float b = 0.0; + + if (h >= 0.0 && h < 1.0) { + r = c; + g = x; + b = 0.0; + } else if (h >= 1.0 && h < 2.0) { + r = x; + g = c; + b = 0.0; + } else if (h >= 2.0 && h < 3.0) { + r = 0.0; + g = c; + b = x; + } else if (h >= 3.0 && h < 4.0) { + r = 0.0; + g = x; + b = c; + } else if (h >= 4.0 && h < 5.0) { + r = x; + g = 0.0; + b = c; + } else { + r = c; + g = 0.0; + b = x; + } + + float4 rgba; + rgba.x = (r + m); + rgba.y = (g + m); + rgba.z = (b + m); + rgba.w = a; + return rgba; +} + +float3 srgb_to_linear(float3 color) { + return pow(color, float3(2.2)); +} + +float3 linear_to_srgb(float3 color) { + return pow(color, float3(1.0 / 2.2)); +} + +// Converts a sRGB color to the Oklab color space. +// Reference: https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab +float4 srgb_to_oklab(float4 color) { + // Convert non-linear sRGB to linear sRGB + color = float4(srgb_to_linear(color.rgb), color.a); + + float l = 0.4122214708 * color.r + 0.5363325363 * color.g + 0.0514459929 * color.b; + float m = 0.2119034982 * color.r + 0.6806995451 * color.g + 0.1073969566 * color.b; + float s = 0.0883024619 * color.r + 0.2817188376 * color.g + 0.6299787005 * color.b; + + float l_ = pow(l, 1.0/3.0); + float m_ = pow(m, 1.0/3.0); + float s_ = pow(s, 1.0/3.0); + + return float4( + 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_, + 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_, + 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_, + color.a + ); +} + +// Converts an Oklab color to the sRGB color space. +float4 oklab_to_srgb(float4 color) { + float l_ = color.r + 0.3963377774 * color.g + 0.2158037573 * color.b; + float m_ = color.r - 0.1055613458 * color.g - 0.0638541728 * color.b; + float s_ = color.r - 0.0894841775 * color.g - 1.2914855480 * color.b; + + float l = l_ * l_ * l_; + float m = m_ * m_ * m_; + float s = s_ * s_ * s_; + + float3 linear_rgb = float3( + 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, + -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, + -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s + ); + + // Convert linear sRGB to non-linear sRGB + return float4(linear_to_srgb(linear_rgb), color.a); +} + +float4 to_device_position(float2 unit_vertex, Bounds_ScaledPixels bounds, + constant Size_DevicePixels *input_viewport_size) { + float2 position = + unit_vertex * float2(bounds.size.width, bounds.size.height) + + float2(bounds.origin.x, bounds.origin.y); + float2 viewport_size = float2((float)input_viewport_size->width, + (float)input_viewport_size->height); + float2 device_position = + position / viewport_size * float2(2., -2.) + float2(-1., 1.); + return float4(device_position, 0., 1.); +} + +float4 to_device_position_transformed(float2 unit_vertex, Bounds_ScaledPixels bounds, + TransformationMatrix transformation, + constant Size_DevicePixels *input_viewport_size) { + float2 position = + unit_vertex * float2(bounds.size.width, bounds.size.height) + + float2(bounds.origin.x, bounds.origin.y); + + // Apply the transformation matrix to the position via matrix multiplication. + float2 transformed_position = float2(0, 0); + transformed_position[0] = position[0] * transformation.rotation_scale[0][0] + position[1] * transformation.rotation_scale[0][1]; + transformed_position[1] = position[0] * transformation.rotation_scale[1][0] + position[1] * transformation.rotation_scale[1][1]; + + // Add in the translation component of the transformation matrix. + transformed_position[0] += transformation.translation[0]; + transformed_position[1] += transformation.translation[1]; + + float2 viewport_size = float2((float)input_viewport_size->width, + (float)input_viewport_size->height); + float2 device_position = + transformed_position / viewport_size * float2(2., -2.) + float2(-1., 1.); + return float4(device_position, 0., 1.); +} + + +float2 to_tile_position(float2 unit_vertex, AtlasTile tile, + constant Size_DevicePixels *atlas_size) { + float2 tile_origin = float2(tile.bounds.origin.x, tile.bounds.origin.y); + float2 tile_size = float2(tile.bounds.size.width, tile.bounds.size.height); + return (tile_origin + unit_vertex * tile_size) / + float2((float)atlas_size->width, (float)atlas_size->height); +} + +// Selects corner radius based on quadrant. +float pick_corner_radius(float2 center_to_point, Corners_ScaledPixels corner_radii) { + if (center_to_point.x < 0.) { + if (center_to_point.y < 0.) { + return corner_radii.top_left; + } else { + return corner_radii.bottom_left; + } + } else { + if (center_to_point.y < 0.) { + return corner_radii.top_right; + } else { + return corner_radii.bottom_right; + } + } +} + +// Signed distance of the point to the quad's border - positive outside the +// border, and negative inside. +float quad_sdf(float2 point, Bounds_ScaledPixels bounds, + Corners_ScaledPixels corner_radii) { + float2 half_size = float2(bounds.size.width, bounds.size.height) / 2.0; + float2 center = float2(bounds.origin.x, bounds.origin.y) + half_size; + float2 center_to_point = point - center; + float corner_radius = pick_corner_radius(center_to_point, corner_radii); + float2 corner_to_point = fabs(center_to_point) - half_size; + float2 corner_center_to_point = corner_to_point + corner_radius; + return quad_sdf_impl(corner_center_to_point, corner_radius); +} + +// Implementation of quad signed distance field +float quad_sdf_impl(float2 corner_center_to_point, float corner_radius) { + if (corner_radius == 0.0) { + // Fast path for unrounded corners + return max(corner_center_to_point.x, corner_center_to_point.y); + } else { + // Signed distance of the point from a quad that is inset by corner_radius + // It is negative inside this quad, and positive outside + float signed_distance_to_inset_quad = + // 0 inside the inset quad, and positive outside + length(max(float2(0.0), corner_center_to_point)) + + // 0 outside the inset quad, and negative inside + min(0.0, max(corner_center_to_point.x, corner_center_to_point.y)); + + return signed_distance_to_inset_quad - corner_radius; + } +} + +// A standard gaussian function, used for weighting samples +float gaussian(float x, float sigma) { + return exp(-(x * x) / (2. * sigma * sigma)) / (sqrt(2. * M_PI_F) * sigma); +} + +// This approximates the error function, needed for the gaussian integral +float2 erf(float2 x) { + float2 s = sign(x); + float2 a = abs(x); + float2 r1 = 1. + (0.278393 + (0.230389 + (0.000972 + 0.078108 * a) * a) * a) * a; + float2 r2 = r1 * r1; + return s - s / (r2 * r2); +} + +float blur_along_x(float x, float y, float sigma, float corner, + float2 half_size) { + float delta = min(half_size.y - corner - abs(y), 0.); + float curved = + half_size.x - corner + sqrt(max(0., corner * corner - delta * delta)); + float2 integral = + 0.5 + 0.5 * erf((x + float2(-curved, curved)) * (sqrt(0.5) / sigma)); + return integral.y - integral.x; +} + +float4 distance_from_clip_rect(float2 unit_vertex, Bounds_ScaledPixels bounds, + Bounds_ScaledPixels clip_bounds) { + float2 position = + unit_vertex * float2(bounds.size.width, bounds.size.height) + + float2(bounds.origin.x, bounds.origin.y); + return float4(position.x - clip_bounds.origin.x, + clip_bounds.origin.x + clip_bounds.size.width - position.x, + position.y - clip_bounds.origin.y, + clip_bounds.origin.y + clip_bounds.size.height - position.y); +} + +float4 distance_from_clip_rect_transformed(float2 unit_vertex, Bounds_ScaledPixels bounds, + Bounds_ScaledPixels clip_bounds, TransformationMatrix transformation) { + float2 position = + unit_vertex * float2(bounds.size.width, bounds.size.height) + + float2(bounds.origin.x, bounds.origin.y); + float2 transformed_position = float2(0, 0); + transformed_position[0] = position[0] * transformation.rotation_scale[0][0] + position[1] * transformation.rotation_scale[0][1]; + transformed_position[1] = position[0] * transformation.rotation_scale[1][0] + position[1] * transformation.rotation_scale[1][1]; + transformed_position[0] += transformation.translation[0]; + transformed_position[1] += transformation.translation[1]; + + return float4(transformed_position.x - clip_bounds.origin.x, + clip_bounds.origin.x + clip_bounds.size.width - transformed_position.x, + transformed_position.y - clip_bounds.origin.y, + clip_bounds.origin.y + clip_bounds.size.height - transformed_position.y); +} + +float4 over(float4 below, float4 above) { + float4 result; + float alpha = above.a + below.a * (1.0 - above.a); + result.rgb = + (above.rgb * above.a + below.rgb * below.a * (1.0 - above.a)) / alpha; + result.a = alpha; + return result; +} + +GradientColor prepare_fill_color(uint tag, uint color_space, Hsla solid, + Hsla color0, Hsla color1) { + GradientColor out; + if (tag == 0 || tag == 2) { + out.solid = hsla_to_rgba(solid); + } else if (tag == 1) { + out.color0 = hsla_to_rgba(color0); + out.color1 = hsla_to_rgba(color1); + + // Prepare color space in vertex for avoid conversion + // in fragment shader for performance reasons + if (color_space == 1) { + // Oklab + out.color0 = srgb_to_oklab(out.color0); + out.color1 = srgb_to_oklab(out.color1); + } + } + + return out; +} + +float2x2 rotate2d(float angle) { + float s = sin(angle); + float c = cos(angle); + return float2x2(c, -s, s, c); +} + +float4 fill_color(Background background, + float2 position, + Bounds_ScaledPixels bounds, + float4 solid_color, float4 color0, float4 color1) { + float4 color; + + switch (background.tag) { + case 0: + color = solid_color; + break; + case 1: { + // -90 degrees to match the CSS gradient angle. + float gradient_angle = background.gradient_angle_or_pattern_height; + float radians = (fmod(gradient_angle, 360.0) - 90.0) * (M_PI_F / 180.0); + float2 direction = float2(cos(radians), sin(radians)); + + // Expand the short side to be the same as the long side + if (bounds.size.width > bounds.size.height) { + direction.y *= bounds.size.height / bounds.size.width; + } else { + direction.x *= bounds.size.width / bounds.size.height; + } + + // Get the t value for the linear gradient with the color stop percentages. + float2 half_size = float2(bounds.size.width, bounds.size.height) / 2.; + float2 center = float2(bounds.origin.x, bounds.origin.y) + half_size; + float2 center_to_point = position - center; + float t = dot(center_to_point, direction) / length(direction); + // Check the direction to determine whether to use x or y + if (abs(direction.x) > abs(direction.y)) { + t = (t + half_size.x) / bounds.size.width; + } else { + t = (t + half_size.y) / bounds.size.height; + } + + // Adjust t based on the stop percentages + t = (t - background.colors[0].percentage) + / (background.colors[1].percentage + - background.colors[0].percentage); + t = clamp(t, 0.0, 1.0); + + switch (background.color_space) { + case 0: + color = mix(color0, color1, t); + break; + case 1: { + float4 oklab_color = mix(color0, color1, t); + color = oklab_to_srgb(oklab_color); + break; + } + } + break; + } + case 2: { + float gradient_angle_or_pattern_height = background.gradient_angle_or_pattern_height; + float pattern_width = (gradient_angle_or_pattern_height / 65535.0f) / 255.0f; + float pattern_interval = fmod(gradient_angle_or_pattern_height, 65535.0f) / 255.0f; + float pattern_height = pattern_width + pattern_interval; + float stripe_angle = M_PI_F / 4.0; + float pattern_period = pattern_height * sin(stripe_angle); + float2x2 rotation = rotate2d(stripe_angle); + float2 relative_position = position - float2(bounds.origin.x, bounds.origin.y); + float2 rotated_point = rotation * relative_position; + float pattern = fmod(rotated_point.x, pattern_period); + float distance = min(pattern, pattern_period - pattern) - pattern_period * (pattern_width / pattern_height) / 2.0f; + color = solid_color; + color.a *= saturate(0.5 - distance); + break; + } + } + + return color; +} diff --git a/third_party/gpui/src/platform/mac/status_item.rs b/third_party/gpui/src/platform/mac/status_item.rs new file mode 100644 index 0000000..21cc860 --- /dev/null +++ b/third_party/gpui/src/platform/mac/status_item.rs @@ -0,0 +1,388 @@ +use crate::{ + geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, + }, + platform::{ + self, + mac::{platform::NSViewLayerContentsRedrawDuringViewResize, renderer::Renderer}, + Event, FontSystem, WindowBounds, + }, + Scene, +}; +use cocoa::{ + appkit::{NSScreen, NSSquareStatusItemLength, NSStatusBar, NSStatusItem, NSView, NSWindow}, + base::{id, nil, YES}, + foundation::{NSPoint, NSRect, NSSize}, +}; +use ctor::ctor; +use foreign_types::ForeignTypeRef; +use objc::{ + class, + declare::ClassDecl, + msg_send, + rc::StrongPtr, + runtime::{Class, Object, Protocol, Sel}, + sel, sel_impl, +}; +use std::{ + cell::RefCell, + ffi::c_void, + ptr, + rc::{Rc, Weak}, + sync::Arc, +}; + +use super::screen::Screen; + +static mut VIEW_CLASS: *const Class = ptr::null(); +const STATE_IVAR: &str = "state"; + +#[ctor] +unsafe fn build_classes() { + VIEW_CLASS = { + let mut decl = ClassDecl::new("GPUIStatusItemView", class!(NSView)).unwrap(); + decl.add_ivar::<*mut c_void>(STATE_IVAR); + + decl.add_method(sel!(dealloc), dealloc_view as extern "C" fn(&Object, Sel)); + + decl.add_method( + sel!(mouseDown:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(mouseUp:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(rightMouseDown:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(rightMouseUp:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(otherMouseDown:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(otherMouseUp:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(mouseMoved:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(mouseDragged:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(scrollWheel:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(flagsChanged:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(makeBackingLayer), + make_backing_layer as extern "C" fn(&Object, Sel) -> id, + ); + decl.add_method( + sel!(viewDidChangeEffectiveAppearance), + view_did_change_effective_appearance as extern "C" fn(&Object, Sel), + ); + + decl.add_protocol(Protocol::get("CALayerDelegate").unwrap()); + decl.add_method( + sel!(displayLayer:), + display_layer as extern "C" fn(&Object, Sel, id), + ); + + decl.register() + }; +} + +pub struct StatusItem(Rc>); + +struct StatusItemState { + native_item: StrongPtr, + native_view: StrongPtr, + renderer: Renderer, + scene: Option, + event_callback: Option bool>>, + appearance_changed_callback: Option>, +} + +impl StatusItem { + pub fn add(fonts: Arc) -> Self { + unsafe { + let renderer = Renderer::new(false, fonts); + let status_bar = NSStatusBar::systemStatusBar(nil); + let native_item = + StrongPtr::retain(status_bar.statusItemWithLength_(NSSquareStatusItemLength)); + + let button = native_item.button(); + let _: () = msg_send![button, setHidden: YES]; + + let native_view = msg_send![VIEW_CLASS, alloc]; + let state = Rc::new(RefCell::new(StatusItemState { + native_item, + native_view: StrongPtr::new(native_view), + renderer, + scene: None, + event_callback: None, + appearance_changed_callback: None, + })); + + let parent_view = button.superview().superview(); + NSView::initWithFrame_( + native_view, + NSRect::new(NSPoint::new(0., 0.), NSView::frame(parent_view).size), + ); + (*native_view).set_ivar( + STATE_IVAR, + Weak::into_raw(Rc::downgrade(&state)) as *const c_void, + ); + native_view.setWantsBestResolutionOpenGLSurface_(YES); + native_view.setWantsLayer(YES); + let _: () = msg_send![ + native_view, + setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize + ]; + + parent_view.addSubview_(native_view); + + { + let state = state.borrow(); + let layer = state.renderer.layer(); + let scale_factor = state.scale_factor(); + let size = state.content_size() * scale_factor; + layer.set_contents_scale(scale_factor.into()); + layer.set_drawable_size(metal::CGSize::new(size.x().into(), size.y().into())); + } + + Self(state) + } + } +} + +impl platform::Window for StatusItem { + fn bounds(&self) -> WindowBounds { + self.0.borrow().bounds() + } + + fn content_size(&self) -> Vector2F { + self.0.borrow().content_size() + } + + fn scale_factor(&self) -> f32 { + self.0.borrow().scale_factor() + } + + fn appearance(&self) -> platform::Appearance { + unsafe { + let appearance: id = + msg_send![self.0.borrow().native_item.button(), effectiveAppearance]; + platform::Appearance::from_native(appearance) + } + } + + fn screen(&self) -> Rc { + unsafe { + Rc::new(Screen { + native_screen: self.0.borrow().native_window().screen(), + }) + } + } + + fn mouse_position(&self) -> Vector2F { + unimplemented!() + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + + fn set_input_handler(&mut self, _: Box) {} + + fn prompt( + &self, + _: crate::platform::PromptLevel, + _: &str, + _: &[&str], + ) -> postage::oneshot::Receiver { + unimplemented!() + } + + fn activate(&self) { + unimplemented!() + } + + fn set_title(&mut self, _: &str) { + unimplemented!() + } + + fn set_edited(&mut self, _: bool) { + unimplemented!() + } + + fn show_character_palette(&self) { + unimplemented!() + } + + fn minimize(&self) { + unimplemented!() + } + + fn zoom(&self) { + unimplemented!() + } + + fn present_scene(&mut self, scene: Scene) { + self.0.borrow_mut().scene = Some(scene); + unsafe { + let _: () = msg_send![*self.0.borrow().native_view, setNeedsDisplay: YES]; + } + } + + fn toggle_fullscreen(&self) { + unimplemented!() + } + + fn on_event(&mut self, callback: Box bool>) { + self.0.borrow_mut().event_callback = Some(callback); + } + + fn on_active_status_change(&mut self, _: Box) {} + + fn on_resize(&mut self, _: Box) {} + + fn on_fullscreen(&mut self, _: Box) {} + + fn on_moved(&mut self, _: Box) {} + + fn on_should_close(&mut self, _: Box bool>) {} + + fn on_close(&mut self, _: Box) {} + + fn on_appearance_changed(&mut self, callback: Box) { + self.0.borrow_mut().appearance_changed_callback = Some(callback); + } + + fn is_topmost_for_position(&self, _: Vector2F) -> bool { + true + } +} + +impl StatusItemState { + fn bounds(&self) -> WindowBounds { + unsafe { + let window: id = self.native_window(); + let screen_frame = window.screen().visibleFrame(); + let window_frame = NSWindow::frame(window); + let origin = vec2f( + window_frame.origin.x as f32, + (window_frame.origin.y - screen_frame.size.height - window_frame.size.height) + as f32, + ); + let size = vec2f( + window_frame.size.width as f32, + window_frame.size.height as f32, + ); + WindowBounds::Fixed(RectF::new(origin, size)) + } + } + + fn content_size(&self) -> Vector2F { + unsafe { + let NSSize { width, height, .. } = + NSView::frame(self.native_item.button().superview().superview()).size; + vec2f(width as f32, height as f32) + } + } + + fn scale_factor(&self) -> f32 { + unsafe { + let window: id = msg_send![self.native_item.button(), window]; + NSScreen::backingScaleFactor(window.screen()) as f32 + } + } + + pub fn native_window(&self) -> id { + unsafe { msg_send![self.native_item.button(), window] } + } +} + +extern "C" fn dealloc_view(this: &Object, _: Sel) { + unsafe { + drop_state(this); + + let _: () = msg_send![super(this, class!(NSView)), dealloc]; + } +} + +extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { + unsafe { + if let Some(state) = get_state(this).upgrade() { + let mut state_borrow = state.as_ref().borrow_mut(); + if let Some(event) = + Event::from_native(native_event, Some(state_borrow.content_size().y())) + { + if let Some(mut callback) = state_borrow.event_callback.take() { + drop(state_borrow); + callback(event); + state.borrow_mut().event_callback = Some(callback); + } + } + } + } +} + +extern "C" fn make_backing_layer(this: &Object, _: Sel) -> id { + if let Some(state) = unsafe { get_state(this).upgrade() } { + let state = state.borrow(); + state.renderer.layer().as_ptr() as id + } else { + nil + } +} + +extern "C" fn display_layer(this: &Object, _: Sel, _: id) { + unsafe { + if let Some(state) = get_state(this).upgrade() { + let mut state = state.borrow_mut(); + if let Some(scene) = state.scene.take() { + state.renderer.render(&scene); + } + } + } +} + +extern "C" fn view_did_change_effective_appearance(this: &Object, _: Sel) { + unsafe { + if let Some(state) = get_state(this).upgrade() { + let mut state_borrow = state.as_ref().borrow_mut(); + if let Some(mut callback) = state_borrow.appearance_changed_callback.take() { + drop(state_borrow); + callback(); + state.borrow_mut().appearance_changed_callback = Some(callback); + } + } + } +} + +unsafe fn get_state(object: &Object) -> Weak> { + let raw: *mut c_void = *object.get_ivar(STATE_IVAR); + let weak1 = Weak::from_raw(raw as *mut RefCell); + let weak2 = weak1.clone(); + let _ = Weak::into_raw(weak1); + weak2 +} + +unsafe fn drop_state(object: &Object) { + let raw: *const c_void = *object.get_ivar(STATE_IVAR); + Weak::from_raw(raw as *const RefCell); +} diff --git a/third_party/gpui/src/platform/mac/text_system.rs b/third_party/gpui/src/platform/mac/text_system.rs new file mode 100644 index 0000000..3d89ba7 --- /dev/null +++ b/third_party/gpui/src/platform/mac/text_system.rs @@ -0,0 +1,846 @@ +use crate::{ + Bounds, DevicePixels, Font, FontFallbacks, FontFeatures, FontId, FontMetrics, FontRun, + FontStyle, FontWeight, GlyphId, LineLayout, Pixels, PlatformTextSystem, Point, + RenderGlyphParams, Result, SUBPIXEL_VARIANTS_X, ShapedGlyph, ShapedRun, SharedString, Size, + point, px, size, swap_rgba_pa_to_bgra, +}; +use anyhow::anyhow; +use cocoa::appkit::CGFloat; +use collections::HashMap; +use core_foundation::{ + attributed_string::CFMutableAttributedString, + base::{CFRange, TCFType}, + number::CFNumber, + string::CFString, +}; +use core_graphics::{ + base::{CGGlyph, kCGImageAlphaPremultipliedLast}, + color_space::CGColorSpace, + context::{CGContext, CGTextDrawingMode}, + display::CGPoint, +}; +use core_text::{ + font::CTFont, + font_descriptor::{ + kCTFontSlantTrait, kCTFontSymbolicTrait, kCTFontWeightTrait, kCTFontWidthTrait, + }, + line::CTLine, + string_attributes::kCTFontAttributeName, +}; +use font_kit::{ + font::Font as FontKitFont, + handle::Handle, + hinting::HintingOptions, + metrics::Metrics, + properties::{Style as FontkitStyle, Weight as FontkitWeight}, + source::SystemSource, + sources::mem::MemSource, +}; +use parking_lot::{RwLock, RwLockUpgradableReadGuard}; +use pathfinder_geometry::{ + rect::{RectF, RectI}, + transform2d::Transform2F, + vector::{Vector2F, Vector2I}, +}; +use smallvec::SmallVec; +use std::{borrow::Cow, char, convert::TryFrom, sync::Arc}; + +use super::open_type::apply_features_and_fallbacks; + +#[allow(non_upper_case_globals)] +const kCGImageAlphaOnly: u32 = 7; + +pub(crate) struct MacTextSystem(RwLock); + +#[derive(Clone, PartialEq, Eq, Hash)] +struct FontKey { + font_family: SharedString, + font_features: FontFeatures, + font_fallbacks: Option, +} + +struct MacTextSystemState { + memory_source: MemSource, + system_source: SystemSource, + fonts: Vec, + font_selections: HashMap, + font_ids_by_postscript_name: HashMap, + font_ids_by_font_key: HashMap>, + postscript_names_by_font_id: HashMap, + /// UTF-16 indices of ZWNJS + zwnjs_scratch_space: Vec, +} + +impl MacTextSystem { + pub(crate) fn new() -> Self { + Self(RwLock::new(MacTextSystemState { + memory_source: MemSource::empty(), + system_source: SystemSource::new(), + fonts: Vec::new(), + font_selections: HashMap::default(), + font_ids_by_postscript_name: HashMap::default(), + font_ids_by_font_key: HashMap::default(), + postscript_names_by_font_id: HashMap::default(), + zwnjs_scratch_space: Vec::new(), + })) + } +} + +impl Default for MacTextSystem { + fn default() -> Self { + Self::new() + } +} + +impl PlatformTextSystem for MacTextSystem { + fn add_fonts(&self, fonts: Vec>) -> Result<()> { + self.0.write().add_fonts(fonts) + } + + fn all_font_names(&self) -> Vec { + let mut names = Vec::new(); + let collection = core_text::font_collection::create_for_all_families(); + let Some(descriptors) = collection.get_descriptors() else { + return names; + }; + for descriptor in descriptors.into_iter() { + names.extend(lenient_font_attributes::family_name(&descriptor)); + } + if let Ok(fonts_in_memory) = self.0.read().memory_source.all_families() { + names.extend(fonts_in_memory); + } + names + } + + fn font_id(&self, font: &Font) -> Result { + let lock = self.0.upgradable_read(); + if let Some(font_id) = lock.font_selections.get(font) { + Ok(*font_id) + } else { + let mut lock = RwLockUpgradableReadGuard::upgrade(lock); + let font_key = FontKey { + font_family: font.family.clone(), + font_features: font.features.clone(), + font_fallbacks: font.fallbacks.clone(), + }; + let candidates = if let Some(font_ids) = lock.font_ids_by_font_key.get(&font_key) { + font_ids.as_slice() + } else { + let font_ids = + lock.load_family(&font.family, &font.features, font.fallbacks.as_ref())?; + lock.font_ids_by_font_key.insert(font_key.clone(), font_ids); + lock.font_ids_by_font_key[&font_key].as_ref() + }; + + let candidate_properties = candidates + .iter() + .map(|font_id| lock.fonts[font_id.0].properties()) + .collect::>(); + + let ix = font_kit::matching::find_best_match( + &candidate_properties, + &font_kit::properties::Properties { + style: font.style.into(), + weight: font.weight.into(), + stretch: Default::default(), + }, + )?; + + let font_id = candidates[ix]; + lock.font_selections.insert(font.clone(), font_id); + Ok(font_id) + } + } + + fn font_metrics(&self, font_id: FontId) -> FontMetrics { + self.0.read().fonts[font_id.0].metrics().into() + } + + fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + Ok(self.0.read().fonts[font_id.0] + .typographic_bounds(glyph_id.0)? + .into()) + } + + fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + self.0.read().advance(font_id, glyph_id) + } + + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { + self.0.read().glyph_for_char(font_id, ch) + } + + fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result> { + self.0.read().raster_bounds(params) + } + + fn rasterize_glyph( + &self, + glyph_id: &RenderGlyphParams, + raster_bounds: Bounds, + ) -> Result<(Size, Vec)> { + self.0.read().rasterize_glyph(glyph_id, raster_bounds) + } + + fn layout_line(&self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout { + self.0.write().layout_line(text, font_size, font_runs) + } +} + +impl MacTextSystemState { + fn add_fonts(&mut self, fonts: Vec>) -> Result<()> { + let fonts = fonts + .into_iter() + .map(|bytes| match bytes { + Cow::Borrowed(embedded_font) => { + let data_provider = unsafe { + core_graphics::data_provider::CGDataProvider::from_slice(embedded_font) + }; + let font = core_graphics::font::CGFont::from_data_provider(data_provider) + .map_err(|()| anyhow!("Could not load an embedded font."))?; + let font = font_kit::loaders::core_text::Font::from_core_graphics_font(font); + Ok(Handle::from_native(&font)) + } + Cow::Owned(bytes) => Ok(Handle::from_memory(Arc::new(bytes), 0)), + }) + .collect::>>()?; + self.memory_source.add_fonts(fonts.into_iter())?; + Ok(()) + } + + fn load_family( + &mut self, + name: &str, + features: &FontFeatures, + fallbacks: Option<&FontFallbacks>, + ) -> Result> { + let name = crate::text_system::font_name_with_fallbacks(name, ".AppleSystemUIFont"); + + let mut font_ids = SmallVec::new(); + let family = self + .memory_source + .select_family_by_name(name) + .or_else(|_| self.system_source.select_family_by_name(name))?; + for font in family.fonts() { + let mut font = font.load()?; + + apply_features_and_fallbacks(&mut font, features, fallbacks)?; + // This block contains a precautionary fix to guard against loading fonts + // that might cause panics due to `.unwrap()`s up the chain. + { + // We use the 'm' character for text measurements in various spots + // (e.g., the editor). However, at time of writing some of those usages + // will panic if the font has no 'm' glyph. + // + // Therefore, we check up front that the font has the necessary glyph. + let has_m_glyph = font.glyph_for_char('m').is_some(); + + // HACK: The 'Segoe Fluent Icons' font does not have an 'm' glyph, + // but we need to be able to load it for rendering Windows icons in + // the Storybook (on macOS). + let is_segoe_fluent_icons = font.full_name() == "Segoe Fluent Icons"; + + if !has_m_glyph && !is_segoe_fluent_icons { + // I spent far too long trying to track down why a font missing the 'm' + // character wasn't loading. This log statement will hopefully save + // someone else from suffering the same fate. + log::warn!( + "font '{}' has no 'm' character and was not loaded", + font.full_name() + ); + continue; + } + } + + // We've seen a number of panics in production caused by calling font.properties() + // which unwraps a downcast to CFNumber. This is an attempt to avoid the panic, + // and to try and identify the incalcitrant font. + let traits = font.native_font().all_traits(); + if unsafe { + !(traits + .get(kCTFontSymbolicTrait) + .downcast::() + .is_some() + && traits + .get(kCTFontWidthTrait) + .downcast::() + .is_some() + && traits + .get(kCTFontWeightTrait) + .downcast::() + .is_some() + && traits + .get(kCTFontSlantTrait) + .downcast::() + .is_some()) + } { + log::error!( + "Failed to read traits for font {:?}", + font.postscript_name().unwrap() + ); + continue; + } + + let font_id = FontId(self.fonts.len()); + font_ids.push(font_id); + let postscript_name = font.postscript_name().unwrap(); + self.font_ids_by_postscript_name + .insert(postscript_name.clone(), font_id); + self.postscript_names_by_font_id + .insert(font_id, postscript_name); + self.fonts.push(font); + } + Ok(font_ids) + } + + fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + Ok(self.fonts[font_id.0].advance(glyph_id.0)?.into()) + } + + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { + self.fonts[font_id.0].glyph_for_char(ch).map(GlyphId) + } + + fn id_for_native_font(&mut self, requested_font: CTFont) -> FontId { + let postscript_name = requested_font.postscript_name(); + if let Some(font_id) = self.font_ids_by_postscript_name.get(&postscript_name) { + *font_id + } else { + let font_id = FontId(self.fonts.len()); + self.font_ids_by_postscript_name + .insert(postscript_name.clone(), font_id); + self.postscript_names_by_font_id + .insert(font_id, postscript_name); + self.fonts + .push(font_kit::font::Font::from_core_graphics_font( + requested_font.copy_to_CGFont(), + )); + font_id + } + } + + fn is_emoji(&self, font_id: FontId) -> bool { + self.postscript_names_by_font_id + .get(&font_id) + .is_some_and(|postscript_name| { + postscript_name == "AppleColorEmoji" || postscript_name == ".AppleColorEmojiUI" + }) + } + + fn raster_bounds(&self, params: &RenderGlyphParams) -> Result> { + let font = &self.fonts[params.font_id.0]; + let scale = Transform2F::from_scale(params.scale_factor); + Ok(font + .raster_bounds( + params.glyph_id.0, + params.font_size.into(), + scale, + HintingOptions::None, + font_kit::canvas::RasterizationOptions::GrayscaleAa, + )? + .into()) + } + + fn rasterize_glyph( + &self, + params: &RenderGlyphParams, + glyph_bounds: Bounds, + ) -> Result<(Size, Vec)> { + if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 { + anyhow::bail!("glyph bounds are empty"); + } else { + // Add an extra pixel when the subpixel variant isn't zero to make room for anti-aliasing. + let mut bitmap_size = glyph_bounds.size; + if params.subpixel_variant.x > 0 { + bitmap_size.width += DevicePixels(1); + } + if params.subpixel_variant.y > 0 { + bitmap_size.height += DevicePixels(1); + } + let bitmap_size = bitmap_size; + + let mut bytes; + let cx; + if params.is_emoji { + bytes = vec![0; bitmap_size.width.0 as usize * 4 * bitmap_size.height.0 as usize]; + cx = CGContext::create_bitmap_context( + Some(bytes.as_mut_ptr() as *mut _), + bitmap_size.width.0 as usize, + bitmap_size.height.0 as usize, + 8, + bitmap_size.width.0 as usize * 4, + &CGColorSpace::create_device_rgb(), + kCGImageAlphaPremultipliedLast, + ); + } else { + bytes = vec![0; bitmap_size.width.0 as usize * bitmap_size.height.0 as usize]; + cx = CGContext::create_bitmap_context( + Some(bytes.as_mut_ptr() as *mut _), + bitmap_size.width.0 as usize, + bitmap_size.height.0 as usize, + 8, + bitmap_size.width.0 as usize, + &CGColorSpace::create_device_gray(), + kCGImageAlphaOnly, + ); + } + + // Move the origin to bottom left and account for scaling, this + // makes drawing text consistent with the font-kit's raster_bounds. + cx.translate( + -glyph_bounds.origin.x.0 as CGFloat, + (glyph_bounds.origin.y.0 + glyph_bounds.size.height.0) as CGFloat, + ); + cx.scale( + params.scale_factor as CGFloat, + params.scale_factor as CGFloat, + ); + + let subpixel_shift = params + .subpixel_variant + .map(|v| v as f32 / SUBPIXEL_VARIANTS_X as f32); + cx.set_text_drawing_mode(CGTextDrawingMode::CGTextFill); + cx.set_gray_fill_color(0.0, 1.0); + cx.set_allows_antialiasing(true); + cx.set_should_antialias(true); + cx.set_allows_font_subpixel_positioning(true); + cx.set_should_subpixel_position_fonts(true); + cx.set_allows_font_subpixel_quantization(false); + cx.set_should_subpixel_quantize_fonts(false); + self.fonts[params.font_id.0] + .native_font() + .clone_with_font_size(f32::from(params.font_size) as CGFloat) + .draw_glyphs( + &[params.glyph_id.0 as CGGlyph], + &[CGPoint::new( + (subpixel_shift.x / params.scale_factor) as CGFloat, + (subpixel_shift.y / params.scale_factor) as CGFloat, + )], + cx, + ); + + if params.is_emoji { + // Convert from RGBA with premultiplied alpha to BGRA with straight alpha. + for pixel in bytes.chunks_exact_mut(4) { + swap_rgba_pa_to_bgra(pixel); + } + } + + Ok((bitmap_size, bytes)) + } + } + + fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout { + const ZWNJ: char = '\u{200C}'; + const ZWNJ_STR: &str = "\u{200C}"; + const ZWNJ_SIZE_16: usize = ZWNJ.len_utf16(); + + self.zwnjs_scratch_space.clear(); + // Construct the attributed string, converting UTF8 ranges to UTF16 ranges. + let mut string = CFMutableAttributedString::new(); + let mut max_ascent = 0.0f32; + let mut max_descent = 0.0f32; + + { + let mut ix_converter = StringIndexConverter::new(&text); + let mut last_font_run = None; + for run in font_runs { + let text = &text[ix_converter.utf8_ix..][..run.len]; + // if the fonts are the same, we need to disconnect the text with a ZWNJ + // to prevent core text from forming ligatures between them + let needs_zwnj = last_font_run.replace(run.font_id) == Some(run.font_id); + + let utf16_start = string.char_len(); // insert at end of string + ix_converter.advance_to_utf8_ix(ix_converter.utf8_ix + run.len); + + // note: replace_str may silently ignore codepoints it dislikes (e.g., BOM at start of string) + string.replace_str(&CFString::new(text), CFRange::init(utf16_start, 0)); + if needs_zwnj { + let zwnjs_pos = string.char_len(); + self.zwnjs_scratch_space.push(zwnjs_pos as usize); + string.replace_str( + &CFString::from_static_string(ZWNJ_STR), + CFRange::init(zwnjs_pos, 0), + ); + } + let utf16_end = string.char_len(); + + let cf_range = CFRange::init(utf16_start, utf16_end - utf16_start); + let font = &self.fonts[run.font_id.0]; + + let font_metrics = font.metrics(); + let font_scale = font_size.0 / font_metrics.units_per_em as f32; + max_ascent = max_ascent.max(font_metrics.ascent * font_scale); + max_descent = max_descent.max(-font_metrics.descent * font_scale); + + unsafe { + string.set_attribute( + cf_range, + kCTFontAttributeName, + &font.native_font().clone_with_font_size(font_size.into()), + ); + } + } + } + // Retrieve the glyphs from the shaped line, converting UTF16 offsets to UTF8 offsets. + let line = CTLine::new_with_attributed_string(string.as_concrete_TypeRef()); + let glyph_runs = line.glyph_runs(); + let mut runs = >::with_capacity(glyph_runs.len() as usize); + let mut ix_converter = StringIndexConverter::new(text); + for run in glyph_runs.into_iter() { + let attributes = run.attributes().unwrap(); + let font = unsafe { + attributes + .get(kCTFontAttributeName) + .downcast::() + .unwrap() + }; + let font_id = self.id_for_native_font(font); + + let mut glyphs = match runs.last_mut() { + Some(run) if run.font_id == font_id => &mut run.glyphs, + _ => { + runs.push(ShapedRun { + font_id, + glyphs: Vec::with_capacity(run.glyph_count().try_into().unwrap_or(0)), + }); + &mut runs.last_mut().unwrap().glyphs + } + }; + for ((&glyph_id, position), &glyph_utf16_ix) in run + .glyphs() + .iter() + .zip(run.positions().iter()) + .zip(run.string_indices().iter()) + { + let mut glyph_utf16_ix = usize::try_from(glyph_utf16_ix).unwrap(); + let r = self + .zwnjs_scratch_space + .binary_search_by(|&it| it.cmp(&glyph_utf16_ix)); + match r { + // this glyph is a ZWNJ, skip it + Ok(_) => continue, + // adjust the index to account for the ZWNJs we've inserted + Err(idx) => glyph_utf16_ix -= idx * ZWNJ_SIZE_16, + } + if ix_converter.utf16_ix > glyph_utf16_ix { + // We cannot reuse current index converter, as it can only seek forward. Restart the search. + ix_converter = StringIndexConverter::new(text); + } + ix_converter.advance_to_utf16_ix(glyph_utf16_ix); + glyphs.push(ShapedGlyph { + id: GlyphId(glyph_id as u32), + position: point(position.x as f32, position.y as f32).map(px), + index: ix_converter.utf8_ix, + is_emoji: self.is_emoji(font_id), + }); + } + } + let typographic_bounds = line.get_typographic_bounds(); + LineLayout { + runs, + font_size, + width: typographic_bounds.width.into(), + ascent: max_ascent.into(), + descent: max_descent.into(), + len: text.len(), + } + } +} + +#[derive(Debug, Clone)] +struct StringIndexConverter<'a> { + text: &'a str, + /// Index in UTF-8 bytes + utf8_ix: usize, + /// Index in UTF-16 code units + utf16_ix: usize, +} + +impl<'a> StringIndexConverter<'a> { + fn new(text: &'a str) -> Self { + Self { + text, + utf8_ix: 0, + utf16_ix: 0, + } + } + + fn advance_to_utf8_ix(&mut self, utf8_target: usize) { + for (ix, c) in self.text[self.utf8_ix..].char_indices() { + if self.utf8_ix + ix >= utf8_target { + self.utf8_ix += ix; + return; + } + self.utf16_ix += c.len_utf16(); + } + self.utf8_ix = self.text.len(); + } + + fn advance_to_utf16_ix(&mut self, utf16_target: usize) { + for (ix, c) in self.text[self.utf8_ix..].char_indices() { + if self.utf16_ix >= utf16_target { + self.utf8_ix += ix; + return; + } + self.utf16_ix += c.len_utf16(); + } + self.utf8_ix = self.text.len(); + } +} + +impl From for FontMetrics { + fn from(metrics: Metrics) -> Self { + FontMetrics { + units_per_em: metrics.units_per_em, + ascent: metrics.ascent, + descent: metrics.descent, + line_gap: metrics.line_gap, + underline_position: metrics.underline_position, + underline_thickness: metrics.underline_thickness, + cap_height: metrics.cap_height, + x_height: metrics.x_height, + bounding_box: metrics.bounding_box.into(), + } + } +} + +impl From for Bounds { + fn from(rect: RectF) -> Self { + Bounds { + origin: point(rect.origin_x(), rect.origin_y()), + size: size(rect.width(), rect.height()), + } + } +} + +impl From for Bounds { + fn from(rect: RectI) -> Self { + Bounds { + origin: point(DevicePixels(rect.origin_x()), DevicePixels(rect.origin_y())), + size: size(DevicePixels(rect.width()), DevicePixels(rect.height())), + } + } +} + +impl From for Size { + fn from(value: Vector2I) -> Self { + size(value.x().into(), value.y().into()) + } +} + +impl From for Bounds { + fn from(rect: RectI) -> Self { + Bounds { + origin: point(rect.origin_x(), rect.origin_y()), + size: size(rect.width(), rect.height()), + } + } +} + +impl From> for Vector2I { + fn from(size: Point) -> Self { + Vector2I::new(size.x as i32, size.y as i32) + } +} + +impl From for Size { + fn from(vec: Vector2F) -> Self { + size(vec.x(), vec.y()) + } +} + +impl From for FontkitWeight { + fn from(value: FontWeight) -> Self { + FontkitWeight(value.0) + } +} + +impl From for FontkitStyle { + fn from(style: FontStyle) -> Self { + match style { + FontStyle::Normal => FontkitStyle::Normal, + FontStyle::Italic => FontkitStyle::Italic, + FontStyle::Oblique => FontkitStyle::Oblique, + } + } +} + +// Some fonts may have no attributes despite `core_text` requiring them (and panicking). +// This is the same version as `core_text` has without `expect` calls. +mod lenient_font_attributes { + use core_foundation::{ + base::{CFRetain, CFType, TCFType}, + string::{CFString, CFStringRef}, + }; + use core_text::font_descriptor::{ + CTFontDescriptor, CTFontDescriptorCopyAttribute, kCTFontFamilyNameAttribute, + }; + + pub fn family_name(descriptor: &CTFontDescriptor) -> Option { + unsafe { get_string_attribute(descriptor, kCTFontFamilyNameAttribute) } + } + + fn get_string_attribute( + descriptor: &CTFontDescriptor, + attribute: CFStringRef, + ) -> Option { + unsafe { + let value = CTFontDescriptorCopyAttribute(descriptor.as_concrete_TypeRef(), attribute); + if value.is_null() { + return None; + } + + let value = CFType::wrap_under_create_rule(value); + assert!(value.instance_of::()); + let s = wrap_under_get_rule(value.as_CFTypeRef() as CFStringRef); + Some(s.to_string()) + } + } + + unsafe fn wrap_under_get_rule(reference: CFStringRef) -> CFString { + unsafe { + assert!(!reference.is_null(), "Attempted to create a NULL object."); + let reference = CFRetain(reference as *const ::std::os::raw::c_void) as CFStringRef; + TCFType::wrap_under_create_rule(reference) + } + } +} + +#[cfg(test)] +mod tests { + use crate::{FontRun, GlyphId, MacTextSystem, PlatformTextSystem, font, px}; + + #[test] + fn test_layout_line_bom_char() { + let fonts = MacTextSystem::new(); + let font_id = fonts.font_id(&font("Helvetica")).unwrap(); + let line = "\u{feff}"; + let mut style = FontRun { + font_id, + len: line.len(), + }; + + let layout = fonts.layout_line(line, px(16.), &[style]); + assert_eq!(layout.len, line.len()); + assert!(layout.runs.is_empty()); + + let line = "a\u{feff}b"; + style.len = line.len(); + let layout = fonts.layout_line(line, px(16.), &[style]); + assert_eq!(layout.len, line.len()); + assert_eq!(layout.runs.len(), 1); + assert_eq!(layout.runs[0].glyphs.len(), 2); + assert_eq!(layout.runs[0].glyphs[0].id, GlyphId(68u32)); // a + // There's no glyph for \u{feff} + assert_eq!(layout.runs[0].glyphs[1].id, GlyphId(69u32)); // b + + let line = "\u{feff}ab"; + let font_runs = &[ + FontRun { + len: "\u{feff}".len(), + font_id, + }, + FontRun { + len: "ab".len(), + font_id, + }, + ]; + let layout = fonts.layout_line(line, px(16.), font_runs); + assert_eq!(layout.len, line.len()); + assert_eq!(layout.runs.len(), 1); + assert_eq!(layout.runs[0].glyphs.len(), 2); + // There's no glyph for \u{feff} + assert_eq!(layout.runs[0].glyphs[0].id, GlyphId(68u32)); // a + assert_eq!(layout.runs[0].glyphs[1].id, GlyphId(69u32)); // b + } + + #[test] + fn test_layout_line_zwnj_insertion() { + let fonts = MacTextSystem::new(); + let font_id = fonts.font_id(&font("Helvetica")).unwrap(); + + let text = "hello world"; + let font_runs = &[ + FontRun { font_id, len: 5 }, // "hello" + FontRun { font_id, len: 6 }, // " world" + ]; + + let layout = fonts.layout_line(text, px(16.), font_runs); + assert_eq!(layout.len, text.len()); + + for run in &layout.runs { + for glyph in &run.glyphs { + assert!( + glyph.index < text.len(), + "Glyph index {} is out of bounds for text length {}", + glyph.index, + text.len() + ); + } + } + + // Test with different font runs - should not insert ZWNJ + let font_id2 = fonts.font_id(&font("Times")).unwrap_or(font_id); + let font_runs_different = &[ + FontRun { font_id, len: 5 }, // "hello" + // " world" + FontRun { + font_id: font_id2, + len: 6, + }, + ]; + + let layout2 = fonts.layout_line(text, px(16.), font_runs_different); + assert_eq!(layout2.len, text.len()); + + for run in &layout2.runs { + for glyph in &run.glyphs { + assert!( + glyph.index < text.len(), + "Glyph index {} is out of bounds for text length {}", + glyph.index, + text.len() + ); + } + } + } + + #[test] + fn test_layout_line_zwnj_edge_cases() { + let fonts = MacTextSystem::new(); + let font_id = fonts.font_id(&font("Helvetica")).unwrap(); + + let text = "hello"; + let font_runs = &[FontRun { font_id, len: 5 }]; + let layout = fonts.layout_line(text, px(16.), font_runs); + assert_eq!(layout.len, text.len()); + + let text = "abc"; + let font_runs = &[ + FontRun { font_id, len: 1 }, // "a" + FontRun { font_id, len: 1 }, // "b" + FontRun { font_id, len: 1 }, // "c" + ]; + let layout = fonts.layout_line(text, px(16.), font_runs); + assert_eq!(layout.len, text.len()); + + for run in &layout.runs { + for glyph in &run.glyphs { + assert!( + glyph.index < text.len(), + "Glyph index {} is out of bounds for text length {}", + glyph.index, + text.len() + ); + } + } + + // Test with empty text + let text = ""; + let font_runs = &[]; + let layout = fonts.layout_line(text, px(16.), font_runs); + assert_eq!(layout.len, 0); + assert!(layout.runs.is_empty()); + } +} diff --git a/third_party/gpui/src/platform/mac/window.rs b/third_party/gpui/src/platform/mac/window.rs new file mode 100644 index 0000000..95efffa --- /dev/null +++ b/third_party/gpui/src/platform/mac/window.rs @@ -0,0 +1,2647 @@ +use super::{BoolExt, MacDisplay, NSRange, NSStringExt, ns_string, renderer}; +use crate::{ + AnyWindowHandle, Bounds, Capslock, DisplayLink, ExternalPaths, FileDropEvent, + ForegroundExecutor, KeyDownEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, + MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, + PlatformInput, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions, + SharedString, Size, SystemWindowTab, Timer, WindowAppearance, WindowBackgroundAppearance, + WindowBounds, WindowControlArea, WindowKind, WindowParams, dispatch_get_main_queue, + dispatch_sys::dispatch_async_f, platform::PlatformInputHandler, point, px, size, +}; +use block::ConcreteBlock; +use cocoa::{ + appkit::{ + NSAppKitVersionNumber, NSAppKitVersionNumber12_0, NSApplication, NSBackingStoreBuffered, + NSColor, NSEvent, NSEventModifierFlags, NSFilenamesPboardType, NSPasteboard, NSScreen, + NSView, NSViewHeightSizable, NSViewWidthSizable, NSVisualEffectMaterial, + NSVisualEffectState, NSVisualEffectView, NSWindow, NSWindowButton, + NSWindowCollectionBehavior, NSWindowOcclusionState, NSWindowOrderingMode, + NSWindowStyleMask, NSWindowTitleVisibility, + }, + base::{id, nil}, + foundation::{ + NSArray, NSAutoreleasePool, NSDictionary, NSFastEnumeration, NSInteger, NSNotFound, + NSOperatingSystemVersion, NSPoint, NSProcessInfo, NSRect, NSSize, NSString, NSUInteger, + NSUserDefaults, + }, +}; + +use core_graphics::display::{CGDirectDisplayID, CGPoint, CGRect}; +use ctor::ctor; +use futures::channel::oneshot; +use objc::{ + class, + declare::ClassDecl, + msg_send, + runtime::{BOOL, Class, NO, Object, Protocol, Sel, YES}, + sel, sel_impl, +}; +use parking_lot::Mutex; +use raw_window_handle as rwh; +use smallvec::SmallVec; +use std::{ + cell::Cell, + ffi::{CStr, c_void}, + mem, + ops::Range, + path::PathBuf, + ptr::{self, NonNull}, + rc::Rc, + sync::{Arc, Weak}, + time::Duration, +}; +use util::ResultExt; + +const WINDOW_STATE_IVAR: &str = "windowState"; + +static mut WINDOW_CLASS: *const Class = ptr::null(); +static mut PANEL_CLASS: *const Class = ptr::null(); +static mut VIEW_CLASS: *const Class = ptr::null(); +static mut BLURRED_VIEW_CLASS: *const Class = ptr::null(); + +#[allow(non_upper_case_globals)] +const NSWindowStyleMaskNonactivatingPanel: NSWindowStyleMask = + NSWindowStyleMask::from_bits_retain(1 << 7); +#[allow(non_upper_case_globals)] +const NSNormalWindowLevel: NSInteger = 0; +#[allow(non_upper_case_globals)] +const NSPopUpWindowLevel: NSInteger = 101; +#[allow(non_upper_case_globals)] +const NSTrackingMouseEnteredAndExited: NSUInteger = 0x01; +#[allow(non_upper_case_globals)] +const NSTrackingMouseMoved: NSUInteger = 0x02; +#[allow(non_upper_case_globals)] +const NSTrackingActiveAlways: NSUInteger = 0x80; +#[allow(non_upper_case_globals)] +const NSTrackingInVisibleRect: NSUInteger = 0x200; +#[allow(non_upper_case_globals)] +const NSWindowAnimationBehaviorUtilityWindow: NSInteger = 4; +#[allow(non_upper_case_globals)] +const NSViewLayerContentsRedrawDuringViewResize: NSInteger = 2; +// https://developer.apple.com/documentation/appkit/nsdragoperation +type NSDragOperation = NSUInteger; +#[allow(non_upper_case_globals)] +const NSDragOperationNone: NSDragOperation = 0; +#[allow(non_upper_case_globals)] +const NSDragOperationCopy: NSDragOperation = 1; +#[derive(PartialEq)] +pub enum UserTabbingPreference { + Never, + Always, + InFullScreen, +} + +#[link(name = "CoreGraphics", kind = "framework")] +unsafe extern "C" { + // Widely used private APIs; Apple uses them for their Terminal.app. + fn CGSMainConnectionID() -> id; + fn CGSSetWindowBackgroundBlurRadius( + connection_id: id, + window_id: NSInteger, + radius: i64, + ) -> i32; +} + +#[ctor] +unsafe fn build_classes() { + unsafe { + WINDOW_CLASS = build_window_class("GPUIWindow", class!(NSWindow)); + PANEL_CLASS = build_window_class("GPUIPanel", class!(NSPanel)); + VIEW_CLASS = { + let mut decl = ClassDecl::new("GPUIView", class!(NSView)).unwrap(); + decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR); + unsafe { + decl.add_method(sel!(dealloc), dealloc_view as extern "C" fn(&Object, Sel)); + + decl.add_method( + sel!(performKeyEquivalent:), + handle_key_equivalent as extern "C" fn(&Object, Sel, id) -> BOOL, + ); + decl.add_method( + sel!(keyDown:), + handle_key_down as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(keyUp:), + handle_key_up as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(mouseDown:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(mouseUp:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(rightMouseDown:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(rightMouseUp:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(otherMouseDown:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(otherMouseUp:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(mouseMoved:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(mouseExited:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(mouseDragged:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(scrollWheel:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(swipeWithEvent:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(flagsChanged:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + + decl.add_method( + sel!(makeBackingLayer), + make_backing_layer as extern "C" fn(&Object, Sel) -> id, + ); + + decl.add_protocol(Protocol::get("CALayerDelegate").unwrap()); + decl.add_method( + sel!(viewDidChangeBackingProperties), + view_did_change_backing_properties as extern "C" fn(&Object, Sel), + ); + decl.add_method( + sel!(setFrameSize:), + set_frame_size as extern "C" fn(&Object, Sel, NSSize), + ); + decl.add_method( + sel!(displayLayer:), + display_layer as extern "C" fn(&Object, Sel, id), + ); + + decl.add_protocol(Protocol::get("NSTextInputClient").unwrap()); + decl.add_method( + sel!(validAttributesForMarkedText), + valid_attributes_for_marked_text as extern "C" fn(&Object, Sel) -> id, + ); + decl.add_method( + sel!(hasMarkedText), + has_marked_text as extern "C" fn(&Object, Sel) -> BOOL, + ); + decl.add_method( + sel!(markedRange), + marked_range as extern "C" fn(&Object, Sel) -> NSRange, + ); + decl.add_method( + sel!(selectedRange), + selected_range as extern "C" fn(&Object, Sel) -> NSRange, + ); + decl.add_method( + sel!(firstRectForCharacterRange:actualRange:), + first_rect_for_character_range + as extern "C" fn(&Object, Sel, NSRange, id) -> NSRect, + ); + decl.add_method( + sel!(insertText:replacementRange:), + insert_text as extern "C" fn(&Object, Sel, id, NSRange), + ); + decl.add_method( + sel!(setMarkedText:selectedRange:replacementRange:), + set_marked_text as extern "C" fn(&Object, Sel, id, NSRange, NSRange), + ); + decl.add_method(sel!(unmarkText), unmark_text as extern "C" fn(&Object, Sel)); + decl.add_method( + sel!(attributedSubstringForProposedRange:actualRange:), + attributed_substring_for_proposed_range + as extern "C" fn(&Object, Sel, NSRange, *mut c_void) -> id, + ); + decl.add_method( + sel!(viewDidChangeEffectiveAppearance), + view_did_change_effective_appearance as extern "C" fn(&Object, Sel), + ); + + // Suppress beep on keystrokes with modifier keys. + decl.add_method( + sel!(doCommandBySelector:), + do_command_by_selector as extern "C" fn(&Object, Sel, Sel), + ); + + decl.add_method( + sel!(acceptsFirstMouse:), + accepts_first_mouse as extern "C" fn(&Object, Sel, id) -> BOOL, + ); + + decl.add_method( + sel!(characterIndexForPoint:), + character_index_for_point as extern "C" fn(&Object, Sel, NSPoint) -> u64, + ); + } + decl.register() + }; + BLURRED_VIEW_CLASS = { + let mut decl = ClassDecl::new("BlurredView", class!(NSVisualEffectView)).unwrap(); + unsafe { + decl.add_method( + sel!(initWithFrame:), + blurred_view_init_with_frame as extern "C" fn(&Object, Sel, NSRect) -> id, + ); + decl.add_method( + sel!(updateLayer), + blurred_view_update_layer as extern "C" fn(&Object, Sel), + ); + decl.register() + } + }; + } +} + +pub(crate) fn convert_mouse_position(position: NSPoint, window_height: Pixels) -> Point { + point( + px(position.x as f32), + // macOS screen coordinates are relative to bottom left + window_height - px(position.y as f32), + ) +} + +unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const Class { + unsafe { + let mut decl = ClassDecl::new(name, superclass).unwrap(); + decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR); + decl.add_method(sel!(dealloc), dealloc_window as extern "C" fn(&Object, Sel)); + + decl.add_method( + sel!(canBecomeMainWindow), + yes as extern "C" fn(&Object, Sel) -> BOOL, + ); + decl.add_method( + sel!(canBecomeKeyWindow), + yes as extern "C" fn(&Object, Sel) -> BOOL, + ); + decl.add_method( + sel!(windowDidResize:), + window_did_resize as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidChangeOcclusionState:), + window_did_change_occlusion_state as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowWillEnterFullScreen:), + window_will_enter_fullscreen as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowWillExitFullScreen:), + window_will_exit_fullscreen as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidMove:), + window_did_move as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidChangeScreen:), + window_did_change_screen as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidBecomeKey:), + window_did_change_key_status as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidResignKey:), + window_did_change_key_status as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowShouldClose:), + window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL, + ); + + decl.add_method(sel!(close), close_window as extern "C" fn(&Object, Sel)); + + decl.add_method( + sel!(draggingEntered:), + dragging_entered as extern "C" fn(&Object, Sel, id) -> NSDragOperation, + ); + decl.add_method( + sel!(draggingUpdated:), + dragging_updated as extern "C" fn(&Object, Sel, id) -> NSDragOperation, + ); + decl.add_method( + sel!(draggingExited:), + dragging_exited as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(performDragOperation:), + perform_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL, + ); + decl.add_method( + sel!(concludeDragOperation:), + conclude_drag_operation as extern "C" fn(&Object, Sel, id), + ); + + decl.add_method( + sel!(addTitlebarAccessoryViewController:), + add_titlebar_accessory_view_controller as extern "C" fn(&Object, Sel, id), + ); + + decl.add_method( + sel!(moveTabToNewWindow:), + move_tab_to_new_window as extern "C" fn(&Object, Sel, id), + ); + + decl.add_method( + sel!(mergeAllWindows:), + merge_all_windows as extern "C" fn(&Object, Sel, id), + ); + + decl.add_method( + sel!(selectNextTab:), + select_next_tab as extern "C" fn(&Object, Sel, id), + ); + + decl.add_method( + sel!(selectPreviousTab:), + select_previous_tab as extern "C" fn(&Object, Sel, id), + ); + + decl.add_method( + sel!(toggleTabBar:), + toggle_tab_bar as extern "C" fn(&Object, Sel, id), + ); + + decl.register() + } +} + +struct MacWindowState { + handle: AnyWindowHandle, + executor: ForegroundExecutor, + native_window: id, + native_view: NonNull, + blurred_view: Option, + display_link: Option, + renderer: renderer::Renderer, + request_frame_callback: Option>, + event_callback: Option crate::DispatchEventResult>>, + activate_callback: Option>, + resize_callback: Option, f32)>>, + moved_callback: Option>, + should_close_callback: Option bool>>, + close_callback: Option>, + appearance_changed_callback: Option>, + input_handler: Option, + last_key_equivalent: Option, + synthetic_drag_counter: usize, + traffic_light_position: Option>, + transparent_titlebar: bool, + previous_modifiers_changed_event: Option, + keystroke_for_do_command: Option, + do_command_handled: Option, + external_files_dragged: bool, + // Whether the next left-mouse click is also the focusing click. + first_mouse: bool, + fullscreen_restore_bounds: Bounds, + move_tab_to_new_window_callback: Option>, + merge_all_windows_callback: Option>, + select_next_tab_callback: Option>, + select_previous_tab_callback: Option>, + toggle_tab_bar_callback: Option>, + activated_least_once: bool, +} + +impl MacWindowState { + fn move_traffic_light(&self) { + if let Some(traffic_light_position) = self.traffic_light_position { + if self.is_fullscreen() { + // Moving traffic lights while fullscreen doesn't work, + // see https://github.com/zed-industries/zed/issues/4712 + return; + } + + let titlebar_height = self.titlebar_height(); + + unsafe { + let close_button: id = msg_send![ + self.native_window, + standardWindowButton: NSWindowButton::NSWindowCloseButton + ]; + let min_button: id = msg_send![ + self.native_window, + standardWindowButton: NSWindowButton::NSWindowMiniaturizeButton + ]; + let zoom_button: id = msg_send![ + self.native_window, + standardWindowButton: NSWindowButton::NSWindowZoomButton + ]; + + let mut close_button_frame: CGRect = msg_send![close_button, frame]; + let mut min_button_frame: CGRect = msg_send![min_button, frame]; + let mut zoom_button_frame: CGRect = msg_send![zoom_button, frame]; + let mut origin = point( + traffic_light_position.x, + titlebar_height + - traffic_light_position.y + - px(close_button_frame.size.height as f32), + ); + let button_spacing = + px((min_button_frame.origin.x - close_button_frame.origin.x) as f32); + + close_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into()); + let _: () = msg_send![close_button, setFrame: close_button_frame]; + origin.x += button_spacing; + + min_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into()); + let _: () = msg_send![min_button, setFrame: min_button_frame]; + origin.x += button_spacing; + + zoom_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into()); + let _: () = msg_send![zoom_button, setFrame: zoom_button_frame]; + origin.x += button_spacing; + } + } + } + + fn start_display_link(&mut self) { + self.stop_display_link(); + unsafe { + if !self + .native_window + .occlusionState() + .contains(NSWindowOcclusionState::NSWindowOcclusionStateVisible) + { + return; + } + } + let display_id = unsafe { display_id_for_screen(self.native_window.screen()) }; + if let Some(mut display_link) = + DisplayLink::new(display_id, self.native_view.as_ptr() as *mut c_void, step).log_err() + { + display_link.start().log_err(); + self.display_link = Some(display_link); + } + } + + fn stop_display_link(&mut self) { + self.display_link = None; + } + + fn is_maximized(&self) -> bool { + unsafe { + let bounds = self.bounds(); + let screen_size = self.native_window.screen().visibleFrame().into(); + bounds.size == screen_size + } + } + + fn is_fullscreen(&self) -> bool { + unsafe { + let style_mask = self.native_window.styleMask(); + style_mask.contains(NSWindowStyleMask::NSFullScreenWindowMask) + } + } + + fn bounds(&self) -> Bounds { + let mut window_frame = unsafe { NSWindow::frame(self.native_window) }; + let screen = unsafe { NSWindow::screen(self.native_window) }; + if screen == nil { + return Bounds::new(point(px(0.), px(0.)), crate::DEFAULT_WINDOW_SIZE); + } + let screen_frame = unsafe { NSScreen::frame(screen) }; + + // Flip the y coordinate to be top-left origin + window_frame.origin.y = + screen_frame.size.height - window_frame.origin.y - window_frame.size.height; + + Bounds::new( + point( + px((window_frame.origin.x - screen_frame.origin.x) as f32), + px((window_frame.origin.y + screen_frame.origin.y) as f32), + ), + size( + px(window_frame.size.width as f32), + px(window_frame.size.height as f32), + ), + ) + } + + fn content_size(&self) -> Size { + let NSSize { width, height, .. } = + unsafe { NSView::frame(self.native_window.contentView()) }.size; + size(px(width as f32), px(height as f32)) + } + + fn scale_factor(&self) -> f32 { + get_scale_factor(self.native_window) + } + + fn titlebar_height(&self) -> Pixels { + unsafe { + let frame = NSWindow::frame(self.native_window); + let content_layout_rect: CGRect = msg_send![self.native_window, contentLayoutRect]; + px((frame.size.height - content_layout_rect.size.height) as f32) + } + } + + fn window_bounds(&self) -> WindowBounds { + if self.is_fullscreen() { + WindowBounds::Fullscreen(self.fullscreen_restore_bounds) + } else { + WindowBounds::Windowed(self.bounds()) + } + } +} + +unsafe impl Send for MacWindowState {} + +pub(crate) struct MacWindow(Arc>); + +impl MacWindow { + pub fn open( + handle: AnyWindowHandle, + WindowParams { + bounds, + titlebar, + kind, + is_movable, + is_resizable, + is_minimizable, + focus, + show, + display_id, + window_min_size, + tabbing_identifier, + }: WindowParams, + executor: ForegroundExecutor, + renderer_context: renderer::Context, + ) -> Self { + unsafe { + let pool = NSAutoreleasePool::new(nil); + + let allows_automatic_window_tabbing = tabbing_identifier.is_some(); + if allows_automatic_window_tabbing { + let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: YES]; + } else { + let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: NO]; + } + + let mut style_mask; + if let Some(titlebar) = titlebar.as_ref() { + style_mask = + NSWindowStyleMask::NSClosableWindowMask | NSWindowStyleMask::NSTitledWindowMask; + + if is_resizable { + style_mask |= NSWindowStyleMask::NSResizableWindowMask; + } + + if is_minimizable { + style_mask |= NSWindowStyleMask::NSMiniaturizableWindowMask; + } + + if titlebar.appears_transparent { + style_mask |= NSWindowStyleMask::NSFullSizeContentViewWindowMask; + } + } else { + style_mask = NSWindowStyleMask::NSTitledWindowMask + | NSWindowStyleMask::NSFullSizeContentViewWindowMask; + } + + let native_window: id = match kind { + WindowKind::Normal | WindowKind::Floating => msg_send![WINDOW_CLASS, alloc], + WindowKind::PopUp => { + style_mask |= NSWindowStyleMaskNonactivatingPanel; + msg_send![PANEL_CLASS, alloc] + } + }; + + let display = display_id + .and_then(MacDisplay::find_by_id) + .unwrap_or_else(MacDisplay::primary); + + let mut target_screen = nil; + let mut screen_frame = None; + + let screens = NSScreen::screens(nil); + let count: u64 = cocoa::foundation::NSArray::count(screens); + for i in 0..count { + let screen = cocoa::foundation::NSArray::objectAtIndex(screens, i); + let frame = NSScreen::frame(screen); + let display_id = display_id_for_screen(screen); + if display_id == display.0 { + screen_frame = Some(frame); + target_screen = screen; + } + } + + let screen_frame = screen_frame.unwrap_or_else(|| { + let screen = NSScreen::mainScreen(nil); + target_screen = screen; + NSScreen::frame(screen) + }); + + let window_rect = NSRect::new( + NSPoint::new( + screen_frame.origin.x + bounds.origin.x.0 as f64, + screen_frame.origin.y + + (display.bounds().size.height - bounds.origin.y).0 as f64, + ), + NSSize::new(bounds.size.width.0 as f64, bounds.size.height.0 as f64), + ); + + let native_window = native_window.initWithContentRect_styleMask_backing_defer_screen_( + window_rect, + style_mask, + NSBackingStoreBuffered, + NO, + target_screen, + ); + assert!(!native_window.is_null()); + let () = msg_send![ + native_window, + registerForDraggedTypes: + NSArray::arrayWithObject(nil, NSFilenamesPboardType) + ]; + let () = msg_send![ + native_window, + setReleasedWhenClosed: NO + ]; + + let content_view = native_window.contentView(); + let native_view: id = msg_send![VIEW_CLASS, alloc]; + let native_view = NSView::initWithFrame_(native_view, NSView::bounds(content_view)); + assert!(!native_view.is_null()); + + let mut window = Self(Arc::new(Mutex::new(MacWindowState { + handle, + executor, + native_window, + native_view: NonNull::new_unchecked(native_view), + blurred_view: None, + display_link: None, + renderer: renderer::new_renderer( + renderer_context, + native_window as *mut _, + native_view as *mut _, + bounds.size.map(|pixels| pixels.0), + false, + ), + request_frame_callback: None, + event_callback: None, + activate_callback: None, + resize_callback: None, + moved_callback: None, + should_close_callback: None, + close_callback: None, + appearance_changed_callback: None, + input_handler: None, + last_key_equivalent: None, + synthetic_drag_counter: 0, + traffic_light_position: titlebar + .as_ref() + .and_then(|titlebar| titlebar.traffic_light_position), + transparent_titlebar: titlebar + .as_ref() + .is_none_or(|titlebar| titlebar.appears_transparent), + previous_modifiers_changed_event: None, + keystroke_for_do_command: None, + do_command_handled: None, + external_files_dragged: false, + first_mouse: false, + fullscreen_restore_bounds: Bounds::default(), + move_tab_to_new_window_callback: None, + merge_all_windows_callback: None, + select_next_tab_callback: None, + select_previous_tab_callback: None, + toggle_tab_bar_callback: None, + activated_least_once: false, + }))); + + (*native_window).set_ivar( + WINDOW_STATE_IVAR, + Arc::into_raw(window.0.clone()) as *const c_void, + ); + native_window.setDelegate_(native_window); + (*native_view).set_ivar( + WINDOW_STATE_IVAR, + Arc::into_raw(window.0.clone()) as *const c_void, + ); + + if let Some(title) = titlebar + .as_ref() + .and_then(|t| t.title.as_ref().map(AsRef::as_ref)) + { + window.set_title(title); + } + + native_window.setMovable_(is_movable as BOOL); + + if let Some(window_min_size) = window_min_size { + native_window.setContentMinSize_(NSSize { + width: window_min_size.width.to_f64(), + height: window_min_size.height.to_f64(), + }); + } + + if titlebar.is_none_or(|titlebar| titlebar.appears_transparent) { + native_window.setTitlebarAppearsTransparent_(YES); + native_window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden); + } + + native_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable); + native_view.setWantsBestResolutionOpenGLSurface_(YES); + + // From winit crate: On Mojave, views automatically become layer-backed shortly after + // being added to a native_window. Changing the layer-backedness of a view breaks the + // association between the view and its associated OpenGL context. To work around this, + // on we explicitly make the view layer-backed up front so that AppKit doesn't do it + // itself and break the association with its context. + native_view.setWantsLayer(YES); + let _: () = msg_send![ + native_view, + setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize + ]; + + content_view.addSubview_(native_view.autorelease()); + native_window.makeFirstResponder_(native_view); + + match kind { + WindowKind::Normal | WindowKind::Floating => { + native_window.setLevel_(NSNormalWindowLevel); + native_window.setAcceptsMouseMovedEvents_(YES); + + if let Some(tabbing_identifier) = tabbing_identifier { + let tabbing_id = NSString::alloc(nil).init_str(tabbing_identifier.as_str()); + let _: () = msg_send![native_window, setTabbingIdentifier: tabbing_id]; + } else { + let _: () = msg_send![native_window, setTabbingIdentifier:nil]; + } + } + WindowKind::PopUp => { + // Use a tracking area to allow receiving MouseMoved events even when + // the window or application aren't active, which is often the case + // e.g. for notification windows. + let tracking_area: id = msg_send![class!(NSTrackingArea), alloc]; + let _: () = msg_send![ + tracking_area, + initWithRect: NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.)) + options: NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways | NSTrackingInVisibleRect + owner: native_view + userInfo: nil + ]; + let _: () = + msg_send![native_view, addTrackingArea: tracking_area.autorelease()]; + + native_window.setLevel_(NSPopUpWindowLevel); + let _: () = msg_send![ + native_window, + setAnimationBehavior: NSWindowAnimationBehaviorUtilityWindow + ]; + native_window.setCollectionBehavior_( + NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces | + NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary + ); + } + } + + let app = NSApplication::sharedApplication(nil); + let main_window: id = msg_send![app, mainWindow]; + if allows_automatic_window_tabbing + && !main_window.is_null() + && main_window != native_window + { + let main_window_is_fullscreen = main_window + .styleMask() + .contains(NSWindowStyleMask::NSFullScreenWindowMask); + let user_tabbing_preference = Self::get_user_tabbing_preference() + .unwrap_or(UserTabbingPreference::InFullScreen); + let should_add_as_tab = user_tabbing_preference == UserTabbingPreference::Always + || user_tabbing_preference == UserTabbingPreference::InFullScreen + && main_window_is_fullscreen; + + if should_add_as_tab { + let main_window_can_tab: BOOL = + msg_send![main_window, respondsToSelector: sel!(addTabbedWindow:ordered:)]; + let main_window_visible: BOOL = msg_send![main_window, isVisible]; + + if main_window_can_tab == YES && main_window_visible == YES { + let _: () = msg_send![main_window, addTabbedWindow: native_window ordered: NSWindowOrderingMode::NSWindowAbove]; + + // Ensure the window is visible immediately after adding the tab, since the tab bar is updated with a new entry at this point. + // Note: Calling orderFront here can break fullscreen mode (makes fullscreen windows exit fullscreen), so only do this if the main window is not fullscreen. + if !main_window_is_fullscreen { + let _: () = msg_send![native_window, orderFront: nil]; + } + } + } + } + + if focus && show { + native_window.makeKeyAndOrderFront_(nil); + } else if show { + native_window.orderFront_(nil); + } + + // Set the initial position of the window to the specified origin. + // Although we already specified the position using `initWithContentRect_styleMask_backing_defer_screen_`, + // the window position might be incorrect if the main screen (the screen that contains the window that has focus) + // is different from the primary screen. + NSWindow::setFrameTopLeftPoint_(native_window, window_rect.origin); + window.0.lock().move_traffic_light(); + + pool.drain(); + + window + } + } + + pub fn active_window() -> Option { + unsafe { + let app = NSApplication::sharedApplication(nil); + let main_window: id = msg_send![app, mainWindow]; + if main_window.is_null() { + return None; + } + + if msg_send![main_window, isKindOfClass: WINDOW_CLASS] { + let handle = get_window_state(&*main_window).lock().handle; + Some(handle) + } else { + None + } + } + } + + pub fn ordered_windows() -> Vec { + unsafe { + let app = NSApplication::sharedApplication(nil); + let windows: id = msg_send![app, orderedWindows]; + let count: NSUInteger = msg_send![windows, count]; + + let mut window_handles = Vec::new(); + for i in 0..count { + let window: id = msg_send![windows, objectAtIndex:i]; + if msg_send![window, isKindOfClass: WINDOW_CLASS] { + let handle = get_window_state(&*window).lock().handle; + window_handles.push(handle); + } + } + + window_handles + } + } + + pub fn get_user_tabbing_preference() -> Option { + unsafe { + let defaults: id = NSUserDefaults::standardUserDefaults(); + let domain = NSString::alloc(nil).init_str("NSGlobalDomain"); + let key = NSString::alloc(nil).init_str("AppleWindowTabbingMode"); + + let dict: id = msg_send![defaults, persistentDomainForName: domain]; + let value: id = if !dict.is_null() { + msg_send![dict, objectForKey: key] + } else { + nil + }; + + let value_str = if !value.is_null() { + CStr::from_ptr(NSString::UTF8String(value)).to_string_lossy() + } else { + "".into() + }; + + match value_str.as_ref() { + "manual" => Some(UserTabbingPreference::Never), + "always" => Some(UserTabbingPreference::Always), + _ => Some(UserTabbingPreference::InFullScreen), + } + } + } +} + +impl Drop for MacWindow { + fn drop(&mut self) { + let mut this = self.0.lock(); + this.renderer.destroy(); + let window = this.native_window; + this.display_link.take(); + unsafe { + this.native_window.setDelegate_(nil); + } + this.input_handler.take(); + this.executor + .spawn(async move { + unsafe { + window.close(); + window.autorelease(); + } + }) + .detach(); + } +} + +impl PlatformWindow for MacWindow { + fn bounds(&self) -> Bounds { + self.0.as_ref().lock().bounds() + } + + fn window_bounds(&self) -> WindowBounds { + self.0.as_ref().lock().window_bounds() + } + + fn is_maximized(&self) -> bool { + self.0.as_ref().lock().is_maximized() + } + + fn content_size(&self) -> Size { + self.0.as_ref().lock().content_size() + } + + fn resize(&mut self, size: Size) { + let this = self.0.lock(); + let window = this.native_window; + this.executor + .spawn(async move { + unsafe { + window.setContentSize_(NSSize { + width: size.width.0 as f64, + height: size.height.0 as f64, + }); + } + }) + .detach(); + } + + fn merge_all_windows(&self) { + let native_window = self.0.lock().native_window; + unsafe extern "C" fn merge_windows_async(context: *mut std::ffi::c_void) { + let native_window = context as id; + let _: () = msg_send![native_window, mergeAllWindows:nil]; + } + + unsafe { + dispatch_async_f( + dispatch_get_main_queue(), + native_window as *mut std::ffi::c_void, + Some(merge_windows_async), + ); + } + } + + fn move_tab_to_new_window(&self) { + let native_window = self.0.lock().native_window; + unsafe extern "C" fn move_tab_async(context: *mut std::ffi::c_void) { + let native_window = context as id; + let _: () = msg_send![native_window, moveTabToNewWindow:nil]; + let _: () = msg_send![native_window, makeKeyAndOrderFront: nil]; + } + + unsafe { + dispatch_async_f( + dispatch_get_main_queue(), + native_window as *mut std::ffi::c_void, + Some(move_tab_async), + ); + } + } + + fn toggle_window_tab_overview(&self) { + let native_window = self.0.lock().native_window; + unsafe { + let _: () = msg_send![native_window, toggleTabOverview:nil]; + } + } + + fn set_tabbing_identifier(&self, tabbing_identifier: Option) { + let native_window = self.0.lock().native_window; + unsafe { + let allows_automatic_window_tabbing = tabbing_identifier.is_some(); + if allows_automatic_window_tabbing { + let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: YES]; + } else { + let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: NO]; + } + + if let Some(tabbing_identifier) = tabbing_identifier { + let tabbing_id = NSString::alloc(nil).init_str(tabbing_identifier.as_str()); + let _: () = msg_send![native_window, setTabbingIdentifier: tabbing_id]; + } else { + let _: () = msg_send![native_window, setTabbingIdentifier:nil]; + } + } + } + + fn scale_factor(&self) -> f32 { + self.0.as_ref().lock().scale_factor() + } + + fn appearance(&self) -> WindowAppearance { + unsafe { + let appearance: id = msg_send![self.0.lock().native_window, effectiveAppearance]; + WindowAppearance::from_native(appearance) + } + } + + fn display(&self) -> Option> { + unsafe { + let screen = self.0.lock().native_window.screen(); + if screen.is_null() { + return None; + } + let device_description: id = msg_send![screen, deviceDescription]; + let screen_number: id = NSDictionary::valueForKey_( + device_description, + NSString::alloc(nil).init_str("NSScreenNumber"), + ); + + let screen_number: u32 = msg_send![screen_number, unsignedIntValue]; + + Some(Rc::new(MacDisplay(screen_number))) + } + } + + fn mouse_position(&self) -> Point { + let position = unsafe { + self.0 + .lock() + .native_window + .mouseLocationOutsideOfEventStream() + }; + convert_mouse_position(position, self.content_size().height) + } + + fn modifiers(&self) -> Modifiers { + unsafe { + let modifiers: NSEventModifierFlags = msg_send![class!(NSEvent), modifierFlags]; + + let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask); + let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); + let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); + let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); + let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask); + + Modifiers { + control, + alt, + shift, + platform: command, + function, + } + } + } + + fn capslock(&self) -> Capslock { + unsafe { + let modifiers: NSEventModifierFlags = msg_send![class!(NSEvent), modifierFlags]; + + Capslock { + on: modifiers.contains(NSEventModifierFlags::NSAlphaShiftKeyMask), + } + } + } + + fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { + self.0.as_ref().lock().input_handler = Some(input_handler); + } + + fn take_input_handler(&mut self) -> Option { + self.0.as_ref().lock().input_handler.take() + } + + fn prompt( + &self, + level: PromptLevel, + msg: &str, + detail: Option<&str>, + answers: &[PromptButton], + ) -> Option> { + // macOs applies overrides to modal window buttons after they are added. + // Two most important for this logic are: + // * Buttons with "Cancel" title will be displayed as the last buttons in the modal + // * Last button added to the modal via `addButtonWithTitle` stays focused + // * Focused buttons react on "space"/" " keypresses + // * Usage of `keyEquivalent`, `makeFirstResponder` or `setInitialFirstResponder` does not change the focus + // + // See also https://developer.apple.com/documentation/appkit/nsalert/1524532-addbuttonwithtitle#discussion + // ``` + // By default, the first button has a key equivalent of Return, + // any button with a title of “Cancel” has a key equivalent of Escape, + // and any button with the title “Don’t Save” has a key equivalent of Command-D (but only if it’s not the first button). + // ``` + // + // To avoid situations when the last element added is "Cancel" and it gets the focus + // (hence stealing both ESC and Space shortcuts), we find and add one non-Cancel button + // last, so it gets focus and a Space shortcut. + // This way, "Save this file? Yes/No/Cancel"-ish modals will get all three buttons mapped with a key. + let latest_non_cancel_label = answers + .iter() + .enumerate() + .rev() + .find(|(_, label)| !label.is_cancel()) + .filter(|&(label_index, _)| label_index > 0); + + unsafe { + let alert: id = msg_send![class!(NSAlert), alloc]; + let alert: id = msg_send![alert, init]; + let alert_style = match level { + PromptLevel::Info => 1, + PromptLevel::Warning => 0, + PromptLevel::Critical => 2, + }; + let _: () = msg_send![alert, setAlertStyle: alert_style]; + let _: () = msg_send![alert, setMessageText: ns_string(msg)]; + if let Some(detail) = detail { + let _: () = msg_send![alert, setInformativeText: ns_string(detail)]; + } + + for (ix, answer) in answers + .iter() + .enumerate() + .filter(|&(ix, _)| Some(ix) != latest_non_cancel_label.map(|(ix, _)| ix)) + { + let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer.label())]; + let _: () = msg_send![button, setTag: ix as NSInteger]; + + if answer.is_cancel() { + // Bind Escape Key to Cancel Button + if let Some(key) = std::char::from_u32(super::events::ESCAPE_KEY as u32) { + let _: () = + msg_send![button, setKeyEquivalent: ns_string(&key.to_string())]; + } + } + } + if let Some((ix, answer)) = latest_non_cancel_label { + let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer.label())]; + let _: () = msg_send![button, setTag: ix as NSInteger]; + } + + let (done_tx, done_rx) = oneshot::channel(); + let done_tx = Cell::new(Some(done_tx)); + let block = ConcreteBlock::new(move |answer: NSInteger| { + if let Some(done_tx) = done_tx.take() { + let _ = done_tx.send(answer.try_into().unwrap()); + } + }); + let block = block.copy(); + let native_window = self.0.lock().native_window; + let executor = self.0.lock().executor.clone(); + executor + .spawn(async move { + let _: () = msg_send![ + alert, + beginSheetModalForWindow: native_window + completionHandler: block + ]; + }) + .detach(); + + Some(done_rx) + } + } + + fn activate(&self) { + let window = self.0.lock().native_window; + let executor = self.0.lock().executor.clone(); + executor + .spawn(async move { + unsafe { + let _: () = msg_send![window, makeKeyAndOrderFront: nil]; + } + }) + .detach(); + } + + fn is_active(&self) -> bool { + unsafe { self.0.lock().native_window.isKeyWindow() == YES } + } + + // is_hovered is unused on macOS. See Window::is_window_hovered. + fn is_hovered(&self) -> bool { + false + } + + fn set_title(&mut self, title: &str) { + unsafe { + let app = NSApplication::sharedApplication(nil); + let window = self.0.lock().native_window; + let title = ns_string(title); + let _: () = msg_send![app, changeWindowsItem:window title:title filename:false]; + let _: () = msg_send![window, setTitle: title]; + self.0.lock().move_traffic_light(); + } + } + + fn get_title(&self) -> String { + unsafe { + let title: id = msg_send![self.0.lock().native_window, title]; + if title.is_null() { + "".to_string() + } else { + title.to_str().to_string() + } + } + } + + fn set_app_id(&mut self, _app_id: &str) {} + + fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { + let mut this = self.0.as_ref().lock(); + + let opaque = background_appearance == WindowBackgroundAppearance::Opaque; + this.renderer.update_transparency(!opaque); + + unsafe { + this.native_window.setOpaque_(opaque as BOOL); + let background_color = if opaque { + NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0f64, 0f64, 0f64, 1f64) + } else { + // Not using `+[NSColor clearColor]` to avoid broken shadow. + NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0f64, 0f64, 0f64, 0.0001) + }; + this.native_window.setBackgroundColor_(background_color); + + if NSAppKitVersionNumber < NSAppKitVersionNumber12_0 { + // Whether `-[NSVisualEffectView respondsToSelector:@selector(_updateProxyLayer)]`. + // On macOS Catalina/Big Sur `NSVisualEffectView` doesn’t own concrete sublayers + // but uses a `CAProxyLayer`. Use the legacy WindowServer API. + let blur_radius = if background_appearance == WindowBackgroundAppearance::Blurred { + 80 + } else { + 0 + }; + + let window_number = this.native_window.windowNumber(); + CGSSetWindowBackgroundBlurRadius(CGSMainConnectionID(), window_number, blur_radius); + } else { + // On newer macOS `NSVisualEffectView` manages the effect layer directly. Using it + // could have a better performance (it downsamples the backdrop) and more control + // over the effect layer. + if background_appearance != WindowBackgroundAppearance::Blurred { + if let Some(blur_view) = this.blurred_view { + NSView::removeFromSuperview(blur_view); + this.blurred_view = None; + } + } else if this.blurred_view.is_none() { + let content_view = this.native_window.contentView(); + let frame = NSView::bounds(content_view); + let mut blur_view: id = msg_send![BLURRED_VIEW_CLASS, alloc]; + blur_view = NSView::initWithFrame_(blur_view, frame); + blur_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable); + + let _: () = msg_send![ + content_view, + addSubview: blur_view + positioned: NSWindowOrderingMode::NSWindowBelow + relativeTo: nil + ]; + this.blurred_view = Some(blur_view.autorelease()); + } + } + } + } + + fn set_edited(&mut self, edited: bool) { + unsafe { + let window = self.0.lock().native_window; + msg_send![window, setDocumentEdited: edited as BOOL] + } + + // Changing the document edited state resets the traffic light position, + // so we have to move it again. + self.0.lock().move_traffic_light(); + } + + fn show_character_palette(&self) { + let this = self.0.lock(); + let window = this.native_window; + this.executor + .spawn(async move { + unsafe { + let app = NSApplication::sharedApplication(nil); + let _: () = msg_send![app, orderFrontCharacterPalette: window]; + } + }) + .detach(); + } + + fn minimize(&self) { + let window = self.0.lock().native_window; + unsafe { + window.miniaturize_(nil); + } + } + + fn zoom(&self) { + let this = self.0.lock(); + let window = this.native_window; + this.executor + .spawn(async move { + unsafe { + window.zoom_(nil); + } + }) + .detach(); + } + + fn toggle_fullscreen(&self) { + let this = self.0.lock(); + let window = this.native_window; + this.executor + .spawn(async move { + unsafe { + window.toggleFullScreen_(nil); + } + }) + .detach(); + } + + fn is_fullscreen(&self) -> bool { + let this = self.0.lock(); + let window = this.native_window; + + unsafe { + window + .styleMask() + .contains(NSWindowStyleMask::NSFullScreenWindowMask) + } + } + + fn on_request_frame(&self, callback: Box) { + self.0.as_ref().lock().request_frame_callback = Some(callback); + } + + fn on_input(&self, callback: Box crate::DispatchEventResult>) { + self.0.as_ref().lock().event_callback = Some(callback); + } + + fn on_active_status_change(&self, callback: Box) { + self.0.as_ref().lock().activate_callback = Some(callback); + } + + fn on_hover_status_change(&self, _: Box) {} + + fn on_resize(&self, callback: Box, f32)>) { + self.0.as_ref().lock().resize_callback = Some(callback); + } + + fn on_moved(&self, callback: Box) { + self.0.as_ref().lock().moved_callback = Some(callback); + } + + fn on_should_close(&self, callback: Box bool>) { + self.0.as_ref().lock().should_close_callback = Some(callback); + } + + fn on_close(&self, callback: Box) { + self.0.as_ref().lock().close_callback = Some(callback); + } + + fn on_hit_test_window_control(&self, _callback: Box Option>) { + } + + fn on_appearance_changed(&self, callback: Box) { + self.0.lock().appearance_changed_callback = Some(callback); + } + + fn tabbed_windows(&self) -> Option> { + unsafe { + let windows: id = msg_send![self.0.lock().native_window, tabbedWindows]; + if windows.is_null() { + return None; + } + + let count: NSUInteger = msg_send![windows, count]; + let mut result = Vec::new(); + for i in 0..count { + let window: id = msg_send![windows, objectAtIndex:i]; + if msg_send![window, isKindOfClass: WINDOW_CLASS] { + let handle = get_window_state(&*window).lock().handle; + let title: id = msg_send![window, title]; + let title = SharedString::from(title.to_str().to_string()); + + result.push(SystemWindowTab::new(title, handle)); + } + } + + Some(result) + } + } + + fn tab_bar_visible(&self) -> bool { + unsafe { + let tab_group: id = msg_send![self.0.lock().native_window, tabGroup]; + if tab_group.is_null() { + false + } else { + let tab_bar_visible: BOOL = msg_send![tab_group, isTabBarVisible]; + tab_bar_visible == YES + } + } + } + + fn on_move_tab_to_new_window(&self, callback: Box) { + self.0.as_ref().lock().move_tab_to_new_window_callback = Some(callback); + } + + fn on_merge_all_windows(&self, callback: Box) { + self.0.as_ref().lock().merge_all_windows_callback = Some(callback); + } + + fn on_select_next_tab(&self, callback: Box) { + self.0.as_ref().lock().select_next_tab_callback = Some(callback); + } + + fn on_select_previous_tab(&self, callback: Box) { + self.0.as_ref().lock().select_previous_tab_callback = Some(callback); + } + + fn on_toggle_tab_bar(&self, callback: Box) { + self.0.as_ref().lock().toggle_tab_bar_callback = Some(callback); + } + + fn draw(&self, scene: &crate::Scene) { + let mut this = self.0.lock(); + this.renderer.draw(scene); + } + + fn sprite_atlas(&self) -> Arc { + self.0.lock().renderer.sprite_atlas().clone() + } + + fn gpu_specs(&self) -> Option { + None + } + + fn update_ime_position(&self, _bounds: Bounds) { + let executor = self.0.lock().executor.clone(); + executor + .spawn(async move { + unsafe { + let input_context: id = + msg_send![class!(NSTextInputContext), currentInputContext]; + if input_context.is_null() { + return; + } + let _: () = msg_send![input_context, invalidateCharacterCoordinates]; + } + }) + .detach() + } + + fn titlebar_double_click(&self) { + let this = self.0.lock(); + let window = this.native_window; + this.executor + .spawn(async move { + unsafe { + let defaults: id = NSUserDefaults::standardUserDefaults(); + let domain = NSString::alloc(nil).init_str("NSGlobalDomain"); + let key = NSString::alloc(nil).init_str("AppleActionOnDoubleClick"); + + let dict: id = msg_send![defaults, persistentDomainForName: domain]; + let action: id = if !dict.is_null() { + msg_send![dict, objectForKey: key] + } else { + nil + }; + + let action_str = if !action.is_null() { + CStr::from_ptr(NSString::UTF8String(action)).to_string_lossy() + } else { + "".into() + }; + + match action_str.as_ref() { + "None" => { + // "Do Nothing" selected, so do no action + } + "Minimize" => { + window.miniaturize_(nil); + } + "Maximize" => { + window.zoom_(nil); + } + "Fill" => { + // There is no documented API for "Fill" action, so we'll just zoom the window + window.zoom_(nil); + } + _ => { + window.zoom_(nil); + } + } + } + }) + .detach(); + } +} + +impl rwh::HasWindowHandle for MacWindow { + fn window_handle(&self) -> Result, rwh::HandleError> { + // SAFETY: The AppKitWindowHandle is a wrapper around a pointer to an NSView + unsafe { + Ok(rwh::WindowHandle::borrow_raw(rwh::RawWindowHandle::AppKit( + rwh::AppKitWindowHandle::new(self.0.lock().native_view.cast()), + ))) + } + } +} + +impl rwh::HasDisplayHandle for MacWindow { + fn display_handle(&self) -> Result, rwh::HandleError> { + // SAFETY: This is a no-op on macOS + unsafe { + Ok(rwh::DisplayHandle::borrow_raw( + rwh::AppKitDisplayHandle::new().into(), + )) + } + } +} + +fn get_scale_factor(native_window: id) -> f32 { + let factor = unsafe { + let screen: id = msg_send![native_window, screen]; + if screen.is_null() { + return 2.0; + } + NSScreen::backingScaleFactor(screen) as f32 + }; + + // We are not certain what triggers this, but it seems that sometimes + // this method would return 0 (https://github.com/zed-industries/zed/issues/6412) + // It seems most likely that this would happen if the window has no screen + // (if it is off-screen), though we'd expect to see viewDidChangeBackingProperties before + // it was rendered for real. + // Regardless, attempt to avoid the issue here. + if factor == 0.0 { 2. } else { factor } +} + +unsafe fn get_window_state(object: &Object) -> Arc> { + unsafe { + let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR); + let rc1 = Arc::from_raw(raw as *mut Mutex); + let rc2 = rc1.clone(); + mem::forget(rc1); + rc2 + } +} + +unsafe fn drop_window_state(object: &Object) { + unsafe { + let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR); + Arc::from_raw(raw as *mut Mutex); + } +} + +extern "C" fn yes(_: &Object, _: Sel) -> BOOL { + YES +} + +extern "C" fn dealloc_window(this: &Object, _: Sel) { + unsafe { + drop_window_state(this); + let _: () = msg_send![super(this, class!(NSWindow)), dealloc]; + } +} + +extern "C" fn dealloc_view(this: &Object, _: Sel) { + unsafe { + drop_window_state(this); + let _: () = msg_send![super(this, class!(NSView)), dealloc]; + } +} + +extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> BOOL { + handle_key_event(this, native_event, true) +} + +extern "C" fn handle_key_down(this: &Object, _: Sel, native_event: id) { + handle_key_event(this, native_event, false); +} + +extern "C" fn handle_key_up(this: &Object, _: Sel, native_event: id) { + handle_key_event(this, native_event, false); +} + +// Things to test if you're modifying this method: +// U.S. layout: +// - The IME consumes characters like 'j' and 'k', which makes paging through `less` in +// the terminal behave incorrectly by default. This behavior should be patched by our +// IME integration +// - `alt-t` should open the tasks menu +// - In vim mode, this keybinding should work: +// ``` +// { +// "context": "Editor && vim_mode == insert", +// "bindings": {"j j": "vim::NormalBefore"} +// } +// ``` +// and typing 'j k' in insert mode with this keybinding should insert the two characters +// Brazilian layout: +// - `" space` should create an unmarked quote +// - `" backspace` should delete the marked quote +// - `" "`should create an unmarked quote and a second marked quote +// - `" up` should insert a quote, unmark it, and move up one line +// - `" cmd-down` should insert a quote, unmark it, and move to the end of the file +// - `cmd-ctrl-space` and clicking on an emoji should type it +// Czech (QWERTY) layout: +// - in vim mode `option-4` should go to end of line (same as $) +// Japanese (Romaji) layout: +// - type `a i left down up enter enter` should create an unmarked text "愛" +extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: bool) -> BOOL { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + + let window_height = lock.content_size().height; + let event = unsafe { PlatformInput::from_native(native_event, Some(window_height)) }; + + let Some(event) = event else { + return NO; + }; + + let run_callback = |event: PlatformInput| -> BOOL { + let mut callback = window_state.as_ref().lock().event_callback.take(); + let handled: BOOL = if let Some(callback) = callback.as_mut() { + !callback(event).propagate as BOOL + } else { + NO + }; + window_state.as_ref().lock().event_callback = callback; + handled + }; + + match event { + PlatformInput::KeyDown(mut key_down_event) => { + // For certain keystrokes, macOS will first dispatch a "key equivalent" event. + // If that event isn't handled, it will then dispatch a "key down" event. GPUI + // makes no distinction between these two types of events, so we need to ignore + // the "key down" event if we've already just processed its "key equivalent" version. + if key_equivalent { + lock.last_key_equivalent = Some(key_down_event.clone()); + } else if lock.last_key_equivalent.take().as_ref() == Some(&key_down_event) { + return NO; + } + + drop(lock); + + let is_composing = + with_input_handler(this, |input_handler| input_handler.marked_text_range()) + .flatten() + .is_some(); + + // If we're composing, send the key to the input handler first; + // otherwise we only send to the input handler if we don't have a matching binding. + // The input handler may call `do_command_by_selector` if it doesn't know how to handle + // a key. If it does so, it will return YES so we won't send the key twice. + // We also do this for non-printing keys (like arrow keys and escape) as the IME menu + // may need them even if there is no marked text; + // however we skip keys with control or the input handler adds control-characters to the buffer. + // and keys with function, as the input handler swallows them. + if is_composing + || (key_down_event.keystroke.key_char.is_none() + && !key_down_event.keystroke.modifiers.control + && !key_down_event.keystroke.modifiers.function) + { + { + let mut lock = window_state.as_ref().lock(); + lock.keystroke_for_do_command = Some(key_down_event.keystroke.clone()); + lock.do_command_handled.take(); + drop(lock); + } + + let handled: BOOL = unsafe { + let input_context: id = msg_send![this, inputContext]; + msg_send![input_context, handleEvent: native_event] + }; + window_state.as_ref().lock().keystroke_for_do_command.take(); + if let Some(handled) = window_state.as_ref().lock().do_command_handled.take() { + return handled as BOOL; + } else if handled == YES { + return YES; + } + + let handled = run_callback(PlatformInput::KeyDown(key_down_event)); + return handled; + } + + let handled = run_callback(PlatformInput::KeyDown(key_down_event.clone())); + if handled == YES { + return YES; + } + + if key_down_event.is_held + && let Some(key_char) = key_down_event.keystroke.key_char.as_ref() + { + let handled = with_input_handler(this, |input_handler| { + if !input_handler.apple_press_and_hold_enabled() { + input_handler.replace_text_in_range(None, key_char); + return YES; + } + NO + }); + if handled == Some(YES) { + return YES; + } + } + + // Don't send key equivalents to the input handler, + // or macOS shortcuts like cmd-` will stop working. + if key_equivalent { + return NO; + } + + unsafe { + let input_context: id = msg_send![this, inputContext]; + msg_send![input_context, handleEvent: native_event] + } + } + + PlatformInput::KeyUp(_) => { + drop(lock); + run_callback(event) + } + + _ => NO, + } +} + +extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { + let window_state = unsafe { get_window_state(this) }; + let weak_window_state = Arc::downgrade(&window_state); + let mut lock = window_state.as_ref().lock(); + let window_height = lock.content_size().height; + let event = unsafe { PlatformInput::from_native(native_event, Some(window_height)) }; + + if let Some(mut event) = event { + match &mut event { + PlatformInput::MouseDown( + event @ MouseDownEvent { + button: MouseButton::Left, + modifiers: Modifiers { control: true, .. }, + .. + }, + ) => { + // On mac, a ctrl-left click should be handled as a right click. + *event = MouseDownEvent { + button: MouseButton::Right, + modifiers: Modifiers { + control: false, + ..event.modifiers + }, + click_count: 1, + ..*event + }; + } + + // Handles focusing click. + PlatformInput::MouseDown( + event @ MouseDownEvent { + button: MouseButton::Left, + .. + }, + ) if (lock.first_mouse) => { + *event = MouseDownEvent { + first_mouse: true, + ..*event + }; + lock.first_mouse = false; + } + + // Because we map a ctrl-left_down to a right_down -> right_up let's ignore + // the ctrl-left_up to avoid having a mismatch in button down/up events if the + // user is still holding ctrl when releasing the left mouse button + PlatformInput::MouseUp( + event @ MouseUpEvent { + button: MouseButton::Left, + modifiers: Modifiers { control: true, .. }, + .. + }, + ) => { + *event = MouseUpEvent { + button: MouseButton::Right, + modifiers: Modifiers { + control: false, + ..event.modifiers + }, + click_count: 1, + ..*event + }; + } + + _ => {} + }; + + match &event { + PlatformInput::MouseDown(_) => { + drop(lock); + unsafe { + let input_context: id = msg_send![this, inputContext]; + msg_send![input_context, handleEvent: native_event] + } + lock = window_state.as_ref().lock(); + } + PlatformInput::MouseMove( + event @ MouseMoveEvent { + pressed_button: Some(_), + .. + }, + ) => { + // Synthetic drag is used for selecting long buffer contents while buffer is being scrolled. + // External file drag and drop is able to emit its own synthetic mouse events which will conflict + // with these ones. + if !lock.external_files_dragged { + lock.synthetic_drag_counter += 1; + let executor = lock.executor.clone(); + executor + .spawn(synthetic_drag( + weak_window_state, + lock.synthetic_drag_counter, + event.clone(), + )) + .detach(); + } + } + + PlatformInput::MouseUp(MouseUpEvent { .. }) => { + lock.synthetic_drag_counter += 1; + } + + PlatformInput::ModifiersChanged(ModifiersChangedEvent { + modifiers, + capslock, + }) => { + // Only raise modifiers changed event when they have actually changed + if let Some(PlatformInput::ModifiersChanged(ModifiersChangedEvent { + modifiers: prev_modifiers, + capslock: prev_capslock, + })) = &lock.previous_modifiers_changed_event + && prev_modifiers == modifiers + && prev_capslock == capslock + { + return; + } + + lock.previous_modifiers_changed_event = Some(event.clone()); + } + + _ => {} + } + + if let Some(mut callback) = lock.event_callback.take() { + drop(lock); + callback(event); + window_state.lock().event_callback = Some(callback); + } + } +} + +extern "C" fn window_did_change_occlusion_state(this: &Object, _: Sel, _: id) { + let window_state = unsafe { get_window_state(this) }; + let lock = &mut *window_state.lock(); + unsafe { + if lock + .native_window + .occlusionState() + .contains(NSWindowOcclusionState::NSWindowOcclusionStateVisible) + { + lock.move_traffic_light(); + lock.start_display_link(); + } else { + lock.stop_display_link(); + } + } +} + +extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) { + let window_state = unsafe { get_window_state(this) }; + window_state.as_ref().lock().move_traffic_light(); +} + +extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + lock.fullscreen_restore_bounds = lock.bounds(); + + let min_version = NSOperatingSystemVersion::new(15, 3, 0); + + if is_macos_version_at_least(min_version) { + unsafe { + lock.native_window.setTitlebarAppearsTransparent_(NO); + } + } +} + +extern "C" fn window_will_exit_fullscreen(this: &Object, _: Sel, _: id) { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + + let min_version = NSOperatingSystemVersion::new(15, 3, 0); + + if is_macos_version_at_least(min_version) && lock.transparent_titlebar { + unsafe { + lock.native_window.setTitlebarAppearsTransparent_(YES); + } + } +} + +pub(crate) fn is_macos_version_at_least(version: NSOperatingSystemVersion) -> bool { + unsafe { NSProcessInfo::processInfo(nil).isOperatingSystemAtLeastVersion(version) } +} + +extern "C" fn window_did_move(this: &Object, _: Sel, _: id) { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + if let Some(mut callback) = lock.moved_callback.take() { + drop(lock); + callback(); + window_state.lock().moved_callback = Some(callback); + } +} + +extern "C" fn window_did_change_screen(this: &Object, _: Sel, _: id) { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + lock.start_display_link(); +} + +extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.lock(); + let is_active = unsafe { lock.native_window.isKeyWindow() == YES }; + + // When opening a pop-up while the application isn't active, Cocoa sends a spurious + // `windowDidBecomeKey` message to the previous key window even though that window + // isn't actually key. This causes a bug if the application is later activated while + // the pop-up is still open, making it impossible to activate the previous key window + // even if the pop-up gets closed. The only way to activate it again is to de-activate + // the app and re-activate it, which is a pretty bad UX. + // The following code detects the spurious event and invokes `resignKeyWindow`: + // in theory, we're not supposed to invoke this method manually but it balances out + // the spurious `becomeKeyWindow` event and helps us work around that bug. + if selector == sel!(windowDidBecomeKey:) && !is_active { + unsafe { + let _: () = msg_send![lock.native_window, resignKeyWindow]; + return; + } + } + + let executor = lock.executor.clone(); + drop(lock); + + // When a window becomes active, trigger an immediate synchronous frame request to prevent + // tab flicker when switching between windows in native tabs mode. + // + // This is only done on subsequent activations (not the first) to ensure the initial focus + // path is properly established. Without this guard, the focus state would remain unset until + // the first mouse click, causing keybindings to be non-functional. + if selector == sel!(windowDidBecomeKey:) && is_active { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.lock(); + + if lock.activated_least_once { + if let Some(mut callback) = lock.request_frame_callback.take() { + #[cfg(not(feature = "macos-blade"))] + lock.renderer.set_presents_with_transaction(true); + lock.stop_display_link(); + drop(lock); + callback(Default::default()); + + let mut lock = window_state.lock(); + lock.request_frame_callback = Some(callback); + #[cfg(not(feature = "macos-blade"))] + lock.renderer.set_presents_with_transaction(false); + lock.start_display_link(); + } + } else { + lock.activated_least_once = true; + } + } + + executor + .spawn(async move { + let mut lock = window_state.as_ref().lock(); + if is_active { + lock.move_traffic_light(); + } + + if let Some(mut callback) = lock.activate_callback.take() { + drop(lock); + callback(is_active); + window_state.lock().activate_callback = Some(callback); + }; + }) + .detach(); +} + +extern "C" fn window_should_close(this: &Object, _: Sel, _: id) -> BOOL { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + if let Some(mut callback) = lock.should_close_callback.take() { + drop(lock); + let should_close = callback(); + window_state.lock().should_close_callback = Some(callback); + should_close as BOOL + } else { + YES + } +} + +extern "C" fn close_window(this: &Object, _: Sel) { + unsafe { + let close_callback = { + let window_state = get_window_state(this); + let mut lock = window_state.as_ref().lock(); + lock.close_callback.take() + }; + + if let Some(callback) = close_callback { + callback(); + } + + let _: () = msg_send![super(this, class!(NSWindow)), close]; + } +} + +extern "C" fn make_backing_layer(this: &Object, _: Sel) -> id { + let window_state = unsafe { get_window_state(this) }; + let window_state = window_state.as_ref().lock(); + window_state.renderer.layer_ptr() as id +} + +extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel) { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + + let scale_factor = lock.scale_factor(); + let size = lock.content_size(); + let drawable_size = size.to_device_pixels(scale_factor); + unsafe { + let _: () = msg_send![ + lock.renderer.layer(), + setContentsScale: scale_factor as f64 + ]; + } + + lock.renderer.update_drawable_size(drawable_size); + + if let Some(mut callback) = lock.resize_callback.take() { + let content_size = lock.content_size(); + let scale_factor = lock.scale_factor(); + drop(lock); + callback(content_size, scale_factor); + window_state.as_ref().lock().resize_callback = Some(callback); + }; +} + +extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + + let new_size = Size::::from(size); + let old_size = unsafe { + let old_frame: NSRect = msg_send![this, frame]; + Size::::from(old_frame.size) + }; + + if old_size == new_size { + return; + } + + unsafe { + let _: () = msg_send![super(this, class!(NSView)), setFrameSize: size]; + } + + let scale_factor = lock.scale_factor(); + let drawable_size = new_size.to_device_pixels(scale_factor); + lock.renderer.update_drawable_size(drawable_size); + + if let Some(mut callback) = lock.resize_callback.take() { + let content_size = lock.content_size(); + let scale_factor = lock.scale_factor(); + drop(lock); + callback(content_size, scale_factor); + window_state.lock().resize_callback = Some(callback); + }; +} + +extern "C" fn display_layer(this: &Object, _: Sel, _: id) { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.lock(); + if let Some(mut callback) = lock.request_frame_callback.take() { + #[cfg(not(feature = "macos-blade"))] + lock.renderer.set_presents_with_transaction(true); + lock.stop_display_link(); + drop(lock); + callback(Default::default()); + + let mut lock = window_state.lock(); + lock.request_frame_callback = Some(callback); + #[cfg(not(feature = "macos-blade"))] + lock.renderer.set_presents_with_transaction(false); + lock.start_display_link(); + } +} + +unsafe extern "C" fn step(view: *mut c_void) { + let view = view as id; + let window_state = unsafe { get_window_state(&*view) }; + let mut lock = window_state.lock(); + + if let Some(mut callback) = lock.request_frame_callback.take() { + drop(lock); + callback(Default::default()); + window_state.lock().request_frame_callback = Some(callback); + } +} + +extern "C" fn valid_attributes_for_marked_text(_: &Object, _: Sel) -> id { + unsafe { msg_send![class!(NSArray), array] } +} + +extern "C" fn has_marked_text(this: &Object, _: Sel) -> BOOL { + let has_marked_text_result = + with_input_handler(this, |input_handler| input_handler.marked_text_range()).flatten(); + + has_marked_text_result.is_some() as BOOL +} + +extern "C" fn marked_range(this: &Object, _: Sel) -> NSRange { + let marked_range_result = + with_input_handler(this, |input_handler| input_handler.marked_text_range()).flatten(); + + marked_range_result.map_or(NSRange::invalid(), |range| range.into()) +} + +extern "C" fn selected_range(this: &Object, _: Sel) -> NSRange { + let selected_range_result = with_input_handler(this, |input_handler| { + input_handler.selected_text_range(false) + }) + .flatten(); + + selected_range_result.map_or(NSRange::invalid(), |selection| selection.range.into()) +} + +extern "C" fn first_rect_for_character_range( + this: &Object, + _: Sel, + range: NSRange, + _: id, +) -> NSRect { + let frame = get_frame(this); + with_input_handler(this, |input_handler| { + input_handler.bounds_for_range(range.to_range()?) + }) + .flatten() + .map_or( + NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.)), + |bounds| { + NSRect::new( + NSPoint::new( + frame.origin.x + bounds.origin.x.0 as f64, + frame.origin.y + frame.size.height + - bounds.origin.y.0 as f64 + - bounds.size.height.0 as f64, + ), + NSSize::new(bounds.size.width.0 as f64, bounds.size.height.0 as f64), + ) + }, + ) +} + +fn get_frame(this: &Object) -> NSRect { + unsafe { + let state = get_window_state(this); + let lock = state.lock(); + let mut frame = NSWindow::frame(lock.native_window); + let content_layout_rect: CGRect = msg_send![lock.native_window, contentLayoutRect]; + let style_mask: NSWindowStyleMask = msg_send![lock.native_window, styleMask]; + if !style_mask.contains(NSWindowStyleMask::NSFullSizeContentViewWindowMask) { + frame.origin.y -= frame.size.height - content_layout_rect.size.height; + } + frame + } +} + +extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NSRange) { + unsafe { + let is_attributed_string: BOOL = + msg_send![text, isKindOfClass: [class!(NSAttributedString)]]; + let text: id = if is_attributed_string == YES { + msg_send![text, string] + } else { + text + }; + + let text = text.to_str(); + let replacement_range = replacement_range.to_range(); + with_input_handler(this, |input_handler| { + input_handler.replace_text_in_range(replacement_range, text) + }); + } +} + +extern "C" fn set_marked_text( + this: &Object, + _: Sel, + text: id, + selected_range: NSRange, + replacement_range: NSRange, +) { + unsafe { + let is_attributed_string: BOOL = + msg_send![text, isKindOfClass: [class!(NSAttributedString)]]; + let text: id = if is_attributed_string == YES { + msg_send![text, string] + } else { + text + }; + let selected_range = selected_range.to_range(); + let replacement_range = replacement_range.to_range(); + let text = text.to_str(); + with_input_handler(this, |input_handler| { + input_handler.replace_and_mark_text_in_range(replacement_range, text, selected_range) + }); + } +} +extern "C" fn unmark_text(this: &Object, _: Sel) { + with_input_handler(this, |input_handler| input_handler.unmark_text()); +} + +extern "C" fn attributed_substring_for_proposed_range( + this: &Object, + _: Sel, + range: NSRange, + actual_range: *mut c_void, +) -> id { + with_input_handler(this, |input_handler| { + let range = range.to_range()?; + if range.is_empty() { + return None; + } + let mut adjusted: Option> = None; + + let selected_text = input_handler.text_for_range(range.clone(), &mut adjusted)?; + if let Some(adjusted) = adjusted + && adjusted != range + { + unsafe { (actual_range as *mut NSRange).write(NSRange::from(adjusted)) }; + } + unsafe { + let string: id = msg_send![class!(NSAttributedString), alloc]; + let string: id = msg_send![string, initWithString: ns_string(&selected_text)]; + Some(string) + } + }) + .flatten() + .unwrap_or(nil) +} + +// We ignore which selector it asks us to do because the user may have +// bound the shortcut to something else. +extern "C" fn do_command_by_selector(this: &Object, _: Sel, _: Sel) { + let state = unsafe { get_window_state(this) }; + let mut lock = state.as_ref().lock(); + let keystroke = lock.keystroke_for_do_command.take(); + let mut event_callback = lock.event_callback.take(); + drop(lock); + + if let Some((keystroke, mut callback)) = keystroke.zip(event_callback.as_mut()) { + let handled = (callback)(PlatformInput::KeyDown(KeyDownEvent { + keystroke, + is_held: false, + })); + state.as_ref().lock().do_command_handled = Some(!handled.propagate); + } + + state.as_ref().lock().event_callback = event_callback; +} + +extern "C" fn view_did_change_effective_appearance(this: &Object, _: Sel) { + unsafe { + let state = get_window_state(this); + let mut lock = state.as_ref().lock(); + if let Some(mut callback) = lock.appearance_changed_callback.take() { + drop(lock); + callback(); + state.lock().appearance_changed_callback = Some(callback); + } + } +} + +extern "C" fn accepts_first_mouse(this: &Object, _: Sel, _: id) -> BOOL { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + lock.first_mouse = true; + YES +} + +extern "C" fn character_index_for_point(this: &Object, _: Sel, position: NSPoint) -> u64 { + let position = screen_point_to_gpui_point(this, position); + with_input_handler(this, |input_handler| { + input_handler.character_index_for_point(position) + }) + .flatten() + .map(|index| index as u64) + .unwrap_or(NSNotFound as u64) +} + +fn screen_point_to_gpui_point(this: &Object, position: NSPoint) -> Point { + let frame = get_frame(this); + let window_x = position.x - frame.origin.x; + let window_y = frame.size.height - (position.y - frame.origin.y); + + point(px(window_x as f32), px(window_y as f32)) +} + +extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDragOperation { + let window_state = unsafe { get_window_state(this) }; + let position = drag_event_position(&window_state, dragging_info); + let paths = external_paths_from_event(dragging_info); + if let Some(event) = + paths.map(|paths| PlatformInput::FileDrop(FileDropEvent::Entered { position, paths })) + && send_new_event(&window_state, event) + { + window_state.lock().external_files_dragged = true; + return NSDragOperationCopy; + } + NSDragOperationNone +} + +extern "C" fn dragging_updated(this: &Object, _: Sel, dragging_info: id) -> NSDragOperation { + let window_state = unsafe { get_window_state(this) }; + let position = drag_event_position(&window_state, dragging_info); + if send_new_event( + &window_state, + PlatformInput::FileDrop(FileDropEvent::Pending { position }), + ) { + NSDragOperationCopy + } else { + NSDragOperationNone + } +} + +extern "C" fn dragging_exited(this: &Object, _: Sel, _: id) { + let window_state = unsafe { get_window_state(this) }; + send_new_event( + &window_state, + PlatformInput::FileDrop(FileDropEvent::Exited), + ); + window_state.lock().external_files_dragged = false; +} + +extern "C" fn perform_drag_operation(this: &Object, _: Sel, dragging_info: id) -> BOOL { + let window_state = unsafe { get_window_state(this) }; + let position = drag_event_position(&window_state, dragging_info); + send_new_event( + &window_state, + PlatformInput::FileDrop(FileDropEvent::Submit { position }), + ) + .to_objc() +} + +fn external_paths_from_event(dragging_info: *mut Object) -> Option { + let mut paths = SmallVec::new(); + let pasteboard: id = unsafe { msg_send![dragging_info, draggingPasteboard] }; + let filenames = unsafe { NSPasteboard::propertyListForType(pasteboard, NSFilenamesPboardType) }; + if filenames == nil { + return None; + } + for file in unsafe { filenames.iter() } { + let path = unsafe { + let f = NSString::UTF8String(file); + CStr::from_ptr(f).to_string_lossy().into_owned() + }; + paths.push(PathBuf::from(path)) + } + Some(ExternalPaths(paths)) +} + +extern "C" fn conclude_drag_operation(this: &Object, _: Sel, _: id) { + let window_state = unsafe { get_window_state(this) }; + send_new_event( + &window_state, + PlatformInput::FileDrop(FileDropEvent::Exited), + ); +} + +async fn synthetic_drag( + window_state: Weak>, + drag_id: usize, + event: MouseMoveEvent, +) { + loop { + Timer::after(Duration::from_millis(16)).await; + if let Some(window_state) = window_state.upgrade() { + let mut lock = window_state.lock(); + if lock.synthetic_drag_counter == drag_id { + if let Some(mut callback) = lock.event_callback.take() { + drop(lock); + callback(PlatformInput::MouseMove(event.clone())); + window_state.lock().event_callback = Some(callback); + } + } else { + break; + } + } + } +} + +fn send_new_event(window_state_lock: &Mutex, e: PlatformInput) -> bool { + let window_state = window_state_lock.lock().event_callback.take(); + if let Some(mut callback) = window_state { + callback(e); + window_state_lock.lock().event_callback = Some(callback); + true + } else { + false + } +} + +fn drag_event_position(window_state: &Mutex, dragging_info: id) -> Point { + let drag_location: NSPoint = unsafe { msg_send![dragging_info, draggingLocation] }; + convert_mouse_position(drag_location, window_state.lock().content_size().height) +} + +fn with_input_handler(window: &Object, f: F) -> Option +where + F: FnOnce(&mut PlatformInputHandler) -> R, +{ + let window_state = unsafe { get_window_state(window) }; + let mut lock = window_state.as_ref().lock(); + if let Some(mut input_handler) = lock.input_handler.take() { + drop(lock); + let result = f(&mut input_handler); + window_state.lock().input_handler = Some(input_handler); + Some(result) + } else { + None + } +} + +unsafe fn display_id_for_screen(screen: id) -> CGDirectDisplayID { + unsafe { + let device_description = NSScreen::deviceDescription(screen); + let screen_number_key: id = NSString::alloc(nil).init_str("NSScreenNumber"); + let screen_number = device_description.objectForKey_(screen_number_key); + let screen_number: NSUInteger = msg_send![screen_number, unsignedIntegerValue]; + screen_number as CGDirectDisplayID + } +} + +extern "C" fn blurred_view_init_with_frame(this: &Object, _: Sel, frame: NSRect) -> id { + unsafe { + let view = msg_send![super(this, class!(NSVisualEffectView)), initWithFrame: frame]; + // Use a colorless semantic material. The default value `AppearanceBased`, though not + // manually set, is deprecated. + NSVisualEffectView::setMaterial_(view, NSVisualEffectMaterial::Selection); + NSVisualEffectView::setState_(view, NSVisualEffectState::Active); + view + } +} + +extern "C" fn blurred_view_update_layer(this: &Object, _: Sel) { + unsafe { + let _: () = msg_send![super(this, class!(NSVisualEffectView)), updateLayer]; + let layer: id = msg_send![this, layer]; + if !layer.is_null() { + remove_layer_background(layer); + } + } +} + +unsafe fn remove_layer_background(layer: id) { + unsafe { + let _: () = msg_send![layer, setBackgroundColor:nil]; + + let class_name: id = msg_send![layer, className]; + if class_name.isEqualToString("CAChameleonLayer") { + // Remove the desktop tinting effect. + let _: () = msg_send![layer, setHidden: YES]; + return; + } + + let filters: id = msg_send![layer, filters]; + if !filters.is_null() { + // Remove the increased saturation. + // The effect of a `CAFilter` or `CIFilter` is determined by its name, and the + // `description` reflects its name and some parameters. Currently `NSVisualEffectView` + // uses a `CAFilter` named "colorSaturate". If one day they switch to `CIFilter`, the + // `description` will still contain "Saturat" ("... inputSaturation = ..."). + let test_string: id = NSString::alloc(nil).init_str("Saturat").autorelease(); + let count = NSArray::count(filters); + for i in 0..count { + let description: id = msg_send![filters.objectAtIndex(i), description]; + let hit: BOOL = msg_send![description, containsString: test_string]; + if hit == NO { + continue; + } + + let all_indices = NSRange { + location: 0, + length: count, + }; + let indices: id = msg_send![class!(NSMutableIndexSet), indexSet]; + let _: () = msg_send![indices, addIndexesInRange: all_indices]; + let _: () = msg_send![indices, removeIndex:i]; + let filtered: id = msg_send![filters, objectsAtIndexes: indices]; + let _: () = msg_send![layer, setFilters: filtered]; + break; + } + } + + let sublayers: id = msg_send![layer, sublayers]; + if !sublayers.is_null() { + let count = NSArray::count(sublayers); + for i in 0..count { + let sublayer = sublayers.objectAtIndex(i); + remove_layer_background(sublayer); + } + } + } +} + +extern "C" fn add_titlebar_accessory_view_controller(this: &Object, _: Sel, view_controller: id) { + unsafe { + let _: () = msg_send![super(this, class!(NSWindow)), addTitlebarAccessoryViewController: view_controller]; + + // Hide the native tab bar and set its height to 0, since we render our own. + let accessory_view: id = msg_send![view_controller, view]; + let _: () = msg_send![accessory_view, setHidden: YES]; + let mut frame: NSRect = msg_send![accessory_view, frame]; + frame.size.height = 0.0; + let _: () = msg_send![accessory_view, setFrame: frame]; + } +} + +extern "C" fn move_tab_to_new_window(this: &Object, _: Sel, _: id) { + unsafe { + let _: () = msg_send![super(this, class!(NSWindow)), moveTabToNewWindow:nil]; + + let window_state = get_window_state(this); + let mut lock = window_state.as_ref().lock(); + if let Some(mut callback) = lock.move_tab_to_new_window_callback.take() { + drop(lock); + callback(); + window_state.lock().move_tab_to_new_window_callback = Some(callback); + } + } +} + +extern "C" fn merge_all_windows(this: &Object, _: Sel, _: id) { + unsafe { + let _: () = msg_send![super(this, class!(NSWindow)), mergeAllWindows:nil]; + + let window_state = get_window_state(this); + let mut lock = window_state.as_ref().lock(); + if let Some(mut callback) = lock.merge_all_windows_callback.take() { + drop(lock); + callback(); + window_state.lock().merge_all_windows_callback = Some(callback); + } + } +} + +extern "C" fn select_next_tab(this: &Object, _sel: Sel, _id: id) { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + if let Some(mut callback) = lock.select_next_tab_callback.take() { + drop(lock); + callback(); + window_state.lock().select_next_tab_callback = Some(callback); + } +} + +extern "C" fn select_previous_tab(this: &Object, _sel: Sel, _id: id) { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + if let Some(mut callback) = lock.select_previous_tab_callback.take() { + drop(lock); + callback(); + window_state.lock().select_previous_tab_callback = Some(callback); + } +} + +extern "C" fn toggle_tab_bar(this: &Object, _sel: Sel, _id: id) { + unsafe { + let _: () = msg_send![super(this, class!(NSWindow)), toggleTabBar:nil]; + + let window_state = get_window_state(this); + let mut lock = window_state.as_ref().lock(); + lock.move_traffic_light(); + + if let Some(mut callback) = lock.toggle_tab_bar_callback.take() { + drop(lock); + callback(); + window_state.lock().toggle_tab_bar_callback = Some(callback); + } + } +} diff --git a/third_party/gpui/src/platform/mac/window_appearance.rs b/third_party/gpui/src/platform/mac/window_appearance.rs new file mode 100644 index 0000000..65c409d --- /dev/null +++ b/third_party/gpui/src/platform/mac/window_appearance.rs @@ -0,0 +1,37 @@ +use crate::WindowAppearance; +use cocoa::{ + appkit::{NSAppearanceNameVibrantDark, NSAppearanceNameVibrantLight}, + base::id, + foundation::NSString, +}; +use objc::{msg_send, sel, sel_impl}; +use std::ffi::CStr; + +impl WindowAppearance { + pub(crate) unsafe fn from_native(appearance: id) -> Self { + let name: id = msg_send![appearance, name]; + unsafe { + if name == NSAppearanceNameVibrantLight { + Self::VibrantLight + } else if name == NSAppearanceNameVibrantDark { + Self::VibrantDark + } else if name == NSAppearanceNameAqua { + Self::Light + } else if name == NSAppearanceNameDarkAqua { + Self::Dark + } else { + println!( + "unknown appearance: {:?}", + CStr::from_ptr(name.UTF8String()) + ); + Self::Light + } + } + } +} + +#[link(name = "AppKit", kind = "framework")] +unsafe extern "C" { + pub static NSAppearanceNameAqua: id; + pub static NSAppearanceNameDarkAqua: id; +} diff --git a/third_party/gpui/src/platform/scap_screen_capture.rs b/third_party/gpui/src/platform/scap_screen_capture.rs new file mode 100644 index 0000000..d6d19cd --- /dev/null +++ b/third_party/gpui/src/platform/scap_screen_capture.rs @@ -0,0 +1,325 @@ +//! Screen capture for Linux and Windows +use crate::{ + DevicePixels, ForegroundExecutor, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, + Size, SourceMetadata, size, +}; +use anyhow::{Context as _, Result, anyhow}; +use futures::channel::oneshot; +use scap::Target; +use std::rc::Rc; +use std::sync::Arc; +use std::sync::atomic::{self, AtomicBool}; + +/// Populates the receiver with the screens that can be captured. +/// +/// `scap_default_target_source` should be used instead on Wayland, since `scap_screen_sources` +/// won't return any results. +#[allow(dead_code)] +pub(crate) fn scap_screen_sources( + foreground_executor: &ForegroundExecutor, +) -> oneshot::Receiver>>> { + let (sources_tx, sources_rx) = oneshot::channel(); + get_screen_targets(sources_tx); + to_dyn_screen_capture_sources(sources_rx, foreground_executor) +} + +/// Starts screen capture for the default target, and populates the receiver with a single source +/// for it. The first frame of the screen capture is used to determine the size of the stream. +/// +/// On Wayland (Linux), prompts the user to select a target, and populates the receiver with a +/// single screen capture source for their selection. +#[allow(dead_code)] +pub(crate) fn start_scap_default_target_source( + foreground_executor: &ForegroundExecutor, +) -> oneshot::Receiver>>> { + let (sources_tx, sources_rx) = oneshot::channel(); + start_default_target_screen_capture(sources_tx); + to_dyn_screen_capture_sources(sources_rx, foreground_executor) +} + +struct ScapCaptureSource { + target: scap::Display, + size: Size, +} + +/// Populates the sender with the screens available for capture. +fn get_screen_targets(sources_tx: oneshot::Sender>>) { + // Due to use of blocking APIs, a new thread is used. + std::thread::spawn(|| { + let targets = match scap::get_all_targets() { + Ok(targets) => targets, + Err(err) => { + sources_tx.send(Err(err)).ok(); + return; + } + }; + let sources = targets + .into_iter() + .filter_map(|target| match target { + scap::Target::Display(display) => { + let size = Size { + width: DevicePixels(display.width as i32), + height: DevicePixels(display.height as i32), + }; + Some(ScapCaptureSource { + target: display, + size, + }) + } + scap::Target::Window(_) => None, + }) + .collect::>(); + sources_tx.send(Ok(sources)).ok(); + }); +} + +impl ScreenCaptureSource for ScapCaptureSource { + fn metadata(&self) -> Result { + Ok(SourceMetadata { + resolution: self.size, + label: Some(self.target.title.clone().into()), + is_main: None, + id: self.target.id as u64, + }) + } + + fn stream( + &self, + foreground_executor: &ForegroundExecutor, + frame_callback: Box, + ) -> oneshot::Receiver>> { + let (stream_tx, stream_rx) = oneshot::channel(); + let target = self.target.clone(); + + // Due to use of blocking APIs, a dedicated thread is used. + std::thread::spawn(move || { + match new_scap_capturer(Some(scap::Target::Display(target.clone()))) { + Ok(mut capturer) => { + capturer.start_capture(); + run_capture(capturer, target.clone(), frame_callback, stream_tx); + } + Err(e) => { + stream_tx.send(Err(e)).ok(); + } + } + }); + + to_dyn_screen_capture_stream(stream_rx, foreground_executor) + } +} + +struct ScapDefaultTargetCaptureSource { + // Sender populated by single call to `ScreenCaptureSource::stream`. + stream_call_tx: std::sync::mpsc::SyncSender<( + // Provides the result of `ScreenCaptureSource::stream`. + oneshot::Sender>, + // Callback for frames. + Box, + )>, + target: scap::Display, + size: Size, +} + +/// Starts screen capture on the default capture target, and populates the sender with the source. +fn start_default_target_screen_capture( + sources_tx: oneshot::Sender>>, +) { + // Due to use of blocking APIs, a dedicated thread is used. + std::thread::spawn(|| { + let start_result = util::maybe!({ + let mut capturer = new_scap_capturer(None)?; + capturer.start_capture(); + let first_frame = capturer + .get_next_frame() + .context("Failed to get first frame of screenshare to get the size.")?; + let size = frame_size(&first_frame); + let target = capturer + .target() + .context("Unable to determine the target display.")?; + let target = target.clone(); + Ok((capturer, size, target)) + }); + + match start_result { + Ok((capturer, size, Target::Display(display))) => { + let (stream_call_tx, stream_rx) = std::sync::mpsc::sync_channel(1); + sources_tx + .send(Ok(vec![ScapDefaultTargetCaptureSource { + stream_call_tx, + size, + target: display.clone(), + }])) + .ok(); + let Ok((stream_tx, frame_callback)) = stream_rx.recv() else { + return; + }; + run_capture(capturer, display, frame_callback, stream_tx); + } + Err(e) => { + sources_tx.send(Err(e)).ok(); + } + _ => { + sources_tx + .send(Err(anyhow!("The screen capture source is not a display"))) + .ok(); + } + } + }); +} + +impl ScreenCaptureSource for ScapDefaultTargetCaptureSource { + fn metadata(&self) -> Result { + Ok(SourceMetadata { + resolution: self.size, + label: None, + is_main: None, + id: self.target.id as u64, + }) + } + + fn stream( + &self, + foreground_executor: &ForegroundExecutor, + frame_callback: Box, + ) -> oneshot::Receiver>> { + let (tx, rx) = oneshot::channel(); + match self.stream_call_tx.try_send((tx, frame_callback)) { + Ok(()) => {} + Err(std::sync::mpsc::TrySendError::Full((tx, _))) + | Err(std::sync::mpsc::TrySendError::Disconnected((tx, _))) => { + // Note: support could be added for being called again after end of prior stream. + tx.send(Err(anyhow!( + "Can't call ScapDefaultTargetCaptureSource::stream multiple times." + ))) + .ok(); + } + } + to_dyn_screen_capture_stream(rx, foreground_executor) + } +} + +fn new_scap_capturer(target: Option) -> Result { + scap::capturer::Capturer::build(scap::capturer::Options { + fps: 60, + show_cursor: true, + show_highlight: true, + // Note that the actual frame output type may differ. + output_type: scap::frame::FrameType::YUVFrame, + output_resolution: scap::capturer::Resolution::Captured, + crop_area: None, + target, + excluded_targets: None, + }) +} + +fn run_capture( + mut capturer: scap::capturer::Capturer, + display: scap::Display, + frame_callback: Box, + stream_tx: oneshot::Sender>, +) { + let cancel_stream = Arc::new(AtomicBool::new(false)); + let size = Size { + width: DevicePixels(display.width as i32), + height: DevicePixels(display.height as i32), + }; + let stream_send_result = stream_tx.send(Ok(ScapStream { + cancel_stream: cancel_stream.clone(), + display, + size, + })); + if stream_send_result.is_err() { + return; + } + while !cancel_stream.load(std::sync::atomic::Ordering::SeqCst) { + match capturer.get_next_frame() { + Ok(frame) => frame_callback(ScreenCaptureFrame(frame)), + Err(err) => { + log::error!("Halting screen capture due to error: {err}"); + break; + } + } + } + capturer.stop_capture(); +} + +struct ScapStream { + cancel_stream: Arc, + display: scap::Display, + size: Size, +} + +impl ScreenCaptureStream for ScapStream { + fn metadata(&self) -> Result { + Ok(SourceMetadata { + resolution: self.size, + label: Some(self.display.title.clone().into()), + is_main: None, + id: self.display.id as u64, + }) + } +} + +impl Drop for ScapStream { + fn drop(&mut self) { + self.cancel_stream.store(true, atomic::Ordering::SeqCst); + } +} + +fn frame_size(frame: &scap::frame::Frame) -> Size { + let (width, height) = match frame { + scap::frame::Frame::YUVFrame(frame) => (frame.width, frame.height), + scap::frame::Frame::RGB(frame) => (frame.width, frame.height), + scap::frame::Frame::RGBx(frame) => (frame.width, frame.height), + scap::frame::Frame::XBGR(frame) => (frame.width, frame.height), + scap::frame::Frame::BGRx(frame) => (frame.width, frame.height), + scap::frame::Frame::BGR0(frame) => (frame.width, frame.height), + scap::frame::Frame::BGRA(frame) => (frame.width, frame.height), + }; + size(DevicePixels(width), DevicePixels(height)) +} + +/// This is used by `get_screen_targets` and `start_default_target_screen_capture` to turn their +/// results into `Rc`. They need to `Send` their capture source, and so +/// the capture source structs are used as `Rc` is not `Send`. +fn to_dyn_screen_capture_sources( + sources_rx: oneshot::Receiver>>, + foreground_executor: &ForegroundExecutor, +) -> oneshot::Receiver>>> { + let (dyn_sources_tx, dyn_sources_rx) = oneshot::channel(); + foreground_executor + .spawn(async move { + match sources_rx.await { + Ok(Ok(results)) => dyn_sources_tx + .send(Ok(results + .into_iter() + .map(|source| Rc::new(source) as Rc) + .collect::>())) + .ok(), + Ok(Err(err)) => dyn_sources_tx.send(Err(err)).ok(), + Err(oneshot::Canceled) => None, + } + }) + .detach(); + dyn_sources_rx +} + +/// Same motivation as `to_dyn_screen_capture_sources` above. +fn to_dyn_screen_capture_stream( + sources_rx: oneshot::Receiver>, + foreground_executor: &ForegroundExecutor, +) -> oneshot::Receiver>> { + let (dyn_sources_tx, dyn_sources_rx) = oneshot::channel(); + foreground_executor + .spawn(async move { + match sources_rx.await { + Ok(Ok(stream)) => dyn_sources_tx + .send(Ok(Box::new(stream) as Box)) + .ok(), + Ok(Err(err)) => dyn_sources_tx.send(Err(err)).ok(), + Err(oneshot::Canceled) => None, + } + }) + .detach(); + dyn_sources_rx +} diff --git a/third_party/gpui/src/platform/test.rs b/third_party/gpui/src/platform/test.rs new file mode 100644 index 0000000..9227df5 --- /dev/null +++ b/third_party/gpui/src/platform/test.rs @@ -0,0 +1,11 @@ +mod dispatcher; +mod display; +mod platform; +mod window; + +pub use dispatcher::*; +pub(crate) use display::*; +pub(crate) use platform::*; +pub(crate) use window::*; + +pub use platform::{TestScreenCaptureSource, TestScreenCaptureStream}; diff --git a/third_party/gpui/src/platform/test/dispatcher.rs b/third_party/gpui/src/platform/test/dispatcher.rs new file mode 100644 index 0000000..017c29b --- /dev/null +++ b/third_party/gpui/src/platform/test/dispatcher.rs @@ -0,0 +1,314 @@ +use crate::{PlatformDispatcher, TaskLabel}; +use async_task::Runnable; +use backtrace::Backtrace; +use collections::{HashMap, HashSet, VecDeque}; +use parking::Unparker; +use parking_lot::Mutex; +use rand::prelude::*; +use std::{ + future::Future, + ops::RangeInclusive, + pin::Pin, + sync::Arc, + task::{Context, Poll}, + time::{Duration, Instant}, +}; +use util::post_inc; + +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +struct TestDispatcherId(usize); + +#[doc(hidden)] +pub struct TestDispatcher { + id: TestDispatcherId, + state: Arc>, +} + +struct TestDispatcherState { + random: StdRng, + foreground: HashMap>, + background: Vec, + deprioritized_background: Vec, + delayed: Vec<(Duration, Runnable)>, + start_time: Instant, + time: Duration, + is_main_thread: bool, + next_id: TestDispatcherId, + allow_parking: bool, + waiting_hint: Option, + waiting_backtrace: Option, + deprioritized_task_labels: HashSet, + block_on_ticks: RangeInclusive, + last_parked: Option, +} + +impl TestDispatcher { + pub fn new(random: StdRng) -> Self { + let state = TestDispatcherState { + random, + foreground: HashMap::default(), + background: Vec::new(), + deprioritized_background: Vec::new(), + delayed: Vec::new(), + time: Duration::ZERO, + start_time: Instant::now(), + is_main_thread: true, + next_id: TestDispatcherId(1), + allow_parking: false, + waiting_hint: None, + waiting_backtrace: None, + deprioritized_task_labels: Default::default(), + block_on_ticks: 0..=1000, + last_parked: None, + }; + + TestDispatcher { + id: TestDispatcherId(0), + state: Arc::new(Mutex::new(state)), + } + } + + pub fn advance_clock(&self, by: Duration) { + let new_now = self.state.lock().time + by; + loop { + self.run_until_parked(); + let state = self.state.lock(); + let next_due_time = state.delayed.first().map(|(time, _)| *time); + drop(state); + if let Some(due_time) = next_due_time + && due_time <= new_now + { + self.state.lock().time = due_time; + continue; + } + break; + } + self.state.lock().time = new_now; + } + + pub fn advance_clock_to_next_delayed(&self) -> bool { + let next_due_time = self.state.lock().delayed.first().map(|(time, _)| *time); + if let Some(next_due_time) = next_due_time { + self.state.lock().time = next_due_time; + return true; + } + false + } + + pub fn simulate_random_delay(&self) -> impl 'static + Send + Future + use<> { + struct YieldNow { + pub(crate) count: usize, + } + + impl Future for YieldNow { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { + if self.count > 0 { + self.count -= 1; + cx.waker().wake_by_ref(); + Poll::Pending + } else { + Poll::Ready(()) + } + } + } + + YieldNow { + count: self.state.lock().random.random_range(0..10), + } + } + + pub fn tick(&self, background_only: bool) -> bool { + let mut state = self.state.lock(); + + while let Some((deadline, _)) = state.delayed.first() { + if *deadline > state.time { + break; + } + let (_, runnable) = state.delayed.remove(0); + state.background.push(runnable); + } + + let foreground_len: usize = if background_only { + 0 + } else { + state + .foreground + .values() + .map(|runnables| runnables.len()) + .sum() + }; + let background_len = state.background.len(); + + let runnable; + let main_thread; + if foreground_len == 0 && background_len == 0 { + let deprioritized_background_len = state.deprioritized_background.len(); + if deprioritized_background_len == 0 { + return false; + } + let ix = state.random.random_range(0..deprioritized_background_len); + main_thread = false; + runnable = state.deprioritized_background.swap_remove(ix); + } else { + main_thread = state.random.random_ratio( + foreground_len as u32, + (foreground_len + background_len) as u32, + ); + if main_thread { + let state = &mut *state; + runnable = state + .foreground + .values_mut() + .filter(|runnables| !runnables.is_empty()) + .choose(&mut state.random) + .unwrap() + .pop_front() + .unwrap(); + } else { + let ix = state.random.random_range(0..background_len); + runnable = state.background.swap_remove(ix); + }; + }; + + let was_main_thread = state.is_main_thread; + state.is_main_thread = main_thread; + drop(state); + runnable.run(); + self.state.lock().is_main_thread = was_main_thread; + + true + } + + pub fn deprioritize(&self, task_label: TaskLabel) { + self.state + .lock() + .deprioritized_task_labels + .insert(task_label); + } + + pub fn run_until_parked(&self) { + while self.tick(false) {} + } + + pub fn parking_allowed(&self) -> bool { + self.state.lock().allow_parking + } + + pub fn allow_parking(&self) { + self.state.lock().allow_parking = true + } + + pub fn forbid_parking(&self) { + self.state.lock().allow_parking = false + } + + pub fn set_waiting_hint(&self, msg: Option) { + self.state.lock().waiting_hint = msg + } + + pub fn waiting_hint(&self) -> Option { + self.state.lock().waiting_hint.clone() + } + + pub fn start_waiting(&self) { + self.state.lock().waiting_backtrace = Some(Backtrace::new_unresolved()); + } + + pub fn finish_waiting(&self) { + self.state.lock().waiting_backtrace.take(); + } + + pub fn waiting_backtrace(&self) -> Option { + self.state.lock().waiting_backtrace.take().map(|mut b| { + b.resolve(); + b + }) + } + + pub fn rng(&self) -> StdRng { + self.state.lock().random.clone() + } + + pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive) { + self.state.lock().block_on_ticks = range; + } + + pub fn gen_block_on_ticks(&self) -> usize { + let mut lock = self.state.lock(); + let block_on_ticks = lock.block_on_ticks.clone(); + lock.random.random_range(block_on_ticks) + } + pub fn unpark_last(&self) { + self.state + .lock() + .last_parked + .take() + .as_ref() + .map(Unparker::unpark); + } + + pub fn set_unparker(&self, unparker: Unparker) { + let last = { self.state.lock().last_parked.replace(unparker) }; + if let Some(last) = last { + last.unpark(); + } + } +} + +impl Clone for TestDispatcher { + fn clone(&self) -> Self { + let id = post_inc(&mut self.state.lock().next_id.0); + Self { + id: TestDispatcherId(id), + state: self.state.clone(), + } + } +} + +impl PlatformDispatcher for TestDispatcher { + fn is_main_thread(&self) -> bool { + self.state.lock().is_main_thread + } + + fn now(&self) -> Instant { + let state = self.state.lock(); + state.start_time + state.time + } + + fn dispatch(&self, runnable: Runnable, label: Option) { + { + let mut state = self.state.lock(); + if label.is_some_and(|label| state.deprioritized_task_labels.contains(&label)) { + state.deprioritized_background.push(runnable); + } else { + state.background.push(runnable); + } + } + self.unpark_last(); + } + + fn dispatch_on_main_thread(&self, runnable: Runnable) { + self.state + .lock() + .foreground + .entry(self.id) + .or_default() + .push_back(runnable); + self.unpark_last(); + } + + fn dispatch_after(&self, duration: std::time::Duration, runnable: Runnable) { + let mut state = self.state.lock(); + let next_time = state.time + duration; + let ix = match state.delayed.binary_search_by_key(&next_time, |e| e.0) { + Ok(ix) | Err(ix) => ix, + }; + state.delayed.insert(ix, (next_time, runnable)); + } + + fn as_test(&self) -> Option<&TestDispatcher> { + Some(self) + } +} diff --git a/third_party/gpui/src/platform/test/display.rs b/third_party/gpui/src/platform/test/display.rs new file mode 100644 index 0000000..c4adb01 --- /dev/null +++ b/third_party/gpui/src/platform/test/display.rs @@ -0,0 +1,33 @@ +use crate::{Bounds, DisplayId, Pixels, PlatformDisplay, Point, px}; +use anyhow::{Ok, Result}; + +#[derive(Debug)] +pub(crate) struct TestDisplay { + id: DisplayId, + uuid: uuid::Uuid, + bounds: Bounds, +} + +impl TestDisplay { + pub fn new() -> Self { + TestDisplay { + id: DisplayId(1), + uuid: uuid::Uuid::new_v4(), + bounds: Bounds::from_corners(Point::default(), Point::new(px(1920.), px(1080.))), + } + } +} + +impl PlatformDisplay for TestDisplay { + fn id(&self) -> crate::DisplayId { + self.id + } + + fn uuid(&self) -> Result { + Ok(self.uuid) + } + + fn bounds(&self) -> crate::Bounds { + self.bounds + } +} diff --git a/third_party/gpui/src/platform/test/platform.rs b/third_party/gpui/src/platform/test/platform.rs new file mode 100644 index 0000000..15b9091 --- /dev/null +++ b/third_party/gpui/src/platform/test/platform.rs @@ -0,0 +1,463 @@ +use crate::{ + AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels, + DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, + PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PromptButton, + ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task, + TestDisplay, TestWindow, WindowAppearance, WindowParams, size, +}; +use anyhow::Result; +use collections::VecDeque; +use futures::channel::oneshot; +use parking_lot::Mutex; +use std::{ + cell::RefCell, + path::{Path, PathBuf}, + rc::{Rc, Weak}, + sync::Arc, +}; +#[cfg(target_os = "windows")] +use windows::Win32::{ + Graphics::Imaging::{CLSID_WICImagingFactory, IWICImagingFactory}, + System::Com::{CLSCTX_INPROC_SERVER, CoCreateInstance}, +}; + +/// TestPlatform implements the Platform trait for use in tests. +pub(crate) struct TestPlatform { + background_executor: BackgroundExecutor, + foreground_executor: ForegroundExecutor, + + pub(crate) active_window: RefCell>, + active_display: Rc, + active_cursor: Mutex, + current_clipboard_item: Mutex>, + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + current_primary_item: Mutex>, + pub(crate) prompts: RefCell, + screen_capture_sources: RefCell>, + pub opened_url: RefCell>, + pub text_system: Arc, + #[cfg(target_os = "windows")] + bitmap_factory: std::mem::ManuallyDrop, + weak: Weak, +} + +#[derive(Clone)] +/// A fake screen capture source, used for testing. +pub struct TestScreenCaptureSource {} + +/// A fake screen capture stream, used for testing. +pub struct TestScreenCaptureStream {} + +impl ScreenCaptureSource for TestScreenCaptureSource { + fn metadata(&self) -> Result { + Ok(SourceMetadata { + id: 0, + is_main: None, + label: None, + resolution: size(DevicePixels(1), DevicePixels(1)), + }) + } + + fn stream( + &self, + _foreground_executor: &ForegroundExecutor, + _frame_callback: Box, + ) -> oneshot::Receiver>> { + let (mut tx, rx) = oneshot::channel(); + let stream = TestScreenCaptureStream {}; + tx.send(Ok(Box::new(stream) as Box)) + .ok(); + rx + } +} + +impl ScreenCaptureStream for TestScreenCaptureStream { + fn metadata(&self) -> Result { + TestScreenCaptureSource {}.metadata() + } +} + +struct TestPrompt { + msg: String, + detail: Option, + answers: Vec, + tx: oneshot::Sender, +} + +#[derive(Default)] +pub(crate) struct TestPrompts { + multiple_choice: VecDeque, + new_path: VecDeque<(PathBuf, oneshot::Sender>>)>, +} + +impl TestPlatform { + pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Rc { + #[cfg(target_os = "windows")] + let bitmap_factory = unsafe { + windows::Win32::System::Ole::OleInitialize(None) + .expect("unable to initialize Windows OLE"); + std::mem::ManuallyDrop::new( + CoCreateInstance(&CLSID_WICImagingFactory, None, CLSCTX_INPROC_SERVER) + .expect("Error creating bitmap factory."), + ) + }; + + let text_system = Arc::new(NoopTextSystem); + + Rc::new_cyclic(|weak| TestPlatform { + background_executor: executor, + foreground_executor, + prompts: Default::default(), + screen_capture_sources: Default::default(), + active_cursor: Default::default(), + active_display: Rc::new(TestDisplay::new()), + active_window: Default::default(), + current_clipboard_item: Mutex::new(None), + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + current_primary_item: Mutex::new(None), + weak: weak.clone(), + opened_url: Default::default(), + #[cfg(target_os = "windows")] + bitmap_factory, + text_system, + }) + } + + pub(crate) fn simulate_new_path_selection( + &self, + select_path: impl FnOnce(&std::path::Path) -> Option, + ) { + let (path, tx) = self + .prompts + .borrow_mut() + .new_path + .pop_front() + .expect("no pending new path prompt"); + self.background_executor().set_waiting_hint(None); + tx.send(Ok(select_path(&path))).ok(); + } + + #[track_caller] + pub(crate) fn simulate_prompt_answer(&self, response: &str) { + let prompt = self + .prompts + .borrow_mut() + .multiple_choice + .pop_front() + .expect("no pending multiple choice prompt"); + self.background_executor().set_waiting_hint(None); + let Some(ix) = prompt.answers.iter().position(|a| a == response) else { + panic!( + "PROMPT: {}\n{:?}\n{:?}\nCannot respond with {}", + prompt.msg, prompt.detail, prompt.answers, response + ) + }; + prompt.tx.send(ix).ok(); + } + + pub(crate) fn has_pending_prompt(&self) -> bool { + !self.prompts.borrow().multiple_choice.is_empty() + } + + pub(crate) fn pending_prompt(&self) -> Option<(String, String)> { + let prompts = self.prompts.borrow(); + let prompt = prompts.multiple_choice.front()?; + Some(( + prompt.msg.clone(), + prompt.detail.clone().unwrap_or_default(), + )) + } + + pub(crate) fn set_screen_capture_sources(&self, sources: Vec) { + *self.screen_capture_sources.borrow_mut() = sources; + } + + pub(crate) fn prompt( + &self, + msg: &str, + detail: Option<&str>, + answers: &[PromptButton], + ) -> oneshot::Receiver { + let (tx, rx) = oneshot::channel(); + let answers: Vec = answers.iter().map(|s| s.label().to_string()).collect(); + self.background_executor() + .set_waiting_hint(Some(format!("PROMPT: {:?} {:?}", msg, detail))); + self.prompts + .borrow_mut() + .multiple_choice + .push_back(TestPrompt { + msg: msg.to_string(), + detail: detail.map(|s| s.to_string()), + answers, + tx, + }); + rx + } + + pub(crate) fn set_active_window(&self, window: Option) { + let executor = self.foreground_executor(); + let previous_window = self.active_window.borrow_mut().take(); + self.active_window.borrow_mut().clone_from(&window); + + executor + .spawn(async move { + if let Some(previous_window) = previous_window { + if let Some(window) = window.as_ref() + && Rc::ptr_eq(&previous_window.0, &window.0) + { + return; + } + previous_window.simulate_active_status_change(false); + } + if let Some(window) = window { + window.simulate_active_status_change(true); + } + }) + .detach(); + } + + pub(crate) fn did_prompt_for_new_path(&self) -> bool { + !self.prompts.borrow().new_path.is_empty() + } +} + +impl Platform for TestPlatform { + fn background_executor(&self) -> BackgroundExecutor { + self.background_executor.clone() + } + + fn foreground_executor(&self) -> ForegroundExecutor { + self.foreground_executor.clone() + } + + fn text_system(&self) -> Arc { + self.text_system.clone() + } + + fn keyboard_layout(&self) -> Box { + Box::new(TestKeyboardLayout) + } + + fn keyboard_mapper(&self) -> Rc { + Rc::new(DummyKeyboardMapper) + } + + fn on_keyboard_layout_change(&self, _: Box) {} + + fn run(&self, _on_finish_launching: Box) { + unimplemented!() + } + + fn quit(&self) {} + + fn restart(&self, _: Option) { + // + } + + fn activate(&self, _ignoring_other_apps: bool) { + // + } + + fn hide(&self) { + unimplemented!() + } + + fn hide_other_apps(&self) { + unimplemented!() + } + + fn unhide_other_apps(&self) { + unimplemented!() + } + + fn displays(&self) -> Vec> { + vec![self.active_display.clone()] + } + + fn primary_display(&self) -> Option> { + Some(self.active_display.clone()) + } + + #[cfg(feature = "screen-capture")] + fn is_screen_capture_supported(&self) -> bool { + true + } + + #[cfg(feature = "screen-capture")] + fn screen_capture_sources( + &self, + ) -> oneshot::Receiver>>> { + let (mut tx, rx) = oneshot::channel(); + tx.send(Ok(self + .screen_capture_sources + .borrow() + .iter() + .map(|source| Rc::new(source.clone()) as Rc) + .collect())) + .ok(); + rx + } + + fn active_window(&self) -> Option { + self.active_window + .borrow() + .as_ref() + .map(|window| window.0.lock().handle) + } + + fn open_window( + &self, + handle: AnyWindowHandle, + params: WindowParams, + ) -> anyhow::Result> { + let window = TestWindow::new( + handle, + params, + self.weak.clone(), + self.active_display.clone(), + ); + Ok(Box::new(window)) + } + + fn window_appearance(&self) -> WindowAppearance { + WindowAppearance::Light + } + + fn open_url(&self, url: &str) { + *self.opened_url.borrow_mut() = Some(url.to_string()) + } + + fn on_open_urls(&self, _callback: Box)>) { + unimplemented!() + } + + fn prompt_for_paths( + &self, + _options: crate::PathPromptOptions, + ) -> oneshot::Receiver>>> { + unimplemented!() + } + + fn prompt_for_new_path( + &self, + directory: &std::path::Path, + _suggested_name: Option<&str>, + ) -> oneshot::Receiver>> { + let (tx, rx) = oneshot::channel(); + self.background_executor() + .set_waiting_hint(Some(format!("PROMPT FOR PATH: {:?}", directory))); + self.prompts + .borrow_mut() + .new_path + .push_back((directory.to_path_buf(), tx)); + rx + } + + fn can_select_mixed_files_and_dirs(&self) -> bool { + true + } + + fn reveal_path(&self, _path: &std::path::Path) { + unimplemented!() + } + + fn on_quit(&self, _callback: Box) {} + + fn on_reopen(&self, _callback: Box) { + unimplemented!() + } + + fn set_menus(&self, _menus: Vec, _keymap: &Keymap) {} + fn set_dock_menu(&self, _menu: Vec, _keymap: &Keymap) {} + + fn add_recent_document(&self, _paths: &Path) {} + + fn on_app_menu_action(&self, _callback: Box) {} + + fn on_will_open_app_menu(&self, _callback: Box) {} + + fn on_validate_app_menu_command(&self, _callback: Box bool>) {} + + fn app_path(&self) -> Result { + unimplemented!() + } + + fn path_for_auxiliary_executable(&self, _name: &str) -> Result { + unimplemented!() + } + + fn set_cursor_style(&self, style: crate::CursorStyle) { + *self.active_cursor.lock() = style; + } + + fn should_auto_hide_scrollbars(&self) -> bool { + false + } + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + fn write_to_primary(&self, item: ClipboardItem) { + *self.current_primary_item.lock() = Some(item); + } + + fn write_to_clipboard(&self, item: ClipboardItem) { + *self.current_clipboard_item.lock() = Some(item); + } + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + fn read_from_primary(&self) -> Option { + self.current_primary_item.lock().clone() + } + + fn read_from_clipboard(&self) -> Option { + self.current_clipboard_item.lock().clone() + } + + fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task> { + Task::ready(Ok(())) + } + + fn read_credentials(&self, _url: &str) -> Task)>>> { + Task::ready(Ok(None)) + } + + fn delete_credentials(&self, _url: &str) -> Task> { + Task::ready(Ok(())) + } + + fn register_url_scheme(&self, _: &str) -> Task> { + unimplemented!() + } + + fn open_with_system(&self, _path: &Path) { + unimplemented!() + } +} + +impl TestScreenCaptureSource { + /// Create a fake screen capture source, for testing. + pub fn new() -> Self { + Self {} + } +} + +#[cfg(target_os = "windows")] +impl Drop for TestPlatform { + fn drop(&mut self) { + unsafe { + std::mem::ManuallyDrop::drop(&mut self.bitmap_factory); + windows::Win32::System::Ole::OleUninitialize(); + } + } +} + +struct TestKeyboardLayout; + +impl PlatformKeyboardLayout for TestKeyboardLayout { + fn id(&self) -> &str { + "zed.keyboard.example" + } + + fn name(&self) -> &str { + "zed.keyboard.example" + } +} diff --git a/third_party/gpui/src/platform/test/window.rs b/third_party/gpui/src/platform/test/window.rs new file mode 100644 index 0000000..9e87f45 --- /dev/null +++ b/third_party/gpui/src/platform/test/window.rs @@ -0,0 +1,362 @@ +use crate::{ + AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, DispatchEventResult, GpuSpecs, + Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, + Point, PromptButton, RequestFrameOptions, Size, TestPlatform, TileId, WindowAppearance, + WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowParams, +}; +use collections::HashMap; +use parking_lot::Mutex; +use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; +use std::{ + rc::{Rc, Weak}, + sync::{self, Arc}, +}; + +pub(crate) struct TestWindowState { + pub(crate) bounds: Bounds, + pub(crate) handle: AnyWindowHandle, + display: Rc, + pub(crate) title: Option, + pub(crate) edited: bool, + platform: Weak, + sprite_atlas: Arc, + pub(crate) should_close_handler: Option bool>>, + hit_test_window_control_callback: Option Option>>, + input_callback: Option DispatchEventResult>>, + active_status_change_callback: Option>, + hover_status_change_callback: Option>, + resize_callback: Option, f32)>>, + moved_callback: Option>, + input_handler: Option, + is_fullscreen: bool, +} + +#[derive(Clone)] +pub(crate) struct TestWindow(pub(crate) Rc>); + +impl HasWindowHandle for TestWindow { + fn window_handle( + &self, + ) -> Result, raw_window_handle::HandleError> { + unimplemented!("Test Windows are not backed by a real platform window") + } +} + +impl HasDisplayHandle for TestWindow { + fn display_handle( + &self, + ) -> Result, raw_window_handle::HandleError> { + unimplemented!("Test Windows are not backed by a real platform window") + } +} + +impl TestWindow { + pub fn new( + handle: AnyWindowHandle, + params: WindowParams, + platform: Weak, + display: Rc, + ) -> Self { + Self(Rc::new(Mutex::new(TestWindowState { + bounds: params.bounds, + display, + platform, + handle, + sprite_atlas: Arc::new(TestAtlas::new()), + title: Default::default(), + edited: false, + should_close_handler: None, + hit_test_window_control_callback: None, + input_callback: None, + active_status_change_callback: None, + hover_status_change_callback: None, + resize_callback: None, + moved_callback: None, + input_handler: None, + is_fullscreen: false, + }))) + } + + pub fn simulate_resize(&mut self, size: Size) { + let scale_factor = self.scale_factor(); + let mut lock = self.0.lock(); + let Some(mut callback) = lock.resize_callback.take() else { + return; + }; + lock.bounds.size = size; + drop(lock); + callback(size, scale_factor); + self.0.lock().resize_callback = Some(callback); + } + + pub(crate) fn simulate_active_status_change(&self, active: bool) { + let mut lock = self.0.lock(); + let Some(mut callback) = lock.active_status_change_callback.take() else { + return; + }; + drop(lock); + callback(active); + self.0.lock().active_status_change_callback = Some(callback); + } + + pub fn simulate_input(&mut self, event: PlatformInput) -> bool { + let mut lock = self.0.lock(); + let Some(mut callback) = lock.input_callback.take() else { + return false; + }; + drop(lock); + let result = callback(event); + self.0.lock().input_callback = Some(callback); + !result.propagate + } +} + +impl PlatformWindow for TestWindow { + fn bounds(&self) -> Bounds { + self.0.lock().bounds + } + + fn window_bounds(&self) -> WindowBounds { + WindowBounds::Windowed(self.bounds()) + } + + fn is_maximized(&self) -> bool { + false + } + + fn content_size(&self) -> Size { + self.bounds().size + } + + fn resize(&mut self, size: Size) { + let mut lock = self.0.lock(); + lock.bounds.size = size; + } + + fn scale_factor(&self) -> f32 { + 2.0 + } + + fn appearance(&self) -> WindowAppearance { + WindowAppearance::Light + } + + fn display(&self) -> Option> { + Some(self.0.lock().display.clone()) + } + + fn mouse_position(&self) -> Point { + Point::default() + } + + fn modifiers(&self) -> crate::Modifiers { + crate::Modifiers::default() + } + + fn capslock(&self) -> crate::Capslock { + crate::Capslock::default() + } + + fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { + self.0.lock().input_handler = Some(input_handler); + } + + fn take_input_handler(&mut self) -> Option { + self.0.lock().input_handler.take() + } + + fn prompt( + &self, + _level: crate::PromptLevel, + msg: &str, + detail: Option<&str>, + answers: &[PromptButton], + ) -> Option> { + Some( + self.0 + .lock() + .platform + .upgrade() + .expect("platform dropped") + .prompt(msg, detail, answers), + ) + } + + fn activate(&self) { + self.0 + .lock() + .platform + .upgrade() + .unwrap() + .set_active_window(Some(self.clone())) + } + + fn is_active(&self) -> bool { + false + } + + fn is_hovered(&self) -> bool { + false + } + + fn set_title(&mut self, title: &str) { + self.0.lock().title = Some(title.to_owned()); + } + + fn set_app_id(&mut self, _app_id: &str) {} + + fn set_background_appearance(&self, _background: WindowBackgroundAppearance) {} + + fn set_edited(&mut self, edited: bool) { + self.0.lock().edited = edited; + } + + fn show_character_palette(&self) { + unimplemented!() + } + + fn minimize(&self) { + unimplemented!() + } + + fn zoom(&self) { + unimplemented!() + } + + fn toggle_fullscreen(&self) { + let mut lock = self.0.lock(); + lock.is_fullscreen = !lock.is_fullscreen; + } + + fn is_fullscreen(&self) -> bool { + self.0.lock().is_fullscreen + } + + fn on_request_frame(&self, _callback: Box) {} + + fn on_input(&self, callback: Box DispatchEventResult>) { + self.0.lock().input_callback = Some(callback) + } + + fn on_active_status_change(&self, callback: Box) { + self.0.lock().active_status_change_callback = Some(callback) + } + + fn on_hover_status_change(&self, callback: Box) { + self.0.lock().hover_status_change_callback = Some(callback) + } + + fn on_resize(&self, callback: Box, f32)>) { + self.0.lock().resize_callback = Some(callback) + } + + fn on_moved(&self, callback: Box) { + self.0.lock().moved_callback = Some(callback) + } + + fn on_should_close(&self, callback: Box bool>) { + self.0.lock().should_close_handler = Some(callback); + } + + fn on_close(&self, _callback: Box) {} + + fn on_hit_test_window_control(&self, callback: Box Option>) { + self.0.lock().hit_test_window_control_callback = Some(callback); + } + + fn on_appearance_changed(&self, _callback: Box) {} + + fn draw(&self, _scene: &crate::Scene) {} + + fn sprite_atlas(&self) -> sync::Arc { + self.0.lock().sprite_atlas.clone() + } + + fn as_test(&mut self) -> Option<&mut TestWindow> { + Some(self) + } + + #[cfg(target_os = "windows")] + fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND { + unimplemented!() + } + + fn show_window_menu(&self, _position: Point) { + unimplemented!() + } + + fn start_window_move(&self) { + unimplemented!() + } + + fn update_ime_position(&self, _bounds: Bounds) {} + + fn gpu_specs(&self) -> Option { + None + } +} + +pub(crate) struct TestAtlasState { + next_id: u32, + tiles: HashMap, +} + +pub(crate) struct TestAtlas(Mutex); + +impl TestAtlas { + pub fn new() -> Self { + TestAtlas(Mutex::new(TestAtlasState { + next_id: 0, + tiles: HashMap::default(), + })) + } +} + +impl PlatformAtlas for TestAtlas { + fn get_or_insert_with<'a>( + &self, + key: &crate::AtlasKey, + build: &mut dyn FnMut() -> anyhow::Result< + Option<(Size, std::borrow::Cow<'a, [u8]>)>, + >, + ) -> anyhow::Result> { + let mut state = self.0.lock(); + if let Some(tile) = state.tiles.get(key) { + return Ok(Some(tile.clone())); + } + drop(state); + + let Some((size, _)) = build()? else { + return Ok(None); + }; + + let mut state = self.0.lock(); + state.next_id += 1; + let texture_id = state.next_id; + state.next_id += 1; + let tile_id = state.next_id; + + state.tiles.insert( + key.clone(), + crate::AtlasTile { + texture_id: AtlasTextureId { + index: texture_id, + kind: crate::AtlasTextureKind::Monochrome, + }, + tile_id: TileId(tile_id), + padding: 0, + bounds: crate::Bounds { + origin: Point::default(), + size, + }, + }, + ); + + Ok(Some(state.tiles[key].clone())) + } + + fn remove(&self, key: &AtlasKey) { + let mut state = self.0.lock(); + state.tiles.remove(key); + } +} diff --git a/third_party/gpui/src/platform/windows.rs b/third_party/gpui/src/platform/windows.rs new file mode 100644 index 0000000..9cd1a7d --- /dev/null +++ b/third_party/gpui/src/platform/windows.rs @@ -0,0 +1,40 @@ +mod clipboard; +mod destination_list; +mod direct_write; +mod directx_atlas; +mod directx_devices; +mod directx_renderer; +mod dispatcher; +mod display; +mod events; +mod keyboard; +mod platform; +mod system_settings; +mod util; +mod vsync; +mod window; +mod wrapper; + +pub(crate) use clipboard::*; +pub(crate) use destination_list::*; +pub(crate) use direct_write::*; +pub(crate) use directx_atlas::*; +pub(crate) use directx_devices::*; +pub(crate) use directx_renderer::*; +pub(crate) use dispatcher::*; +pub(crate) use display::*; +pub(crate) use events::*; +pub(crate) use keyboard::*; +pub(crate) use platform::*; +pub(crate) use system_settings::*; +pub(crate) use util::*; +pub(crate) use vsync::*; +pub(crate) use window::*; +pub(crate) use wrapper::*; + +pub(crate) use windows::Win32::Foundation::HWND; + +#[cfg(feature = "screen-capture")] +pub(crate) type PlatformScreenCaptureFrame = scap::frame::Frame; +#[cfg(not(feature = "screen-capture"))] +pub(crate) type PlatformScreenCaptureFrame = (); diff --git a/third_party/gpui/src/platform/windows/alpha_correction.hlsl b/third_party/gpui/src/platform/windows/alpha_correction.hlsl new file mode 100644 index 0000000..dc8d0b5 --- /dev/null +++ b/third_party/gpui/src/platform/windows/alpha_correction.hlsl @@ -0,0 +1,28 @@ +float color_brightness(float3 color) { + // REC. 601 luminance coefficients for perceived brightness + return dot(color, float3(0.30f, 0.59f, 0.11f)); +} + +float light_on_dark_contrast(float enhancedContrast, float3 color) { + float brightness = color_brightness(color); + float multiplier = saturate(4.0f * (0.75f - brightness)); + return enhancedContrast * multiplier; +} + +float enhance_contrast(float alpha, float k) { + return alpha * (k + 1.0f) / (alpha * k + 1.0f); +} + +float apply_alpha_correction(float a, float b, float4 g) { + float brightness_adjustment = g.x * b + g.y; + float correction = brightness_adjustment * a + (g.z * b + g.w); + return a + a * (1.0f - a) * correction; +} + +float apply_contrast_and_gamma_correction(float sample, float3 color, float enhanced_contrast_factor, float4 gamma_ratios) { + float enhanced_contrast = light_on_dark_contrast(enhanced_contrast_factor, color); + float brightness = color_brightness(color); + + float contrasted = enhance_contrast(sample, enhanced_contrast); + return apply_alpha_correction(contrasted, brightness, gamma_ratios); +} diff --git a/third_party/gpui/src/platform/windows/clipboard.rs b/third_party/gpui/src/platform/windows/clipboard.rs new file mode 100644 index 0000000..90d97a8 --- /dev/null +++ b/third_party/gpui/src/platform/windows/clipboard.rs @@ -0,0 +1,388 @@ +use std::sync::LazyLock; + +use anyhow::Result; +use collections::{FxHashMap, FxHashSet}; +use itertools::Itertools; +use windows::Win32::{ + Foundation::{HANDLE, HGLOBAL}, + System::{ + DataExchange::{ + CloseClipboard, CountClipboardFormats, EmptyClipboard, EnumClipboardFormats, + GetClipboardData, GetClipboardFormatNameW, IsClipboardFormatAvailable, OpenClipboard, + RegisterClipboardFormatW, SetClipboardData, + }, + Memory::{GMEM_MOVEABLE, GlobalAlloc, GlobalLock, GlobalSize, GlobalUnlock}, + Ole::{CF_HDROP, CF_UNICODETEXT}, + }, + UI::Shell::{DragQueryFileW, HDROP}, +}; +use windows_core::PCWSTR; + +use crate::{ClipboardEntry, ClipboardItem, ClipboardString, Image, ImageFormat, hash}; + +// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-dragqueryfilew +const DRAGDROP_GET_FILES_COUNT: u32 = 0xFFFFFFFF; + +// Clipboard formats +static CLIPBOARD_HASH_FORMAT: LazyLock = + LazyLock::new(|| register_clipboard_format(windows::core::w!("GPUI internal text hash"))); +static CLIPBOARD_METADATA_FORMAT: LazyLock = + LazyLock::new(|| register_clipboard_format(windows::core::w!("GPUI internal metadata"))); +static CLIPBOARD_SVG_FORMAT: LazyLock = + LazyLock::new(|| register_clipboard_format(windows::core::w!("image/svg+xml"))); +static CLIPBOARD_GIF_FORMAT: LazyLock = + LazyLock::new(|| register_clipboard_format(windows::core::w!("GIF"))); +static CLIPBOARD_PNG_FORMAT: LazyLock = + LazyLock::new(|| register_clipboard_format(windows::core::w!("PNG"))); +static CLIPBOARD_JPG_FORMAT: LazyLock = + LazyLock::new(|| register_clipboard_format(windows::core::w!("JFIF"))); + +// Helper maps and sets +static FORMATS_MAP: LazyLock> = LazyLock::new(|| { + let mut formats_map = FxHashMap::default(); + formats_map.insert(CF_UNICODETEXT.0 as u32, ClipboardFormatType::Text); + formats_map.insert(*CLIPBOARD_PNG_FORMAT, ClipboardFormatType::Image); + formats_map.insert(*CLIPBOARD_GIF_FORMAT, ClipboardFormatType::Image); + formats_map.insert(*CLIPBOARD_JPG_FORMAT, ClipboardFormatType::Image); + formats_map.insert(*CLIPBOARD_SVG_FORMAT, ClipboardFormatType::Image); + formats_map.insert(CF_HDROP.0 as u32, ClipboardFormatType::Files); + formats_map +}); +static FORMATS_SET: LazyLock> = LazyLock::new(|| { + let mut formats_map = FxHashSet::default(); + formats_map.insert(CF_UNICODETEXT.0 as u32); + formats_map.insert(*CLIPBOARD_PNG_FORMAT); + formats_map.insert(*CLIPBOARD_GIF_FORMAT); + formats_map.insert(*CLIPBOARD_JPG_FORMAT); + formats_map.insert(*CLIPBOARD_SVG_FORMAT); + formats_map.insert(CF_HDROP.0 as u32); + formats_map +}); +static IMAGE_FORMATS_MAP: LazyLock> = LazyLock::new(|| { + let mut formats_map = FxHashMap::default(); + formats_map.insert(*CLIPBOARD_PNG_FORMAT, ImageFormat::Png); + formats_map.insert(*CLIPBOARD_GIF_FORMAT, ImageFormat::Gif); + formats_map.insert(*CLIPBOARD_JPG_FORMAT, ImageFormat::Jpeg); + formats_map.insert(*CLIPBOARD_SVG_FORMAT, ImageFormat::Svg); + formats_map +}); + +#[derive(Debug, Clone, Copy)] +enum ClipboardFormatType { + Text, + Image, + Files, +} + +pub(crate) fn write_to_clipboard(item: ClipboardItem) { + with_clipboard(|| write_to_clipboard_inner(item)); +} + +pub(crate) fn read_from_clipboard() -> Option { + with_clipboard(|| { + with_best_match_format(|item_format| match format_to_type(item_format) { + ClipboardFormatType::Text => read_string_from_clipboard(), + ClipboardFormatType::Image => read_image_from_clipboard(item_format), + ClipboardFormatType::Files => read_files_from_clipboard(), + }) + }) + .flatten() +} + +pub(crate) fn with_file_names(hdrop: HDROP, mut f: F) +where + F: FnMut(String), +{ + let file_count = unsafe { DragQueryFileW(hdrop, DRAGDROP_GET_FILES_COUNT, None) }; + for file_index in 0..file_count { + let filename_length = unsafe { DragQueryFileW(hdrop, file_index, None) } as usize; + let mut buffer = vec![0u16; filename_length + 1]; + let ret = unsafe { DragQueryFileW(hdrop, file_index, Some(buffer.as_mut_slice())) }; + if ret == 0 { + log::error!("unable to read file name of dragged file"); + continue; + } + match String::from_utf16(&buffer[0..filename_length]) { + Ok(file_name) => f(file_name), + Err(e) => { + log::error!("dragged file name is not UTF-16: {}", e) + } + } + } +} + +fn with_clipboard(f: F) -> Option +where + F: FnOnce() -> T, +{ + match unsafe { OpenClipboard(None) } { + Ok(()) => { + let result = f(); + if let Err(e) = unsafe { CloseClipboard() } { + log::error!("Failed to close clipboard: {e}",); + } + Some(result) + } + Err(e) => { + log::error!("Failed to open clipboard: {e}",); + None + } + } +} + +fn register_clipboard_format(format: PCWSTR) -> u32 { + let ret = unsafe { RegisterClipboardFormatW(format) }; + if ret == 0 { + panic!( + "Error when registering clipboard format: {}", + std::io::Error::last_os_error() + ); + } + ret +} + +#[inline] +fn format_to_type(item_format: u32) -> &'static ClipboardFormatType { + FORMATS_MAP.get(&item_format).unwrap() +} + +// Currently, we only write the first item. +fn write_to_clipboard_inner(item: ClipboardItem) -> Result<()> { + unsafe { + EmptyClipboard()?; + } + match item.entries().first() { + Some(entry) => match entry { + ClipboardEntry::String(string) => { + write_string_to_clipboard(string)?; + } + ClipboardEntry::Image(image) => { + write_image_to_clipboard(image)?; + } + }, + None => { + // Writing an empty list of entries just clears the clipboard. + } + } + Ok(()) +} + +fn write_string_to_clipboard(item: &ClipboardString) -> Result<()> { + let encode_wide = item.text.encode_utf16().chain(Some(0)).collect_vec(); + set_data_to_clipboard(&encode_wide, CF_UNICODETEXT.0 as u32)?; + + if let Some(metadata) = item.metadata.as_ref() { + let hash_result = { + let hash = ClipboardString::text_hash(&item.text); + hash.to_ne_bytes() + }; + let encode_wide = + unsafe { std::slice::from_raw_parts(hash_result.as_ptr().cast::(), 4) }; + set_data_to_clipboard(encode_wide, *CLIPBOARD_HASH_FORMAT)?; + + let metadata_wide = metadata.encode_utf16().chain(Some(0)).collect_vec(); + set_data_to_clipboard(&metadata_wide, *CLIPBOARD_METADATA_FORMAT)?; + } + Ok(()) +} + +fn set_data_to_clipboard(data: &[T], format: u32) -> Result<()> { + unsafe { + let global = GlobalAlloc(GMEM_MOVEABLE, std::mem::size_of_val(data))?; + let handle = GlobalLock(global); + std::ptr::copy_nonoverlapping(data.as_ptr(), handle as _, data.len()); + let _ = GlobalUnlock(global); + SetClipboardData(format, Some(HANDLE(global.0)))?; + } + Ok(()) +} + +// Here writing PNG to the clipboard to better support other apps. For more info, please ref to +// the PR. +fn write_image_to_clipboard(item: &Image) -> Result<()> { + match item.format { + ImageFormat::Svg => set_data_to_clipboard(item.bytes(), *CLIPBOARD_SVG_FORMAT)?, + ImageFormat::Gif => { + set_data_to_clipboard(item.bytes(), *CLIPBOARD_GIF_FORMAT)?; + let png_bytes = convert_image_to_png_format(item.bytes(), ImageFormat::Gif)?; + set_data_to_clipboard(&png_bytes, *CLIPBOARD_PNG_FORMAT)?; + } + ImageFormat::Png => { + set_data_to_clipboard(item.bytes(), *CLIPBOARD_PNG_FORMAT)?; + let png_bytes = convert_image_to_png_format(item.bytes(), ImageFormat::Png)?; + set_data_to_clipboard(&png_bytes, *CLIPBOARD_PNG_FORMAT)?; + } + ImageFormat::Jpeg => { + set_data_to_clipboard(item.bytes(), *CLIPBOARD_JPG_FORMAT)?; + let png_bytes = convert_image_to_png_format(item.bytes(), ImageFormat::Jpeg)?; + set_data_to_clipboard(&png_bytes, *CLIPBOARD_PNG_FORMAT)?; + } + other => { + log::warn!( + "Clipboard unsupported image format: {:?}, convert to PNG instead.", + item.format + ); + let png_bytes = convert_image_to_png_format(item.bytes(), other)?; + set_data_to_clipboard(&png_bytes, *CLIPBOARD_PNG_FORMAT)?; + } + } + Ok(()) +} + +fn convert_image_to_png_format(bytes: &[u8], image_format: ImageFormat) -> Result> { + let image = image::load_from_memory_with_format(bytes, image_format.into())?; + let mut output_buf = Vec::new(); + image.write_to( + &mut std::io::Cursor::new(&mut output_buf), + image::ImageFormat::Png, + )?; + Ok(output_buf) +} + +// Here, we enumerate all formats on the clipboard and find the first one that we can process. +// The reason we don't use `GetPriorityClipboardFormat` is that it sometimes returns the +// wrong format. +// For instance, when copying a JPEG image from Microsoft Word, there may be several formats +// on the clipboard: Jpeg, Png, Svg. +// If we use `GetPriorityClipboardFormat`, it will return Svg, which is not what we want. +fn with_best_match_format(f: F) -> Option +where + F: Fn(u32) -> Option, +{ + let count = unsafe { CountClipboardFormats() }; + let mut clipboard_format = 0; + for _ in 0..count { + clipboard_format = unsafe { EnumClipboardFormats(clipboard_format) }; + let Some(item_format) = FORMATS_SET.get(&clipboard_format) else { + continue; + }; + if let Some(entry) = f(*item_format) { + return Some(ClipboardItem { + entries: vec![entry], + }); + } + } + // log the formats that we don't support yet. + { + clipboard_format = 0; + for _ in 0..count { + clipboard_format = unsafe { EnumClipboardFormats(clipboard_format) }; + let mut buffer = [0u16; 64]; + unsafe { GetClipboardFormatNameW(clipboard_format, &mut buffer) }; + let format_name = String::from_utf16_lossy(&buffer); + log::warn!( + "Try to paste with unsupported clipboard format: {}, {}.", + clipboard_format, + format_name + ); + } + } + None +} + +fn read_string_from_clipboard() -> Option { + let text = with_clipboard_data(CF_UNICODETEXT.0 as u32, |data_ptr, _| { + let pcwstr = PCWSTR(data_ptr as *const u16); + String::from_utf16_lossy(unsafe { pcwstr.as_wide() }) + })?; + let Some(hash) = read_hash_from_clipboard() else { + return Some(ClipboardEntry::String(ClipboardString::new(text))); + }; + let Some(metadata) = read_metadata_from_clipboard() else { + return Some(ClipboardEntry::String(ClipboardString::new(text))); + }; + if hash == ClipboardString::text_hash(&text) { + Some(ClipboardEntry::String(ClipboardString { + text, + metadata: Some(metadata), + })) + } else { + Some(ClipboardEntry::String(ClipboardString::new(text))) + } +} + +fn read_hash_from_clipboard() -> Option { + if unsafe { IsClipboardFormatAvailable(*CLIPBOARD_HASH_FORMAT).is_err() } { + return None; + } + with_clipboard_data(*CLIPBOARD_HASH_FORMAT, |data_ptr, size| { + if size < 8 { + return None; + } + let hash_bytes: [u8; 8] = unsafe { + std::slice::from_raw_parts(data_ptr.cast::(), 8) + .try_into() + .ok() + }?; + Some(u64::from_ne_bytes(hash_bytes)) + })? +} + +fn read_metadata_from_clipboard() -> Option { + unsafe { IsClipboardFormatAvailable(*CLIPBOARD_METADATA_FORMAT).ok()? }; + with_clipboard_data(*CLIPBOARD_METADATA_FORMAT, |data_ptr, _size| { + let pcwstr = PCWSTR(data_ptr as *const u16); + String::from_utf16_lossy(unsafe { pcwstr.as_wide() }) + }) +} + +fn read_image_from_clipboard(format: u32) -> Option { + let image_format = format_number_to_image_format(format)?; + read_image_for_type(format, *image_format) +} + +#[inline] +fn format_number_to_image_format(format_number: u32) -> Option<&'static ImageFormat> { + IMAGE_FORMATS_MAP.get(&format_number) +} + +fn read_image_for_type(format_number: u32, format: ImageFormat) -> Option { + let (bytes, id) = with_clipboard_data(format_number, |data_ptr, size| { + let bytes = unsafe { std::slice::from_raw_parts(data_ptr as *mut u8 as _, size).to_vec() }; + let id = hash(&bytes); + (bytes, id) + })?; + Some(ClipboardEntry::Image(Image { format, bytes, id })) +} + +fn read_files_from_clipboard() -> Option { + let text = with_clipboard_data(CF_HDROP.0 as u32, |data_ptr, _size| { + let hdrop = HDROP(data_ptr); + let mut filenames = String::new(); + with_file_names(hdrop, |file_name| { + filenames.push_str(&file_name); + }); + filenames + })?; + Some(ClipboardEntry::String(ClipboardString { + text, + metadata: None, + })) +} + +fn with_clipboard_data(format: u32, f: F) -> Option +where + F: FnOnce(*mut std::ffi::c_void, usize) -> R, +{ + let global = HGLOBAL(unsafe { GetClipboardData(format).ok() }?.0); + let size = unsafe { GlobalSize(global) }; + let data_ptr = unsafe { GlobalLock(global) }; + let result = f(data_ptr, size); + unsafe { GlobalUnlock(global).ok() }; + Some(result) +} + +impl From for image::ImageFormat { + fn from(value: ImageFormat) -> Self { + match value { + ImageFormat::Png => image::ImageFormat::Png, + ImageFormat::Jpeg => image::ImageFormat::Jpeg, + ImageFormat::Webp => image::ImageFormat::WebP, + ImageFormat::Gif => image::ImageFormat::Gif, + // TODO: ImageFormat::Svg + ImageFormat::Bmp => image::ImageFormat::Bmp, + ImageFormat::Tiff => image::ImageFormat::Tiff, + _ => unreachable!(), + } + } +} diff --git a/third_party/gpui/src/platform/windows/color_text_raster.hlsl b/third_party/gpui/src/platform/windows/color_text_raster.hlsl new file mode 100644 index 0000000..2fbc156 --- /dev/null +++ b/third_party/gpui/src/platform/windows/color_text_raster.hlsl @@ -0,0 +1,44 @@ +#include "alpha_correction.hlsl" + +struct RasterVertexOutput { + float4 position : SV_Position; + float2 texcoord : TEXCOORD0; +}; + +RasterVertexOutput emoji_rasterization_vertex(uint vertexID : SV_VERTEXID) +{ + RasterVertexOutput output; + output.texcoord = float2((vertexID << 1) & 2, vertexID & 2); + output.position = float4(output.texcoord * 2.0f - 1.0f, 0.0f, 1.0f); + output.position.y = -output.position.y; + + return output; +} + +struct PixelInput { + float4 position: SV_Position; + float2 texcoord : TEXCOORD0; +}; + +struct Bounds { + int2 origin; + int2 size; +}; + +Texture2D t_layer : register(t0); +SamplerState s_layer : register(s0); + +cbuffer GlyphLayerTextureParams : register(b0) { + Bounds bounds; + float4 run_color; + float4 gamma_ratios; + float grayscale_enhanced_contrast; + float3 _pad; +}; + +float4 emoji_rasterization_fragment(PixelInput input): SV_Target { + float sample = t_layer.Sample(s_layer, input.texcoord.xy).r; + float alpha_corrected = apply_contrast_and_gamma_correction(sample, run_color.rgb, grayscale_enhanced_contrast, gamma_ratios); + float alpha = alpha_corrected * run_color.a; + return float4(run_color.rgb * alpha, alpha); +} diff --git a/third_party/gpui/src/platform/windows/destination_list.rs b/third_party/gpui/src/platform/windows/destination_list.rs new file mode 100644 index 0000000..fdfa52a --- /dev/null +++ b/third_party/gpui/src/platform/windows/destination_list.rs @@ -0,0 +1,201 @@ +use std::path::PathBuf; + +use itertools::Itertools; +use smallvec::SmallVec; +use windows::{ + Win32::{ + Foundation::PROPERTYKEY, + Globalization::u_strlen, + System::Com::{CLSCTX_INPROC_SERVER, CoCreateInstance, StructuredStorage::PROPVARIANT}, + UI::{ + Controls::INFOTIPSIZE, + Shell::{ + Common::{IObjectArray, IObjectCollection}, + DestinationList, EnumerableObjectCollection, ICustomDestinationList, IShellLinkW, + PropertiesSystem::IPropertyStore, + ShellLink, + }, + }, + }, + core::{GUID, HSTRING, Interface}, +}; + +use crate::{Action, MenuItem}; + +pub(crate) struct JumpList { + pub(crate) dock_menus: Vec, + pub(crate) recent_workspaces: Vec>, +} + +impl JumpList { + pub(crate) fn new() -> Self { + Self { + dock_menus: Vec::new(), + recent_workspaces: Vec::new(), + } + } +} + +pub(crate) struct DockMenuItem { + pub(crate) name: String, + pub(crate) description: String, + pub(crate) action: Box, +} + +impl DockMenuItem { + pub(crate) fn new(item: MenuItem) -> anyhow::Result { + match item { + MenuItem::Action { name, action, .. } => Ok(Self { + name: name.clone().into(), + description: if name == "New Window" { + "Opens a new window".to_string() + } else { + name.into() + }, + action, + }), + _ => anyhow::bail!("Only `MenuItem::Action` is supported for dock menu on Windows."), + } + } +} + +// This code is based on the example from Microsoft: +// https://github.com/microsoft/Windows-classic-samples/blob/main/Samples/Win7Samples/winui/shell/appshellintegration/RecipePropertyHandler/RecipePropertyHandler.cpp +pub(crate) fn update_jump_list( + jump_list: &JumpList, +) -> anyhow::Result>> { + let (list, removed) = create_destination_list()?; + add_recent_folders(&list, &jump_list.recent_workspaces, removed.as_ref())?; + add_dock_menu(&list, &jump_list.dock_menus)?; + unsafe { list.CommitList() }?; + Ok(removed) +} + +// Copied from: +// https://github.com/microsoft/windows-rs/blob/0fc3c2e5a13d4316d242bdeb0a52af611eba8bd4/crates/libs/windows/src/Windows/Win32/Storage/EnhancedStorage/mod.rs#L1881 +const PKEY_TITLE: PROPERTYKEY = PROPERTYKEY { + fmtid: GUID::from_u128(0xf29f85e0_4ff9_1068_ab91_08002b27b3d9), + pid: 2, +}; + +fn create_destination_list() -> anyhow::Result<(ICustomDestinationList, Vec>)> +{ + let list: ICustomDestinationList = + unsafe { CoCreateInstance(&DestinationList, None, CLSCTX_INPROC_SERVER) }?; + + let mut slots = 0; + let user_removed: IObjectArray = unsafe { list.BeginList(&mut slots) }?; + + let count = unsafe { user_removed.GetCount() }?; + if count == 0 { + return Ok((list, Vec::new())); + } + + let mut removed = Vec::with_capacity(count as usize); + for i in 0..count { + let shell_link: IShellLinkW = unsafe { user_removed.GetAt(i)? }; + let description = { + // INFOTIPSIZE is the maximum size of the buffer + // see https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ishelllinkw-getdescription + let mut buffer = [0u16; INFOTIPSIZE as usize]; + unsafe { shell_link.GetDescription(&mut buffer)? }; + let len = unsafe { u_strlen(buffer.as_ptr()) }; + String::from_utf16_lossy(&buffer[..len as usize]) + }; + let args = description.split('\n').map(PathBuf::from).collect(); + + removed.push(args); + } + + Ok((list, removed)) +} + +fn add_dock_menu(list: &ICustomDestinationList, dock_menus: &[DockMenuItem]) -> anyhow::Result<()> { + unsafe { + let tasks: IObjectCollection = + CoCreateInstance(&EnumerableObjectCollection, None, CLSCTX_INPROC_SERVER)?; + for (idx, dock_menu) in dock_menus.iter().enumerate() { + let argument = HSTRING::from(format!("--dock-action {}", idx)); + let description = HSTRING::from(dock_menu.description.as_str()); + let display = dock_menu.name.as_str(); + let task = create_shell_link(argument, description, None, display)?; + tasks.AddObject(&task)?; + } + list.AddUserTasks(&tasks)?; + Ok(()) + } +} + +fn add_recent_folders( + list: &ICustomDestinationList, + entries: &[SmallVec<[PathBuf; 2]>], + removed: &Vec>, +) -> anyhow::Result<()> { + unsafe { + let tasks: IObjectCollection = + CoCreateInstance(&EnumerableObjectCollection, None, CLSCTX_INPROC_SERVER)?; + + for folder_path in entries.iter().filter(|path| !removed.contains(path)) { + let argument = HSTRING::from( + folder_path + .iter() + .map(|path| format!("\"{}\"", path.display())) + .join(" "), + ); + + let description = HSTRING::from( + folder_path + .iter() + .map(|path| path.to_string_lossy()) + .collect::>() + .join("\n"), + ); + // simulate folder icon + // https://github.com/microsoft/vscode/blob/7a5dc239516a8953105da34f84bae152421a8886/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts#L380 + let icon = HSTRING::from("explorer.exe"); + + let display = folder_path + .iter() + .map(|p| { + p.file_name() + .map(|name| name.to_string_lossy()) + .unwrap_or_else(|| p.to_string_lossy()) + }) + .join(", "); + + tasks.AddObject(&create_shell_link( + argument, + description, + Some(icon), + &display, + )?)?; + } + + list.AppendCategory(&HSTRING::from("Recent Folders"), &tasks)?; + Ok(()) + } +} + +fn create_shell_link( + argument: HSTRING, + description: HSTRING, + icon: Option, + display: &str, +) -> anyhow::Result { + unsafe { + let link: IShellLinkW = CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER)?; + let exe_path = HSTRING::from(std::env::current_exe()?.as_os_str()); + link.SetPath(&exe_path)?; + link.SetArguments(&argument)?; + link.SetDescription(&description)?; + if let Some(icon) = icon { + link.SetIconLocation(&icon, 0)?; + } + let store: IPropertyStore = link.cast()?; + let title = PROPVARIANT::from(display); + store.SetValue(&PKEY_TITLE, &title)?; + store.Commit()?; + + Ok(link) + } +} diff --git a/third_party/gpui/src/platform/windows/direct_write.rs b/third_party/gpui/src/platform/windows/direct_write.rs new file mode 100644 index 0000000..e187fc4 --- /dev/null +++ b/third_party/gpui/src/platform/windows/direct_write.rs @@ -0,0 +1,1923 @@ +use std::{borrow::Cow, sync::Arc}; + +use ::util::ResultExt; +use anyhow::{Context, Result}; +use collections::HashMap; +use itertools::Itertools; +use parking_lot::{RwLock, RwLockUpgradableReadGuard}; +use windows::{ + Win32::{ + Foundation::*, + Globalization::GetUserDefaultLocaleName, + Graphics::{ + Direct3D::D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP, Direct3D11::*, DirectWrite::*, + Dxgi::Common::*, Gdi::LOGFONTW, + }, + System::SystemServices::LOCALE_NAME_MAX_LENGTH, + UI::WindowsAndMessaging::*, + }, + core::*, +}; +use windows_numerics::Vector2; + +use crate::*; + +#[derive(Debug)] +struct FontInfo { + font_family: String, + font_face: IDWriteFontFace3, + features: IDWriteTypography, + fallbacks: Option, + is_system_font: bool, +} + +pub(crate) struct DirectWriteTextSystem(RwLock); + +struct DirectWriteComponent { + locale: String, + factory: IDWriteFactory5, + in_memory_loader: IDWriteInMemoryFontFileLoader, + builder: IDWriteFontSetBuilder1, + text_renderer: Arc, + + gpu_state: GPUState, +} + +struct GPUState { + device: ID3D11Device, + device_context: ID3D11DeviceContext, + sampler: [Option; 1], + blend_state: ID3D11BlendState, + vertex_shader: ID3D11VertexShader, + pixel_shader: ID3D11PixelShader, +} + +struct DirectWriteState { + components: DirectWriteComponent, + system_ui_font_name: SharedString, + system_font_collection: IDWriteFontCollection1, + custom_font_collection: IDWriteFontCollection1, + fonts: Vec, + font_selections: HashMap, + font_id_by_identifier: HashMap, +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +struct FontIdentifier { + postscript_name: String, + weight: i32, + style: i32, +} + +impl DirectWriteComponent { + pub fn new(directx_devices: &DirectXDevices) -> Result { + // todo: ideally this would not be a large unsafe block but smaller isolated ones for easier auditing + unsafe { + let factory: IDWriteFactory5 = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED)?; + // The `IDWriteInMemoryFontFileLoader` here is supported starting from + // Windows 10 Creators Update, which consequently requires the entire + // `DirectWriteTextSystem` to run on `win10 1703`+. + let in_memory_loader = factory.CreateInMemoryFontFileLoader()?; + factory.RegisterFontFileLoader(&in_memory_loader)?; + let builder = factory.CreateFontSetBuilder()?; + let mut locale_vec = vec![0u16; LOCALE_NAME_MAX_LENGTH as usize]; + GetUserDefaultLocaleName(&mut locale_vec); + let locale = String::from_utf16_lossy(&locale_vec); + let text_renderer = Arc::new(TextRendererWrapper::new(&locale)); + + let gpu_state = GPUState::new(directx_devices)?; + + Ok(DirectWriteComponent { + locale, + factory, + in_memory_loader, + builder, + text_renderer, + gpu_state, + }) + } + } +} + +impl GPUState { + fn new(directx_devices: &DirectXDevices) -> Result { + let device = directx_devices.device.clone(); + let device_context = directx_devices.device_context.clone(); + + let blend_state = { + let mut blend_state = None; + let desc = D3D11_BLEND_DESC { + AlphaToCoverageEnable: false.into(), + IndependentBlendEnable: false.into(), + RenderTarget: [ + D3D11_RENDER_TARGET_BLEND_DESC { + BlendEnable: true.into(), + SrcBlend: D3D11_BLEND_ONE, + DestBlend: D3D11_BLEND_INV_SRC_ALPHA, + BlendOp: D3D11_BLEND_OP_ADD, + SrcBlendAlpha: D3D11_BLEND_ONE, + DestBlendAlpha: D3D11_BLEND_INV_SRC_ALPHA, + BlendOpAlpha: D3D11_BLEND_OP_ADD, + RenderTargetWriteMask: D3D11_COLOR_WRITE_ENABLE_ALL.0 as u8, + }, + Default::default(), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + ], + }; + unsafe { device.CreateBlendState(&desc, Some(&mut blend_state)) }?; + blend_state.unwrap() + }; + + let sampler = { + let mut sampler = None; + let desc = D3D11_SAMPLER_DESC { + Filter: D3D11_FILTER_MIN_MAG_MIP_POINT, + AddressU: D3D11_TEXTURE_ADDRESS_BORDER, + AddressV: D3D11_TEXTURE_ADDRESS_BORDER, + AddressW: D3D11_TEXTURE_ADDRESS_BORDER, + MipLODBias: 0.0, + MaxAnisotropy: 1, + ComparisonFunc: D3D11_COMPARISON_ALWAYS, + BorderColor: [0.0, 0.0, 0.0, 0.0], + MinLOD: 0.0, + MaxLOD: 0.0, + }; + unsafe { device.CreateSamplerState(&desc, Some(&mut sampler)) }?; + [sampler] + }; + + let vertex_shader = { + let source = shader_resources::RawShaderBytes::new( + shader_resources::ShaderModule::EmojiRasterization, + shader_resources::ShaderTarget::Vertex, + )?; + let mut shader = None; + unsafe { device.CreateVertexShader(source.as_bytes(), None, Some(&mut shader)) }?; + shader.unwrap() + }; + + let pixel_shader = { + let source = shader_resources::RawShaderBytes::new( + shader_resources::ShaderModule::EmojiRasterization, + shader_resources::ShaderTarget::Fragment, + )?; + let mut shader = None; + unsafe { device.CreatePixelShader(source.as_bytes(), None, Some(&mut shader)) }?; + shader.unwrap() + }; + + Ok(Self { + device, + device_context, + sampler, + blend_state, + vertex_shader, + pixel_shader, + }) + } +} + +impl DirectWriteTextSystem { + pub(crate) fn new(directx_devices: &DirectXDevices) -> Result { + let components = DirectWriteComponent::new(directx_devices)?; + let system_font_collection = unsafe { + let mut result = std::mem::zeroed(); + components + .factory + .GetSystemFontCollection(false, &mut result, true)?; + result.unwrap() + }; + let custom_font_set = unsafe { components.builder.CreateFontSet()? }; + let custom_font_collection = unsafe { + components + .factory + .CreateFontCollectionFromFontSet(&custom_font_set)? + }; + let system_ui_font_name = get_system_ui_font_name(); + + Ok(Self(RwLock::new(DirectWriteState { + components, + system_ui_font_name, + system_font_collection, + custom_font_collection, + fonts: Vec::new(), + font_selections: HashMap::default(), + font_id_by_identifier: HashMap::default(), + }))) + } + + pub(crate) fn handle_gpu_lost(&self, directx_devices: &DirectXDevices) { + self.0.write().handle_gpu_lost(directx_devices); + } +} + +impl PlatformTextSystem for DirectWriteTextSystem { + fn add_fonts(&self, fonts: Vec>) -> Result<()> { + self.0.write().add_fonts(fonts) + } + + fn all_font_names(&self) -> Vec { + self.0.read().all_font_names() + } + + fn font_id(&self, font: &Font) -> Result { + let lock = self.0.upgradable_read(); + if let Some(font_id) = lock.font_selections.get(font) { + Ok(*font_id) + } else { + let mut lock = RwLockUpgradableReadGuard::upgrade(lock); + let font_id = lock.select_font(font); + lock.font_selections.insert(font.clone(), font_id); + Ok(font_id) + } + } + + fn font_metrics(&self, font_id: FontId) -> FontMetrics { + self.0.read().font_metrics(font_id) + } + + fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + self.0.read().get_typographic_bounds(font_id, glyph_id) + } + + fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result> { + self.0.read().get_advance(font_id, glyph_id) + } + + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { + self.0.read().glyph_for_char(font_id, ch) + } + + fn glyph_raster_bounds( + &self, + params: &RenderGlyphParams, + ) -> anyhow::Result> { + self.0.read().raster_bounds(params) + } + + fn rasterize_glyph( + &self, + params: &RenderGlyphParams, + raster_bounds: Bounds, + ) -> anyhow::Result<(Size, Vec)> { + self.0.read().rasterize_glyph(params, raster_bounds) + } + + fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout { + self.0 + .write() + .layout_line(text, font_size, runs) + .log_err() + .unwrap_or(LineLayout { + font_size, + ..Default::default() + }) + } +} + +impl DirectWriteState { + fn add_fonts(&mut self, fonts: Vec>) -> Result<()> { + for font_data in fonts { + match font_data { + Cow::Borrowed(data) => unsafe { + let font_file = self + .components + .in_memory_loader + .CreateInMemoryFontFileReference( + &self.components.factory, + data.as_ptr() as _, + data.len() as _, + None, + )?; + self.components.builder.AddFontFile(&font_file)?; + }, + Cow::Owned(data) => unsafe { + let font_file = self + .components + .in_memory_loader + .CreateInMemoryFontFileReference( + &self.components.factory, + data.as_ptr() as _, + data.len() as _, + None, + )?; + self.components.builder.AddFontFile(&font_file)?; + }, + } + } + let set = unsafe { self.components.builder.CreateFontSet()? }; + let collection = unsafe { + self.components + .factory + .CreateFontCollectionFromFontSet(&set)? + }; + self.custom_font_collection = collection; + + Ok(()) + } + + fn generate_font_fallbacks( + &self, + fallbacks: &FontFallbacks, + ) -> Result> { + if fallbacks.fallback_list().is_empty() { + return Ok(None); + } + unsafe { + let builder = self.components.factory.CreateFontFallbackBuilder()?; + let font_set = &self.system_font_collection.GetFontSet()?; + for family_name in fallbacks.fallback_list() { + let Some(fonts) = font_set + .GetMatchingFonts( + &HSTRING::from(family_name), + DWRITE_FONT_WEIGHT_NORMAL, + DWRITE_FONT_STRETCH_NORMAL, + DWRITE_FONT_STYLE_NORMAL, + ) + .log_err() + else { + continue; + }; + if fonts.GetFontCount() == 0 { + log::error!("No matching font found for {}", family_name); + continue; + } + let font = fonts.GetFontFaceReference(0)?.CreateFontFace()?; + let mut count = 0; + font.GetUnicodeRanges(None, &mut count).ok(); + if count == 0 { + continue; + } + let mut unicode_ranges = vec![DWRITE_UNICODE_RANGE::default(); count as usize]; + let Some(_) = font + .GetUnicodeRanges(Some(&mut unicode_ranges), &mut count) + .log_err() + else { + continue; + }; + let target_family_name = HSTRING::from(family_name); + builder.AddMapping( + &unicode_ranges, + &[target_family_name.as_ptr()], + None, + None, + None, + 1.0, + )?; + } + let system_fallbacks = self.components.factory.GetSystemFontFallback()?; + builder.AddMappings(&system_fallbacks)?; + Ok(Some(builder.CreateFontFallback()?)) + } + } + + unsafe fn generate_font_features( + &self, + font_features: &FontFeatures, + ) -> Result { + let direct_write_features = unsafe { self.components.factory.CreateTypography()? }; + apply_font_features(&direct_write_features, font_features)?; + Ok(direct_write_features) + } + + unsafe fn get_font_id_from_font_collection( + &mut self, + family_name: &str, + font_weight: FontWeight, + font_style: FontStyle, + font_features: &FontFeatures, + font_fallbacks: Option<&FontFallbacks>, + is_system_font: bool, + ) -> Option { + let collection = if is_system_font { + &self.system_font_collection + } else { + &self.custom_font_collection + }; + let fontset = unsafe { collection.GetFontSet().log_err()? }; + let font = unsafe { + fontset + .GetMatchingFonts( + &HSTRING::from(family_name), + font_weight.into(), + DWRITE_FONT_STRETCH_NORMAL, + font_style.into(), + ) + .log_err()? + }; + let total_number = unsafe { font.GetFontCount() }; + for index in 0..total_number { + let Some(font_face_ref) = (unsafe { font.GetFontFaceReference(index).log_err() }) + else { + continue; + }; + let Some(font_face) = (unsafe { font_face_ref.CreateFontFace().log_err() }) else { + continue; + }; + let Some(identifier) = get_font_identifier(&font_face, &self.components.locale) else { + continue; + }; + let Some(direct_write_features) = + (unsafe { self.generate_font_features(font_features).log_err() }) + else { + continue; + }; + let fallbacks = font_fallbacks + .and_then(|fallbacks| self.generate_font_fallbacks(fallbacks).log_err().flatten()); + let font_info = FontInfo { + font_family: family_name.to_owned(), + font_face, + features: direct_write_features, + fallbacks, + is_system_font, + }; + let font_id = FontId(self.fonts.len()); + self.fonts.push(font_info); + self.font_id_by_identifier.insert(identifier, font_id); + return Some(font_id); + } + None + } + + unsafe fn update_system_font_collection(&mut self) { + let mut collection = unsafe { std::mem::zeroed() }; + if unsafe { + self.components + .factory + .GetSystemFontCollection(false, &mut collection, true) + .log_err() + .is_some() + } { + self.system_font_collection = collection.unwrap(); + } + } + + fn select_font(&mut self, target_font: &Font) -> FontId { + unsafe { + if target_font.family == ".SystemUIFont" { + let family = self.system_ui_font_name.clone(); + self.find_font_id( + family.as_ref(), + target_font.weight, + target_font.style, + &target_font.features, + target_font.fallbacks.as_ref(), + ) + .unwrap() + } else { + let family = self.system_ui_font_name.clone(); + self.find_font_id( + font_name_with_fallbacks(target_font.family.as_ref(), family.as_ref()), + target_font.weight, + target_font.style, + &target_font.features, + target_font.fallbacks.as_ref(), + ) + .unwrap_or_else(|| { + #[cfg(any(test, feature = "test-support"))] + { + panic!("ERROR: {} font not found!", target_font.family); + } + #[cfg(not(any(test, feature = "test-support")))] + { + log::error!("{} not found, use {} instead.", target_font.family, family); + self.get_font_id_from_font_collection( + family.as_ref(), + target_font.weight, + target_font.style, + &target_font.features, + target_font.fallbacks.as_ref(), + true, + ) + .unwrap() + } + }) + } + } + } + + unsafe fn find_font_id( + &mut self, + family_name: &str, + weight: FontWeight, + style: FontStyle, + features: &FontFeatures, + fallbacks: Option<&FontFallbacks>, + ) -> Option { + // try to find target font in custom font collection first + unsafe { + self.get_font_id_from_font_collection( + family_name, + weight, + style, + features, + fallbacks, + false, + ) + .or_else(|| { + self.get_font_id_from_font_collection( + family_name, + weight, + style, + features, + fallbacks, + true, + ) + }) + .or_else(|| { + self.update_system_font_collection(); + self.get_font_id_from_font_collection( + family_name, + weight, + style, + features, + fallbacks, + true, + ) + }) + } + } + + fn layout_line( + &mut self, + text: &str, + font_size: Pixels, + font_runs: &[FontRun], + ) -> Result { + if font_runs.is_empty() { + return Ok(LineLayout { + font_size, + ..Default::default() + }); + } + unsafe { + let text_renderer = self.components.text_renderer.clone(); + let text_wide = text.encode_utf16().collect_vec(); + + let mut utf8_offset = 0usize; + let mut utf16_offset = 0u32; + let text_layout = { + let first_run = &font_runs[0]; + let font_info = &self.fonts[first_run.font_id.0]; + let collection = if font_info.is_system_font { + &self.system_font_collection + } else { + &self.custom_font_collection + }; + let format: IDWriteTextFormat1 = self + .components + .factory + .CreateTextFormat( + &HSTRING::from(&font_info.font_family), + collection, + font_info.font_face.GetWeight(), + font_info.font_face.GetStyle(), + DWRITE_FONT_STRETCH_NORMAL, + font_size.0, + &HSTRING::from(&self.components.locale), + )? + .cast()?; + if let Some(ref fallbacks) = font_info.fallbacks { + format.SetFontFallback(fallbacks)?; + } + + let layout = self.components.factory.CreateTextLayout( + &text_wide, + &format, + f32::INFINITY, + f32::INFINITY, + )?; + let current_text = &text[utf8_offset..(utf8_offset + first_run.len)]; + utf8_offset += first_run.len; + let current_text_utf16_length = current_text.encode_utf16().count() as u32; + let text_range = DWRITE_TEXT_RANGE { + startPosition: utf16_offset, + length: current_text_utf16_length, + }; + layout.SetTypography(&font_info.features, text_range)?; + utf16_offset += current_text_utf16_length; + + layout + }; + + let mut first_run = true; + let mut ascent = Pixels::default(); + let mut descent = Pixels::default(); + for run in font_runs { + if first_run { + first_run = false; + let mut metrics = vec![DWRITE_LINE_METRICS::default(); 4]; + let mut line_count = 0u32; + text_layout.GetLineMetrics(Some(&mut metrics), &mut line_count as _)?; + ascent = px(metrics[0].baseline); + descent = px(metrics[0].height - metrics[0].baseline); + continue; + } + let font_info = &self.fonts[run.font_id.0]; + let current_text = &text[utf8_offset..(utf8_offset + run.len)]; + utf8_offset += run.len; + let current_text_utf16_length = current_text.encode_utf16().count() as u32; + + let collection = if font_info.is_system_font { + &self.system_font_collection + } else { + &self.custom_font_collection + }; + let text_range = DWRITE_TEXT_RANGE { + startPosition: utf16_offset, + length: current_text_utf16_length, + }; + utf16_offset += current_text_utf16_length; + text_layout.SetFontCollection(collection, text_range)?; + text_layout + .SetFontFamilyName(&HSTRING::from(&font_info.font_family), text_range)?; + text_layout.SetFontSize(font_size.0, text_range)?; + text_layout.SetFontStyle(font_info.font_face.GetStyle(), text_range)?; + text_layout.SetFontWeight(font_info.font_face.GetWeight(), text_range)?; + text_layout.SetTypography(&font_info.features, text_range)?; + } + + let mut runs = Vec::new(); + let renderer_context = RendererContext { + text_system: self, + index_converter: StringIndexConverter::new(text), + runs: &mut runs, + width: 0.0, + }; + text_layout.Draw( + Some(&renderer_context as *const _ as _), + &text_renderer.0, + 0.0, + 0.0, + )?; + let width = px(renderer_context.width); + + Ok(LineLayout { + font_size, + width, + ascent, + descent, + runs, + len: text.len(), + }) + } + } + + fn font_metrics(&self, font_id: FontId) -> FontMetrics { + unsafe { + let font_info = &self.fonts[font_id.0]; + let mut metrics = std::mem::zeroed(); + font_info.font_face.GetMetrics(&mut metrics); + + FontMetrics { + units_per_em: metrics.Base.designUnitsPerEm as _, + ascent: metrics.Base.ascent as _, + descent: -(metrics.Base.descent as f32), + line_gap: metrics.Base.lineGap as _, + underline_position: metrics.Base.underlinePosition as _, + underline_thickness: metrics.Base.underlineThickness as _, + cap_height: metrics.Base.capHeight as _, + x_height: metrics.Base.xHeight as _, + bounding_box: Bounds { + origin: Point { + x: metrics.glyphBoxLeft as _, + y: metrics.glyphBoxBottom as _, + }, + size: Size { + width: (metrics.glyphBoxRight - metrics.glyphBoxLeft) as _, + height: (metrics.glyphBoxTop - metrics.glyphBoxBottom) as _, + }, + }, + } + } + } + + fn create_glyph_run_analysis( + &self, + params: &RenderGlyphParams, + ) -> Result { + let font = &self.fonts[params.font_id.0]; + let glyph_id = [params.glyph_id.0 as u16]; + let advance = [0.0]; + let offset = [DWRITE_GLYPH_OFFSET::default()]; + let glyph_run = DWRITE_GLYPH_RUN { + fontFace: unsafe { std::mem::transmute_copy(&font.font_face) }, + fontEmSize: params.font_size.0, + glyphCount: 1, + glyphIndices: glyph_id.as_ptr(), + glyphAdvances: advance.as_ptr(), + glyphOffsets: offset.as_ptr(), + isSideways: BOOL(0), + bidiLevel: 0, + }; + let transform = DWRITE_MATRIX { + m11: params.scale_factor, + m12: 0.0, + m21: 0.0, + m22: params.scale_factor, + dx: 0.0, + dy: 0.0, + }; + let baseline_origin_x = + params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor; + let baseline_origin_y = + params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor; + + let mut rendering_mode = DWRITE_RENDERING_MODE1::default(); + let mut grid_fit_mode = DWRITE_GRID_FIT_MODE::default(); + unsafe { + font.font_face.GetRecommendedRenderingMode( + params.font_size.0, + // Using 96 as scale is applied by the transform + 96.0, + 96.0, + Some(&transform), + false, + DWRITE_OUTLINE_THRESHOLD_ANTIALIASED, + DWRITE_MEASURING_MODE_NATURAL, + None, + &mut rendering_mode, + &mut grid_fit_mode, + )?; + } + let rendering_mode = match rendering_mode { + DWRITE_RENDERING_MODE1_OUTLINE => DWRITE_RENDERING_MODE1_NATURAL_SYMMETRIC, + m => m, + }; + + let glyph_analysis = unsafe { + self.components.factory.CreateGlyphRunAnalysis( + &glyph_run, + Some(&transform), + rendering_mode, + DWRITE_MEASURING_MODE_NATURAL, + grid_fit_mode, + DWRITE_TEXT_ANTIALIAS_MODE_GRAYSCALE, + baseline_origin_x, + baseline_origin_y, + ) + }?; + Ok(glyph_analysis) + } + + fn raster_bounds(&self, params: &RenderGlyphParams) -> Result> { + let glyph_analysis = self.create_glyph_run_analysis(params)?; + + let bounds = unsafe { glyph_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_ALIASED_1x1)? }; + + if bounds.right < bounds.left { + Ok(Bounds { + origin: point(0.into(), 0.into()), + size: size(0.into(), 0.into()), + }) + } else { + Ok(Bounds { + origin: point(bounds.left.into(), bounds.top.into()), + size: size( + (bounds.right - bounds.left).into(), + (bounds.bottom - bounds.top).into(), + ), + }) + } + } + + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { + let font_info = &self.fonts[font_id.0]; + let codepoints = [ch as u32]; + let mut glyph_indices = vec![0u16; 1]; + unsafe { + font_info + .font_face + .GetGlyphIndices(codepoints.as_ptr(), 1, glyph_indices.as_mut_ptr()) + .log_err() + } + .map(|_| GlyphId(glyph_indices[0] as u32)) + } + + fn rasterize_glyph( + &self, + params: &RenderGlyphParams, + glyph_bounds: Bounds, + ) -> Result<(Size, Vec)> { + if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 { + anyhow::bail!("glyph bounds are empty"); + } + + let bitmap_data = if params.is_emoji { + if let Ok(color) = self.rasterize_color(params, glyph_bounds) { + color + } else { + let monochrome = self.rasterize_monochrome(params, glyph_bounds)?; + monochrome + .into_iter() + .flat_map(|pixel| [0, 0, 0, pixel]) + .collect::>() + } + } else { + self.rasterize_monochrome(params, glyph_bounds)? + }; + + Ok((glyph_bounds.size, bitmap_data)) + } + + fn rasterize_monochrome( + &self, + params: &RenderGlyphParams, + glyph_bounds: Bounds, + ) -> Result> { + let mut bitmap_data = + vec![0u8; glyph_bounds.size.width.0 as usize * glyph_bounds.size.height.0 as usize]; + + let glyph_analysis = self.create_glyph_run_analysis(params)?; + unsafe { + glyph_analysis.CreateAlphaTexture( + DWRITE_TEXTURE_ALIASED_1x1, + &RECT { + left: glyph_bounds.origin.x.0, + top: glyph_bounds.origin.y.0, + right: glyph_bounds.size.width.0 + glyph_bounds.origin.x.0, + bottom: glyph_bounds.size.height.0 + glyph_bounds.origin.y.0, + }, + &mut bitmap_data, + )?; + } + + Ok(bitmap_data) + } + + fn rasterize_color( + &self, + params: &RenderGlyphParams, + glyph_bounds: Bounds, + ) -> Result> { + let bitmap_size = glyph_bounds.size; + let subpixel_shift = params + .subpixel_variant + .map(|v| v as f32 / SUBPIXEL_VARIANTS_X as f32); + let baseline_origin_x = subpixel_shift.x / params.scale_factor; + let baseline_origin_y = subpixel_shift.y / params.scale_factor; + + let transform = DWRITE_MATRIX { + m11: params.scale_factor, + m12: 0.0, + m21: 0.0, + m22: params.scale_factor, + dx: 0.0, + dy: 0.0, + }; + + let font = &self.fonts[params.font_id.0]; + let glyph_id = [params.glyph_id.0 as u16]; + let advance = [glyph_bounds.size.width.0 as f32]; + let offset = [DWRITE_GLYPH_OFFSET { + advanceOffset: -glyph_bounds.origin.x.0 as f32 / params.scale_factor, + ascenderOffset: glyph_bounds.origin.y.0 as f32 / params.scale_factor, + }]; + let glyph_run = DWRITE_GLYPH_RUN { + fontFace: unsafe { std::mem::transmute_copy(&font.font_face) }, + fontEmSize: params.font_size.0, + glyphCount: 1, + glyphIndices: glyph_id.as_ptr(), + glyphAdvances: advance.as_ptr(), + glyphOffsets: offset.as_ptr(), + isSideways: BOOL(0), + bidiLevel: 0, + }; + + // todo: support formats other than COLR + let color_enumerator = unsafe { + self.components.factory.TranslateColorGlyphRun( + Vector2::new(baseline_origin_x, baseline_origin_y), + &glyph_run, + None, + DWRITE_GLYPH_IMAGE_FORMATS_COLR, + DWRITE_MEASURING_MODE_NATURAL, + Some(&transform), + 0, + ) + }?; + + let mut glyph_layers = Vec::new(); + loop { + let color_run = unsafe { color_enumerator.GetCurrentRun() }?; + let color_run = unsafe { &*color_run }; + let image_format = color_run.glyphImageFormat & !DWRITE_GLYPH_IMAGE_FORMATS_TRUETYPE; + if image_format == DWRITE_GLYPH_IMAGE_FORMATS_COLR { + let color_analysis = unsafe { + self.components.factory.CreateGlyphRunAnalysis( + &color_run.Base.glyphRun as *const _, + Some(&transform), + DWRITE_RENDERING_MODE1_NATURAL_SYMMETRIC, + DWRITE_MEASURING_MODE_NATURAL, + DWRITE_GRID_FIT_MODE_DEFAULT, + DWRITE_TEXT_ANTIALIAS_MODE_GRAYSCALE, + baseline_origin_x, + baseline_origin_y, + ) + }?; + + let color_bounds = + unsafe { color_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_ALIASED_1x1) }?; + + let color_size = size( + color_bounds.right - color_bounds.left, + color_bounds.bottom - color_bounds.top, + ); + if color_size.width > 0 && color_size.height > 0 { + let mut alpha_data = vec![0u8; (color_size.width * color_size.height) as usize]; + unsafe { + color_analysis.CreateAlphaTexture( + DWRITE_TEXTURE_ALIASED_1x1, + &color_bounds, + &mut alpha_data, + ) + }?; + + let run_color = { + let run_color = color_run.Base.runColor; + Rgba { + r: run_color.r, + g: run_color.g, + b: run_color.b, + a: run_color.a, + } + }; + let bounds = bounds(point(color_bounds.left, color_bounds.top), color_size); + glyph_layers.push(GlyphLayerTexture::new( + &self.components.gpu_state, + run_color, + bounds, + &alpha_data, + )?); + } + } + + let has_next = unsafe { color_enumerator.MoveNext() } + .map(|e| e.as_bool()) + .unwrap_or(false); + if !has_next { + break; + } + } + + let gpu_state = &self.components.gpu_state; + let params_buffer = { + let desc = D3D11_BUFFER_DESC { + ByteWidth: std::mem::size_of::() as u32, + Usage: D3D11_USAGE_DYNAMIC, + BindFlags: D3D11_BIND_CONSTANT_BUFFER.0 as u32, + CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, + MiscFlags: 0, + StructureByteStride: 0, + }; + + let mut buffer = None; + unsafe { + gpu_state + .device + .CreateBuffer(&desc, None, Some(&mut buffer)) + }?; + [buffer] + }; + + let render_target_texture = { + let mut texture = None; + let desc = D3D11_TEXTURE2D_DESC { + Width: bitmap_size.width.0 as u32, + Height: bitmap_size.height.0 as u32, + MipLevels: 1, + ArraySize: 1, + Format: DXGI_FORMAT_B8G8R8A8_UNORM, + SampleDesc: DXGI_SAMPLE_DESC { + Count: 1, + Quality: 0, + }, + Usage: D3D11_USAGE_DEFAULT, + BindFlags: D3D11_BIND_RENDER_TARGET.0 as u32, + CPUAccessFlags: 0, + MiscFlags: 0, + }; + unsafe { + gpu_state + .device + .CreateTexture2D(&desc, None, Some(&mut texture)) + }?; + texture.unwrap() + }; + + let render_target_view = { + let desc = D3D11_RENDER_TARGET_VIEW_DESC { + Format: DXGI_FORMAT_B8G8R8A8_UNORM, + ViewDimension: D3D11_RTV_DIMENSION_TEXTURE2D, + Anonymous: D3D11_RENDER_TARGET_VIEW_DESC_0 { + Texture2D: D3D11_TEX2D_RTV { MipSlice: 0 }, + }, + }; + let mut rtv = None; + unsafe { + gpu_state.device.CreateRenderTargetView( + &render_target_texture, + Some(&desc), + Some(&mut rtv), + ) + }?; + [rtv] + }; + + let staging_texture = { + let mut texture = None; + let desc = D3D11_TEXTURE2D_DESC { + Width: bitmap_size.width.0 as u32, + Height: bitmap_size.height.0 as u32, + MipLevels: 1, + ArraySize: 1, + Format: DXGI_FORMAT_B8G8R8A8_UNORM, + SampleDesc: DXGI_SAMPLE_DESC { + Count: 1, + Quality: 0, + }, + Usage: D3D11_USAGE_STAGING, + BindFlags: 0, + CPUAccessFlags: D3D11_CPU_ACCESS_READ.0 as u32, + MiscFlags: 0, + }; + unsafe { + gpu_state + .device + .CreateTexture2D(&desc, None, Some(&mut texture)) + }?; + texture.unwrap() + }; + + let device_context = &gpu_state.device_context; + unsafe { device_context.IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP) }; + unsafe { device_context.VSSetShader(&gpu_state.vertex_shader, None) }; + unsafe { device_context.PSSetShader(&gpu_state.pixel_shader, None) }; + unsafe { device_context.VSSetConstantBuffers(0, Some(¶ms_buffer)) }; + unsafe { device_context.PSSetConstantBuffers(0, Some(¶ms_buffer)) }; + unsafe { device_context.OMSetRenderTargets(Some(&render_target_view), None) }; + unsafe { device_context.PSSetSamplers(0, Some(&gpu_state.sampler)) }; + unsafe { device_context.OMSetBlendState(&gpu_state.blend_state, None, 0xffffffff) }; + + let crate::FontInfo { + gamma_ratios, + grayscale_enhanced_contrast, + } = DirectXRenderer::get_font_info(); + + for layer in glyph_layers { + let params = GlyphLayerTextureParams { + run_color: layer.run_color, + bounds: layer.bounds, + gamma_ratios: *gamma_ratios, + grayscale_enhanced_contrast: *grayscale_enhanced_contrast, + _pad: [0f32; 3], + }; + unsafe { + let mut dest = std::mem::zeroed(); + gpu_state.device_context.Map( + params_buffer[0].as_ref().unwrap(), + 0, + D3D11_MAP_WRITE_DISCARD, + 0, + Some(&mut dest), + )?; + std::ptr::copy_nonoverlapping(¶ms as *const _, dest.pData as *mut _, 1); + gpu_state + .device_context + .Unmap(params_buffer[0].as_ref().unwrap(), 0); + }; + + let texture = [Some(layer.texture_view)]; + unsafe { device_context.PSSetShaderResources(0, Some(&texture)) }; + + let viewport = [D3D11_VIEWPORT { + TopLeftX: layer.bounds.origin.x as f32, + TopLeftY: layer.bounds.origin.y as f32, + Width: layer.bounds.size.width as f32, + Height: layer.bounds.size.height as f32, + MinDepth: 0.0, + MaxDepth: 1.0, + }]; + unsafe { device_context.RSSetViewports(Some(&viewport)) }; + + unsafe { device_context.Draw(4, 0) }; + } + + unsafe { device_context.CopyResource(&staging_texture, &render_target_texture) }; + + let mapped_data = { + let mut mapped_data = D3D11_MAPPED_SUBRESOURCE::default(); + unsafe { + device_context.Map( + &staging_texture, + 0, + D3D11_MAP_READ, + 0, + Some(&mut mapped_data), + ) + }?; + mapped_data + }; + let mut rasterized = + vec![0u8; (bitmap_size.width.0 as u32 * bitmap_size.height.0 as u32 * 4) as usize]; + + for y in 0..bitmap_size.height.0 as usize { + let width = bitmap_size.width.0 as usize; + unsafe { + std::ptr::copy_nonoverlapping::( + (mapped_data.pData as *const u8).byte_add(mapped_data.RowPitch as usize * y), + rasterized + .as_mut_ptr() + .byte_add(width * y * std::mem::size_of::()), + width * std::mem::size_of::(), + ) + }; + } + + // Convert from premultiplied to straight alpha + for chunk in rasterized.chunks_exact_mut(4) { + let b = chunk[0] as f32; + let g = chunk[1] as f32; + let r = chunk[2] as f32; + let a = chunk[3] as f32; + if a > 0.0 { + let inv_a = 255.0 / a; + chunk[0] = (b * inv_a).clamp(0.0, 255.0) as u8; + chunk[1] = (g * inv_a).clamp(0.0, 255.0) as u8; + chunk[2] = (r * inv_a).clamp(0.0, 255.0) as u8; + } + } + + Ok(rasterized) + } + + fn get_typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + unsafe { + let font = &self.fonts[font_id.0].font_face; + let glyph_indices = [glyph_id.0 as u16]; + let mut metrics = [DWRITE_GLYPH_METRICS::default()]; + font.GetDesignGlyphMetrics(glyph_indices.as_ptr(), 1, metrics.as_mut_ptr(), false)?; + + let metrics = &metrics[0]; + let advance_width = metrics.advanceWidth as i32; + let advance_height = metrics.advanceHeight as i32; + let left_side_bearing = metrics.leftSideBearing; + let right_side_bearing = metrics.rightSideBearing; + let top_side_bearing = metrics.topSideBearing; + let bottom_side_bearing = metrics.bottomSideBearing; + let vertical_origin_y = metrics.verticalOriginY; + + let y_offset = vertical_origin_y + bottom_side_bearing - advance_height; + let width = advance_width - (left_side_bearing + right_side_bearing); + let height = advance_height - (top_side_bearing + bottom_side_bearing); + + Ok(Bounds { + origin: Point { + x: left_side_bearing as f32, + y: y_offset as f32, + }, + size: Size { + width: width as f32, + height: height as f32, + }, + }) + } + } + + fn get_advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + unsafe { + let font = &self.fonts[font_id.0].font_face; + let glyph_indices = [glyph_id.0 as u16]; + let mut metrics = [DWRITE_GLYPH_METRICS::default()]; + font.GetDesignGlyphMetrics(glyph_indices.as_ptr(), 1, metrics.as_mut_ptr(), false)?; + + let metrics = &metrics[0]; + + Ok(Size { + width: metrics.advanceWidth as f32, + height: 0.0, + }) + } + } + + fn all_font_names(&self) -> Vec { + let mut result = + get_font_names_from_collection(&self.system_font_collection, &self.components.locale); + result.extend(get_font_names_from_collection( + &self.custom_font_collection, + &self.components.locale, + )); + result + } + + fn handle_gpu_lost(&mut self, directx_devices: &DirectXDevices) { + try_to_recover_from_device_lost( + || GPUState::new(directx_devices).context("Recreating GPU state for DirectWrite"), + |gpu_state| self.components.gpu_state = gpu_state, + || { + log::error!( + "Failed to recreate GPU state for DirectWrite after multiple attempts." + ); + // Do something here? + // At this point, the device loss is considered unrecoverable. + }, + ); + } +} + +impl Drop for DirectWriteState { + fn drop(&mut self) { + unsafe { + let _ = self + .components + .factory + .UnregisterFontFileLoader(&self.components.in_memory_loader); + } + } +} + +struct GlyphLayerTexture { + run_color: Rgba, + bounds: Bounds, + texture_view: ID3D11ShaderResourceView, + // holding on to the texture to not RAII drop it + _texture: ID3D11Texture2D, +} + +impl GlyphLayerTexture { + pub fn new( + gpu_state: &GPUState, + run_color: Rgba, + bounds: Bounds, + alpha_data: &[u8], + ) -> Result { + let texture_size = bounds.size; + + let desc = D3D11_TEXTURE2D_DESC { + Width: texture_size.width as u32, + Height: texture_size.height as u32, + MipLevels: 1, + ArraySize: 1, + Format: DXGI_FORMAT_R8_UNORM, + SampleDesc: DXGI_SAMPLE_DESC { + Count: 1, + Quality: 0, + }, + Usage: D3D11_USAGE_DEFAULT, + BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32, + CPUAccessFlags: 0, + MiscFlags: 0, + }; + + let texture = { + let mut texture: Option = None; + unsafe { + gpu_state + .device + .CreateTexture2D(&desc, None, Some(&mut texture))? + }; + texture.unwrap() + }; + let texture_view = { + let mut view: Option = None; + unsafe { + gpu_state + .device + .CreateShaderResourceView(&texture, None, Some(&mut view))? + }; + view.unwrap() + }; + + unsafe { + gpu_state.device_context.UpdateSubresource( + &texture, + 0, + None, + alpha_data.as_ptr() as _, + texture_size.width as u32, + 0, + ) + }; + + Ok(GlyphLayerTexture { + run_color, + bounds, + texture_view, + _texture: texture, + }) + } +} + +#[repr(C)] +struct GlyphLayerTextureParams { + bounds: Bounds, + run_color: Rgba, + gamma_ratios: [f32; 4], + grayscale_enhanced_contrast: f32, + _pad: [f32; 3], +} + +struct TextRendererWrapper(pub IDWriteTextRenderer); + +impl TextRendererWrapper { + pub fn new(locale_str: &str) -> Self { + let inner = TextRenderer::new(locale_str); + TextRendererWrapper(inner.into()) + } +} + +#[implement(IDWriteTextRenderer)] +struct TextRenderer { + locale: String, +} + +impl TextRenderer { + pub fn new(locale_str: &str) -> Self { + TextRenderer { + locale: locale_str.to_owned(), + } + } +} + +struct RendererContext<'t, 'a, 'b> { + text_system: &'t mut DirectWriteState, + index_converter: StringIndexConverter<'a>, + runs: &'b mut Vec, + width: f32, +} + +#[derive(Debug)] +struct ClusterAnalyzer<'t> { + utf16_idx: usize, + glyph_idx: usize, + glyph_count: usize, + cluster_map: &'t [u16], +} + +impl<'t> ClusterAnalyzer<'t> { + pub fn new(cluster_map: &'t [u16], glyph_count: usize) -> Self { + ClusterAnalyzer { + utf16_idx: 0, + glyph_idx: 0, + glyph_count, + cluster_map, + } + } +} + +impl Iterator for ClusterAnalyzer<'_> { + type Item = (usize, usize); + + fn next(&mut self) -> Option<(usize, usize)> { + if self.utf16_idx >= self.cluster_map.len() { + return None; // No more clusters + } + let start_utf16_idx = self.utf16_idx; + let current_glyph = self.cluster_map[start_utf16_idx] as usize; + + // Find the end of current cluster (where glyph index changes) + let mut end_utf16_idx = start_utf16_idx + 1; + while end_utf16_idx < self.cluster_map.len() + && self.cluster_map[end_utf16_idx] as usize == current_glyph + { + end_utf16_idx += 1; + } + + let utf16_len = end_utf16_idx - start_utf16_idx; + + // Calculate glyph count for this cluster + let next_glyph = if end_utf16_idx < self.cluster_map.len() { + self.cluster_map[end_utf16_idx] as usize + } else { + self.glyph_count + }; + + let glyph_count = next_glyph - current_glyph; + + // Update state for next call + self.utf16_idx = end_utf16_idx; + self.glyph_idx = next_glyph; + + Some((utf16_len, glyph_count)) + } +} + +#[allow(non_snake_case)] +impl IDWritePixelSnapping_Impl for TextRenderer_Impl { + fn IsPixelSnappingDisabled( + &self, + _clientdrawingcontext: *const ::core::ffi::c_void, + ) -> windows::core::Result { + Ok(BOOL(0)) + } + + fn GetCurrentTransform( + &self, + _clientdrawingcontext: *const ::core::ffi::c_void, + transform: *mut DWRITE_MATRIX, + ) -> windows::core::Result<()> { + unsafe { + *transform = DWRITE_MATRIX { + m11: 1.0, + m12: 0.0, + m21: 0.0, + m22: 1.0, + dx: 0.0, + dy: 0.0, + }; + } + Ok(()) + } + + fn GetPixelsPerDip( + &self, + _clientdrawingcontext: *const ::core::ffi::c_void, + ) -> windows::core::Result { + Ok(1.0) + } +} + +#[allow(non_snake_case)] +impl IDWriteTextRenderer_Impl for TextRenderer_Impl { + fn DrawGlyphRun( + &self, + clientdrawingcontext: *const ::core::ffi::c_void, + _baselineoriginx: f32, + _baselineoriginy: f32, + _measuringmode: DWRITE_MEASURING_MODE, + glyphrun: *const DWRITE_GLYPH_RUN, + glyphrundescription: *const DWRITE_GLYPH_RUN_DESCRIPTION, + _clientdrawingeffect: windows::core::Ref, + ) -> windows::core::Result<()> { + let glyphrun = unsafe { &*glyphrun }; + let glyph_count = glyphrun.glyphCount as usize; + if glyph_count == 0 || glyphrun.fontFace.is_none() { + return Ok(()); + } + let desc = unsafe { &*glyphrundescription }; + let context = unsafe { + &mut *(clientdrawingcontext as *const RendererContext as *mut RendererContext) + }; + let font_face = glyphrun.fontFace.as_ref().unwrap(); + // This `cast()` action here should never fail since we are running on Win10+, and + // `IDWriteFontFace3` requires Win10 + let font_face = &font_face.cast::().unwrap(); + let Some((font_identifier, font_struct, color_font)) = + get_font_identifier_and_font_struct(font_face, &self.locale) + else { + return Ok(()); + }; + + let font_id = if let Some(id) = context + .text_system + .font_id_by_identifier + .get(&font_identifier) + { + *id + } else { + context.text_system.select_font(&font_struct) + }; + + let glyph_ids = unsafe { std::slice::from_raw_parts(glyphrun.glyphIndices, glyph_count) }; + let glyph_advances = + unsafe { std::slice::from_raw_parts(glyphrun.glyphAdvances, glyph_count) }; + let glyph_offsets = + unsafe { std::slice::from_raw_parts(glyphrun.glyphOffsets, glyph_count) }; + let cluster_map = + unsafe { std::slice::from_raw_parts(desc.clusterMap, desc.stringLength as usize) }; + + let mut cluster_analyzer = ClusterAnalyzer::new(cluster_map, glyph_count); + let mut utf16_idx = desc.textPosition as usize; + let mut glyph_idx = 0; + let mut glyphs = Vec::with_capacity(glyph_count); + for (cluster_utf16_len, cluster_glyph_count) in cluster_analyzer { + context.index_converter.advance_to_utf16_ix(utf16_idx); + utf16_idx += cluster_utf16_len; + for (cluster_glyph_idx, glyph_id) in glyph_ids + [glyph_idx..(glyph_idx + cluster_glyph_count)] + .iter() + .enumerate() + { + let id = GlyphId(*glyph_id as u32); + let is_emoji = color_font + && is_color_glyph(font_face, id, &context.text_system.components.factory); + let this_glyph_idx = glyph_idx + cluster_glyph_idx; + glyphs.push(ShapedGlyph { + id, + position: point( + px(context.width + glyph_offsets[this_glyph_idx].advanceOffset), + px(0.0), + ), + index: context.index_converter.utf8_ix, + is_emoji, + }); + context.width += glyph_advances[this_glyph_idx]; + } + glyph_idx += cluster_glyph_count; + } + context.runs.push(ShapedRun { font_id, glyphs }); + Ok(()) + } + + fn DrawUnderline( + &self, + _clientdrawingcontext: *const ::core::ffi::c_void, + _baselineoriginx: f32, + _baselineoriginy: f32, + _underline: *const DWRITE_UNDERLINE, + _clientdrawingeffect: windows::core::Ref, + ) -> windows::core::Result<()> { + Err(windows::core::Error::new( + E_NOTIMPL, + "DrawUnderline unimplemented", + )) + } + + fn DrawStrikethrough( + &self, + _clientdrawingcontext: *const ::core::ffi::c_void, + _baselineoriginx: f32, + _baselineoriginy: f32, + _strikethrough: *const DWRITE_STRIKETHROUGH, + _clientdrawingeffect: windows::core::Ref, + ) -> windows::core::Result<()> { + Err(windows::core::Error::new( + E_NOTIMPL, + "DrawStrikethrough unimplemented", + )) + } + + fn DrawInlineObject( + &self, + _clientdrawingcontext: *const ::core::ffi::c_void, + _originx: f32, + _originy: f32, + _inlineobject: windows::core::Ref, + _issideways: BOOL, + _isrighttoleft: BOOL, + _clientdrawingeffect: windows::core::Ref, + ) -> windows::core::Result<()> { + Err(windows::core::Error::new( + E_NOTIMPL, + "DrawInlineObject unimplemented", + )) + } +} + +struct StringIndexConverter<'a> { + text: &'a str, + utf8_ix: usize, + utf16_ix: usize, +} + +impl<'a> StringIndexConverter<'a> { + fn new(text: &'a str) -> Self { + Self { + text, + utf8_ix: 0, + utf16_ix: 0, + } + } + + #[allow(dead_code)] + fn advance_to_utf8_ix(&mut self, utf8_target: usize) { + for (ix, c) in self.text[self.utf8_ix..].char_indices() { + if self.utf8_ix + ix >= utf8_target { + self.utf8_ix += ix; + return; + } + self.utf16_ix += c.len_utf16(); + } + self.utf8_ix = self.text.len(); + } + + fn advance_to_utf16_ix(&mut self, utf16_target: usize) { + for (ix, c) in self.text[self.utf8_ix..].char_indices() { + if self.utf16_ix >= utf16_target { + self.utf8_ix += ix; + return; + } + self.utf16_ix += c.len_utf16(); + } + self.utf8_ix = self.text.len(); + } +} + +impl Into for FontStyle { + fn into(self) -> DWRITE_FONT_STYLE { + match self { + FontStyle::Normal => DWRITE_FONT_STYLE_NORMAL, + FontStyle::Italic => DWRITE_FONT_STYLE_ITALIC, + FontStyle::Oblique => DWRITE_FONT_STYLE_OBLIQUE, + } + } +} + +impl From for FontStyle { + fn from(value: DWRITE_FONT_STYLE) -> Self { + match value.0 { + 0 => FontStyle::Normal, + 1 => FontStyle::Italic, + 2 => FontStyle::Oblique, + _ => unreachable!(), + } + } +} + +impl Into for FontWeight { + fn into(self) -> DWRITE_FONT_WEIGHT { + DWRITE_FONT_WEIGHT(self.0 as i32) + } +} + +impl From for FontWeight { + fn from(value: DWRITE_FONT_WEIGHT) -> Self { + FontWeight(value.0 as f32) + } +} + +fn get_font_names_from_collection( + collection: &IDWriteFontCollection1, + locale: &str, +) -> Vec { + unsafe { + let mut result = Vec::new(); + let family_count = collection.GetFontFamilyCount(); + for index in 0..family_count { + let Some(font_family) = collection.GetFontFamily(index).log_err() else { + continue; + }; + let Some(localized_family_name) = font_family.GetFamilyNames().log_err() else { + continue; + }; + let Some(family_name) = get_name(localized_family_name, locale).log_err() else { + continue; + }; + result.push(family_name); + } + + result + } +} + +fn get_font_identifier_and_font_struct( + font_face: &IDWriteFontFace3, + locale: &str, +) -> Option<(FontIdentifier, Font, bool)> { + let postscript_name = get_postscript_name(font_face, locale).log_err()?; + let localized_family_name = unsafe { font_face.GetFamilyNames().log_err() }?; + let family_name = get_name(localized_family_name, locale).log_err()?; + let weight = unsafe { font_face.GetWeight() }; + let style = unsafe { font_face.GetStyle() }; + let identifier = FontIdentifier { + postscript_name, + weight: weight.0, + style: style.0, + }; + let font_struct = Font { + family: family_name.into(), + features: FontFeatures::default(), + weight: weight.into(), + style: style.into(), + fallbacks: None, + }; + let is_emoji = unsafe { font_face.IsColorFont().as_bool() }; + Some((identifier, font_struct, is_emoji)) +} + +#[inline] +fn get_font_identifier(font_face: &IDWriteFontFace3, locale: &str) -> Option { + let weight = unsafe { font_face.GetWeight().0 }; + let style = unsafe { font_face.GetStyle().0 }; + get_postscript_name(font_face, locale) + .log_err() + .map(|postscript_name| FontIdentifier { + postscript_name, + weight, + style, + }) +} + +#[inline] +fn get_postscript_name(font_face: &IDWriteFontFace3, locale: &str) -> Result { + let mut info = None; + let mut exists = BOOL(0); + unsafe { + font_face.GetInformationalStrings( + DWRITE_INFORMATIONAL_STRING_POSTSCRIPT_NAME, + &mut info, + &mut exists, + )? + }; + if !exists.as_bool() || info.is_none() { + anyhow::bail!("No postscript name found for font face"); + } + + get_name(info.unwrap(), locale) +} + +// https://learn.microsoft.com/en-us/windows/win32/api/dwrite/ne-dwrite-dwrite_font_feature_tag +fn apply_font_features( + direct_write_features: &IDWriteTypography, + features: &FontFeatures, +) -> Result<()> { + let tag_values = features.tag_value_list(); + if tag_values.is_empty() { + return Ok(()); + } + + // All of these features are enabled by default by DirectWrite. + // If you want to (and can) peek into the source of DirectWrite + let mut feature_liga = make_direct_write_feature("liga", 1); + let mut feature_clig = make_direct_write_feature("clig", 1); + let mut feature_calt = make_direct_write_feature("calt", 1); + + for (tag, value) in tag_values { + if tag.as_str() == "liga" && *value == 0 { + feature_liga.parameter = 0; + continue; + } + if tag.as_str() == "clig" && *value == 0 { + feature_clig.parameter = 0; + continue; + } + if tag.as_str() == "calt" && *value == 0 { + feature_calt.parameter = 0; + continue; + } + + unsafe { + direct_write_features.AddFontFeature(make_direct_write_feature(tag, *value))?; + } + } + unsafe { + direct_write_features.AddFontFeature(feature_liga)?; + direct_write_features.AddFontFeature(feature_clig)?; + direct_write_features.AddFontFeature(feature_calt)?; + } + + Ok(()) +} + +#[inline] +const fn make_direct_write_feature(feature_name: &str, parameter: u32) -> DWRITE_FONT_FEATURE { + let tag = make_direct_write_tag(feature_name); + DWRITE_FONT_FEATURE { + nameTag: tag, + parameter, + } +} + +#[inline] +const fn make_open_type_tag(tag_name: &str) -> u32 { + let bytes = tag_name.as_bytes(); + debug_assert!(bytes.len() == 4); + u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) +} + +#[inline] +const fn make_direct_write_tag(tag_name: &str) -> DWRITE_FONT_FEATURE_TAG { + DWRITE_FONT_FEATURE_TAG(make_open_type_tag(tag_name)) +} + +#[inline] +fn get_name(string: IDWriteLocalizedStrings, locale: &str) -> Result { + let mut locale_name_index = 0u32; + let mut exists = BOOL(0); + unsafe { + string.FindLocaleName( + &HSTRING::from(locale), + &mut locale_name_index, + &mut exists as _, + )? + }; + if !exists.as_bool() { + unsafe { + string.FindLocaleName( + DEFAULT_LOCALE_NAME, + &mut locale_name_index as _, + &mut exists as _, + )? + }; + anyhow::ensure!(exists.as_bool(), "No localised string for {locale}"); + } + + let name_length = unsafe { string.GetStringLength(locale_name_index) }? as usize; + let mut name_vec = vec![0u16; name_length + 1]; + unsafe { + string.GetString(locale_name_index, &mut name_vec)?; + } + + Ok(String::from_utf16_lossy(&name_vec[..name_length])) +} + +fn get_system_ui_font_name() -> SharedString { + unsafe { + let mut info: LOGFONTW = std::mem::zeroed(); + let font_family = if SystemParametersInfoW( + SPI_GETICONTITLELOGFONT, + std::mem::size_of::() as u32, + Some(&mut info as *mut _ as _), + SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0), + ) + .log_err() + .is_none() + { + // https://learn.microsoft.com/en-us/windows/win32/uxguide/vis-fonts + // Segoe UI is the Windows font intended for user interface text strings. + "Segoe UI".into() + } else { + let font_name = String::from_utf16_lossy(&info.lfFaceName); + font_name.trim_matches(char::from(0)).to_owned().into() + }; + log::info!("Use {} as UI font.", font_family); + font_family + } +} + +// One would think that with newer DirectWrite method: IDWriteFontFace4::GetGlyphImageFormats +// but that doesn't seem to work for some glyphs, say ❤ +fn is_color_glyph( + font_face: &IDWriteFontFace3, + glyph_id: GlyphId, + factory: &IDWriteFactory5, +) -> bool { + let glyph_run = DWRITE_GLYPH_RUN { + fontFace: unsafe { std::mem::transmute_copy(font_face) }, + fontEmSize: 14.0, + glyphCount: 1, + glyphIndices: &(glyph_id.0 as u16), + glyphAdvances: &0.0, + glyphOffsets: &DWRITE_GLYPH_OFFSET { + advanceOffset: 0.0, + ascenderOffset: 0.0, + }, + isSideways: BOOL(0), + bidiLevel: 0, + }; + unsafe { + factory.TranslateColorGlyphRun( + Vector2::default(), + &glyph_run as _, + None, + DWRITE_GLYPH_IMAGE_FORMATS_COLR + | DWRITE_GLYPH_IMAGE_FORMATS_SVG + | DWRITE_GLYPH_IMAGE_FORMATS_PNG + | DWRITE_GLYPH_IMAGE_FORMATS_JPEG + | DWRITE_GLYPH_IMAGE_FORMATS_PREMULTIPLIED_B8G8R8A8, + DWRITE_MEASURING_MODE_NATURAL, + None, + 0, + ) + } + .is_ok() +} + +const DEFAULT_LOCALE_NAME: PCWSTR = windows::core::w!("en-US"); + +#[cfg(test)] +mod tests { + use crate::platform::windows::direct_write::ClusterAnalyzer; + + #[test] + fn test_cluster_map() { + let cluster_map = [0]; + let mut analyzer = ClusterAnalyzer::new(&cluster_map, 1); + let next = analyzer.next(); + assert_eq!(next, Some((1, 1))); + let next = analyzer.next(); + assert_eq!(next, None); + + let cluster_map = [0, 1, 2]; + let mut analyzer = ClusterAnalyzer::new(&cluster_map, 3); + let next = analyzer.next(); + assert_eq!(next, Some((1, 1))); + let next = analyzer.next(); + assert_eq!(next, Some((1, 1))); + let next = analyzer.next(); + assert_eq!(next, Some((1, 1))); + let next = analyzer.next(); + assert_eq!(next, None); + // 👨‍👩‍👧‍👦👩‍💻 + let cluster_map = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 4, 4]; + let mut analyzer = ClusterAnalyzer::new(&cluster_map, 5); + let next = analyzer.next(); + assert_eq!(next, Some((11, 4))); + let next = analyzer.next(); + assert_eq!(next, Some((5, 1))); + let next = analyzer.next(); + assert_eq!(next, None); + // 👩‍💻 + let cluster_map = [0, 0, 0, 0, 0]; + let mut analyzer = ClusterAnalyzer::new(&cluster_map, 1); + let next = analyzer.next(); + assert_eq!(next, Some((5, 1))); + let next = analyzer.next(); + assert_eq!(next, None); + } +} diff --git a/third_party/gpui/src/platform/windows/directx_atlas.rs b/third_party/gpui/src/platform/windows/directx_atlas.rs new file mode 100644 index 0000000..38c22a4 --- /dev/null +++ b/third_party/gpui/src/platform/windows/directx_atlas.rs @@ -0,0 +1,308 @@ +use collections::FxHashMap; +use etagere::BucketedAtlasAllocator; +use parking_lot::Mutex; +use windows::Win32::Graphics::{ + Direct3D11::{ + D3D11_BIND_SHADER_RESOURCE, D3D11_BOX, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, + ID3D11Device, ID3D11DeviceContext, ID3D11ShaderResourceView, ID3D11Texture2D, + }, + Dxgi::Common::*, +}; + +use crate::{ + AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, DevicePixels, PlatformAtlas, + Point, Size, platform::AtlasTextureList, +}; + +pub(crate) struct DirectXAtlas(Mutex); + +struct DirectXAtlasState { + device: ID3D11Device, + device_context: ID3D11DeviceContext, + monochrome_textures: AtlasTextureList, + polychrome_textures: AtlasTextureList, + tiles_by_key: FxHashMap, +} + +struct DirectXAtlasTexture { + id: AtlasTextureId, + bytes_per_pixel: u32, + allocator: BucketedAtlasAllocator, + texture: ID3D11Texture2D, + view: [Option; 1], + live_atlas_keys: u32, +} + +impl DirectXAtlas { + pub(crate) fn new(device: &ID3D11Device, device_context: &ID3D11DeviceContext) -> Self { + DirectXAtlas(Mutex::new(DirectXAtlasState { + device: device.clone(), + device_context: device_context.clone(), + monochrome_textures: Default::default(), + polychrome_textures: Default::default(), + tiles_by_key: Default::default(), + })) + } + + pub(crate) fn get_texture_view( + &self, + id: AtlasTextureId, + ) -> [Option; 1] { + let lock = self.0.lock(); + let tex = lock.texture(id); + tex.view.clone() + } + + pub(crate) fn handle_device_lost( + &self, + device: &ID3D11Device, + device_context: &ID3D11DeviceContext, + ) { + let mut lock = self.0.lock(); + lock.device = device.clone(); + lock.device_context = device_context.clone(); + lock.monochrome_textures = AtlasTextureList::default(); + lock.polychrome_textures = AtlasTextureList::default(); + lock.tiles_by_key.clear(); + } +} + +impl PlatformAtlas for DirectXAtlas { + fn get_or_insert_with<'a>( + &self, + key: &AtlasKey, + build: &mut dyn FnMut() -> anyhow::Result< + Option<(Size, std::borrow::Cow<'a, [u8]>)>, + >, + ) -> anyhow::Result> { + let mut lock = self.0.lock(); + if let Some(tile) = lock.tiles_by_key.get(key) { + Ok(Some(tile.clone())) + } else { + let Some((size, bytes)) = build()? else { + return Ok(None); + }; + let tile = lock + .allocate(size, key.texture_kind()) + .ok_or_else(|| anyhow::anyhow!("failed to allocate"))?; + let texture = lock.texture(tile.texture_id); + texture.upload(&lock.device_context, tile.bounds, &bytes); + lock.tiles_by_key.insert(key.clone(), tile.clone()); + Ok(Some(tile)) + } + } + + fn remove(&self, key: &AtlasKey) { + let mut lock = self.0.lock(); + + let Some(id) = lock.tiles_by_key.remove(key).map(|tile| tile.texture_id) else { + return; + }; + + let textures = match id.kind { + AtlasTextureKind::Monochrome => &mut lock.monochrome_textures, + AtlasTextureKind::Polychrome => &mut lock.polychrome_textures, + }; + + let Some(texture_slot) = textures.textures.get_mut(id.index as usize) else { + return; + }; + + if let Some(mut texture) = texture_slot.take() { + texture.decrement_ref_count(); + if texture.is_unreferenced() { + textures.free_list.push(texture.id.index as usize); + lock.tiles_by_key.remove(key); + } else { + *texture_slot = Some(texture); + } + } + } +} + +impl DirectXAtlasState { + fn allocate( + &mut self, + size: Size, + texture_kind: AtlasTextureKind, + ) -> Option { + { + let textures = match texture_kind { + AtlasTextureKind::Monochrome => &mut self.monochrome_textures, + AtlasTextureKind::Polychrome => &mut self.polychrome_textures, + }; + + if let Some(tile) = textures + .iter_mut() + .rev() + .find_map(|texture| texture.allocate(size)) + { + return Some(tile); + } + } + + let texture = self.push_texture(size, texture_kind)?; + texture.allocate(size) + } + + fn push_texture( + &mut self, + min_size: Size, + kind: AtlasTextureKind, + ) -> Option<&mut DirectXAtlasTexture> { + const DEFAULT_ATLAS_SIZE: Size = Size { + width: DevicePixels(1024), + height: DevicePixels(1024), + }; + // Max texture size for DirectX. See: + // https://learn.microsoft.com/en-us/windows/win32/direct3d11/overviews-direct3d-11-resources-limits + const MAX_ATLAS_SIZE: Size = Size { + width: DevicePixels(16384), + height: DevicePixels(16384), + }; + let size = min_size.min(&MAX_ATLAS_SIZE).max(&DEFAULT_ATLAS_SIZE); + let pixel_format; + let bind_flag; + let bytes_per_pixel; + match kind { + AtlasTextureKind::Monochrome => { + pixel_format = DXGI_FORMAT_R8_UNORM; + bind_flag = D3D11_BIND_SHADER_RESOURCE; + bytes_per_pixel = 1; + } + AtlasTextureKind::Polychrome => { + pixel_format = DXGI_FORMAT_B8G8R8A8_UNORM; + bind_flag = D3D11_BIND_SHADER_RESOURCE; + bytes_per_pixel = 4; + } + } + let texture_desc = D3D11_TEXTURE2D_DESC { + Width: size.width.0 as u32, + Height: size.height.0 as u32, + MipLevels: 1, + ArraySize: 1, + Format: pixel_format, + SampleDesc: DXGI_SAMPLE_DESC { + Count: 1, + Quality: 0, + }, + Usage: D3D11_USAGE_DEFAULT, + BindFlags: bind_flag.0 as u32, + CPUAccessFlags: 0, + MiscFlags: 0, + }; + let mut texture: Option = None; + unsafe { + // This only returns None if the device is lost, which we will recreate later. + // So it's ok to return None here. + self.device + .CreateTexture2D(&texture_desc, None, Some(&mut texture)) + .ok()?; + } + let texture = texture.unwrap(); + + let texture_list = match kind { + AtlasTextureKind::Monochrome => &mut self.monochrome_textures, + AtlasTextureKind::Polychrome => &mut self.polychrome_textures, + }; + let index = texture_list.free_list.pop(); + let view = unsafe { + let mut view = None; + self.device + .CreateShaderResourceView(&texture, None, Some(&mut view)) + .ok()?; + [view] + }; + let atlas_texture = DirectXAtlasTexture { + id: AtlasTextureId { + index: index.unwrap_or(texture_list.textures.len()) as u32, + kind, + }, + bytes_per_pixel, + allocator: etagere::BucketedAtlasAllocator::new(size.into()), + texture, + view, + live_atlas_keys: 0, + }; + if let Some(ix) = index { + texture_list.textures[ix] = Some(atlas_texture); + texture_list.textures.get_mut(ix).unwrap().as_mut() + } else { + texture_list.textures.push(Some(atlas_texture)); + texture_list.textures.last_mut().unwrap().as_mut() + } + } + + fn texture(&self, id: AtlasTextureId) -> &DirectXAtlasTexture { + let textures = match id.kind { + crate::AtlasTextureKind::Monochrome => &self.monochrome_textures, + crate::AtlasTextureKind::Polychrome => &self.polychrome_textures, + }; + textures[id.index as usize].as_ref().unwrap() + } +} + +impl DirectXAtlasTexture { + fn allocate(&mut self, size: Size) -> Option { + let allocation = self.allocator.allocate(size.into())?; + let tile = AtlasTile { + texture_id: self.id, + tile_id: allocation.id.into(), + bounds: Bounds { + origin: allocation.rectangle.min.into(), + size, + }, + padding: 0, + }; + self.live_atlas_keys += 1; + Some(tile) + } + + fn upload( + &self, + device_context: &ID3D11DeviceContext, + bounds: Bounds, + bytes: &[u8], + ) { + unsafe { + device_context.UpdateSubresource( + &self.texture, + 0, + Some(&D3D11_BOX { + left: bounds.left().0 as u32, + top: bounds.top().0 as u32, + front: 0, + right: bounds.right().0 as u32, + bottom: bounds.bottom().0 as u32, + back: 1, + }), + bytes.as_ptr() as _, + bounds.size.width.to_bytes(self.bytes_per_pixel as u8), + 0, + ); + } + } + + fn decrement_ref_count(&mut self) { + self.live_atlas_keys -= 1; + } + + fn is_unreferenced(&mut self) -> bool { + self.live_atlas_keys == 0 + } +} + +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), + } + } +} diff --git a/third_party/gpui/src/platform/windows/directx_devices.rs b/third_party/gpui/src/platform/windows/directx_devices.rs new file mode 100644 index 0000000..a6a2381 --- /dev/null +++ b/third_party/gpui/src/platform/windows/directx_devices.rs @@ -0,0 +1,197 @@ +use anyhow::{Context, Result}; +use util::ResultExt; +use windows::Win32::{ + Foundation::HMODULE, + Graphics::{ + Direct3D::{ + D3D_DRIVER_TYPE_UNKNOWN, D3D_FEATURE_LEVEL, D3D_FEATURE_LEVEL_10_1, + D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_11_1, + }, + Direct3D11::{ + D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_CREATE_DEVICE_DEBUG, + D3D11_FEATURE_D3D10_X_HARDWARE_OPTIONS, D3D11_FEATURE_DATA_D3D10_X_HARDWARE_OPTIONS, + D3D11_SDK_VERSION, D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, + }, + Dxgi::{ + CreateDXGIFactory2, DXGI_CREATE_FACTORY_DEBUG, DXGI_CREATE_FACTORY_FLAGS, + IDXGIAdapter1, IDXGIFactory6, + }, + }, +}; +use windows::core::Interface; + +pub(crate) fn try_to_recover_from_device_lost( + mut f: impl FnMut() -> Result, + on_success: impl FnOnce(T), + on_error: impl FnOnce(), +) { + let result = (0..5).find_map(|i| { + if i > 0 { + // Add a small delay before retrying + std::thread::sleep(std::time::Duration::from_millis(100)); + } + f().log_err() + }); + + if let Some(result) = result { + on_success(result); + } else { + on_error(); + } +} + +#[derive(Clone)] +pub(crate) struct DirectXDevices { + pub(crate) adapter: IDXGIAdapter1, + pub(crate) dxgi_factory: IDXGIFactory6, + pub(crate) device: ID3D11Device, + pub(crate) device_context: ID3D11DeviceContext, +} + +impl DirectXDevices { + pub(crate) fn new() -> Result { + let debug_layer_available = check_debug_layer_available(); + let dxgi_factory = + get_dxgi_factory(debug_layer_available).context("Creating DXGI factory")?; + let adapter = + get_adapter(&dxgi_factory, debug_layer_available).context("Getting DXGI adapter")?; + let (device, device_context) = { + let mut context: Option = None; + let mut feature_level = D3D_FEATURE_LEVEL::default(); + let device = get_device( + &adapter, + Some(&mut context), + Some(&mut feature_level), + debug_layer_available, + ) + .context("Creating Direct3D device")?; + match feature_level { + D3D_FEATURE_LEVEL_11_1 => { + log::info!("Created device with Direct3D 11.1 feature level.") + } + D3D_FEATURE_LEVEL_11_0 => { + log::info!("Created device with Direct3D 11.0 feature level.") + } + D3D_FEATURE_LEVEL_10_1 => { + log::info!("Created device with Direct3D 10.1 feature level.") + } + _ => unreachable!(), + } + (device, context.unwrap()) + }; + + Ok(Self { + adapter, + dxgi_factory, + device, + device_context, + }) + } +} + +#[inline] +fn check_debug_layer_available() -> bool { + #[cfg(debug_assertions)] + { + use windows::Win32::Graphics::Dxgi::{DXGIGetDebugInterface1, IDXGIInfoQueue}; + + unsafe { DXGIGetDebugInterface1::(0) } + .log_err() + .is_some() + } + #[cfg(not(debug_assertions))] + { + false + } +} + +#[inline] +fn get_dxgi_factory(debug_layer_available: bool) -> Result { + let factory_flag = if debug_layer_available { + DXGI_CREATE_FACTORY_DEBUG + } else { + #[cfg(debug_assertions)] + log::warn!( + "Failed to get DXGI debug interface. DirectX debugging features will be disabled." + ); + DXGI_CREATE_FACTORY_FLAGS::default() + }; + unsafe { Ok(CreateDXGIFactory2(factory_flag)?) } +} + +#[inline] +fn get_adapter(dxgi_factory: &IDXGIFactory6, debug_layer_available: bool) -> Result { + for adapter_index in 0.. { + let adapter: IDXGIAdapter1 = unsafe { dxgi_factory.EnumAdapters(adapter_index)?.cast()? }; + if let Ok(desc) = unsafe { adapter.GetDesc1() } { + let gpu_name = String::from_utf16_lossy(&desc.Description) + .trim_matches(char::from(0)) + .to_string(); + log::info!("Using GPU: {}", gpu_name); + } + // Check to see whether the adapter supports Direct3D 11, but don't + // create the actual device yet. + if get_device(&adapter, None, None, debug_layer_available) + .log_err() + .is_some() + { + return Ok(adapter); + } + } + + unreachable!() +} + +#[inline] +fn get_device( + adapter: &IDXGIAdapter1, + context: Option<*mut Option>, + feature_level: Option<*mut D3D_FEATURE_LEVEL>, + debug_layer_available: bool, +) -> Result { + let mut device: Option = None; + let device_flags = if debug_layer_available { + D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_DEBUG + } else { + D3D11_CREATE_DEVICE_BGRA_SUPPORT + }; + unsafe { + D3D11CreateDevice( + adapter, + D3D_DRIVER_TYPE_UNKNOWN, + HMODULE::default(), + device_flags, + // 4x MSAA is required for Direct3D Feature Level 10.1 or better + Some(&[ + D3D_FEATURE_LEVEL_11_1, + D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL_10_1, + ]), + D3D11_SDK_VERSION, + Some(&mut device), + feature_level, + context, + )?; + } + let device = device.unwrap(); + let mut data = D3D11_FEATURE_DATA_D3D10_X_HARDWARE_OPTIONS::default(); + unsafe { + device + .CheckFeatureSupport( + D3D11_FEATURE_D3D10_X_HARDWARE_OPTIONS, + &mut data as *mut _ as _, + std::mem::size_of::() as u32, + ) + .context("Checking GPU device feature support")?; + } + if data + .ComputeShaders_Plus_RawAndStructuredBuffers_Via_Shader_4_x + .as_bool() + { + Ok(device) + } else { + Err(anyhow::anyhow!( + "Required feature StructuredBuffer is not supported by GPU/driver" + )) + } +} diff --git a/third_party/gpui/src/platform/windows/directx_renderer.rs b/third_party/gpui/src/platform/windows/directx_renderer.rs new file mode 100644 index 0000000..2baa237 --- /dev/null +++ b/third_party/gpui/src/platform/windows/directx_renderer.rs @@ -0,0 +1,1758 @@ +use std::{ + mem::ManuallyDrop, + sync::{Arc, OnceLock}, +}; + +use ::util::ResultExt; +use anyhow::{Context, Result}; +use windows::{ + Win32::{ + Foundation::HWND, + Graphics::{ + Direct3D::*, + Direct3D11::*, + DirectComposition::*, + DirectWrite::*, + Dxgi::{Common::*, *}, + }, + }, + core::Interface, +}; + +use crate::{ + platform::windows::directx_renderer::shader_resources::{ + RawShaderBytes, ShaderModule, ShaderTarget, + }, + *, +}; + +pub(crate) const DISABLE_DIRECT_COMPOSITION: &str = "GPUI_DISABLE_DIRECT_COMPOSITION"; +const RENDER_TARGET_FORMAT: DXGI_FORMAT = DXGI_FORMAT_B8G8R8A8_UNORM; +// This configuration is used for MSAA rendering on paths only, and it's guaranteed to be supported by DirectX 11. +const PATH_MULTISAMPLE_COUNT: u32 = 4; + +pub(crate) struct FontInfo { + pub gamma_ratios: [f32; 4], + pub grayscale_enhanced_contrast: f32, +} + +pub(crate) struct DirectXRenderer { + hwnd: HWND, + atlas: Arc, + devices: ManuallyDrop, + resources: ManuallyDrop, + globals: DirectXGlobalElements, + pipelines: DirectXRenderPipelines, + direct_composition: Option, + font_info: &'static FontInfo, +} + +/// Direct3D objects +#[derive(Clone)] +pub(crate) struct DirectXRendererDevices { + pub(crate) adapter: IDXGIAdapter1, + pub(crate) dxgi_factory: IDXGIFactory6, + pub(crate) device: ID3D11Device, + pub(crate) device_context: ID3D11DeviceContext, + dxgi_device: Option, +} + +struct DirectXResources { + // Direct3D rendering objects + swap_chain: IDXGISwapChain1, + render_target: ManuallyDrop, + render_target_view: [Option; 1], + + // Path intermediate textures (with MSAA) + path_intermediate_texture: ID3D11Texture2D, + path_intermediate_srv: [Option; 1], + path_intermediate_msaa_texture: ID3D11Texture2D, + path_intermediate_msaa_view: [Option; 1], + + // Cached window size and viewport + width: u32, + height: u32, + viewport: [D3D11_VIEWPORT; 1], +} + +struct DirectXRenderPipelines { + shadow_pipeline: PipelineState, + quad_pipeline: PipelineState, + path_rasterization_pipeline: PipelineState, + path_sprite_pipeline: PipelineState, + underline_pipeline: PipelineState, + mono_sprites: PipelineState, + poly_sprites: PipelineState, +} + +struct DirectXGlobalElements { + global_params_buffer: [Option; 1], + sampler: [Option; 1], +} + +struct DirectComposition { + comp_device: IDCompositionDevice, + comp_target: IDCompositionTarget, + comp_visual: IDCompositionVisual, +} + +impl DirectXRendererDevices { + pub(crate) fn new( + directx_devices: &DirectXDevices, + disable_direct_composition: bool, + ) -> Result> { + let DirectXDevices { + adapter, + dxgi_factory, + device, + device_context, + } = directx_devices; + let dxgi_device = if disable_direct_composition { + None + } else { + Some(device.cast().context("Creating DXGI device")?) + }; + + Ok(ManuallyDrop::new(Self { + adapter: adapter.clone(), + dxgi_factory: dxgi_factory.clone(), + device: device.clone(), + device_context: device_context.clone(), + dxgi_device, + })) + } +} + +impl DirectXRenderer { + pub(crate) fn new( + hwnd: HWND, + directx_devices: &DirectXDevices, + disable_direct_composition: bool, + ) -> Result { + if disable_direct_composition { + log::info!("Direct Composition is disabled."); + } + + let devices = DirectXRendererDevices::new(directx_devices, disable_direct_composition) + .context("Creating DirectX devices")?; + let atlas = Arc::new(DirectXAtlas::new(&devices.device, &devices.device_context)); + + let resources = DirectXResources::new(&devices, 1, 1, hwnd, disable_direct_composition) + .context("Creating DirectX resources")?; + let globals = DirectXGlobalElements::new(&devices.device) + .context("Creating DirectX global elements")?; + let pipelines = DirectXRenderPipelines::new(&devices.device) + .context("Creating DirectX render pipelines")?; + + let direct_composition = if disable_direct_composition { + None + } else { + let composition = DirectComposition::new(devices.dxgi_device.as_ref().unwrap(), hwnd) + .context("Creating DirectComposition")?; + composition + .set_swap_chain(&resources.swap_chain) + .context("Setting swap chain for DirectComposition")?; + Some(composition) + }; + + Ok(DirectXRenderer { + hwnd, + atlas, + devices, + resources, + globals, + pipelines, + direct_composition, + font_info: Self::get_font_info(), + }) + } + + pub(crate) fn sprite_atlas(&self) -> Arc { + self.atlas.clone() + } + + fn pre_draw(&self) -> Result<()> { + update_buffer( + &self.devices.device_context, + self.globals.global_params_buffer[0].as_ref().unwrap(), + &[GlobalParams { + gamma_ratios: self.font_info.gamma_ratios, + viewport_size: [ + self.resources.viewport[0].Width, + self.resources.viewport[0].Height, + ], + grayscale_enhanced_contrast: self.font_info.grayscale_enhanced_contrast, + _pad: 0, + }], + )?; + unsafe { + self.devices.device_context.ClearRenderTargetView( + self.resources.render_target_view[0].as_ref().unwrap(), + &[0.0; 4], + ); + self.devices + .device_context + .OMSetRenderTargets(Some(&self.resources.render_target_view), None); + self.devices + .device_context + .RSSetViewports(Some(&self.resources.viewport)); + } + Ok(()) + } + + #[inline] + fn present(&mut self) -> Result<()> { + let result = unsafe { self.resources.swap_chain.Present(0, DXGI_PRESENT(0)) }; + result.ok().context("Presenting swap chain failed") + } + + pub(crate) fn handle_device_lost(&mut self, directx_devices: &DirectXDevices) { + try_to_recover_from_device_lost( + || { + self.handle_device_lost_impl(directx_devices) + .context("DirectXRenderer handling device lost") + }, + |_| {}, + || { + log::error!( + "DirectXRenderer failed to recover from device lost after multiple attempts" + ); + // Do something here? + // At this point, the device loss is considered unrecoverable. + }, + ); + } + + fn handle_device_lost_impl(&mut self, directx_devices: &DirectXDevices) -> Result<()> { + let disable_direct_composition = self.direct_composition.is_none(); + + unsafe { + #[cfg(debug_assertions)] + report_live_objects(&self.devices.device) + .context("Failed to report live objects after device lost") + .log_err(); + + ManuallyDrop::drop(&mut self.resources); + self.devices.device_context.OMSetRenderTargets(None, None); + self.devices.device_context.ClearState(); + self.devices.device_context.Flush(); + + #[cfg(debug_assertions)] + report_live_objects(&self.devices.device) + .context("Failed to report live objects after device lost") + .log_err(); + + drop(self.direct_composition.take()); + ManuallyDrop::drop(&mut self.devices); + } + + let devices = DirectXRendererDevices::new(directx_devices, disable_direct_composition) + .context("Recreating DirectX devices")?; + let resources = DirectXResources::new( + &devices, + self.resources.width, + self.resources.height, + self.hwnd, + disable_direct_composition, + )?; + let globals = DirectXGlobalElements::new(&devices.device)?; + let pipelines = DirectXRenderPipelines::new(&devices.device)?; + + let direct_composition = if disable_direct_composition { + None + } else { + let composition = + DirectComposition::new(devices.dxgi_device.as_ref().unwrap(), self.hwnd)?; + composition.set_swap_chain(&resources.swap_chain)?; + Some(composition) + }; + + self.atlas + .handle_device_lost(&devices.device, &devices.device_context); + self.devices = devices; + self.resources = resources; + self.globals = globals; + self.pipelines = pipelines; + self.direct_composition = direct_composition; + + unsafe { + self.devices + .device_context + .OMSetRenderTargets(Some(&self.resources.render_target_view), None); + } + Ok(()) + } + + pub(crate) fn draw(&mut self, scene: &Scene) -> Result<()> { + self.pre_draw()?; + for batch in scene.batches() { + match batch { + PrimitiveBatch::Shadows(shadows) => self.draw_shadows(shadows), + PrimitiveBatch::Quads(quads) => self.draw_quads(quads), + PrimitiveBatch::Paths(paths) => { + self.draw_paths_to_intermediate(paths)?; + self.draw_paths_from_intermediate(paths) + } + PrimitiveBatch::Underlines(underlines) => self.draw_underlines(underlines), + PrimitiveBatch::MonochromeSprites { + texture_id, + sprites, + } => self.draw_monochrome_sprites(texture_id, sprites), + PrimitiveBatch::PolychromeSprites { + texture_id, + sprites, + } => self.draw_polychrome_sprites(texture_id, sprites), + PrimitiveBatch::Surfaces(surfaces) => self.draw_surfaces(surfaces), + }.context(format!("scene too large: {} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces", + scene.paths.len(), + scene.shadows.len(), + scene.quads.len(), + scene.underlines.len(), + scene.monochrome_sprites.len(), + scene.polychrome_sprites.len(), + scene.surfaces.len(),))?; + } + self.present() + } + + pub(crate) fn resize(&mut self, new_size: Size) -> Result<()> { + let width = new_size.width.0.max(1) as u32; + let height = new_size.height.0.max(1) as u32; + if self.resources.width == width && self.resources.height == height { + return Ok(()); + } + self.resources.width = width; + self.resources.height = height; + + // Clear the render target before resizing + unsafe { self.devices.device_context.OMSetRenderTargets(None, None) }; + unsafe { ManuallyDrop::drop(&mut self.resources.render_target) }; + drop(self.resources.render_target_view[0].take().unwrap()); + + // Resizing the swap chain requires a call to the underlying DXGI adapter, which can return the device removed error. + // The app might have moved to a monitor that's attached to a different graphics device. + // When a graphics device is removed or reset, the desktop resolution often changes, resulting in a window size change. + // But here we just return the error, because we are handling device lost scenarios elsewhere. + unsafe { + self.resources + .swap_chain + .ResizeBuffers( + BUFFER_COUNT as u32, + width, + height, + RENDER_TARGET_FORMAT, + DXGI_SWAP_CHAIN_FLAG(0), + ) + .context("Failed to resize swap chain")?; + } + + self.resources + .recreate_resources(&self.devices, width, height)?; + unsafe { + self.devices + .device_context + .OMSetRenderTargets(Some(&self.resources.render_target_view), None); + } + + Ok(()) + } + + fn draw_shadows(&mut self, shadows: &[Shadow]) -> Result<()> { + if shadows.is_empty() { + return Ok(()); + } + self.pipelines.shadow_pipeline.update_buffer( + &self.devices.device, + &self.devices.device_context, + shadows, + )?; + self.pipelines.shadow_pipeline.draw( + &self.devices.device_context, + &self.resources.viewport, + &self.globals.global_params_buffer, + D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP, + 4, + shadows.len() as u32, + ) + } + + fn draw_quads(&mut self, quads: &[Quad]) -> Result<()> { + if quads.is_empty() { + return Ok(()); + } + self.pipelines.quad_pipeline.update_buffer( + &self.devices.device, + &self.devices.device_context, + quads, + )?; + self.pipelines.quad_pipeline.draw( + &self.devices.device_context, + &self.resources.viewport, + &self.globals.global_params_buffer, + D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP, + 4, + quads.len() as u32, + ) + } + + fn draw_paths_to_intermediate(&mut self, paths: &[Path]) -> Result<()> { + if paths.is_empty() { + return Ok(()); + } + + // Clear intermediate MSAA texture + unsafe { + self.devices.device_context.ClearRenderTargetView( + self.resources.path_intermediate_msaa_view[0] + .as_ref() + .unwrap(), + &[0.0; 4], + ); + // Set intermediate MSAA texture as render target + self.devices + .device_context + .OMSetRenderTargets(Some(&self.resources.path_intermediate_msaa_view), None); + } + + // Collect all vertices and sprites for a single draw call + let mut vertices = Vec::new(); + + for path in paths { + vertices.extend(path.vertices.iter().map(|v| PathRasterizationSprite { + xy_position: v.xy_position, + st_position: v.st_position, + color: path.color, + bounds: path.clipped_bounds(), + })); + } + + self.pipelines.path_rasterization_pipeline.update_buffer( + &self.devices.device, + &self.devices.device_context, + &vertices, + )?; + self.pipelines.path_rasterization_pipeline.draw( + &self.devices.device_context, + &self.resources.viewport, + &self.globals.global_params_buffer, + D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST, + vertices.len() as u32, + 1, + )?; + + // Resolve MSAA to non-MSAA intermediate texture + unsafe { + self.devices.device_context.ResolveSubresource( + &self.resources.path_intermediate_texture, + 0, + &self.resources.path_intermediate_msaa_texture, + 0, + RENDER_TARGET_FORMAT, + ); + // Restore main render target + self.devices + .device_context + .OMSetRenderTargets(Some(&self.resources.render_target_view), None); + } + + Ok(()) + } + + fn draw_paths_from_intermediate(&mut self, paths: &[Path]) -> Result<()> { + let Some(first_path) = paths.first() else { + return Ok(()); + }; + + // When copying paths from the intermediate texture to the drawable, + // each pixel must only be copied once, in case of transparent paths. + // + // If all paths have the same draw order, then their bounds are all + // disjoint, so we can copy each path's bounds individually. If this + // batch combines different draw orders, we perform a single copy + // for a minimal spanning rect. + let sprites = if paths.last().unwrap().order == first_path.order { + paths + .iter() + .map(|path| PathSprite { + bounds: path.clipped_bounds(), + }) + .collect::>() + } else { + let mut bounds = first_path.clipped_bounds(); + for path in paths.iter().skip(1) { + bounds = bounds.union(&path.clipped_bounds()); + } + vec![PathSprite { bounds }] + }; + + self.pipelines.path_sprite_pipeline.update_buffer( + &self.devices.device, + &self.devices.device_context, + &sprites, + )?; + + // Draw the sprites with the path texture + self.pipelines.path_sprite_pipeline.draw_with_texture( + &self.devices.device_context, + &self.resources.path_intermediate_srv, + &self.resources.viewport, + &self.globals.global_params_buffer, + &self.globals.sampler, + sprites.len() as u32, + ) + } + + fn draw_underlines(&mut self, underlines: &[Underline]) -> Result<()> { + if underlines.is_empty() { + return Ok(()); + } + self.pipelines.underline_pipeline.update_buffer( + &self.devices.device, + &self.devices.device_context, + underlines, + )?; + self.pipelines.underline_pipeline.draw( + &self.devices.device_context, + &self.resources.viewport, + &self.globals.global_params_buffer, + D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP, + 4, + underlines.len() as u32, + ) + } + + fn draw_monochrome_sprites( + &mut self, + texture_id: AtlasTextureId, + sprites: &[MonochromeSprite], + ) -> Result<()> { + if sprites.is_empty() { + return Ok(()); + } + self.pipelines.mono_sprites.update_buffer( + &self.devices.device, + &self.devices.device_context, + sprites, + )?; + let texture_view = self.atlas.get_texture_view(texture_id); + self.pipelines.mono_sprites.draw_with_texture( + &self.devices.device_context, + &texture_view, + &self.resources.viewport, + &self.globals.global_params_buffer, + &self.globals.sampler, + sprites.len() as u32, + ) + } + + fn draw_polychrome_sprites( + &mut self, + texture_id: AtlasTextureId, + sprites: &[PolychromeSprite], + ) -> Result<()> { + if sprites.is_empty() { + return Ok(()); + } + self.pipelines.poly_sprites.update_buffer( + &self.devices.device, + &self.devices.device_context, + sprites, + )?; + let texture_view = self.atlas.get_texture_view(texture_id); + self.pipelines.poly_sprites.draw_with_texture( + &self.devices.device_context, + &texture_view, + &self.resources.viewport, + &self.globals.global_params_buffer, + &self.globals.sampler, + sprites.len() as u32, + ) + } + + fn draw_surfaces(&mut self, surfaces: &[PaintSurface]) -> Result<()> { + if surfaces.is_empty() { + return Ok(()); + } + Ok(()) + } + + pub(crate) fn gpu_specs(&self) -> Result { + let desc = unsafe { self.devices.adapter.GetDesc1() }?; + let is_software_emulated = (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE.0 as u32) != 0; + let device_name = String::from_utf16_lossy(&desc.Description) + .trim_matches(char::from(0)) + .to_string(); + let driver_name = match desc.VendorId { + 0x10DE => "NVIDIA Corporation".to_string(), + 0x1002 => "AMD Corporation".to_string(), + 0x8086 => "Intel Corporation".to_string(), + id => format!("Unknown Vendor (ID: {:#X})", id), + }; + let driver_version = match desc.VendorId { + 0x10DE => nvidia::get_driver_version(), + 0x1002 => amd::get_driver_version(), + // For Intel and other vendors, we use the DXGI API to get the driver version. + _ => dxgi::get_driver_version(&self.devices.adapter), + } + .context("Failed to get gpu driver info") + .log_err() + .unwrap_or("Unknown Driver".to_string()); + Ok(GpuSpecs { + is_software_emulated, + device_name, + driver_name, + driver_info: driver_version, + }) + } + + pub(crate) fn get_font_info() -> &'static FontInfo { + static CACHED_FONT_INFO: OnceLock = OnceLock::new(); + CACHED_FONT_INFO.get_or_init(|| unsafe { + let factory: IDWriteFactory5 = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED).unwrap(); + let render_params: IDWriteRenderingParams1 = + factory.CreateRenderingParams().unwrap().cast().unwrap(); + FontInfo { + gamma_ratios: Self::get_gamma_ratios(render_params.GetGamma()), + grayscale_enhanced_contrast: render_params.GetGrayscaleEnhancedContrast(), + } + }) + } + + // Gamma ratios for brightening/darkening edges for better contrast + // https://github.com/microsoft/terminal/blob/1283c0f5b99a2961673249fa77c6b986efb5086c/src/renderer/atlas/dwrite.cpp#L50 + fn get_gamma_ratios(gamma: f32) -> [f32; 4] { + const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [ + [0.0000 / 4.0, 0.0000 / 4.0, 0.0000 / 4.0, 0.0000 / 4.0], // gamma = 1.0 + [0.0166 / 4.0, -0.0807 / 4.0, 0.2227 / 4.0, -0.0751 / 4.0], // gamma = 1.1 + [0.0350 / 4.0, -0.1760 / 4.0, 0.4325 / 4.0, -0.1370 / 4.0], // gamma = 1.2 + [0.0543 / 4.0, -0.2821 / 4.0, 0.6302 / 4.0, -0.1876 / 4.0], // gamma = 1.3 + [0.0739 / 4.0, -0.3963 / 4.0, 0.8167 / 4.0, -0.2287 / 4.0], // gamma = 1.4 + [0.0933 / 4.0, -0.5161 / 4.0, 0.9926 / 4.0, -0.2616 / 4.0], // gamma = 1.5 + [0.1121 / 4.0, -0.6395 / 4.0, 1.1588 / 4.0, -0.2877 / 4.0], // gamma = 1.6 + [0.1300 / 4.0, -0.7649 / 4.0, 1.3159 / 4.0, -0.3080 / 4.0], // gamma = 1.7 + [0.1469 / 4.0, -0.8911 / 4.0, 1.4644 / 4.0, -0.3234 / 4.0], // gamma = 1.8 + [0.1627 / 4.0, -1.0170 / 4.0, 1.6051 / 4.0, -0.3347 / 4.0], // gamma = 1.9 + [0.1773 / 4.0, -1.1420 / 4.0, 1.7385 / 4.0, -0.3426 / 4.0], // gamma = 2.0 + [0.1908 / 4.0, -1.2652 / 4.0, 1.8650 / 4.0, -0.3476 / 4.0], // gamma = 2.1 + [0.2031 / 4.0, -1.3864 / 4.0, 1.9851 / 4.0, -0.3501 / 4.0], // gamma = 2.2 + ]; + + const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32; + const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32; + + let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10; + let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index]; + + [ + ratios[0] * NORM13, + ratios[1] * NORM24, + ratios[2] * NORM13, + ratios[3] * NORM24, + ] + } +} + +impl DirectXResources { + pub fn new( + devices: &DirectXRendererDevices, + width: u32, + height: u32, + hwnd: HWND, + disable_direct_composition: bool, + ) -> Result> { + let swap_chain = if disable_direct_composition { + create_swap_chain(&devices.dxgi_factory, &devices.device, hwnd, width, height)? + } else { + create_swap_chain_for_composition( + &devices.dxgi_factory, + &devices.device, + width, + height, + )? + }; + + let ( + render_target, + render_target_view, + path_intermediate_texture, + path_intermediate_srv, + path_intermediate_msaa_texture, + path_intermediate_msaa_view, + viewport, + ) = create_resources(devices, &swap_chain, width, height)?; + set_rasterizer_state(&devices.device, &devices.device_context)?; + + Ok(ManuallyDrop::new(Self { + swap_chain, + render_target, + render_target_view, + path_intermediate_texture, + path_intermediate_msaa_texture, + path_intermediate_msaa_view, + path_intermediate_srv, + viewport, + width, + height, + })) + } + + #[inline] + fn recreate_resources( + &mut self, + devices: &DirectXRendererDevices, + width: u32, + height: u32, + ) -> Result<()> { + let ( + render_target, + render_target_view, + path_intermediate_texture, + path_intermediate_srv, + path_intermediate_msaa_texture, + path_intermediate_msaa_view, + viewport, + ) = create_resources(devices, &self.swap_chain, width, height)?; + self.render_target = render_target; + self.render_target_view = render_target_view; + self.path_intermediate_texture = path_intermediate_texture; + self.path_intermediate_msaa_texture = path_intermediate_msaa_texture; + self.path_intermediate_msaa_view = path_intermediate_msaa_view; + self.path_intermediate_srv = path_intermediate_srv; + self.viewport = viewport; + Ok(()) + } +} + +impl DirectXRenderPipelines { + pub fn new(device: &ID3D11Device) -> Result { + let shadow_pipeline = PipelineState::new( + device, + "shadow_pipeline", + ShaderModule::Shadow, + 4, + create_blend_state(device)?, + )?; + let quad_pipeline = PipelineState::new( + device, + "quad_pipeline", + ShaderModule::Quad, + 64, + create_blend_state(device)?, + )?; + let path_rasterization_pipeline = PipelineState::new( + device, + "path_rasterization_pipeline", + ShaderModule::PathRasterization, + 32, + create_blend_state_for_path_rasterization(device)?, + )?; + let path_sprite_pipeline = PipelineState::new( + device, + "path_sprite_pipeline", + ShaderModule::PathSprite, + 4, + create_blend_state_for_path_sprite(device)?, + )?; + let underline_pipeline = PipelineState::new( + device, + "underline_pipeline", + ShaderModule::Underline, + 4, + create_blend_state(device)?, + )?; + let mono_sprites = PipelineState::new( + device, + "monochrome_sprite_pipeline", + ShaderModule::MonochromeSprite, + 512, + create_blend_state(device)?, + )?; + let poly_sprites = PipelineState::new( + device, + "polychrome_sprite_pipeline", + ShaderModule::PolychromeSprite, + 16, + create_blend_state(device)?, + )?; + + Ok(Self { + shadow_pipeline, + quad_pipeline, + path_rasterization_pipeline, + path_sprite_pipeline, + underline_pipeline, + mono_sprites, + poly_sprites, + }) + } +} + +impl DirectComposition { + pub fn new(dxgi_device: &IDXGIDevice, hwnd: HWND) -> Result { + let comp_device = get_comp_device(dxgi_device)?; + let comp_target = unsafe { comp_device.CreateTargetForHwnd(hwnd, true) }?; + let comp_visual = unsafe { comp_device.CreateVisual() }?; + + Ok(Self { + comp_device, + comp_target, + comp_visual, + }) + } + + pub fn set_swap_chain(&self, swap_chain: &IDXGISwapChain1) -> Result<()> { + unsafe { + self.comp_visual.SetContent(swap_chain)?; + self.comp_target.SetRoot(&self.comp_visual)?; + self.comp_device.Commit()?; + } + Ok(()) + } +} + +impl DirectXGlobalElements { + pub fn new(device: &ID3D11Device) -> Result { + let global_params_buffer = unsafe { + let desc = D3D11_BUFFER_DESC { + ByteWidth: std::mem::size_of::() as u32, + Usage: D3D11_USAGE_DYNAMIC, + BindFlags: D3D11_BIND_CONSTANT_BUFFER.0 as u32, + CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, + ..Default::default() + }; + let mut buffer = None; + device.CreateBuffer(&desc, None, Some(&mut buffer))?; + [buffer] + }; + + let sampler = unsafe { + let desc = D3D11_SAMPLER_DESC { + Filter: D3D11_FILTER_MIN_MAG_MIP_LINEAR, + AddressU: D3D11_TEXTURE_ADDRESS_WRAP, + AddressV: D3D11_TEXTURE_ADDRESS_WRAP, + AddressW: D3D11_TEXTURE_ADDRESS_WRAP, + MipLODBias: 0.0, + MaxAnisotropy: 1, + ComparisonFunc: D3D11_COMPARISON_ALWAYS, + BorderColor: [0.0; 4], + MinLOD: 0.0, + MaxLOD: D3D11_FLOAT32_MAX, + }; + let mut output = None; + device.CreateSamplerState(&desc, Some(&mut output))?; + [output] + }; + + Ok(Self { + global_params_buffer, + sampler, + }) + } +} + +#[derive(Debug, Default)] +#[repr(C)] +struct GlobalParams { + gamma_ratios: [f32; 4], + viewport_size: [f32; 2], + grayscale_enhanced_contrast: f32, + _pad: u32, +} + +struct PipelineState { + label: &'static str, + vertex: ID3D11VertexShader, + fragment: ID3D11PixelShader, + buffer: ID3D11Buffer, + buffer_size: usize, + view: [Option; 1], + blend_state: ID3D11BlendState, + _marker: std::marker::PhantomData, +} + +impl PipelineState { + fn new( + device: &ID3D11Device, + label: &'static str, + shader_module: ShaderModule, + buffer_size: usize, + blend_state: ID3D11BlendState, + ) -> Result { + let vertex = { + let raw_shader = RawShaderBytes::new(shader_module, ShaderTarget::Vertex)?; + create_vertex_shader(device, raw_shader.as_bytes())? + }; + let fragment = { + let raw_shader = RawShaderBytes::new(shader_module, ShaderTarget::Fragment)?; + create_fragment_shader(device, raw_shader.as_bytes())? + }; + let buffer = create_buffer(device, std::mem::size_of::(), buffer_size)?; + let view = create_buffer_view(device, &buffer)?; + + Ok(PipelineState { + label, + vertex, + fragment, + buffer, + buffer_size, + view, + blend_state, + _marker: std::marker::PhantomData, + }) + } + + fn update_buffer( + &mut self, + device: &ID3D11Device, + device_context: &ID3D11DeviceContext, + data: &[T], + ) -> Result<()> { + if self.buffer_size < data.len() { + let new_buffer_size = data.len().next_power_of_two(); + log::info!( + "Updating {} buffer size from {} to {}", + self.label, + self.buffer_size, + new_buffer_size + ); + let buffer = create_buffer(device, std::mem::size_of::(), new_buffer_size)?; + let view = create_buffer_view(device, &buffer)?; + self.buffer = buffer; + self.view = view; + self.buffer_size = new_buffer_size; + } + update_buffer(device_context, &self.buffer, data) + } + + fn draw( + &self, + device_context: &ID3D11DeviceContext, + viewport: &[D3D11_VIEWPORT], + global_params: &[Option], + topology: D3D_PRIMITIVE_TOPOLOGY, + vertex_count: u32, + instance_count: u32, + ) -> Result<()> { + set_pipeline_state( + device_context, + &self.view, + topology, + viewport, + &self.vertex, + &self.fragment, + global_params, + &self.blend_state, + ); + unsafe { + device_context.DrawInstanced(vertex_count, instance_count, 0, 0); + } + Ok(()) + } + + fn draw_with_texture( + &self, + device_context: &ID3D11DeviceContext, + texture: &[Option], + viewport: &[D3D11_VIEWPORT], + global_params: &[Option], + sampler: &[Option], + instance_count: u32, + ) -> Result<()> { + set_pipeline_state( + device_context, + &self.view, + D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP, + viewport, + &self.vertex, + &self.fragment, + global_params, + &self.blend_state, + ); + unsafe { + device_context.PSSetSamplers(0, Some(sampler)); + device_context.VSSetShaderResources(0, Some(texture)); + device_context.PSSetShaderResources(0, Some(texture)); + + device_context.DrawInstanced(4, instance_count, 0, 0); + } + Ok(()) + } +} + +#[derive(Clone, Copy)] +#[repr(C)] +struct PathRasterizationSprite { + xy_position: Point, + st_position: Point, + color: Background, + bounds: Bounds, +} + +#[derive(Clone, Copy)] +#[repr(C)] +struct PathSprite { + bounds: Bounds, +} + +impl Drop for DirectXRenderer { + fn drop(&mut self) { + #[cfg(debug_assertions)] + report_live_objects(&self.devices.device).ok(); + unsafe { + ManuallyDrop::drop(&mut self.devices); + ManuallyDrop::drop(&mut self.resources); + } + } +} + +impl Drop for DirectXResources { + fn drop(&mut self) { + unsafe { + ManuallyDrop::drop(&mut self.render_target); + } + } +} + +#[inline] +fn get_comp_device(dxgi_device: &IDXGIDevice) -> Result { + Ok(unsafe { DCompositionCreateDevice(dxgi_device)? }) +} + +fn create_swap_chain_for_composition( + dxgi_factory: &IDXGIFactory6, + device: &ID3D11Device, + width: u32, + height: u32, +) -> Result { + let desc = DXGI_SWAP_CHAIN_DESC1 { + Width: width, + Height: height, + Format: RENDER_TARGET_FORMAT, + Stereo: false.into(), + SampleDesc: DXGI_SAMPLE_DESC { + Count: 1, + Quality: 0, + }, + BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT, + BufferCount: BUFFER_COUNT as u32, + // Composition SwapChains only support the DXGI_SCALING_STRETCH Scaling. + Scaling: DXGI_SCALING_STRETCH, + SwapEffect: DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL, + AlphaMode: DXGI_ALPHA_MODE_PREMULTIPLIED, + Flags: 0, + }; + Ok(unsafe { dxgi_factory.CreateSwapChainForComposition(device, &desc, None)? }) +} + +fn create_swap_chain( + dxgi_factory: &IDXGIFactory6, + device: &ID3D11Device, + hwnd: HWND, + width: u32, + height: u32, +) -> Result { + use windows::Win32::Graphics::Dxgi::DXGI_MWA_NO_ALT_ENTER; + + let desc = DXGI_SWAP_CHAIN_DESC1 { + Width: width, + Height: height, + Format: RENDER_TARGET_FORMAT, + Stereo: false.into(), + SampleDesc: DXGI_SAMPLE_DESC { + Count: 1, + Quality: 0, + }, + BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT, + BufferCount: BUFFER_COUNT as u32, + Scaling: DXGI_SCALING_NONE, + SwapEffect: DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL, + AlphaMode: DXGI_ALPHA_MODE_IGNORE, + Flags: 0, + }; + let swap_chain = + unsafe { dxgi_factory.CreateSwapChainForHwnd(device, hwnd, &desc, None, None) }?; + unsafe { dxgi_factory.MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER) }?; + Ok(swap_chain) +} + +#[inline] +fn create_resources( + devices: &DirectXRendererDevices, + swap_chain: &IDXGISwapChain1, + width: u32, + height: u32, +) -> Result<( + ManuallyDrop, + [Option; 1], + ID3D11Texture2D, + [Option; 1], + ID3D11Texture2D, + [Option; 1], + [D3D11_VIEWPORT; 1], +)> { + let (render_target, render_target_view) = + create_render_target_and_its_view(swap_chain, &devices.device)?; + let (path_intermediate_texture, path_intermediate_srv) = + create_path_intermediate_texture(&devices.device, width, height)?; + let (path_intermediate_msaa_texture, path_intermediate_msaa_view) = + create_path_intermediate_msaa_texture_and_view(&devices.device, width, height)?; + let viewport = set_viewport(&devices.device_context, width as f32, height as f32); + Ok(( + render_target, + render_target_view, + path_intermediate_texture, + path_intermediate_srv, + path_intermediate_msaa_texture, + path_intermediate_msaa_view, + viewport, + )) +} + +#[inline] +fn create_render_target_and_its_view( + swap_chain: &IDXGISwapChain1, + device: &ID3D11Device, +) -> Result<( + ManuallyDrop, + [Option; 1], +)> { + let render_target: ID3D11Texture2D = unsafe { swap_chain.GetBuffer(0) }?; + let mut render_target_view = None; + unsafe { device.CreateRenderTargetView(&render_target, None, Some(&mut render_target_view))? }; + Ok(( + ManuallyDrop::new(render_target), + [Some(render_target_view.unwrap())], + )) +} + +#[inline] +fn create_path_intermediate_texture( + device: &ID3D11Device, + width: u32, + height: u32, +) -> Result<(ID3D11Texture2D, [Option; 1])> { + let texture = unsafe { + let mut output = None; + let desc = D3D11_TEXTURE2D_DESC { + Width: width, + Height: height, + MipLevels: 1, + ArraySize: 1, + Format: RENDER_TARGET_FORMAT, + SampleDesc: DXGI_SAMPLE_DESC { + Count: 1, + Quality: 0, + }, + Usage: D3D11_USAGE_DEFAULT, + BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32, + CPUAccessFlags: 0, + MiscFlags: 0, + }; + device.CreateTexture2D(&desc, None, Some(&mut output))?; + output.unwrap() + }; + + let mut shader_resource_view = None; + unsafe { device.CreateShaderResourceView(&texture, None, Some(&mut shader_resource_view))? }; + + Ok((texture, [Some(shader_resource_view.unwrap())])) +} + +#[inline] +fn create_path_intermediate_msaa_texture_and_view( + device: &ID3D11Device, + width: u32, + height: u32, +) -> Result<(ID3D11Texture2D, [Option; 1])> { + let msaa_texture = unsafe { + let mut output = None; + let desc = D3D11_TEXTURE2D_DESC { + Width: width, + Height: height, + MipLevels: 1, + ArraySize: 1, + Format: RENDER_TARGET_FORMAT, + SampleDesc: DXGI_SAMPLE_DESC { + Count: PATH_MULTISAMPLE_COUNT, + Quality: D3D11_STANDARD_MULTISAMPLE_PATTERN.0 as u32, + }, + Usage: D3D11_USAGE_DEFAULT, + BindFlags: D3D11_BIND_RENDER_TARGET.0 as u32, + CPUAccessFlags: 0, + MiscFlags: 0, + }; + device.CreateTexture2D(&desc, None, Some(&mut output))?; + output.unwrap() + }; + let mut msaa_view = None; + unsafe { device.CreateRenderTargetView(&msaa_texture, None, Some(&mut msaa_view))? }; + Ok((msaa_texture, [Some(msaa_view.unwrap())])) +} + +#[inline] +fn set_viewport( + device_context: &ID3D11DeviceContext, + width: f32, + height: f32, +) -> [D3D11_VIEWPORT; 1] { + let viewport = [D3D11_VIEWPORT { + TopLeftX: 0.0, + TopLeftY: 0.0, + Width: width, + Height: height, + MinDepth: 0.0, + MaxDepth: 1.0, + }]; + unsafe { device_context.RSSetViewports(Some(&viewport)) }; + viewport +} + +#[inline] +fn set_rasterizer_state(device: &ID3D11Device, device_context: &ID3D11DeviceContext) -> Result<()> { + let desc = D3D11_RASTERIZER_DESC { + FillMode: D3D11_FILL_SOLID, + CullMode: D3D11_CULL_NONE, + FrontCounterClockwise: false.into(), + DepthBias: 0, + DepthBiasClamp: 0.0, + SlopeScaledDepthBias: 0.0, + DepthClipEnable: true.into(), + ScissorEnable: false.into(), + MultisampleEnable: true.into(), + AntialiasedLineEnable: false.into(), + }; + let rasterizer_state = unsafe { + let mut state = None; + device.CreateRasterizerState(&desc, Some(&mut state))?; + state.unwrap() + }; + unsafe { device_context.RSSetState(&rasterizer_state) }; + Ok(()) +} + +// https://learn.microsoft.com/en-us/windows/win32/api/d3d11/ns-d3d11-d3d11_blend_desc +#[inline] +fn create_blend_state(device: &ID3D11Device) -> Result { + // If the feature level is set to greater than D3D_FEATURE_LEVEL_9_3, the display + // device performs the blend in linear space, which is ideal. + let mut desc = D3D11_BLEND_DESC::default(); + desc.RenderTarget[0].BlendEnable = true.into(); + desc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD; + desc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD; + desc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA; + desc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE; + desc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA; + desc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ONE; + desc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL.0 as u8; + unsafe { + let mut state = None; + device.CreateBlendState(&desc, Some(&mut state))?; + Ok(state.unwrap()) + } +} + +#[inline] +fn create_blend_state_for_path_rasterization(device: &ID3D11Device) -> Result { + // If the feature level is set to greater than D3D_FEATURE_LEVEL_9_3, the display + // device performs the blend in linear space, which is ideal. + let mut desc = D3D11_BLEND_DESC::default(); + desc.RenderTarget[0].BlendEnable = true.into(); + desc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD; + desc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD; + desc.RenderTarget[0].SrcBlend = D3D11_BLEND_ONE; + desc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE; + desc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA; + desc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_INV_SRC_ALPHA; + desc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL.0 as u8; + unsafe { + let mut state = None; + device.CreateBlendState(&desc, Some(&mut state))?; + Ok(state.unwrap()) + } +} + +#[inline] +fn create_blend_state_for_path_sprite(device: &ID3D11Device) -> Result { + // If the feature level is set to greater than D3D_FEATURE_LEVEL_9_3, the display + // device performs the blend in linear space, which is ideal. + let mut desc = D3D11_BLEND_DESC::default(); + desc.RenderTarget[0].BlendEnable = true.into(); + desc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD; + desc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD; + desc.RenderTarget[0].SrcBlend = D3D11_BLEND_ONE; + desc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE; + desc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA; + desc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ONE; + desc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL.0 as u8; + unsafe { + let mut state = None; + device.CreateBlendState(&desc, Some(&mut state))?; + Ok(state.unwrap()) + } +} + +#[inline] +fn create_vertex_shader(device: &ID3D11Device, bytes: &[u8]) -> Result { + unsafe { + let mut shader = None; + device.CreateVertexShader(bytes, None, Some(&mut shader))?; + Ok(shader.unwrap()) + } +} + +#[inline] +fn create_fragment_shader(device: &ID3D11Device, bytes: &[u8]) -> Result { + unsafe { + let mut shader = None; + device.CreatePixelShader(bytes, None, Some(&mut shader))?; + Ok(shader.unwrap()) + } +} + +#[inline] +fn create_buffer( + device: &ID3D11Device, + element_size: usize, + buffer_size: usize, +) -> Result { + let desc = D3D11_BUFFER_DESC { + ByteWidth: (element_size * buffer_size) as u32, + Usage: D3D11_USAGE_DYNAMIC, + BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32, + CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, + MiscFlags: D3D11_RESOURCE_MISC_BUFFER_STRUCTURED.0 as u32, + StructureByteStride: element_size as u32, + }; + let mut buffer = None; + unsafe { device.CreateBuffer(&desc, None, Some(&mut buffer)) }?; + Ok(buffer.unwrap()) +} + +#[inline] +fn create_buffer_view( + device: &ID3D11Device, + buffer: &ID3D11Buffer, +) -> Result<[Option; 1]> { + let mut view = None; + unsafe { device.CreateShaderResourceView(buffer, None, Some(&mut view)) }?; + Ok([view]) +} + +#[inline] +fn update_buffer( + device_context: &ID3D11DeviceContext, + buffer: &ID3D11Buffer, + data: &[T], +) -> Result<()> { + unsafe { + let mut dest = std::mem::zeroed(); + device_context.Map(buffer, 0, D3D11_MAP_WRITE_DISCARD, 0, Some(&mut dest))?; + std::ptr::copy_nonoverlapping(data.as_ptr(), dest.pData as _, data.len()); + device_context.Unmap(buffer, 0); + } + Ok(()) +} + +#[inline] +fn set_pipeline_state( + device_context: &ID3D11DeviceContext, + buffer_view: &[Option], + topology: D3D_PRIMITIVE_TOPOLOGY, + viewport: &[D3D11_VIEWPORT], + vertex_shader: &ID3D11VertexShader, + fragment_shader: &ID3D11PixelShader, + global_params: &[Option], + blend_state: &ID3D11BlendState, +) { + unsafe { + device_context.VSSetShaderResources(1, Some(buffer_view)); + device_context.PSSetShaderResources(1, Some(buffer_view)); + device_context.IASetPrimitiveTopology(topology); + device_context.RSSetViewports(Some(viewport)); + device_context.VSSetShader(vertex_shader, None); + device_context.PSSetShader(fragment_shader, None); + device_context.VSSetConstantBuffers(0, Some(global_params)); + device_context.PSSetConstantBuffers(0, Some(global_params)); + device_context.OMSetBlendState(blend_state, None, 0xFFFFFFFF); + } +} + +#[cfg(debug_assertions)] +fn report_live_objects(device: &ID3D11Device) -> Result<()> { + let debug_device: ID3D11Debug = device.cast()?; + unsafe { + debug_device.ReportLiveDeviceObjects(D3D11_RLDO_DETAIL)?; + } + Ok(()) +} + +const BUFFER_COUNT: usize = 3; + +pub(crate) mod shader_resources { + use anyhow::Result; + + #[cfg(debug_assertions)] + use windows::{ + Win32::Graphics::Direct3D::{ + Fxc::{D3DCOMPILE_DEBUG, D3DCOMPILE_SKIP_OPTIMIZATION, D3DCompileFromFile}, + ID3DBlob, + }, + core::{HSTRING, PCSTR}, + }; + + #[derive(Copy, Clone, Debug, Eq, PartialEq)] + pub(crate) enum ShaderModule { + Quad, + Shadow, + Underline, + PathRasterization, + PathSprite, + MonochromeSprite, + PolychromeSprite, + EmojiRasterization, + } + + #[derive(Copy, Clone, Debug, Eq, PartialEq)] + pub(crate) enum ShaderTarget { + Vertex, + Fragment, + } + + pub(crate) struct RawShaderBytes<'t> { + inner: &'t [u8], + + #[cfg(debug_assertions)] + _blob: ID3DBlob, + } + + impl<'t> RawShaderBytes<'t> { + pub(crate) fn new(module: ShaderModule, target: ShaderTarget) -> Result { + #[cfg(not(debug_assertions))] + { + Ok(Self::from_bytes(module, target)) + } + #[cfg(debug_assertions)] + { + let blob = build_shader_blob(module, target)?; + let inner = unsafe { + std::slice::from_raw_parts( + blob.GetBufferPointer() as *const u8, + blob.GetBufferSize(), + ) + }; + Ok(Self { inner, _blob: blob }) + } + } + + pub(crate) fn as_bytes(&'t self) -> &'t [u8] { + self.inner + } + + #[cfg(not(debug_assertions))] + fn from_bytes(module: ShaderModule, target: ShaderTarget) -> Self { + let bytes = match module { + ShaderModule::Quad => match target { + ShaderTarget::Vertex => QUAD_VERTEX_BYTES, + ShaderTarget::Fragment => QUAD_FRAGMENT_BYTES, + }, + ShaderModule::Shadow => match target { + ShaderTarget::Vertex => SHADOW_VERTEX_BYTES, + ShaderTarget::Fragment => SHADOW_FRAGMENT_BYTES, + }, + ShaderModule::Underline => match target { + ShaderTarget::Vertex => UNDERLINE_VERTEX_BYTES, + ShaderTarget::Fragment => UNDERLINE_FRAGMENT_BYTES, + }, + ShaderModule::PathRasterization => match target { + ShaderTarget::Vertex => PATH_RASTERIZATION_VERTEX_BYTES, + ShaderTarget::Fragment => PATH_RASTERIZATION_FRAGMENT_BYTES, + }, + ShaderModule::PathSprite => match target { + ShaderTarget::Vertex => PATH_SPRITE_VERTEX_BYTES, + ShaderTarget::Fragment => PATH_SPRITE_FRAGMENT_BYTES, + }, + ShaderModule::MonochromeSprite => match target { + ShaderTarget::Vertex => MONOCHROME_SPRITE_VERTEX_BYTES, + ShaderTarget::Fragment => MONOCHROME_SPRITE_FRAGMENT_BYTES, + }, + ShaderModule::PolychromeSprite => match target { + ShaderTarget::Vertex => POLYCHROME_SPRITE_VERTEX_BYTES, + ShaderTarget::Fragment => POLYCHROME_SPRITE_FRAGMENT_BYTES, + }, + ShaderModule::EmojiRasterization => match target { + ShaderTarget::Vertex => EMOJI_RASTERIZATION_VERTEX_BYTES, + ShaderTarget::Fragment => EMOJI_RASTERIZATION_FRAGMENT_BYTES, + }, + }; + Self { inner: bytes } + } + } + + #[cfg(debug_assertions)] + pub(super) fn build_shader_blob(entry: ShaderModule, target: ShaderTarget) -> Result { + unsafe { + use windows::Win32::Graphics::{ + Direct3D::ID3DInclude, Hlsl::D3D_COMPILE_STANDARD_FILE_INCLUDE, + }; + + let shader_name = if matches!(entry, ShaderModule::EmojiRasterization) { + "color_text_raster.hlsl" + } else { + "shaders.hlsl" + }; + + let entry = format!( + "{}_{}\0", + entry.as_str(), + match target { + ShaderTarget::Vertex => "vertex", + ShaderTarget::Fragment => "fragment", + } + ); + let target = match target { + ShaderTarget::Vertex => "vs_4_1\0", + ShaderTarget::Fragment => "ps_4_1\0", + }; + + let mut compile_blob = None; + let mut error_blob = None; + let shader_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join(&format!("src/platform/windows/{}", shader_name)) + .canonicalize()?; + + let entry_point = PCSTR::from_raw(entry.as_ptr()); + let target_cstr = PCSTR::from_raw(target.as_ptr()); + + // really dirty trick because winapi bindings are unhappy otherwise + let include_handler = &std::mem::transmute::( + D3D_COMPILE_STANDARD_FILE_INCLUDE as usize, + ); + + let ret = D3DCompileFromFile( + &HSTRING::from(shader_path.to_str().unwrap()), + None, + include_handler, + entry_point, + target_cstr, + D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION, + 0, + &mut compile_blob, + Some(&mut error_blob), + ); + if ret.is_err() { + let Some(error_blob) = error_blob else { + return Err(anyhow::anyhow!("{ret:?}")); + }; + + let error_string = + std::ffi::CStr::from_ptr(error_blob.GetBufferPointer() as *const i8) + .to_string_lossy(); + log::error!("Shader compile error: {}", error_string); + return Err(anyhow::anyhow!("Compile error: {}", error_string)); + } + Ok(compile_blob.unwrap()) + } + } + + #[cfg(not(debug_assertions))] + include!(concat!(env!("OUT_DIR"), "/shaders_bytes.rs")); + + #[cfg(debug_assertions)] + impl ShaderModule { + pub fn as_str(&self) -> &str { + match self { + ShaderModule::Quad => "quad", + ShaderModule::Shadow => "shadow", + ShaderModule::Underline => "underline", + ShaderModule::PathRasterization => "path_rasterization", + ShaderModule::PathSprite => "path_sprite", + ShaderModule::MonochromeSprite => "monochrome_sprite", + ShaderModule::PolychromeSprite => "polychrome_sprite", + ShaderModule::EmojiRasterization => "emoji_rasterization", + } + } + } +} + +mod nvidia { + use std::{ + ffi::CStr, + os::raw::{c_char, c_int, c_uint}, + }; + + use anyhow::Result; + use windows::{Win32::System::LibraryLoader::GetProcAddress, core::s}; + + use crate::with_dll_library; + + // https://github.com/NVIDIA/nvapi/blob/7cb76fce2f52de818b3da497af646af1ec16ce27/nvapi_lite_common.h#L180 + const NVAPI_SHORT_STRING_MAX: usize = 64; + + // https://github.com/NVIDIA/nvapi/blob/7cb76fce2f52de818b3da497af646af1ec16ce27/nvapi_lite_common.h#L235 + #[allow(non_camel_case_types)] + type NvAPI_ShortString = [c_char; NVAPI_SHORT_STRING_MAX]; + + // https://github.com/NVIDIA/nvapi/blob/7cb76fce2f52de818b3da497af646af1ec16ce27/nvapi_lite_common.h#L447 + #[allow(non_camel_case_types)] + type NvAPI_SYS_GetDriverAndBranchVersion_t = unsafe extern "C" fn( + driver_version: *mut c_uint, + build_branch_string: *mut NvAPI_ShortString, + ) -> c_int; + + pub(super) fn get_driver_version() -> Result { + #[cfg(target_pointer_width = "64")] + let nvidia_dll_name = s!("nvapi64.dll"); + #[cfg(target_pointer_width = "32")] + let nvidia_dll_name = s!("nvapi.dll"); + + with_dll_library(nvidia_dll_name, |nvidia_dll| unsafe { + let nvapi_query_addr = GetProcAddress(nvidia_dll, s!("nvapi_QueryInterface")) + .ok_or_else(|| anyhow::anyhow!("Failed to get nvapi_QueryInterface address"))?; + let nvapi_query: extern "C" fn(u32) -> *mut () = std::mem::transmute(nvapi_query_addr); + + // https://github.com/NVIDIA/nvapi/blob/7cb76fce2f52de818b3da497af646af1ec16ce27/nvapi_interface.h#L41 + let nvapi_get_driver_version_ptr = nvapi_query(0x2926aaad); + if nvapi_get_driver_version_ptr.is_null() { + anyhow::bail!("Failed to get NVIDIA driver version function pointer"); + } + let nvapi_get_driver_version: NvAPI_SYS_GetDriverAndBranchVersion_t = + std::mem::transmute(nvapi_get_driver_version_ptr); + + let mut driver_version: c_uint = 0; + let mut build_branch_string: NvAPI_ShortString = [0; NVAPI_SHORT_STRING_MAX]; + let result = nvapi_get_driver_version( + &mut driver_version as *mut c_uint, + &mut build_branch_string as *mut NvAPI_ShortString, + ); + + if result != 0 { + anyhow::bail!( + "Failed to get NVIDIA driver version, error code: {}", + result + ); + } + let major = driver_version / 100; + let minor = driver_version % 100; + let branch_string = CStr::from_ptr(build_branch_string.as_ptr()); + Ok(format!( + "{}.{} {}", + major, + minor, + branch_string.to_string_lossy() + )) + }) + } +} + +mod amd { + use std::os::raw::{c_char, c_int, c_void}; + + use anyhow::Result; + use windows::{Win32::System::LibraryLoader::GetProcAddress, core::s}; + + use crate::with_dll_library; + + // https://github.com/GPUOpen-LibrariesAndSDKs/AGS_SDK/blob/5d8812d703d0335741b6f7ffc37838eeb8b967f7/ags_lib/inc/amd_ags.h#L145 + const AGS_CURRENT_VERSION: i32 = (6 << 22) | (3 << 12); + + // https://github.com/GPUOpen-LibrariesAndSDKs/AGS_SDK/blob/5d8812d703d0335741b6f7ffc37838eeb8b967f7/ags_lib/inc/amd_ags.h#L204 + // This is an opaque type, using struct to represent it properly for FFI + #[repr(C)] + struct AGSContext { + _private: [u8; 0], + } + + #[repr(C)] + pub struct AGSGPUInfo { + pub driver_version: *const c_char, + pub radeon_software_version: *const c_char, + pub num_devices: c_int, + pub devices: *mut c_void, + } + + // https://github.com/GPUOpen-LibrariesAndSDKs/AGS_SDK/blob/5d8812d703d0335741b6f7ffc37838eeb8b967f7/ags_lib/inc/amd_ags.h#L429 + #[allow(non_camel_case_types)] + type agsInitialize_t = unsafe extern "C" fn( + version: c_int, + config: *const c_void, + context: *mut *mut AGSContext, + gpu_info: *mut AGSGPUInfo, + ) -> c_int; + + // https://github.com/GPUOpen-LibrariesAndSDKs/AGS_SDK/blob/5d8812d703d0335741b6f7ffc37838eeb8b967f7/ags_lib/inc/amd_ags.h#L436 + #[allow(non_camel_case_types)] + type agsDeInitialize_t = unsafe extern "C" fn(context: *mut AGSContext) -> c_int; + + pub(super) fn get_driver_version() -> Result { + #[cfg(target_pointer_width = "64")] + let amd_dll_name = s!("amd_ags_x64.dll"); + #[cfg(target_pointer_width = "32")] + let amd_dll_name = s!("amd_ags_x86.dll"); + + with_dll_library(amd_dll_name, |amd_dll| unsafe { + let ags_initialize_addr = GetProcAddress(amd_dll, s!("agsInitialize")) + .ok_or_else(|| anyhow::anyhow!("Failed to get agsInitialize address"))?; + let ags_deinitialize_addr = GetProcAddress(amd_dll, s!("agsDeInitialize")) + .ok_or_else(|| anyhow::anyhow!("Failed to get agsDeInitialize address"))?; + + let ags_initialize: agsInitialize_t = std::mem::transmute(ags_initialize_addr); + let ags_deinitialize: agsDeInitialize_t = std::mem::transmute(ags_deinitialize_addr); + + let mut context: *mut AGSContext = std::ptr::null_mut(); + let mut gpu_info: AGSGPUInfo = AGSGPUInfo { + driver_version: std::ptr::null(), + radeon_software_version: std::ptr::null(), + num_devices: 0, + devices: std::ptr::null_mut(), + }; + + let result = ags_initialize( + AGS_CURRENT_VERSION, + std::ptr::null(), + &mut context, + &mut gpu_info, + ); + if result != 0 { + anyhow::bail!("Failed to initialize AMD AGS, error code: {}", result); + } + + // Vulkan actually returns this as the driver version + let software_version = if !gpu_info.radeon_software_version.is_null() { + std::ffi::CStr::from_ptr(gpu_info.radeon_software_version) + .to_string_lossy() + .into_owned() + } else { + "Unknown Radeon Software Version".to_string() + }; + + let driver_version = if !gpu_info.driver_version.is_null() { + std::ffi::CStr::from_ptr(gpu_info.driver_version) + .to_string_lossy() + .into_owned() + } else { + "Unknown Radeon Driver Version".to_string() + }; + + ags_deinitialize(context); + Ok(format!("{} ({})", software_version, driver_version)) + }) + } +} + +mod dxgi { + use windows::{ + Win32::Graphics::Dxgi::{IDXGIAdapter1, IDXGIDevice}, + core::Interface, + }; + + pub(super) fn get_driver_version(adapter: &IDXGIAdapter1) -> anyhow::Result { + let number = unsafe { adapter.CheckInterfaceSupport(&IDXGIDevice::IID as _) }?; + Ok(format!( + "{}.{}.{}.{}", + number >> 48, + (number >> 32) & 0xFFFF, + (number >> 16) & 0xFFFF, + number & 0xFFFF + )) + } +} diff --git a/third_party/gpui/src/platform/windows/dispatcher.rs b/third_party/gpui/src/platform/windows/dispatcher.rs new file mode 100644 index 0000000..8d3e630 --- /dev/null +++ b/third_party/gpui/src/platform/windows/dispatcher.rs @@ -0,0 +1,110 @@ +use std::{ + thread::{ThreadId, current}, + time::Duration, +}; + +use async_task::Runnable; +use flume::Sender; +use util::ResultExt; +use windows::{ + System::Threading::{ + ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemPriority, + }, + Win32::{ + Foundation::{LPARAM, WPARAM}, + UI::WindowsAndMessaging::PostMessageW, + }, +}; + +use crate::{ + HWND, PlatformDispatcher, SafeHwnd, TaskLabel, WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD, +}; + +pub(crate) struct WindowsDispatcher { + main_sender: Sender, + main_thread_id: ThreadId, + platform_window_handle: SafeHwnd, + validation_number: usize, +} + +impl WindowsDispatcher { + pub(crate) fn new( + main_sender: Sender, + platform_window_handle: HWND, + validation_number: usize, + ) -> Self { + let main_thread_id = current().id(); + let platform_window_handle = platform_window_handle.into(); + + WindowsDispatcher { + main_sender, + main_thread_id, + platform_window_handle, + validation_number, + } + } + + fn dispatch_on_threadpool(&self, runnable: Runnable) { + let handler = { + let mut task_wrapper = Some(runnable); + WorkItemHandler::new(move |_| { + task_wrapper.take().unwrap().run(); + Ok(()) + }) + }; + ThreadPool::RunWithPriorityAsync(&handler, WorkItemPriority::High).log_err(); + } + + fn dispatch_on_threadpool_after(&self, runnable: Runnable, duration: Duration) { + let handler = { + let mut task_wrapper = Some(runnable); + TimerElapsedHandler::new(move |_| { + task_wrapper.take().unwrap().run(); + Ok(()) + }) + }; + ThreadPoolTimer::CreateTimer(&handler, duration.into()).log_err(); + } +} + +impl PlatformDispatcher for WindowsDispatcher { + fn is_main_thread(&self) -> bool { + current().id() == self.main_thread_id + } + + fn dispatch(&self, runnable: Runnable, label: Option) { + self.dispatch_on_threadpool(runnable); + if let Some(label) = label { + log::debug!("TaskLabel: {label:?}"); + } + } + + fn dispatch_on_main_thread(&self, runnable: Runnable) { + match self.main_sender.send(runnable) { + Ok(_) => unsafe { + PostMessageW( + Some(self.platform_window_handle.as_raw()), + WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD, + WPARAM(self.validation_number), + LPARAM(0), + ) + .log_err(); + }, + Err(runnable) => { + // NOTE: Runnable may wrap a Future that is !Send. + // + // This is usually safe because we only poll it on the main thread. + // However if the send fails, we know that: + // 1. main_receiver has been dropped (which implies the app is shutting down) + // 2. we are on a background thread. + // It is not safe to drop something !Send on the wrong thread, and + // the app will exit soon anyway, so we must forget the runnable. + std::mem::forget(runnable); + } + } + } + + fn dispatch_after(&self, duration: Duration, runnable: Runnable) { + self.dispatch_on_threadpool_after(runnable, duration); + } +} diff --git a/third_party/gpui/src/platform/windows/display.rs b/third_party/gpui/src/platform/windows/display.rs new file mode 100644 index 0000000..79716c9 --- /dev/null +++ b/third_party/gpui/src/platform/windows/display.rs @@ -0,0 +1,255 @@ +use itertools::Itertools; +use smallvec::SmallVec; +use std::rc::Rc; +use util::ResultExt; +use uuid::Uuid; +use windows::{ + Win32::{ + Foundation::*, + Graphics::Gdi::*, + UI::{ + HiDpi::{GetDpiForMonitor, MDT_EFFECTIVE_DPI}, + WindowsAndMessaging::USER_DEFAULT_SCREEN_DPI, + }, + }, + core::*, +}; + +use crate::{Bounds, DevicePixels, DisplayId, Pixels, PlatformDisplay, logical_point, point, size}; + +#[derive(Debug, Clone, Copy)] +pub(crate) struct WindowsDisplay { + pub handle: HMONITOR, + pub display_id: DisplayId, + scale_factor: f32, + bounds: Bounds, + physical_bounds: Bounds, + uuid: Uuid, +} + +// The `HMONITOR` is thread-safe. +unsafe impl Send for WindowsDisplay {} +unsafe impl Sync for WindowsDisplay {} + +impl WindowsDisplay { + pub(crate) fn new(display_id: DisplayId) -> Option { + let screen = available_monitors().into_iter().nth(display_id.0 as _)?; + let info = get_monitor_info(screen).log_err()?; + let monitor_size = info.monitorInfo.rcMonitor; + let uuid = generate_uuid(&info.szDevice); + let scale_factor = get_scale_factor_for_monitor(screen).log_err()?; + let physical_size = size( + (monitor_size.right - monitor_size.left).into(), + (monitor_size.bottom - monitor_size.top).into(), + ); + + Some(WindowsDisplay { + handle: screen, + display_id, + scale_factor, + bounds: Bounds { + origin: logical_point( + monitor_size.left as f32, + monitor_size.top as f32, + scale_factor, + ), + size: physical_size.to_pixels(scale_factor), + }, + physical_bounds: Bounds { + origin: point(monitor_size.left.into(), monitor_size.top.into()), + size: physical_size, + }, + uuid, + }) + } + + pub fn new_with_handle(monitor: HMONITOR) -> Self { + let info = get_monitor_info(monitor).expect("unable to get monitor info"); + let monitor_size = info.monitorInfo.rcMonitor; + let uuid = generate_uuid(&info.szDevice); + let display_id = available_monitors() + .iter() + .position(|handle| handle.0 == monitor.0) + .unwrap(); + let scale_factor = + get_scale_factor_for_monitor(monitor).expect("unable to get scale factor for monitor"); + let physical_size = size( + (monitor_size.right - monitor_size.left).into(), + (monitor_size.bottom - monitor_size.top).into(), + ); + + WindowsDisplay { + handle: monitor, + display_id: DisplayId(display_id as _), + scale_factor, + bounds: Bounds { + origin: logical_point( + monitor_size.left as f32, + monitor_size.top as f32, + scale_factor, + ), + size: physical_size.to_pixels(scale_factor), + }, + physical_bounds: Bounds { + origin: point(monitor_size.left.into(), monitor_size.top.into()), + size: physical_size, + }, + uuid, + } + } + + fn new_with_handle_and_id(handle: HMONITOR, display_id: DisplayId) -> Self { + let info = get_monitor_info(handle).expect("unable to get monitor info"); + let monitor_size = info.monitorInfo.rcMonitor; + let uuid = generate_uuid(&info.szDevice); + let scale_factor = + get_scale_factor_for_monitor(handle).expect("unable to get scale factor for monitor"); + let physical_size = size( + (monitor_size.right - monitor_size.left).into(), + (monitor_size.bottom - monitor_size.top).into(), + ); + + WindowsDisplay { + handle, + display_id, + scale_factor, + bounds: Bounds { + origin: logical_point( + monitor_size.left as f32, + monitor_size.top as f32, + scale_factor, + ), + size: physical_size.to_pixels(scale_factor), + }, + physical_bounds: Bounds { + origin: point(monitor_size.left.into(), monitor_size.top.into()), + size: physical_size, + }, + uuid, + } + } + + pub fn primary_monitor() -> Option { + // https://devblogs.microsoft.com/oldnewthing/20070809-00/?p=25643 + const POINT_ZERO: POINT = POINT { x: 0, y: 0 }; + let monitor = unsafe { MonitorFromPoint(POINT_ZERO, MONITOR_DEFAULTTOPRIMARY) }; + if monitor.is_invalid() { + log::error!( + "can not find the primary monitor: {}", + std::io::Error::last_os_error() + ); + return None; + } + Some(WindowsDisplay::new_with_handle(monitor)) + } + + /// Check if the center point of given bounds is inside this monitor + pub fn check_given_bounds(&self, bounds: Bounds) -> bool { + let center = bounds.center(); + let center = POINT { + x: (center.x.0 * self.scale_factor) as i32, + y: (center.y.0 * self.scale_factor) as i32, + }; + let monitor = unsafe { MonitorFromPoint(center, MONITOR_DEFAULTTONULL) }; + if monitor.is_invalid() { + false + } else { + let display = WindowsDisplay::new_with_handle(monitor); + display.uuid == self.uuid + } + } + + pub fn displays() -> Vec> { + available_monitors() + .into_iter() + .enumerate() + .map(|(id, handle)| { + Rc::new(WindowsDisplay::new_with_handle_and_id( + handle, + DisplayId(id as _), + )) as Rc + }) + .collect() + } + + /// Check if this monitor is still online + pub fn is_connected(hmonitor: HMONITOR) -> bool { + available_monitors().iter().contains(&hmonitor) + } + + pub fn physical_bounds(&self) -> Bounds { + self.physical_bounds + } +} + +impl PlatformDisplay for WindowsDisplay { + fn id(&self) -> DisplayId { + self.display_id + } + + fn uuid(&self) -> anyhow::Result { + Ok(self.uuid) + } + + fn bounds(&self) -> Bounds { + self.bounds + } +} + +fn available_monitors() -> SmallVec<[HMONITOR; 4]> { + let mut monitors: SmallVec<[HMONITOR; 4]> = SmallVec::new(); + unsafe { + EnumDisplayMonitors( + None, + None, + Some(monitor_enum_proc), + LPARAM(&mut monitors as *mut _ as _), + ) + .ok() + .log_err(); + } + monitors +} + +unsafe extern "system" fn monitor_enum_proc( + hmonitor: HMONITOR, + _hdc: HDC, + _place: *mut RECT, + data: LPARAM, +) -> BOOL { + let monitors = data.0 as *mut SmallVec<[HMONITOR; 4]>; + unsafe { (*monitors).push(hmonitor) }; + BOOL(1) +} + +fn get_monitor_info(hmonitor: HMONITOR) -> anyhow::Result { + let mut monitor_info: MONITORINFOEXW = unsafe { std::mem::zeroed() }; + monitor_info.monitorInfo.cbSize = std::mem::size_of::() as u32; + let status = unsafe { + GetMonitorInfoW( + hmonitor, + &mut monitor_info as *mut MONITORINFOEXW as *mut MONITORINFO, + ) + }; + if status.as_bool() { + Ok(monitor_info) + } else { + Err(anyhow::anyhow!(std::io::Error::last_os_error())) + } +} + +fn generate_uuid(device_name: &[u16]) -> Uuid { + let name = device_name + .iter() + .flat_map(|&a| a.to_be_bytes()) + .collect_vec(); + Uuid::new_v5(&Uuid::NAMESPACE_DNS, &name) +} + +fn get_scale_factor_for_monitor(monitor: HMONITOR) -> Result { + let mut dpi_x = 0; + let mut dpi_y = 0; + unsafe { GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) }?; + assert_eq!(dpi_x, dpi_y); + Ok(dpi_x as f32 / USER_DEFAULT_SCREEN_DPI as f32) +} diff --git a/third_party/gpui/src/platform/windows/events.rs b/third_party/gpui/src/platform/windows/events.rs new file mode 100644 index 0000000..9c10dce --- /dev/null +++ b/third_party/gpui/src/platform/windows/events.rs @@ -0,0 +1,1563 @@ +use std::rc::Rc; + +use ::util::ResultExt; +use anyhow::Context as _; +use windows::{ + Win32::{ + Foundation::*, + Graphics::Gdi::*, + System::SystemServices::*, + UI::{ + Controls::*, + HiDpi::*, + Input::{Ime::*, KeyboardAndMouse::*}, + WindowsAndMessaging::*, + }, + }, + core::PCWSTR, +}; + +use crate::*; + +pub(crate) const WM_GPUI_CURSOR_STYLE_CHANGED: u32 = WM_USER + 1; +pub(crate) const WM_GPUI_CLOSE_ONE_WINDOW: u32 = WM_USER + 2; +pub(crate) const WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD: u32 = WM_USER + 3; +pub(crate) const WM_GPUI_DOCK_MENU_ACTION: u32 = WM_USER + 4; +pub(crate) const WM_GPUI_FORCE_UPDATE_WINDOW: u32 = WM_USER + 5; +pub(crate) const WM_GPUI_KEYBOARD_LAYOUT_CHANGED: u32 = WM_USER + 6; +pub(crate) const WM_GPUI_GPU_DEVICE_LOST: u32 = WM_USER + 7; + +const SIZE_MOVE_LOOP_TIMER_ID: usize = 1; +const AUTO_HIDE_TASKBAR_THICKNESS_PX: i32 = 1; + +impl WindowsWindowInner { + pub(crate) fn handle_msg( + self: &Rc, + handle: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + ) -> LRESULT { + let handled = match msg { + WM_ACTIVATE => self.handle_activate_msg(wparam), + WM_CREATE => self.handle_create_msg(handle), + WM_MOVE => self.handle_move_msg(handle, lparam), + WM_SIZE => self.handle_size_msg(wparam, lparam), + WM_GETMINMAXINFO => self.handle_get_min_max_info_msg(lparam), + WM_ENTERSIZEMOVE | WM_ENTERMENULOOP => self.handle_size_move_loop(handle), + WM_EXITSIZEMOVE | WM_EXITMENULOOP => self.handle_size_move_loop_exit(handle), + WM_TIMER => self.handle_timer_msg(handle, wparam), + WM_NCCALCSIZE => self.handle_calc_client_size(handle, wparam, lparam), + WM_DPICHANGED => self.handle_dpi_changed_msg(handle, wparam, lparam), + WM_DISPLAYCHANGE => self.handle_display_change_msg(handle), + WM_NCHITTEST => self.handle_hit_test_msg(handle, msg, wparam, lparam), + WM_PAINT => self.handle_paint_msg(handle), + WM_CLOSE => self.handle_close_msg(), + WM_DESTROY => self.handle_destroy_msg(handle), + WM_MOUSEMOVE => self.handle_mouse_move_msg(handle, lparam, wparam), + WM_MOUSELEAVE | WM_NCMOUSELEAVE => self.handle_mouse_leave_msg(), + WM_NCMOUSEMOVE => self.handle_nc_mouse_move_msg(handle, lparam), + // Treat double click as a second single click, since we track the double clicks ourselves. + // If you don't interact with any elements, this will fall through to the windows default + // behavior of toggling whether the window is maximized. + WM_NCLBUTTONDBLCLK | WM_NCLBUTTONDOWN => { + self.handle_nc_mouse_down_msg(handle, MouseButton::Left, wparam, lparam) + } + WM_NCRBUTTONDOWN => { + self.handle_nc_mouse_down_msg(handle, MouseButton::Right, wparam, lparam) + } + WM_NCMBUTTONDOWN => { + self.handle_nc_mouse_down_msg(handle, MouseButton::Middle, wparam, lparam) + } + WM_NCLBUTTONUP => { + self.handle_nc_mouse_up_msg(handle, MouseButton::Left, wparam, lparam) + } + WM_NCRBUTTONUP => { + self.handle_nc_mouse_up_msg(handle, MouseButton::Right, wparam, lparam) + } + WM_NCMBUTTONUP => { + self.handle_nc_mouse_up_msg(handle, MouseButton::Middle, wparam, lparam) + } + WM_LBUTTONDOWN => self.handle_mouse_down_msg(handle, MouseButton::Left, lparam), + WM_RBUTTONDOWN => self.handle_mouse_down_msg(handle, MouseButton::Right, lparam), + WM_MBUTTONDOWN => self.handle_mouse_down_msg(handle, MouseButton::Middle, lparam), + WM_XBUTTONDOWN => { + self.handle_xbutton_msg(handle, wparam, lparam, Self::handle_mouse_down_msg) + } + WM_LBUTTONUP => self.handle_mouse_up_msg(handle, MouseButton::Left, lparam), + WM_RBUTTONUP => self.handle_mouse_up_msg(handle, MouseButton::Right, lparam), + WM_MBUTTONUP => self.handle_mouse_up_msg(handle, MouseButton::Middle, lparam), + WM_XBUTTONUP => { + self.handle_xbutton_msg(handle, wparam, lparam, Self::handle_mouse_up_msg) + } + WM_MOUSEWHEEL => self.handle_mouse_wheel_msg(handle, wparam, lparam), + WM_MOUSEHWHEEL => self.handle_mouse_horizontal_wheel_msg(handle, wparam, lparam), + WM_SYSKEYDOWN => self.handle_syskeydown_msg(handle, wparam, lparam), + WM_SYSKEYUP => self.handle_syskeyup_msg(handle, wparam, lparam), + WM_SYSCOMMAND => self.handle_system_command(wparam), + WM_KEYDOWN => self.handle_keydown_msg(handle, wparam, lparam), + WM_KEYUP => self.handle_keyup_msg(handle, wparam, lparam), + WM_CHAR => self.handle_char_msg(wparam), + WM_DEADCHAR => self.handle_dead_char_msg(wparam), + WM_IME_STARTCOMPOSITION => self.handle_ime_position(handle), + WM_IME_COMPOSITION => self.handle_ime_composition(handle, lparam), + WM_SETCURSOR => self.handle_set_cursor(handle, lparam), + WM_SETTINGCHANGE => self.handle_system_settings_changed(handle, wparam, lparam), + WM_INPUTLANGCHANGE => self.handle_input_language_changed(), + WM_SHOWWINDOW => self.handle_window_visibility_changed(handle, wparam), + WM_GPUI_CURSOR_STYLE_CHANGED => self.handle_cursor_changed(lparam), + WM_GPUI_FORCE_UPDATE_WINDOW => self.draw_window(handle, true), + WM_GPUI_GPU_DEVICE_LOST => self.handle_device_lost(lparam), + _ => None, + }; + if let Some(n) = handled { + LRESULT(n) + } else { + unsafe { DefWindowProcW(handle, msg, wparam, lparam) } + } + } + + fn handle_move_msg(&self, handle: HWND, lparam: LPARAM) -> Option { + let mut lock = self.state.borrow_mut(); + let origin = logical_point( + lparam.signed_loword() as f32, + lparam.signed_hiword() as f32, + lock.scale_factor, + ); + lock.origin = origin; + let size = lock.logical_size; + let center_x = origin.x.0 + size.width.0 / 2.; + let center_y = origin.y.0 + size.height.0 / 2.; + let monitor_bounds = lock.display.bounds(); + if center_x < monitor_bounds.left().0 + || center_x > monitor_bounds.right().0 + || center_y < monitor_bounds.top().0 + || center_y > monitor_bounds.bottom().0 + { + // center of the window may have moved to another monitor + let monitor = unsafe { MonitorFromWindow(handle, MONITOR_DEFAULTTONULL) }; + // minimize the window can trigger this event too, in this case, + // monitor is invalid, we do nothing. + if !monitor.is_invalid() && lock.display.handle != monitor { + // we will get the same monitor if we only have one + lock.display = WindowsDisplay::new_with_handle(monitor); + } + } + if let Some(mut callback) = lock.callbacks.moved.take() { + drop(lock); + callback(); + self.state.borrow_mut().callbacks.moved = Some(callback); + } + Some(0) + } + + fn handle_get_min_max_info_msg(&self, lparam: LPARAM) -> Option { + let lock = self.state.borrow(); + let min_size = lock.min_size?; + let scale_factor = lock.scale_factor; + let boarder_offset = lock.border_offset; + drop(lock); + unsafe { + let minmax_info = &mut *(lparam.0 as *mut MINMAXINFO); + minmax_info.ptMinTrackSize.x = + min_size.width.scale(scale_factor).0 as i32 + boarder_offset.width_offset; + minmax_info.ptMinTrackSize.y = + min_size.height.scale(scale_factor).0 as i32 + boarder_offset.height_offset; + } + Some(0) + } + + fn handle_size_msg(&self, wparam: WPARAM, lparam: LPARAM) -> Option { + let mut lock = self.state.borrow_mut(); + + // Don't resize the renderer when the window is minimized, but record that it was minimized so + // that on restore the swap chain can be recreated via `update_drawable_size_even_if_unchanged`. + if wparam.0 == SIZE_MINIMIZED as usize { + lock.restore_from_minimized = lock.callbacks.request_frame.take(); + return Some(0); + } + + let width = lparam.loword().max(1) as i32; + let height = lparam.hiword().max(1) as i32; + let new_size = size(DevicePixels(width), DevicePixels(height)); + + let scale_factor = lock.scale_factor; + let mut should_resize_renderer = false; + if lock.restore_from_minimized.is_some() { + lock.callbacks.request_frame = lock.restore_from_minimized.take(); + } else { + should_resize_renderer = true; + } + drop(lock); + + self.handle_size_change(new_size, scale_factor, should_resize_renderer); + Some(0) + } + + fn handle_size_change( + &self, + device_size: Size, + scale_factor: f32, + should_resize_renderer: bool, + ) { + let new_logical_size = device_size.to_pixels(scale_factor); + let mut lock = self.state.borrow_mut(); + lock.logical_size = new_logical_size; + if should_resize_renderer { + lock.renderer.resize(device_size).log_err(); + } + if let Some(mut callback) = lock.callbacks.resize.take() { + drop(lock); + callback(new_logical_size, scale_factor); + self.state.borrow_mut().callbacks.resize = Some(callback); + } + } + + fn handle_size_move_loop(&self, handle: HWND) -> Option { + unsafe { + let ret = SetTimer( + Some(handle), + SIZE_MOVE_LOOP_TIMER_ID, + USER_TIMER_MINIMUM, + None, + ); + if ret == 0 { + log::error!( + "unable to create timer: {}", + std::io::Error::last_os_error() + ); + } + } + None + } + + fn handle_size_move_loop_exit(&self, handle: HWND) -> Option { + unsafe { + KillTimer(Some(handle), SIZE_MOVE_LOOP_TIMER_ID).log_err(); + } + None + } + + fn handle_timer_msg(&self, handle: HWND, wparam: WPARAM) -> Option { + if wparam.0 == SIZE_MOVE_LOOP_TIMER_ID { + for runnable in self.main_receiver.drain() { + runnable.run(); + } + self.handle_paint_msg(handle) + } else { + None + } + } + + fn handle_paint_msg(&self, handle: HWND) -> Option { + self.draw_window(handle, false) + } + + fn handle_close_msg(&self) -> Option { + let mut callback = self.state.borrow_mut().callbacks.should_close.take()?; + let should_close = callback(); + self.state.borrow_mut().callbacks.should_close = Some(callback); + if should_close { None } else { Some(0) } + } + + fn handle_destroy_msg(&self, handle: HWND) -> Option { + let callback = { + let mut lock = self.state.borrow_mut(); + lock.callbacks.close.take() + }; + if let Some(callback) = callback { + callback(); + } + unsafe { + PostMessageW( + Some(self.platform_window_handle), + WM_GPUI_CLOSE_ONE_WINDOW, + WPARAM(self.validation_number), + LPARAM(handle.0 as isize), + ) + .log_err(); + } + Some(0) + } + + fn handle_mouse_move_msg(&self, handle: HWND, lparam: LPARAM, wparam: WPARAM) -> Option { + self.start_tracking_mouse(handle, TME_LEAVE); + + let mut lock = self.state.borrow_mut(); + let Some(mut func) = lock.callbacks.input.take() else { + return Some(1); + }; + let scale_factor = lock.scale_factor; + drop(lock); + + let pressed_button = match MODIFIERKEYS_FLAGS(wparam.loword() as u32) { + flags if flags.contains(MK_LBUTTON) => Some(MouseButton::Left), + flags if flags.contains(MK_RBUTTON) => Some(MouseButton::Right), + flags if flags.contains(MK_MBUTTON) => Some(MouseButton::Middle), + flags if flags.contains(MK_XBUTTON1) => { + Some(MouseButton::Navigate(NavigationDirection::Back)) + } + flags if flags.contains(MK_XBUTTON2) => { + Some(MouseButton::Navigate(NavigationDirection::Forward)) + } + _ => None, + }; + let x = lparam.signed_loword() as f32; + let y = lparam.signed_hiword() as f32; + let input = PlatformInput::MouseMove(MouseMoveEvent { + position: logical_point(x, y, scale_factor), + pressed_button, + modifiers: current_modifiers(), + }); + let handled = !func(input).propagate; + self.state.borrow_mut().callbacks.input = Some(func); + + if handled { Some(0) } else { Some(1) } + } + + fn handle_mouse_leave_msg(&self) -> Option { + let mut lock = self.state.borrow_mut(); + lock.hovered = false; + if let Some(mut callback) = lock.callbacks.hovered_status_change.take() { + drop(lock); + callback(false); + self.state.borrow_mut().callbacks.hovered_status_change = Some(callback); + } + + Some(0) + } + + fn handle_syskeydown_msg(&self, handle: HWND, wparam: WPARAM, lparam: LPARAM) -> Option { + let mut lock = self.state.borrow_mut(); + let input = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { + PlatformInput::KeyDown(KeyDownEvent { + keystroke, + is_held: lparam.0 & (0x1 << 30) > 0, + }) + })?; + let mut func = lock.callbacks.input.take()?; + drop(lock); + + let handled = !func(input).propagate; + + let mut lock = self.state.borrow_mut(); + lock.callbacks.input = Some(func); + + if handled { + lock.system_key_handled = true; + Some(0) + } else { + // we need to call `DefWindowProcW`, or we will lose the system-wide `Alt+F4`, `Alt+{other keys}` + // shortcuts. + None + } + } + + fn handle_syskeyup_msg(&self, handle: HWND, wparam: WPARAM, lparam: LPARAM) -> Option { + let mut lock = self.state.borrow_mut(); + let input = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { + PlatformInput::KeyUp(KeyUpEvent { keystroke }) + })?; + let mut func = lock.callbacks.input.take()?; + drop(lock); + func(input); + self.state.borrow_mut().callbacks.input = Some(func); + + // Always return 0 to indicate that the message was handled, so we could properly handle `ModifiersChanged` event. + Some(0) + } + + // It's a known bug that you can't trigger `ctrl-shift-0`. See: + // https://superuser.com/questions/1455762/ctrl-shift-number-key-combination-has-stopped-working-for-a-few-numbers + fn handle_keydown_msg(&self, handle: HWND, wparam: WPARAM, lparam: LPARAM) -> Option { + let mut lock = self.state.borrow_mut(); + let Some(input) = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { + PlatformInput::KeyDown(KeyDownEvent { + keystroke, + is_held: lparam.0 & (0x1 << 30) > 0, + }) + }) else { + return Some(1); + }; + drop(lock); + + let is_composing = self + .with_input_handler(|input_handler| input_handler.marked_text_range()) + .flatten() + .is_some(); + if is_composing { + translate_message(handle, wparam, lparam); + return Some(0); + } + + let Some(mut func) = self.state.borrow_mut().callbacks.input.take() else { + return Some(1); + }; + + let handled = !func(input).propagate; + + self.state.borrow_mut().callbacks.input = Some(func); + + if handled { + Some(0) + } else { + translate_message(handle, wparam, lparam); + Some(1) + } + } + + fn handle_keyup_msg(&self, handle: HWND, wparam: WPARAM, lparam: LPARAM) -> Option { + let mut lock = self.state.borrow_mut(); + let Some(input) = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { + PlatformInput::KeyUp(KeyUpEvent { keystroke }) + }) else { + return Some(1); + }; + + let Some(mut func) = lock.callbacks.input.take() else { + return Some(1); + }; + drop(lock); + + let handled = !func(input).propagate; + self.state.borrow_mut().callbacks.input = Some(func); + + if handled { Some(0) } else { Some(1) } + } + + fn handle_char_msg(&self, wparam: WPARAM) -> Option { + let input = self.parse_char_message(wparam)?; + self.with_input_handler(|input_handler| { + input_handler.replace_text_in_range(None, &input); + }); + + Some(0) + } + + fn handle_dead_char_msg(&self, wparam: WPARAM) -> Option { + let ch = char::from_u32(wparam.0 as u32)?.to_string(); + self.with_input_handler(|input_handler| { + input_handler.replace_and_mark_text_in_range(None, &ch, None); + }); + None + } + + fn handle_mouse_down_msg( + &self, + handle: HWND, + button: MouseButton, + lparam: LPARAM, + ) -> Option { + unsafe { SetCapture(handle) }; + let mut lock = self.state.borrow_mut(); + let Some(mut func) = lock.callbacks.input.take() else { + return Some(1); + }; + let x = lparam.signed_loword(); + let y = lparam.signed_hiword(); + let physical_point = point(DevicePixels(x as i32), DevicePixels(y as i32)); + let click_count = lock.click_state.update(button, physical_point); + let scale_factor = lock.scale_factor; + drop(lock); + + let input = PlatformInput::MouseDown(MouseDownEvent { + button, + position: logical_point(x as f32, y as f32, scale_factor), + modifiers: current_modifiers(), + click_count, + first_mouse: false, + }); + let handled = !func(input).propagate; + self.state.borrow_mut().callbacks.input = Some(func); + + if handled { Some(0) } else { Some(1) } + } + + fn handle_mouse_up_msg( + &self, + _handle: HWND, + button: MouseButton, + lparam: LPARAM, + ) -> Option { + unsafe { ReleaseCapture().log_err() }; + let mut lock = self.state.borrow_mut(); + let Some(mut func) = lock.callbacks.input.take() else { + return Some(1); + }; + let x = lparam.signed_loword() as f32; + let y = lparam.signed_hiword() as f32; + let click_count = lock.click_state.current_count; + let scale_factor = lock.scale_factor; + drop(lock); + + let input = PlatformInput::MouseUp(MouseUpEvent { + button, + position: logical_point(x, y, scale_factor), + modifiers: current_modifiers(), + click_count, + }); + let handled = !func(input).propagate; + self.state.borrow_mut().callbacks.input = Some(func); + + if handled { Some(0) } else { Some(1) } + } + + fn handle_xbutton_msg( + &self, + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + handler: impl Fn(&Self, HWND, MouseButton, LPARAM) -> Option, + ) -> Option { + let nav_dir = match wparam.hiword() { + XBUTTON1 => NavigationDirection::Back, + XBUTTON2 => NavigationDirection::Forward, + _ => return Some(1), + }; + handler(self, handle, MouseButton::Navigate(nav_dir), lparam) + } + + fn handle_mouse_wheel_msg( + &self, + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + ) -> Option { + let modifiers = current_modifiers(); + let mut lock = self.state.borrow_mut(); + let Some(mut func) = lock.callbacks.input.take() else { + return Some(1); + }; + let scale_factor = lock.scale_factor; + let wheel_scroll_amount = match modifiers.shift { + true => { + self.system_settings + .borrow() + .mouse_wheel_settings + .wheel_scroll_chars + } + false => { + self.system_settings + .borrow() + .mouse_wheel_settings + .wheel_scroll_lines + } + }; + drop(lock); + + let wheel_distance = + (wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_amount as f32; + let mut cursor_point = POINT { + x: lparam.signed_loword().into(), + y: lparam.signed_hiword().into(), + }; + unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; + let input = PlatformInput::ScrollWheel(ScrollWheelEvent { + position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), + delta: ScrollDelta::Lines(match modifiers.shift { + true => Point { + x: wheel_distance, + y: 0.0, + }, + false => Point { + y: wheel_distance, + x: 0.0, + }, + }), + modifiers, + touch_phase: TouchPhase::Moved, + }); + let handled = !func(input).propagate; + self.state.borrow_mut().callbacks.input = Some(func); + + if handled { Some(0) } else { Some(1) } + } + + fn handle_mouse_horizontal_wheel_msg( + &self, + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + ) -> Option { + let mut lock = self.state.borrow_mut(); + let Some(mut func) = lock.callbacks.input.take() else { + return Some(1); + }; + let scale_factor = lock.scale_factor; + let wheel_scroll_chars = self + .system_settings + .borrow() + .mouse_wheel_settings + .wheel_scroll_chars; + drop(lock); + + let wheel_distance = + (-wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_chars as f32; + let mut cursor_point = POINT { + x: lparam.signed_loword().into(), + y: lparam.signed_hiword().into(), + }; + unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; + let event = PlatformInput::ScrollWheel(ScrollWheelEvent { + position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), + delta: ScrollDelta::Lines(Point { + x: wheel_distance, + y: 0.0, + }), + modifiers: current_modifiers(), + touch_phase: TouchPhase::Moved, + }); + let handled = !func(event).propagate; + self.state.borrow_mut().callbacks.input = Some(func); + + if handled { Some(0) } else { Some(1) } + } + + fn retrieve_caret_position(&self) -> Option { + self.with_input_handler_and_scale_factor(|input_handler, scale_factor| { + let caret_range = input_handler.selected_text_range(false)?; + let caret_position = input_handler.bounds_for_range(caret_range.range)?; + Some(POINT { + // logical to physical + x: (caret_position.origin.x.0 * scale_factor) as i32, + y: (caret_position.origin.y.0 * scale_factor) as i32 + + ((caret_position.size.height.0 * scale_factor) as i32 / 2), + }) + }) + } + + fn handle_ime_position(&self, handle: HWND) -> Option { + unsafe { + let ctx = ImmGetContext(handle); + + let Some(caret_position) = self.retrieve_caret_position() else { + return Some(0); + }; + { + let config = COMPOSITIONFORM { + dwStyle: CFS_POINT, + ptCurrentPos: caret_position, + ..Default::default() + }; + ImmSetCompositionWindow(ctx, &config as _).ok().log_err(); + } + { + let config = CANDIDATEFORM { + dwStyle: CFS_CANDIDATEPOS, + ptCurrentPos: caret_position, + ..Default::default() + }; + ImmSetCandidateWindow(ctx, &config as _).ok().log_err(); + } + ImmReleaseContext(handle, ctx).ok().log_err(); + Some(0) + } + } + + fn handle_ime_composition(&self, handle: HWND, lparam: LPARAM) -> Option { + let ctx = unsafe { ImmGetContext(handle) }; + let result = self.handle_ime_composition_inner(ctx, lparam); + unsafe { ImmReleaseContext(handle, ctx).ok().log_err() }; + result + } + + fn handle_ime_composition_inner(&self, ctx: HIMC, lparam: LPARAM) -> Option { + let lparam = lparam.0 as u32; + if lparam == 0 { + // Japanese IME may send this message with lparam = 0, which indicates that + // there is no composition string. + self.with_input_handler(|input_handler| { + input_handler.replace_text_in_range(None, ""); + })?; + Some(0) + } else { + if lparam & GCS_COMPSTR.0 > 0 { + let comp_string = parse_ime_composition_string(ctx, GCS_COMPSTR)?; + let caret_pos = + (!comp_string.is_empty() && lparam & GCS_CURSORPOS.0 > 0).then(|| { + let pos = retrieve_composition_cursor_position(ctx); + pos..pos + }); + self.with_input_handler(|input_handler| { + input_handler.replace_and_mark_text_in_range(None, &comp_string, caret_pos); + })?; + } + if lparam & GCS_RESULTSTR.0 > 0 { + let comp_result = parse_ime_composition_string(ctx, GCS_RESULTSTR)?; + self.with_input_handler(|input_handler| { + input_handler.replace_text_in_range(None, &comp_result); + })?; + return Some(0); + } + + // currently, we don't care other stuff + None + } + } + + /// SEE: https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-nccalcsize + fn handle_calc_client_size( + &self, + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + ) -> Option { + if !self.hide_title_bar || self.state.borrow().is_fullscreen() || wparam.0 == 0 { + return None; + } + + let is_maximized = self.state.borrow().is_maximized(); + let insets = get_client_area_insets(handle, is_maximized, self.windows_version); + // wparam is TRUE so lparam points to an NCCALCSIZE_PARAMS structure + let mut params = lparam.0 as *mut NCCALCSIZE_PARAMS; + let mut requested_client_rect = unsafe { &mut ((*params).rgrc) }; + + requested_client_rect[0].left += insets.left; + requested_client_rect[0].top += insets.top; + requested_client_rect[0].right -= insets.right; + requested_client_rect[0].bottom -= insets.bottom; + + // Fix auto hide taskbar not showing. This solution is based on the approach + // used by Chrome. However, it may result in one row of pixels being obscured + // in our client area. But as Chrome says, "there seems to be no better solution." + if is_maximized + && let Some(ref taskbar_position) = + self.system_settings.borrow().auto_hide_taskbar_position + { + // For the auto-hide taskbar, adjust in by 1 pixel on taskbar edge, + // so the window isn't treated as a "fullscreen app", which would cause + // the taskbar to disappear. + match taskbar_position { + AutoHideTaskbarPosition::Left => { + requested_client_rect[0].left += AUTO_HIDE_TASKBAR_THICKNESS_PX + } + AutoHideTaskbarPosition::Top => { + requested_client_rect[0].top += AUTO_HIDE_TASKBAR_THICKNESS_PX + } + AutoHideTaskbarPosition::Right => { + requested_client_rect[0].right -= AUTO_HIDE_TASKBAR_THICKNESS_PX + } + AutoHideTaskbarPosition::Bottom => { + requested_client_rect[0].bottom -= AUTO_HIDE_TASKBAR_THICKNESS_PX + } + } + } + + Some(0) + } + + fn handle_activate_msg(self: &Rc, wparam: WPARAM) -> Option { + let activated = wparam.loword() > 0; + let this = self.clone(); + self.executor + .spawn(async move { + let mut lock = this.state.borrow_mut(); + if let Some(mut func) = lock.callbacks.active_status_change.take() { + drop(lock); + func(activated); + this.state.borrow_mut().callbacks.active_status_change = Some(func); + } + }) + .detach(); + + None + } + + fn handle_create_msg(&self, handle: HWND) -> Option { + if self.hide_title_bar { + notify_frame_changed(handle); + Some(0) + } else { + None + } + } + + fn handle_dpi_changed_msg( + &self, + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + ) -> Option { + let new_dpi = wparam.loword() as f32; + let mut lock = self.state.borrow_mut(); + let is_maximized = lock.is_maximized(); + let new_scale_factor = new_dpi / USER_DEFAULT_SCREEN_DPI as f32; + lock.scale_factor = new_scale_factor; + lock.border_offset.update(handle).log_err(); + drop(lock); + + let rect = unsafe { &*(lparam.0 as *const RECT) }; + let width = rect.right - rect.left; + let height = rect.bottom - rect.top; + // this will emit `WM_SIZE` and `WM_MOVE` right here + // even before this function returns + // the new size is handled in `WM_SIZE` + unsafe { + SetWindowPos( + handle, + None, + rect.left, + rect.top, + width, + height, + SWP_NOZORDER | SWP_NOACTIVATE, + ) + .context("unable to set window position after dpi has changed") + .log_err(); + } + + // When maximized, SetWindowPos doesn't send WM_SIZE, so we need to manually + // update the size and call the resize callback + if is_maximized { + let device_size = size(DevicePixels(width), DevicePixels(height)); + self.handle_size_change(device_size, new_scale_factor, true); + } + + Some(0) + } + + /// The following conditions will trigger this event: + /// 1. The monitor on which the window is located goes offline or changes resolution. + /// 2. Another monitor goes offline, is plugged in, or changes resolution. + /// + /// In either case, the window will only receive information from the monitor on which + /// it is located. + /// + /// For example, in the case of condition 2, where the monitor on which the window is + /// located has actually changed nothing, it will still receive this event. + fn handle_display_change_msg(&self, handle: HWND) -> Option { + // NOTE: + // Even the `lParam` holds the resolution of the screen, we just ignore it. + // Because WM_DPICHANGED, WM_MOVE, WM_SIZE will come first, window reposition and resize + // are handled there. + // So we only care about if monitor is disconnected. + let previous_monitor = self.state.borrow().display; + if WindowsDisplay::is_connected(previous_monitor.handle) { + // we are fine, other display changed + return None; + } + // display disconnected + // in this case, the OS will move our window to another monitor, and minimize it. + // we deminimize the window and query the monitor after moving + unsafe { + let _ = ShowWindow(handle, SW_SHOWNORMAL); + }; + let new_monitor = unsafe { MonitorFromWindow(handle, MONITOR_DEFAULTTONULL) }; + // all monitors disconnected + if new_monitor.is_invalid() { + log::error!("No monitor detected!"); + return None; + } + let new_display = WindowsDisplay::new_with_handle(new_monitor); + self.state.borrow_mut().display = new_display; + Some(0) + } + + fn handle_hit_test_msg( + &self, + handle: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + ) -> Option { + if !self.is_movable || self.state.borrow().is_fullscreen() { + return None; + } + + let mut lock = self.state.borrow_mut(); + if let Some(mut callback) = lock.callbacks.hit_test_window_control.take() { + drop(lock); + let area = callback(); + self.state.borrow_mut().callbacks.hit_test_window_control = Some(callback); + if let Some(area) = area { + return match area { + WindowControlArea::Drag => Some(HTCAPTION as _), + WindowControlArea::Close => Some(HTCLOSE as _), + WindowControlArea::Max => Some(HTMAXBUTTON as _), + WindowControlArea::Min => Some(HTMINBUTTON as _), + }; + } + } else { + drop(lock); + } + + if !self.hide_title_bar { + // If the OS draws the title bar, we don't need to handle hit test messages. + return None; + } + + // default handler for resize areas + let hit = unsafe { DefWindowProcW(handle, msg, wparam, lparam) }; + if matches!( + hit.0 as u32, + HTNOWHERE + | HTRIGHT + | HTLEFT + | HTTOPLEFT + | HTTOP + | HTTOPRIGHT + | HTBOTTOMRIGHT + | HTBOTTOM + | HTBOTTOMLEFT + ) { + return Some(hit.0); + } + + if self.state.borrow().is_fullscreen() { + return Some(HTCLIENT as _); + } + + let dpi = unsafe { GetDpiForWindow(handle) }; + let frame_y = unsafe { GetSystemMetricsForDpi(SM_CYFRAME, dpi) }; + + let mut cursor_point = POINT { + x: lparam.signed_loword().into(), + y: lparam.signed_hiword().into(), + }; + unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; + if !self.state.borrow().is_maximized() && cursor_point.y >= 0 && cursor_point.y <= frame_y { + return Some(HTTOP as _); + } + + Some(HTCLIENT as _) + } + + fn handle_nc_mouse_move_msg(&self, handle: HWND, lparam: LPARAM) -> Option { + self.start_tracking_mouse(handle, TME_LEAVE | TME_NONCLIENT); + + let mut lock = self.state.borrow_mut(); + let mut func = lock.callbacks.input.take()?; + let scale_factor = lock.scale_factor; + drop(lock); + + let mut cursor_point = POINT { + x: lparam.signed_loword().into(), + y: lparam.signed_hiword().into(), + }; + unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; + let input = PlatformInput::MouseMove(MouseMoveEvent { + position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), + pressed_button: None, + modifiers: current_modifiers(), + }); + let handled = !func(input).propagate; + self.state.borrow_mut().callbacks.input = Some(func); + + if handled { Some(0) } else { None } + } + + fn handle_nc_mouse_down_msg( + &self, + handle: HWND, + button: MouseButton, + wparam: WPARAM, + lparam: LPARAM, + ) -> Option { + let mut lock = self.state.borrow_mut(); + if let Some(mut func) = lock.callbacks.input.take() { + let scale_factor = lock.scale_factor; + let mut cursor_point = POINT { + x: lparam.signed_loword().into(), + y: lparam.signed_hiword().into(), + }; + unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; + let physical_point = point(DevicePixels(cursor_point.x), DevicePixels(cursor_point.y)); + let click_count = lock.click_state.update(button, physical_point); + drop(lock); + + let input = PlatformInput::MouseDown(MouseDownEvent { + button, + position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), + modifiers: current_modifiers(), + click_count, + first_mouse: false, + }); + let result = func(input); + let handled = !result.propagate || result.default_prevented; + self.state.borrow_mut().callbacks.input = Some(func); + + if handled { + return Some(0); + } + } else { + drop(lock); + }; + + // Since these are handled in handle_nc_mouse_up_msg we must prevent the default window proc + if button == MouseButton::Left { + match wparam.0 as u32 { + HTMINBUTTON => self.state.borrow_mut().nc_button_pressed = Some(HTMINBUTTON), + HTMAXBUTTON => self.state.borrow_mut().nc_button_pressed = Some(HTMAXBUTTON), + HTCLOSE => self.state.borrow_mut().nc_button_pressed = Some(HTCLOSE), + _ => return None, + }; + Some(0) + } else { + None + } + } + + fn handle_nc_mouse_up_msg( + &self, + handle: HWND, + button: MouseButton, + wparam: WPARAM, + lparam: LPARAM, + ) -> Option { + let mut lock = self.state.borrow_mut(); + if let Some(mut func) = lock.callbacks.input.take() { + let scale_factor = lock.scale_factor; + drop(lock); + + let mut cursor_point = POINT { + x: lparam.signed_loword().into(), + y: lparam.signed_hiword().into(), + }; + unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; + let input = PlatformInput::MouseUp(MouseUpEvent { + button, + position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), + modifiers: current_modifiers(), + click_count: 1, + }); + let handled = !func(input).propagate; + self.state.borrow_mut().callbacks.input = Some(func); + + if handled { + return Some(0); + } + } else { + drop(lock); + } + + let last_pressed = self.state.borrow_mut().nc_button_pressed.take(); + if button == MouseButton::Left + && let Some(last_pressed) = last_pressed + { + let handled = match (wparam.0 as u32, last_pressed) { + (HTMINBUTTON, HTMINBUTTON) => { + unsafe { ShowWindowAsync(handle, SW_MINIMIZE).ok().log_err() }; + true + } + (HTMAXBUTTON, HTMAXBUTTON) => { + if self.state.borrow().is_maximized() { + unsafe { ShowWindowAsync(handle, SW_NORMAL).ok().log_err() }; + } else { + unsafe { ShowWindowAsync(handle, SW_MAXIMIZE).ok().log_err() }; + } + true + } + (HTCLOSE, HTCLOSE) => { + unsafe { + PostMessageW(Some(handle), WM_CLOSE, WPARAM::default(), LPARAM::default()) + .log_err() + }; + true + } + _ => false, + }; + if handled { + return Some(0); + } + } + + None + } + + fn handle_cursor_changed(&self, lparam: LPARAM) -> Option { + let mut state = self.state.borrow_mut(); + let had_cursor = state.current_cursor.is_some(); + + state.current_cursor = if lparam.0 == 0 { + None + } else { + Some(HCURSOR(lparam.0 as _)) + }; + + if had_cursor != state.current_cursor.is_some() { + unsafe { SetCursor(state.current_cursor) }; + } + + Some(0) + } + + fn handle_set_cursor(&self, handle: HWND, lparam: LPARAM) -> Option { + if unsafe { !IsWindowEnabled(handle).as_bool() } + || matches!( + lparam.loword() as u32, + HTLEFT + | HTRIGHT + | HTTOP + | HTTOPLEFT + | HTTOPRIGHT + | HTBOTTOM + | HTBOTTOMLEFT + | HTBOTTOMRIGHT + ) + { + return None; + } + unsafe { + SetCursor(self.state.borrow().current_cursor); + }; + Some(1) + } + + fn handle_system_settings_changed( + &self, + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + ) -> Option { + if wparam.0 != 0 { + let mut lock = self.state.borrow_mut(); + let display = lock.display; + lock.click_state.system_update(wparam.0); + lock.border_offset.update(handle).log_err(); + // system settings may emit a window message which wants to take the refcell lock, so drop it + drop(lock); + self.system_settings.borrow_mut().update(display, wparam.0); + } else { + self.handle_system_theme_changed(handle, lparam)?; + }; + // Force to trigger WM_NCCALCSIZE event to ensure that we handle auto hide + // taskbar correctly. + notify_frame_changed(handle); + + Some(0) + } + + fn handle_system_command(&self, wparam: WPARAM) -> Option { + if wparam.0 == SC_KEYMENU as usize { + let mut lock = self.state.borrow_mut(); + if lock.system_key_handled { + lock.system_key_handled = false; + return Some(0); + } + } + None + } + + fn handle_system_theme_changed(&self, handle: HWND, lparam: LPARAM) -> Option { + // lParam is a pointer to a string that indicates the area containing the system parameter + // that was changed. + let parameter = PCWSTR::from_raw(lparam.0 as _); + if unsafe { !parameter.is_null() && !parameter.is_empty() } + && let Some(parameter_string) = unsafe { parameter.to_string() }.log_err() + { + log::info!("System settings changed: {}", parameter_string); + if parameter_string.as_str() == "ImmersiveColorSet" { + let new_appearance = system_appearance() + .context("unable to get system appearance when handling ImmersiveColorSet") + .log_err()?; + let mut lock = self.state.borrow_mut(); + if new_appearance != lock.appearance { + lock.appearance = new_appearance; + let mut callback = lock.callbacks.appearance_changed.take()?; + drop(lock); + callback(); + self.state.borrow_mut().callbacks.appearance_changed = Some(callback); + configure_dwm_dark_mode(handle, new_appearance); + } + } + } + Some(0) + } + + fn handle_input_language_changed(&self) -> Option { + unsafe { + PostMessageW( + Some(self.platform_window_handle), + WM_GPUI_KEYBOARD_LAYOUT_CHANGED, + WPARAM(self.validation_number), + LPARAM(0), + ) + .log_err(); + } + Some(0) + } + + fn handle_window_visibility_changed(&self, handle: HWND, wparam: WPARAM) -> Option { + if wparam.0 == 1 { + self.draw_window(handle, false); + } + None + } + + fn handle_device_lost(&self, lparam: LPARAM) -> Option { + let mut lock = self.state.borrow_mut(); + let devices = lparam.0 as *const DirectXDevices; + let devices = unsafe { &*devices }; + lock.renderer.handle_device_lost(&devices); + Some(0) + } + + #[inline] + fn draw_window(&self, handle: HWND, force_render: bool) -> Option { + let mut request_frame = self.state.borrow_mut().callbacks.request_frame.take()?; + request_frame(RequestFrameOptions { + require_presentation: false, + force_render, + }); + self.state.borrow_mut().callbacks.request_frame = Some(request_frame); + unsafe { ValidateRect(Some(handle), None).ok().log_err() }; + Some(0) + } + + #[inline] + fn parse_char_message(&self, wparam: WPARAM) -> Option { + let code_point = wparam.loword(); + let mut lock = self.state.borrow_mut(); + // https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-3/#G2630 + match code_point { + 0xD800..=0xDBFF => { + // High surrogate, wait for low surrogate + lock.pending_surrogate = Some(code_point); + None + } + 0xDC00..=0xDFFF => { + if let Some(high_surrogate) = lock.pending_surrogate.take() { + // Low surrogate, combine with pending high surrogate + String::from_utf16(&[high_surrogate, code_point]).ok() + } else { + // Invalid low surrogate without a preceding high surrogate + log::warn!( + "Received low surrogate without a preceding high surrogate: {code_point:x}" + ); + None + } + } + _ => { + lock.pending_surrogate = None; + char::from_u32(code_point as u32) + .filter(|c| !c.is_control()) + .map(|c| c.to_string()) + } + } + } + + fn start_tracking_mouse(&self, handle: HWND, flags: TRACKMOUSEEVENT_FLAGS) { + let mut lock = self.state.borrow_mut(); + if !lock.hovered { + lock.hovered = true; + unsafe { + TrackMouseEvent(&mut TRACKMOUSEEVENT { + cbSize: std::mem::size_of::() as u32, + dwFlags: flags, + hwndTrack: handle, + dwHoverTime: HOVER_DEFAULT, + }) + .log_err() + }; + if let Some(mut callback) = lock.callbacks.hovered_status_change.take() { + drop(lock); + callback(true); + self.state.borrow_mut().callbacks.hovered_status_change = Some(callback); + } + } + } + + fn with_input_handler(&self, f: F) -> Option + where + F: FnOnce(&mut PlatformInputHandler) -> R, + { + let mut input_handler = self.state.borrow_mut().input_handler.take()?; + let result = f(&mut input_handler); + self.state.borrow_mut().input_handler = Some(input_handler); + Some(result) + } + + fn with_input_handler_and_scale_factor(&self, f: F) -> Option + where + F: FnOnce(&mut PlatformInputHandler, f32) -> Option, + { + let mut lock = self.state.borrow_mut(); + let mut input_handler = lock.input_handler.take()?; + let scale_factor = lock.scale_factor; + drop(lock); + let result = f(&mut input_handler, scale_factor); + self.state.borrow_mut().input_handler = Some(input_handler); + result + } +} + +#[inline] +fn translate_message(handle: HWND, wparam: WPARAM, lparam: LPARAM) { + let msg = MSG { + hwnd: handle, + message: WM_KEYDOWN, + wParam: wparam, + lParam: lparam, + // It seems like leaving the following two parameters empty doesn't break key events, they still work as expected. + // But if any bugs pop up after this PR, this is probably the place to look first. + time: 0, + pt: POINT::default(), + }; + unsafe { TranslateMessage(&msg).ok().log_err() }; +} + +fn handle_key_event( + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + state: &mut WindowsWindowState, + f: F, +) -> Option +where + F: FnOnce(Keystroke) -> PlatformInput, +{ + let virtual_key = VIRTUAL_KEY(wparam.loword()); + let modifiers = current_modifiers(); + + match virtual_key { + VK_SHIFT | VK_CONTROL | VK_MENU | VK_LMENU | VK_RMENU | VK_LWIN | VK_RWIN => { + if state + .last_reported_modifiers + .is_some_and(|prev_modifiers| prev_modifiers == modifiers) + { + return None; + } + state.last_reported_modifiers = Some(modifiers); + Some(PlatformInput::ModifiersChanged(ModifiersChangedEvent { + modifiers, + capslock: current_capslock(), + })) + } + VK_PACKET => { + translate_message(handle, wparam, lparam); + None + } + VK_CAPITAL => { + let capslock = current_capslock(); + if state + .last_reported_capslock + .is_some_and(|prev_capslock| prev_capslock == capslock) + { + return None; + } + state.last_reported_capslock = Some(capslock); + Some(PlatformInput::ModifiersChanged(ModifiersChangedEvent { + modifiers, + capslock, + })) + } + vkey => { + let vkey = if vkey == VK_PROCESSKEY { + VIRTUAL_KEY(unsafe { ImmGetVirtualKey(handle) } as u16) + } else { + vkey + }; + let keystroke = parse_normal_key(vkey, lparam, modifiers)?; + Some(f(keystroke)) + } + } +} + +fn parse_immutable(vkey: VIRTUAL_KEY) -> Option { + Some( + match vkey { + VK_SPACE => "space", + VK_BACK => "backspace", + VK_RETURN => "enter", + VK_TAB => "tab", + VK_UP => "up", + VK_DOWN => "down", + VK_RIGHT => "right", + VK_LEFT => "left", + VK_HOME => "home", + VK_END => "end", + VK_PRIOR => "pageup", + VK_NEXT => "pagedown", + VK_BROWSER_BACK => "back", + VK_BROWSER_FORWARD => "forward", + VK_ESCAPE => "escape", + VK_INSERT => "insert", + VK_DELETE => "delete", + VK_APPS => "menu", + VK_F1 => "f1", + VK_F2 => "f2", + VK_F3 => "f3", + VK_F4 => "f4", + VK_F5 => "f5", + VK_F6 => "f6", + VK_F7 => "f7", + VK_F8 => "f8", + VK_F9 => "f9", + VK_F10 => "f10", + VK_F11 => "f11", + VK_F12 => "f12", + VK_F13 => "f13", + VK_F14 => "f14", + VK_F15 => "f15", + VK_F16 => "f16", + VK_F17 => "f17", + VK_F18 => "f18", + VK_F19 => "f19", + VK_F20 => "f20", + VK_F21 => "f21", + VK_F22 => "f22", + VK_F23 => "f23", + VK_F24 => "f24", + _ => return None, + } + .to_string(), + ) +} + +fn parse_normal_key( + vkey: VIRTUAL_KEY, + lparam: LPARAM, + mut modifiers: Modifiers, +) -> Option { + let mut key_char = None; + let key = parse_immutable(vkey).or_else(|| { + let scan_code = lparam.hiword() & 0xFF; + key_char = generate_key_char( + vkey, + scan_code as u32, + modifiers.control, + modifiers.shift, + modifiers.alt, + ); + get_keystroke_key(vkey, scan_code as u32, &mut modifiers) + })?; + Some(Keystroke { + modifiers, + key, + key_char, + }) +} + +fn parse_ime_composition_string(ctx: HIMC, comp_type: IME_COMPOSITION_STRING) -> Option { + unsafe { + let string_len = ImmGetCompositionStringW(ctx, comp_type, None, 0); + if string_len >= 0 { + let mut buffer = vec![0u8; string_len as usize + 2]; + ImmGetCompositionStringW( + ctx, + comp_type, + Some(buffer.as_mut_ptr() as _), + string_len as _, + ); + let wstring = std::slice::from_raw_parts::( + buffer.as_mut_ptr().cast::(), + string_len as usize / 2, + ); + Some(String::from_utf16_lossy(wstring)) + } else { + None + } + } +} + +#[inline] +fn retrieve_composition_cursor_position(ctx: HIMC) -> usize { + unsafe { ImmGetCompositionStringW(ctx, GCS_CURSORPOS, None, 0) as usize } +} + +#[inline] +fn is_virtual_key_pressed(vkey: VIRTUAL_KEY) -> bool { + unsafe { GetKeyState(vkey.0 as i32) < 0 } +} + +fn keyboard_uses_altgr() -> bool { + use crate::platform::windows::keyboard::WindowsKeyboardLayout; + WindowsKeyboardLayout::new() + .map(|layout| layout.uses_altgr()) + .unwrap_or(false) +} + +#[inline] +pub(crate) fn current_modifiers() -> Modifiers { + let lmenu_pressed = is_virtual_key_pressed(VK_LMENU); + let rmenu_pressed = is_virtual_key_pressed(VK_RMENU); + let lcontrol_pressed = is_virtual_key_pressed(VK_LCONTROL); + + // Only treat right Alt + left Ctrl as AltGr on keyboards that actually use it + let altgr = keyboard_uses_altgr() && rmenu_pressed && lcontrol_pressed; + + Modifiers { + control: is_virtual_key_pressed(VK_CONTROL) && !altgr, + alt: (lmenu_pressed || rmenu_pressed) && !altgr, + shift: is_virtual_key_pressed(VK_SHIFT), + platform: is_virtual_key_pressed(VK_LWIN) || is_virtual_key_pressed(VK_RWIN), + function: false, + } +} + +#[inline] +pub(crate) fn current_capslock() -> Capslock { + let on = unsafe { GetKeyState(VK_CAPITAL.0 as i32) & 1 } > 0; + Capslock { on } +} + +fn get_client_area_insets( + handle: HWND, + is_maximized: bool, + windows_version: WindowsVersion, +) -> RECT { + // For maximized windows, Windows outdents the window rect from the screen's client rect + // by `frame_thickness` on each edge, meaning `insets` must contain `frame_thickness` + // on all sides (including the top) to avoid the client area extending onto adjacent + // monitors. + // + // For non-maximized windows, things become complicated: + // + // - On Windows 10 + // The top inset must be zero, since if there is any nonclient area, Windows will draw + // a full native titlebar outside the client area. (This doesn't occur in the maximized + // case.) + // + // - On Windows 11 + // The top inset is calculated using an empirical formula that I derived through various + // tests. Without this, the top 1-2 rows of pixels in our window would be obscured. + let dpi = unsafe { GetDpiForWindow(handle) }; + let frame_thickness = get_frame_thickness(dpi); + let top_insets = if is_maximized { + frame_thickness + } else { + match windows_version { + WindowsVersion::Win10 => 0, + WindowsVersion::Win11 => (dpi as f32 / USER_DEFAULT_SCREEN_DPI as f32).round() as i32, + } + }; + RECT { + left: frame_thickness, + top: top_insets, + right: frame_thickness, + bottom: frame_thickness, + } +} + +// there is some additional non-visible space when talking about window +// borders on Windows: +// - SM_CXSIZEFRAME: The resize handle. +// - SM_CXPADDEDBORDER: Additional border space that isn't part of the resize handle. +fn get_frame_thickness(dpi: u32) -> i32 { + let resize_frame_thickness = unsafe { GetSystemMetricsForDpi(SM_CXSIZEFRAME, dpi) }; + let padding_thickness = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) }; + resize_frame_thickness + padding_thickness +} + +fn notify_frame_changed(handle: HWND) { + unsafe { + SetWindowPos( + handle, + None, + 0, + 0, + 0, + 0, + SWP_FRAMECHANGED + | SWP_NOACTIVATE + | SWP_NOCOPYBITS + | SWP_NOMOVE + | SWP_NOOWNERZORDER + | SWP_NOREPOSITION + | SWP_NOSENDCHANGING + | SWP_NOSIZE + | SWP_NOZORDER, + ) + .log_err(); + } +} diff --git a/third_party/gpui/src/platform/windows/keyboard.rs b/third_party/gpui/src/platform/windows/keyboard.rs new file mode 100644 index 0000000..7a8478d --- /dev/null +++ b/third_party/gpui/src/platform/windows/keyboard.rs @@ -0,0 +1,404 @@ +use anyhow::Result; +use collections::HashMap; +use windows::Win32::UI::{ + Input::KeyboardAndMouse::{ + GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MAPVK_VK_TO_VSC, MapVirtualKeyW, ToUnicode, + VIRTUAL_KEY, VK_0, VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1, + VK_CONTROL, VK_MENU, VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7, + VK_OEM_8, VK_OEM_102, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT, + }, + WindowsAndMessaging::KL_NAMELENGTH, +}; +use windows_core::HSTRING; + +use crate::{ + KeybindingKeystroke, Keystroke, Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper, +}; + +pub(crate) struct WindowsKeyboardLayout { + id: String, + name: String, +} + +pub(crate) struct WindowsKeyboardMapper { + key_to_vkey: HashMap, + vkey_to_key: HashMap, + vkey_to_shifted: HashMap, +} + +impl PlatformKeyboardLayout for WindowsKeyboardLayout { + fn id(&self) -> &str { + &self.id + } + + fn name(&self) -> &str { + &self.name + } +} + +impl PlatformKeyboardMapper for WindowsKeyboardMapper { + fn map_key_equivalent( + &self, + mut keystroke: Keystroke, + use_key_equivalents: bool, + ) -> KeybindingKeystroke { + let Some((vkey, shifted_key)) = self.get_vkey_from_key(&keystroke.key, use_key_equivalents) + else { + return KeybindingKeystroke::from_keystroke(keystroke); + }; + if shifted_key && keystroke.modifiers.shift { + log::warn!( + "Keystroke '{}' has both shift and a shifted key, this is likely a bug", + keystroke.key + ); + } + + let shift = shifted_key || keystroke.modifiers.shift; + keystroke.modifiers.shift = false; + + let Some(key) = self.vkey_to_key.get(&vkey).cloned() else { + log::error!( + "Failed to map key equivalent '{:?}' to a valid key", + keystroke + ); + return KeybindingKeystroke::from_keystroke(keystroke); + }; + + keystroke.key = if shift { + let Some(shifted_key) = self.vkey_to_shifted.get(&vkey).cloned() else { + log::error!( + "Failed to map keystroke {:?} with virtual key '{:?}' to a shifted key", + keystroke, + vkey + ); + return KeybindingKeystroke::from_keystroke(keystroke); + }; + shifted_key + } else { + key.clone() + }; + + let modifiers = Modifiers { + shift, + ..keystroke.modifiers + }; + + KeybindingKeystroke::new(keystroke, modifiers, key) + } + + fn get_key_equivalents(&self) -> Option<&HashMap> { + None + } +} + +impl WindowsKeyboardLayout { + pub(crate) fn new() -> Result { + let mut buffer = [0u16; KL_NAMELENGTH as usize]; + unsafe { GetKeyboardLayoutNameW(&mut buffer)? }; + let id = HSTRING::from_wide(&buffer).to_string(); + let entry = windows_registry::LOCAL_MACHINE.open(format!( + "System\\CurrentControlSet\\Control\\Keyboard Layouts\\{}", + id + ))?; + let name = entry.get_hstring("Layout Text")?.to_string(); + Ok(Self { id, name }) + } + + pub(crate) fn unknown() -> Self { + Self { + id: "unknown".to_string(), + name: "unknown".to_string(), + } + } + + pub(crate) fn uses_altgr(&self) -> bool { + // Check if this is a known AltGr layout by examining the layout ID + // The layout ID is a hex string like "00000409" (US) or "00000407" (German) + // Extract the language ID (last 4 bytes) + let id_bytes = self.id.as_bytes(); + if id_bytes.len() >= 4 { + let lang_id = &id_bytes[id_bytes.len() - 4..]; + // List of keyboard layouts that use AltGr (non-exhaustive) + matches!( + lang_id, + b"0407" | // German + b"040C" | // French + b"040A" | // Spanish + b"0415" | // Polish + b"0413" | // Dutch + b"0816" | // Portuguese + b"041D" | // Swedish + b"0414" | // Norwegian + b"040B" | // Finnish + b"041F" | // Turkish + b"0419" | // Russian + b"0405" | // Czech + b"040E" | // Hungarian + b"0424" | // Slovenian + b"041B" | // Slovak + b"0418" // Romanian + ) + } else { + false + } + } +} + +impl WindowsKeyboardMapper { + pub(crate) fn new() -> Self { + let mut key_to_vkey = HashMap::default(); + let mut vkey_to_key = HashMap::default(); + let mut vkey_to_shifted = HashMap::default(); + for vkey in CANDIDATE_VKEYS { + if let Some(key) = get_key_from_vkey(*vkey) { + key_to_vkey.insert(key.clone(), (vkey.0, false)); + vkey_to_key.insert(vkey.0, key); + } + let scan_code = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_VSC) }; + if scan_code == 0 { + continue; + } + if let Some(shifted_key) = get_shifted_key(*vkey, scan_code) { + key_to_vkey.insert(shifted_key.clone(), (vkey.0, true)); + vkey_to_shifted.insert(vkey.0, shifted_key); + } + } + Self { + key_to_vkey, + vkey_to_key, + vkey_to_shifted, + } + } + + fn get_vkey_from_key(&self, key: &str, use_key_equivalents: bool) -> Option<(u16, bool)> { + if use_key_equivalents { + get_vkey_from_key_with_us_layout(key) + } else { + self.key_to_vkey.get(key).cloned() + } + } +} + +pub(crate) fn get_keystroke_key( + vkey: VIRTUAL_KEY, + scan_code: u32, + modifiers: &mut Modifiers, +) -> Option { + if modifiers.shift && need_to_convert_to_shifted_key(vkey) { + get_shifted_key(vkey, scan_code).inspect(|_| { + modifiers.shift = false; + }) + } else { + get_key_from_vkey(vkey) + } +} + +fn get_key_from_vkey(vkey: VIRTUAL_KEY) -> Option { + let key_data = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_CHAR) }; + if key_data == 0 { + return None; + } + + // The high word contains dead key flag, the low word contains the character + let key = char::from_u32(key_data & 0xFFFF)?; + + Some(key.to_ascii_lowercase().to_string()) +} + +#[inline] +fn need_to_convert_to_shifted_key(vkey: VIRTUAL_KEY) -> bool { + matches!( + vkey, + VK_OEM_3 + | VK_OEM_MINUS + | VK_OEM_PLUS + | VK_OEM_4 + | VK_OEM_5 + | VK_OEM_6 + | VK_OEM_1 + | VK_OEM_7 + | VK_OEM_COMMA + | VK_OEM_PERIOD + | VK_OEM_2 + | VK_OEM_102 + | VK_OEM_8 + | VK_ABNT_C1 + | VK_0 + | VK_1 + | VK_2 + | VK_3 + | VK_4 + | VK_5 + | VK_6 + | VK_7 + | VK_8 + | VK_9 + ) +} + +fn get_shifted_key(vkey: VIRTUAL_KEY, scan_code: u32) -> Option { + generate_key_char(vkey, scan_code, false, true, false) +} + +pub(crate) fn generate_key_char( + vkey: VIRTUAL_KEY, + scan_code: u32, + control: bool, + shift: bool, + alt: bool, +) -> Option { + let mut state = [0; 256]; + if control { + state[VK_CONTROL.0 as usize] = 0x80; + } + if shift { + state[VK_SHIFT.0 as usize] = 0x80; + } + if alt { + state[VK_MENU.0 as usize] = 0x80; + } + + let mut buffer = [0; 8]; + let len = unsafe { ToUnicode(vkey.0 as u32, scan_code, Some(&state), &mut buffer, 1 << 2) }; + + match len { + len if len > 0 => String::from_utf16(&buffer[..len as usize]) + .ok() + .filter(|candidate| { + !candidate.is_empty() && !candidate.chars().next().unwrap().is_control() + }), + len if len < 0 => String::from_utf16(&buffer[..(-len as usize)]).ok(), + _ => None, + } +} + +fn get_vkey_from_key_with_us_layout(key: &str) -> Option<(u16, bool)> { + match key { + // ` => VK_OEM_3 + "`" => Some((VK_OEM_3.0, false)), + "~" => Some((VK_OEM_3.0, true)), + "1" => Some((VK_1.0, false)), + "!" => Some((VK_1.0, true)), + "2" => Some((VK_2.0, false)), + "@" => Some((VK_2.0, true)), + "3" => Some((VK_3.0, false)), + "#" => Some((VK_3.0, true)), + "4" => Some((VK_4.0, false)), + "$" => Some((VK_4.0, true)), + "5" => Some((VK_5.0, false)), + "%" => Some((VK_5.0, true)), + "6" => Some((VK_6.0, false)), + "^" => Some((VK_6.0, true)), + "7" => Some((VK_7.0, false)), + "&" => Some((VK_7.0, true)), + "8" => Some((VK_8.0, false)), + "*" => Some((VK_8.0, true)), + "9" => Some((VK_9.0, false)), + "(" => Some((VK_9.0, true)), + "0" => Some((VK_0.0, false)), + ")" => Some((VK_0.0, true)), + "-" => Some((VK_OEM_MINUS.0, false)), + "_" => Some((VK_OEM_MINUS.0, true)), + "=" => Some((VK_OEM_PLUS.0, false)), + "+" => Some((VK_OEM_PLUS.0, true)), + "[" => Some((VK_OEM_4.0, false)), + "{" => Some((VK_OEM_4.0, true)), + "]" => Some((VK_OEM_6.0, false)), + "}" => Some((VK_OEM_6.0, true)), + "\\" => Some((VK_OEM_5.0, false)), + "|" => Some((VK_OEM_5.0, true)), + ";" => Some((VK_OEM_1.0, false)), + ":" => Some((VK_OEM_1.0, true)), + "'" => Some((VK_OEM_7.0, false)), + "\"" => Some((VK_OEM_7.0, true)), + "," => Some((VK_OEM_COMMA.0, false)), + "<" => Some((VK_OEM_COMMA.0, true)), + "." => Some((VK_OEM_PERIOD.0, false)), + ">" => Some((VK_OEM_PERIOD.0, true)), + "/" => Some((VK_OEM_2.0, false)), + "?" => Some((VK_OEM_2.0, true)), + _ => None, + } +} + +const CANDIDATE_VKEYS: &[VIRTUAL_KEY] = &[ + VK_OEM_3, + VK_OEM_MINUS, + VK_OEM_PLUS, + VK_OEM_4, + VK_OEM_5, + VK_OEM_6, + VK_OEM_1, + VK_OEM_7, + VK_OEM_COMMA, + VK_OEM_PERIOD, + VK_OEM_2, + VK_OEM_102, + VK_OEM_8, + VK_ABNT_C1, + VK_0, + VK_1, + VK_2, + VK_3, + VK_4, + VK_5, + VK_6, + VK_7, + VK_8, + VK_9, +]; + +#[cfg(test)] +mod tests { + use crate::{Keystroke, Modifiers, PlatformKeyboardMapper, WindowsKeyboardMapper}; + + #[test] + fn test_keyboard_mapper() { + let mapper = WindowsKeyboardMapper::new(); + + // Normal case + let keystroke = Keystroke { + modifiers: Modifiers::control(), + key: "a".to_string(), + key_char: None, + }; + let mapped = mapper.map_key_equivalent(keystroke.clone(), true); + assert_eq!(*mapped.inner(), keystroke); + assert_eq!(mapped.key(), "a"); + assert_eq!(*mapped.modifiers(), Modifiers::control()); + + // Shifted case, ctrl-$ + let keystroke = Keystroke { + modifiers: Modifiers::control(), + key: "$".to_string(), + key_char: None, + }; + let mapped = mapper.map_key_equivalent(keystroke.clone(), true); + assert_eq!(*mapped.inner(), keystroke); + assert_eq!(mapped.key(), "4"); + assert_eq!(*mapped.modifiers(), Modifiers::control_shift()); + + // Shifted case, but shift is true + let keystroke = Keystroke { + modifiers: Modifiers::control_shift(), + key: "$".to_string(), + key_char: None, + }; + let mapped = mapper.map_key_equivalent(keystroke, true); + assert_eq!(mapped.inner().modifiers, Modifiers::control()); + assert_eq!(mapped.key(), "4"); + assert_eq!(*mapped.modifiers(), Modifiers::control_shift()); + + // Windows style + let keystroke = Keystroke { + modifiers: Modifiers::control_shift(), + key: "4".to_string(), + key_char: None, + }; + let mapped = mapper.map_key_equivalent(keystroke, true); + assert_eq!(mapped.inner().modifiers, Modifiers::control()); + assert_eq!(mapped.inner().key, "$"); + assert_eq!(mapped.key(), "4"); + assert_eq!(*mapped.modifiers(), Modifiers::control_shift()); + } +} diff --git a/third_party/gpui/src/platform/windows/platform.rs b/third_party/gpui/src/platform/windows/platform.rs new file mode 100644 index 0000000..361d8e1 --- /dev/null +++ b/third_party/gpui/src/platform/windows/platform.rs @@ -0,0 +1,1169 @@ +use std::{ + cell::RefCell, + ffi::OsStr, + mem::ManuallyDrop, + path::{Path, PathBuf}, + rc::{Rc, Weak}, + sync::Arc, +}; + +use ::util::{ResultExt, paths::SanitizedPath}; +use anyhow::{Context as _, Result, anyhow}; +use async_task::Runnable; +use futures::channel::oneshot::{self, Receiver}; +use itertools::Itertools; +use parking_lot::RwLock; +use smallvec::SmallVec; +use windows::{ + UI::ViewManagement::UISettings, + Win32::{ + Foundation::*, + Graphics::{Direct3D11::ID3D11Device, Gdi::*}, + Security::Credentials::*, + System::{Com::*, LibraryLoader::*, Ole::*, SystemInformation::*}, + UI::{Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*}, + }, + core::*, +}; + +use crate::*; + +pub(crate) struct WindowsPlatform { + inner: Rc, + raw_window_handles: Arc>>, + // The below members will never change throughout the entire lifecycle of the app. + icon: HICON, + background_executor: BackgroundExecutor, + foreground_executor: ForegroundExecutor, + text_system: Arc, + windows_version: WindowsVersion, + drop_target_helper: IDropTargetHelper, + handle: HWND, + disable_direct_composition: bool, +} + +struct WindowsPlatformInner { + state: RefCell, + raw_window_handles: std::sync::Weak>>, + // The below members will never change throughout the entire lifecycle of the app. + validation_number: usize, + main_receiver: flume::Receiver, +} + +pub(crate) struct WindowsPlatformState { + callbacks: PlatformCallbacks, + menus: Vec, + jump_list: JumpList, + // NOTE: standard cursor handles don't need to close. + pub(crate) current_cursor: Option, + directx_devices: ManuallyDrop, +} + +#[derive(Default)] +struct PlatformCallbacks { + open_urls: Option)>>, + quit: Option>, + reopen: Option>, + app_menu_action: Option>, + will_open_app_menu: Option>, + validate_app_menu_command: Option bool>>, + keyboard_layout_change: Option>, +} + +impl WindowsPlatformState { + fn new(directx_devices: DirectXDevices) -> Self { + let callbacks = PlatformCallbacks::default(); + let jump_list = JumpList::new(); + let current_cursor = load_cursor(CursorStyle::Arrow); + let directx_devices = ManuallyDrop::new(directx_devices); + + Self { + callbacks, + jump_list, + current_cursor, + directx_devices, + menus: Vec::new(), + } + } +} + +impl WindowsPlatform { + pub(crate) fn new() -> Result { + unsafe { + OleInitialize(None).context("unable to initialize Windows OLE")?; + } + let directx_devices = DirectXDevices::new().context("Creating DirectX devices")?; + let (main_sender, main_receiver) = flume::unbounded::(); + let validation_number = if usize::BITS == 64 { + rand::random::() as usize + } else { + rand::random::() as usize + }; + let raw_window_handles = Arc::new(RwLock::new(SmallVec::new())); + let text_system = Arc::new( + DirectWriteTextSystem::new(&directx_devices) + .context("Error creating DirectWriteTextSystem")?, + ); + register_platform_window_class(); + let mut context = PlatformWindowCreateContext { + inner: None, + raw_window_handles: Arc::downgrade(&raw_window_handles), + validation_number, + main_receiver: Some(main_receiver), + directx_devices: Some(directx_devices), + }; + let result = unsafe { + CreateWindowExW( + WINDOW_EX_STYLE(0), + PLATFORM_WINDOW_CLASS_NAME, + None, + WINDOW_STYLE(0), + 0, + 0, + 0, + 0, + Some(HWND_MESSAGE), + None, + None, + Some(&context as *const _ as *const _), + ) + }; + let inner = context.inner.take().unwrap()?; + let handle = result?; + let dispatcher = Arc::new(WindowsDispatcher::new( + main_sender, + handle, + validation_number, + )); + let disable_direct_composition = std::env::var(DISABLE_DIRECT_COMPOSITION) + .is_ok_and(|value| value == "true" || value == "1"); + let background_executor = BackgroundExecutor::new(dispatcher.clone()); + let foreground_executor = ForegroundExecutor::new(dispatcher); + + let drop_target_helper: IDropTargetHelper = unsafe { + CoCreateInstance(&CLSID_DragDropHelper, None, CLSCTX_INPROC_SERVER) + .context("Error creating drop target helper.")? + }; + let icon = load_icon().unwrap_or_default(); + let windows_version = WindowsVersion::new().context("Error retrieve windows version")?; + + Ok(Self { + inner, + handle, + raw_window_handles, + icon, + background_executor, + foreground_executor, + text_system, + disable_direct_composition, + windows_version, + drop_target_helper, + }) + } + + pub fn window_from_hwnd(&self, hwnd: HWND) -> Option> { + self.raw_window_handles + .read() + .iter() + .find(|entry| entry.as_raw() == hwnd) + .and_then(|hwnd| window_from_hwnd(hwnd.as_raw())) + } + + #[inline] + fn post_message(&self, message: u32, wparam: WPARAM, lparam: LPARAM) { + self.raw_window_handles + .read() + .iter() + .for_each(|handle| unsafe { + PostMessageW(Some(handle.as_raw()), message, wparam, lparam).log_err(); + }); + } + + fn generate_creation_info(&self) -> WindowCreationInfo { + WindowCreationInfo { + icon: self.icon, + executor: self.foreground_executor.clone(), + current_cursor: self.inner.state.borrow().current_cursor, + windows_version: self.windows_version, + drop_target_helper: self.drop_target_helper.clone(), + validation_number: self.inner.validation_number, + main_receiver: self.inner.main_receiver.clone(), + platform_window_handle: self.handle, + disable_direct_composition: self.disable_direct_composition, + directx_devices: (*self.inner.state.borrow().directx_devices).clone(), + } + } + + fn set_dock_menus(&self, menus: Vec) { + let mut actions = Vec::new(); + menus.into_iter().for_each(|menu| { + if let Some(dock_menu) = DockMenuItem::new(menu).log_err() { + actions.push(dock_menu); + } + }); + let mut lock = self.inner.state.borrow_mut(); + lock.jump_list.dock_menus = actions; + update_jump_list(&lock.jump_list).log_err(); + } + + fn update_jump_list( + &self, + menus: Vec, + entries: Vec>, + ) -> Vec> { + let mut actions = Vec::new(); + menus.into_iter().for_each(|menu| { + if let Some(dock_menu) = DockMenuItem::new(menu).log_err() { + actions.push(dock_menu); + } + }); + let mut lock = self.inner.state.borrow_mut(); + lock.jump_list.dock_menus = actions; + lock.jump_list.recent_workspaces = entries; + update_jump_list(&lock.jump_list) + .log_err() + .unwrap_or_default() + } + + fn find_current_active_window(&self) -> Option { + let active_window_hwnd = unsafe { GetActiveWindow() }; + if active_window_hwnd.is_invalid() { + return None; + } + self.raw_window_handles + .read() + .iter() + .find(|hwnd| hwnd.as_raw() == active_window_hwnd) + .map(|hwnd| hwnd.as_raw()) + } + + fn begin_vsync_thread(&self) { + let mut directx_device = (*self.inner.state.borrow().directx_devices).clone(); + let platform_window: SafeHwnd = self.handle.into(); + let validation_number = self.inner.validation_number; + let all_windows = Arc::downgrade(&self.raw_window_handles); + let text_system = Arc::downgrade(&self.text_system); + std::thread::Builder::new() + .name("VSyncProvider".to_owned()) + .spawn(move || { + let vsync_provider = VSyncProvider::new(); + loop { + vsync_provider.wait_for_vsync(); + if check_device_lost(&directx_device.device) { + handle_gpu_device_lost( + &mut directx_device, + platform_window.as_raw(), + validation_number, + &all_windows, + &text_system, + ); + } + let Some(all_windows) = all_windows.upgrade() else { + break; + }; + for hwnd in all_windows.read().iter() { + unsafe { + let _ = RedrawWindow(Some(hwnd.as_raw()), None, None, RDW_INVALIDATE); + } + } + } + }) + .unwrap(); + } +} + +impl Platform for WindowsPlatform { + fn background_executor(&self) -> BackgroundExecutor { + self.background_executor.clone() + } + + fn foreground_executor(&self) -> ForegroundExecutor { + self.foreground_executor.clone() + } + + fn text_system(&self) -> Arc { + self.text_system.clone() + } + + fn keyboard_layout(&self) -> Box { + Box::new( + WindowsKeyboardLayout::new() + .log_err() + .unwrap_or(WindowsKeyboardLayout::unknown()), + ) + } + + fn keyboard_mapper(&self) -> Rc { + Rc::new(WindowsKeyboardMapper::new()) + } + + fn on_keyboard_layout_change(&self, callback: Box) { + self.inner + .state + .borrow_mut() + .callbacks + .keyboard_layout_change = Some(callback); + } + + fn run(&self, on_finish_launching: Box) { + on_finish_launching(); + self.begin_vsync_thread(); + + let mut msg = MSG::default(); + unsafe { + while GetMessageW(&mut msg, None, 0, 0).as_bool() { + DispatchMessageW(&msg); + } + } + + if let Some(ref mut callback) = self.inner.state.borrow_mut().callbacks.quit { + callback(); + } + } + + fn quit(&self) { + self.foreground_executor() + .spawn(async { unsafe { PostQuitMessage(0) } }) + .detach(); + } + + fn restart(&self, binary_path: Option) { + let pid = std::process::id(); + let Some(app_path) = binary_path.or(self.app_path().log_err()) else { + return; + }; + let script = format!( + r#" + $pidToWaitFor = {} + $exePath = "{}" + + while ($true) {{ + $process = Get-Process -Id $pidToWaitFor -ErrorAction SilentlyContinue + if (-not $process) {{ + Start-Process -FilePath $exePath + break + }} + Start-Sleep -Seconds 0.1 + }} + "#, + pid, + app_path.display(), + ); + + #[allow( + clippy::disallowed_methods, + reason = "We are restarting ourselves, using std command thus is fine" + )] + let restart_process = util::command::new_std_command("powershell.exe") + .arg("-command") + .arg(script) + .spawn(); + + match restart_process { + Ok(_) => self.quit(), + Err(e) => log::error!("failed to spawn restart script: {:?}", e), + } + } + + fn activate(&self, _ignoring_other_apps: bool) {} + + fn hide(&self) {} + + // todo(windows) + fn hide_other_apps(&self) { + unimplemented!() + } + + // todo(windows) + fn unhide_other_apps(&self) { + unimplemented!() + } + + fn displays(&self) -> Vec> { + WindowsDisplay::displays() + } + + fn primary_display(&self) -> Option> { + WindowsDisplay::primary_monitor().map(|display| Rc::new(display) as Rc) + } + + #[cfg(feature = "screen-capture")] + fn is_screen_capture_supported(&self) -> bool { + true + } + + #[cfg(feature = "screen-capture")] + fn screen_capture_sources( + &self, + ) -> oneshot::Receiver>>> { + crate::platform::scap_screen_capture::scap_screen_sources(&self.foreground_executor) + } + + fn active_window(&self) -> Option { + let active_window_hwnd = unsafe { GetActiveWindow() }; + self.window_from_hwnd(active_window_hwnd) + .map(|inner| inner.handle) + } + + fn open_window( + &self, + handle: AnyWindowHandle, + options: WindowParams, + ) -> Result> { + let window = WindowsWindow::new(handle, options, self.generate_creation_info())?; + let handle = window.get_raw_handle(); + self.raw_window_handles.write().push(handle.into()); + + Ok(Box::new(window)) + } + + fn window_appearance(&self) -> WindowAppearance { + system_appearance().log_err().unwrap_or_default() + } + + fn open_url(&self, url: &str) { + if url.is_empty() { + return; + } + let url_string = url.to_string(); + self.background_executor() + .spawn(async move { + open_target(&url_string) + .with_context(|| format!("Opening url: {}", url_string)) + .log_err(); + }) + .detach(); + } + + fn on_open_urls(&self, callback: Box)>) { + self.inner.state.borrow_mut().callbacks.open_urls = Some(callback); + } + + fn prompt_for_paths( + &self, + options: PathPromptOptions, + ) -> Receiver>>> { + let (tx, rx) = oneshot::channel(); + let window = self.find_current_active_window(); + self.foreground_executor() + .spawn(async move { + let _ = tx.send(file_open_dialog(options, window)); + }) + .detach(); + + rx + } + + fn prompt_for_new_path( + &self, + directory: &Path, + suggested_name: Option<&str>, + ) -> Receiver>> { + let directory = directory.to_owned(); + let suggested_name = suggested_name.map(|s| s.to_owned()); + let (tx, rx) = oneshot::channel(); + let window = self.find_current_active_window(); + self.foreground_executor() + .spawn(async move { + let _ = tx.send(file_save_dialog(directory, suggested_name, window)); + }) + .detach(); + + rx + } + + fn can_select_mixed_files_and_dirs(&self) -> bool { + // The FOS_PICKFOLDERS flag toggles between "only files" and "only folders". + false + } + + fn reveal_path(&self, path: &Path) { + if path.as_os_str().is_empty() { + return; + } + let path = path.to_path_buf(); + self.background_executor() + .spawn(async move { + open_target_in_explorer(&path) + .with_context(|| format!("Revealing path {} in explorer", path.display())) + .log_err(); + }) + .detach(); + } + + fn open_with_system(&self, path: &Path) { + if path.as_os_str().is_empty() { + return; + } + let path = path.to_path_buf(); + self.background_executor() + .spawn(async move { + open_target(&path) + .with_context(|| format!("Opening {} with system", path.display())) + .log_err(); + }) + .detach(); + } + + fn on_quit(&self, callback: Box) { + self.inner.state.borrow_mut().callbacks.quit = Some(callback); + } + + fn on_reopen(&self, callback: Box) { + self.inner.state.borrow_mut().callbacks.reopen = Some(callback); + } + + fn set_menus(&self, menus: Vec, _keymap: &Keymap) { + self.inner.state.borrow_mut().menus = menus.into_iter().map(|menu| menu.owned()).collect(); + } + + fn get_menus(&self) -> Option> { + Some(self.inner.state.borrow().menus.clone()) + } + + fn set_dock_menu(&self, menus: Vec, _keymap: &Keymap) { + self.set_dock_menus(menus); + } + + fn on_app_menu_action(&self, callback: Box) { + self.inner.state.borrow_mut().callbacks.app_menu_action = Some(callback); + } + + fn on_will_open_app_menu(&self, callback: Box) { + self.inner.state.borrow_mut().callbacks.will_open_app_menu = Some(callback); + } + + fn on_validate_app_menu_command(&self, callback: Box bool>) { + self.inner + .state + .borrow_mut() + .callbacks + .validate_app_menu_command = Some(callback); + } + + fn app_path(&self) -> Result { + Ok(std::env::current_exe()?) + } + + // todo(windows) + fn path_for_auxiliary_executable(&self, _name: &str) -> Result { + anyhow::bail!("not yet implemented"); + } + + fn set_cursor_style(&self, style: CursorStyle) { + let hcursor = load_cursor(style); + let mut lock = self.inner.state.borrow_mut(); + if lock.current_cursor.map(|c| c.0) != hcursor.map(|c| c.0) { + self.post_message( + WM_GPUI_CURSOR_STYLE_CHANGED, + WPARAM(0), + LPARAM(hcursor.map_or(0, |c| c.0 as isize)), + ); + lock.current_cursor = hcursor; + } + } + + fn should_auto_hide_scrollbars(&self) -> bool { + should_auto_hide_scrollbars().log_err().unwrap_or(false) + } + + fn write_to_clipboard(&self, item: ClipboardItem) { + write_to_clipboard(item); + } + + fn read_from_clipboard(&self) -> Option { + read_from_clipboard() + } + + fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task> { + let mut password = password.to_vec(); + let mut username = username.encode_utf16().chain(Some(0)).collect_vec(); + let mut target_name = windows_credentials_target_name(url) + .encode_utf16() + .chain(Some(0)) + .collect_vec(); + self.foreground_executor().spawn(async move { + let credentials = CREDENTIALW { + LastWritten: unsafe { GetSystemTimeAsFileTime() }, + Flags: CRED_FLAGS(0), + Type: CRED_TYPE_GENERIC, + TargetName: PWSTR::from_raw(target_name.as_mut_ptr()), + CredentialBlobSize: password.len() as u32, + CredentialBlob: password.as_ptr() as *mut _, + Persist: CRED_PERSIST_LOCAL_MACHINE, + UserName: PWSTR::from_raw(username.as_mut_ptr()), + ..CREDENTIALW::default() + }; + unsafe { CredWriteW(&credentials, 0) }?; + Ok(()) + }) + } + + fn read_credentials(&self, url: &str) -> Task)>>> { + let mut target_name = windows_credentials_target_name(url) + .encode_utf16() + .chain(Some(0)) + .collect_vec(); + self.foreground_executor().spawn(async move { + let mut credentials: *mut CREDENTIALW = std::ptr::null_mut(); + unsafe { + CredReadW( + PCWSTR::from_raw(target_name.as_ptr()), + CRED_TYPE_GENERIC, + None, + &mut credentials, + )? + }; + + if credentials.is_null() { + Ok(None) + } else { + let username: String = unsafe { (*credentials).UserName.to_string()? }; + let credential_blob = unsafe { + std::slice::from_raw_parts( + (*credentials).CredentialBlob, + (*credentials).CredentialBlobSize as usize, + ) + }; + let password = credential_blob.to_vec(); + unsafe { CredFree(credentials as *const _ as _) }; + Ok(Some((username, password))) + } + }) + } + + fn delete_credentials(&self, url: &str) -> Task> { + let mut target_name = windows_credentials_target_name(url) + .encode_utf16() + .chain(Some(0)) + .collect_vec(); + self.foreground_executor().spawn(async move { + unsafe { + CredDeleteW( + PCWSTR::from_raw(target_name.as_ptr()), + CRED_TYPE_GENERIC, + None, + )? + }; + Ok(()) + }) + } + + fn register_url_scheme(&self, _: &str) -> Task> { + Task::ready(Err(anyhow!("register_url_scheme unimplemented"))) + } + + fn perform_dock_menu_action(&self, action: usize) { + unsafe { + PostMessageW( + Some(self.handle), + WM_GPUI_DOCK_MENU_ACTION, + WPARAM(self.inner.validation_number), + LPARAM(action as isize), + ) + .log_err(); + } + } + + fn update_jump_list( + &self, + menus: Vec, + entries: Vec>, + ) -> Vec> { + self.update_jump_list(menus, entries) + } +} + +impl WindowsPlatformInner { + fn new(context: &mut PlatformWindowCreateContext) -> Result> { + let state = RefCell::new(WindowsPlatformState::new( + context.directx_devices.take().unwrap(), + )); + Ok(Rc::new(Self { + state, + raw_window_handles: context.raw_window_handles.clone(), + validation_number: context.validation_number, + main_receiver: context.main_receiver.take().unwrap(), + })) + } + + fn handle_msg( + self: &Rc, + handle: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + ) -> LRESULT { + let handled = match msg { + WM_GPUI_CLOSE_ONE_WINDOW + | WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD + | WM_GPUI_DOCK_MENU_ACTION + | WM_GPUI_KEYBOARD_LAYOUT_CHANGED + | WM_GPUI_GPU_DEVICE_LOST => self.handle_gpui_events(msg, wparam, lparam), + _ => None, + }; + if let Some(result) = handled { + LRESULT(result) + } else { + unsafe { DefWindowProcW(handle, msg, wparam, lparam) } + } + } + + fn handle_gpui_events(&self, message: u32, wparam: WPARAM, lparam: LPARAM) -> Option { + if wparam.0 != self.validation_number { + log::error!("Wrong validation number while processing message: {message}"); + return None; + } + match message { + WM_GPUI_CLOSE_ONE_WINDOW => { + if self.close_one_window(HWND(lparam.0 as _)) { + unsafe { PostQuitMessage(0) }; + } + Some(0) + } + WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD => self.run_foreground_task(), + WM_GPUI_DOCK_MENU_ACTION => self.handle_dock_action_event(lparam.0 as _), + WM_GPUI_KEYBOARD_LAYOUT_CHANGED => self.handle_keyboard_layout_change(), + WM_GPUI_GPU_DEVICE_LOST => self.handle_device_lost(lparam), + _ => unreachable!(), + } + } + + fn close_one_window(&self, target_window: HWND) -> bool { + let Some(all_windows) = self.raw_window_handles.upgrade() else { + log::error!("Failed to upgrade raw window handles"); + return false; + }; + let mut lock = all_windows.write(); + let index = lock + .iter() + .position(|handle| handle.as_raw() == target_window) + .unwrap(); + lock.remove(index); + + lock.is_empty() + } + + #[inline] + fn run_foreground_task(&self) -> Option { + for runnable in self.main_receiver.drain() { + runnable.run(); + } + Some(0) + } + + fn handle_dock_action_event(&self, action_idx: usize) -> Option { + let mut lock = self.state.borrow_mut(); + let mut callback = lock.callbacks.app_menu_action.take()?; + let Some(action) = lock + .jump_list + .dock_menus + .get(action_idx) + .map(|dock_menu| dock_menu.action.boxed_clone()) + else { + lock.callbacks.app_menu_action = Some(callback); + log::error!("Dock menu for index {action_idx} not found"); + return Some(1); + }; + drop(lock); + callback(&*action); + self.state.borrow_mut().callbacks.app_menu_action = Some(callback); + Some(0) + } + + fn handle_keyboard_layout_change(&self) -> Option { + let mut callback = self + .state + .borrow_mut() + .callbacks + .keyboard_layout_change + .take()?; + callback(); + self.state.borrow_mut().callbacks.keyboard_layout_change = Some(callback); + Some(0) + } + + fn handle_device_lost(&self, lparam: LPARAM) -> Option { + let mut lock = self.state.borrow_mut(); + let directx_devices = lparam.0 as *const DirectXDevices; + let directx_devices = unsafe { &*directx_devices }; + unsafe { + ManuallyDrop::drop(&mut lock.directx_devices); + } + lock.directx_devices = ManuallyDrop::new(directx_devices.clone()); + + Some(0) + } +} + +impl Drop for WindowsPlatform { + fn drop(&mut self) { + unsafe { + DestroyWindow(self.handle) + .context("Destroying platform window") + .log_err(); + OleUninitialize(); + } + } +} + +impl Drop for WindowsPlatformState { + fn drop(&mut self) { + unsafe { + ManuallyDrop::drop(&mut self.directx_devices); + } + } +} + +pub(crate) struct WindowCreationInfo { + pub(crate) icon: HICON, + pub(crate) executor: ForegroundExecutor, + pub(crate) current_cursor: Option, + pub(crate) windows_version: WindowsVersion, + pub(crate) drop_target_helper: IDropTargetHelper, + pub(crate) validation_number: usize, + pub(crate) main_receiver: flume::Receiver, + pub(crate) platform_window_handle: HWND, + pub(crate) disable_direct_composition: bool, + pub(crate) directx_devices: DirectXDevices, +} + +struct PlatformWindowCreateContext { + inner: Option>>, + raw_window_handles: std::sync::Weak>>, + validation_number: usize, + main_receiver: Option>, + directx_devices: Option, +} + +fn open_target(target: impl AsRef) -> Result<()> { + let target = target.as_ref(); + let ret = unsafe { + ShellExecuteW( + None, + windows::core::w!("open"), + &HSTRING::from(target), + None, + None, + SW_SHOWDEFAULT, + ) + }; + if ret.0 as isize <= 32 { + Err(anyhow::anyhow!( + "Unable to open target: {}", + std::io::Error::last_os_error() + )) + } else { + Ok(()) + } +} + +fn open_target_in_explorer(target: &Path) -> Result<()> { + let dir = target.parent().context("No parent folder found")?; + let desktop = unsafe { SHGetDesktopFolder()? }; + + let mut dir_item = std::ptr::null_mut(); + unsafe { + desktop.ParseDisplayName( + HWND::default(), + None, + &HSTRING::from(dir), + None, + &mut dir_item, + std::ptr::null_mut(), + )?; + } + + let mut file_item = std::ptr::null_mut(); + unsafe { + desktop.ParseDisplayName( + HWND::default(), + None, + &HSTRING::from(target), + None, + &mut file_item, + std::ptr::null_mut(), + )?; + } + + let highlight = [file_item as *const _]; + unsafe { SHOpenFolderAndSelectItems(dir_item as _, Some(&highlight), 0) }.or_else(|err| { + if err.code().0 == ERROR_FILE_NOT_FOUND.0 as i32 { + // On some systems, the above call mysteriously fails with "file not + // found" even though the file is there. In these cases, ShellExecute() + // seems to work as a fallback (although it won't select the file). + open_target(dir).context("Opening target parent folder") + } else { + Err(anyhow::anyhow!("Can not open target path: {}", err)) + } + }) +} + +fn file_open_dialog( + options: PathPromptOptions, + window: Option, +) -> Result>> { + let folder_dialog: IFileOpenDialog = + unsafe { CoCreateInstance(&FileOpenDialog, None, CLSCTX_ALL)? }; + + let mut dialog_options = FOS_FILEMUSTEXIST; + if options.multiple { + dialog_options |= FOS_ALLOWMULTISELECT; + } + if options.directories { + dialog_options |= FOS_PICKFOLDERS; + } + + unsafe { + folder_dialog.SetOptions(dialog_options)?; + + if let Some(prompt) = options.prompt { + let prompt: &str = &prompt; + folder_dialog.SetOkButtonLabel(&HSTRING::from(prompt))?; + } + + if folder_dialog.Show(window).is_err() { + // User cancelled + return Ok(None); + } + } + + let results = unsafe { folder_dialog.GetResults()? }; + let file_count = unsafe { results.GetCount()? }; + if file_count == 0 { + return Ok(None); + } + + let mut paths = Vec::with_capacity(file_count as usize); + for i in 0..file_count { + let item = unsafe { results.GetItemAt(i)? }; + let path = unsafe { item.GetDisplayName(SIGDN_FILESYSPATH)?.to_string()? }; + paths.push(PathBuf::from(path)); + } + + Ok(Some(paths)) +} + +fn file_save_dialog( + directory: PathBuf, + suggested_name: Option, + window: Option, +) -> Result> { + let dialog: IFileSaveDialog = unsafe { CoCreateInstance(&FileSaveDialog, None, CLSCTX_ALL)? }; + if !directory.to_string_lossy().is_empty() + && let Some(full_path) = directory + .canonicalize() + .context("failed to canonicalize directory") + .log_err() + { + let full_path = SanitizedPath::new(&full_path); + let full_path_string = full_path.to_string(); + let path_item: IShellItem = + unsafe { SHCreateItemFromParsingName(&HSTRING::from(full_path_string), None)? }; + unsafe { + dialog + .SetFolder(&path_item) + .context("failed to set dialog folder") + .log_err() + }; + } + + if let Some(suggested_name) = suggested_name { + unsafe { + dialog + .SetFileName(&HSTRING::from(suggested_name)) + .context("failed to set file name") + .log_err() + }; + } + + unsafe { + dialog.SetFileTypes(&[Common::COMDLG_FILTERSPEC { + pszName: windows::core::w!("All files"), + pszSpec: windows::core::w!("*.*"), + }])?; + if dialog.Show(window).is_err() { + // User cancelled + return Ok(None); + } + } + let shell_item = unsafe { dialog.GetResult()? }; + let file_path_string = unsafe { + let pwstr = shell_item.GetDisplayName(SIGDN_FILESYSPATH)?; + let string = pwstr.to_string()?; + CoTaskMemFree(Some(pwstr.0 as _)); + string + }; + Ok(Some(PathBuf::from(file_path_string))) +} + +fn load_icon() -> Result { + let module = unsafe { GetModuleHandleW(None).context("unable to get module handle")? }; + let handle = unsafe { + LoadImageW( + Some(module.into()), + windows::core::PCWSTR(1 as _), + IMAGE_ICON, + 0, + 0, + LR_DEFAULTSIZE | LR_SHARED, + ) + .context("unable to load icon file")? + }; + Ok(HICON(handle.0)) +} + +#[inline] +fn should_auto_hide_scrollbars() -> Result { + let ui_settings = UISettings::new()?; + Ok(ui_settings.AutoHideScrollBars()?) +} + +fn check_device_lost(device: &ID3D11Device) -> bool { + let device_state = unsafe { device.GetDeviceRemovedReason() }; + match device_state { + Ok(_) => false, + Err(err) => { + log::error!("DirectX device lost detected: {:?}", err); + true + } + } +} + +fn handle_gpu_device_lost( + directx_devices: &mut DirectXDevices, + platform_window: HWND, + validation_number: usize, + all_windows: &std::sync::Weak>>, + text_system: &std::sync::Weak, +) { + // Here we wait a bit to ensure the system has time to recover from the device lost state. + // If we don't wait, the final drawing result will be blank. + std::thread::sleep(std::time::Duration::from_millis(350)); + + try_to_recover_from_device_lost( + || { + DirectXDevices::new() + .context("Failed to recreate new DirectX devices after device lost") + }, + |new_devices| *directx_devices = new_devices, + || { + log::error!("Failed to recover DirectX devices after multiple attempts."); + // Do something here? + // At this point, the device loss is considered unrecoverable. + // std::process::exit(1); + }, + ); + log::info!("DirectX devices successfully recreated."); + + unsafe { + SendMessageW( + platform_window, + WM_GPUI_GPU_DEVICE_LOST, + Some(WPARAM(validation_number)), + Some(LPARAM(directx_devices as *const _ as _)), + ); + } + + if let Some(text_system) = text_system.upgrade() { + text_system.handle_gpu_lost(&directx_devices); + } + if let Some(all_windows) = all_windows.upgrade() { + for window in all_windows.read().iter() { + unsafe { + SendMessageW( + window.as_raw(), + WM_GPUI_GPU_DEVICE_LOST, + Some(WPARAM(validation_number)), + Some(LPARAM(directx_devices as *const _ as _)), + ); + } + } + std::thread::sleep(std::time::Duration::from_millis(200)); + for window in all_windows.read().iter() { + unsafe { + SendMessageW( + window.as_raw(), + WM_GPUI_FORCE_UPDATE_WINDOW, + Some(WPARAM(validation_number)), + None, + ); + } + } + } +} + +const PLATFORM_WINDOW_CLASS_NAME: PCWSTR = w!("Zed::PlatformWindow"); + +fn register_platform_window_class() { + let wc = WNDCLASSW { + lpfnWndProc: Some(window_procedure), + lpszClassName: PCWSTR(PLATFORM_WINDOW_CLASS_NAME.as_ptr()), + ..Default::default() + }; + unsafe { RegisterClassW(&wc) }; +} + +unsafe extern "system" fn window_procedure( + hwnd: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, +) -> LRESULT { + if msg == WM_NCCREATE { + let params = lparam.0 as *const CREATESTRUCTW; + let params = unsafe { &*params }; + let creation_context = params.lpCreateParams as *mut PlatformWindowCreateContext; + let creation_context = unsafe { &mut *creation_context }; + return match WindowsPlatformInner::new(creation_context) { + Ok(inner) => { + let weak = Box::new(Rc::downgrade(&inner)); + unsafe { set_window_long(hwnd, GWLP_USERDATA, Box::into_raw(weak) as isize) }; + creation_context.inner = Some(Ok(inner)); + unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) } + } + Err(error) => { + creation_context.inner = Some(Err(error)); + LRESULT(0) + } + }; + } + + let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak; + if ptr.is_null() { + return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }; + } + let inner = unsafe { &*ptr }; + let result = if let Some(inner) = inner.upgrade() { + inner.handle_msg(hwnd, msg, wparam, lparam) + } else { + unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) } + }; + + if msg == WM_NCDESTROY { + unsafe { set_window_long(hwnd, GWLP_USERDATA, 0) }; + unsafe { drop(Box::from_raw(ptr)) }; + } + + result +} + +#[cfg(test)] +mod tests { + use crate::{ClipboardItem, read_from_clipboard, write_to_clipboard}; + + #[test] + fn test_clipboard() { + let item = ClipboardItem::new_string("你好,我是张小白".to_string()); + write_to_clipboard(item.clone()); + assert_eq!(read_from_clipboard(), Some(item)); + + let item = ClipboardItem::new_string("12345".to_string()); + write_to_clipboard(item.clone()); + assert_eq!(read_from_clipboard(), Some(item)); + + let item = ClipboardItem::new_string_with_json_metadata("abcdef".to_string(), vec![3, 4]); + write_to_clipboard(item.clone()); + assert_eq!(read_from_clipboard(), Some(item)); + } +} diff --git a/third_party/gpui/src/platform/windows/shaders.hlsl b/third_party/gpui/src/platform/windows/shaders.hlsl new file mode 100644 index 0000000..d6168ee --- /dev/null +++ b/third_party/gpui/src/platform/windows/shaders.hlsl @@ -0,0 +1,1182 @@ +#include "alpha_correction.hlsl" + +cbuffer GlobalParams: register(b0) { + float4 gamma_ratios; + float2 global_viewport_size; + float grayscale_enhanced_contrast; + uint _pad; +}; + +Texture2D t_sprite: register(t0); +SamplerState s_sprite: register(s0); + +struct Bounds { + float2 origin; + float2 size; +}; + +struct Corners { + float top_left; + float top_right; + float bottom_right; + float bottom_left; +}; + +struct Edges { + float top; + float right; + float bottom; + float left; +}; + +struct Hsla { + float h; + float s; + float l; + float a; +}; + +struct LinearColorStop { + Hsla color; + float percentage; +}; + +struct Background { + // 0u is Solid + // 1u is LinearGradient + // 2u is PatternSlash + uint tag; + // 0u is sRGB linear color + // 1u is Oklab color + uint color_space; + Hsla solid; + float gradient_angle_or_pattern_height; + LinearColorStop colors[2]; + uint pad; +}; + +struct GradientColor { + float4 solid; + float4 color0; + float4 color1; +}; + +struct AtlasTextureId { + uint index; + uint kind; +}; + +struct AtlasBounds { + int2 origin; + int2 size; +}; + +struct AtlasTile { + AtlasTextureId texture_id; + uint tile_id; + uint padding; + AtlasBounds bounds; +}; + +struct TransformationMatrix { + float2x2 rotation_scale; + float2 translation; +}; + +static const float M_PI_F = 3.141592653f; +static const float3 GRAYSCALE_FACTORS = float3(0.2126f, 0.7152f, 0.0722f); + +float4 to_device_position_impl(float2 position) { + float2 device_position = position / global_viewport_size * float2(2.0, -2.0) + float2(-1.0, 1.0); + return float4(device_position, 0., 1.); +} + +float4 to_device_position(float2 unit_vertex, Bounds bounds) { + float2 position = unit_vertex * bounds.size + bounds.origin; + return to_device_position_impl(position); +} + +float4 distance_from_clip_rect_impl(float2 position, Bounds clip_bounds) { + float2 tl = position - clip_bounds.origin; + float2 br = clip_bounds.origin + clip_bounds.size - position; + return float4(tl.x, br.x, tl.y, br.y); +} + +float4 distance_from_clip_rect(float2 unit_vertex, Bounds bounds, Bounds clip_bounds) { + float2 position = unit_vertex * bounds.size + bounds.origin; + return distance_from_clip_rect_impl(position, clip_bounds); +} + +float4 distance_from_clip_rect_transformed(float2 unit_vertex, Bounds bounds, Bounds clip_bounds, TransformationMatrix transformation) { + float2 position = unit_vertex * bounds.size + bounds.origin; + float2 transformed = mul(position, transformation.rotation_scale) + transformation.translation; + return distance_from_clip_rect_impl(transformed, clip_bounds); +} + +// Convert linear RGB to sRGB +float3 linear_to_srgb(float3 color) { + return pow(color, float3(2.2, 2.2, 2.2)); +} + +// Convert sRGB to linear RGB +float3 srgb_to_linear(float3 color) { + return pow(color, float3(1.0 / 2.2, 1.0 / 2.2, 1.0 / 2.2)); +} + +/// Hsla to linear RGBA conversion. +float4 hsla_to_rgba(Hsla hsla) { + float h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range + float s = hsla.s; + float l = hsla.l; + float a = hsla.a; + + float c = (1.0 - abs(2.0 * l - 1.0)) * s; + float x = c * (1.0 - abs(fmod(h, 2.0) - 1.0)); + float m = l - c / 2.0; + + float r = 0.0; + float g = 0.0; + float b = 0.0; + + if (h >= 0.0 && h < 1.0) { + r = c; + g = x; + b = 0.0; + } else if (h >= 1.0 && h < 2.0) { + r = x; + g = c; + b = 0.0; + } else if (h >= 2.0 && h < 3.0) { + r = 0.0; + g = c; + b = x; + } else if (h >= 3.0 && h < 4.0) { + r = 0.0; + g = x; + b = c; + } else if (h >= 4.0 && h < 5.0) { + r = x; + g = 0.0; + b = c; + } else { + r = c; + g = 0.0; + b = x; + } + + float4 rgba; + rgba.x = (r + m); + rgba.y = (g + m); + rgba.z = (b + m); + rgba.w = a; + return rgba; +} + +// Converts a sRGB color to the Oklab color space. +// Reference: https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab +float4 srgb_to_oklab(float4 color) { + // Convert non-linear sRGB to linear sRGB + color = float4(srgb_to_linear(color.rgb), color.a); + + float l = 0.4122214708 * color.r + 0.5363325363 * color.g + 0.0514459929 * color.b; + float m = 0.2119034982 * color.r + 0.6806995451 * color.g + 0.1073969566 * color.b; + float s = 0.0883024619 * color.r + 0.2817188376 * color.g + 0.6299787005 * color.b; + + float l_ = pow(l, 1.0/3.0); + float m_ = pow(m, 1.0/3.0); + float s_ = pow(s, 1.0/3.0); + + return float4( + 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_, + 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_, + 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_, + color.a + ); +} + +// Converts an Oklab color to the sRGB color space. +float4 oklab_to_srgb(float4 color) { + float l_ = color.r + 0.3963377774 * color.g + 0.2158037573 * color.b; + float m_ = color.r - 0.1055613458 * color.g - 0.0638541728 * color.b; + float s_ = color.r - 0.0894841775 * color.g - 1.2914855480 * color.b; + + float l = l_ * l_ * l_; + float m = m_ * m_ * m_; + float s = s_ * s_ * s_; + + float3 linear_rgb = float3( + 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, + -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, + -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s + ); + + // Convert linear sRGB to non-linear sRGB + return float4(linear_to_srgb(linear_rgb), color.a); +} + +// This approximates the error function, needed for the gaussian integral +float2 erf(float2 x) { + float2 s = sign(x); + float2 a = abs(x); + x = 1. + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a; + x *= x; + return s - s / (x * x); +} + +float blur_along_x(float x, float y, float sigma, float corner, float2 half_size) { + float delta = min(half_size.y - corner - abs(y), 0.); + float curved = half_size.x - corner + sqrt(max(0., corner * corner - delta * delta)); + float2 integral = 0.5 + 0.5 * erf((x + float2(-curved, curved)) * (sqrt(0.5) / sigma)); + return integral.y - integral.x; +} + +// A standard gaussian function, used for weighting samples +float gaussian(float x, float sigma) { + return exp(-(x * x) / (2. * sigma * sigma)) / (sqrt(2. * M_PI_F) * sigma); +} + +float4 over(float4 below, float4 above) { + float4 result; + float alpha = above.a + below.a * (1.0 - above.a); + result.rgb = (above.rgb * above.a + below.rgb * below.a * (1.0 - above.a)) / alpha; + result.a = alpha; + return result; +} + +float2 to_tile_position(float2 unit_vertex, AtlasTile tile) { + float2 atlas_size; + t_sprite.GetDimensions(atlas_size.x, atlas_size.y); + return (float2(tile.bounds.origin) + unit_vertex * float2(tile.bounds.size)) / atlas_size; +} + +// Selects corner radius based on quadrant. +float pick_corner_radius(float2 center_to_point, Corners corner_radii) { + if (center_to_point.x < 0.) { + if (center_to_point.y < 0.) { + return corner_radii.top_left; + } else { + return corner_radii.bottom_left; + } + } else { + if (center_to_point.y < 0.) { + return corner_radii.top_right; + } else { + return corner_radii.bottom_right; + } + } +} + +float4 to_device_position_transformed(float2 unit_vertex, Bounds bounds, + TransformationMatrix transformation) { + float2 position = unit_vertex * bounds.size + bounds.origin; + float2 transformed = mul(position, transformation.rotation_scale) + transformation.translation; + float2 device_position = transformed / global_viewport_size * float2(2.0, -2.0) + float2(-1.0, 1.0); + return float4(device_position, 0.0, 1.0); +} + +// Implementation of quad signed distance field +float quad_sdf_impl(float2 corner_center_to_point, float corner_radius) { + if (corner_radius == 0.0) { + // Fast path for unrounded corners + return max(corner_center_to_point.x, corner_center_to_point.y); + } else { + // Signed distance of the point from a quad that is inset by corner_radius + // It is negative inside this quad, and positive outside + float signed_distance_to_inset_quad = + // 0 inside the inset quad, and positive outside + length(max(float2(0.0, 0.0), corner_center_to_point)) + + // 0 outside the inset quad, and negative inside + min(0.0, max(corner_center_to_point.x, corner_center_to_point.y)); + + return signed_distance_to_inset_quad - corner_radius; + } +} + +float quad_sdf(float2 pt, Bounds bounds, Corners corner_radii) { + float2 half_size = bounds.size / 2.; + float2 center = bounds.origin + half_size; + float2 center_to_point = pt - center; + float corner_radius = pick_corner_radius(center_to_point, corner_radii); + float2 corner_to_point = abs(center_to_point) - half_size; + float2 corner_center_to_point = corner_to_point + corner_radius; + return quad_sdf_impl(corner_center_to_point, corner_radius); +} + +GradientColor prepare_gradient_color(uint tag, uint color_space, Hsla solid, LinearColorStop colors[2]) { + GradientColor output; + if (tag == 0 || tag == 2) { + output.solid = hsla_to_rgba(solid); + } else if (tag == 1) { + output.color0 = hsla_to_rgba(colors[0].color); + output.color1 = hsla_to_rgba(colors[1].color); + + // Prepare color space in vertex for avoid conversion + // in fragment shader for performance reasons + if (color_space == 1) { + // Oklab + output.color0 = srgb_to_oklab(output.color0); + output.color1 = srgb_to_oklab(output.color1); + } + } + + return output; +} + +float2x2 rotate2d(float angle) { + float s = sin(angle); + float c = cos(angle); + return float2x2(c, -s, s, c); +} + +float4 gradient_color(Background background, + float2 position, + Bounds bounds, + float4 solid_color, float4 color0, float4 color1) { + float4 color; + + switch (background.tag) { + case 0: + color = solid_color; + break; + case 1: { + // -90 degrees to match the CSS gradient angle. + float gradient_angle = background.gradient_angle_or_pattern_height; + float radians = (fmod(gradient_angle, 360.0) - 90.0) * (M_PI_F / 180.0); + float2 direction = float2(cos(radians), sin(radians)); + + // Expand the short side to be the same as the long side + if (bounds.size.x > bounds.size.y) { + direction.y *= bounds.size.y / bounds.size.x; + } else { + direction.x *= bounds.size.x / bounds.size.y; + } + + // Get the t value for the linear gradient with the color stop percentages. + float2 half_size = bounds.size * 0.5; + float2 center = bounds.origin + half_size; + float2 center_to_point = position - center; + float t = dot(center_to_point, direction) / length(direction); + // Check the direct to determine the use x or y + if (abs(direction.x) > abs(direction.y)) { + t = (t + half_size.x) / bounds.size.x; + } else { + t = (t + half_size.y) / bounds.size.y; + } + + // Adjust t based on the stop percentages + t = (t - background.colors[0].percentage) + / (background.colors[1].percentage + - background.colors[0].percentage); + t = clamp(t, 0.0, 1.0); + + switch (background.color_space) { + case 0: + color = lerp(color0, color1, t); + break; + case 1: { + float4 oklab_color = lerp(color0, color1, t); + color = oklab_to_srgb(oklab_color); + break; + } + } + break; + } + case 2: { + float gradient_angle_or_pattern_height = background.gradient_angle_or_pattern_height; + float pattern_width = (gradient_angle_or_pattern_height / 65535.0f) / 255.0f; + float pattern_interval = fmod(gradient_angle_or_pattern_height, 65535.0f) / 255.0f; + float pattern_height = pattern_width + pattern_interval; + float stripe_angle = M_PI_F / 4.0; + float pattern_period = pattern_height * sin(stripe_angle); + float2x2 rotation = rotate2d(stripe_angle); + float2 relative_position = position - bounds.origin; + float2 rotated_point = mul(relative_position, rotation); + float pattern = fmod(rotated_point.x, pattern_period); + float distance = min(pattern, pattern_period - pattern) - pattern_period * (pattern_width / pattern_height) / 2.0f; + color = solid_color; + color.a *= saturate(0.5 - distance); + break; + } + } + + return color; +} + +// Returns the dash velocity of a corner given the dash velocity of the two +// sides, by returning the slower velocity (larger dashes). +// +// Since 0 is used for dash velocity when the border width is 0 (instead of +// +inf), this returns the other dash velocity in that case. +// +// An alternative to this might be to appropriately interpolate the dash +// velocity around the corner, but that seems overcomplicated. +float corner_dash_velocity(float dv1, float dv2) { + if (dv1 == 0.0) { + return dv2; + } else if (dv2 == 0.0) { + return dv1; + } else { + return min(dv1, dv2); + } +} + +// Returns alpha used to render antialiased dashes. +// `t` is within the dash when `fmod(t, period) < length`. +float dash_alpha( + float t, float period, float length, float dash_velocity, + float antialias_threshold +) { + float half_period = period / 2.0; + float half_length = length / 2.0; + // Value in [-half_period, half_period] + // The dash is in [-half_length, half_length] + float centered = fmod(t + half_period - half_length, period) - half_period; + // Signed distance for the dash, negative values are inside the dash + float signed_distance = abs(centered) - half_length; + // Antialiased alpha based on the signed distance + return saturate(antialias_threshold - signed_distance / dash_velocity); +} + +// This approximates distance to the nearest point to a quarter ellipse in a way +// that is sufficient for anti-aliasing when the ellipse is not very eccentric. +// The components of `point` are expected to be positive. +// +// Negative on the outside and positive on the inside. +float quarter_ellipse_sdf(float2 pt, float2 radii) { + // Scale the space to treat the ellipse like a unit circle + float2 circle_vec = pt / radii; + float unit_circle_sdf = length(circle_vec) - 1.0; + // Approximate up-scaling of the length by using the average of the radii. + // + // TODO: A better solution would be to use the gradient of the implicit + // function for an ellipse to approximate a scaling factor. + return unit_circle_sdf * (radii.x + radii.y) * -0.5; +} + +/* +** +** Quads +** +*/ + +struct Quad { + uint order; + uint border_style; + Bounds bounds; + Bounds content_mask; + Background background; + Hsla border_color; + Corners corner_radii; + Edges border_widths; +}; + +struct QuadVertexOutput { + nointerpolation uint quad_id: TEXCOORD0; + float4 position: SV_Position; + nointerpolation float4 border_color: COLOR0; + nointerpolation float4 background_solid: COLOR1; + nointerpolation float4 background_color0: COLOR2; + nointerpolation float4 background_color1: COLOR3; + float4 clip_distance: SV_ClipDistance; +}; + +struct QuadFragmentInput { + nointerpolation uint quad_id: TEXCOORD0; + float4 position: SV_Position; + nointerpolation float4 border_color: COLOR0; + nointerpolation float4 background_solid: COLOR1; + nointerpolation float4 background_color0: COLOR2; + nointerpolation float4 background_color1: COLOR3; +}; + +StructuredBuffer quads: register(t1); + +QuadVertexOutput quad_vertex(uint vertex_id: SV_VertexID, uint quad_id: SV_InstanceID) { + float2 unit_vertex = float2(float(vertex_id & 1u), 0.5 * float(vertex_id & 2u)); + Quad quad = quads[quad_id]; + float4 device_position = to_device_position(unit_vertex, quad.bounds); + + GradientColor gradient = prepare_gradient_color( + quad.background.tag, + quad.background.color_space, + quad.background.solid, + quad.background.colors + ); + float4 clip_distance = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask); + float4 border_color = hsla_to_rgba(quad.border_color); + + QuadVertexOutput output; + output.position = device_position; + output.border_color = border_color; + output.quad_id = quad_id; + output.background_solid = gradient.solid; + output.background_color0 = gradient.color0; + output.background_color1 = gradient.color1; + output.clip_distance = clip_distance; + return output; +} + +float4 quad_fragment(QuadFragmentInput input): SV_Target { + Quad quad = quads[input.quad_id]; + float4 background_color = gradient_color(quad.background, input.position.xy, quad.bounds, + input.background_solid, input.background_color0, input.background_color1); + + bool unrounded = quad.corner_radii.top_left == 0.0 && + quad.corner_radii.top_right == 0.0 && + quad.corner_radii.bottom_left == 0.0 && + quad.corner_radii.bottom_right == 0.0; + + // Fast path when the quad is not rounded and doesn't have any border + if (quad.border_widths.top == 0.0 && + quad.border_widths.left == 0.0 && + quad.border_widths.right == 0.0 && + quad.border_widths.bottom == 0.0 && + unrounded) { + return background_color; + } + + float2 size = quad.bounds.size; + float2 half_size = size / 2.; + float2 the_point = input.position.xy - quad.bounds.origin; + float2 center_to_point = the_point - half_size; + + // Signed distance field threshold for inclusion of pixels. 0.5 is the + // minimum distance between the center of the pixel and the edge. + const float antialias_threshold = 0.5; + + // Radius of the nearest corner + float corner_radius = pick_corner_radius(center_to_point, quad.corner_radii); + + float2 border = float2( + center_to_point.x < 0.0 ? quad.border_widths.left : quad.border_widths.right, + center_to_point.y < 0.0 ? quad.border_widths.top : quad.border_widths.bottom + ); + + // 0-width borders are reduced so that `inner_sdf >= antialias_threshold`. + // The purpose of this is to not draw antialiasing pixels in this case. + float2 reduced_border = float2( + border.x == 0.0 ? -antialias_threshold : border.x, + border.y == 0.0 ? -antialias_threshold : border.y + ); + + // Vector from the corner of the quad bounds to the point, after mirroring + // the point into the bottom right quadrant. Both components are <= 0. + float2 corner_to_point = abs(center_to_point) - half_size; + + // Vector from the point to the center of the rounded corner's circle, also + // mirrored into bottom right quadrant. + float2 corner_center_to_point = corner_to_point + corner_radius; + + // Whether the nearest point on the border is rounded + bool is_near_rounded_corner = + corner_center_to_point.x >= 0.0 && + corner_center_to_point.y >= 0.0; + + // Vector from straight border inner corner to point. + // + // 0-width borders are turned into width -1 so that inner_sdf is > 1.0 near + // the border. Without this, antialiasing pixels would be drawn. + float2 straight_border_inner_corner_to_point = corner_to_point + reduced_border; + + // Whether the point is beyond the inner edge of the straight border + bool is_beyond_inner_straight_border = + straight_border_inner_corner_to_point.x > 0.0 || + straight_border_inner_corner_to_point.y > 0.0; + + // Whether the point is far enough inside the quad, such that the pixels are + // not affected by the straight border. + bool is_within_inner_straight_border = + straight_border_inner_corner_to_point.x < -antialias_threshold && + straight_border_inner_corner_to_point.y < -antialias_threshold; + + // Fast path for points that must be part of the background + if (is_within_inner_straight_border && !is_near_rounded_corner) { + return background_color; + } + + // Signed distance of the point to the outside edge of the quad's border + float outer_sdf = quad_sdf_impl(corner_center_to_point, corner_radius); + + // Approximate signed distance of the point to the inside edge of the quad's + // border. It is negative outside this edge (within the border), and + // positive inside. + // + // This is not always an accurate signed distance: + // * The rounded portions with varying border width use an approximation of + // nearest-point-on-ellipse. + // * When it is quickly known to be outside the edge, -1.0 is used. + float inner_sdf = 0.0; + if (corner_center_to_point.x <= 0.0 || corner_center_to_point.y <= 0.0) { + // Fast paths for straight borders + inner_sdf = -max(straight_border_inner_corner_to_point.x, + straight_border_inner_corner_to_point.y); + } else if (is_beyond_inner_straight_border) { + // Fast path for points that must be outside the inner edge + inner_sdf = -1.0; + } else if (reduced_border.x == reduced_border.y) { + // Fast path for circular inner edge. + inner_sdf = -(outer_sdf + reduced_border.x); + } else { + float2 ellipse_radii = max(float2(0.0, 0.0), float2(corner_radius, corner_radius) - reduced_border); + inner_sdf = quarter_ellipse_sdf(corner_center_to_point, ellipse_radii); + } + + // Negative when inside the border + float border_sdf = max(inner_sdf, outer_sdf); + + float4 color = background_color; + if (border_sdf < antialias_threshold) { + float4 border_color = input.border_color; + // Dashed border logic when border_style == 1 + if (quad.border_style == 1) { + // Position along the perimeter in "dash space", where each dash + // period has length 1 + float t = 0.0; + + // Total number of dash periods, so that the dash spacing can be + // adjusted to evenly divide it + float max_t = 0.0; + + // Border width is proportional to dash size. This is the behavior + // used by browsers, but also avoids dashes from different segments + // overlapping when dash size is smaller than the border width. + // + // Dash pattern: (2 * border width) dash, (1 * border width) gap + const float dash_length_per_width = 2.0; + const float dash_gap_per_width = 1.0; + const float dash_period_per_width = dash_length_per_width + dash_gap_per_width; + + // Since the dash size is determined by border width, the density of + // dashes varies. Multiplying a pixel distance by this returns a + // position in dash space - it has units (dash period / pixels). So + // a dash velocity of (1 / 10) is 1 dash every 10 pixels. + float dash_velocity = 0.0; + + // Dividing this by the border width gives the dash velocity + const float dv_numerator = 1.0 / dash_period_per_width; + + if (unrounded) { + // When corners aren't rounded, the dashes are separately laid + // out on each straight line, rather than around the whole + // perimeter. This way each line starts and ends with a dash. + bool is_horizontal = corner_center_to_point.x < corner_center_to_point.y; + // Choosing the right border width for dashed borders. + // TODO: A better solution exists taking a look at the whole file. + // this does not fix single dashed borders at the corners + float2 dashed_border = float2( + max(quad.border_widths.bottom, quad.border_widths.top), + max(quad.border_widths.right, quad.border_widths.left) + ); + float border_width = is_horizontal ? dashed_border.x : dashed_border.y; + dash_velocity = dv_numerator / border_width; + t = is_horizontal ? the_point.x : the_point.y; + t *= dash_velocity; + max_t = is_horizontal ? size.x : size.y; + max_t *= dash_velocity; + } else { + // When corners are rounded, the dashes are laid out clockwise + // around the whole perimeter. + + float r_tr = quad.corner_radii.top_right; + float r_br = quad.corner_radii.bottom_right; + float r_bl = quad.corner_radii.bottom_left; + float r_tl = quad.corner_radii.top_left; + + float w_t = quad.border_widths.top; + float w_r = quad.border_widths.right; + float w_b = quad.border_widths.bottom; + float w_l = quad.border_widths.left; + + // Straight side dash velocities + float dv_t = w_t <= 0.0 ? 0.0 : dv_numerator / w_t; + float dv_r = w_r <= 0.0 ? 0.0 : dv_numerator / w_r; + float dv_b = w_b <= 0.0 ? 0.0 : dv_numerator / w_b; + float dv_l = w_l <= 0.0 ? 0.0 : dv_numerator / w_l; + + // Straight side lengths in dash space + float s_t = (size.x - r_tl - r_tr) * dv_t; + float s_r = (size.y - r_tr - r_br) * dv_r; + float s_b = (size.x - r_br - r_bl) * dv_b; + float s_l = (size.y - r_bl - r_tl) * dv_l; + + float corner_dash_velocity_tr = corner_dash_velocity(dv_t, dv_r); + float corner_dash_velocity_br = corner_dash_velocity(dv_b, dv_r); + float corner_dash_velocity_bl = corner_dash_velocity(dv_b, dv_l); + float corner_dash_velocity_tl = corner_dash_velocity(dv_t, dv_l); + + // Corner lengths in dash space + float c_tr = r_tr * (M_PI_F / 2.0) * corner_dash_velocity_tr; + float c_br = r_br * (M_PI_F / 2.0) * corner_dash_velocity_br; + float c_bl = r_bl * (M_PI_F / 2.0) * corner_dash_velocity_bl; + float c_tl = r_tl * (M_PI_F / 2.0) * corner_dash_velocity_tl; + + // Cumulative dash space upto each segment + float upto_tr = s_t; + float upto_r = upto_tr + c_tr; + float upto_br = upto_r + s_r; + float upto_b = upto_br + c_br; + float upto_bl = upto_b + s_b; + float upto_l = upto_bl + c_bl; + float upto_tl = upto_l + s_l; + max_t = upto_tl + c_tl; + + if (is_near_rounded_corner) { + float radians = atan2(corner_center_to_point.y, corner_center_to_point.x); + float corner_t = radians * corner_radius; + + if (center_to_point.x >= 0.0) { + if (center_to_point.y < 0.0) { + dash_velocity = corner_dash_velocity_tr; + // Subtracted because radians is pi/2 to 0 when + // going clockwise around the top right corner, + // since the y axis has been flipped + t = upto_r - corner_t * dash_velocity; + } else { + dash_velocity = corner_dash_velocity_br; + // Added because radians is 0 to pi/2 when going + // clockwise around the bottom-right corner + t = upto_br + corner_t * dash_velocity; + } + } else { + if (center_to_point.y >= 0.0) { + dash_velocity = corner_dash_velocity_bl; + // Subtracted because radians is pi/1 to 0 when + // going clockwise around the bottom-left corner, + // since the x axis has been flipped + t = upto_l - corner_t * dash_velocity; + } else { + dash_velocity = corner_dash_velocity_tl; + // Added because radians is 0 to pi/2 when going + // clockwise around the top-left corner, since both + // axis were flipped + t = upto_tl + corner_t * dash_velocity; + } + } + } else { + // Straight borders + bool is_horizontal = corner_center_to_point.x < corner_center_to_point.y; + if (is_horizontal) { + if (center_to_point.y < 0.0) { + dash_velocity = dv_t; + t = (the_point.x - r_tl) * dash_velocity; + } else { + dash_velocity = dv_b; + t = upto_bl - (the_point.x - r_bl) * dash_velocity; + } + } else { + if (center_to_point.x < 0.0) { + dash_velocity = dv_l; + t = upto_tl - (the_point.y - r_tl) * dash_velocity; + } else { + dash_velocity = dv_r; + t = upto_r + (the_point.y - r_tr) * dash_velocity; + } + } + } + } + float dash_length = dash_length_per_width / dash_period_per_width; + float desired_dash_gap = dash_gap_per_width / dash_period_per_width; + + // Straight borders should start and end with a dash, so max_t is + // reduced to cause this. + max_t -= unrounded ? dash_length : 0.0; + if (max_t >= 1.0) { + // Adjust dash gap to evenly divide max_t + float dash_count = floor(max_t); + float dash_period = max_t / dash_count; + border_color.a *= dash_alpha(t, dash_period, dash_length, dash_velocity, antialias_threshold); + } else if (unrounded) { + // When there isn't enough space for the full gap between the + // two start / end dashes of a straight border, reduce gap to + // make them fit. + float dash_gap = max_t - dash_length; + if (dash_gap > 0.0) { + float dash_period = dash_length + dash_gap; + border_color.a *= dash_alpha(t, dash_period, dash_length, dash_velocity, antialias_threshold); + } + } + } + + // Blend the border on top of the background and then linearly interpolate + // between the two as we slide inside the background. + float4 blended_border = over(background_color, border_color); + color = lerp(background_color, blended_border, + saturate(antialias_threshold - inner_sdf)); + } + + return color * float4(1.0, 1.0, 1.0, saturate(antialias_threshold - outer_sdf)); +} + +/* +** +** Shadows +** +*/ + +struct Shadow { + uint order; + float blur_radius; + Bounds bounds; + Corners corner_radii; + Bounds content_mask; + Hsla color; +}; + +struct ShadowVertexOutput { + nointerpolation uint shadow_id: TEXCOORD0; + float4 position: SV_Position; + nointerpolation float4 color: COLOR; + float4 clip_distance: SV_ClipDistance; +}; + +struct ShadowFragmentInput { + nointerpolation uint shadow_id: TEXCOORD0; + float4 position: SV_Position; + nointerpolation float4 color: COLOR; +}; + +StructuredBuffer shadows: register(t1); + +ShadowVertexOutput shadow_vertex(uint vertex_id: SV_VertexID, uint shadow_id: SV_InstanceID) { + float2 unit_vertex = float2(float(vertex_id & 1u), 0.5 * float(vertex_id & 2u)); + Shadow shadow = shadows[shadow_id]; + + float margin = 3.0 * shadow.blur_radius; + Bounds bounds = shadow.bounds; + bounds.origin -= margin; + bounds.size += 2.0 * margin; + + float4 device_position = to_device_position(unit_vertex, bounds); + float4 clip_distance = distance_from_clip_rect(unit_vertex, bounds, shadow.content_mask); + float4 color = hsla_to_rgba(shadow.color); + + ShadowVertexOutput output; + output.position = device_position; + output.color = color; + output.shadow_id = shadow_id; + output.clip_distance = clip_distance; + + return output; +} + +float4 shadow_fragment(ShadowFragmentInput input): SV_TARGET { + Shadow shadow = shadows[input.shadow_id]; + + float2 half_size = shadow.bounds.size / 2.; + float2 center = shadow.bounds.origin + half_size; + float2 point0 = input.position.xy - center; + float corner_radius = pick_corner_radius(point0, shadow.corner_radii); + + // The signal is only non-zero in a limited range, so don't waste samples + float low = point0.y - half_size.y; + float high = point0.y + half_size.y; + float start = clamp(-3. * shadow.blur_radius, low, high); + float end = clamp(3. * shadow.blur_radius, low, high); + + // Accumulate samples (we can get away with surprisingly few samples) + float step = (end - start) / 4.; + float y = start + step * 0.5; + float alpha = 0.; + for (int i = 0; i < 4; i++) { + alpha += blur_along_x(point0.x, point0.y - y, shadow.blur_radius, + corner_radius, half_size) * + gaussian(y, shadow.blur_radius) * step; + y += step; + } + + return input.color * float4(1., 1., 1., alpha); +} + +/* +** +** Path Rasterization +** +*/ + +struct PathRasterizationSprite { + float2 xy_position; + float2 st_position; + Background color; + Bounds bounds; +}; + +StructuredBuffer path_rasterization_sprites: register(t1); + +struct PathVertexOutput { + float4 position: SV_Position; + float2 st_position: TEXCOORD0; + nointerpolation uint vertex_id: TEXCOORD1; + float4 clip_distance: SV_ClipDistance; +}; + +struct PathFragmentInput { + float4 position: SV_Position; + float2 st_position: TEXCOORD0; + nointerpolation uint vertex_id: TEXCOORD1; +}; + +PathVertexOutput path_rasterization_vertex(uint vertex_id: SV_VertexID) { + PathRasterizationSprite sprite = path_rasterization_sprites[vertex_id]; + + PathVertexOutput output; + output.position = to_device_position_impl(sprite.xy_position); + output.st_position = sprite.st_position; + output.vertex_id = vertex_id; + output.clip_distance = distance_from_clip_rect_impl(sprite.xy_position, sprite.bounds); + + return output; +} + +float4 path_rasterization_fragment(PathFragmentInput input): SV_Target { + float2 dx = ddx(input.st_position); + float2 dy = ddy(input.st_position); + PathRasterizationSprite sprite = path_rasterization_sprites[input.vertex_id]; + + Background background = sprite.color; + Bounds bounds = sprite.bounds; + + float alpha; + if (length(float2(dx.x, dy.x))) { + alpha = 1.0; + } else { + float2 gradient = 2.0 * input.st_position.xx * float2(dx.x, dy.x) - float2(dx.y, dy.y); + float f = input.st_position.x * input.st_position.x - input.st_position.y; + float distance = f / length(gradient); + alpha = saturate(0.5 - distance); + } + + GradientColor gradient = prepare_gradient_color( + background.tag, background.color_space, background.solid, background.colors); + + float4 color = gradient_color(background, input.position.xy, bounds, + gradient.solid, gradient.color0, gradient.color1); + return float4(color.rgb * color.a * alpha, alpha * color.a); +} + +/* +** +** Path Sprites +** +*/ + +struct PathSprite { + Bounds bounds; +}; + +struct PathSpriteVertexOutput { + float4 position: SV_Position; + float2 texture_coords: TEXCOORD0; +}; + +StructuredBuffer path_sprites: register(t1); + +PathSpriteVertexOutput path_sprite_vertex(uint vertex_id: SV_VertexID, uint sprite_id: SV_InstanceID) { + float2 unit_vertex = float2(float(vertex_id & 1u), 0.5 * float(vertex_id & 2u)); + PathSprite sprite = path_sprites[sprite_id]; + + // Don't apply content mask because it was already accounted for when rasterizing the path + float4 device_position = to_device_position(unit_vertex, sprite.bounds); + + float2 screen_position = sprite.bounds.origin + unit_vertex * sprite.bounds.size; + float2 texture_coords = screen_position / global_viewport_size; + + PathSpriteVertexOutput output; + output.position = device_position; + output.texture_coords = texture_coords; + return output; +} + +float4 path_sprite_fragment(PathSpriteVertexOutput input): SV_Target { + return t_sprite.Sample(s_sprite, input.texture_coords); +} + +/* +** +** Underlines +** +*/ + +struct Underline { + uint order; + uint pad; + Bounds bounds; + Bounds content_mask; + Hsla color; + float thickness; + uint wavy; +}; + +struct UnderlineVertexOutput { + nointerpolation uint underline_id: TEXCOORD0; + float4 position: SV_Position; + nointerpolation float4 color: COLOR; + float4 clip_distance: SV_ClipDistance; +}; + +struct UnderlineFragmentInput { + nointerpolation uint underline_id: TEXCOORD0; + float4 position: SV_Position; + nointerpolation float4 color: COLOR; +}; + +StructuredBuffer underlines: register(t1); + +UnderlineVertexOutput underline_vertex(uint vertex_id: SV_VertexID, uint underline_id: SV_InstanceID) { + float2 unit_vertex = float2(float(vertex_id & 1u), 0.5 * float(vertex_id & 2u)); + Underline underline = underlines[underline_id]; + float4 device_position = to_device_position(unit_vertex, underline.bounds); + float4 clip_distance = distance_from_clip_rect(unit_vertex, underline.bounds, + underline.content_mask); + float4 color = hsla_to_rgba(underline.color); + + UnderlineVertexOutput output; + output.position = device_position; + output.color = color; + output.underline_id = underline_id; + output.clip_distance = clip_distance; + return output; +} + +float4 underline_fragment(UnderlineFragmentInput input): SV_Target { + const float WAVE_FREQUENCY = 2.0; + const float WAVE_HEIGHT_RATIO = 0.8; + + Underline underline = underlines[input.underline_id]; + if (underline.wavy) { + float half_thickness = underline.thickness * 0.5; + float2 origin = underline.bounds.origin; + + float2 st = ((input.position.xy - origin) / underline.bounds.size.y) - float2(0., 0.5); + float frequency = (M_PI_F * WAVE_FREQUENCY * underline.thickness) / underline.bounds.size.y; + float amplitude = (underline.thickness * WAVE_HEIGHT_RATIO) / underline.bounds.size.y; + + float sine = sin(st.x * frequency) * amplitude; + float dSine = cos(st.x * frequency) * amplitude * frequency; + float distance = (st.y - sine) / sqrt(1. + dSine * dSine); + float distance_in_pixels = distance * underline.bounds.size.y; + float distance_from_top_border = distance_in_pixels - half_thickness; + float distance_from_bottom_border = distance_in_pixels + half_thickness; + float alpha = saturate( + 0.5 - max(-distance_from_bottom_border, distance_from_top_border)); + return input.color * float4(1., 1., 1., alpha); + } else { + return input.color; + } +} + +/* +** +** Monochrome sprites +** +*/ + +struct MonochromeSprite { + uint order; + uint pad; + Bounds bounds; + Bounds content_mask; + Hsla color; + AtlasTile tile; + TransformationMatrix transformation; +}; + +struct MonochromeSpriteVertexOutput { + float4 position: SV_Position; + float2 tile_position: POSITION; + nointerpolation float4 color: COLOR; + float4 clip_distance: SV_ClipDistance; +}; + +struct MonochromeSpriteFragmentInput { + float4 position: SV_Position; + float2 tile_position: POSITION; + nointerpolation float4 color: COLOR; + float4 clip_distance: SV_ClipDistance; +}; + +StructuredBuffer mono_sprites: register(t1); + +MonochromeSpriteVertexOutput monochrome_sprite_vertex(uint vertex_id: SV_VertexID, uint sprite_id: SV_InstanceID) { + float2 unit_vertex = float2(float(vertex_id & 1u), 0.5 * float(vertex_id & 2u)); + MonochromeSprite sprite = mono_sprites[sprite_id]; + float4 device_position = + to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation); + float4 clip_distance = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation); + float2 tile_position = to_tile_position(unit_vertex, sprite.tile); + float4 color = hsla_to_rgba(sprite.color); + + MonochromeSpriteVertexOutput output; + output.position = device_position; + output.tile_position = tile_position; + output.color = color; + output.clip_distance = clip_distance; + return output; +} + +float4 monochrome_sprite_fragment(MonochromeSpriteFragmentInput input): SV_Target { + float sample = t_sprite.Sample(s_sprite, input.tile_position).r; + float alpha_corrected = apply_contrast_and_gamma_correction(sample, input.color.rgb, grayscale_enhanced_contrast, gamma_ratios); + return float4(input.color.rgb, input.color.a * alpha_corrected); +} + +/* +** +** Polychrome sprites +** +*/ + +struct PolychromeSprite { + uint order; + uint pad; + uint grayscale; + float opacity; + Bounds bounds; + Bounds content_mask; + Corners corner_radii; + AtlasTile tile; +}; + +struct PolychromeSpriteVertexOutput { + nointerpolation uint sprite_id: TEXCOORD0; + float4 position: SV_Position; + float2 tile_position: POSITION; + float4 clip_distance: SV_ClipDistance; +}; + +struct PolychromeSpriteFragmentInput { + nointerpolation uint sprite_id: TEXCOORD0; + float4 position: SV_Position; + float2 tile_position: POSITION; +}; + +StructuredBuffer poly_sprites: register(t1); + +PolychromeSpriteVertexOutput polychrome_sprite_vertex(uint vertex_id: SV_VertexID, uint sprite_id: SV_InstanceID) { + float2 unit_vertex = float2(float(vertex_id & 1u), 0.5 * float(vertex_id & 2u)); + PolychromeSprite sprite = poly_sprites[sprite_id]; + float4 device_position = to_device_position(unit_vertex, sprite.bounds); + float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds, + sprite.content_mask); + float2 tile_position = to_tile_position(unit_vertex, sprite.tile); + + PolychromeSpriteVertexOutput output; + output.position = device_position; + output.tile_position = tile_position; + output.sprite_id = sprite_id; + output.clip_distance = clip_distance; + return output; +} + +float4 polychrome_sprite_fragment(PolychromeSpriteFragmentInput input): SV_Target { + PolychromeSprite sprite = poly_sprites[input.sprite_id]; + float4 sample = t_sprite.Sample(s_sprite, input.tile_position); + float distance = quad_sdf(input.position.xy, sprite.bounds, sprite.corner_radii); + + float4 color = sample; + if ((sprite.grayscale & 0xFFu) != 0u) { + float3 grayscale = dot(color.rgb, GRAYSCALE_FACTORS); + color = float4(grayscale, sample.a); + } + color.a *= sprite.opacity * saturate(0.5 - distance); + return color; +} diff --git a/third_party/gpui/src/platform/windows/system_settings.rs b/third_party/gpui/src/platform/windows/system_settings.rs new file mode 100644 index 0000000..b2bd289 --- /dev/null +++ b/third_party/gpui/src/platform/windows/system_settings.rs @@ -0,0 +1,197 @@ +use std::ffi::{c_uint, c_void}; + +use ::util::ResultExt; +use windows::Win32::UI::{ + Shell::{ABM_GETSTATE, ABM_GETTASKBARPOS, ABS_AUTOHIDE, APPBARDATA, SHAppBarMessage}, + WindowsAndMessaging::{ + SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES, SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, + SystemParametersInfoW, + }, +}; + +use crate::*; + +use super::WindowsDisplay; + +/// Windows settings pulled from SystemParametersInfo +/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-systemparametersinfow +#[derive(Default, Debug, Clone, Copy)] +pub(crate) struct WindowsSystemSettings { + pub(crate) mouse_wheel_settings: MouseWheelSettings, + pub(crate) auto_hide_taskbar_position: Option, +} + +#[derive(Default, Debug, Clone, Copy)] +pub(crate) struct MouseWheelSettings { + /// SEE: SPI_GETWHEELSCROLLCHARS + pub(crate) wheel_scroll_chars: u32, + /// SEE: SPI_GETWHEELSCROLLLINES + pub(crate) wheel_scroll_lines: u32, +} + +impl WindowsSystemSettings { + pub(crate) fn new(display: WindowsDisplay) -> Self { + let mut settings = Self::default(); + settings.init(display); + settings + } + + fn init(&mut self, display: WindowsDisplay) { + self.mouse_wheel_settings.update(); + self.auto_hide_taskbar_position = AutoHideTaskbarPosition::new(display).log_err().flatten(); + } + + pub(crate) fn update(&mut self, display: WindowsDisplay, wparam: usize) { + match wparam { + // SPI_SETWORKAREA + 47 => self.update_taskbar_position(display), + // SPI_GETWHEELSCROLLLINES, SPI_GETWHEELSCROLLCHARS + 104 | 108 => self.update_mouse_wheel_settings(), + _ => {} + } + } + + fn update_mouse_wheel_settings(&mut self) { + self.mouse_wheel_settings.update(); + } + + fn update_taskbar_position(&mut self, display: WindowsDisplay) { + self.auto_hide_taskbar_position = AutoHideTaskbarPosition::new(display).log_err().flatten(); + } +} + +impl MouseWheelSettings { + fn update(&mut self) { + self.update_wheel_scroll_chars(); + self.update_wheel_scroll_lines(); + } + + fn update_wheel_scroll_chars(&mut self) { + let mut value = c_uint::default(); + let result = unsafe { + SystemParametersInfoW( + SPI_GETWHEELSCROLLCHARS, + 0, + Some((&mut value) as *mut c_uint as *mut c_void), + SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS::default(), + ) + }; + + if result.log_err() != None && self.wheel_scroll_chars != value { + self.wheel_scroll_chars = value; + } + } + + fn update_wheel_scroll_lines(&mut self) { + let mut value = c_uint::default(); + let result = unsafe { + SystemParametersInfoW( + SPI_GETWHEELSCROLLLINES, + 0, + Some((&mut value) as *mut c_uint as *mut c_void), + SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS::default(), + ) + }; + + if result.log_err() != None && self.wheel_scroll_lines != value { + self.wheel_scroll_lines = value; + } + } +} + +#[derive(Debug, Clone, Copy, Default)] +pub(crate) enum AutoHideTaskbarPosition { + Left, + Right, + Top, + #[default] + Bottom, +} + +impl AutoHideTaskbarPosition { + fn new(display: WindowsDisplay) -> anyhow::Result> { + if !check_auto_hide_taskbar_enable() { + // If auto hide taskbar is not enable, we do nothing in this case. + return Ok(None); + } + let mut info = APPBARDATA { + cbSize: std::mem::size_of::() as u32, + ..Default::default() + }; + let ret = unsafe { SHAppBarMessage(ABM_GETTASKBARPOS, &mut info) }; + if ret == 0 { + anyhow::bail!( + "Unable to retrieve taskbar position: {}", + std::io::Error::last_os_error() + ); + } + let taskbar_bounds: Bounds = Bounds::new( + point(info.rc.left.into(), info.rc.top.into()), + size( + (info.rc.right - info.rc.left).into(), + (info.rc.bottom - info.rc.top).into(), + ), + ); + let display_bounds = display.physical_bounds(); + if display_bounds.intersect(&taskbar_bounds) != taskbar_bounds { + // This case indicates that taskbar is not on the current monitor. + return Ok(None); + } + if taskbar_bounds.bottom() == display_bounds.bottom() + && taskbar_bounds.right() == display_bounds.right() + { + if taskbar_bounds.size.height < display_bounds.size.height + && taskbar_bounds.size.width == display_bounds.size.width + { + return Ok(Some(Self::Bottom)); + } + if taskbar_bounds.size.width < display_bounds.size.width + && taskbar_bounds.size.height == display_bounds.size.height + { + return Ok(Some(Self::Right)); + } + log::error!( + "Unrecognized taskbar bounds {:?} give display bounds {:?}", + taskbar_bounds, + display_bounds + ); + return Ok(None); + } + if taskbar_bounds.top() == display_bounds.top() + && taskbar_bounds.left() == display_bounds.left() + { + if taskbar_bounds.size.height < display_bounds.size.height + && taskbar_bounds.size.width == display_bounds.size.width + { + return Ok(Some(Self::Top)); + } + if taskbar_bounds.size.width < display_bounds.size.width + && taskbar_bounds.size.height == display_bounds.size.height + { + return Ok(Some(Self::Left)); + } + log::error!( + "Unrecognized taskbar bounds {:?} give display bounds {:?}", + taskbar_bounds, + display_bounds + ); + return Ok(None); + } + log::error!( + "Unrecognized taskbar bounds {:?} give display bounds {:?}", + taskbar_bounds, + display_bounds + ); + Ok(None) + } +} + +/// Check if auto hide taskbar is enable or not. +fn check_auto_hide_taskbar_enable() -> bool { + let mut info = APPBARDATA { + cbSize: std::mem::size_of::() as u32, + ..Default::default() + }; + let ret = unsafe { SHAppBarMessage(ABM_GETSTATE, &mut info) } as u32; + ret == ABS_AUTOHIDE +} diff --git a/third_party/gpui/src/platform/windows/util.rs b/third_party/gpui/src/platform/windows/util.rs new file mode 100644 index 0000000..af71dfe --- /dev/null +++ b/third_party/gpui/src/platform/windows/util.rs @@ -0,0 +1,219 @@ +use std::sync::OnceLock; + +use ::util::ResultExt; +use anyhow::Context; +use windows::{ + UI::{ + Color, + ViewManagement::{UIColorType, UISettings}, + }, + Wdk::System::SystemServices::RtlGetVersion, + Win32::{ + Foundation::*, Graphics::Dwm::*, System::LibraryLoader::LoadLibraryA, + UI::WindowsAndMessaging::*, + }, + core::{BOOL, HSTRING, PCSTR}, +}; + +use crate::*; + +#[derive(Debug, Clone, Copy)] +pub(crate) enum WindowsVersion { + Win10, + Win11, +} + +impl WindowsVersion { + pub(crate) fn new() -> anyhow::Result { + let mut version = unsafe { std::mem::zeroed() }; + let status = unsafe { RtlGetVersion(&mut version) }; + + status.ok()?; + if version.dwBuildNumber >= 22000 { + Ok(WindowsVersion::Win11) + } else { + Ok(WindowsVersion::Win10) + } + } +} + +pub(crate) trait HiLoWord { + fn hiword(&self) -> u16; + fn loword(&self) -> u16; + fn signed_hiword(&self) -> i16; + fn signed_loword(&self) -> i16; +} + +impl HiLoWord for WPARAM { + fn hiword(&self) -> u16 { + ((self.0 >> 16) & 0xFFFF) as u16 + } + + fn loword(&self) -> u16 { + (self.0 & 0xFFFF) as u16 + } + + fn signed_hiword(&self) -> i16 { + ((self.0 >> 16) & 0xFFFF) as i16 + } + + fn signed_loword(&self) -> i16 { + (self.0 & 0xFFFF) as i16 + } +} + +impl HiLoWord for LPARAM { + fn hiword(&self) -> u16 { + ((self.0 >> 16) & 0xFFFF) as u16 + } + + fn loword(&self) -> u16 { + (self.0 & 0xFFFF) as u16 + } + + fn signed_hiword(&self) -> i16 { + ((self.0 >> 16) & 0xFFFF) as i16 + } + + fn signed_loword(&self) -> i16 { + (self.0 & 0xFFFF) as i16 + } +} + +pub(crate) unsafe fn get_window_long(hwnd: HWND, nindex: WINDOW_LONG_PTR_INDEX) -> isize { + #[cfg(target_pointer_width = "64")] + unsafe { + GetWindowLongPtrW(hwnd, nindex) + } + #[cfg(target_pointer_width = "32")] + unsafe { + GetWindowLongW(hwnd, nindex) as isize + } +} + +pub(crate) unsafe fn set_window_long( + hwnd: HWND, + nindex: WINDOW_LONG_PTR_INDEX, + dwnewlong: isize, +) -> isize { + #[cfg(target_pointer_width = "64")] + unsafe { + SetWindowLongPtrW(hwnd, nindex, dwnewlong) + } + #[cfg(target_pointer_width = "32")] + unsafe { + SetWindowLongW(hwnd, nindex, dwnewlong as i32) as isize + } +} + +pub(crate) fn windows_credentials_target_name(url: &str) -> String { + format!("zed:url={}", url) +} + +pub(crate) fn load_cursor(style: CursorStyle) -> Option { + static ARROW: OnceLock = OnceLock::new(); + static IBEAM: OnceLock = OnceLock::new(); + static CROSS: OnceLock = OnceLock::new(); + static HAND: OnceLock = OnceLock::new(); + static SIZEWE: OnceLock = OnceLock::new(); + static SIZENS: OnceLock = OnceLock::new(); + static NO: OnceLock = OnceLock::new(); + let (lock, name) = match style { + CursorStyle::IBeam | CursorStyle::IBeamCursorForVerticalLayout => (&IBEAM, IDC_IBEAM), + CursorStyle::Crosshair => (&CROSS, IDC_CROSS), + CursorStyle::PointingHand | CursorStyle::DragLink => (&HAND, IDC_HAND), + CursorStyle::ResizeLeft + | CursorStyle::ResizeRight + | CursorStyle::ResizeLeftRight + | CursorStyle::ResizeColumn => (&SIZEWE, IDC_SIZEWE), + CursorStyle::ResizeUp + | CursorStyle::ResizeDown + | CursorStyle::ResizeUpDown + | CursorStyle::ResizeRow => (&SIZENS, IDC_SIZENS), + CursorStyle::OperationNotAllowed => (&NO, IDC_NO), + CursorStyle::None => return None, + _ => (&ARROW, IDC_ARROW), + }; + Some( + *(*lock.get_or_init(|| { + HCURSOR( + unsafe { LoadImageW(None, name, IMAGE_CURSOR, 0, 0, LR_DEFAULTSIZE | LR_SHARED) } + .log_err() + .unwrap_or_default() + .0, + ) + .into() + })), + ) +} + +/// This function is used to configure the dark mode for the window built-in title bar. +pub(crate) fn configure_dwm_dark_mode(hwnd: HWND, appearance: WindowAppearance) { + let dark_mode_enabled: BOOL = match appearance { + WindowAppearance::Dark | WindowAppearance::VibrantDark => true.into(), + WindowAppearance::Light | WindowAppearance::VibrantLight => false.into(), + }; + unsafe { + DwmSetWindowAttribute( + hwnd, + DWMWA_USE_IMMERSIVE_DARK_MODE, + &dark_mode_enabled as *const _ as _, + std::mem::size_of::() as u32, + ) + .log_err(); + } +} + +#[inline] +pub(crate) fn logical_point(x: f32, y: f32, scale_factor: f32) -> Point { + Point { + x: px(x / scale_factor), + y: px(y / scale_factor), + } +} + +// https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/apply-windows-themes +#[inline] +pub(crate) fn system_appearance() -> Result { + let ui_settings = UISettings::new()?; + let foreground_color = ui_settings.GetColorValue(UIColorType::Foreground)?; + // If the foreground is light, then is_color_light will evaluate to true, + // meaning Dark mode is enabled. + if is_color_light(&foreground_color) { + Ok(WindowAppearance::Dark) + } else { + Ok(WindowAppearance::Light) + } +} + +#[inline(always)] +fn is_color_light(color: &Color) -> bool { + ((5 * color.G as u32) + (2 * color.R as u32) + color.B as u32) > (8 * 128) +} + +pub(crate) fn show_error(title: &str, content: String) { + let _ = unsafe { + MessageBoxW( + None, + &HSTRING::from(content), + &HSTRING::from(title), + MB_ICONERROR | MB_SYSTEMMODAL, + ) + }; +} + +pub(crate) fn with_dll_library(dll_name: PCSTR, f: F) -> Result +where + F: FnOnce(HMODULE) -> Result, +{ + let library = unsafe { + LoadLibraryA(dll_name).with_context(|| format!("Loading dll: {}", dll_name.display()))? + }; + let result = f(library); + unsafe { + FreeLibrary(library) + .with_context(|| format!("Freeing dll: {}", dll_name.display())) + .log_err(); + } + result +} diff --git a/third_party/gpui/src/platform/windows/vsync.rs b/third_party/gpui/src/platform/windows/vsync.rs new file mode 100644 index 0000000..73c32cf --- /dev/null +++ b/third_party/gpui/src/platform/windows/vsync.rs @@ -0,0 +1,81 @@ +use std::{ + sync::LazyLock, + time::{Duration, Instant}, +}; + +use anyhow::{Context, Result}; +use util::ResultExt; +use windows::Win32::{ + Foundation::HWND, + Graphics::Dwm::{DWM_TIMING_INFO, DwmFlush, DwmGetCompositionTimingInfo}, + System::Performance::QueryPerformanceFrequency, +}; + +static QPC_TICKS_PER_SECOND: LazyLock = LazyLock::new(|| { + let mut frequency = 0; + // On systems that run Windows XP or later, the function will always succeed and + // will thus never return zero. + unsafe { QueryPerformanceFrequency(&mut frequency).unwrap() }; + frequency as u64 +}); + +const VSYNC_INTERVAL_THRESHOLD: Duration = Duration::from_millis(1); +const DEFAULT_VSYNC_INTERVAL: Duration = Duration::from_micros(16_666); // ~60Hz + +pub(crate) struct VSyncProvider { + interval: Duration, + f: Box bool>, +} + +impl VSyncProvider { + pub(crate) fn new() -> Self { + let interval = get_dwm_interval() + .context("Failed to get DWM interval") + .log_err() + .unwrap_or(DEFAULT_VSYNC_INTERVAL); + let f = Box::new(|| unsafe { DwmFlush().is_ok() }); + Self { interval, f } + } + + pub(crate) fn wait_for_vsync(&self) { + let vsync_start = Instant::now(); + let wait_succeeded = (self.f)(); + let elapsed = vsync_start.elapsed(); + // DwmFlush and DCompositionWaitForCompositorClock returns very early + // instead of waiting until vblank when the monitor goes to sleep or is + // unplugged (nothing to present due to desktop occlusion). We use 1ms as + // a threshold for the duration of the wait functions and fallback to + // Sleep() if it returns before that. This could happen during normal + // operation for the first call after the vsync thread becomes non-idle, + // but it shouldn't happen often. + if !wait_succeeded || elapsed < VSYNC_INTERVAL_THRESHOLD { + log::trace!("VSyncProvider::wait_for_vsync() took less time than expected"); + std::thread::sleep(self.interval); + } + } +} + +fn get_dwm_interval() -> Result { + let mut timing_info = DWM_TIMING_INFO { + cbSize: std::mem::size_of::() as u32, + ..Default::default() + }; + unsafe { DwmGetCompositionTimingInfo(HWND::default(), &mut timing_info) }?; + let interval = retrieve_duration(timing_info.qpcRefreshPeriod, *QPC_TICKS_PER_SECOND); + // Check for interval values that are impossibly low. A 29 microsecond + // interval was seen (from a qpcRefreshPeriod of 60). + if interval < VSYNC_INTERVAL_THRESHOLD { + Ok(retrieve_duration( + timing_info.rateRefresh.uiDenominator as u64, + timing_info.rateRefresh.uiNumerator as u64, + )) + } else { + Ok(interval) + } +} + +#[inline] +fn retrieve_duration(counts: u64, ticks_per_second: u64) -> Duration { + let ticks_per_microsecond = ticks_per_second / 1_000_000; + Duration::from_micros(counts / ticks_per_microsecond) +} diff --git a/third_party/gpui/src/platform/windows/window.rs b/third_party/gpui/src/platform/windows/window.rs new file mode 100644 index 0000000..e765fa1 --- /dev/null +++ b/third_party/gpui/src/platform/windows/window.rs @@ -0,0 +1,1413 @@ +#![deny(unsafe_op_in_unsafe_fn)] + +use std::{ + cell::RefCell, + num::NonZeroIsize, + path::PathBuf, + rc::{Rc, Weak}, + str::FromStr, + sync::{Arc, Once}, + time::{Duration, Instant}, +}; + +use ::util::ResultExt; +use anyhow::{Context as _, Result}; +use async_task::Runnable; +use futures::channel::oneshot::{self, Receiver}; +use raw_window_handle as rwh; +use smallvec::SmallVec; +use windows::{ + Win32::{ + Foundation::*, + Graphics::Gdi::*, + System::{Com::*, LibraryLoader::*, Ole::*, SystemServices::*}, + UI::{Controls::*, HiDpi::*, Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*}, + }, + core::*, +}; + +use crate::*; + +pub(crate) struct WindowsWindow(pub Rc); + +pub struct WindowsWindowState { + pub origin: Point, + pub logical_size: Size, + pub min_size: Option>, + pub fullscreen_restore_bounds: Bounds, + pub border_offset: WindowBorderOffset, + pub appearance: WindowAppearance, + pub scale_factor: f32, + pub restore_from_minimized: Option>, + + pub callbacks: Callbacks, + pub input_handler: Option, + pub pending_surrogate: Option, + pub last_reported_modifiers: Option, + pub last_reported_capslock: Option, + pub system_key_handled: bool, + pub hovered: bool, + + pub renderer: DirectXRenderer, + + pub click_state: ClickState, + pub current_cursor: Option, + pub nc_button_pressed: Option, + + pub display: WindowsDisplay, + fullscreen: Option, + initial_placement: Option, + hwnd: HWND, +} + +pub(crate) struct WindowsWindowInner { + hwnd: HWND, + pub(super) this: Weak, + drop_target_helper: IDropTargetHelper, + pub(crate) state: RefCell, + pub(crate) system_settings: RefCell, + pub(crate) handle: AnyWindowHandle, + pub(crate) hide_title_bar: bool, + pub(crate) is_movable: bool, + pub(crate) executor: ForegroundExecutor, + pub(crate) windows_version: WindowsVersion, + pub(crate) validation_number: usize, + pub(crate) main_receiver: flume::Receiver, + pub(crate) platform_window_handle: HWND, +} + +impl WindowsWindowState { + fn new( + hwnd: HWND, + directx_devices: &DirectXDevices, + window_params: &CREATESTRUCTW, + current_cursor: Option, + display: WindowsDisplay, + min_size: Option>, + appearance: WindowAppearance, + disable_direct_composition: bool, + ) -> Result { + let scale_factor = { + let monitor_dpi = unsafe { GetDpiForWindow(hwnd) } as f32; + monitor_dpi / USER_DEFAULT_SCREEN_DPI as f32 + }; + let origin = logical_point(window_params.x as f32, window_params.y as f32, scale_factor); + let logical_size = { + let physical_size = size( + DevicePixels(window_params.cx), + DevicePixels(window_params.cy), + ); + physical_size.to_pixels(scale_factor) + }; + let fullscreen_restore_bounds = Bounds { + origin, + size: logical_size, + }; + let border_offset = WindowBorderOffset::default(); + let restore_from_minimized = None; + let renderer = DirectXRenderer::new(hwnd, directx_devices, disable_direct_composition) + .context("Creating DirectX renderer")?; + let callbacks = Callbacks::default(); + let input_handler = None; + let pending_surrogate = None; + let last_reported_modifiers = None; + let last_reported_capslock = None; + let system_key_handled = false; + let hovered = false; + let click_state = ClickState::new(); + let nc_button_pressed = None; + let fullscreen = None; + let initial_placement = None; + + Ok(Self { + origin, + logical_size, + fullscreen_restore_bounds, + border_offset, + appearance, + scale_factor, + restore_from_minimized, + min_size, + callbacks, + input_handler, + pending_surrogate, + last_reported_modifiers, + last_reported_capslock, + system_key_handled, + hovered, + renderer, + click_state, + current_cursor, + nc_button_pressed, + display, + fullscreen, + initial_placement, + hwnd, + }) + } + + #[inline] + pub(crate) fn is_fullscreen(&self) -> bool { + self.fullscreen.is_some() + } + + pub(crate) fn is_maximized(&self) -> bool { + !self.is_fullscreen() && unsafe { IsZoomed(self.hwnd) }.as_bool() + } + + fn bounds(&self) -> Bounds { + Bounds { + origin: self.origin, + size: self.logical_size, + } + } + + // Calculate the bounds used for saving and whether the window is maximized. + fn calculate_window_bounds(&self) -> (Bounds, bool) { + let placement = unsafe { + let mut placement = WINDOWPLACEMENT { + length: std::mem::size_of::() as u32, + ..Default::default() + }; + GetWindowPlacement(self.hwnd, &mut placement) + .context("failed to get window placement") + .log_err(); + placement + }; + ( + calculate_client_rect( + placement.rcNormalPosition, + self.border_offset, + self.scale_factor, + ), + placement.showCmd == SW_SHOWMAXIMIZED.0 as u32, + ) + } + + fn window_bounds(&self) -> WindowBounds { + let (bounds, maximized) = self.calculate_window_bounds(); + + if self.is_fullscreen() { + WindowBounds::Fullscreen(self.fullscreen_restore_bounds) + } else if maximized { + WindowBounds::Maximized(bounds) + } else { + WindowBounds::Windowed(bounds) + } + } + + /// get the logical size of the app's drawable area. + /// + /// Currently, GPUI uses the logical size of the app to handle mouse interactions (such as + /// whether the mouse collides with other elements of GPUI). + fn content_size(&self) -> Size { + self.logical_size + } +} + +impl WindowsWindowInner { + fn new(context: &mut WindowCreateContext, hwnd: HWND, cs: &CREATESTRUCTW) -> Result> { + let state = RefCell::new(WindowsWindowState::new( + hwnd, + &context.directx_devices, + cs, + context.current_cursor, + context.display, + context.min_size, + context.appearance, + context.disable_direct_composition, + )?); + + Ok(Rc::new_cyclic(|this| Self { + hwnd, + this: this.clone(), + drop_target_helper: context.drop_target_helper.clone(), + state, + handle: context.handle, + hide_title_bar: context.hide_title_bar, + is_movable: context.is_movable, + executor: context.executor.clone(), + windows_version: context.windows_version, + validation_number: context.validation_number, + main_receiver: context.main_receiver.clone(), + platform_window_handle: context.platform_window_handle, + system_settings: RefCell::new(WindowsSystemSettings::new(context.display)), + })) + } + + fn toggle_fullscreen(&self) { + let Some(this) = self.this.upgrade() else { + log::error!("Unable to toggle fullscreen: window has been dropped"); + return; + }; + self.executor + .spawn(async move { + let mut lock = this.state.borrow_mut(); + let StyleAndBounds { + style, + x, + y, + cx, + cy, + } = if let Some(state) = lock.fullscreen.take() { + state + } else { + let (window_bounds, _) = lock.calculate_window_bounds(); + lock.fullscreen_restore_bounds = window_bounds; + let style = WINDOW_STYLE(unsafe { get_window_long(this.hwnd, GWL_STYLE) } as _); + let mut rc = RECT::default(); + unsafe { GetWindowRect(this.hwnd, &mut rc) } + .context("failed to get window rect") + .log_err(); + let _ = lock.fullscreen.insert(StyleAndBounds { + style, + x: rc.left, + y: rc.top, + cx: rc.right - rc.left, + cy: rc.bottom - rc.top, + }); + let style = style + & !(WS_THICKFRAME + | WS_SYSMENU + | WS_MAXIMIZEBOX + | WS_MINIMIZEBOX + | WS_CAPTION); + let physical_bounds = lock.display.physical_bounds(); + StyleAndBounds { + style, + x: physical_bounds.left().0, + y: physical_bounds.top().0, + cx: physical_bounds.size.width.0, + cy: physical_bounds.size.height.0, + } + }; + drop(lock); + unsafe { set_window_long(this.hwnd, GWL_STYLE, style.0 as isize) }; + unsafe { + SetWindowPos( + this.hwnd, + None, + x, + y, + cx, + cy, + SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOZORDER, + ) + } + .log_err(); + }) + .detach(); + } + + fn set_window_placement(&self) -> Result<()> { + let Some(open_status) = self.state.borrow_mut().initial_placement.take() else { + return Ok(()); + }; + match open_status.state { + WindowOpenState::Maximized => unsafe { + SetWindowPlacement(self.hwnd, &open_status.placement) + .context("failed to set window placement")?; + ShowWindowAsync(self.hwnd, SW_MAXIMIZE).ok()?; + }, + WindowOpenState::Fullscreen => { + unsafe { + SetWindowPlacement(self.hwnd, &open_status.placement) + .context("failed to set window placement")? + }; + self.toggle_fullscreen(); + } + WindowOpenState::Windowed => unsafe { + SetWindowPlacement(self.hwnd, &open_status.placement) + .context("failed to set window placement")?; + }, + } + Ok(()) + } +} + +#[derive(Default)] +pub(crate) struct Callbacks { + pub(crate) request_frame: Option>, + pub(crate) input: Option DispatchEventResult>>, + pub(crate) active_status_change: Option>, + pub(crate) hovered_status_change: Option>, + pub(crate) resize: Option, f32)>>, + pub(crate) moved: Option>, + pub(crate) should_close: Option bool>>, + pub(crate) close: Option>, + pub(crate) hit_test_window_control: Option Option>>, + pub(crate) appearance_changed: Option>, +} + +struct WindowCreateContext { + inner: Option>>, + handle: AnyWindowHandle, + hide_title_bar: bool, + display: WindowsDisplay, + is_movable: bool, + min_size: Option>, + executor: ForegroundExecutor, + current_cursor: Option, + windows_version: WindowsVersion, + drop_target_helper: IDropTargetHelper, + validation_number: usize, + main_receiver: flume::Receiver, + platform_window_handle: HWND, + appearance: WindowAppearance, + disable_direct_composition: bool, + directx_devices: DirectXDevices, +} + +impl WindowsWindow { + pub(crate) fn new( + handle: AnyWindowHandle, + params: WindowParams, + creation_info: WindowCreationInfo, + ) -> Result { + let WindowCreationInfo { + icon, + executor, + current_cursor, + windows_version, + drop_target_helper, + validation_number, + main_receiver, + platform_window_handle, + disable_direct_composition, + directx_devices, + } = creation_info; + register_window_class(icon); + let hide_title_bar = params + .titlebar + .as_ref() + .map(|titlebar| titlebar.appears_transparent) + .unwrap_or(true); + let window_name = HSTRING::from( + params + .titlebar + .as_ref() + .and_then(|titlebar| titlebar.title.as_ref()) + .map(|title| title.as_ref()) + .unwrap_or(""), + ); + + let (mut dwexstyle, dwstyle) = if params.kind == WindowKind::PopUp { + (WS_EX_TOOLWINDOW, WINDOW_STYLE(0x0)) + } else { + let mut dwstyle = WS_SYSMENU; + + if params.is_resizable { + dwstyle |= WS_THICKFRAME | WS_MAXIMIZEBOX; + } + + if params.is_minimizable { + dwstyle |= WS_MINIMIZEBOX; + } + + (WS_EX_APPWINDOW, dwstyle) + }; + if !disable_direct_composition { + dwexstyle |= WS_EX_NOREDIRECTIONBITMAP; + } + + let hinstance = get_module_handle(); + let display = if let Some(display_id) = params.display_id { + // if we obtain a display_id, then this ID must be valid. + WindowsDisplay::new(display_id).unwrap() + } else { + WindowsDisplay::primary_monitor().unwrap() + }; + let appearance = system_appearance().unwrap_or_default(); + let mut context = WindowCreateContext { + inner: None, + handle, + hide_title_bar, + display, + is_movable: params.is_movable, + min_size: params.window_min_size, + executor, + current_cursor, + windows_version, + drop_target_helper, + validation_number, + main_receiver, + platform_window_handle, + appearance, + disable_direct_composition, + directx_devices, + }; + let creation_result = unsafe { + CreateWindowExW( + dwexstyle, + WINDOW_CLASS_NAME, + &window_name, + dwstyle, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + None, + None, + Some(hinstance.into()), + Some(&context as *const _ as *const _), + ) + }; + + // Failure to create a `WindowsWindowState` can cause window creation to fail, + // so check the inner result first. + let this = context.inner.take().unwrap()?; + let hwnd = creation_result?; + + register_drag_drop(&this)?; + configure_dwm_dark_mode(hwnd, appearance); + this.state.borrow_mut().border_offset.update(hwnd)?; + let placement = retrieve_window_placement( + hwnd, + display, + params.bounds, + this.state.borrow().scale_factor, + this.state.borrow().border_offset, + )?; + if params.show { + unsafe { SetWindowPlacement(hwnd, &placement)? }; + } else { + this.state.borrow_mut().initial_placement = Some(WindowOpenStatus { + placement, + state: WindowOpenState::Windowed, + }); + } + + Ok(Self(this)) + } +} + +impl rwh::HasWindowHandle for WindowsWindow { + fn window_handle(&self) -> std::result::Result, rwh::HandleError> { + let raw = rwh::Win32WindowHandle::new(unsafe { + NonZeroIsize::new_unchecked(self.0.hwnd.0 as isize) + }) + .into(); + Ok(unsafe { rwh::WindowHandle::borrow_raw(raw) }) + } +} + +// todo(windows) +impl rwh::HasDisplayHandle for WindowsWindow { + fn display_handle(&self) -> std::result::Result, rwh::HandleError> { + unimplemented!() + } +} + +impl Drop for WindowsWindow { + fn drop(&mut self) { + // clone this `Rc` to prevent early release of the pointer + let this = self.0.clone(); + self.0 + .executor + .spawn(async move { + let handle = this.hwnd; + unsafe { + RevokeDragDrop(handle).log_err(); + DestroyWindow(handle).log_err(); + } + }) + .detach(); + } +} + +impl PlatformWindow for WindowsWindow { + fn bounds(&self) -> Bounds { + self.0.state.borrow().bounds() + } + + fn is_maximized(&self) -> bool { + self.0.state.borrow().is_maximized() + } + + fn window_bounds(&self) -> WindowBounds { + self.0.state.borrow().window_bounds() + } + + /// get the logical size of the app's drawable area. + /// + /// Currently, GPUI uses the logical size of the app to handle mouse interactions (such as + /// whether the mouse collides with other elements of GPUI). + fn content_size(&self) -> Size { + self.0.state.borrow().content_size() + } + + fn resize(&mut self, size: Size) { + let hwnd = self.0.hwnd; + let bounds = + crate::bounds(self.bounds().origin, size).to_device_pixels(self.scale_factor()); + let rect = calculate_window_rect(bounds, self.0.state.borrow().border_offset); + + self.0 + .executor + .spawn(async move { + unsafe { + SetWindowPos( + hwnd, + None, + bounds.origin.x.0, + bounds.origin.y.0, + rect.right - rect.left, + rect.bottom - rect.top, + SWP_NOMOVE, + ) + .context("unable to set window content size") + .log_err(); + } + }) + .detach(); + } + + fn scale_factor(&self) -> f32 { + self.0.state.borrow().scale_factor + } + + fn appearance(&self) -> WindowAppearance { + self.0.state.borrow().appearance + } + + fn display(&self) -> Option> { + Some(Rc::new(self.0.state.borrow().display)) + } + + fn mouse_position(&self) -> Point { + let scale_factor = self.scale_factor(); + let point = unsafe { + let mut point: POINT = std::mem::zeroed(); + GetCursorPos(&mut point) + .context("unable to get cursor position") + .log_err(); + ScreenToClient(self.0.hwnd, &mut point).ok().log_err(); + point + }; + logical_point(point.x as f32, point.y as f32, scale_factor) + } + + fn modifiers(&self) -> Modifiers { + current_modifiers() + } + + fn capslock(&self) -> Capslock { + current_capslock() + } + + fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { + self.0.state.borrow_mut().input_handler = Some(input_handler); + } + + fn take_input_handler(&mut self) -> Option { + self.0.state.borrow_mut().input_handler.take() + } + + fn prompt( + &self, + level: PromptLevel, + msg: &str, + detail: Option<&str>, + answers: &[PromptButton], + ) -> Option> { + let (done_tx, done_rx) = oneshot::channel(); + let msg = msg.to_string(); + let detail_string = detail.map(|detail| detail.to_string()); + let handle = self.0.hwnd; + let answers = answers.to_vec(); + self.0 + .executor + .spawn(async move { + unsafe { + let mut config = TASKDIALOGCONFIG::default(); + config.cbSize = std::mem::size_of::() as _; + config.hwndParent = handle; + let title; + let main_icon; + match level { + crate::PromptLevel::Info => { + title = windows::core::w!("Info"); + main_icon = TD_INFORMATION_ICON; + } + crate::PromptLevel::Warning => { + title = windows::core::w!("Warning"); + main_icon = TD_WARNING_ICON; + } + crate::PromptLevel::Critical => { + title = windows::core::w!("Critical"); + main_icon = TD_ERROR_ICON; + } + }; + config.pszWindowTitle = title; + config.Anonymous1.pszMainIcon = main_icon; + let instruction = HSTRING::from(msg); + config.pszMainInstruction = PCWSTR::from_raw(instruction.as_ptr()); + let hints_encoded; + if let Some(ref hints) = detail_string { + hints_encoded = HSTRING::from(hints); + config.pszContent = PCWSTR::from_raw(hints_encoded.as_ptr()); + }; + let mut button_id_map = Vec::with_capacity(answers.len()); + let mut buttons = Vec::new(); + let mut btn_encoded = Vec::new(); + for (index, btn) in answers.iter().enumerate() { + let encoded = HSTRING::from(btn.label().as_ref()); + let button_id = match btn { + PromptButton::Ok(_) => IDOK.0, + PromptButton::Cancel(_) => IDCANCEL.0, + // the first few low integer values are reserved for known buttons + // so for simplicity we just go backwards from -1 + PromptButton::Other(_) => -(index as i32) - 1, + }; + button_id_map.push(button_id); + buttons.push(TASKDIALOG_BUTTON { + nButtonID: button_id, + pszButtonText: PCWSTR::from_raw(encoded.as_ptr()), + }); + btn_encoded.push(encoded); + } + config.cButtons = buttons.len() as _; + config.pButtons = buttons.as_ptr(); + + config.pfCallback = None; + let mut res = std::mem::zeroed(); + let _ = TaskDialogIndirect(&config, Some(&mut res), None, None) + .context("unable to create task dialog") + .log_err(); + + if let Some(clicked) = + button_id_map.iter().position(|&button_id| button_id == res) + { + let _ = done_tx.send(clicked); + } + } + }) + .detach(); + + Some(done_rx) + } + + fn activate(&self) { + let hwnd = self.0.hwnd; + let this = self.0.clone(); + self.0 + .executor + .spawn(async move { + this.set_window_placement().log_err(); + + unsafe { + // If the window is minimized, restore it. + if IsIconic(hwnd).as_bool() { + ShowWindowAsync(hwnd, SW_RESTORE).ok().log_err(); + } + + SetActiveWindow(hwnd).log_err(); + SetFocus(Some(hwnd)).log_err(); + } + + // premium ragebait by windows, this is needed because the window + // must have received an input event to be able to set itself to foreground + // so let's just simulate user input as that seems to be the most reliable way + // some more info: https://gist.github.com/Aetopia/1581b40f00cc0cadc93a0e8ccb65dc8c + // bonus: this bug also doesn't manifest if you have vs attached to the process + let inputs = [ + INPUT { + r#type: INPUT_KEYBOARD, + Anonymous: INPUT_0 { + ki: KEYBDINPUT { + wVk: VK_MENU, + dwFlags: KEYBD_EVENT_FLAGS(0), + ..Default::default() + }, + }, + }, + INPUT { + r#type: INPUT_KEYBOARD, + Anonymous: INPUT_0 { + ki: KEYBDINPUT { + wVk: VK_MENU, + dwFlags: KEYEVENTF_KEYUP, + ..Default::default() + }, + }, + }, + ]; + unsafe { SendInput(&inputs, std::mem::size_of::() as i32) }; + + // todo(windows) + // crate `windows 0.56` reports true as Err + unsafe { SetForegroundWindow(hwnd).as_bool() }; + }) + .detach(); + } + + fn is_active(&self) -> bool { + self.0.hwnd == unsafe { GetActiveWindow() } + } + + fn is_hovered(&self) -> bool { + self.0.state.borrow().hovered + } + + fn set_title(&mut self, title: &str) { + unsafe { SetWindowTextW(self.0.hwnd, &HSTRING::from(title)) } + .inspect_err(|e| log::error!("Set title failed: {e}")) + .ok(); + } + + fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { + let hwnd = self.0.hwnd; + + match background_appearance { + WindowBackgroundAppearance::Opaque => { + // ACCENT_DISABLED + set_window_composition_attribute(hwnd, None, 0); + } + WindowBackgroundAppearance::Transparent => { + // Use ACCENT_ENABLE_TRANSPARENTGRADIENT for transparent background + set_window_composition_attribute(hwnd, None, 2); + } + WindowBackgroundAppearance::Blurred => { + // Enable acrylic blur + // ACCENT_ENABLE_ACRYLICBLURBEHIND + set_window_composition_attribute(hwnd, Some((0, 0, 0, 0)), 4); + } + } + } + + fn minimize(&self) { + unsafe { ShowWindowAsync(self.0.hwnd, SW_MINIMIZE).ok().log_err() }; + } + + fn zoom(&self) { + unsafe { + if IsWindowVisible(self.0.hwnd).as_bool() { + ShowWindowAsync(self.0.hwnd, SW_MAXIMIZE).ok().log_err(); + } else if let Some(status) = self.0.state.borrow_mut().initial_placement.as_mut() { + status.state = WindowOpenState::Maximized; + } + } + } + + fn toggle_fullscreen(&self) { + if unsafe { IsWindowVisible(self.0.hwnd).as_bool() } { + self.0.toggle_fullscreen(); + } else if let Some(status) = self.0.state.borrow_mut().initial_placement.as_mut() { + status.state = WindowOpenState::Fullscreen; + } + } + + fn is_fullscreen(&self) -> bool { + self.0.state.borrow().is_fullscreen() + } + + fn on_request_frame(&self, callback: Box) { + self.0.state.borrow_mut().callbacks.request_frame = Some(callback); + } + + fn on_input(&self, callback: Box DispatchEventResult>) { + self.0.state.borrow_mut().callbacks.input = Some(callback); + } + + fn on_active_status_change(&self, callback: Box) { + self.0.state.borrow_mut().callbacks.active_status_change = Some(callback); + } + + fn on_hover_status_change(&self, callback: Box) { + self.0.state.borrow_mut().callbacks.hovered_status_change = Some(callback); + } + + fn on_resize(&self, callback: Box, f32)>) { + self.0.state.borrow_mut().callbacks.resize = Some(callback); + } + + fn on_moved(&self, callback: Box) { + self.0.state.borrow_mut().callbacks.moved = Some(callback); + } + + fn on_should_close(&self, callback: Box bool>) { + self.0.state.borrow_mut().callbacks.should_close = Some(callback); + } + + fn on_close(&self, callback: Box) { + self.0.state.borrow_mut().callbacks.close = Some(callback); + } + + fn on_hit_test_window_control(&self, callback: Box Option>) { + self.0.state.borrow_mut().callbacks.hit_test_window_control = Some(callback); + } + + fn on_appearance_changed(&self, callback: Box) { + self.0.state.borrow_mut().callbacks.appearance_changed = Some(callback); + } + + fn draw(&self, scene: &Scene) { + self.0.state.borrow_mut().renderer.draw(scene).log_err(); + } + + fn sprite_atlas(&self) -> Arc { + self.0.state.borrow().renderer.sprite_atlas() + } + + fn get_raw_handle(&self) -> HWND { + self.0.hwnd + } + + fn gpu_specs(&self) -> Option { + self.0.state.borrow().renderer.gpu_specs().log_err() + } + + fn update_ime_position(&self, _bounds: Bounds) { + // There is no such thing on Windows. + } +} + +#[implement(IDropTarget)] +struct WindowsDragDropHandler(pub Rc); + +impl WindowsDragDropHandler { + fn handle_drag_drop(&self, input: PlatformInput) { + let mut lock = self.0.state.borrow_mut(); + if let Some(mut func) = lock.callbacks.input.take() { + drop(lock); + func(input); + self.0.state.borrow_mut().callbacks.input = Some(func); + } + } +} + +#[allow(non_snake_case)] +impl IDropTarget_Impl for WindowsDragDropHandler_Impl { + fn DragEnter( + &self, + pdataobj: windows::core::Ref, + _grfkeystate: MODIFIERKEYS_FLAGS, + pt: &POINTL, + pdweffect: *mut DROPEFFECT, + ) -> windows::core::Result<()> { + unsafe { + let idata_obj = pdataobj.ok()?; + let config = FORMATETC { + cfFormat: CF_HDROP.0, + ptd: std::ptr::null_mut() as _, + dwAspect: DVASPECT_CONTENT.0, + lindex: -1, + tymed: TYMED_HGLOBAL.0 as _, + }; + let cursor_position = POINT { x: pt.x, y: pt.y }; + if idata_obj.QueryGetData(&config as _) == S_OK { + *pdweffect = DROPEFFECT_COPY; + let Some(mut idata) = idata_obj.GetData(&config as _).log_err() else { + return Ok(()); + }; + if idata.u.hGlobal.is_invalid() { + return Ok(()); + } + let hdrop = idata.u.hGlobal.0 as *mut HDROP; + let mut paths = SmallVec::<[PathBuf; 2]>::new(); + with_file_names(*hdrop, |file_name| { + if let Some(path) = PathBuf::from_str(&file_name).log_err() { + paths.push(path); + } + }); + ReleaseStgMedium(&mut idata); + let mut cursor_position = cursor_position; + ScreenToClient(self.0.hwnd, &mut cursor_position) + .ok() + .log_err(); + let scale_factor = self.0.state.borrow().scale_factor; + let input = PlatformInput::FileDrop(FileDropEvent::Entered { + position: logical_point( + cursor_position.x as f32, + cursor_position.y as f32, + scale_factor, + ), + paths: ExternalPaths(paths), + }); + self.handle_drag_drop(input); + } else { + *pdweffect = DROPEFFECT_NONE; + } + self.0 + .drop_target_helper + .DragEnter(self.0.hwnd, idata_obj, &cursor_position, *pdweffect) + .log_err(); + } + Ok(()) + } + + fn DragOver( + &self, + _grfkeystate: MODIFIERKEYS_FLAGS, + pt: &POINTL, + pdweffect: *mut DROPEFFECT, + ) -> windows::core::Result<()> { + let mut cursor_position = POINT { x: pt.x, y: pt.y }; + unsafe { + *pdweffect = DROPEFFECT_COPY; + self.0 + .drop_target_helper + .DragOver(&cursor_position, *pdweffect) + .log_err(); + ScreenToClient(self.0.hwnd, &mut cursor_position) + .ok() + .log_err(); + } + let scale_factor = self.0.state.borrow().scale_factor; + let input = PlatformInput::FileDrop(FileDropEvent::Pending { + position: logical_point( + cursor_position.x as f32, + cursor_position.y as f32, + scale_factor, + ), + }); + self.handle_drag_drop(input); + + Ok(()) + } + + fn DragLeave(&self) -> windows::core::Result<()> { + unsafe { + self.0.drop_target_helper.DragLeave().log_err(); + } + let input = PlatformInput::FileDrop(FileDropEvent::Exited); + self.handle_drag_drop(input); + + Ok(()) + } + + fn Drop( + &self, + pdataobj: windows::core::Ref, + _grfkeystate: MODIFIERKEYS_FLAGS, + pt: &POINTL, + pdweffect: *mut DROPEFFECT, + ) -> windows::core::Result<()> { + let idata_obj = pdataobj.ok()?; + let mut cursor_position = POINT { x: pt.x, y: pt.y }; + unsafe { + *pdweffect = DROPEFFECT_COPY; + self.0 + .drop_target_helper + .Drop(idata_obj, &cursor_position, *pdweffect) + .log_err(); + ScreenToClient(self.0.hwnd, &mut cursor_position) + .ok() + .log_err(); + } + let scale_factor = self.0.state.borrow().scale_factor; + let input = PlatformInput::FileDrop(FileDropEvent::Submit { + position: logical_point( + cursor_position.x as f32, + cursor_position.y as f32, + scale_factor, + ), + }); + self.handle_drag_drop(input); + + Ok(()) + } +} + +#[derive(Debug, Clone, Copy)] +pub(crate) struct ClickState { + button: MouseButton, + last_click: Instant, + last_position: Point, + double_click_spatial_tolerance_width: i32, + double_click_spatial_tolerance_height: i32, + double_click_interval: Duration, + pub(crate) current_count: usize, +} + +impl ClickState { + pub fn new() -> Self { + let double_click_spatial_tolerance_width = unsafe { GetSystemMetrics(SM_CXDOUBLECLK) }; + let double_click_spatial_tolerance_height = unsafe { GetSystemMetrics(SM_CYDOUBLECLK) }; + let double_click_interval = Duration::from_millis(unsafe { GetDoubleClickTime() } as u64); + + ClickState { + button: MouseButton::Left, + last_click: Instant::now(), + last_position: Point::default(), + double_click_spatial_tolerance_width, + double_click_spatial_tolerance_height, + double_click_interval, + current_count: 0, + } + } + + /// update self and return the needed click count + pub fn update(&mut self, button: MouseButton, new_position: Point) -> usize { + if self.button == button && self.is_double_click(new_position) { + self.current_count += 1; + } else { + self.current_count = 1; + } + self.last_click = Instant::now(); + self.last_position = new_position; + self.button = button; + + self.current_count + } + + pub fn system_update(&mut self, wparam: usize) { + match wparam { + // SPI_SETDOUBLECLKWIDTH + 29 => { + self.double_click_spatial_tolerance_width = + unsafe { GetSystemMetrics(SM_CXDOUBLECLK) } + } + // SPI_SETDOUBLECLKHEIGHT + 30 => { + self.double_click_spatial_tolerance_height = + unsafe { GetSystemMetrics(SM_CYDOUBLECLK) } + } + // SPI_SETDOUBLECLICKTIME + 32 => { + self.double_click_interval = + Duration::from_millis(unsafe { GetDoubleClickTime() } as u64) + } + _ => {} + } + } + + #[inline] + fn is_double_click(&self, new_position: Point) -> bool { + let diff = self.last_position - new_position; + + self.last_click.elapsed() < self.double_click_interval + && diff.x.0.abs() <= self.double_click_spatial_tolerance_width + && diff.y.0.abs() <= self.double_click_spatial_tolerance_height + } +} + +struct StyleAndBounds { + style: WINDOW_STYLE, + x: i32, + y: i32, + cx: i32, + cy: i32, +} + +#[repr(C)] +struct WINDOWCOMPOSITIONATTRIBDATA { + attrib: u32, + pv_data: *mut std::ffi::c_void, + cb_data: usize, +} + +#[repr(C)] +struct AccentPolicy { + accent_state: u32, + accent_flags: u32, + gradient_color: u32, + animation_id: u32, +} + +type Color = (u8, u8, u8, u8); + +#[derive(Debug, Default, Clone, Copy)] +pub(crate) struct WindowBorderOffset { + pub(crate) width_offset: i32, + pub(crate) height_offset: i32, +} + +impl WindowBorderOffset { + pub(crate) fn update(&mut self, hwnd: HWND) -> anyhow::Result<()> { + let window_rect = unsafe { + let mut rect = std::mem::zeroed(); + GetWindowRect(hwnd, &mut rect)?; + rect + }; + let client_rect = unsafe { + let mut rect = std::mem::zeroed(); + GetClientRect(hwnd, &mut rect)?; + rect + }; + self.width_offset = + (window_rect.right - window_rect.left) - (client_rect.right - client_rect.left); + self.height_offset = + (window_rect.bottom - window_rect.top) - (client_rect.bottom - client_rect.top); + Ok(()) + } +} + +struct WindowOpenStatus { + placement: WINDOWPLACEMENT, + state: WindowOpenState, +} + +enum WindowOpenState { + Maximized, + Fullscreen, + Windowed, +} + +const WINDOW_CLASS_NAME: PCWSTR = w!("Zed::Window"); + +fn register_window_class(icon_handle: HICON) { + static ONCE: Once = Once::new(); + ONCE.call_once(|| { + let wc = WNDCLASSW { + lpfnWndProc: Some(window_procedure), + hIcon: icon_handle, + lpszClassName: PCWSTR(WINDOW_CLASS_NAME.as_ptr()), + style: CS_HREDRAW | CS_VREDRAW, + hInstance: get_module_handle().into(), + hbrBackground: unsafe { CreateSolidBrush(COLORREF(0x00000000)) }, + ..Default::default() + }; + unsafe { RegisterClassW(&wc) }; + }); +} + +unsafe extern "system" fn window_procedure( + hwnd: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, +) -> LRESULT { + if msg == WM_NCCREATE { + let window_params = lparam.0 as *const CREATESTRUCTW; + let window_params = unsafe { &*window_params }; + let window_creation_context = window_params.lpCreateParams as *mut WindowCreateContext; + let window_creation_context = unsafe { &mut *window_creation_context }; + return match WindowsWindowInner::new(window_creation_context, hwnd, window_params) { + Ok(window_state) => { + let weak = Box::new(Rc::downgrade(&window_state)); + unsafe { set_window_long(hwnd, GWLP_USERDATA, Box::into_raw(weak) as isize) }; + window_creation_context.inner = Some(Ok(window_state)); + unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) } + } + Err(error) => { + window_creation_context.inner = Some(Err(error)); + LRESULT(0) + } + }; + } + + let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak; + if ptr.is_null() { + return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }; + } + let inner = unsafe { &*ptr }; + let result = if let Some(inner) = inner.upgrade() { + inner.handle_msg(hwnd, msg, wparam, lparam) + } else { + unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) } + }; + + if msg == WM_NCDESTROY { + unsafe { set_window_long(hwnd, GWLP_USERDATA, 0) }; + unsafe { drop(Box::from_raw(ptr)) }; + } + + result +} + +pub(crate) fn window_from_hwnd(hwnd: HWND) -> Option> { + if hwnd.is_invalid() { + return None; + } + + let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak; + if !ptr.is_null() { + let inner = unsafe { &*ptr }; + inner.upgrade() + } else { + None + } +} + +fn get_module_handle() -> HMODULE { + unsafe { + let mut h_module = std::mem::zeroed(); + GetModuleHandleExW( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + windows::core::w!("ZedModule"), + &mut h_module, + ) + .expect("Unable to get module handle"); // this should never fail + + h_module + } +} + +fn register_drag_drop(window: &Rc) -> Result<()> { + let window_handle = window.hwnd; + let handler = WindowsDragDropHandler(window.clone()); + // The lifetime of `IDropTarget` is handled by Windows, it won't release until + // we call `RevokeDragDrop`. + // So, it's safe to drop it here. + let drag_drop_handler: IDropTarget = handler.into(); + unsafe { + RegisterDragDrop(window_handle, &drag_drop_handler) + .context("unable to register drag-drop event")?; + } + Ok(()) +} + +fn calculate_window_rect(bounds: Bounds, border_offset: WindowBorderOffset) -> RECT { + // NOTE: + // The reason we're not using `AdjustWindowRectEx()` here is + // that the size reported by this function is incorrect. + // You can test it, and there are similar discussions online. + // See: https://stackoverflow.com/questions/12423584/how-to-set-exact-client-size-for-overlapped-window-winapi + // + // So we manually calculate these values here. + let mut rect = RECT { + left: bounds.left().0, + top: bounds.top().0, + right: bounds.right().0, + bottom: bounds.bottom().0, + }; + let left_offset = border_offset.width_offset / 2; + let top_offset = border_offset.height_offset / 2; + let right_offset = border_offset.width_offset - left_offset; + let bottom_offset = border_offset.height_offset - top_offset; + rect.left -= left_offset; + rect.top -= top_offset; + rect.right += right_offset; + rect.bottom += bottom_offset; + rect +} + +fn calculate_client_rect( + rect: RECT, + border_offset: WindowBorderOffset, + scale_factor: f32, +) -> Bounds { + let left_offset = border_offset.width_offset / 2; + let top_offset = border_offset.height_offset / 2; + let right_offset = border_offset.width_offset - left_offset; + let bottom_offset = border_offset.height_offset - top_offset; + let left = rect.left + left_offset; + let top = rect.top + top_offset; + let right = rect.right - right_offset; + let bottom = rect.bottom - bottom_offset; + let physical_size = size(DevicePixels(right - left), DevicePixels(bottom - top)); + Bounds { + origin: logical_point(left as f32, top as f32, scale_factor), + size: physical_size.to_pixels(scale_factor), + } +} + +fn retrieve_window_placement( + hwnd: HWND, + display: WindowsDisplay, + initial_bounds: Bounds, + scale_factor: f32, + border_offset: WindowBorderOffset, +) -> Result { + let mut placement = WINDOWPLACEMENT { + length: std::mem::size_of::() as u32, + ..Default::default() + }; + unsafe { GetWindowPlacement(hwnd, &mut placement)? }; + // the bounds may be not inside the display + let bounds = if display.check_given_bounds(initial_bounds) { + initial_bounds + } else { + display.default_bounds() + }; + let bounds = bounds.to_device_pixels(scale_factor); + placement.rcNormalPosition = calculate_window_rect(bounds, border_offset); + Ok(placement) +} + +fn set_window_composition_attribute(hwnd: HWND, color: Option, state: u32) { + let mut version = unsafe { std::mem::zeroed() }; + let status = unsafe { windows::Wdk::System::SystemServices::RtlGetVersion(&mut version) }; + if !status.is_ok() || version.dwBuildNumber < 17763 { + return; + } + + unsafe { + type SetWindowCompositionAttributeType = + unsafe extern "system" fn(HWND, *mut WINDOWCOMPOSITIONATTRIBDATA) -> BOOL; + let module_name = PCSTR::from_raw(c"user32.dll".as_ptr() as *const u8); + if let Some(user32) = GetModuleHandleA(module_name) + .context("Unable to get user32.dll handle") + .log_err() + { + let func_name = PCSTR::from_raw(c"SetWindowCompositionAttribute".as_ptr() as *const u8); + let set_window_composition_attribute: SetWindowCompositionAttributeType = + std::mem::transmute(GetProcAddress(user32, func_name)); + let mut color = color.unwrap_or_default(); + let is_acrylic = state == 4; + if is_acrylic && color.3 == 0 { + color.3 = 1; + } + let accent = AccentPolicy { + accent_state: state, + accent_flags: if is_acrylic { 0 } else { 2 }, + gradient_color: (color.0 as u32) + | ((color.1 as u32) << 8) + | ((color.2 as u32) << 16) + | ((color.3 as u32) << 24), + animation_id: 0, + }; + let mut data = WINDOWCOMPOSITIONATTRIBDATA { + attrib: 0x13, + pv_data: &accent as *const _ as *mut _, + cb_data: std::mem::size_of::(), + }; + let _ = set_window_composition_attribute(hwnd, &mut data as *mut _ as _); + } + } +} + +#[cfg(test)] +mod tests { + use super::ClickState; + use crate::{DevicePixels, MouseButton, point}; + use std::time::Duration; + + #[test] + fn test_double_click_interval() { + let mut state = ClickState::new(); + assert_eq!( + state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(0))), + 1 + ); + assert_eq!( + state.update(MouseButton::Right, point(DevicePixels(0), DevicePixels(0))), + 1 + ); + assert_eq!( + state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(0))), + 1 + ); + assert_eq!( + state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(0))), + 2 + ); + state.last_click -= Duration::from_millis(700); + assert_eq!( + state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(0))), + 1 + ); + } + + #[test] + fn test_double_click_spatial_tolerance() { + let mut state = ClickState::new(); + assert_eq!( + state.update(MouseButton::Left, point(DevicePixels(-3), DevicePixels(0))), + 1 + ); + assert_eq!( + state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(3))), + 2 + ); + assert_eq!( + state.update(MouseButton::Right, point(DevicePixels(3), DevicePixels(2))), + 1 + ); + assert_eq!( + state.update(MouseButton::Right, point(DevicePixels(10), DevicePixels(0))), + 1 + ); + } +} diff --git a/third_party/gpui/src/platform/windows/wrapper.rs b/third_party/gpui/src/platform/windows/wrapper.rs new file mode 100644 index 0000000..60bbc43 --- /dev/null +++ b/third_party/gpui/src/platform/windows/wrapper.rs @@ -0,0 +1,53 @@ +use std::ops::Deref; + +use windows::Win32::{Foundation::HWND, UI::WindowsAndMessaging::HCURSOR}; + +#[derive(Debug, Clone, Copy)] +pub(crate) struct SafeCursor { + raw: HCURSOR, +} + +unsafe impl Send for SafeCursor {} +unsafe impl Sync for SafeCursor {} + +impl From for SafeCursor { + fn from(value: HCURSOR) -> Self { + SafeCursor { raw: value } + } +} + +impl Deref for SafeCursor { + type Target = HCURSOR; + + fn deref(&self) -> &Self::Target { + &self.raw + } +} + +#[derive(Debug, Clone, Copy)] +pub(crate) struct SafeHwnd { + raw: HWND, +} + +impl SafeHwnd { + pub(crate) fn as_raw(&self) -> HWND { + self.raw + } +} + +unsafe impl Send for SafeHwnd {} +unsafe impl Sync for SafeHwnd {} + +impl From for SafeHwnd { + fn from(value: HWND) -> Self { + SafeHwnd { raw: value } + } +} + +impl Deref for SafeHwnd { + type Target = HWND; + + fn deref(&self) -> &Self::Target { + &self.raw + } +} diff --git a/third_party/gpui/src/prelude.rs b/third_party/gpui/src/prelude.rs new file mode 100644 index 0000000..191d0a0 --- /dev/null +++ b/third_party/gpui/src/prelude.rs @@ -0,0 +1,9 @@ +//! The GPUI prelude is a collection of traits and types that are widely used +//! throughout the library. It is recommended to import this prelude into your +//! application to avoid having to import each trait individually. + +pub use crate::{ + AppContext as _, BorrowAppContext, Context, Element, InteractiveElement, IntoElement, + ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement, Styled, StyledImage, + VisualContext, util::FluentBuilder, +}; diff --git a/third_party/gpui/src/scene.rs b/third_party/gpui/src/scene.rs new file mode 100644 index 0000000..758d06e --- /dev/null +++ b/third_party/gpui/src/scene.rs @@ -0,0 +1,833 @@ +// todo("windows"): remove +#![cfg_attr(windows, allow(dead_code))] + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{ + AtlasTextureId, AtlasTile, Background, Bounds, ContentMask, Corners, Edges, Hsla, Pixels, + Point, Radians, ScaledPixels, Size, bounds_tree::BoundsTree, point, +}; +use std::{ + fmt::Debug, + iter::Peekable, + ops::{Add, Range, Sub}, + slice, +}; + +#[allow(non_camel_case_types, unused)] +pub(crate) type PathVertex_ScaledPixels = PathVertex; + +pub(crate) type DrawOrder = u32; + +#[derive(Default)] +pub(crate) struct Scene { + pub(crate) paint_operations: Vec, + primitive_bounds: BoundsTree, + layer_stack: Vec, + pub(crate) shadows: Vec, + pub(crate) quads: Vec, + pub(crate) paths: Vec>, + pub(crate) underlines: Vec, + pub(crate) monochrome_sprites: Vec, + pub(crate) polychrome_sprites: Vec, + pub(crate) surfaces: Vec, +} + +impl Scene { + pub fn clear(&mut self) { + self.paint_operations.clear(); + self.primitive_bounds.clear(); + self.layer_stack.clear(); + self.paths.clear(); + self.shadows.clear(); + self.quads.clear(); + self.underlines.clear(); + self.monochrome_sprites.clear(); + self.polychrome_sprites.clear(); + self.surfaces.clear(); + } + + pub fn len(&self) -> usize { + self.paint_operations.len() + } + + pub fn push_layer(&mut self, bounds: Bounds) { + let order = self.primitive_bounds.insert(bounds); + self.layer_stack.push(order); + self.paint_operations + .push(PaintOperation::StartLayer(bounds)); + } + + pub fn pop_layer(&mut self) { + self.layer_stack.pop(); + self.paint_operations.push(PaintOperation::EndLayer); + } + + pub fn insert_primitive(&mut self, primitive: impl Into) { + let mut primitive = primitive.into(); + let clipped_bounds = primitive + .bounds() + .intersect(&primitive.content_mask().bounds); + + if clipped_bounds.is_empty() { + return; + } + + let order = self + .layer_stack + .last() + .copied() + .unwrap_or_else(|| self.primitive_bounds.insert(clipped_bounds)); + match &mut primitive { + Primitive::Shadow(shadow) => { + shadow.order = order; + self.shadows.push(shadow.clone()); + } + Primitive::Quad(quad) => { + quad.order = order; + self.quads.push(quad.clone()); + } + Primitive::Path(path) => { + path.order = order; + path.id = PathId(self.paths.len()); + self.paths.push(path.clone()); + } + Primitive::Underline(underline) => { + underline.order = order; + self.underlines.push(underline.clone()); + } + Primitive::MonochromeSprite(sprite) => { + sprite.order = order; + self.monochrome_sprites.push(sprite.clone()); + } + Primitive::PolychromeSprite(sprite) => { + sprite.order = order; + self.polychrome_sprites.push(sprite.clone()); + } + Primitive::Surface(surface) => { + surface.order = order; + self.surfaces.push(surface.clone()); + } + } + self.paint_operations + .push(PaintOperation::Primitive(primitive)); + } + + pub fn replay(&mut self, range: Range, prev_scene: &Scene) { + for operation in &prev_scene.paint_operations[range] { + match operation { + PaintOperation::Primitive(primitive) => self.insert_primitive(primitive.clone()), + PaintOperation::StartLayer(bounds) => self.push_layer(*bounds), + PaintOperation::EndLayer => self.pop_layer(), + } + } + } + + pub fn finish(&mut self) { + self.shadows.sort_by_key(|shadow| shadow.order); + self.quads.sort_by_key(|quad| quad.order); + self.paths.sort_by_key(|path| path.order); + self.underlines.sort_by_key(|underline| underline.order); + self.monochrome_sprites + .sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id)); + self.polychrome_sprites + .sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id)); + self.surfaces.sort_by_key(|surface| surface.order); + } + + #[cfg_attr( + all( + any(target_os = "linux", target_os = "freebsd"), + not(any(feature = "x11", feature = "wayland")) + ), + allow(dead_code) + )] + pub(crate) fn batches(&self) -> impl Iterator> { + BatchIterator { + shadows: &self.shadows, + shadows_start: 0, + shadows_iter: self.shadows.iter().peekable(), + quads: &self.quads, + quads_start: 0, + quads_iter: self.quads.iter().peekable(), + paths: &self.paths, + paths_start: 0, + paths_iter: self.paths.iter().peekable(), + underlines: &self.underlines, + underlines_start: 0, + underlines_iter: self.underlines.iter().peekable(), + monochrome_sprites: &self.monochrome_sprites, + monochrome_sprites_start: 0, + monochrome_sprites_iter: self.monochrome_sprites.iter().peekable(), + polychrome_sprites: &self.polychrome_sprites, + polychrome_sprites_start: 0, + polychrome_sprites_iter: self.polychrome_sprites.iter().peekable(), + surfaces: &self.surfaces, + surfaces_start: 0, + surfaces_iter: self.surfaces.iter().peekable(), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Default)] +#[cfg_attr( + all( + any(target_os = "linux", target_os = "freebsd"), + not(any(feature = "x11", feature = "wayland")) + ), + allow(dead_code) +)] +pub(crate) enum PrimitiveKind { + Shadow, + #[default] + Quad, + Path, + Underline, + MonochromeSprite, + PolychromeSprite, + Surface, +} + +pub(crate) enum PaintOperation { + Primitive(Primitive), + StartLayer(Bounds), + EndLayer, +} + +#[derive(Clone)] +pub(crate) enum Primitive { + Shadow(Shadow), + Quad(Quad), + Path(Path), + Underline(Underline), + MonochromeSprite(MonochromeSprite), + PolychromeSprite(PolychromeSprite), + Surface(PaintSurface), +} + +impl Primitive { + pub fn bounds(&self) -> &Bounds { + match self { + Primitive::Shadow(shadow) => &shadow.bounds, + Primitive::Quad(quad) => &quad.bounds, + Primitive::Path(path) => &path.bounds, + Primitive::Underline(underline) => &underline.bounds, + Primitive::MonochromeSprite(sprite) => &sprite.bounds, + Primitive::PolychromeSprite(sprite) => &sprite.bounds, + Primitive::Surface(surface) => &surface.bounds, + } + } + + pub fn content_mask(&self) -> &ContentMask { + match self { + Primitive::Shadow(shadow) => &shadow.content_mask, + Primitive::Quad(quad) => &quad.content_mask, + Primitive::Path(path) => &path.content_mask, + Primitive::Underline(underline) => &underline.content_mask, + Primitive::MonochromeSprite(sprite) => &sprite.content_mask, + Primitive::PolychromeSprite(sprite) => &sprite.content_mask, + Primitive::Surface(surface) => &surface.content_mask, + } + } +} + +#[cfg_attr( + all( + any(target_os = "linux", target_os = "freebsd"), + not(any(feature = "x11", feature = "wayland")) + ), + allow(dead_code) +)] +struct BatchIterator<'a> { + shadows: &'a [Shadow], + shadows_start: usize, + shadows_iter: Peekable>, + quads: &'a [Quad], + quads_start: usize, + quads_iter: Peekable>, + paths: &'a [Path], + paths_start: usize, + paths_iter: Peekable>>, + underlines: &'a [Underline], + underlines_start: usize, + underlines_iter: Peekable>, + monochrome_sprites: &'a [MonochromeSprite], + monochrome_sprites_start: usize, + monochrome_sprites_iter: Peekable>, + polychrome_sprites: &'a [PolychromeSprite], + polychrome_sprites_start: usize, + polychrome_sprites_iter: Peekable>, + surfaces: &'a [PaintSurface], + surfaces_start: usize, + surfaces_iter: Peekable>, +} + +impl<'a> Iterator for BatchIterator<'a> { + type Item = PrimitiveBatch<'a>; + + fn next(&mut self) -> Option { + let mut orders_and_kinds = [ + ( + self.shadows_iter.peek().map(|s| s.order), + PrimitiveKind::Shadow, + ), + (self.quads_iter.peek().map(|q| q.order), PrimitiveKind::Quad), + (self.paths_iter.peek().map(|q| q.order), PrimitiveKind::Path), + ( + self.underlines_iter.peek().map(|u| u.order), + PrimitiveKind::Underline, + ), + ( + self.monochrome_sprites_iter.peek().map(|s| s.order), + PrimitiveKind::MonochromeSprite, + ), + ( + self.polychrome_sprites_iter.peek().map(|s| s.order), + PrimitiveKind::PolychromeSprite, + ), + ( + self.surfaces_iter.peek().map(|s| s.order), + PrimitiveKind::Surface, + ), + ]; + orders_and_kinds.sort_by_key(|(order, kind)| (order.unwrap_or(u32::MAX), *kind)); + + let first = orders_and_kinds[0]; + let second = orders_and_kinds[1]; + let (batch_kind, max_order_and_kind) = if first.0.is_some() { + (first.1, (second.0.unwrap_or(u32::MAX), second.1)) + } else { + return None; + }; + + match batch_kind { + PrimitiveKind::Shadow => { + let shadows_start = self.shadows_start; + let mut shadows_end = shadows_start + 1; + self.shadows_iter.next(); + while self + .shadows_iter + .next_if(|shadow| (shadow.order, batch_kind) < max_order_and_kind) + .is_some() + { + shadows_end += 1; + } + self.shadows_start = shadows_end; + Some(PrimitiveBatch::Shadows( + &self.shadows[shadows_start..shadows_end], + )) + } + PrimitiveKind::Quad => { + let quads_start = self.quads_start; + let mut quads_end = quads_start + 1; + self.quads_iter.next(); + while self + .quads_iter + .next_if(|quad| (quad.order, batch_kind) < max_order_and_kind) + .is_some() + { + quads_end += 1; + } + self.quads_start = quads_end; + Some(PrimitiveBatch::Quads(&self.quads[quads_start..quads_end])) + } + PrimitiveKind::Path => { + let paths_start = self.paths_start; + let mut paths_end = paths_start + 1; + self.paths_iter.next(); + while self + .paths_iter + .next_if(|path| (path.order, batch_kind) < max_order_and_kind) + .is_some() + { + paths_end += 1; + } + self.paths_start = paths_end; + Some(PrimitiveBatch::Paths(&self.paths[paths_start..paths_end])) + } + PrimitiveKind::Underline => { + let underlines_start = self.underlines_start; + let mut underlines_end = underlines_start + 1; + self.underlines_iter.next(); + while self + .underlines_iter + .next_if(|underline| (underline.order, batch_kind) < max_order_and_kind) + .is_some() + { + underlines_end += 1; + } + self.underlines_start = underlines_end; + Some(PrimitiveBatch::Underlines( + &self.underlines[underlines_start..underlines_end], + )) + } + PrimitiveKind::MonochromeSprite => { + let texture_id = self.monochrome_sprites_iter.peek().unwrap().tile.texture_id; + let sprites_start = self.monochrome_sprites_start; + let mut sprites_end = sprites_start + 1; + self.monochrome_sprites_iter.next(); + while self + .monochrome_sprites_iter + .next_if(|sprite| { + (sprite.order, batch_kind) < max_order_and_kind + && sprite.tile.texture_id == texture_id + }) + .is_some() + { + sprites_end += 1; + } + self.monochrome_sprites_start = sprites_end; + Some(PrimitiveBatch::MonochromeSprites { + texture_id, + sprites: &self.monochrome_sprites[sprites_start..sprites_end], + }) + } + PrimitiveKind::PolychromeSprite => { + let texture_id = self.polychrome_sprites_iter.peek().unwrap().tile.texture_id; + let sprites_start = self.polychrome_sprites_start; + let mut sprites_end = self.polychrome_sprites_start + 1; + self.polychrome_sprites_iter.next(); + while self + .polychrome_sprites_iter + .next_if(|sprite| { + (sprite.order, batch_kind) < max_order_and_kind + && sprite.tile.texture_id == texture_id + }) + .is_some() + { + sprites_end += 1; + } + self.polychrome_sprites_start = sprites_end; + Some(PrimitiveBatch::PolychromeSprites { + texture_id, + sprites: &self.polychrome_sprites[sprites_start..sprites_end], + }) + } + PrimitiveKind::Surface => { + let surfaces_start = self.surfaces_start; + let mut surfaces_end = surfaces_start + 1; + self.surfaces_iter.next(); + while self + .surfaces_iter + .next_if(|surface| (surface.order, batch_kind) < max_order_and_kind) + .is_some() + { + surfaces_end += 1; + } + self.surfaces_start = surfaces_end; + Some(PrimitiveBatch::Surfaces( + &self.surfaces[surfaces_start..surfaces_end], + )) + } + } + } +} + +#[derive(Debug)] +#[cfg_attr( + all( + any(target_os = "linux", target_os = "freebsd"), + not(any(feature = "x11", feature = "wayland")) + ), + allow(dead_code) +)] +pub(crate) enum PrimitiveBatch<'a> { + Shadows(&'a [Shadow]), + Quads(&'a [Quad]), + Paths(&'a [Path]), + Underlines(&'a [Underline]), + MonochromeSprites { + texture_id: AtlasTextureId, + sprites: &'a [MonochromeSprite], + }, + PolychromeSprites { + texture_id: AtlasTextureId, + sprites: &'a [PolychromeSprite], + }, + Surfaces(&'a [PaintSurface]), +} + +#[derive(Default, Debug, Clone)] +#[repr(C)] +pub(crate) struct Quad { + pub order: DrawOrder, + pub border_style: BorderStyle, + pub bounds: Bounds, + pub content_mask: ContentMask, + pub background: Background, + pub border_color: Hsla, + pub corner_radii: Corners, + pub border_widths: Edges, +} + +impl From for Primitive { + fn from(quad: Quad) -> Self { + Primitive::Quad(quad) + } +} + +#[derive(Debug, Clone)] +#[repr(C)] +pub(crate) struct Underline { + pub order: DrawOrder, + pub pad: u32, // align to 8 bytes + pub bounds: Bounds, + pub content_mask: ContentMask, + pub color: Hsla, + pub thickness: ScaledPixels, + pub wavy: u32, +} + +impl From for Primitive { + fn from(underline: Underline) -> Self { + Primitive::Underline(underline) + } +} + +#[derive(Debug, Clone)] +#[repr(C)] +pub(crate) struct Shadow { + pub order: DrawOrder, + pub blur_radius: ScaledPixels, + pub bounds: Bounds, + pub corner_radii: Corners, + pub content_mask: ContentMask, + pub color: Hsla, +} + +impl From for Primitive { + fn from(shadow: Shadow) -> Self { + Primitive::Shadow(shadow) + } +} + +/// The style of a border. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] +#[repr(C)] +pub enum BorderStyle { + /// A solid border. + #[default] + Solid = 0, + /// A dashed border. + Dashed = 1, +} + +/// A data type representing a 2 dimensional transformation that can be applied to an element. +#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(C)] +pub struct TransformationMatrix { + /// 2x2 matrix containing rotation and scale, + /// stored row-major + pub rotation_scale: [[f32; 2]; 2], + /// translation vector + pub translation: [f32; 2], +} + +impl Eq for TransformationMatrix {} + +impl TransformationMatrix { + /// The unit matrix, has no effect. + pub fn unit() -> Self { + Self { + rotation_scale: [[1.0, 0.0], [0.0, 1.0]], + translation: [0.0, 0.0], + } + } + + /// Move the origin by a given point + pub fn translate(mut self, point: Point) -> Self { + self.compose(Self { + rotation_scale: [[1.0, 0.0], [0.0, 1.0]], + translation: [point.x.0, point.y.0], + }) + } + + /// Clockwise rotation in radians around the origin + pub fn rotate(self, angle: Radians) -> Self { + self.compose(Self { + rotation_scale: [ + [angle.0.cos(), -angle.0.sin()], + [angle.0.sin(), angle.0.cos()], + ], + translation: [0.0, 0.0], + }) + } + + /// Scale around the origin + pub fn scale(self, size: Size) -> Self { + self.compose(Self { + rotation_scale: [[size.width, 0.0], [0.0, size.height]], + translation: [0.0, 0.0], + }) + } + + /// Perform matrix multiplication with another transformation + /// to produce a new transformation that is the result of + /// applying both transformations: first, `other`, then `self`. + #[inline] + pub fn compose(self, other: TransformationMatrix) -> TransformationMatrix { + if other == Self::unit() { + return self; + } + // Perform matrix multiplication + TransformationMatrix { + rotation_scale: [ + [ + self.rotation_scale[0][0] * other.rotation_scale[0][0] + + self.rotation_scale[0][1] * other.rotation_scale[1][0], + self.rotation_scale[0][0] * other.rotation_scale[0][1] + + self.rotation_scale[0][1] * other.rotation_scale[1][1], + ], + [ + self.rotation_scale[1][0] * other.rotation_scale[0][0] + + self.rotation_scale[1][1] * other.rotation_scale[1][0], + self.rotation_scale[1][0] * other.rotation_scale[0][1] + + self.rotation_scale[1][1] * other.rotation_scale[1][1], + ], + ], + translation: [ + self.translation[0] + + self.rotation_scale[0][0] * other.translation[0] + + self.rotation_scale[0][1] * other.translation[1], + self.translation[1] + + self.rotation_scale[1][0] * other.translation[0] + + self.rotation_scale[1][1] * other.translation[1], + ], + } + } + + /// Apply transformation to a point, mainly useful for debugging + pub fn apply(&self, point: Point) -> Point { + let input = [point.x.0, point.y.0]; + let mut output = self.translation; + for (i, output_cell) in output.iter_mut().enumerate() { + for (k, input_cell) in input.iter().enumerate() { + *output_cell += self.rotation_scale[i][k] * *input_cell; + } + } + Point::new(output[0].into(), output[1].into()) + } +} + +impl Default for TransformationMatrix { + fn default() -> Self { + Self::unit() + } +} + +#[derive(Clone, Debug)] +#[repr(C)] +pub(crate) struct MonochromeSprite { + pub order: DrawOrder, + pub pad: u32, // align to 8 bytes + pub bounds: Bounds, + pub content_mask: ContentMask, + pub color: Hsla, + pub tile: AtlasTile, + pub transformation: TransformationMatrix, +} + +impl From for Primitive { + fn from(sprite: MonochromeSprite) -> Self { + Primitive::MonochromeSprite(sprite) + } +} + +#[derive(Clone, Debug)] +#[repr(C)] +pub(crate) struct PolychromeSprite { + pub order: DrawOrder, + pub pad: u32, // align to 8 bytes + pub grayscale: bool, + pub opacity: f32, + pub bounds: Bounds, + pub content_mask: ContentMask, + pub corner_radii: Corners, + pub tile: AtlasTile, +} + +impl From for Primitive { + fn from(sprite: PolychromeSprite) -> Self { + Primitive::PolychromeSprite(sprite) + } +} + +#[derive(Clone, Debug)] +pub(crate) struct PaintSurface { + pub order: DrawOrder, + pub bounds: Bounds, + pub content_mask: ContentMask, + #[cfg(target_os = "macos")] + pub image_buffer: core_video::pixel_buffer::CVPixelBuffer, +} + +impl From for Primitive { + fn from(surface: PaintSurface) -> Self { + Primitive::Surface(surface) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub(crate) struct PathId(pub(crate) usize); + +/// A line made up of a series of vertices and control points. +#[derive(Clone, Debug)] +pub struct Path { + pub(crate) id: PathId, + pub(crate) order: DrawOrder, + pub(crate) bounds: Bounds

, + pub(crate) content_mask: ContentMask

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

, + current: Point

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

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

, +} + +impl PathVertex { + pub fn scale(&self, factor: f32) -> PathVertex { + PathVertex { + xy_position: self.xy_position.scale(factor), + st_position: self.st_position, + content_mask: self.content_mask.scale(factor), + } + } +} diff --git a/third_party/gpui/src/shared_string.rs b/third_party/gpui/src/shared_string.rs new file mode 100644 index 0000000..350184d --- /dev/null +++ b/third_party/gpui/src/shared_string.rs @@ -0,0 +1,145 @@ +use derive_more::{Deref, DerefMut}; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::{ + borrow::{Borrow, Cow}, + sync::Arc, +}; +use util::arc_cow::ArcCow; + +/// A shared string is an immutable string that can be cheaply cloned in GPUI +/// tasks. Essentially an abstraction over an `Arc` and `&'static str`, +#[derive(Deref, DerefMut, Eq, PartialEq, PartialOrd, Ord, Hash, Clone)] +pub struct SharedString(ArcCow<'static, str>); + +impl SharedString { + /// Creates a static [`SharedString`] from a `&'static str`. + pub const fn new_static(str: &'static str) -> Self { + Self(ArcCow::Borrowed(str)) + } + + /// Creates a [`SharedString`] from anything that can become an `Arc` + pub fn new(str: impl Into>) -> Self { + SharedString(ArcCow::Owned(str.into())) + } + + /// Get a &str from the underlying string. + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl JsonSchema for SharedString { + fn inline_schema() -> bool { + String::inline_schema() + } + + fn schema_name() -> Cow<'static, str> { + String::schema_name() + } + + fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + String::json_schema(generator) + } +} + +impl Default for SharedString { + fn default() -> Self { + Self(ArcCow::Owned(Arc::default())) + } +} + +impl AsRef for SharedString { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Borrow for SharedString { + fn borrow(&self) -> &str { + self.as_ref() + } +} + +impl std::fmt::Debug for SharedString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl std::fmt::Display for SharedString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.as_ref()) + } +} + +impl PartialEq for SharedString { + fn eq(&self, other: &String) -> bool { + self.as_ref() == other + } +} + +impl PartialEq for String { + fn eq(&self, other: &SharedString) -> bool { + self == other.as_ref() + } +} + +impl PartialEq for SharedString { + fn eq(&self, other: &str) -> bool { + self.as_ref() == other + } +} + +impl<'a> PartialEq<&'a str> for SharedString { + fn eq(&self, other: &&'a str) -> bool { + self.as_ref() == *other + } +} + +impl From<&SharedString> for SharedString { + fn from(value: &SharedString) -> Self { + value.clone() + } +} + +impl From for Arc { + fn from(val: SharedString) -> Self { + match val.0 { + ArcCow::Borrowed(borrowed) => Arc::from(borrowed), + ArcCow::Owned(owned) => owned, + } + } +} + +impl>> From for SharedString { + fn from(value: T) -> Self { + Self(value.into()) + } +} + +impl From for String { + fn from(val: SharedString) -> Self { + val.0.to_string() + } +} + +impl Serialize for SharedString { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_ref()) + } +} + +impl<'de> Deserialize<'de> for SharedString { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Ok(SharedString::from(s)) + } +} diff --git a/third_party/gpui/src/shared_uri.rs b/third_party/gpui/src/shared_uri.rs new file mode 100644 index 0000000..e257aaf --- /dev/null +++ b/third_party/gpui/src/shared_uri.rs @@ -0,0 +1,25 @@ +use derive_more::{Deref, DerefMut}; + +use crate::SharedString; + +/// A [`SharedString`] containing a URI. +#[derive(Deref, DerefMut, Default, PartialEq, Eq, Hash, Clone)] +pub struct SharedUri(SharedString); + +impl std::fmt::Debug for SharedUri { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl std::fmt::Display for SharedUri { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.as_ref()) + } +} + +impl> From for SharedUri { + fn from(value: T) -> Self { + Self(value.into()) + } +} diff --git a/third_party/gpui/src/style.rs b/third_party/gpui/src/style.rs new file mode 100644 index 0000000..42f8f25 --- /dev/null +++ b/third_party/gpui/src/style.rs @@ -0,0 +1,1472 @@ +use std::{ + hash::{Hash, Hasher}, + iter, mem, + ops::Range, +}; + +use crate::{ + AbsoluteLength, App, Background, BackgroundTag, BorderStyle, Bounds, ContentMask, Corners, + CornersRefinement, CursorStyle, DefiniteLength, DevicePixels, Edges, EdgesRefinement, Font, + FontFallbacks, FontFeatures, FontStyle, FontWeight, GridLocation, Hsla, Length, Pixels, Point, + PointRefinement, Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, Window, black, phi, + point, quad, rems, size, +}; +use collections::HashSet; +use refineable::Refineable; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// Use this struct for interfacing with the 'debug_below' styling from your own elements. +/// If a parent element has this style set on it, then this struct will be set as a global in +/// GPUI. +#[cfg(debug_assertions)] +pub struct DebugBelow; + +#[cfg(debug_assertions)] +impl crate::Global for DebugBelow {} + +/// How to fit the image into the bounds of the element. +pub enum ObjectFit { + /// The image will be stretched to fill the bounds of the element. + Fill, + /// The image will be scaled to fit within the bounds of the element. + Contain, + /// The image will be scaled to cover the bounds of the element. + Cover, + /// The image will be scaled down to fit within the bounds of the element. + ScaleDown, + /// The image will maintain its original size. + None, +} + +impl ObjectFit { + /// Get the bounds of the image within the given bounds. + pub fn get_bounds( + &self, + bounds: Bounds, + image_size: Size, + ) -> Bounds { + let image_size = image_size.map(|dimension| Pixels::from(u32::from(dimension))); + let image_ratio = image_size.width / image_size.height; + let bounds_ratio = bounds.size.width / bounds.size.height; + + match self { + ObjectFit::Fill => bounds, + ObjectFit::Contain => { + let new_size = if bounds_ratio > image_ratio { + size( + image_size.width * (bounds.size.height / image_size.height), + bounds.size.height, + ) + } else { + size( + bounds.size.width, + image_size.height * (bounds.size.width / image_size.width), + ) + }; + + Bounds { + origin: point( + bounds.origin.x + (bounds.size.width - new_size.width) / 2.0, + bounds.origin.y + (bounds.size.height - new_size.height) / 2.0, + ), + size: new_size, + } + } + ObjectFit::ScaleDown => { + // Check if the image is larger than the bounds in either dimension. + if image_size.width > bounds.size.width || image_size.height > bounds.size.height { + // If the image is larger, use the same logic as Contain to scale it down. + let new_size = if bounds_ratio > image_ratio { + size( + image_size.width * (bounds.size.height / image_size.height), + bounds.size.height, + ) + } else { + size( + bounds.size.width, + image_size.height * (bounds.size.width / image_size.width), + ) + }; + + Bounds { + origin: point( + bounds.origin.x + (bounds.size.width - new_size.width) / 2.0, + bounds.origin.y + (bounds.size.height - new_size.height) / 2.0, + ), + size: new_size, + } + } else { + // If the image is smaller than or equal to the container, display it at its original size, + // centered within the container. + let original_size = size(image_size.width, image_size.height); + Bounds { + origin: point( + bounds.origin.x + (bounds.size.width - original_size.width) / 2.0, + bounds.origin.y + (bounds.size.height - original_size.height) / 2.0, + ), + size: original_size, + } + } + } + ObjectFit::Cover => { + let new_size = if bounds_ratio > image_ratio { + size( + bounds.size.width, + image_size.height * (bounds.size.width / image_size.width), + ) + } else { + size( + image_size.width * (bounds.size.height / image_size.height), + bounds.size.height, + ) + }; + + Bounds { + origin: point( + bounds.origin.x + (bounds.size.width - new_size.width) / 2.0, + bounds.origin.y + (bounds.size.height - new_size.height) / 2.0, + ), + size: new_size, + } + } + ObjectFit::None => Bounds { + origin: bounds.origin, + size: image_size, + }, + } + } +} + +/// The CSS styling that can be applied to an element via the `Styled` trait +#[derive(Clone, Refineable, Debug)] +#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct Style { + /// What layout strategy should be used? + pub display: Display, + + /// Should the element be painted on screen? + pub visibility: Visibility, + + // Overflow properties + /// How children overflowing their container should affect layout + #[refineable] + pub overflow: Point, + /// How much space (in points) should be reserved for the scrollbars of `Overflow::Scroll` and `Overflow::Auto` nodes. + pub scrollbar_width: AbsoluteLength, + /// Whether both x and y axis should be scrollable at the same time. + pub allow_concurrent_scroll: bool, + /// Whether scrolling should be restricted to the axis indicated by the mouse wheel. + /// + /// This means that: + /// - The mouse wheel alone will only ever scroll the Y axis. + /// - Holding `Shift` and using the mouse wheel will scroll the X axis. + /// + /// ## Motivation + /// + /// On the web when scrolling with the mouse wheel, scrolling up and down will always scroll the Y axis, even when + /// the mouse is over a horizontally-scrollable element. + /// + /// The only way to scroll horizontally is to hold down `Shift` while scrolling, which then changes the scroll axis + /// to the X axis. + /// + /// Currently, GPUI operates differently from the web in that it will scroll an element in either the X or Y axis + /// when scrolling with just the mouse wheel. This causes problems when scrolling in a vertical list that contains + /// horizontally-scrollable elements, as when you get to the horizontally-scrollable elements the scroll will be + /// hijacked. + /// + /// Ideally we would match the web's behavior and not have a need for this, but right now we're adding this opt-in + /// style property to limit the potential blast radius. + pub restrict_scroll_to_axis: bool, + + // Position properties + /// What should the `position` value of this struct use as a base offset? + pub position: Position, + /// How should the position of this element be tweaked relative to the layout defined? + #[refineable] + pub inset: Edges, + + // Size properties + /// Sets the initial size of the item + #[refineable] + pub size: Size, + /// Controls the minimum size of the item + #[refineable] + pub min_size: Size, + /// Controls the maximum size of the item + #[refineable] + pub max_size: Size, + /// Sets the preferred aspect ratio for the item. The ratio is calculated as width divided by height. + pub aspect_ratio: Option, + + // Spacing Properties + /// How large should the margin be on each side? + #[refineable] + pub margin: Edges, + /// How large should the padding be on each side? + #[refineable] + pub padding: Edges, + /// How large should the border be on each side? + #[refineable] + pub border_widths: Edges, + + // Alignment properties + /// How this node's children aligned in the cross/block axis? + pub align_items: Option, + /// How this node should be aligned in the cross/block axis. Falls back to the parents [`AlignItems`] if not set + pub align_self: Option, + /// How should content contained within this item be aligned in the cross/block axis + pub align_content: Option, + /// How should contained within this item be aligned in the main/inline axis + pub justify_content: Option, + /// How large should the gaps between items in a flex container be? + #[refineable] + pub gap: Size, + + // Flexbox properties + /// Which direction does the main axis flow in? + pub flex_direction: FlexDirection, + /// Should elements wrap, or stay in a single line? + pub flex_wrap: FlexWrap, + /// Sets the initial main axis size of the item + pub flex_basis: Length, + /// The relative rate at which this item grows when it is expanding to fill space, 0.0 is the default value, and this value must be positive. + pub flex_grow: f32, + /// The relative rate at which this item shrinks when it is contracting to fit into space, 1.0 is the default value, and this value must be positive. + pub flex_shrink: f32, + + /// The fill color of this element + pub background: Option, + + /// The border color of this element + pub border_color: Option, + + /// The border style of this element + pub border_style: BorderStyle, + + /// The radius of the corners of this element + #[refineable] + pub corner_radii: Corners, + + /// Box shadow of the element + pub box_shadow: Vec, + + /// The text style of this element + pub text: TextStyleRefinement, + + /// The mouse cursor style shown when the mouse pointer is over an element. + pub mouse_cursor: Option, + + /// The opacity of this element + pub opacity: Option, + + /// The grid columns of this element + /// Equivalent to the Tailwind `grid-cols-` + pub grid_cols: Option, + + /// The row span of this element + /// Equivalent to the Tailwind `grid-rows-` + pub grid_rows: Option, + + /// The grid location of this element + pub grid_location: Option, + + /// Whether to draw a red debugging outline around this element + #[cfg(debug_assertions)] + pub debug: bool, + + /// Whether to draw a red debugging outline around this element and all of its conforming children + #[cfg(debug_assertions)] + pub debug_below: bool, +} + +impl Styled for StyleRefinement { + fn style(&mut self) -> &mut StyleRefinement { + self + } +} + +impl StyleRefinement { + /// The grid location of this element + pub fn grid_location_mut(&mut self) -> &mut GridLocation { + self.grid_location.get_or_insert_default() + } +} + +/// The value of the visibility property, similar to the CSS property `visibility` +#[derive(Default, Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)] +pub enum Visibility { + /// The element should be drawn as normal. + #[default] + Visible, + /// The element should not be drawn, but should still take up space in the layout. + Hidden, +} + +/// The possible values of the box-shadow property +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct BoxShadow { + /// What color should the shadow have? + pub color: Hsla, + /// How should it be offset from its element? + pub offset: Point, + /// How much should the shadow be blurred? + pub blur_radius: Pixels, + /// How much should the shadow spread? + pub spread_radius: Pixels, +} + +/// How to handle whitespace in text +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub enum WhiteSpace { + /// Normal line wrapping when text overflows the width of the element + #[default] + Normal, + /// No line wrapping, text will overflow the width of the element + Nowrap, +} + +/// How to truncate text that overflows the width of the element +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub enum TextOverflow { + /// Truncate the text when it doesn't fit, and represent this truncation by displaying the + /// provided string. + Truncate(SharedString), +} + +/// How to align text within the element +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub enum TextAlign { + /// Align the text to the left of the element + #[default] + Left, + + /// Center the text within the element + Center, + + /// Align the text to the right of the element + Right, +} + +/// The properties that can be used to style text in GPUI +#[derive(Refineable, Clone, Debug, PartialEq)] +#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct TextStyle { + /// The color of the text + pub color: Hsla, + + /// The font family to use + pub font_family: SharedString, + + /// The font features to use + pub font_features: FontFeatures, + + /// The fallback fonts to use + pub font_fallbacks: Option, + + /// The font size to use, in pixels or rems. + pub font_size: AbsoluteLength, + + /// The line height to use, in pixels or fractions + pub line_height: DefiniteLength, + + /// The font weight, e.g. bold + pub font_weight: FontWeight, + + /// The font style, e.g. italic + pub font_style: FontStyle, + + /// The background color of the text + pub background_color: Option, + + /// The underline style of the text + pub underline: Option, + + /// The strikethrough style of the text + pub strikethrough: Option, + + /// How to handle whitespace in the text + pub white_space: WhiteSpace, + + /// The text should be truncated if it overflows the width of the element + pub text_overflow: Option, + + /// How the text should be aligned within the element + pub text_align: TextAlign, + + /// The number of lines to display before truncating the text + pub line_clamp: Option, +} + +impl Default for TextStyle { + fn default() -> Self { + TextStyle { + color: black(), + // todo(linux) make this configurable or choose better default + font_family: ".SystemUIFont".into(), + font_features: FontFeatures::default(), + font_fallbacks: None, + font_size: rems(1.).into(), + line_height: phi(), + font_weight: FontWeight::default(), + font_style: FontStyle::default(), + background_color: None, + underline: None, + strikethrough: None, + white_space: WhiteSpace::Normal, + text_overflow: None, + text_align: TextAlign::default(), + line_clamp: None, + } + } +} + +impl TextStyle { + /// Create a new text style with the given highlighting applied. + pub fn highlight(mut self, style: impl Into) -> Self { + let style = style.into(); + if let Some(weight) = style.font_weight { + self.font_weight = weight; + } + if let Some(style) = style.font_style { + self.font_style = style; + } + + if let Some(color) = style.color { + self.color = self.color.blend(color); + } + + if let Some(factor) = style.fade_out { + self.color.fade_out(factor); + } + + if let Some(background_color) = style.background_color { + self.background_color = Some(background_color); + } + + if let Some(underline) = style.underline { + self.underline = Some(underline); + } + + if let Some(strikethrough) = style.strikethrough { + self.strikethrough = Some(strikethrough); + } + + self + } + + /// Get the font configured for this text style. + pub fn font(&self) -> Font { + Font { + family: self.font_family.clone(), + features: self.font_features.clone(), + fallbacks: self.font_fallbacks.clone(), + weight: self.font_weight, + style: self.font_style, + } + } + + /// Returns the rounded line height in pixels. + pub fn line_height_in_pixels(&self, rem_size: Pixels) -> Pixels { + self.line_height.to_pixels(self.font_size, rem_size).round() + } + + /// Convert this text style into a [`TextRun`], for the given length of the text. + pub fn to_run(&self, len: usize) -> TextRun { + TextRun { + len, + font: Font { + family: self.font_family.clone(), + features: self.font_features.clone(), + fallbacks: self.font_fallbacks.clone(), + weight: self.font_weight, + style: self.font_style, + }, + color: self.color, + background_color: self.background_color, + underline: self.underline, + strikethrough: self.strikethrough, + } + } +} + +/// A highlight style to apply, similar to a `TextStyle` except +/// for a single font, uniformly sized and spaced text. +#[derive(Copy, Clone, Debug, Default, PartialEq)] +pub struct HighlightStyle { + /// The color of the text + pub color: Option, + + /// The font weight, e.g. bold + pub font_weight: Option, + + /// The font style, e.g. italic + pub font_style: Option, + + /// The background color of the text + pub background_color: Option, + + /// The underline style of the text + pub underline: Option, + + /// The underline style of the text + pub strikethrough: Option, + + /// Similar to the CSS `opacity` property, this will cause the text to be less vibrant. + pub fade_out: Option, +} + +impl Eq for HighlightStyle {} + +impl Hash for HighlightStyle { + fn hash(&self, state: &mut H) { + self.color.hash(state); + self.font_weight.hash(state); + self.font_style.hash(state); + self.background_color.hash(state); + self.underline.hash(state); + self.strikethrough.hash(state); + state.write_u32(u32::from_be_bytes( + self.fade_out.map(|f| f.to_be_bytes()).unwrap_or_default(), + )); + } +} + +impl Style { + /// Returns true if the style is visible and the background is opaque. + pub fn has_opaque_background(&self) -> bool { + self.background + .as_ref() + .is_some_and(|fill| fill.color().is_some_and(|color| !color.is_transparent())) + } + + /// Get the text style in this element style. + pub fn text_style(&self) -> Option<&TextStyleRefinement> { + if self.text.is_some() { + Some(&self.text) + } else { + None + } + } + + /// Get the content mask for this element style, based on the given bounds. + /// If the element does not hide its overflow, this will return `None`. + pub fn overflow_mask( + &self, + bounds: Bounds, + rem_size: Pixels, + ) -> Option> { + match self.overflow { + Point { + x: Overflow::Visible, + y: Overflow::Visible, + } => None, + _ => { + let mut min = bounds.origin; + let mut max = bounds.bottom_right(); + + if self + .border_color + .is_some_and(|color| !color.is_transparent()) + { + min.x += self.border_widths.left.to_pixels(rem_size); + max.x -= self.border_widths.right.to_pixels(rem_size); + min.y += self.border_widths.top.to_pixels(rem_size); + max.y -= self.border_widths.bottom.to_pixels(rem_size); + } + + let bounds = match ( + self.overflow.x == Overflow::Visible, + self.overflow.y == Overflow::Visible, + ) { + // x and y both visible + (true, true) => return None, + // x visible, y hidden + (true, false) => Bounds::from_corners( + point(min.x, bounds.origin.y), + point(max.x, bounds.bottom_right().y), + ), + // x hidden, y visible + (false, true) => Bounds::from_corners( + point(bounds.origin.x, min.y), + point(bounds.bottom_right().x, max.y), + ), + // both hidden + (false, false) => Bounds::from_corners(min, max), + }; + + Some(ContentMask { bounds }) + } + } + } + + /// Paints the background of an element styled with this style. + pub fn paint( + &self, + bounds: Bounds, + window: &mut Window, + cx: &mut App, + continuation: impl FnOnce(&mut Window, &mut App), + ) { + #[cfg(debug_assertions)] + if self.debug_below { + cx.set_global(DebugBelow) + } + + #[cfg(debug_assertions)] + if self.debug || cx.has_global::() { + window.paint_quad(crate::outline(bounds, crate::red(), BorderStyle::default())); + } + + let rem_size = window.rem_size(); + let corner_radii = self + .corner_radii + .to_pixels(rem_size) + .clamp_radii_for_quad_size(bounds.size); + + window.paint_shadows(bounds, corner_radii, &self.box_shadow); + + let background_color = self.background.as_ref().and_then(Fill::color); + if background_color.is_some_and(|color| !color.is_transparent()) { + let mut border_color = match background_color { + Some(color) => match color.tag { + BackgroundTag::Solid => color.solid, + BackgroundTag::LinearGradient => color + .colors + .first() + .map(|stop| stop.color) + .unwrap_or_default(), + BackgroundTag::PatternSlash => color.solid, + }, + None => Hsla::default(), + }; + border_color.a = 0.; + window.paint_quad(quad( + bounds, + corner_radii, + background_color.unwrap_or_default(), + Edges::default(), + border_color, + self.border_style, + )); + } + + continuation(window, cx); + + if self.is_border_visible() { + let border_widths = self.border_widths.to_pixels(rem_size); + let max_border_width = border_widths.max(); + let max_corner_radius = corner_radii.max(); + + let top_bounds = Bounds::from_corners( + bounds.origin, + bounds.top_right() + point(Pixels::ZERO, max_border_width.max(max_corner_radius)), + ); + let bottom_bounds = Bounds::from_corners( + bounds.bottom_left() - point(Pixels::ZERO, max_border_width.max(max_corner_radius)), + bounds.bottom_right(), + ); + let left_bounds = Bounds::from_corners( + top_bounds.bottom_left(), + bottom_bounds.origin + point(max_border_width, Pixels::ZERO), + ); + let right_bounds = Bounds::from_corners( + top_bounds.bottom_right() - point(max_border_width, Pixels::ZERO), + bottom_bounds.top_right(), + ); + + let mut background = self.border_color.unwrap_or_default(); + background.a = 0.; + let quad = quad( + bounds, + corner_radii, + background, + border_widths, + self.border_color.unwrap_or_default(), + self.border_style, + ); + + window.with_content_mask(Some(ContentMask { bounds: top_bounds }), |window| { + window.paint_quad(quad.clone()); + }); + window.with_content_mask( + Some(ContentMask { + bounds: right_bounds, + }), + |window| { + window.paint_quad(quad.clone()); + }, + ); + window.with_content_mask( + Some(ContentMask { + bounds: bottom_bounds, + }), + |window| { + window.paint_quad(quad.clone()); + }, + ); + window.with_content_mask( + Some(ContentMask { + bounds: left_bounds, + }), + |window| { + window.paint_quad(quad); + }, + ); + } + + #[cfg(debug_assertions)] + if self.debug_below { + cx.remove_global::(); + } + } + + fn is_border_visible(&self) -> bool { + self.border_color + .is_some_and(|color| !color.is_transparent()) + && self.border_widths.any(|length| !length.is_zero()) + } +} + +impl Default for Style { + fn default() -> Self { + Style { + display: Display::Block, + visibility: Visibility::Visible, + overflow: Point { + x: Overflow::Visible, + y: Overflow::Visible, + }, + allow_concurrent_scroll: false, + restrict_scroll_to_axis: false, + scrollbar_width: AbsoluteLength::default(), + position: Position::Relative, + inset: Edges::auto(), + margin: Edges::::zero(), + padding: Edges::::zero(), + border_widths: Edges::::zero(), + size: Size::auto(), + min_size: Size::auto(), + max_size: Size::auto(), + aspect_ratio: None, + gap: Size::default(), + // Alignment + align_items: None, + align_self: None, + align_content: None, + justify_content: None, + // Flexbox + flex_direction: FlexDirection::Row, + flex_wrap: FlexWrap::NoWrap, + flex_grow: 0.0, + flex_shrink: 1.0, + flex_basis: Length::Auto, + background: None, + border_color: None, + border_style: BorderStyle::default(), + corner_radii: Corners::default(), + box_shadow: Default::default(), + text: TextStyleRefinement::default(), + mouse_cursor: None, + opacity: None, + grid_rows: None, + grid_cols: None, + grid_location: None, + + #[cfg(debug_assertions)] + debug: false, + #[cfg(debug_assertions)] + debug_below: false, + } + } +} + +/// The properties that can be applied to an underline. +#[derive( + Refineable, Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, +)] +pub struct UnderlineStyle { + /// The thickness of the underline. + pub thickness: Pixels, + + /// The color of the underline. + pub color: Option, + + /// Whether the underline should be wavy, like in a spell checker. + pub wavy: bool, +} + +/// The properties that can be applied to a strikethrough. +#[derive( + Refineable, Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, +)] +pub struct StrikethroughStyle { + /// The thickness of the strikethrough. + pub thickness: Pixels, + + /// The color of the strikethrough. + pub color: Option, +} + +/// The kinds of fill that can be applied to a shape. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub enum Fill { + /// A solid color fill. + Color(Background), +} + +impl Fill { + /// Unwrap this fill into a solid color, if it is one. + /// + /// If the fill is not a solid color, this method returns `None`. + pub fn color(&self) -> Option { + match self { + Fill::Color(color) => Some(*color), + } + } +} + +impl Default for Fill { + fn default() -> Self { + Self::Color(Background::default()) + } +} + +impl From for Fill { + fn from(color: Hsla) -> Self { + Self::Color(color.into()) + } +} + +impl From for Fill { + fn from(color: Rgba) -> Self { + Self::Color(color.into()) + } +} + +impl From for Fill { + fn from(background: Background) -> Self { + Self::Color(background) + } +} + +impl From for HighlightStyle { + fn from(other: TextStyle) -> Self { + Self::from(&other) + } +} + +impl From<&TextStyle> for HighlightStyle { + fn from(other: &TextStyle) -> Self { + Self { + color: Some(other.color), + font_weight: Some(other.font_weight), + font_style: Some(other.font_style), + background_color: other.background_color, + underline: other.underline, + strikethrough: other.strikethrough, + fade_out: None, + } + } +} + +impl HighlightStyle { + /// Create a highlight style with just a color + pub fn color(color: Hsla) -> Self { + Self { + color: Some(color), + ..Default::default() + } + } + /// Blend this highlight style with another. + /// Non-continuous properties, like font_weight and font_style, are overwritten. + #[must_use] + pub fn highlight(self, other: HighlightStyle) -> Self { + Self { + color: other + .color + .map(|other_color| { + if let Some(color) = self.color { + color.blend(other_color) + } else { + other_color + } + }) + .or(self.color), + font_weight: other.font_weight.or(self.font_weight), + font_style: other.font_style.or(self.font_style), + background_color: other.background_color.or(self.background_color), + underline: other.underline.or(self.underline), + strikethrough: other.strikethrough.or(self.strikethrough), + fade_out: other + .fade_out + .map(|source_fade| { + self.fade_out + .map(|dest_fade| (dest_fade * (1. + source_fade)).clamp(0., 1.)) + .unwrap_or(source_fade) + }) + .or(self.fade_out), + } + } +} + +impl From for HighlightStyle { + fn from(color: Hsla) -> Self { + Self { + color: Some(color), + ..Default::default() + } + } +} + +impl From for HighlightStyle { + fn from(font_weight: FontWeight) -> Self { + Self { + font_weight: Some(font_weight), + ..Default::default() + } + } +} + +impl From for HighlightStyle { + fn from(font_style: FontStyle) -> Self { + Self { + font_style: Some(font_style), + ..Default::default() + } + } +} + +impl From for HighlightStyle { + fn from(color: Rgba) -> Self { + Self { + color: Some(color.into()), + ..Default::default() + } + } +} + +/// Combine and merge the highlights and ranges in the two iterators. +pub fn combine_highlights( + a: impl IntoIterator, HighlightStyle)>, + b: impl IntoIterator, HighlightStyle)>, +) -> impl Iterator, HighlightStyle)> { + let mut endpoints = Vec::new(); + let mut highlights = Vec::new(); + for (range, highlight) in a.into_iter().chain(b) { + if !range.is_empty() { + let highlight_id = highlights.len(); + endpoints.push((range.start, highlight_id, true)); + endpoints.push((range.end, highlight_id, false)); + highlights.push(highlight); + } + } + endpoints.sort_unstable_by_key(|(position, _, _)| *position); + let mut endpoints = endpoints.into_iter().peekable(); + + let mut active_styles = HashSet::default(); + let mut ix = 0; + iter::from_fn(move || { + while let Some((endpoint_ix, highlight_id, is_start)) = endpoints.peek() { + let prev_index = mem::replace(&mut ix, *endpoint_ix); + if ix > prev_index && !active_styles.is_empty() { + let current_style = active_styles + .iter() + .fold(HighlightStyle::default(), |acc, highlight_id| { + acc.highlight(highlights[*highlight_id]) + }); + return Some((prev_index..ix, current_style)); + } + + if *is_start { + active_styles.insert(*highlight_id); + } else { + active_styles.remove(highlight_id); + } + endpoints.next(); + } + None + }) +} + +/// Used to control how child nodes are aligned. +/// For Flexbox it controls alignment in the cross axis +/// For Grid it controls alignment in the block axis +/// +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-items) +#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)] +// Copy of taffy::style type of the same name, to derive JsonSchema. +pub enum AlignItems { + /// Items are packed toward the start of the axis + Start, + /// Items are packed toward the end of the axis + End, + /// Items are packed towards the flex-relative start of the axis. + /// + /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent + /// to End. In all other cases it is equivalent to Start. + FlexStart, + /// Items are packed towards the flex-relative end of the axis. + /// + /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent + /// to Start. In all other cases it is equivalent to End. + FlexEnd, + /// Items are packed along the center of the cross axis + Center, + /// Items are aligned such as their baselines align + Baseline, + /// Stretch to fill the container + Stretch, +} +/// Used to control how child nodes are aligned. +/// Does not apply to Flexbox, and will be ignored if specified on a flex container +/// For Grid it controls alignment in the inline axis +/// +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-items) +pub type JustifyItems = AlignItems; +/// Used to control how the specified nodes is aligned. +/// Overrides the parent Node's `AlignItems` property. +/// For Flexbox it controls alignment in the cross axis +/// For Grid it controls alignment in the block axis +/// +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-self) +pub type AlignSelf = AlignItems; +/// Used to control how the specified nodes is aligned. +/// Overrides the parent Node's `JustifyItems` property. +/// Does not apply to Flexbox, and will be ignored if specified on a flex child +/// For Grid it controls alignment in the inline axis +/// +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-self) +pub type JustifySelf = AlignItems; + +/// Sets the distribution of space between and around content items +/// For Flexbox it controls alignment in the cross axis +/// For Grid it controls alignment in the block axis +/// +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-content) +#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)] +// Copy of taffy::style type of the same name, to derive JsonSchema. +pub enum AlignContent { + /// Items are packed toward the start of the axis + Start, + /// Items are packed toward the end of the axis + End, + /// Items are packed towards the flex-relative start of the axis. + /// + /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent + /// to End. In all other cases it is equivalent to Start. + FlexStart, + /// Items are packed towards the flex-relative end of the axis. + /// + /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent + /// to Start. In all other cases it is equivalent to End. + FlexEnd, + /// Items are centered around the middle of the axis + Center, + /// Items are stretched to fill the container + Stretch, + /// The first and last items are aligned flush with the edges of the container (no gap) + /// The gap between items is distributed evenly. + SpaceBetween, + /// The gap between the first and last items is exactly THE SAME as the gap between items. + /// The gaps are distributed evenly + SpaceEvenly, + /// The gap between the first and last items is exactly HALF the gap between items. + /// The gaps are distributed evenly in proportion to these ratios. + SpaceAround, +} + +/// Sets the distribution of space between and around content items +/// For Flexbox it controls alignment in the main axis +/// For Grid it controls alignment in the inline axis +/// +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content) +pub type JustifyContent = AlignContent; + +/// Sets the layout used for the children of this node +/// +/// The default values depends on on which feature flags are enabled. The order of precedence is: Flex, Grid, Block, None. +#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)] +// Copy of taffy::style type of the same name, to derive JsonSchema. +pub enum Display { + /// The children will follow the block layout algorithm + Block, + /// The children will follow the flexbox layout algorithm + #[default] + Flex, + /// The children will follow the CSS Grid layout algorithm + Grid, + /// The children will not be laid out, and will follow absolute positioning + None, +} + +/// Controls whether flex items are forced onto one line or can wrap onto multiple lines. +/// +/// Defaults to [`FlexWrap::NoWrap`] +/// +/// [Specification](https://www.w3.org/TR/css-flexbox-1/#flex-wrap-property) +#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)] +// Copy of taffy::style type of the same name, to derive JsonSchema. +pub enum FlexWrap { + /// Items will not wrap and stay on a single line + #[default] + NoWrap, + /// Items will wrap according to this item's [`FlexDirection`] + Wrap, + /// Items will wrap in the opposite direction to this item's [`FlexDirection`] + WrapReverse, +} + +/// The direction of the flexbox layout main axis. +/// +/// There are always two perpendicular layout axes: main (or primary) and cross (or secondary). +/// Adding items will cause them to be positioned adjacent to each other along the main axis. +/// By varying this value throughout your tree, you can create complex axis-aligned layouts. +/// +/// Items are always aligned relative to the cross axis, and justified relative to the main axis. +/// +/// The default behavior is [`FlexDirection::Row`]. +/// +/// [Specification](https://www.w3.org/TR/css-flexbox-1/#flex-direction-property) +#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)] +// Copy of taffy::style type of the same name, to derive JsonSchema. +pub enum FlexDirection { + /// Defines +x as the main axis + /// + /// Items will be added from left to right in a row. + #[default] + Row, + /// Defines +y as the main axis + /// + /// Items will be added from top to bottom in a column. + Column, + /// Defines -x as the main axis + /// + /// Items will be added from right to left in a row. + RowReverse, + /// Defines -y as the main axis + /// + /// Items will be added from bottom to top in a column. + ColumnReverse, +} + +/// How children overflowing their container should affect layout +/// +/// In CSS the primary effect of this property is to control whether contents of a parent container that overflow that container should +/// be displayed anyway, be clipped, or trigger the container to become a scroll container. However it also has secondary effects on layout, +/// the main ones being: +/// +/// - The automatic minimum size Flexbox/CSS Grid items with non-`Visible` overflow is `0` rather than being content based +/// - `Overflow::Scroll` nodes have space in the layout reserved for a scrollbar (width controlled by the `scrollbar_width` property) +/// +/// In Taffy, we only implement the layout related secondary effects as we are not concerned with drawing/painting. The amount of space reserved for +/// a scrollbar is controlled by the `scrollbar_width` property. If this is `0` then `Scroll` behaves identically to `Hidden`. +/// +/// +#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)] +// Copy of taffy::style type of the same name, to derive JsonSchema. +pub enum Overflow { + /// The automatic minimum size of this node as a flexbox/grid item should be based on the size of its content. + /// Content that overflows this node *should* contribute to the scroll region of its parent. + #[default] + Visible, + /// The automatic minimum size of this node as a flexbox/grid item should be based on the size of its content. + /// Content that overflows this node should *not* contribute to the scroll region of its parent. + Clip, + /// The automatic minimum size of this node as a flexbox/grid item should be `0`. + /// Content that overflows this node should *not* contribute to the scroll region of its parent. + Hidden, + /// The automatic minimum size of this node as a flexbox/grid item should be `0`. Additionally, space should be reserved + /// for a scrollbar. The amount of space reserved is controlled by the `scrollbar_width` property. + /// Content that overflows this node should *not* contribute to the scroll region of its parent. + Scroll, +} + +/// The positioning strategy for this item. +/// +/// This controls both how the origin is determined for the [`Style::position`] field, +/// and whether or not the item will be controlled by flexbox's layout algorithm. +/// +/// WARNING: this enum follows the behavior of [CSS's `position` property](https://developer.mozilla.org/en-US/docs/Web/CSS/position), +/// which can be unintuitive. +/// +/// [`Position::Relative`] is the default value, in contrast to the default behavior in CSS. +#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)] +// Copy of taffy::style type of the same name, to derive JsonSchema. +pub enum Position { + /// The offset is computed relative to the final position given by the layout algorithm. + /// Offsets do not affect the position of any other items; they are effectively a correction factor applied at the end. + #[default] + Relative, + /// The offset is computed relative to this item's closest positioned ancestor, if any. + /// Otherwise, it is placed relative to the origin. + /// No space is created for the item in the page layout, and its size will not be altered. + /// + /// WARNING: to opt-out of layouting entirely, you must use [`Display::None`] instead on your [`Style`] object. + Absolute, +} + +impl From for taffy::style::AlignItems { + fn from(value: AlignItems) -> Self { + match value { + AlignItems::Start => Self::Start, + AlignItems::End => Self::End, + AlignItems::FlexStart => Self::FlexStart, + AlignItems::FlexEnd => Self::FlexEnd, + AlignItems::Center => Self::Center, + AlignItems::Baseline => Self::Baseline, + AlignItems::Stretch => Self::Stretch, + } + } +} + +impl From for taffy::style::AlignContent { + fn from(value: AlignContent) -> Self { + match value { + AlignContent::Start => Self::Start, + AlignContent::End => Self::End, + AlignContent::FlexStart => Self::FlexStart, + AlignContent::FlexEnd => Self::FlexEnd, + AlignContent::Center => Self::Center, + AlignContent::Stretch => Self::Stretch, + AlignContent::SpaceBetween => Self::SpaceBetween, + AlignContent::SpaceEvenly => Self::SpaceEvenly, + AlignContent::SpaceAround => Self::SpaceAround, + } + } +} + +impl From for taffy::style::Display { + fn from(value: Display) -> Self { + match value { + Display::Block => Self::Block, + Display::Flex => Self::Flex, + Display::Grid => Self::Grid, + Display::None => Self::None, + } + } +} + +impl From for taffy::style::FlexWrap { + fn from(value: FlexWrap) -> Self { + match value { + FlexWrap::NoWrap => Self::NoWrap, + FlexWrap::Wrap => Self::Wrap, + FlexWrap::WrapReverse => Self::WrapReverse, + } + } +} + +impl From for taffy::style::FlexDirection { + fn from(value: FlexDirection) -> Self { + match value { + FlexDirection::Row => Self::Row, + FlexDirection::Column => Self::Column, + FlexDirection::RowReverse => Self::RowReverse, + FlexDirection::ColumnReverse => Self::ColumnReverse, + } + } +} + +impl From for taffy::style::Overflow { + fn from(value: Overflow) -> Self { + match value { + Overflow::Visible => Self::Visible, + Overflow::Clip => Self::Clip, + Overflow::Hidden => Self::Hidden, + Overflow::Scroll => Self::Scroll, + } + } +} + +impl From for taffy::style::Position { + fn from(value: Position) -> Self { + match value { + Position::Relative => Self::Relative, + Position::Absolute => Self::Absolute, + } + } +} + +#[cfg(test)] +mod tests { + use crate::{blue, green, px, red, yellow}; + + use super::*; + + use util_macros::perf; + + #[perf] + fn test_basic_highlight_style_combination() { + let style_a = HighlightStyle::default(); + let style_b = HighlightStyle::default(); + let style_a = style_a.highlight(style_b); + assert_eq!( + style_a, + HighlightStyle::default(), + "Combining empty styles should not produce a non-empty style." + ); + + let mut style_b = HighlightStyle { + color: Some(red()), + strikethrough: Some(StrikethroughStyle { + thickness: px(2.), + color: Some(blue()), + }), + fade_out: Some(0.), + font_style: Some(FontStyle::Italic), + font_weight: Some(FontWeight(300.)), + background_color: Some(yellow()), + underline: Some(UnderlineStyle { + thickness: px(2.), + color: Some(red()), + wavy: true, + }), + }; + let expected_style = style_b; + + let style_a = style_a.highlight(style_b); + assert_eq!( + style_a, expected_style, + "Blending an empty style with another style should return the other style" + ); + + let style_b = style_b.highlight(Default::default()); + assert_eq!( + style_b, expected_style, + "Blending a style with an empty style should not change the style." + ); + + let mut style_c = expected_style; + + let style_d = HighlightStyle { + color: Some(blue().alpha(0.7)), + strikethrough: Some(StrikethroughStyle { + thickness: px(4.), + color: Some(crate::red()), + }), + fade_out: Some(0.), + font_style: Some(FontStyle::Oblique), + font_weight: Some(FontWeight(800.)), + background_color: Some(green()), + underline: Some(UnderlineStyle { + thickness: px(4.), + color: None, + wavy: false, + }), + }; + + let expected_style = HighlightStyle { + color: Some(red().blend(blue().alpha(0.7))), + strikethrough: Some(StrikethroughStyle { + thickness: px(4.), + color: Some(red()), + }), + // TODO this does not seem right + fade_out: Some(0.), + font_style: Some(FontStyle::Oblique), + font_weight: Some(FontWeight(800.)), + background_color: Some(green()), + underline: Some(UnderlineStyle { + thickness: px(4.), + color: None, + wavy: false, + }), + }; + + let style_c = style_c.highlight(style_d); + assert_eq!( + style_c, expected_style, + "Blending styles should blend properties where possible and override all others" + ); + } + + #[perf] + fn test_combine_highlights() { + assert_eq!( + combine_highlights( + [ + (0..5, green().into()), + (4..10, FontWeight::BOLD.into()), + (15..20, yellow().into()), + ], + [ + (2..6, FontStyle::Italic.into()), + (1..3, blue().into()), + (21..23, red().into()), + ] + ) + .collect::>(), + [ + ( + 0..1, + HighlightStyle { + color: Some(green()), + ..Default::default() + } + ), + ( + 1..2, + HighlightStyle { + color: Some(blue()), + ..Default::default() + } + ), + ( + 2..3, + HighlightStyle { + color: Some(blue()), + font_style: Some(FontStyle::Italic), + ..Default::default() + } + ), + ( + 3..4, + HighlightStyle { + color: Some(green()), + font_style: Some(FontStyle::Italic), + ..Default::default() + } + ), + ( + 4..5, + HighlightStyle { + color: Some(green()), + font_weight: Some(FontWeight::BOLD), + font_style: Some(FontStyle::Italic), + ..Default::default() + } + ), + ( + 5..6, + HighlightStyle { + font_weight: Some(FontWeight::BOLD), + font_style: Some(FontStyle::Italic), + ..Default::default() + } + ), + ( + 6..10, + HighlightStyle { + font_weight: Some(FontWeight::BOLD), + ..Default::default() + } + ), + ( + 15..20, + HighlightStyle { + color: Some(yellow()), + ..Default::default() + } + ), + ( + 21..23, + HighlightStyle { + color: Some(red()), + ..Default::default() + } + ) + ] + ); + } +} diff --git a/third_party/gpui/src/styled.rs b/third_party/gpui/src/styled.rs new file mode 100644 index 0000000..4475718 --- /dev/null +++ b/third_party/gpui/src/styled.rs @@ -0,0 +1,766 @@ +use crate::{ + self as gpui, AbsoluteLength, AlignContent, AlignItems, BorderStyle, CursorStyle, + DefiniteLength, Display, Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight, + GridPlacement, Hsla, JustifyContent, Length, SharedString, StrikethroughStyle, StyleRefinement, + TextAlign, TextOverflow, TextStyleRefinement, UnderlineStyle, WhiteSpace, px, relative, rems, +}; +pub use gpui_macros::{ + border_style_methods, box_shadow_style_methods, cursor_style_methods, margin_style_methods, + overflow_style_methods, padding_style_methods, position_style_methods, + visibility_style_methods, +}; + +const ELLIPSIS: SharedString = SharedString::new_static("…"); + +/// A trait for elements that can be styled. +/// Use this to opt-in to a utility CSS-like styling API. +#[cfg_attr( + any(feature = "inspector", debug_assertions), + gpui_macros::derive_inspector_reflection +)] +pub trait Styled: Sized { + /// Returns a reference to the style memory of this element. + fn style(&mut self) -> &mut StyleRefinement; + + gpui_macros::style_helpers!(); + gpui_macros::visibility_style_methods!(); + gpui_macros::margin_style_methods!(); + gpui_macros::padding_style_methods!(); + gpui_macros::position_style_methods!(); + gpui_macros::overflow_style_methods!(); + gpui_macros::cursor_style_methods!(); + gpui_macros::border_style_methods!(); + gpui_macros::box_shadow_style_methods!(); + + /// Sets the display type of the element to `block`. + /// [Docs](https://tailwindcss.com/docs/display) + fn block(mut self) -> Self { + self.style().display = Some(Display::Block); + self + } + + /// Sets the display type of the element to `flex`. + /// [Docs](https://tailwindcss.com/docs/display) + fn flex(mut self) -> Self { + self.style().display = Some(Display::Flex); + self + } + + /// Sets the display type of the element to `grid`. + /// [Docs](https://tailwindcss.com/docs/display) + fn grid(mut self) -> Self { + self.style().display = Some(Display::Grid); + self + } + + /// Sets the display type of the element to `none`. + /// [Docs](https://tailwindcss.com/docs/display) + fn hidden(mut self) -> Self { + self.style().display = Some(Display::None); + self + } + + /// Sets the whitespace of the element to `normal`. + /// [Docs](https://tailwindcss.com/docs/whitespace#normal) + fn whitespace_normal(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .white_space = Some(WhiteSpace::Normal); + self + } + + /// Sets the whitespace of the element to `nowrap`. + /// [Docs](https://tailwindcss.com/docs/whitespace#nowrap) + fn whitespace_nowrap(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .white_space = Some(WhiteSpace::Nowrap); + self + } + + /// Sets the truncate overflowing text with an ellipsis (…) if needed. + /// [Docs](https://tailwindcss.com/docs/text-overflow#ellipsis) + fn text_ellipsis(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .text_overflow = Some(TextOverflow::Truncate(ELLIPSIS)); + self + } + + /// Sets the text overflow behavior of the element. + fn text_overflow(mut self, overflow: TextOverflow) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .text_overflow = Some(overflow); + self + } + + /// Set the text alignment of the element. + fn text_align(mut self, align: TextAlign) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .text_align = Some(align); + self + } + + /// Sets the text alignment to left + fn text_left(mut self) -> Self { + self.text_align(TextAlign::Left) + } + + /// Sets the text alignment to center + fn text_center(mut self) -> Self { + self.text_align(TextAlign::Center) + } + + /// Sets the text alignment to right + fn text_right(mut self) -> Self { + self.text_align(TextAlign::Right) + } + + /// Sets the truncate to prevent text from wrapping and truncate overflowing text with an ellipsis (…) if needed. + /// [Docs](https://tailwindcss.com/docs/text-overflow#truncate) + fn truncate(mut self) -> Self { + self.overflow_hidden().whitespace_nowrap().text_ellipsis() + } + + /// Sets number of lines to show before truncating the text. + /// [Docs](https://tailwindcss.com/docs/line-clamp) + fn line_clamp(mut self, lines: usize) -> Self { + let mut text_style = self.text_style().get_or_insert_with(Default::default); + text_style.line_clamp = Some(lines); + self.overflow_hidden() + } + + /// Sets the flex direction of the element to `column`. + /// [Docs](https://tailwindcss.com/docs/flex-direction#column) + fn flex_col(mut self) -> Self { + self.style().flex_direction = Some(FlexDirection::Column); + self + } + + /// Sets the flex direction of the element to `column-reverse`. + /// [Docs](https://tailwindcss.com/docs/flex-direction#column-reverse) + fn flex_col_reverse(mut self) -> Self { + self.style().flex_direction = Some(FlexDirection::ColumnReverse); + self + } + + /// Sets the flex direction of the element to `row`. + /// [Docs](https://tailwindcss.com/docs/flex-direction#row) + fn flex_row(mut self) -> Self { + self.style().flex_direction = Some(FlexDirection::Row); + self + } + + /// Sets the flex direction of the element to `row-reverse`. + /// [Docs](https://tailwindcss.com/docs/flex-direction#row-reverse) + fn flex_row_reverse(mut self) -> Self { + self.style().flex_direction = Some(FlexDirection::RowReverse); + self + } + + /// Sets the element to allow a flex item to grow and shrink as needed, ignoring its initial size. + /// [Docs](https://tailwindcss.com/docs/flex#flex-1) + fn flex_1(mut self) -> Self { + self.style().flex_grow = Some(1.); + self.style().flex_shrink = Some(1.); + self.style().flex_basis = Some(relative(0.).into()); + self + } + + /// Sets the element to allow a flex item to grow and shrink, taking into account its initial size. + /// [Docs](https://tailwindcss.com/docs/flex#auto) + fn flex_auto(mut self) -> Self { + self.style().flex_grow = Some(1.); + self.style().flex_shrink = Some(1.); + self.style().flex_basis = Some(Length::Auto); + self + } + + /// Sets the element to allow a flex item to shrink but not grow, taking into account its initial size. + /// [Docs](https://tailwindcss.com/docs/flex#initial) + fn flex_initial(mut self) -> Self { + self.style().flex_grow = Some(0.); + self.style().flex_shrink = Some(1.); + self.style().flex_basis = Some(Length::Auto); + self + } + + /// Sets the element to prevent a flex item from growing or shrinking. + /// [Docs](https://tailwindcss.com/docs/flex#none) + fn flex_none(mut self) -> Self { + self.style().flex_grow = Some(0.); + self.style().flex_shrink = Some(0.); + self + } + + /// Sets the initial size of flex items for this element. + /// [Docs](https://tailwindcss.com/docs/flex-basis) + fn flex_basis(mut self, basis: impl Into) -> Self { + self.style().flex_basis = Some(basis.into()); + self + } + + /// Sets the element to allow a flex item to grow to fill any available space. + /// [Docs](https://tailwindcss.com/docs/flex-grow) + fn flex_grow(mut self) -> Self { + self.style().flex_grow = Some(1.); + self + } + + /// Sets the element to allow a flex item to shrink if needed. + /// [Docs](https://tailwindcss.com/docs/flex-shrink) + fn flex_shrink(mut self) -> Self { + self.style().flex_shrink = Some(1.); + self + } + + /// Sets the element to prevent a flex item from shrinking. + /// [Docs](https://tailwindcss.com/docs/flex-shrink#dont-shrink) + fn flex_shrink_0(mut self) -> Self { + self.style().flex_shrink = Some(0.); + self + } + + /// Sets the element to allow flex items to wrap. + /// [Docs](https://tailwindcss.com/docs/flex-wrap#wrap-normally) + fn flex_wrap(mut self) -> Self { + self.style().flex_wrap = Some(FlexWrap::Wrap); + self + } + + /// Sets the element wrap flex items in the reverse direction. + /// [Docs](https://tailwindcss.com/docs/flex-wrap#wrap-reversed) + fn flex_wrap_reverse(mut self) -> Self { + self.style().flex_wrap = Some(FlexWrap::WrapReverse); + self + } + + /// Sets the element to prevent flex items from wrapping, causing inflexible items to overflow the container if necessary. + /// [Docs](https://tailwindcss.com/docs/flex-wrap#dont-wrap) + fn flex_nowrap(mut self) -> Self { + self.style().flex_wrap = Some(FlexWrap::NoWrap); + self + } + + /// Sets the element to align flex items to the start of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-items#start) + fn items_start(mut self) -> Self { + self.style().align_items = Some(AlignItems::FlexStart); + self + } + + /// Sets the element to align flex items to the end of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-items#end) + fn items_end(mut self) -> Self { + self.style().align_items = Some(AlignItems::FlexEnd); + self + } + + /// Sets the element to align flex items along the center of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-items#center) + fn items_center(mut self) -> Self { + self.style().align_items = Some(AlignItems::Center); + self + } + + /// Sets the element to align flex items along the baseline of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-items#baseline) + fn items_baseline(mut self) -> Self { + self.style().align_items = Some(AlignItems::Baseline); + self + } + + /// Sets the element to justify flex items against the start of the container's main axis. + /// [Docs](https://tailwindcss.com/docs/justify-content#start) + fn justify_start(mut self) -> Self { + self.style().justify_content = Some(JustifyContent::Start); + self + } + + /// Sets the element to justify flex items against the end of the container's main axis. + /// [Docs](https://tailwindcss.com/docs/justify-content#end) + fn justify_end(mut self) -> Self { + self.style().justify_content = Some(JustifyContent::End); + self + } + + /// Sets the element to justify flex items along the center of the container's main axis. + /// [Docs](https://tailwindcss.com/docs/justify-content#center) + fn justify_center(mut self) -> Self { + self.style().justify_content = Some(JustifyContent::Center); + self + } + + /// Sets the element to justify flex items along the container's main axis + /// such that there is an equal amount of space between each item. + /// [Docs](https://tailwindcss.com/docs/justify-content#space-between) + fn justify_between(mut self) -> Self { + self.style().justify_content = Some(JustifyContent::SpaceBetween); + self + } + + /// Sets the element to justify items along the container's main axis such + /// that there is an equal amount of space on each side of each item. + /// [Docs](https://tailwindcss.com/docs/justify-content#space-around) + fn justify_around(mut self) -> Self { + self.style().justify_content = Some(JustifyContent::SpaceAround); + self + } + + /// Sets the element to pack content items in their default position as if no align-content value was set. + /// [Docs](https://tailwindcss.com/docs/align-content#normal) + fn content_normal(mut self) -> Self { + self.style().align_content = None; + self + } + + /// Sets the element to pack content items in the center of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-content#center) + fn content_center(mut self) -> Self { + self.style().align_content = Some(AlignContent::Center); + self + } + + /// Sets the element to pack content items against the start of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-content#start) + fn content_start(mut self) -> Self { + self.style().align_content = Some(AlignContent::FlexStart); + self + } + + /// Sets the element to pack content items against the end of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-content#end) + fn content_end(mut self) -> Self { + self.style().align_content = Some(AlignContent::FlexEnd); + self + } + + /// Sets the element to pack content items along the container's cross axis + /// such that there is an equal amount of space between each item. + /// [Docs](https://tailwindcss.com/docs/align-content#space-between) + fn content_between(mut self) -> Self { + self.style().align_content = Some(AlignContent::SpaceBetween); + self + } + + /// Sets the element to pack content items along the container's cross axis + /// such that there is an equal amount of space on each side of each item. + /// [Docs](https://tailwindcss.com/docs/align-content#space-around) + fn content_around(mut self) -> Self { + self.style().align_content = Some(AlignContent::SpaceAround); + self + } + + /// Sets the element to pack content items along the container's cross axis + /// such that there is an equal amount of space between each item. + /// [Docs](https://tailwindcss.com/docs/align-content#space-evenly) + fn content_evenly(mut self) -> Self { + self.style().align_content = Some(AlignContent::SpaceEvenly); + self + } + + /// Sets the element to allow content items to fill the available space along the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-content#stretch) + fn content_stretch(mut self) -> Self { + self.style().align_content = Some(AlignContent::Stretch); + self + } + + /// Sets the background color of the element. + fn bg(mut self, fill: F) -> Self + where + F: Into, + Self: Sized, + { + self.style().background = Some(fill.into()); + self + } + + /// Sets the border style of the element. + fn border_dashed(mut self) -> Self { + self.style().border_style = Some(BorderStyle::Dashed); + self + } + + /// Returns a mutable reference to the text style that has been configured on this element. + fn text_style(&mut self) -> &mut Option { + let style: &mut StyleRefinement = self.style(); + &mut style.text + } + + /// Sets the text color of this element. + /// + /// This value cascades to its child elements. + fn text_color(mut self, color: impl Into) -> Self { + self.text_style().get_or_insert_with(Default::default).color = Some(color.into()); + self + } + + /// Sets the font weight of this element + /// + /// This value cascades to its child elements. + fn font_weight(mut self, weight: FontWeight) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .font_weight = Some(weight); + self + } + + /// Sets the background color of this element. + /// + /// This value cascades to its child elements. + fn text_bg(mut self, bg: impl Into) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .background_color = Some(bg.into()); + self + } + + /// Sets the text size of this element. + /// + /// This value cascades to its child elements. + fn text_size(mut self, size: impl Into) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .font_size = Some(size.into()); + self + } + + /// Sets the text size to 'extra small'. + /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) + fn text_xs(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .font_size = Some(rems(0.75).into()); + self + } + + /// Sets the text size to 'small'. + /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) + fn text_sm(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .font_size = Some(rems(0.875).into()); + self + } + + /// Sets the text size to 'base'. + /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) + fn text_base(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .font_size = Some(rems(1.0).into()); + self + } + + /// Sets the text size to 'large'. + /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) + fn text_lg(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .font_size = Some(rems(1.125).into()); + self + } + + /// Sets the text size to 'extra large'. + /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) + fn text_xl(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .font_size = Some(rems(1.25).into()); + self + } + + /// Sets the text size to 'extra extra large'. + /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) + fn text_2xl(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .font_size = Some(rems(1.5).into()); + self + } + + /// Sets the text size to 'extra extra extra large'. + /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) + fn text_3xl(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .font_size = Some(rems(1.875).into()); + self + } + + /// Sets the font style of the element to italic. + /// [Docs](https://tailwindcss.com/docs/font-style#italicizing-text) + fn italic(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .font_style = Some(FontStyle::Italic); + self + } + + /// Sets the font style of the element to normal (not italic). + /// [Docs](https://tailwindcss.com/docs/font-style#displaying-text-normally) + fn not_italic(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .font_style = Some(FontStyle::Normal); + self + } + + /// Sets the text decoration to underline. + /// [Docs](https://tailwindcss.com/docs/text-decoration-line#underling-text) + fn underline(mut self) -> Self { + let style = self.text_style().get_or_insert_with(Default::default); + style.underline = Some(UnderlineStyle { + thickness: px(1.), + ..Default::default() + }); + self + } + + /// Sets the decoration of the text to have a line through it. + /// [Docs](https://tailwindcss.com/docs/text-decoration-line#adding-a-line-through-text) + fn line_through(mut self) -> Self { + let style = self.text_style().get_or_insert_with(Default::default); + style.strikethrough = Some(StrikethroughStyle { + thickness: px(1.), + ..Default::default() + }); + self + } + + /// Removes the text decoration on this element. + /// + /// This value cascades to its child elements. + fn text_decoration_none(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .underline = None; + self + } + + /// Sets the color for the underline on this element + fn text_decoration_color(mut self, color: impl Into) -> Self { + let style = self.text_style().get_or_insert_with(Default::default); + let underline = style.underline.get_or_insert_with(Default::default); + underline.color = Some(color.into()); + self + } + + /// Sets the text decoration style to a solid line. + /// [Docs](https://tailwindcss.com/docs/text-decoration-style) + fn text_decoration_solid(mut self) -> Self { + let style = self.text_style().get_or_insert_with(Default::default); + let underline = style.underline.get_or_insert_with(Default::default); + underline.wavy = false; + self + } + + /// Sets the text decoration style to a wavy line. + /// [Docs](https://tailwindcss.com/docs/text-decoration-style) + fn text_decoration_wavy(mut self) -> Self { + let style = self.text_style().get_or_insert_with(Default::default); + let underline = style.underline.get_or_insert_with(Default::default); + underline.wavy = true; + self + } + + /// Sets the text decoration to be 0px thick. + /// [Docs](https://tailwindcss.com/docs/text-decoration-thickness) + fn text_decoration_0(mut self) -> Self { + let style = self.text_style().get_or_insert_with(Default::default); + let underline = style.underline.get_or_insert_with(Default::default); + underline.thickness = px(0.); + self + } + + /// Sets the text decoration to be 1px thick. + /// [Docs](https://tailwindcss.com/docs/text-decoration-thickness) + fn text_decoration_1(mut self) -> Self { + let style = self.text_style().get_or_insert_with(Default::default); + let underline = style.underline.get_or_insert_with(Default::default); + underline.thickness = px(1.); + self + } + + /// Sets the text decoration to be 2px thick. + /// [Docs](https://tailwindcss.com/docs/text-decoration-thickness) + fn text_decoration_2(mut self) -> Self { + let style = self.text_style().get_or_insert_with(Default::default); + let underline = style.underline.get_or_insert_with(Default::default); + underline.thickness = px(2.); + self + } + + /// Sets the text decoration to be 4px thick. + /// [Docs](https://tailwindcss.com/docs/text-decoration-thickness) + fn text_decoration_4(mut self) -> Self { + let style = self.text_style().get_or_insert_with(Default::default); + let underline = style.underline.get_or_insert_with(Default::default); + underline.thickness = px(4.); + self + } + + /// Sets the text decoration to be 8px thick. + /// [Docs](https://tailwindcss.com/docs/text-decoration-thickness) + fn text_decoration_8(mut self) -> Self { + let style = self.text_style().get_or_insert_with(Default::default); + let underline = style.underline.get_or_insert_with(Default::default); + underline.thickness = px(8.); + self + } + + /// Sets the font family of this element and its children. + fn font_family(mut self, family_name: impl Into) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .font_family = Some(family_name.into()); + self + } + + /// Sets the font of this element and its children. + fn font(mut self, font: Font) -> Self { + let Font { + family, + features, + fallbacks, + weight, + style, + } = font; + + let text_style = self.text_style().get_or_insert_with(Default::default); + text_style.font_family = Some(family); + text_style.font_features = Some(features); + text_style.font_weight = Some(weight); + text_style.font_style = Some(style); + text_style.font_fallbacks = fallbacks; + + self + } + + /// Sets the line height of this element and its children. + fn line_height(mut self, line_height: impl Into) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .line_height = Some(line_height.into()); + self + } + + /// Sets the opacity of this element and its children. + fn opacity(mut self, opacity: f32) -> Self { + self.style().opacity = Some(opacity); + self + } + + /// Sets the grid columns of this element. + fn grid_cols(mut self, cols: u16) -> Self { + self.style().grid_cols = Some(cols); + self + } + + /// Sets the grid rows of this element. + fn grid_rows(mut self, rows: u16) -> Self { + self.style().grid_rows = Some(rows); + self + } + + /// Sets the column start of this element. + fn col_start(mut self, start: i16) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.column.start = GridPlacement::Line(start); + self + } + + /// Sets the column start of this element to auto. + fn col_start_auto(mut self) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.column.start = GridPlacement::Auto; + self + } + + /// Sets the column end of this element. + fn col_end(mut self, end: i16) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.column.end = GridPlacement::Line(end); + self + } + + /// Sets the column end of this element to auto. + fn col_end_auto(mut self) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.column.end = GridPlacement::Auto; + self + } + + /// Sets the column span of this element. + fn col_span(mut self, span: u16) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.column = GridPlacement::Span(span)..GridPlacement::Span(span); + self + } + + /// Sets the row span of this element. + fn col_span_full(mut self) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.column = GridPlacement::Line(1)..GridPlacement::Line(-1); + self + } + + /// Sets the row start of this element. + fn row_start(mut self, start: i16) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.row.start = GridPlacement::Line(start); + self + } + + /// Sets the row start of this element to "auto" + fn row_start_auto(mut self) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.row.start = GridPlacement::Auto; + self + } + + /// Sets the row end of this element. + fn row_end(mut self, end: i16) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.row.end = GridPlacement::Line(end); + self + } + + /// Sets the row end of this element to "auto" + fn row_end_auto(mut self) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.row.end = GridPlacement::Auto; + self + } + + /// Sets the row span of this element. + fn row_span(mut self, span: u16) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.row = GridPlacement::Span(span)..GridPlacement::Span(span); + self + } + + /// Sets the row span of this element. + fn row_span_full(mut self) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.row = GridPlacement::Line(1)..GridPlacement::Line(-1); + self + } + + /// Draws a debug border around this element. + #[cfg(debug_assertions)] + fn debug(mut self) -> Self { + self.style().debug = Some(true); + self + } + + /// Draws a debug border on all conforming elements below this element. + #[cfg(debug_assertions)] + fn debug_below(mut self) -> Self { + self.style().debug_below = Some(true); + self + } +} diff --git a/third_party/gpui/src/subscription.rs b/third_party/gpui/src/subscription.rs new file mode 100644 index 0000000..bd869f8 --- /dev/null +++ b/third_party/gpui/src/subscription.rs @@ -0,0 +1,209 @@ +use collections::{BTreeMap, BTreeSet}; +use std::{ + cell::{Cell, RefCell}, + fmt::Debug, + mem, + rc::Rc, +}; +use util::post_inc; + +pub(crate) struct SubscriberSet( + Rc>>, +); + +impl Clone for SubscriberSet { + fn clone(&self) -> Self { + SubscriberSet(self.0.clone()) + } +} + +struct SubscriberSetState { + subscribers: BTreeMap>>>, + dropped_subscribers: BTreeSet<(EmitterKey, usize)>, + next_subscriber_id: usize, +} + +struct Subscriber { + active: Rc>, + callback: Callback, +} + +impl SubscriberSet +where + EmitterKey: 'static + Ord + Clone + Debug, + Callback: 'static, +{ + pub fn new() -> Self { + Self(Rc::new(RefCell::new(SubscriberSetState { + subscribers: Default::default(), + dropped_subscribers: Default::default(), + next_subscriber_id: 0, + }))) + } + + /// Inserts a new [`Subscription`] for the given `emitter_key`. By default, subscriptions + /// are inert, meaning that they won't be listed when calling `[SubscriberSet::remove]` or `[SubscriberSet::retain]`. + /// This method returns a tuple of a [`Subscription`] and an `impl FnOnce`, and you can use the latter + /// to activate the [`Subscription`]. + pub fn insert( + &self, + emitter_key: EmitterKey, + callback: Callback, + ) -> (Subscription, impl FnOnce() + use) { + let active = Rc::new(Cell::new(false)); + let mut lock = self.0.borrow_mut(); + let subscriber_id = post_inc(&mut lock.next_subscriber_id); + lock.subscribers + .entry(emitter_key.clone()) + .or_default() + .get_or_insert_with(Default::default) + .insert( + subscriber_id, + Subscriber { + active: active.clone(), + callback, + }, + ); + let this = self.0.clone(); + + let subscription = Subscription { + unsubscribe: Some(Box::new(move || { + let mut lock = this.borrow_mut(); + let Some(subscribers) = lock.subscribers.get_mut(&emitter_key) else { + // remove was called with this emitter_key + return; + }; + + if let Some(subscribers) = subscribers { + subscribers.remove(&subscriber_id); + if subscribers.is_empty() { + lock.subscribers.remove(&emitter_key); + } + return; + } + + // We didn't manage to remove the subscription, which means it was dropped + // while invoking the callback. Mark it as dropped so that we can remove it + // later. + lock.dropped_subscribers + .insert((emitter_key, subscriber_id)); + })), + }; + (subscription, move || active.set(true)) + } + + pub fn remove( + &self, + emitter: &EmitterKey, + ) -> impl IntoIterator + use { + let subscribers = self.0.borrow_mut().subscribers.remove(emitter); + subscribers + .unwrap_or_default() + .map(|s| s.into_values()) + .into_iter() + .flatten() + .filter_map(|subscriber| { + if subscriber.active.get() { + Some(subscriber.callback) + } else { + None + } + }) + } + + /// Call the given callback for each subscriber to the given emitter. + /// If the callback returns false, the subscriber is removed. + pub fn retain(&self, emitter: &EmitterKey, mut f: F) + where + F: FnMut(&mut Callback) -> bool, + { + let Some(mut subscribers) = self + .0 + .borrow_mut() + .subscribers + .get_mut(emitter) + .and_then(|s| s.take()) + else { + return; + }; + + subscribers.retain(|_, subscriber| { + if subscriber.active.get() { + f(&mut subscriber.callback) + } else { + true + } + }); + let mut lock = self.0.borrow_mut(); + + // Add any new subscribers that were added while invoking the callback. + if let Some(Some(new_subscribers)) = lock.subscribers.remove(emitter) { + subscribers.extend(new_subscribers); + } + + // Remove any dropped subscriptions that were dropped while invoking the callback. + for (dropped_emitter, dropped_subscription_id) in mem::take(&mut lock.dropped_subscribers) { + debug_assert_eq!(*emitter, dropped_emitter); + subscribers.remove(&dropped_subscription_id); + } + + if !subscribers.is_empty() { + lock.subscribers.insert(emitter.clone(), Some(subscribers)); + } + } +} + +/// A handle to a subscription created by GPUI. When dropped, the subscription +/// is cancelled and the callback will no longer be invoked. +#[must_use] +pub struct Subscription { + unsubscribe: Option>, +} + +impl Subscription { + /// Creates a new subscription with a callback that gets invoked when + /// this subscription is dropped. + pub fn new(unsubscribe: impl 'static + FnOnce()) -> Self { + Self { + unsubscribe: Some(Box::new(unsubscribe)), + } + } + + /// Detaches the subscription from this handle. The callback will + /// continue to be invoked until the entities it has been + /// subscribed to are dropped + pub fn detach(mut self) { + self.unsubscribe.take(); + } + + /// Joins two subscriptions into a single subscription. Detach will + /// detach both interior subscriptions. + pub fn join(mut subscription_a: Self, mut subscription_b: Self) -> Self { + let a_unsubscribe = subscription_a.unsubscribe.take(); + let b_unsubscribe = subscription_b.unsubscribe.take(); + Self { + unsubscribe: Some(Box::new(move || { + if let Some(self_unsubscribe) = a_unsubscribe { + self_unsubscribe(); + } + if let Some(other_unsubscribe) = b_unsubscribe { + other_unsubscribe(); + } + })), + } + } +} + +impl Drop for Subscription { + fn drop(&mut self) { + if let Some(unsubscribe) = self.unsubscribe.take() { + unsubscribe(); + } + } +} + +impl std::fmt::Debug for Subscription { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Subscription").finish() + } +} diff --git a/third_party/gpui/src/svg_renderer.rs b/third_party/gpui/src/svg_renderer.rs new file mode 100644 index 0000000..b2bf126 --- /dev/null +++ b/third_party/gpui/src/svg_renderer.rs @@ -0,0 +1,104 @@ +use crate::{AssetSource, DevicePixels, IsZero, Result, SharedString, Size}; +use resvg::tiny_skia::Pixmap; +use std::{ + hash::Hash, + sync::{Arc, LazyLock}, +}; + +/// When rendering SVGs, we render them at twice the size to get a higher-quality result. +pub const SMOOTH_SVG_SCALE_FACTOR: f32 = 2.; + +#[derive(Clone, PartialEq, Hash, Eq)] +pub(crate) struct RenderSvgParams { + pub(crate) path: SharedString, + pub(crate) size: Size, +} + +#[derive(Clone)] +pub struct SvgRenderer { + asset_source: Arc, + usvg_options: Arc>, +} + +pub enum SvgSize { + Size(Size), + ScaleFactor(f32), +} + +impl SvgRenderer { + pub fn new(asset_source: Arc) -> Self { + static FONT_DB: LazyLock> = LazyLock::new(|| { + let mut db = usvg::fontdb::Database::new(); + db.load_system_fonts(); + Arc::new(db) + }); + let default_font_resolver = usvg::FontResolver::default_font_selector(); + let font_resolver = Box::new( + move |font: &usvg::Font, db: &mut Arc| { + if db.is_empty() { + *db = FONT_DB.clone(); + } + default_font_resolver(font, db) + }, + ); + let options = usvg::Options { + font_resolver: usvg::FontResolver { + select_font: font_resolver, + select_fallback: usvg::FontResolver::default_fallback_selector(), + }, + ..Default::default() + }; + Self { + asset_source, + usvg_options: Arc::new(options), + } + } + + pub(crate) fn render( + &self, + params: &RenderSvgParams, + ) -> Result, Vec)>> { + anyhow::ensure!(!params.size.is_zero(), "can't render at a zero size"); + + // Load the tree. + let Some(bytes) = self.asset_source.load(¶ms.path)? else { + return Ok(None); + }; + + let pixmap = self.render_pixmap(&bytes, SvgSize::Size(params.size))?; + + // Convert the pixmap's pixels into an alpha mask. + let size = Size::new( + DevicePixels(pixmap.width() as i32), + DevicePixels(pixmap.height() as i32), + ); + let alpha_mask = pixmap + .pixels() + .iter() + .map(|p| p.alpha()) + .collect::>(); + Ok(Some((size, alpha_mask))) + } + + pub fn render_pixmap(&self, bytes: &[u8], size: SvgSize) -> Result { + let tree = usvg::Tree::from_data(bytes, &self.usvg_options)?; + let svg_size = tree.size(); + let scale = match size { + SvgSize::Size(size) => size.width.0 as f32 / svg_size.width(), + SvgSize::ScaleFactor(scale) => scale, + }; + + // Render the SVG to a pixmap with the specified width and height. + let mut pixmap = resvg::tiny_skia::Pixmap::new( + (svg_size.width() * scale) as u32, + (svg_size.height() * scale) as u32, + ) + .ok_or(usvg::Error::InvalidSize)?; + + let transform = resvg::tiny_skia::Transform::from_scale(scale, scale); + + resvg::render(&tree, transform, &mut pixmap.as_mut()); + + Ok(pixmap) + } +} diff --git a/third_party/gpui/src/tab_stop.rs b/third_party/gpui/src/tab_stop.rs new file mode 100644 index 0000000..8a95a39 --- /dev/null +++ b/third_party/gpui/src/tab_stop.rs @@ -0,0 +1,611 @@ +use std::fmt::Debug; + +use ::sum_tree::SumTree; +use collections::FxHashMap; +use sum_tree::Bias; + +use crate::{FocusHandle, FocusId}; + +/// Represents a collection of focus handles using the tab-index APIs. +#[derive(Debug)] +pub(crate) struct TabStopMap { + current_path: TabStopPath, + pub(crate) insertion_history: Vec, + by_id: FxHashMap, + order: SumTree, +} + +#[derive(Debug, Clone)] +pub enum TabStopOperation { + Insert(FocusHandle), + Group(TabIndex), + GroupEnd, +} + +impl TabStopOperation { + fn focus_handle(&self) -> Option<&FocusHandle> { + match self { + TabStopOperation::Insert(focus_handle) => Some(focus_handle), + _ => None, + } + } +} + +type TabIndex = isize; + +#[derive(Debug, Default, PartialEq, Eq, Clone, Ord, PartialOrd)] +struct TabStopPath(smallvec::SmallVec<[TabIndex; 6]>); + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +struct TabStopNode { + /// Path to access the node in the tree + /// The final node in the list is a leaf node corresponding to an actual focus handle, + /// all other nodes are group nodes + path: TabStopPath, + /// index into the backing array of nodes. Corresponds to insertion order + node_insertion_index: usize, + + /// Whether this node is a tab stop + tab_stop: bool, +} + +impl Ord for TabStopNode { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.path + .cmp(&other.path) + .then(self.node_insertion_index.cmp(&other.node_insertion_index)) + } +} + +impl PartialOrd for TabStopNode { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(&other)) + } +} + +impl Default for TabStopMap { + fn default() -> Self { + Self { + current_path: TabStopPath::default(), + insertion_history: Vec::new(), + by_id: FxHashMap::default(), + order: SumTree::new(()), + } + } +} + +impl TabStopMap { + pub fn insert(&mut self, focus_handle: &FocusHandle) { + self.insertion_history + .push(TabStopOperation::Insert(focus_handle.clone())); + let mut path = self.current_path.clone(); + path.0.push(focus_handle.tab_index); + let order = TabStopNode { + node_insertion_index: self.insertion_history.len() - 1, + tab_stop: focus_handle.tab_stop, + path, + }; + self.by_id.insert(focus_handle.id, order.clone()); + self.order.insert_or_replace(order, ()); + } + + pub fn begin_group(&mut self, tab_index: isize) { + self.insertion_history + .push(TabStopOperation::Group(tab_index)); + self.current_path.0.push(tab_index); + } + + pub fn end_group(&mut self) { + self.insertion_history.push(TabStopOperation::GroupEnd); + self.current_path.0.pop(); + } + + pub fn clear(&mut self) { + *self = Self::default(); + self.current_path.0.clear(); + self.insertion_history.clear(); + self.by_id.clear(); + self.order = SumTree::new(()); + } + + pub fn next(&self, focused_id: Option<&FocusId>) -> Option { + let Some(focused_id) = focused_id else { + let first = self.order.first()?; + if first.tab_stop { + return self.focus_handle_for_order(first); + } else { + return self + .next_inner(first) + .and_then(|order| self.focus_handle_for_order(order)); + } + }; + + let Some(node) = self.tab_node_for_focus_id(focused_id) else { + return self.next(None); + }; + let item = self.next_inner(node); + + if let Some(item) = item { + self.focus_handle_for_order(&item) + } else { + self.next(None) + } + } + + fn next_inner(&self, node: &TabStopNode) -> Option<&TabStopNode> { + let mut cursor = self.order.cursor::(()); + cursor.seek(&node, Bias::Left); + cursor.next(); + while let Some(item) = cursor.item() + && !item.tab_stop + { + cursor.next(); + } + + cursor.item() + } + + pub fn prev(&self, focused_id: Option<&FocusId>) -> Option { + let Some(focused_id) = focused_id else { + let last = self.order.last()?; + if last.tab_stop { + return self.focus_handle_for_order(last); + } else { + return self + .prev_inner(last) + .and_then(|order| self.focus_handle_for_order(order)); + } + }; + + let Some(node) = self.tab_node_for_focus_id(focused_id) else { + return self.prev(None); + }; + let item = self.prev_inner(node); + + if let Some(item) = item { + self.focus_handle_for_order(&item) + } else { + self.prev(None) + } + } + + fn prev_inner(&self, node: &TabStopNode) -> Option<&TabStopNode> { + let mut cursor = self.order.cursor::(()); + cursor.seek(&node, Bias::Left); + cursor.prev(); + while let Some(item) = cursor.item() + && !item.tab_stop + { + cursor.prev(); + } + + cursor.item() + } + + pub fn replay(&mut self, nodes: &[TabStopOperation]) { + for node in nodes { + match node { + TabStopOperation::Insert(focus_handle) => self.insert(focus_handle), + TabStopOperation::Group(tab_index) => self.begin_group(*tab_index), + TabStopOperation::GroupEnd => self.end_group(), + } + } + } + + pub fn paint_index(&self) -> usize { + self.insertion_history.len() + } + + fn focus_handle_for_order(&self, order: &TabStopNode) -> Option { + let handle = self.insertion_history[order.node_insertion_index].focus_handle(); + debug_assert!( + handle.is_some(), + "The order node did not correspond to an element, this is a GPUI bug" + ); + handle.cloned() + } + + fn tab_node_for_focus_id(&self, focused_id: &FocusId) -> Option<&TabStopNode> { + let Some(order) = self.by_id.get(focused_id) else { + return None; + }; + Some(order) + } +} + +mod sum_tree_impl { + use sum_tree::SeekTarget; + + use crate::tab_stop::{TabStopNode, TabStopPath}; + + #[derive(Clone, Debug)] + pub struct TabStopOrderNodeSummary { + max_index: usize, + max_path: TabStopPath, + pub tab_stops: usize, + } + + pub type TabStopCount = usize; + + impl sum_tree::ContextLessSummary for TabStopOrderNodeSummary { + fn zero() -> Self { + TabStopOrderNodeSummary { + max_index: 0, + max_path: TabStopPath::default(), + tab_stops: 0, + } + } + + fn add_summary(&mut self, summary: &Self) { + self.max_index = summary.max_index; + self.max_path = summary.max_path.clone(); + self.tab_stops += summary.tab_stops; + } + } + + impl sum_tree::KeyedItem for TabStopNode { + type Key = Self; + + fn key(&self) -> Self::Key { + self.clone() + } + } + + impl sum_tree::Item for TabStopNode { + type Summary = TabStopOrderNodeSummary; + + fn summary(&self, _cx: ::Context<'_>) -> Self::Summary { + TabStopOrderNodeSummary { + max_index: self.node_insertion_index, + max_path: self.path.clone(), + tab_stops: if self.tab_stop { 1 } else { 0 }, + } + } + } + + impl<'a> sum_tree::Dimension<'a, TabStopOrderNodeSummary> for TabStopCount { + fn zero(_: ::Context<'_>) -> Self { + 0 + } + + fn add_summary( + &mut self, + summary: &'a TabStopOrderNodeSummary, + _: ::Context<'_>, + ) { + *self += summary.tab_stops; + } + } + + impl<'a> sum_tree::Dimension<'a, TabStopOrderNodeSummary> for TabStopNode { + fn zero(_: ::Context<'_>) -> Self { + TabStopNode::default() + } + + fn add_summary( + &mut self, + summary: &'a TabStopOrderNodeSummary, + _: ::Context<'_>, + ) { + self.node_insertion_index = summary.max_index; + self.path = summary.max_path.clone(); + } + } + + impl<'a, 'b> SeekTarget<'a, TabStopOrderNodeSummary, TabStopNode> for &'b TabStopNode { + fn cmp( + &self, + cursor_location: &TabStopNode, + _: ::Context<'_>, + ) -> std::cmp::Ordering { + Iterator::cmp(self.path.0.iter(), cursor_location.path.0.iter()).then( + ::cmp( + &self.node_insertion_index, + &cursor_location.node_insertion_index, + ), + ) + } + } +} + +#[cfg(test)] +mod tests { + use itertools::Itertools as _; + + use crate::{FocusHandle, FocusId, FocusMap, TabStopMap}; + use std::sync::Arc; + + #[test] + fn test_tab_handles() { + let focus_map = Arc::new(FocusMap::default()); + let mut tab_index_map = TabStopMap::default(); + + let focus_handles = vec![ + FocusHandle::new(&focus_map).tab_stop(true).tab_index(0), + FocusHandle::new(&focus_map).tab_stop(true).tab_index(1), + FocusHandle::new(&focus_map).tab_stop(true).tab_index(1), + FocusHandle::new(&focus_map), + FocusHandle::new(&focus_map).tab_index(2), + FocusHandle::new(&focus_map).tab_stop(true).tab_index(0), + FocusHandle::new(&focus_map).tab_stop(true).tab_index(2), + ]; + + for handle in focus_handles.iter() { + tab_index_map.insert(handle); + } + let expected = [ + focus_handles[0].clone(), + focus_handles[5].clone(), + focus_handles[1].clone(), + focus_handles[2].clone(), + focus_handles[6].clone(), + ]; + + let mut prev = None; + let mut found = vec![]; + for _ in 0..expected.len() { + let handle = tab_index_map.next(prev.as_ref()).unwrap(); + prev = Some(handle.id); + found.push(handle.id); + } + + assert_eq!( + found, + expected.iter().map(|handle| handle.id).collect::>() + ); + + // Select first tab index if no handle is currently focused. + assert_eq!(tab_index_map.next(None), Some(expected[0].clone())); + // Select last tab index if no handle is currently focused. + assert_eq!(tab_index_map.prev(None), expected.last().cloned(),); + + assert_eq!( + tab_index_map.next(Some(&expected[0].id)), + Some(expected[1].clone()) + ); + assert_eq!( + tab_index_map.next(Some(&expected[1].id)), + Some(expected[2].clone()) + ); + assert_eq!( + tab_index_map.next(Some(&expected[2].id)), + Some(expected[3].clone()) + ); + assert_eq!( + tab_index_map.next(Some(&expected[3].id)), + Some(expected[4].clone()) + ); + assert_eq!( + tab_index_map.next(Some(&expected[4].id)), + Some(expected[0].clone()) + ); + + // prev + assert_eq!(tab_index_map.prev(None), Some(expected[4].clone())); + assert_eq!( + tab_index_map.prev(Some(&expected[0].id)), + Some(expected[4].clone()) + ); + assert_eq!( + tab_index_map.prev(Some(&expected[1].id)), + Some(expected[0].clone()) + ); + assert_eq!( + tab_index_map.prev(Some(&expected[2].id)), + Some(expected[1].clone()) + ); + assert_eq!( + tab_index_map.prev(Some(&expected[3].id)), + Some(expected[2].clone()) + ); + assert_eq!( + tab_index_map.prev(Some(&expected[4].id)), + Some(expected[3].clone()) + ); + } + + #[test] + fn test_tab_non_stop_filtering() { + let focus_map = Arc::new(FocusMap::default()); + let mut tab_index_map = TabStopMap::default(); + + // Check that we can query next from a non-stop tab + let tab_non_stop_1 = FocusHandle::new(&focus_map).tab_stop(false).tab_index(1); + let tab_stop_2 = FocusHandle::new(&focus_map).tab_stop(true).tab_index(2); + tab_index_map.insert(&tab_non_stop_1); + tab_index_map.insert(&tab_stop_2); + let result = tab_index_map.next(Some(&tab_non_stop_1.id)).unwrap(); + assert_eq!(result.id, tab_stop_2.id); + + // Check that we skip over non-stop tabs + let tab_stop_0 = FocusHandle::new(&focus_map).tab_stop(true).tab_index(0); + let tab_non_stop_0 = FocusHandle::new(&focus_map).tab_stop(false).tab_index(0); + tab_index_map.insert(&tab_stop_0); + tab_index_map.insert(&tab_non_stop_0); + let result = tab_index_map.next(Some(&tab_stop_0.id)).unwrap(); + assert_eq!(result.id, tab_stop_2.id); + } + + #[must_use] + struct TabStopMapTest { + tab_map: TabStopMap, + focus_map: Arc, + expected: Vec<(usize, FocusId)>, + } + + impl TabStopMapTest { + #[must_use] + fn new() -> Self { + Self { + tab_map: TabStopMap::default(), + focus_map: Arc::new(FocusMap::default()), + expected: Vec::default(), + } + } + + #[must_use] + fn tab_non_stop(mut self, index: isize) -> Self { + let handle = FocusHandle::new(&self.focus_map) + .tab_stop(false) + .tab_index(index); + self.tab_map.insert(&handle); + self + } + + #[must_use] + fn tab_stop(mut self, index: isize, expected: usize) -> Self { + let handle = FocusHandle::new(&self.focus_map) + .tab_stop(true) + .tab_index(index); + self.tab_map.insert(&handle); + self.expected.push((expected, handle.id)); + self.expected.sort_by_key(|(expected, _)| *expected); + self + } + + #[must_use] + fn tab_group(mut self, tab_index: isize, children: impl FnOnce(Self) -> Self) -> Self { + self.tab_map.begin_group(tab_index); + self = children(self); + self.tab_map.end_group(); + self + } + + fn traverse_tab_map( + &self, + traverse: impl Fn(&TabStopMap, Option<&FocusId>) -> Option, + ) -> Vec { + let mut last_focus_id = None; + let mut found = vec![]; + for _ in 0..self.expected.len() { + let handle = traverse(&self.tab_map, last_focus_id.as_ref()).unwrap(); + last_focus_id = Some(handle.id); + found.push(handle.id); + } + found + } + + fn assert(self) { + let mut expected = self.expected.iter().map(|(_, id)| *id).collect_vec(); + + // Check next order + let forward_found = self.traverse_tab_map(|tab_map, prev| tab_map.next(prev)); + assert_eq!(forward_found, expected); + + // Test overflow. Last to first + assert_eq!( + self.tab_map + .next(forward_found.last()) + .map(|handle| handle.id), + expected.first().cloned() + ); + + // Check previous order + let reversed_found = self.traverse_tab_map(|tab_map, prev| tab_map.prev(prev)); + expected.reverse(); + assert_eq!(reversed_found, expected); + + // Test overflow. First to last + assert_eq!( + self.tab_map + .prev(reversed_found.last()) + .map(|handle| handle.id), + expected.first().cloned(), + ); + } + } + + #[test] + fn test_with_disabled_tab_stop() { + TabStopMapTest::new() + .tab_stop(0, 0) + .tab_non_stop(1) + .tab_stop(2, 1) + .tab_stop(3, 2) + .assert(); + } + + #[test] + fn test_with_multiple_disabled_tab_stops() { + TabStopMapTest::new() + .tab_non_stop(0) + .tab_stop(1, 0) + .tab_non_stop(3) + .tab_stop(3, 1) + .tab_non_stop(4) + .assert(); + } + + #[test] + fn test_tab_group_functionality() { + TabStopMapTest::new() + .tab_stop(0, 0) + .tab_stop(0, 1) + .tab_group(2, |t| t.tab_stop(0, 2).tab_stop(1, 3)) + .tab_stop(3, 4) + .tab_stop(4, 5) + .assert() + } + + #[test] + fn test_sibling_groups() { + TabStopMapTest::new() + .tab_stop(0, 0) + .tab_stop(1, 1) + .tab_group(2, |test| test.tab_stop(0, 2).tab_stop(1, 3)) + .tab_stop(3, 4) + .tab_stop(4, 5) + .tab_group(6, |test| test.tab_stop(0, 6).tab_stop(1, 7)) + .tab_stop(7, 8) + .tab_stop(8, 9) + .assert(); + } + + #[test] + fn test_nested_group() { + TabStopMapTest::new() + .tab_stop(0, 0) + .tab_stop(1, 1) + .tab_group(2, |t| { + t.tab_group(0, |t| t.tab_stop(0, 2).tab_stop(1, 3)) + .tab_stop(1, 4) + }) + .tab_stop(3, 5) + .tab_stop(4, 6) + .assert(); + } + + #[test] + fn test_sibling_nested_groups() { + TabStopMapTest::new() + .tab_stop(0, 0) + .tab_stop(1, 1) + .tab_group(2, |builder| { + builder + .tab_stop(0, 2) + .tab_stop(2, 5) + .tab_group(1, |builder| builder.tab_stop(0, 3).tab_stop(1, 4)) + .tab_group(3, |builder| builder.tab_stop(0, 6).tab_stop(1, 7)) + }) + .tab_stop(3, 8) + .tab_stop(4, 9) + .assert(); + } + + #[test] + fn test_sibling_nested_groups_out_of_order() { + TabStopMapTest::new() + .tab_stop(9, 9) + .tab_stop(8, 8) + .tab_group(7, |builder| { + builder + .tab_stop(0, 2) + .tab_stop(2, 5) + .tab_group(3, |builder| builder.tab_stop(1, 7).tab_stop(0, 6)) + .tab_group(1, |builder| builder.tab_stop(0, 3).tab_stop(1, 4)) + }) + .tab_stop(3, 0) + .tab_stop(4, 1) + .assert(); + } +} diff --git a/third_party/gpui/src/taffy.rs b/third_party/gpui/src/taffy.rs new file mode 100644 index 0000000..29b4ce6 --- /dev/null +++ b/third_party/gpui/src/taffy.rs @@ -0,0 +1,608 @@ +use crate::{ + AbsoluteLength, App, Bounds, DefiniteLength, Edges, Length, Pixels, Point, Size, Style, Window, + point, size, +}; +use collections::{FxHashMap, FxHashSet}; +use smallvec::SmallVec; +use stacksafe::{StackSafe, stacksafe}; +use std::{fmt::Debug, ops::Range}; +use taffy::{ + TaffyTree, TraversePartialTree as _, + geometry::{Point as TaffyPoint, Rect as TaffyRect, Size as TaffySize}, + style::AvailableSpace as TaffyAvailableSpace, + tree::NodeId, +}; + +type NodeMeasureFn = StackSafe< + Box< + dyn FnMut( + Size>, + Size, + &mut Window, + &mut App, + ) -> Size, + >, +>; + +struct NodeContext { + measure: NodeMeasureFn, +} +pub struct TaffyLayoutEngine { + taffy: TaffyTree, + absolute_layout_bounds: FxHashMap>, + computed_layouts: FxHashSet, +} + +const EXPECT_MESSAGE: &str = "we should avoid taffy layout errors by construction if possible"; + +impl TaffyLayoutEngine { + pub fn new() -> Self { + let mut taffy = TaffyTree::new(); + taffy.enable_rounding(); + TaffyLayoutEngine { + taffy, + absolute_layout_bounds: FxHashMap::default(), + computed_layouts: FxHashSet::default(), + } + } + + pub fn clear(&mut self) { + self.taffy.clear(); + self.absolute_layout_bounds.clear(); + self.computed_layouts.clear(); + } + + pub fn request_layout( + &mut self, + style: Style, + rem_size: Pixels, + scale_factor: f32, + children: &[LayoutId], + ) -> LayoutId { + let taffy_style = style.to_taffy(rem_size, scale_factor); + + if children.is_empty() { + self.taffy + .new_leaf(taffy_style) + .expect(EXPECT_MESSAGE) + .into() + } else { + self.taffy + // This is safe because LayoutId is repr(transparent) to taffy::tree::NodeId. + .new_with_children(taffy_style, LayoutId::to_taffy_slice(children)) + .expect(EXPECT_MESSAGE) + .into() + } + } + + pub fn request_measured_layout( + &mut self, + style: Style, + rem_size: Pixels, + scale_factor: f32, + measure: impl FnMut( + Size>, + Size, + &mut Window, + &mut App, + ) -> Size + + 'static, + ) -> LayoutId { + let taffy_style = style.to_taffy(rem_size, scale_factor); + + self.taffy + .new_leaf_with_context( + taffy_style, + NodeContext { + measure: StackSafe::new(Box::new(measure)), + }, + ) + .expect(EXPECT_MESSAGE) + .into() + } + + // Used to understand performance + #[allow(dead_code)] + fn count_all_children(&self, parent: LayoutId) -> anyhow::Result { + let mut count = 0; + + for child in self.taffy.children(parent.0)? { + // Count this child. + count += 1; + + // Count all of this child's children. + count += self.count_all_children(LayoutId(child))? + } + + Ok(count) + } + + // Used to understand performance + #[allow(dead_code)] + fn max_depth(&self, depth: u32, parent: LayoutId) -> anyhow::Result { + println!( + "{parent:?} at depth {depth} has {} children", + self.taffy.child_count(parent.0) + ); + + let mut max_child_depth = 0; + + for child in self.taffy.children(parent.0)? { + max_child_depth = std::cmp::max(max_child_depth, self.max_depth(0, LayoutId(child))?); + } + + Ok(depth + 1 + max_child_depth) + } + + // Used to understand performance + #[allow(dead_code)] + fn get_edges(&self, parent: LayoutId) -> anyhow::Result> { + let mut edges = Vec::new(); + + for child in self.taffy.children(parent.0)? { + edges.push((parent, LayoutId(child))); + + edges.extend(self.get_edges(LayoutId(child))?); + } + + Ok(edges) + } + + #[stacksafe] + pub fn compute_layout( + &mut self, + id: LayoutId, + available_space: Size, + window: &mut Window, + cx: &mut App, + ) { + // Leaving this here until we have a better instrumentation approach. + // println!("Laying out {} children", self.count_all_children(id)?); + // println!("Max layout depth: {}", self.max_depth(0, id)?); + + // Output the edges (branches) of the tree in Mermaid format for visualization. + // println!("Edges:"); + // for (a, b) in self.get_edges(id)? { + // println!("N{} --> N{}", u64::from(a), u64::from(b)); + // } + // + + if !self.computed_layouts.insert(id) { + let mut stack = SmallVec::<[LayoutId; 64]>::new(); + stack.push(id); + while let Some(id) = stack.pop() { + self.absolute_layout_bounds.remove(&id); + stack.extend( + self.taffy + .children(id.into()) + .expect(EXPECT_MESSAGE) + .into_iter() + .map(Into::into), + ); + } + } + + let scale_factor = window.scale_factor(); + + let transform = |v: AvailableSpace| match v { + AvailableSpace::Definite(pixels) => { + AvailableSpace::Definite(Pixels(pixels.0 * scale_factor)) + } + AvailableSpace::MinContent => AvailableSpace::MinContent, + AvailableSpace::MaxContent => AvailableSpace::MaxContent, + }; + let available_space = size( + transform(available_space.width), + transform(available_space.height), + ); + + self.taffy + .compute_layout_with_measure( + id.into(), + available_space.into(), + |known_dimensions, available_space, _id, node_context, _style| { + let Some(node_context) = node_context else { + return taffy::geometry::Size::default(); + }; + + let known_dimensions = Size { + width: known_dimensions.width.map(|e| Pixels(e / scale_factor)), + height: known_dimensions.height.map(|e| Pixels(e / scale_factor)), + }; + + let available_space: Size = available_space.into(); + let untransform = |ev: AvailableSpace| match ev { + AvailableSpace::Definite(pixels) => { + AvailableSpace::Definite(Pixels(pixels.0 / scale_factor)) + } + AvailableSpace::MinContent => AvailableSpace::MinContent, + AvailableSpace::MaxContent => AvailableSpace::MaxContent, + }; + let available_space = size( + untransform(available_space.width), + untransform(available_space.height), + ); + + let a: Size = + (node_context.measure)(known_dimensions, available_space, window, cx); + size(a.width.0 * scale_factor, a.height.0 * scale_factor).into() + }, + ) + .expect(EXPECT_MESSAGE); + } + + pub fn layout_bounds(&mut self, id: LayoutId, scale_factor: f32) -> Bounds { + if let Some(layout) = self.absolute_layout_bounds.get(&id).cloned() { + return layout; + } + + let layout = self.taffy.layout(id.into()).expect(EXPECT_MESSAGE); + let mut bounds = Bounds { + origin: point( + Pixels(layout.location.x / scale_factor), + Pixels(layout.location.y / scale_factor), + ), + size: size( + Pixels(layout.size.width / scale_factor), + Pixels(layout.size.height / scale_factor), + ), + }; + + if let Some(parent_id) = self.taffy.parent(id.0) { + let parent_bounds = self.layout_bounds(parent_id.into(), scale_factor); + bounds.origin += parent_bounds.origin; + } + self.absolute_layout_bounds.insert(id, bounds); + + bounds + } +} + +/// A unique identifier for a layout node, generated when requesting a layout from Taffy +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +#[repr(transparent)] +pub struct LayoutId(NodeId); + +impl LayoutId { + fn to_taffy_slice(node_ids: &[Self]) -> &[taffy::NodeId] { + // SAFETY: LayoutId is repr(transparent) to taffy::tree::NodeId. + unsafe { std::mem::transmute::<&[LayoutId], &[taffy::NodeId]>(node_ids) } + } +} + +impl std::hash::Hash for LayoutId { + fn hash(&self, state: &mut H) { + u64::from(self.0).hash(state); + } +} + +impl From for LayoutId { + fn from(node_id: NodeId) -> Self { + Self(node_id) + } +} + +impl From for NodeId { + fn from(layout_id: LayoutId) -> NodeId { + layout_id.0 + } +} + +trait ToTaffy { + fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> Output; +} + +impl ToTaffy for Style { + fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::style::Style { + use taffy::style_helpers::{fr, length, minmax, repeat}; + + fn to_grid_line( + placement: &Range, + ) -> taffy::Line { + taffy::Line { + start: placement.start.into(), + end: placement.end.into(), + } + } + + fn to_grid_repeat( + unit: &Option, + ) -> Vec> { + // grid-template-columns: repeat(, minmax(0, 1fr)); + unit.map(|count| vec![repeat(count, vec![minmax(length(0.0), fr(1.0))])]) + .unwrap_or_default() + } + + taffy::style::Style { + display: self.display.into(), + overflow: self.overflow.into(), + scrollbar_width: self.scrollbar_width.to_taffy(rem_size, scale_factor), + position: self.position.into(), + inset: self.inset.to_taffy(rem_size, scale_factor), + size: self.size.to_taffy(rem_size, scale_factor), + min_size: self.min_size.to_taffy(rem_size, scale_factor), + max_size: self.max_size.to_taffy(rem_size, scale_factor), + aspect_ratio: self.aspect_ratio, + margin: self.margin.to_taffy(rem_size, scale_factor), + padding: self.padding.to_taffy(rem_size, scale_factor), + border: self.border_widths.to_taffy(rem_size, scale_factor), + align_items: self.align_items.map(|x| x.into()), + align_self: self.align_self.map(|x| x.into()), + align_content: self.align_content.map(|x| x.into()), + justify_content: self.justify_content.map(|x| x.into()), + gap: self.gap.to_taffy(rem_size, scale_factor), + flex_direction: self.flex_direction.into(), + flex_wrap: self.flex_wrap.into(), + flex_basis: self.flex_basis.to_taffy(rem_size, scale_factor), + flex_grow: self.flex_grow, + flex_shrink: self.flex_shrink, + grid_template_rows: to_grid_repeat(&self.grid_rows), + grid_template_columns: to_grid_repeat(&self.grid_cols), + grid_row: self + .grid_location + .as_ref() + .map(|location| to_grid_line(&location.row)) + .unwrap_or_default(), + grid_column: self + .grid_location + .as_ref() + .map(|location| to_grid_line(&location.column)) + .unwrap_or_default(), + ..Default::default() + } + } +} + +impl ToTaffy for AbsoluteLength { + fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> f32 { + match self { + AbsoluteLength::Pixels(pixels) => { + let pixels: f32 = pixels.into(); + pixels * scale_factor + } + AbsoluteLength::Rems(rems) => { + let pixels: f32 = (*rems * rem_size).into(); + pixels * scale_factor + } + } + } +} + +impl ToTaffy for Length { + fn to_taffy( + &self, + rem_size: Pixels, + scale_factor: f32, + ) -> taffy::prelude::LengthPercentageAuto { + match self { + Length::Definite(length) => length.to_taffy(rem_size, scale_factor), + Length::Auto => taffy::prelude::LengthPercentageAuto::auto(), + } + } +} + +impl ToTaffy for Length { + fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::prelude::Dimension { + match self { + Length::Definite(length) => length.to_taffy(rem_size, scale_factor), + Length::Auto => taffy::prelude::Dimension::auto(), + } + } +} + +impl ToTaffy for DefiniteLength { + fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::style::LengthPercentage { + match self { + DefiniteLength::Absolute(length) => match length { + AbsoluteLength::Pixels(pixels) => { + let pixels: f32 = pixels.into(); + taffy::style::LengthPercentage::length(pixels * scale_factor) + } + AbsoluteLength::Rems(rems) => { + let pixels: f32 = (*rems * rem_size).into(); + taffy::style::LengthPercentage::length(pixels * scale_factor) + } + }, + DefiniteLength::Fraction(fraction) => { + taffy::style::LengthPercentage::percent(*fraction) + } + } + } +} + +impl ToTaffy for DefiniteLength { + fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::style::LengthPercentageAuto { + match self { + DefiniteLength::Absolute(length) => match length { + AbsoluteLength::Pixels(pixels) => { + let pixels: f32 = pixels.into(); + taffy::style::LengthPercentageAuto::length(pixels * scale_factor) + } + AbsoluteLength::Rems(rems) => { + let pixels: f32 = (*rems * rem_size).into(); + taffy::style::LengthPercentageAuto::length(pixels * scale_factor) + } + }, + DefiniteLength::Fraction(fraction) => { + taffy::style::LengthPercentageAuto::percent(*fraction) + } + } + } +} + +impl ToTaffy for DefiniteLength { + fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::style::Dimension { + match self { + DefiniteLength::Absolute(length) => match length { + AbsoluteLength::Pixels(pixels) => { + let pixels: f32 = pixels.into(); + taffy::style::Dimension::length(pixels * scale_factor) + } + AbsoluteLength::Rems(rems) => { + taffy::style::Dimension::length((*rems * rem_size * scale_factor).into()) + } + }, + DefiniteLength::Fraction(fraction) => taffy::style::Dimension::percent(*fraction), + } + } +} + +impl ToTaffy for AbsoluteLength { + fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::style::LengthPercentage { + match self { + AbsoluteLength::Pixels(pixels) => { + let pixels: f32 = pixels.into(); + taffy::style::LengthPercentage::length(pixels * scale_factor) + } + AbsoluteLength::Rems(rems) => { + let pixels: f32 = (*rems * rem_size).into(); + taffy::style::LengthPercentage::length(pixels * scale_factor) + } + } + } +} + +impl From> for Point +where + T: Into, + T2: Clone + Debug + Default + PartialEq, +{ + fn from(point: TaffyPoint) -> Point { + Point { + x: point.x.into(), + y: point.y.into(), + } + } +} + +impl From> for TaffyPoint +where + T: Into + Clone + Debug + Default + PartialEq, +{ + fn from(val: Point) -> Self { + TaffyPoint { + x: val.x.into(), + y: val.y.into(), + } + } +} + +impl ToTaffy> for Size +where + T: ToTaffy + Clone + Debug + Default + PartialEq, +{ + fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> TaffySize { + TaffySize { + width: self.width.to_taffy(rem_size, scale_factor), + height: self.height.to_taffy(rem_size, scale_factor), + } + } +} + +impl ToTaffy> for Edges +where + T: ToTaffy + Clone + Debug + Default + PartialEq, +{ + fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> TaffyRect { + TaffyRect { + top: self.top.to_taffy(rem_size, scale_factor), + right: self.right.to_taffy(rem_size, scale_factor), + bottom: self.bottom.to_taffy(rem_size, scale_factor), + left: self.left.to_taffy(rem_size, scale_factor), + } + } +} + +impl From> for Size +where + T: Into, + U: Clone + Debug + Default + PartialEq, +{ + fn from(taffy_size: TaffySize) -> Self { + Size { + width: taffy_size.width.into(), + height: taffy_size.height.into(), + } + } +} + +impl From> for TaffySize +where + T: Into + Clone + Debug + Default + PartialEq, +{ + fn from(size: Size) -> Self { + TaffySize { + width: size.width.into(), + height: size.height.into(), + } + } +} + +/// The space available for an element to be laid out in +#[derive(Copy, Clone, Default, Debug, Eq, PartialEq)] +pub enum AvailableSpace { + /// The amount of space available is the specified number of pixels + Definite(Pixels), + /// The amount of space available is indefinite and the node should be laid out under a min-content constraint + #[default] + MinContent, + /// The amount of space available is indefinite and the node should be laid out under a max-content constraint + MaxContent, +} + +impl AvailableSpace { + /// Returns a `Size` with both width and height set to `AvailableSpace::MinContent`. + /// + /// This function is useful when you want to create a `Size` with the minimum content constraints + /// for both dimensions. + /// + /// # Examples + /// + /// ``` + /// use gpui::AvailableSpace; + /// let min_content_size = AvailableSpace::min_size(); + /// assert_eq!(min_content_size.width, AvailableSpace::MinContent); + /// assert_eq!(min_content_size.height, AvailableSpace::MinContent); + /// ``` + pub const fn min_size() -> Size { + Size { + width: Self::MinContent, + height: Self::MinContent, + } + } +} + +impl From for TaffyAvailableSpace { + fn from(space: AvailableSpace) -> TaffyAvailableSpace { + match space { + AvailableSpace::Definite(Pixels(value)) => TaffyAvailableSpace::Definite(value), + AvailableSpace::MinContent => TaffyAvailableSpace::MinContent, + AvailableSpace::MaxContent => TaffyAvailableSpace::MaxContent, + } + } +} + +impl From for AvailableSpace { + fn from(space: TaffyAvailableSpace) -> AvailableSpace { + match space { + TaffyAvailableSpace::Definite(value) => AvailableSpace::Definite(Pixels(value)), + TaffyAvailableSpace::MinContent => AvailableSpace::MinContent, + TaffyAvailableSpace::MaxContent => AvailableSpace::MaxContent, + } + } +} + +impl From for AvailableSpace { + fn from(pixels: Pixels) -> Self { + AvailableSpace::Definite(pixels) + } +} + +impl From> for Size { + fn from(size: Size) -> Self { + Size { + width: AvailableSpace::Definite(size.width), + height: AvailableSpace::Definite(size.height), + } + } +} diff --git a/third_party/gpui/src/test.rs b/third_party/gpui/src/test.rs new file mode 100644 index 0000000..5ae72d2 --- /dev/null +++ b/third_party/gpui/src/test.rs @@ -0,0 +1,161 @@ +//! Test support for GPUI. +//! +//! GPUI provides first-class support for testing, which includes a macro to run test that rely on having a context, +//! and a test implementation of the `ForegroundExecutor` and `BackgroundExecutor` which ensure that your tests run +//! deterministically even in the face of arbitrary parallelism. +//! +//! The output of the `gpui::test` macro is understood by other rust test runners, so you can use it with `cargo test` +//! or `cargo-nextest`, or another runner of your choice. +//! +//! To make it possible to test collaborative user interfaces (like Zed) you can ask for as many different contexts +//! as you need. +//! +//! ## Example +//! +//! ``` +//! use gpui; +//! +//! #[gpui::test] +//! async fn test_example(cx: &TestAppContext) { +//! assert!(true) +//! } +//! +//! #[gpui::test] +//! async fn test_collaboration_example(cx_a: &TestAppContext, cx_b: &TestAppContext) { +//! assert!(true) +//! } +//! ``` +use crate::{Entity, Subscription, TestAppContext, TestDispatcher}; +use futures::StreamExt as _; +use rand::prelude::*; +use smol::channel; +use std::{ + env, + panic::{self, RefUnwindSafe}, + pin::Pin, +}; + +/// Run the given test function with the configured parameters. +/// This is intended for use with the `gpui::test` macro +/// and generally should not be used directly. +pub fn run_test( + num_iterations: usize, + explicit_seeds: &[u64], + max_retries: usize, + test_fn: &mut (dyn RefUnwindSafe + Fn(TestDispatcher, u64)), + on_fail_fn: Option, +) { + let (seeds, is_multiple_runs) = calculate_seeds(num_iterations as u64, explicit_seeds); + + for seed in seeds { + let mut attempt = 0; + loop { + if is_multiple_runs { + eprintln!("seed = {seed}"); + } + let result = panic::catch_unwind(|| { + let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(seed)); + test_fn(dispatcher, seed); + }); + + match result { + Ok(_) => break, + Err(error) => { + if attempt < max_retries { + println!("attempt {} failed, retrying", attempt); + attempt += 1; + // The panic payload might itself trigger an unwind on drop: + // https://doc.rust-lang.org/std/panic/fn.catch_unwind.html#notes + std::mem::forget(error); + } else { + if is_multiple_runs { + eprintln!("failing seed: {}", seed); + } + if let Some(on_fail_fn) = on_fail_fn { + on_fail_fn() + } + panic::resume_unwind(error); + } + } + } + } + } +} + +fn calculate_seeds( + iterations: u64, + explicit_seeds: &[u64], +) -> (impl Iterator + '_, bool) { + let iterations = env::var("ITERATIONS") + .ok() + .map(|var| var.parse().expect("invalid ITERATIONS variable")) + .unwrap_or(iterations); + + let env_num = env::var("SEED") + .map(|seed| seed.parse().expect("invalid SEED variable as integer")) + .ok(); + + let empty_range = || 0..0; + + let iter = { + let env_range = if let Some(env_num) = env_num { + env_num..env_num + 1 + } else { + empty_range() + }; + + // if `iterations` is 1 and !(`explicit_seeds` is non-empty || `SEED` is set), then add the run `0` + // if `iterations` is 1 and (`explicit_seeds` is non-empty || `SEED` is set), then discard the run `0` + // if `iterations` isn't 1 and `SEED` is set, do `SEED..SEED+iterations` + // otherwise, do `0..iterations` + let iterations_range = match (iterations, env_num) { + (1, None) if explicit_seeds.is_empty() => 0..1, + (1, None) | (1, Some(_)) => empty_range(), + (iterations, Some(env)) => env..env + iterations, + (iterations, None) => 0..iterations, + }; + + // if `SEED` is set, ignore `explicit_seeds` + let explicit_seeds = if env_num.is_some() { + &[] + } else { + explicit_seeds + }; + + env_range + .chain(iterations_range) + .chain(explicit_seeds.iter().copied()) + }; + let is_multiple_runs = iter.clone().nth(1).is_some(); + (iter, is_multiple_runs) +} + +/// A test struct for converting an observation callback into a stream. +pub struct Observation { + rx: Pin>>, + _subscription: Subscription, +} + +impl futures::Stream for Observation { + type Item = T; + + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.rx.poll_next_unpin(cx) + } +} + +/// observe returns a stream of the change events from the given `Entity` +pub fn observe(entity: &Entity, cx: &mut TestAppContext) -> Observation<()> { + let (tx, rx) = smol::channel::unbounded(); + let _subscription = cx.update(|cx| { + cx.observe(entity, move |_, _| { + let _ = smol::block_on(tx.send(())); + }) + }); + let rx = Box::pin(rx); + + Observation { rx, _subscription } +} diff --git a/third_party/gpui/src/text_system.rs b/third_party/gpui/src/text_system.rs new file mode 100644 index 0000000..85a3133 --- /dev/null +++ b/third_party/gpui/src/text_system.rs @@ -0,0 +1,918 @@ +mod font_fallbacks; +mod font_features; +mod line; +mod line_layout; +mod line_wrapper; + +pub use font_fallbacks::*; +pub use font_features::*; +pub use line::*; +pub use line_layout::*; +pub use line_wrapper::*; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{ + Bounds, DevicePixels, Hsla, Pixels, PlatformTextSystem, Point, Result, SharedString, Size, + StrikethroughStyle, UnderlineStyle, px, +}; +use anyhow::{Context as _, anyhow}; +use collections::FxHashMap; +use core::fmt; +use derive_more::{Add, Deref, FromStr, Sub}; +use itertools::Itertools; +use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; +use smallvec::{SmallVec, smallvec}; +use std::{ + borrow::Cow, + cmp, + fmt::{Debug, Display, Formatter}, + hash::{Hash, Hasher}, + ops::{Deref, DerefMut, Range}, + sync::Arc, +}; + +/// An opaque identifier for a specific font. +#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)] +#[repr(C)] +pub struct FontId(pub usize); + +/// An opaque identifier for a specific font family. +#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)] +pub struct FontFamilyId(pub usize); + +pub(crate) const SUBPIXEL_VARIANTS_X: u8 = 4; + +pub(crate) const SUBPIXEL_VARIANTS_Y: u8 = + if cfg!(target_os = "windows") || cfg!(target_os = "linux") { + 1 + } else { + SUBPIXEL_VARIANTS_X + }; + +/// The GPUI text rendering sub system. +pub struct TextSystem { + platform_text_system: Arc, + font_ids_by_font: RwLock>>, + font_metrics: RwLock>, + raster_bounds: RwLock>>, + wrapper_pool: Mutex>>, + font_runs_pool: Mutex>>, + fallback_font_stack: SmallVec<[Font; 2]>, +} + +impl TextSystem { + pub(crate) fn new(platform_text_system: Arc) -> Self { + TextSystem { + platform_text_system, + font_metrics: RwLock::default(), + raster_bounds: RwLock::default(), + font_ids_by_font: RwLock::default(), + wrapper_pool: Mutex::default(), + font_runs_pool: Mutex::default(), + fallback_font_stack: smallvec![ + // TODO: Remove this when Linux have implemented setting fallbacks. + font(".ZedMono"), + font(".ZedSans"), + font("Helvetica"), + font("Segoe UI"), // Windows + font("Ubuntu"), // Gnome (Ubuntu) + font("Adwaita Sans"), // Gnome 47 + font("Cantarell"), // Gnome + font("Noto Sans"), // KDE + font("DejaVu Sans"), + font("Arial"), // macOS, Windows + ], + } + } + + /// Get a list of all available font names from the operating system. + pub fn all_font_names(&self) -> Vec { + let mut names = self.platform_text_system.all_font_names(); + names.extend( + self.fallback_font_stack + .iter() + .map(|font| font.family.to_string()), + ); + names.push(".SystemUIFont".to_string()); + names.sort(); + names.dedup(); + names + } + + /// Add a font's data to the text system. + pub fn add_fonts(&self, fonts: Vec>) -> Result<()> { + self.platform_text_system.add_fonts(fonts) + } + + /// Get the FontId for the configure font family and style. + fn font_id(&self, font: &Font) -> Result { + fn clone_font_id_result(font_id: &Result) -> Result { + match font_id { + Ok(font_id) => Ok(*font_id), + Err(err) => Err(anyhow!("{err}")), + } + } + + let font_id = self + .font_ids_by_font + .read() + .get(font) + .map(clone_font_id_result); + if let Some(font_id) = font_id { + font_id + } else { + let font_id = self.platform_text_system.font_id(font); + self.font_ids_by_font + .write() + .insert(font.clone(), clone_font_id_result(&font_id)); + font_id + } + } + + /// Get the Font for the Font Id. + pub fn get_font_for_id(&self, id: FontId) -> Option { + let lock = self.font_ids_by_font.read(); + lock.iter() + .filter_map(|(font, result)| match result { + Ok(font_id) if *font_id == id => Some(font.clone()), + _ => None, + }) + .next() + } + + /// Resolves the specified font, falling back to the default font stack if + /// the font fails to load. + /// + /// # Panics + /// + /// Panics if the font and none of the fallbacks can be resolved. + pub fn resolve_font(&self, font: &Font) -> FontId { + if let Ok(font_id) = self.font_id(font) { + return font_id; + } + for fallback in &self.fallback_font_stack { + if let Ok(font_id) = self.font_id(fallback) { + return font_id; + } + } + + panic!( + "failed to resolve font '{}' or any of the fallbacks: {}", + font.family, + self.fallback_font_stack + .iter() + .map(|fallback| &fallback.family) + .join(", ") + ); + } + + /// Get the bounding box for the given font and font size. + /// A font's bounding box is the smallest rectangle that could enclose all glyphs + /// in the font. superimposed over one another. + pub fn bounding_box(&self, font_id: FontId, font_size: Pixels) -> Bounds { + self.read_metrics(font_id, |metrics| metrics.bounding_box(font_size)) + } + + /// Get the typographic bounds for the given character, in the given font and size. + pub fn typographic_bounds( + &self, + font_id: FontId, + font_size: Pixels, + character: char, + ) -> Result> { + let glyph_id = self + .platform_text_system + .glyph_for_char(font_id, character) + .with_context(|| format!("glyph not found for character '{character}'"))?; + let bounds = self + .platform_text_system + .typographic_bounds(font_id, glyph_id)?; + Ok(self.read_metrics(font_id, |metrics| { + (bounds / metrics.units_per_em as f32 * font_size.0).map(px) + })) + } + + /// Get the advance width for the given character, in the given font and size. + pub fn advance(&self, font_id: FontId, font_size: Pixels, ch: char) -> Result> { + let glyph_id = self + .platform_text_system + .glyph_for_char(font_id, ch) + .with_context(|| format!("glyph not found for character '{ch}'"))?; + let result = self.platform_text_system.advance(font_id, glyph_id)? + / self.units_per_em(font_id) as f32; + + Ok(result * font_size) + } + + /// Returns the width of an `em`. + /// + /// Uses the width of the `m` character in the given font and size. + pub fn em_width(&self, font_id: FontId, font_size: Pixels) -> Result { + Ok(self.typographic_bounds(font_id, font_size, 'm')?.size.width) + } + + /// Returns the advance width of an `em`. + /// + /// Uses the advance width of the `m` character in the given font and size. + pub fn em_advance(&self, font_id: FontId, font_size: Pixels) -> Result { + Ok(self.advance(font_id, font_size, 'm')?.width) + } + + /// Returns the width of an `ch`. + /// + /// Uses the width of the `0` character in the given font and size. + pub fn ch_width(&self, font_id: FontId, font_size: Pixels) -> Result { + Ok(self.typographic_bounds(font_id, font_size, '0')?.size.width) + } + + /// Returns the advance width of an `ch`. + /// + /// Uses the advance width of the `0` character in the given font and size. + pub fn ch_advance(&self, font_id: FontId, font_size: Pixels) -> Result { + Ok(self.advance(font_id, font_size, '0')?.width) + } + + /// Get the number of font size units per 'em square', + /// Per MDN: "an abstract square whose height is the intended distance between + /// lines of type in the same type size" + pub fn units_per_em(&self, font_id: FontId) -> u32 { + self.read_metrics(font_id, |metrics| metrics.units_per_em) + } + + /// Get the height of a capital letter in the given font and size. + pub fn cap_height(&self, font_id: FontId, font_size: Pixels) -> Pixels { + self.read_metrics(font_id, |metrics| metrics.cap_height(font_size)) + } + + /// Get the height of the x character in the given font and size. + pub fn x_height(&self, font_id: FontId, font_size: Pixels) -> Pixels { + self.read_metrics(font_id, |metrics| metrics.x_height(font_size)) + } + + /// Get the recommended distance from the baseline for the given font + pub fn ascent(&self, font_id: FontId, font_size: Pixels) -> Pixels { + self.read_metrics(font_id, |metrics| metrics.ascent(font_size)) + } + + /// Get the recommended distance below the baseline for the given font, + /// in single spaced text. + pub fn descent(&self, font_id: FontId, font_size: Pixels) -> Pixels { + self.read_metrics(font_id, |metrics| metrics.descent(font_size)) + } + + /// Get the recommended baseline offset for the given font and line height. + pub fn baseline_offset( + &self, + font_id: FontId, + font_size: Pixels, + line_height: Pixels, + ) -> Pixels { + let ascent = self.ascent(font_id, font_size); + let descent = self.descent(font_id, font_size); + let padding_top = (line_height - ascent - descent) / 2.; + padding_top + ascent + } + + fn read_metrics(&self, font_id: FontId, read: impl FnOnce(&FontMetrics) -> T) -> T { + let lock = self.font_metrics.upgradable_read(); + + if let Some(metrics) = lock.get(&font_id) { + read(metrics) + } else { + let mut lock = RwLockUpgradableReadGuard::upgrade(lock); + let metrics = lock + .entry(font_id) + .or_insert_with(|| self.platform_text_system.font_metrics(font_id)); + read(metrics) + } + } + + /// Returns a handle to a line wrapper, for the given font and font size. + pub fn line_wrapper(self: &Arc, font: Font, font_size: Pixels) -> LineWrapperHandle { + let lock = &mut self.wrapper_pool.lock(); + let font_id = self.resolve_font(&font); + let wrappers = lock + .entry(FontIdWithSize { font_id, font_size }) + .or_default(); + let wrapper = wrappers.pop().unwrap_or_else(|| { + LineWrapper::new(font_id, font_size, self.platform_text_system.clone()) + }); + + LineWrapperHandle { + wrapper: Some(wrapper), + text_system: self.clone(), + } + } + + /// Get the rasterized size and location of a specific, rendered glyph. + pub(crate) fn raster_bounds(&self, params: &RenderGlyphParams) -> Result> { + let raster_bounds = self.raster_bounds.upgradable_read(); + if let Some(bounds) = raster_bounds.get(params) { + Ok(*bounds) + } else { + let mut raster_bounds = RwLockUpgradableReadGuard::upgrade(raster_bounds); + let bounds = self.platform_text_system.glyph_raster_bounds(params)?; + raster_bounds.insert(params.clone(), bounds); + Ok(bounds) + } + } + + pub(crate) fn rasterize_glyph( + &self, + params: &RenderGlyphParams, + ) -> Result<(Size, Vec)> { + let raster_bounds = self.raster_bounds(params)?; + self.platform_text_system + .rasterize_glyph(params, raster_bounds) + } +} + +/// The GPUI text layout subsystem. +#[derive(Deref)] +pub struct WindowTextSystem { + line_layout_cache: LineLayoutCache, + #[deref] + text_system: Arc, +} + +impl WindowTextSystem { + pub(crate) fn new(text_system: Arc) -> Self { + Self { + line_layout_cache: LineLayoutCache::new(text_system.platform_text_system.clone()), + text_system, + } + } + + pub(crate) fn layout_index(&self) -> LineLayoutIndex { + self.line_layout_cache.layout_index() + } + + pub(crate) fn reuse_layouts(&self, index: Range) { + self.line_layout_cache.reuse_layouts(index) + } + + pub(crate) fn truncate_layouts(&self, index: LineLayoutIndex) { + self.line_layout_cache.truncate_layouts(index) + } + + /// Shape the given line, at the given font_size, for painting to the screen. + /// Subsets of the line can be styled independently with the `runs` parameter. + /// + /// Note that this method can only shape a single line of text. It will panic + /// if the text contains newlines. If you need to shape multiple lines of text, + /// use [`Self::shape_text`] instead. + pub fn shape_line( + &self, + text: SharedString, + font_size: Pixels, + runs: &[TextRun], + force_width: Option, + ) -> ShapedLine { + debug_assert!( + text.find('\n').is_none(), + "text argument should not contain newlines" + ); + + let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new(); + for run in runs { + if let Some(last_run) = decoration_runs.last_mut() + && last_run.color == run.color + && last_run.underline == run.underline + && last_run.strikethrough == run.strikethrough + && last_run.background_color == run.background_color + { + last_run.len += run.len as u32; + continue; + } + decoration_runs.push(DecorationRun { + len: run.len as u32, + color: run.color, + background_color: run.background_color, + underline: run.underline, + strikethrough: run.strikethrough, + }); + } + + let layout = self.layout_line(&text, font_size, runs, force_width); + + ShapedLine { + layout, + text, + decoration_runs, + } + } + + /// Shape a multi line string of text, at the given font_size, for painting to the screen. + /// Subsets of the text can be styled independently with the `runs` parameter. + /// If `wrap_width` is provided, the line breaks will be adjusted to fit within the given width. + pub fn shape_text( + &self, + text: SharedString, + font_size: Pixels, + runs: &[TextRun], + wrap_width: Option, + line_clamp: Option, + ) -> Result> { + let mut runs = runs.iter().filter(|run| run.len > 0).cloned().peekable(); + let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default(); + + let mut lines = SmallVec::new(); + let mut line_start = 0; + let mut max_wrap_lines = line_clamp.unwrap_or(usize::MAX); + let mut wrapped_lines = 0; + + let mut process_line = |line_text: SharedString| { + font_runs.clear(); + let line_end = line_start + line_text.len(); + + let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new(); + let mut run_start = line_start; + while run_start < line_end { + let Some(run) = runs.peek_mut() else { + break; + }; + + let run_len_within_line = cmp::min(line_end, run_start + run.len) - run_start; + + let decoration_changed = if let Some(last_run) = decoration_runs.last_mut() + && last_run.color == run.color + && last_run.underline == run.underline + && last_run.strikethrough == run.strikethrough + && last_run.background_color == run.background_color + { + last_run.len += run_len_within_line as u32; + false + } else { + decoration_runs.push(DecorationRun { + len: run_len_within_line as u32, + color: run.color, + background_color: run.background_color, + underline: run.underline, + strikethrough: run.strikethrough, + }); + true + }; + + let font_id = self.resolve_font(&run.font); + if let Some(font_run) = font_runs.last_mut() + && font_id == font_run.font_id + && !decoration_changed + { + font_run.len += run_len_within_line; + } else { + font_runs.push(FontRun { + len: run_len_within_line, + font_id, + }); + } + + if run_len_within_line == run.len { + runs.next(); + } else { + // Preserve the remainder of the run for the next line + run.len -= run_len_within_line; + } + run_start += run_len_within_line; + } + + let layout = self.line_layout_cache.layout_wrapped_line( + &line_text, + font_size, + &font_runs, + wrap_width, + Some(max_wrap_lines - wrapped_lines), + ); + wrapped_lines += layout.wrap_boundaries.len(); + + lines.push(WrappedLine { + layout, + decoration_runs, + text: line_text, + }); + + // Skip `\n` character. + line_start = line_end + 1; + if let Some(run) = runs.peek_mut() { + run.len -= 1; + if run.len == 0 { + runs.next(); + } + } + }; + + let mut split_lines = text.split('\n'); + let mut processed = false; + + if let Some(first_line) = split_lines.next() + && let Some(second_line) = split_lines.next() + { + processed = true; + process_line(first_line.to_string().into()); + process_line(second_line.to_string().into()); + for line_text in split_lines { + process_line(line_text.to_string().into()); + } + } + + if !processed { + process_line(text); + } + + self.font_runs_pool.lock().push(font_runs); + + Ok(lines) + } + + pub(crate) fn finish_frame(&self) { + self.line_layout_cache.finish_frame() + } + + /// Layout the given line of text, at the given font_size. + /// Subsets of the line can be styled independently with the `runs` parameter. + /// Generally, you should prefer to use [`Self::shape_line`] instead, which + /// can be painted directly. + pub fn layout_line( + &self, + text: &str, + font_size: Pixels, + runs: &[TextRun], + force_width: Option, + ) -> Arc { + let mut last_run = None::<&TextRun>; + let mut last_font: Option = None; + let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default(); + font_runs.clear(); + + for run in runs.iter() { + let decoration_changed = if let Some(last_run) = last_run + && last_run.color == run.color + && last_run.underline == run.underline + && last_run.strikethrough == run.strikethrough + // we do not consider differing background color relevant, as it does not affect glyphs + // && last_run.background_color == run.background_color + { + false + } else { + last_run = Some(run); + true + }; + + if let Some(font_run) = font_runs.last_mut() + && Some(font_run.font_id) == last_font + && !decoration_changed + { + font_run.len += run.len; + } else { + let font_id = self.resolve_font(&run.font); + last_font = Some(font_id); + font_runs.push(FontRun { + len: run.len, + font_id, + }); + } + } + + let layout = self.line_layout_cache.layout_line( + &SharedString::new(text), + font_size, + &font_runs, + force_width, + ); + + self.font_runs_pool.lock().push(font_runs); + + layout + } +} + +#[derive(Hash, Eq, PartialEq)] +struct FontIdWithSize { + font_id: FontId, + font_size: Pixels, +} + +/// A handle into the text system, which can be used to compute the wrapped layout of text +pub struct LineWrapperHandle { + wrapper: Option, + text_system: Arc, +} + +impl Drop for LineWrapperHandle { + fn drop(&mut self) { + let mut state = self.text_system.wrapper_pool.lock(); + let wrapper = self.wrapper.take().unwrap(); + state + .get_mut(&FontIdWithSize { + font_id: wrapper.font_id, + font_size: wrapper.font_size, + }) + .unwrap() + .push(wrapper); + } +} + +impl Deref for LineWrapperHandle { + type Target = LineWrapper; + + fn deref(&self) -> &Self::Target { + self.wrapper.as_ref().unwrap() + } +} + +impl DerefMut for LineWrapperHandle { + fn deref_mut(&mut self) -> &mut Self::Target { + self.wrapper.as_mut().unwrap() + } +} + +/// The degree of blackness or stroke thickness of a font. This value ranges from 100.0 to 900.0, +/// with 400.0 as normal. +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Serialize, Deserialize, Add, Sub, FromStr)] +#[serde(transparent)] +pub struct FontWeight(pub f32); + +impl Display for FontWeight { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for FontWeight { + fn from(weight: f32) -> Self { + FontWeight(weight) + } +} + +impl Default for FontWeight { + #[inline] + fn default() -> FontWeight { + FontWeight::NORMAL + } +} + +impl Hash for FontWeight { + fn hash(&self, state: &mut H) { + state.write_u32(u32::from_be_bytes(self.0.to_be_bytes())); + } +} + +impl Eq for FontWeight {} + +impl FontWeight { + /// Thin weight (100), the thinnest value. + pub const THIN: FontWeight = FontWeight(100.0); + /// Extra light weight (200). + pub const EXTRA_LIGHT: FontWeight = FontWeight(200.0); + /// Light weight (300). + pub const LIGHT: FontWeight = FontWeight(300.0); + /// Normal (400). + pub const NORMAL: FontWeight = FontWeight(400.0); + /// Medium weight (500, higher than normal). + pub const MEDIUM: FontWeight = FontWeight(500.0); + /// Semibold weight (600). + pub const SEMIBOLD: FontWeight = FontWeight(600.0); + /// Bold weight (700). + pub const BOLD: FontWeight = FontWeight(700.0); + /// Extra-bold weight (800). + pub const EXTRA_BOLD: FontWeight = FontWeight(800.0); + /// Black weight (900), the thickest value. + pub const BLACK: FontWeight = FontWeight(900.0); + + /// All of the font weights, in order from thinnest to thickest. + pub const ALL: [FontWeight; 9] = [ + Self::THIN, + Self::EXTRA_LIGHT, + Self::LIGHT, + Self::NORMAL, + Self::MEDIUM, + Self::SEMIBOLD, + Self::BOLD, + Self::EXTRA_BOLD, + Self::BLACK, + ]; +} + +impl schemars::JsonSchema for FontWeight { + fn schema_name() -> std::borrow::Cow<'static, str> { + "FontWeight".into() + } + + fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { + use schemars::json_schema; + json_schema!({ + "type": "number", + "minimum": Self::THIN, + "maximum": Self::BLACK, + "default": Self::default(), + "description": "Font weight value between 100 (thin) and 900 (black)" + }) + } +} + +/// Allows italic or oblique faces to be selected. +#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize, JsonSchema)] +pub enum FontStyle { + /// A face that is neither italic not obliqued. + #[default] + Normal, + /// A form that is generally cursive in nature. + Italic, + /// A typically-sloped version of the regular face. + Oblique, +} + +impl Display for FontStyle { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + Debug::fmt(self, f) + } +} + +/// A styled run of text, for use in [`crate::TextLayout`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TextRun { + /// A number of utf8 bytes + pub len: usize, + /// The font to use for this run. + pub font: Font, + /// The color + pub color: Hsla, + /// The background color (if any) + pub background_color: Option, + /// The underline style (if any) + pub underline: Option, + /// The strikethrough style (if any) + pub strikethrough: Option, +} + +#[cfg(all(target_os = "macos", test))] +impl TextRun { + fn with_len(&self, len: usize) -> Self { + let mut this = self.clone(); + this.len = len; + this + } +} + +/// An identifier for a specific glyph, as returned by [`WindowTextSystem::layout_line`]. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +#[repr(C)] +pub struct GlyphId(pub(crate) u32); + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct RenderGlyphParams { + pub(crate) font_id: FontId, + pub(crate) glyph_id: GlyphId, + pub(crate) font_size: Pixels, + pub(crate) subpixel_variant: Point, + pub(crate) scale_factor: f32, + pub(crate) is_emoji: bool, +} + +impl Eq for RenderGlyphParams {} + +impl Hash for RenderGlyphParams { + fn hash(&self, state: &mut H) { + self.font_id.0.hash(state); + self.glyph_id.0.hash(state); + self.font_size.0.to_bits().hash(state); + self.subpixel_variant.hash(state); + self.scale_factor.to_bits().hash(state); + self.is_emoji.hash(state); + } +} + +/// The configuration details for identifying a specific font. +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub struct Font { + /// The font family name. + /// + /// The special name ".SystemUIFont" is used to identify the system UI font, which varies based on platform. + pub family: SharedString, + + /// The font features to use. + pub features: FontFeatures, + + /// The fallbacks fonts to use. + pub fallbacks: Option, + + /// The font weight. + pub weight: FontWeight, + + /// The font style. + pub style: FontStyle, +} + +/// Get a [`Font`] for a given name. +pub fn font(family: impl Into) -> Font { + Font { + family: family.into(), + features: FontFeatures::default(), + weight: FontWeight::default(), + style: FontStyle::default(), + fallbacks: None, + } +} + +impl Font { + /// Set this Font to be bold + pub fn bold(mut self) -> Self { + self.weight = FontWeight::BOLD; + self + } + + /// Set this Font to be italic + pub fn italic(mut self) -> Self { + self.style = FontStyle::Italic; + self + } +} + +/// A struct for storing font metrics. +/// It is used to define the measurements of a typeface. +#[derive(Clone, Copy, Debug)] +pub struct FontMetrics { + /// The number of font units that make up the "em square", + /// a scalable grid for determining the size of a typeface. + pub(crate) units_per_em: u32, + + /// The vertical distance from the baseline of the font to the top of the glyph covers. + pub(crate) ascent: f32, + + /// The vertical distance from the baseline of the font to the bottom of the glyph covers. + pub(crate) descent: f32, + + /// The recommended additional space to add between lines of type. + pub(crate) line_gap: f32, + + /// The suggested position of the underline. + pub(crate) underline_position: f32, + + /// The suggested thickness of the underline. + pub(crate) underline_thickness: f32, + + /// The height of a capital letter measured from the baseline of the font. + pub(crate) cap_height: f32, + + /// The height of a lowercase x. + pub(crate) x_height: f32, + + /// The outer limits of the area that the font covers. + /// Corresponds to the xMin / xMax / yMin / yMax values in the OpenType `head` table + pub(crate) bounding_box: Bounds, +} + +impl FontMetrics { + /// Returns the vertical distance from the baseline of the font to the top of the glyph covers in pixels. + pub fn ascent(&self, font_size: Pixels) -> Pixels { + Pixels((self.ascent / self.units_per_em as f32) * font_size.0) + } + + /// Returns the vertical distance from the baseline of the font to the bottom of the glyph covers in pixels. + pub fn descent(&self, font_size: Pixels) -> Pixels { + Pixels((self.descent / self.units_per_em as f32) * font_size.0) + } + + /// Returns the recommended additional space to add between lines of type in pixels. + pub fn line_gap(&self, font_size: Pixels) -> Pixels { + Pixels((self.line_gap / self.units_per_em as f32) * font_size.0) + } + + /// Returns the suggested position of the underline in pixels. + pub fn underline_position(&self, font_size: Pixels) -> Pixels { + Pixels((self.underline_position / self.units_per_em as f32) * font_size.0) + } + + /// Returns the suggested thickness of the underline in pixels. + pub fn underline_thickness(&self, font_size: Pixels) -> Pixels { + Pixels((self.underline_thickness / self.units_per_em as f32) * font_size.0) + } + + /// Returns the height of a capital letter measured from the baseline of the font in pixels. + pub fn cap_height(&self, font_size: Pixels) -> Pixels { + Pixels((self.cap_height / self.units_per_em as f32) * font_size.0) + } + + /// Returns the height of a lowercase x in pixels. + pub fn x_height(&self, font_size: Pixels) -> Pixels { + Pixels((self.x_height / self.units_per_em as f32) * font_size.0) + } + + /// Returns the outer limits of the area that the font covers in pixels. + pub fn bounding_box(&self, font_size: Pixels) -> Bounds { + (self.bounding_box / self.units_per_em as f32 * font_size.0).map(px) + } +} + +#[allow(unused)] +pub(crate) fn font_name_with_fallbacks<'a>(name: &'a str, system: &'a str) -> &'a str { + // Note: the "Zed Plex" fonts were deprecated as we are not allowed to use "Plex" + // in a derived font name. They are essentially indistinguishable from IBM Plex/Lilex, + // and so retained here for backward compatibility. + match name { + ".SystemUIFont" => system, + ".ZedSans" | "Zed Plex Sans" => "IBM Plex Sans", + ".ZedMono" | "Zed Plex Mono" => "Lilex", + _ => name, + } +} diff --git a/third_party/gpui/src/text_system/font_fallbacks.rs b/third_party/gpui/src/text_system/font_fallbacks.rs new file mode 100644 index 0000000..63dc89b --- /dev/null +++ b/third_party/gpui/src/text_system/font_fallbacks.rs @@ -0,0 +1,21 @@ +use std::sync::Arc; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// The fallback fonts that can be configured for a given font. +/// Fallback fonts family names are stored here. +#[derive(Default, Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize, JsonSchema)] +pub struct FontFallbacks(pub Arc>); + +impl FontFallbacks { + /// Get the fallback fonts family names + pub fn fallback_list(&self) -> &[String] { + self.0.as_slice() + } + + /// Create a font fallback from a list of strings + pub fn from_fonts(fonts: Vec) -> Self { + FontFallbacks(Arc::new(fonts)) + } +} diff --git a/third_party/gpui/src/text_system/font_features.rs b/third_party/gpui/src/text_system/font_features.rs new file mode 100644 index 0000000..c1ab72b --- /dev/null +++ b/third_party/gpui/src/text_system/font_features.rs @@ -0,0 +1,154 @@ +use std::borrow::Cow; +use std::sync::Arc; + +use schemars::{JsonSchema, json_schema}; + +/// The OpenType features that can be configured for a given font. +#[derive(Default, Clone, Eq, PartialEq, Hash)] +pub struct FontFeatures(pub Arc>); + +impl FontFeatures { + /// Disables `calt`. + pub fn disable_ligatures() -> Self { + Self(Arc::new(vec![("calt".into(), 0)])) + } + + /// Get the tag name list of the font OpenType features + /// only enabled or disabled features are returned + pub fn tag_value_list(&self) -> &[(String, u32)] { + self.0.as_slice() + } + + /// Returns whether the `calt` feature is enabled. + /// + /// Returns `None` if the feature is not present. + pub fn is_calt_enabled(&self) -> Option { + self.0 + .iter() + .find(|(feature, _)| feature == "calt") + .map(|(_, value)| *value == 1) + } +} + +impl std::fmt::Debug for FontFeatures { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut debug = f.debug_struct("FontFeatures"); + for (tag, value) in self.tag_value_list() { + debug.field(tag, value); + } + + debug.finish() + } +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +enum FeatureValue { + Bool(bool), + Number(serde_json::Number), +} + +impl<'de> serde::Deserialize<'de> for FontFeatures { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::{MapAccess, Visitor}; + use std::fmt; + + struct FontFeaturesVisitor; + + impl<'de> Visitor<'de> for FontFeaturesVisitor { + type Value = FontFeatures; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a map of font features") + } + + fn visit_map(self, mut access: M) -> Result + where + M: MapAccess<'de>, + { + let mut feature_list = Vec::new(); + + while let Some((key, value)) = + access.next_entry::>()? + { + if !is_valid_feature_tag(&key) { + log::error!("Incorrect font feature tag: {}", key); + continue; + } + if let Some(value) = value { + match value { + FeatureValue::Bool(enable) => { + if enable { + feature_list.push((key, 1)); + } else { + feature_list.push((key, 0)); + } + } + FeatureValue::Number(value) => { + if value.is_u64() { + feature_list.push((key, value.as_u64().unwrap() as u32)); + } else { + log::error!( + "Incorrect font feature value {} for feature tag {}", + value, + key + ); + continue; + } + } + } + } + } + + Ok(FontFeatures(Arc::new(feature_list))) + } + } + + let features = deserializer.deserialize_map(FontFeaturesVisitor)?; + Ok(features) + } +} + +impl serde::Serialize for FontFeatures { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeMap; + + let mut map = serializer.serialize_map(None)?; + + for (tag, value) in self.tag_value_list() { + map.serialize_entry(tag, value)?; + } + + map.end() + } +} + +impl JsonSchema for FontFeatures { + fn schema_name() -> Cow<'static, str> { + "FontFeatures".into() + } + + fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { + json_schema!({ + "type": "object", + "patternProperties": { + "[0-9a-zA-Z]{4}$": { + "type": ["boolean", "integer"], + "minimum": 0, + "multipleOf": 1 + } + }, + "additionalProperties": false + }) + } +} + +fn is_valid_feature_tag(tag: &str) -> bool { + tag.len() == 4 && tag.chars().all(|c| c.is_ascii_alphanumeric()) +} diff --git a/third_party/gpui/src/text_system/line.rs b/third_party/gpui/src/text_system/line.rs new file mode 100644 index 0000000..189a3e8 --- /dev/null +++ b/third_party/gpui/src/text_system/line.rs @@ -0,0 +1,591 @@ +use crate::{ + App, Bounds, Half, Hsla, LineLayout, Pixels, Point, Result, SharedString, StrikethroughStyle, + TextAlign, UnderlineStyle, Window, WrapBoundary, WrappedLineLayout, black, fill, point, px, + size, +}; +use derive_more::{Deref, DerefMut}; +use smallvec::SmallVec; +use std::sync::Arc; + +/// Set the text decoration for a run of text. +#[derive(Debug, Clone)] +pub struct DecorationRun { + /// The length of the run in utf-8 bytes. + pub len: u32, + + /// The color for this run + pub color: Hsla, + + /// The background color for this run + pub background_color: Option, + + /// The underline style for this run + pub underline: Option, + + /// The strikethrough style for this run + pub strikethrough: Option, +} + +/// A line of text that has been shaped and decorated. +#[derive(Clone, Default, Debug, Deref, DerefMut)] +pub struct ShapedLine { + #[deref] + #[deref_mut] + pub(crate) layout: Arc, + /// The text that was shaped for this line. + pub text: SharedString, + pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>, +} + +impl ShapedLine { + /// The length of the line in utf-8 bytes. + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + self.layout.len + } + + /// Override the len, useful if you're rendering text a + /// as text b (e.g. rendering invisibles). + pub fn with_len(mut self, len: usize) -> Self { + let layout = self.layout.as_ref(); + self.layout = Arc::new(LineLayout { + font_size: layout.font_size, + width: layout.width, + ascent: layout.ascent, + descent: layout.descent, + runs: layout.runs.clone(), + len, + }); + self + } + + /// Paint the line of text to the window. + pub fn paint( + &self, + origin: Point, + line_height: Pixels, + window: &mut Window, + cx: &mut App, + ) -> Result<()> { + paint_line( + origin, + &self.layout, + line_height, + TextAlign::default(), + None, + &self.decoration_runs, + &[], + window, + cx, + )?; + + Ok(()) + } + + /// Paint the background of the line to the window. + pub fn paint_background( + &self, + origin: Point, + line_height: Pixels, + window: &mut Window, + cx: &mut App, + ) -> Result<()> { + paint_line_background( + origin, + &self.layout, + line_height, + TextAlign::default(), + None, + &self.decoration_runs, + &[], + window, + cx, + )?; + + Ok(()) + } +} + +/// A line of text that has been shaped, decorated, and wrapped by the text layout system. +#[derive(Clone, Default, Debug, Deref, DerefMut)] +pub struct WrappedLine { + #[deref] + #[deref_mut] + pub(crate) layout: Arc, + /// The text that was shaped for this line. + pub text: SharedString, + pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>, +} + +impl WrappedLine { + /// The length of the underlying, unwrapped layout, in utf-8 bytes. + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + self.layout.len() + } + + /// Paint this line of text to the window. + pub fn paint( + &self, + origin: Point, + line_height: Pixels, + align: TextAlign, + bounds: Option>, + window: &mut Window, + cx: &mut App, + ) -> Result<()> { + let align_width = match bounds { + Some(bounds) => Some(bounds.size.width), + None => self.layout.wrap_width, + }; + + paint_line( + origin, + &self.layout.unwrapped_layout, + line_height, + align, + align_width, + &self.decoration_runs, + &self.wrap_boundaries, + window, + cx, + )?; + + Ok(()) + } + + /// Paint the background of line of text to the window. + pub fn paint_background( + &self, + origin: Point, + line_height: Pixels, + align: TextAlign, + bounds: Option>, + window: &mut Window, + cx: &mut App, + ) -> Result<()> { + let align_width = match bounds { + Some(bounds) => Some(bounds.size.width), + None => self.layout.wrap_width, + }; + + paint_line_background( + origin, + &self.layout.unwrapped_layout, + line_height, + align, + align_width, + &self.decoration_runs, + &self.wrap_boundaries, + window, + cx, + )?; + + Ok(()) + } +} + +fn paint_line( + origin: Point, + layout: &LineLayout, + line_height: Pixels, + align: TextAlign, + align_width: Option, + decoration_runs: &[DecorationRun], + wrap_boundaries: &[WrapBoundary], + window: &mut Window, + cx: &mut App, +) -> Result<()> { + let line_bounds = Bounds::new( + origin, + size( + layout.width, + line_height * (wrap_boundaries.len() as f32 + 1.), + ), + ); + window.paint_layer(line_bounds, |window| { + let padding_top = (line_height - layout.ascent - layout.descent) / 2.; + let baseline_offset = point(px(0.), padding_top + layout.ascent); + let mut decoration_runs = decoration_runs.iter(); + let mut wraps = wrap_boundaries.iter().peekable(); + let mut run_end = 0; + let mut color = black(); + let mut current_underline: Option<(Point, UnderlineStyle)> = None; + let mut current_strikethrough: Option<(Point, StrikethroughStyle)> = None; + let text_system = cx.text_system().clone(); + let mut glyph_origin = point( + aligned_origin_x( + origin, + align_width.unwrap_or(layout.width), + px(0.0), + &align, + layout, + wraps.peek(), + ), + origin.y, + ); + let mut prev_glyph_position = Point::default(); + let mut max_glyph_size = size(px(0.), px(0.)); + let mut first_glyph_x = origin.x; + for (run_ix, run) in layout.runs.iter().enumerate() { + max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size; + + for (glyph_ix, glyph) in run.glyphs.iter().enumerate() { + glyph_origin.x += glyph.position.x - prev_glyph_position.x; + if glyph_ix == 0 && run_ix == 0 { + first_glyph_x = glyph_origin.x; + } + + if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) { + wraps.next(); + if let Some((underline_origin, underline_style)) = current_underline.as_mut() { + if glyph_origin.x == underline_origin.x { + underline_origin.x -= max_glyph_size.width.half(); + }; + window.paint_underline( + *underline_origin, + glyph_origin.x - underline_origin.x, + underline_style, + ); + underline_origin.x = origin.x; + underline_origin.y += line_height; + } + if let Some((strikethrough_origin, strikethrough_style)) = + current_strikethrough.as_mut() + { + if glyph_origin.x == strikethrough_origin.x { + strikethrough_origin.x -= max_glyph_size.width.half(); + }; + window.paint_strikethrough( + *strikethrough_origin, + glyph_origin.x - strikethrough_origin.x, + strikethrough_style, + ); + strikethrough_origin.x = origin.x; + strikethrough_origin.y += line_height; + } + + glyph_origin.x = aligned_origin_x( + origin, + align_width.unwrap_or(layout.width), + glyph.position.x, + &align, + layout, + wraps.peek(), + ); + glyph_origin.y += line_height; + } + prev_glyph_position = glyph.position; + + let mut finished_underline: Option<(Point, UnderlineStyle)> = None; + let mut finished_strikethrough: Option<(Point, StrikethroughStyle)> = None; + if glyph.index >= run_end { + let mut style_run = decoration_runs.next(); + + // ignore style runs that apply to a partial glyph + while let Some(run) = style_run { + if glyph.index < run_end + (run.len as usize) { + break; + } + run_end += run.len as usize; + style_run = decoration_runs.next(); + } + + if let Some(style_run) = style_run { + if let Some((_, underline_style)) = &mut current_underline + && style_run.underline.as_ref() != Some(underline_style) + { + finished_underline = current_underline.take(); + } + if let Some(run_underline) = style_run.underline.as_ref() { + current_underline.get_or_insert(( + point( + glyph_origin.x, + glyph_origin.y + baseline_offset.y + (layout.descent * 0.618), + ), + UnderlineStyle { + color: Some(run_underline.color.unwrap_or(style_run.color)), + thickness: run_underline.thickness, + wavy: run_underline.wavy, + }, + )); + } + if let Some((_, strikethrough_style)) = &mut current_strikethrough + && style_run.strikethrough.as_ref() != Some(strikethrough_style) + { + finished_strikethrough = current_strikethrough.take(); + } + if let Some(run_strikethrough) = style_run.strikethrough.as_ref() { + current_strikethrough.get_or_insert(( + point( + glyph_origin.x, + glyph_origin.y + + (((layout.ascent * 0.5) + baseline_offset.y) * 0.5), + ), + StrikethroughStyle { + color: Some(run_strikethrough.color.unwrap_or(style_run.color)), + thickness: run_strikethrough.thickness, + }, + )); + } + + run_end += style_run.len as usize; + color = style_run.color; + } else { + run_end = layout.len; + finished_underline = current_underline.take(); + finished_strikethrough = current_strikethrough.take(); + } + } + + if let Some((mut underline_origin, underline_style)) = finished_underline { + if underline_origin.x == glyph_origin.x { + underline_origin.x -= max_glyph_size.width.half(); + }; + window.paint_underline( + underline_origin, + glyph_origin.x - underline_origin.x, + &underline_style, + ); + } + + if let Some((mut strikethrough_origin, strikethrough_style)) = + finished_strikethrough + { + if strikethrough_origin.x == glyph_origin.x { + strikethrough_origin.x -= max_glyph_size.width.half(); + }; + window.paint_strikethrough( + strikethrough_origin, + glyph_origin.x - strikethrough_origin.x, + &strikethrough_style, + ); + } + + let max_glyph_bounds = Bounds { + origin: glyph_origin, + size: max_glyph_size, + }; + + let content_mask = window.content_mask(); + if max_glyph_bounds.intersects(&content_mask.bounds) { + if glyph.is_emoji { + window.paint_emoji( + glyph_origin + baseline_offset, + run.font_id, + glyph.id, + layout.font_size, + )?; + } else { + window.paint_glyph( + glyph_origin + baseline_offset, + run.font_id, + glyph.id, + layout.font_size, + color, + )?; + } + } + } + } + + let mut last_line_end_x = first_glyph_x + layout.width; + if let Some(boundary) = wrap_boundaries.last() { + let run = &layout.runs[boundary.run_ix]; + let glyph = &run.glyphs[boundary.glyph_ix]; + last_line_end_x -= glyph.position.x; + } + + if let Some((mut underline_start, underline_style)) = current_underline.take() { + if last_line_end_x == underline_start.x { + underline_start.x -= max_glyph_size.width.half() + }; + window.paint_underline( + underline_start, + last_line_end_x - underline_start.x, + &underline_style, + ); + } + + if let Some((mut strikethrough_start, strikethrough_style)) = current_strikethrough.take() { + if last_line_end_x == strikethrough_start.x { + strikethrough_start.x -= max_glyph_size.width.half() + }; + window.paint_strikethrough( + strikethrough_start, + last_line_end_x - strikethrough_start.x, + &strikethrough_style, + ); + } + + Ok(()) + }) +} + +fn paint_line_background( + origin: Point, + layout: &LineLayout, + line_height: Pixels, + align: TextAlign, + align_width: Option, + decoration_runs: &[DecorationRun], + wrap_boundaries: &[WrapBoundary], + window: &mut Window, + cx: &mut App, +) -> Result<()> { + let line_bounds = Bounds::new( + origin, + size( + layout.width, + line_height * (wrap_boundaries.len() as f32 + 1.), + ), + ); + window.paint_layer(line_bounds, |window| { + let mut decoration_runs = decoration_runs.iter(); + let mut wraps = wrap_boundaries.iter().peekable(); + let mut run_end = 0; + let mut current_background: Option<(Point, Hsla)> = None; + let text_system = cx.text_system().clone(); + let mut glyph_origin = point( + aligned_origin_x( + origin, + align_width.unwrap_or(layout.width), + px(0.0), + &align, + layout, + wraps.peek(), + ), + origin.y, + ); + let mut prev_glyph_position = Point::default(); + let mut max_glyph_size = size(px(0.), px(0.)); + for (run_ix, run) in layout.runs.iter().enumerate() { + max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size; + + for (glyph_ix, glyph) in run.glyphs.iter().enumerate() { + glyph_origin.x += glyph.position.x - prev_glyph_position.x; + + if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) { + wraps.next(); + if let Some((background_origin, background_color)) = current_background.as_mut() + { + if glyph_origin.x == background_origin.x { + background_origin.x -= max_glyph_size.width.half() + } + window.paint_quad(fill( + Bounds { + origin: *background_origin, + size: size(glyph_origin.x - background_origin.x, line_height), + }, + *background_color, + )); + background_origin.x = origin.x; + background_origin.y += line_height; + } + + glyph_origin.x = aligned_origin_x( + origin, + align_width.unwrap_or(layout.width), + glyph.position.x, + &align, + layout, + wraps.peek(), + ); + glyph_origin.y += line_height; + } + prev_glyph_position = glyph.position; + + let mut finished_background: Option<(Point, Hsla)> = None; + if glyph.index >= run_end { + let mut style_run = decoration_runs.next(); + + // ignore style runs that apply to a partial glyph + while let Some(run) = style_run { + if glyph.index < run_end + (run.len as usize) { + break; + } + run_end += run.len as usize; + style_run = decoration_runs.next(); + } + + if let Some(style_run) = style_run { + if let Some((_, background_color)) = &mut current_background + && style_run.background_color.as_ref() != Some(background_color) + { + finished_background = current_background.take(); + } + if let Some(run_background) = style_run.background_color { + current_background.get_or_insert(( + point(glyph_origin.x, glyph_origin.y), + run_background, + )); + } + run_end += style_run.len as usize; + } else { + run_end = layout.len; + finished_background = current_background.take(); + } + } + + if let Some((mut background_origin, background_color)) = finished_background { + let mut width = glyph_origin.x - background_origin.x; + if background_origin.x == glyph_origin.x { + background_origin.x -= max_glyph_size.width.half(); + }; + window.paint_quad(fill( + Bounds { + origin: background_origin, + size: size(width, line_height), + }, + background_color, + )); + } + } + } + + let mut last_line_end_x = origin.x + layout.width; + if let Some(boundary) = wrap_boundaries.last() { + let run = &layout.runs[boundary.run_ix]; + let glyph = &run.glyphs[boundary.glyph_ix]; + last_line_end_x -= glyph.position.x; + } + + if let Some((mut background_origin, background_color)) = current_background.take() { + if last_line_end_x == background_origin.x { + background_origin.x -= max_glyph_size.width.half() + }; + window.paint_quad(fill( + Bounds { + origin: background_origin, + size: size(last_line_end_x - background_origin.x, line_height), + }, + background_color, + )); + } + + Ok(()) + }) +} + +fn aligned_origin_x( + origin: Point, + align_width: Pixels, + last_glyph_x: Pixels, + align: &TextAlign, + layout: &LineLayout, + wrap_boundary: Option<&&WrapBoundary>, +) -> Pixels { + let end_of_line = if let Some(WrapBoundary { run_ix, glyph_ix }) = wrap_boundary { + layout.runs[*run_ix].glyphs[*glyph_ix].position.x + } else { + layout.width + }; + + let line_width = end_of_line - last_glyph_x; + + match align { + TextAlign::Left => origin.x, + TextAlign::Center => (origin.x * 2.0 + align_width - line_width) / 2.0, + TextAlign::Right => origin.x + align_width - line_width, + } +} diff --git a/third_party/gpui/src/text_system/line_layout.rs b/third_party/gpui/src/text_system/line_layout.rs new file mode 100644 index 0000000..375a9bd --- /dev/null +++ b/third_party/gpui/src/text_system/line_layout.rs @@ -0,0 +1,672 @@ +use crate::{FontId, GlyphId, Pixels, PlatformTextSystem, Point, SharedString, Size, point, px}; +use collections::FxHashMap; +use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; +use smallvec::SmallVec; +use std::{ + borrow::Borrow, + hash::{Hash, Hasher}, + ops::Range, + sync::Arc, +}; + +use super::LineWrapper; + +/// A laid out and styled line of text +#[derive(Default, Debug)] +pub struct LineLayout { + /// The font size for this line + pub font_size: Pixels, + /// The width of the line + pub width: Pixels, + /// The ascent of the line + pub ascent: Pixels, + /// The descent of the line + pub descent: Pixels, + /// The shaped runs that make up this line + pub runs: Vec, + /// The length of the line in utf-8 bytes + pub len: usize, +} + +/// A run of text that has been shaped . +#[derive(Debug, Clone)] +pub struct ShapedRun { + /// The font id for this run + pub font_id: FontId, + /// The glyphs that make up this run + pub glyphs: Vec, +} + +/// A single glyph, ready to paint. +#[derive(Clone, Debug)] +pub struct ShapedGlyph { + /// The ID for this glyph, as determined by the text system. + pub id: GlyphId, + + /// The position of this glyph in its containing line. + pub position: Point, + + /// The index of this glyph in the original text. + pub index: usize, + + /// Whether this glyph is an emoji + pub is_emoji: bool, +} + +impl LineLayout { + /// The index for the character at the given x coordinate + pub fn index_for_x(&self, x: Pixels) -> Option { + if x >= self.width { + None + } else { + for run in self.runs.iter().rev() { + for glyph in run.glyphs.iter().rev() { + if glyph.position.x <= x { + return Some(glyph.index); + } + } + } + Some(0) + } + } + + /// closest_index_for_x returns the character boundary closest to the given x coordinate + /// (e.g. to handle aligning up/down arrow keys) + pub fn closest_index_for_x(&self, x: Pixels) -> usize { + let mut prev_index = 0; + let mut prev_x = px(0.); + + for run in self.runs.iter() { + for glyph in run.glyphs.iter() { + if glyph.position.x >= x { + if glyph.position.x - x < x - prev_x { + return glyph.index; + } else { + return prev_index; + } + } + prev_index = glyph.index; + prev_x = glyph.position.x; + } + } + + if self.len == 1 { + if x > self.width / 2. { + return 1; + } else { + return 0; + } + } + + self.len + } + + /// The x position of the character at the given index + pub fn x_for_index(&self, index: usize) -> Pixels { + for run in &self.runs { + for glyph in &run.glyphs { + if glyph.index >= index { + return glyph.position.x; + } + } + } + self.width + } + + /// The corresponding Font at the given index + pub fn font_id_for_index(&self, index: usize) -> Option { + for run in &self.runs { + for glyph in &run.glyphs { + if glyph.index >= index { + return Some(run.font_id); + } + } + } + None + } + + fn compute_wrap_boundaries( + &self, + text: &str, + wrap_width: Pixels, + max_lines: Option, + ) -> SmallVec<[WrapBoundary; 1]> { + let mut boundaries = SmallVec::new(); + let mut first_non_whitespace_ix = None; + let mut last_candidate_ix = None; + let mut last_candidate_x = px(0.); + let mut last_boundary = WrapBoundary { + run_ix: 0, + glyph_ix: 0, + }; + let mut last_boundary_x = px(0.); + let mut prev_ch = '\0'; + let mut glyphs = self + .runs + .iter() + .enumerate() + .flat_map(move |(run_ix, run)| { + run.glyphs.iter().enumerate().map(move |(glyph_ix, glyph)| { + let character = text[glyph.index..].chars().next().unwrap(); + ( + WrapBoundary { run_ix, glyph_ix }, + character, + glyph.position.x, + ) + }) + }) + .peekable(); + + while let Some((boundary, ch, x)) = glyphs.next() { + if ch == '\n' { + continue; + } + + // Here is very similar to `LineWrapper::wrap_line` to determine text wrapping, + // but there are some differences, so we have to duplicate the code here. + if LineWrapper::is_word_char(ch) { + if prev_ch == ' ' && ch != ' ' && first_non_whitespace_ix.is_some() { + last_candidate_ix = Some(boundary); + last_candidate_x = x; + } + } else { + if ch != ' ' && first_non_whitespace_ix.is_some() { + last_candidate_ix = Some(boundary); + last_candidate_x = x; + } + } + + if ch != ' ' && first_non_whitespace_ix.is_none() { + first_non_whitespace_ix = Some(boundary); + } + + let next_x = glyphs.peek().map_or(self.width, |(_, _, x)| *x); + let width = next_x - last_boundary_x; + + if width > wrap_width && boundary > last_boundary { + // When used line_clamp, we should limit the number of lines. + if let Some(max_lines) = max_lines + && boundaries.len() >= max_lines - 1 + { + break; + } + + if let Some(last_candidate_ix) = last_candidate_ix.take() { + last_boundary = last_candidate_ix; + last_boundary_x = last_candidate_x; + } else { + last_boundary = boundary; + last_boundary_x = x; + } + boundaries.push(last_boundary); + } + prev_ch = ch; + } + + boundaries + } +} + +/// A line of text that has been wrapped to fit a given width +#[derive(Default, Debug)] +pub struct WrappedLineLayout { + /// The line layout, pre-wrapping. + pub unwrapped_layout: Arc, + + /// The boundaries at which the line was wrapped + pub wrap_boundaries: SmallVec<[WrapBoundary; 1]>, + + /// The width of the line, if it was wrapped + pub wrap_width: Option, +} + +/// A boundary at which a line was wrapped +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct WrapBoundary { + /// The index in the run just before the line was wrapped + pub run_ix: usize, + /// The index of the glyph just before the line was wrapped + pub glyph_ix: usize, +} + +impl WrappedLineLayout { + /// The length of the underlying text, in utf8 bytes. + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + self.unwrapped_layout.len + } + + /// The width of this line, in pixels, whether or not it was wrapped. + pub fn width(&self) -> Pixels { + self.wrap_width + .unwrap_or(Pixels::MAX) + .min(self.unwrapped_layout.width) + } + + /// The size of the whole wrapped text, for the given line_height. + /// can span multiple lines if there are multiple wrap boundaries. + pub fn size(&self, line_height: Pixels) -> Size { + Size { + width: self.width(), + height: line_height * (self.wrap_boundaries.len() + 1), + } + } + + /// The ascent of a line in this layout + pub fn ascent(&self) -> Pixels { + self.unwrapped_layout.ascent + } + + /// The descent of a line in this layout + pub fn descent(&self) -> Pixels { + self.unwrapped_layout.descent + } + + /// The wrap boundaries in this layout + pub fn wrap_boundaries(&self) -> &[WrapBoundary] { + &self.wrap_boundaries + } + + /// The font size of this layout + pub fn font_size(&self) -> Pixels { + self.unwrapped_layout.font_size + } + + /// The runs in this layout, sans wrapping + pub fn runs(&self) -> &[ShapedRun] { + &self.unwrapped_layout.runs + } + + /// The index corresponding to a given position in this layout for the given line height. + /// + /// See also [`Self::closest_index_for_position`]. + pub fn index_for_position( + &self, + position: Point, + line_height: Pixels, + ) -> Result { + self._index_for_position(position, line_height, false) + } + + /// The closest index to a given position in this layout for the given line height. + /// + /// Closest means the character boundary closest to the given position. + /// + /// See also [`LineLayout::closest_index_for_x`]. + pub fn closest_index_for_position( + &self, + position: Point, + line_height: Pixels, + ) -> Result { + self._index_for_position(position, line_height, true) + } + + fn _index_for_position( + &self, + mut position: Point, + line_height: Pixels, + closest: bool, + ) -> Result { + let wrapped_line_ix = (position.y / line_height) as usize; + + let wrapped_line_start_index; + let wrapped_line_start_x; + if wrapped_line_ix > 0 { + let Some(line_start_boundary) = self.wrap_boundaries.get(wrapped_line_ix - 1) else { + return Err(0); + }; + let run = &self.unwrapped_layout.runs[line_start_boundary.run_ix]; + let glyph = &run.glyphs[line_start_boundary.glyph_ix]; + wrapped_line_start_index = glyph.index; + wrapped_line_start_x = glyph.position.x; + } else { + wrapped_line_start_index = 0; + wrapped_line_start_x = Pixels::ZERO; + }; + + let wrapped_line_end_index; + let wrapped_line_end_x; + if wrapped_line_ix < self.wrap_boundaries.len() { + let next_wrap_boundary_ix = wrapped_line_ix; + let next_wrap_boundary = self.wrap_boundaries[next_wrap_boundary_ix]; + let run = &self.unwrapped_layout.runs[next_wrap_boundary.run_ix]; + let glyph = &run.glyphs[next_wrap_boundary.glyph_ix]; + wrapped_line_end_index = glyph.index; + wrapped_line_end_x = glyph.position.x; + } else { + wrapped_line_end_index = self.unwrapped_layout.len; + wrapped_line_end_x = self.unwrapped_layout.width; + }; + + let mut position_in_unwrapped_line = position; + position_in_unwrapped_line.x += wrapped_line_start_x; + if position_in_unwrapped_line.x < wrapped_line_start_x { + Err(wrapped_line_start_index) + } else if position_in_unwrapped_line.x >= wrapped_line_end_x { + Err(wrapped_line_end_index) + } else { + if closest { + Ok(self + .unwrapped_layout + .closest_index_for_x(position_in_unwrapped_line.x)) + } else { + Ok(self + .unwrapped_layout + .index_for_x(position_in_unwrapped_line.x) + .unwrap()) + } + } + } + + /// Returns the pixel position for the given byte index. + pub fn position_for_index(&self, index: usize, line_height: Pixels) -> Option> { + let mut line_start_ix = 0; + let mut line_end_indices = self + .wrap_boundaries + .iter() + .map(|wrap_boundary| { + let run = &self.unwrapped_layout.runs[wrap_boundary.run_ix]; + let glyph = &run.glyphs[wrap_boundary.glyph_ix]; + glyph.index + }) + .chain([self.len()]) + .enumerate(); + for (ix, line_end_ix) in line_end_indices { + let line_y = ix as f32 * line_height; + if index < line_start_ix { + break; + } else if index > line_end_ix { + line_start_ix = line_end_ix; + continue; + } else { + let line_start_x = self.unwrapped_layout.x_for_index(line_start_ix); + let x = self.unwrapped_layout.x_for_index(index) - line_start_x; + return Some(point(x, line_y)); + } + } + + None + } +} + +pub(crate) struct LineLayoutCache { + previous_frame: Mutex, + current_frame: RwLock, + platform_text_system: Arc, +} + +#[derive(Default)] +struct FrameCache { + lines: FxHashMap, Arc>, + wrapped_lines: FxHashMap, Arc>, + used_lines: Vec>, + used_wrapped_lines: Vec>, +} + +#[derive(Clone, Default)] +pub(crate) struct LineLayoutIndex { + lines_index: usize, + wrapped_lines_index: usize, +} + +impl LineLayoutCache { + pub fn new(platform_text_system: Arc) -> Self { + Self { + previous_frame: Mutex::default(), + current_frame: RwLock::default(), + platform_text_system, + } + } + + pub fn layout_index(&self) -> LineLayoutIndex { + let frame = self.current_frame.read(); + LineLayoutIndex { + lines_index: frame.used_lines.len(), + wrapped_lines_index: frame.used_wrapped_lines.len(), + } + } + + pub fn reuse_layouts(&self, range: Range) { + let mut previous_frame = &mut *self.previous_frame.lock(); + let mut current_frame = &mut *self.current_frame.write(); + + for key in &previous_frame.used_lines[range.start.lines_index..range.end.lines_index] { + if let Some((key, line)) = previous_frame.lines.remove_entry(key) { + current_frame.lines.insert(key, line); + } + current_frame.used_lines.push(key.clone()); + } + + for key in &previous_frame.used_wrapped_lines + [range.start.wrapped_lines_index..range.end.wrapped_lines_index] + { + if let Some((key, line)) = previous_frame.wrapped_lines.remove_entry(key) { + current_frame.wrapped_lines.insert(key, line); + } + current_frame.used_wrapped_lines.push(key.clone()); + } + } + + pub fn truncate_layouts(&self, index: LineLayoutIndex) { + let mut current_frame = &mut *self.current_frame.write(); + current_frame.used_lines.truncate(index.lines_index); + current_frame + .used_wrapped_lines + .truncate(index.wrapped_lines_index); + } + + pub fn finish_frame(&self) { + let mut prev_frame = self.previous_frame.lock(); + let mut curr_frame = self.current_frame.write(); + std::mem::swap(&mut *prev_frame, &mut *curr_frame); + curr_frame.lines.clear(); + curr_frame.wrapped_lines.clear(); + curr_frame.used_lines.clear(); + curr_frame.used_wrapped_lines.clear(); + } + + pub fn layout_wrapped_line( + &self, + text: Text, + font_size: Pixels, + runs: &[FontRun], + wrap_width: Option, + max_lines: Option, + ) -> Arc + where + Text: AsRef, + SharedString: From, + { + let key = &CacheKeyRef { + text: text.as_ref(), + font_size, + runs, + wrap_width, + force_width: None, + } as &dyn AsCacheKeyRef; + + let current_frame = self.current_frame.upgradable_read(); + if let Some(layout) = current_frame.wrapped_lines.get(key) { + return layout.clone(); + } + + let previous_frame_entry = self.previous_frame.lock().wrapped_lines.remove_entry(key); + if let Some((key, layout)) = previous_frame_entry { + let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame); + current_frame + .wrapped_lines + .insert(key.clone(), layout.clone()); + current_frame.used_wrapped_lines.push(key); + layout + } else { + drop(current_frame); + let text = SharedString::from(text); + let unwrapped_layout = self.layout_line::<&SharedString>(&text, font_size, runs, None); + let wrap_boundaries = if let Some(wrap_width) = wrap_width { + unwrapped_layout.compute_wrap_boundaries(text.as_ref(), wrap_width, max_lines) + } else { + SmallVec::new() + }; + let layout = Arc::new(WrappedLineLayout { + unwrapped_layout, + wrap_boundaries, + wrap_width, + }); + let key = Arc::new(CacheKey { + text, + font_size, + runs: SmallVec::from(runs), + wrap_width, + force_width: None, + }); + + let mut current_frame = self.current_frame.write(); + current_frame + .wrapped_lines + .insert(key.clone(), layout.clone()); + current_frame.used_wrapped_lines.push(key); + + layout + } + } + + pub fn layout_line( + &self, + text: Text, + font_size: Pixels, + runs: &[FontRun], + force_width: Option, + ) -> Arc + where + Text: AsRef, + SharedString: From, + { + let key = &CacheKeyRef { + text: text.as_ref(), + font_size, + runs, + wrap_width: None, + force_width, + } as &dyn AsCacheKeyRef; + + let current_frame = self.current_frame.upgradable_read(); + if let Some(layout) = current_frame.lines.get(key) { + return layout.clone(); + } + + let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame); + if let Some((key, layout)) = self.previous_frame.lock().lines.remove_entry(key) { + current_frame.lines.insert(key.clone(), layout.clone()); + current_frame.used_lines.push(key); + layout + } else { + let text = SharedString::from(text); + let mut layout = self + .platform_text_system + .layout_line(&text, font_size, runs); + + if let Some(force_width) = force_width { + let mut glyph_pos = 0; + for run in layout.runs.iter_mut() { + for glyph in run.glyphs.iter_mut() { + if (glyph.position.x - glyph_pos * force_width).abs() > px(1.) { + glyph.position.x = glyph_pos * force_width; + } + glyph_pos += 1; + } + } + } + + let key = Arc::new(CacheKey { + text, + font_size, + runs: SmallVec::from(runs), + wrap_width: None, + force_width, + }); + let layout = Arc::new(layout); + current_frame.lines.insert(key.clone(), layout.clone()); + current_frame.used_lines.push(key); + layout + } + } +} + +/// A run of text with a single font. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct FontRun { + pub(crate) len: usize, + pub(crate) font_id: FontId, +} + +trait AsCacheKeyRef { + fn as_cache_key_ref(&self) -> CacheKeyRef<'_>; +} + +#[derive(Clone, Debug, Eq)] +struct CacheKey { + text: SharedString, + font_size: Pixels, + runs: SmallVec<[FontRun; 1]>, + wrap_width: Option, + force_width: Option, +} + +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +struct CacheKeyRef<'a> { + text: &'a str, + font_size: Pixels, + runs: &'a [FontRun], + wrap_width: Option, + force_width: Option, +} + +impl PartialEq for dyn AsCacheKeyRef + '_ { + fn eq(&self, other: &dyn AsCacheKeyRef) -> bool { + self.as_cache_key_ref() == other.as_cache_key_ref() + } +} + +impl Eq for dyn AsCacheKeyRef + '_ {} + +impl Hash for dyn AsCacheKeyRef + '_ { + fn hash(&self, state: &mut H) { + self.as_cache_key_ref().hash(state) + } +} + +impl AsCacheKeyRef for CacheKey { + fn as_cache_key_ref(&self) -> CacheKeyRef<'_> { + CacheKeyRef { + text: &self.text, + font_size: self.font_size, + runs: self.runs.as_slice(), + wrap_width: self.wrap_width, + force_width: self.force_width, + } + } +} + +impl PartialEq for CacheKey { + fn eq(&self, other: &Self) -> bool { + self.as_cache_key_ref().eq(&other.as_cache_key_ref()) + } +} + +impl Hash for CacheKey { + fn hash(&self, state: &mut H) { + self.as_cache_key_ref().hash(state); + } +} + +impl<'a> Borrow for Arc { + fn borrow(&self) -> &(dyn AsCacheKeyRef + 'a) { + self.as_ref() as &dyn AsCacheKeyRef + } +} + +impl AsCacheKeyRef for CacheKeyRef<'_> { + fn as_cache_key_ref(&self) -> CacheKeyRef<'_> { + *self + } +} diff --git a/third_party/gpui/src/text_system/line_wrapper.rs b/third_party/gpui/src/text_system/line_wrapper.rs new file mode 100644 index 0000000..55599cc --- /dev/null +++ b/third_party/gpui/src/text_system/line_wrapper.rs @@ -0,0 +1,743 @@ +use crate::{FontId, FontRun, Pixels, PlatformTextSystem, SharedString, TextRun, px}; +use collections::HashMap; +use std::{iter, sync::Arc}; + +/// The GPUI line wrapper, used to wrap lines of text to a given width. +pub struct LineWrapper { + platform_text_system: Arc, + pub(crate) font_id: FontId, + pub(crate) font_size: Pixels, + cached_ascii_char_widths: [Option; 128], + cached_other_char_widths: HashMap, +} + +impl LineWrapper { + /// The maximum indent that can be applied to a line. + pub const MAX_INDENT: u32 = 256; + + pub(crate) fn new( + font_id: FontId, + font_size: Pixels, + text_system: Arc, + ) -> Self { + Self { + platform_text_system: text_system, + font_id, + font_size, + cached_ascii_char_widths: [None; 128], + cached_other_char_widths: HashMap::default(), + } + } + + /// Wrap a line of text to the given width with this wrapper's font and font size. + pub fn wrap_line<'a>( + &'a mut self, + fragments: &'a [LineFragment], + wrap_width: Pixels, + ) -> impl Iterator + 'a { + let mut width = px(0.); + let mut first_non_whitespace_ix = None; + let mut indent = None; + let mut last_candidate_ix = 0; + let mut last_candidate_width = px(0.); + let mut last_wrap_ix = 0; + let mut prev_c = '\0'; + let mut index = 0; + let mut candidates = fragments + .iter() + .flat_map(move |fragment| fragment.wrap_boundary_candidates()) + .peekable(); + iter::from_fn(move || { + for candidate in candidates.by_ref() { + let ix = index; + index += candidate.len_utf8(); + let mut new_prev_c = prev_c; + let item_width = match candidate { + WrapBoundaryCandidate::Char { character: c } => { + if c == '\n' { + continue; + } + + if Self::is_word_char(c) { + if prev_c == ' ' && c != ' ' && first_non_whitespace_ix.is_some() { + last_candidate_ix = ix; + last_candidate_width = width; + } + } else { + // CJK may not be space separated, e.g.: `Hello world你好世界` + if c != ' ' && first_non_whitespace_ix.is_some() { + last_candidate_ix = ix; + last_candidate_width = width; + } + } + + if c != ' ' && first_non_whitespace_ix.is_none() { + first_non_whitespace_ix = Some(ix); + } + + new_prev_c = c; + + self.width_for_char(c) + } + WrapBoundaryCandidate::Element { + width: element_width, + .. + } => { + if prev_c == ' ' && first_non_whitespace_ix.is_some() { + last_candidate_ix = ix; + last_candidate_width = width; + } + + if first_non_whitespace_ix.is_none() { + first_non_whitespace_ix = Some(ix); + } + + element_width + } + }; + + width += item_width; + if width > wrap_width && ix > last_wrap_ix { + if let (None, Some(first_non_whitespace_ix)) = (indent, first_non_whitespace_ix) + { + indent = Some( + Self::MAX_INDENT.min((first_non_whitespace_ix - last_wrap_ix) as u32), + ); + } + + if last_candidate_ix > 0 { + last_wrap_ix = last_candidate_ix; + width -= last_candidate_width; + last_candidate_ix = 0; + } else { + last_wrap_ix = ix; + width = item_width; + } + + if let Some(indent) = indent { + width += self.width_for_char(' ') * indent as f32; + } + + return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0))); + } + + prev_c = new_prev_c; + } + + None + }) + } + + /// Truncate a line of text to the given width with this wrapper's font and font size. + pub fn truncate_line( + &mut self, + line: SharedString, + truncate_width: Pixels, + truncation_suffix: &str, + runs: &mut Vec, + ) -> SharedString { + let mut width = px(0.); + let mut suffix_width = truncation_suffix + .chars() + .map(|c| self.width_for_char(c)) + .fold(px(0.0), |a, x| a + x); + let mut char_indices = line.char_indices(); + let mut truncate_ix = 0; + for (ix, c) in char_indices { + if width + suffix_width < truncate_width { + truncate_ix = ix; + } + + let char_width = self.width_for_char(c); + width += char_width; + + if width.floor() > truncate_width { + let result = + SharedString::from(format!("{}{}", &line[..truncate_ix], truncation_suffix)); + update_runs_after_truncation(&result, truncation_suffix, runs); + + return result; + } + } + + line + } + + /// Any character in this list should be treated as a word character, + /// meaning it can be part of a word that should not be wrapped. + pub(crate) fn is_word_char(c: char) -> bool { + // ASCII alphanumeric characters, for English, numbers: `Hello123`, etc. + c.is_ascii_alphanumeric() || + // Latin script in Unicode for French, German, Spanish, etc. + // Latin-1 Supplement + // https://en.wikipedia.org/wiki/Latin-1_Supplement + matches!(c, '\u{00C0}'..='\u{00FF}') || + // Latin Extended-A + // https://en.wikipedia.org/wiki/Latin_Extended-A + matches!(c, '\u{0100}'..='\u{017F}') || + // Latin Extended-B + // https://en.wikipedia.org/wiki/Latin_Extended-B + matches!(c, '\u{0180}'..='\u{024F}') || + // Cyrillic for Russian, Ukrainian, etc. + // https://en.wikipedia.org/wiki/Cyrillic_script_in_Unicode + matches!(c, '\u{0400}'..='\u{04FF}') || + // Some other known special characters that should be treated as word characters, + // e.g. `a-b`, `var_name`, `I'm`, '@mention`, `#hashtag`, `100%`, `3.1415`, + // `2^3`, `a~b`, `a=1`, `Self::new`, etc. + matches!(c, '-' | '_' | '.' | '\'' | '$' | '%' | '@' | '#' | '^' | '~' | ',' | '=' | ':') || + // `⋯` character is special used in Zed, to keep this at the end of the line. + matches!(c, '⋯') + } + + #[inline(always)] + fn width_for_char(&mut self, c: char) -> Pixels { + if (c as u32) < 128 { + if let Some(cached_width) = self.cached_ascii_char_widths[c as usize] { + cached_width + } else { + let width = self.compute_width_for_char(c); + self.cached_ascii_char_widths[c as usize] = Some(width); + width + } + } else if let Some(cached_width) = self.cached_other_char_widths.get(&c) { + *cached_width + } else { + let width = self.compute_width_for_char(c); + self.cached_other_char_widths.insert(c, width); + width + } + } + + fn compute_width_for_char(&self, c: char) -> Pixels { + let mut buffer = [0; 4]; + let buffer = c.encode_utf8(&mut buffer); + self.platform_text_system + .layout_line( + buffer, + self.font_size, + &[FontRun { + len: buffer.len(), + font_id: self.font_id, + }], + ) + .width + } +} + +fn update_runs_after_truncation(result: &str, ellipsis: &str, runs: &mut Vec) { + let mut truncate_at = result.len() - ellipsis.len(); + for (run_index, run) in runs.iter_mut().enumerate() { + if run.len <= truncate_at { + truncate_at -= run.len; + } else { + run.len = truncate_at + ellipsis.len(); + runs.truncate(run_index + 1); + break; + } + } +} + +/// A fragment of a line that can be wrapped. +pub enum LineFragment<'a> { + /// A text fragment consisting of characters. + Text { + /// The text content of the fragment. + text: &'a str, + }, + /// A non-text element with a fixed width. + Element { + /// The width of the element in pixels. + width: Pixels, + /// The UTF-8 encoded length of the element. + len_utf8: usize, + }, +} + +impl<'a> LineFragment<'a> { + /// Creates a new text fragment from the given text. + pub fn text(text: &'a str) -> Self { + LineFragment::Text { text } + } + + /// Creates a new non-text element with the given width and UTF-8 encoded length. + pub fn element(width: Pixels, len_utf8: usize) -> Self { + LineFragment::Element { width, len_utf8 } + } + + fn wrap_boundary_candidates(&self) -> impl Iterator { + let text = match self { + LineFragment::Text { text } => text, + LineFragment::Element { .. } => "\0", + }; + text.chars().map(move |character| { + if let LineFragment::Element { width, len_utf8 } = self { + WrapBoundaryCandidate::Element { + width: *width, + len_utf8: *len_utf8, + } + } else { + WrapBoundaryCandidate::Char { character } + } + }) + } +} + +enum WrapBoundaryCandidate { + Char { character: char }, + Element { width: Pixels, len_utf8: usize }, +} + +impl WrapBoundaryCandidate { + pub fn len_utf8(&self) -> usize { + match self { + WrapBoundaryCandidate::Char { character } => character.len_utf8(), + WrapBoundaryCandidate::Element { len_utf8: len, .. } => *len, + } + } +} + +/// A boundary between two lines of text. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Boundary { + /// The index of the last character in a line + pub ix: usize, + /// The indent of the next line. + pub next_indent: u32, +} + +impl Boundary { + fn new(ix: usize, next_indent: u32) -> Self { + Self { ix, next_indent } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + Font, FontFeatures, FontStyle, FontWeight, Hsla, TestAppContext, TestDispatcher, font, + }; + #[cfg(target_os = "macos")] + use crate::{TextRun, WindowTextSystem, WrapBoundary}; + use rand::prelude::*; + + fn build_wrapper() -> LineWrapper { + let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0)); + let cx = TestAppContext::build(dispatcher, None); + let id = cx.text_system().resolve_font(&font(".ZedMono")); + LineWrapper::new(id, px(16.), cx.text_system().platform_text_system.clone()) + } + + fn generate_test_runs(input_run_len: &[usize]) -> Vec { + input_run_len + .iter() + .map(|run_len| TextRun { + len: *run_len, + font: Font { + family: "Dummy".into(), + features: FontFeatures::default(), + fallbacks: None, + weight: FontWeight::default(), + style: FontStyle::Normal, + }, + color: Hsla::default(), + background_color: None, + underline: None, + strikethrough: None, + }) + .collect() + } + + #[test] + fn test_wrap_line() { + let mut wrapper = build_wrapper(); + + assert_eq!( + wrapper + .wrap_line(&[LineFragment::text("aa bbb cccc ddddd eeee")], px(72.)) + .collect::>(), + &[ + Boundary::new(7, 0), + Boundary::new(12, 0), + Boundary::new(18, 0) + ], + ); + assert_eq!( + wrapper + .wrap_line(&[LineFragment::text("aaa aaaaaaaaaaaaaaaaaa")], px(72.0)) + .collect::>(), + &[ + Boundary::new(4, 0), + Boundary::new(11, 0), + Boundary::new(18, 0) + ], + ); + assert_eq!( + wrapper + .wrap_line(&[LineFragment::text(" aaaaaaa")], px(72.)) + .collect::>(), + &[ + Boundary::new(7, 5), + Boundary::new(9, 5), + Boundary::new(11, 5), + ] + ); + assert_eq!( + wrapper + .wrap_line( + &[LineFragment::text(" ")], + px(72.) + ) + .collect::>(), + &[ + Boundary::new(7, 0), + Boundary::new(14, 0), + Boundary::new(21, 0) + ] + ); + assert_eq!( + wrapper + .wrap_line(&[LineFragment::text(" aaaaaaaaaaaaaa")], px(72.)) + .collect::>(), + &[ + Boundary::new(7, 0), + Boundary::new(14, 3), + Boundary::new(18, 3), + Boundary::new(22, 3), + ] + ); + + // Test wrapping multiple text fragments + assert_eq!( + wrapper + .wrap_line( + &[ + LineFragment::text("aa bbb "), + LineFragment::text("cccc ddddd eeee") + ], + px(72.) + ) + .collect::>(), + &[ + Boundary::new(7, 0), + Boundary::new(12, 0), + Boundary::new(18, 0) + ], + ); + + // Test wrapping with a mix of text and element fragments + assert_eq!( + wrapper + .wrap_line( + &[ + LineFragment::text("aa "), + LineFragment::element(px(20.), 1), + LineFragment::text(" bbb "), + LineFragment::element(px(30.), 1), + LineFragment::text(" cccc") + ], + px(72.) + ) + .collect::>(), + &[ + Boundary::new(5, 0), + Boundary::new(9, 0), + Boundary::new(11, 0) + ], + ); + + // Test with element at the beginning and text afterward + assert_eq!( + wrapper + .wrap_line( + &[ + LineFragment::element(px(50.), 1), + LineFragment::text(" aaaa bbbb cccc dddd") + ], + px(72.) + ) + .collect::>(), + &[ + Boundary::new(2, 0), + Boundary::new(7, 0), + Boundary::new(12, 0), + Boundary::new(17, 0) + ], + ); + + // Test with a large element that forces wrapping by itself + assert_eq!( + wrapper + .wrap_line( + &[ + LineFragment::text("short text "), + LineFragment::element(px(100.), 1), + LineFragment::text(" more text") + ], + px(72.) + ) + .collect::>(), + &[ + Boundary::new(6, 0), + Boundary::new(11, 0), + Boundary::new(12, 0), + Boundary::new(18, 0) + ], + ); + } + + #[test] + fn test_truncate_line() { + let mut wrapper = build_wrapper(); + + fn perform_test( + wrapper: &mut LineWrapper, + text: &'static str, + result: &'static str, + ellipsis: &str, + ) { + let dummy_run_lens = vec![text.len()]; + let mut dummy_runs = generate_test_runs(&dummy_run_lens); + assert_eq!( + wrapper.truncate_line(text.into(), px(220.), ellipsis, &mut dummy_runs), + result + ); + assert_eq!(dummy_runs.first().unwrap().len, result.len()); + } + + perform_test( + &mut wrapper, + "aa bbb cccc ddddd eeee ffff gggg", + "aa bbb cccc ddddd eeee", + "", + ); + perform_test( + &mut wrapper, + "aa bbb cccc ddddd eeee ffff gggg", + "aa bbb cccc ddddd eee…", + "…", + ); + perform_test( + &mut wrapper, + "aa bbb cccc ddddd eeee ffff gggg", + "aa bbb cccc dddd......", + "......", + ); + } + + #[test] + fn test_truncate_multiple_runs() { + let mut wrapper = build_wrapper(); + + fn perform_test( + wrapper: &mut LineWrapper, + text: &'static str, + result: &str, + run_lens: &[usize], + result_run_len: &[usize], + line_width: Pixels, + ) { + let mut dummy_runs = generate_test_runs(run_lens); + assert_eq!( + wrapper.truncate_line(text.into(), line_width, "…", &mut dummy_runs), + result + ); + for (run, result_len) in dummy_runs.iter().zip(result_run_len) { + assert_eq!(run.len, *result_len); + } + } + // Case 0: Normal + // Text: abcdefghijkl + // Runs: Run0 { len: 12, ... } + // + // Truncate res: abcd… (truncate_at = 4) + // Run res: Run0 { string: abcd…, len: 7, ... } + perform_test(&mut wrapper, "abcdefghijkl", "abcd…", &[12], &[7], px(50.)); + // Case 1: Drop some runs + // Text: abcdefghijkl + // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... } + // + // Truncate res: abcdef… (truncate_at = 6) + // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len: + // 5, ... } + perform_test( + &mut wrapper, + "abcdefghijkl", + "abcdef…", + &[4, 4, 4], + &[4, 5], + px(70.), + ); + // Case 2: Truncate at start of some run + // Text: abcdefghijkl + // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... } + // + // Truncate res: abcdefgh… (truncate_at = 8) + // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len: + // 4, ... }, Run2 { string: …, len: 3, ... } + perform_test( + &mut wrapper, + "abcdefghijkl", + "abcdefgh…", + &[4, 4, 4], + &[4, 4, 3], + px(90.), + ); + } + + #[test] + fn test_update_run_after_truncation() { + fn perform_test(result: &str, run_lens: &[usize], result_run_lens: &[usize]) { + let mut dummy_runs = generate_test_runs(run_lens); + update_runs_after_truncation(result, "…", &mut dummy_runs); + for (run, result_len) in dummy_runs.iter().zip(result_run_lens) { + assert_eq!(run.len, *result_len); + } + } + // Case 0: Normal + // Text: abcdefghijkl + // Runs: Run0 { len: 12, ... } + // + // Truncate res: abcd… (truncate_at = 4) + // Run res: Run0 { string: abcd…, len: 7, ... } + perform_test("abcd…", &[12], &[7]); + // Case 1: Drop some runs + // Text: abcdefghijkl + // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... } + // + // Truncate res: abcdef… (truncate_at = 6) + // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len: + // 5, ... } + perform_test("abcdef…", &[4, 4, 4], &[4, 5]); + // Case 2: Truncate at start of some run + // Text: abcdefghijkl + // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... } + // + // Truncate res: abcdefgh… (truncate_at = 8) + // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len: + // 4, ... }, Run2 { string: …, len: 3, ... } + perform_test("abcdefgh…", &[4, 4, 4], &[4, 4, 3]); + } + + #[test] + fn test_is_word_char() { + #[track_caller] + fn assert_word(word: &str) { + for c in word.chars() { + assert!(LineWrapper::is_word_char(c), "assertion failed for '{}'", c); + } + } + + #[track_caller] + fn assert_not_word(word: &str) { + let found = word.chars().any(|c| !LineWrapper::is_word_char(c)); + assert!(found, "assertion failed for '{}'", word); + } + + assert_word("Hello123"); + assert_word("non-English"); + assert_word("var_name"); + assert_word("123456"); + assert_word("3.1415"); + assert_word("10^2"); + assert_word("1~2"); + assert_word("100%"); + assert_word("@mention"); + assert_word("#hashtag"); + assert_word("$variable"); + assert_word("a=1"); + assert_word("Self::is_word_char"); + assert_word("more⋯"); + + // Space + assert_not_word("foo bar"); + + // URL case + assert_word("github.com"); + assert_not_word("zed-industries/zed"); + assert_not_word("zed-industries\\zed"); + assert_not_word("a=1&b=2"); + assert_not_word("foo?b=2"); + + // Latin-1 Supplement + assert_word("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏ"); + // Latin Extended-A + assert_word("ĀāĂ㥹ĆćĈĉĊċČčĎď"); + // Latin Extended-B + assert_word("ƀƁƂƃƄƅƆƇƈƉƊƋƌƍƎƏ"); + // Cyrillic + assert_word("АБВГДЕЖЗИЙКЛМНОП"); + + // non-word characters + assert_not_word("你好"); + assert_not_word("안녕하세요"); + assert_not_word("こんにちは"); + assert_not_word("😀😁😂"); + assert_not_word("()[]{}<>"); + } + + // For compatibility with the test macro + #[cfg(target_os = "macos")] + use crate as gpui; + + // These seem to vary wildly based on the text system. + #[cfg(target_os = "macos")] + #[crate::test] + fn test_wrap_shaped_line(cx: &mut TestAppContext) { + cx.update(|cx| { + let text_system = WindowTextSystem::new(cx.text_system().clone()); + + let normal = TextRun { + len: 0, + font: font("Helvetica"), + color: Default::default(), + underline: Default::default(), + strikethrough: None, + background_color: None, + }; + let bold = TextRun { + len: 0, + font: font("Helvetica").bold(), + color: Default::default(), + underline: Default::default(), + strikethrough: None, + background_color: None, + }; + + let text = "aa bbb cccc ddddd eeee".into(); + let lines = text_system + .shape_text( + text, + px(16.), + &[ + normal.with_len(4), + bold.with_len(5), + normal.with_len(6), + bold.with_len(1), + normal.with_len(7), + ], + Some(px(72.)), + None, + ) + .unwrap(); + + assert_eq!( + lines[0].layout.wrap_boundaries(), + &[ + WrapBoundary { + run_ix: 0, + glyph_ix: 7 + }, + WrapBoundary { + run_ix: 0, + glyph_ix: 12 + }, + WrapBoundary { + run_ix: 0, + glyph_ix: 18 + } + ], + ); + }); + } +} diff --git a/third_party/gpui/src/util.rs b/third_party/gpui/src/util.rs new file mode 100644 index 0000000..92c8681 --- /dev/null +++ b/third_party/gpui/src/util.rs @@ -0,0 +1,175 @@ +use crate::{BackgroundExecutor, Task}; +use std::{ + future::Future, + pin::Pin, + sync::atomic::{AtomicUsize, Ordering::SeqCst}, + task, + time::Duration, +}; + +pub use util::*; + +/// A helper trait for building complex objects with imperative conditionals in a fluent style. +pub trait FluentBuilder { + /// Imperatively modify self with the given closure. + fn map(self, f: impl FnOnce(Self) -> U) -> U + where + Self: Sized, + { + f(self) + } + + /// Conditionally modify self with the given closure. + fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self + where + Self: Sized, + { + self.map(|this| if condition { then(this) } else { this }) + } + + /// Conditionally modify self with the given closure. + fn when_else( + self, + condition: bool, + then: impl FnOnce(Self) -> Self, + else_fn: impl FnOnce(Self) -> Self, + ) -> Self + where + Self: Sized, + { + self.map(|this| if condition { then(this) } else { else_fn(this) }) + } + + /// Conditionally unwrap and modify self with the given closure, if the given option is Some. + fn when_some(self, option: Option, then: impl FnOnce(Self, T) -> Self) -> Self + where + Self: Sized, + { + self.map(|this| { + if let Some(value) = option { + then(this, value) + } else { + this + } + }) + } + /// Conditionally unwrap and modify self with the given closure, if the given option is None. + fn when_none(self, option: &Option, then: impl FnOnce(Self) -> Self) -> Self + where + Self: Sized, + { + self.map(|this| if option.is_some() { this } else { then(this) }) + } +} + +/// Extensions for Future types that provide additional combinators and utilities. +pub trait FutureExt { + /// Requires a Future to complete before the specified duration has elapsed. + /// Similar to tokio::timeout. + fn with_timeout(self, timeout: Duration, executor: &BackgroundExecutor) -> WithTimeout + where + Self: Sized; +} + +impl FutureExt for T { + fn with_timeout(self, timeout: Duration, executor: &BackgroundExecutor) -> WithTimeout + where + Self: Sized, + { + WithTimeout { + future: self, + timer: executor.timer(timeout), + } + } +} + +#[pin_project::pin_project] +pub struct WithTimeout { + #[pin] + future: T, + #[pin] + timer: Task<()>, +} + +#[derive(Debug, thiserror::Error)] +#[error("Timed out before future resolved")] +/// Error returned by with_timeout when the timeout duration elapsed before the future resolved +pub struct Timeout; + +impl Future for WithTimeout { + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut task::Context) -> task::Poll { + let this = self.project(); + + if let task::Poll::Ready(output) = this.future.poll(cx) { + task::Poll::Ready(Ok(output)) + } else if this.timer.poll(cx).is_ready() { + task::Poll::Ready(Err(Timeout)) + } else { + task::Poll::Pending + } + } +} + +#[cfg(any(test, feature = "test-support"))] +/// Uses smol executor to run a given future no longer than the timeout specified. +/// Note that this won't "rewind" on `cx.executor().advance_clock` call, truly waiting for the timeout to elapse. +pub async fn smol_timeout(timeout: Duration, f: F) -> Result +where + F: Future, +{ + let timer = async { + smol::Timer::after(timeout).await; + Err(()) + }; + let future = async move { Ok(f.await) }; + smol::future::FutureExt::race(timer, future).await +} + +/// Increment the given atomic counter if it is not zero. +/// Return the new value of the counter. +pub(crate) fn atomic_incr_if_not_zero(counter: &AtomicUsize) -> usize { + let mut loaded = counter.load(SeqCst); + loop { + if loaded == 0 { + return 0; + } + match counter.compare_exchange_weak(loaded, loaded + 1, SeqCst, SeqCst) { + Ok(x) => return x + 1, + Err(actual) => loaded = actual, + } + } +} + +#[cfg(test)] +mod tests { + use crate::TestAppContext; + + use super::*; + + #[gpui::test] + async fn test_with_timeout(cx: &mut TestAppContext) { + Task::ready(()) + .with_timeout(Duration::from_secs(1), &cx.executor()) + .await + .expect("Timeout should be noop"); + + let long_duration = Duration::from_secs(6000); + let short_duration = Duration::from_secs(1); + cx.executor() + .timer(long_duration) + .with_timeout(short_duration, &cx.executor()) + .await + .expect_err("timeout should have triggered"); + + let fut = cx + .executor() + .timer(long_duration) + .with_timeout(short_duration, &cx.executor()); + cx.executor().advance_clock(short_duration * 2); + futures::FutureExt::now_or_never(fut) + .unwrap_or_else(|| panic!("timeout should have triggered")) + .expect_err("timeout"); + } +} diff --git a/third_party/gpui/src/view.rs b/third_party/gpui/src/view.rs new file mode 100644 index 0000000..2179717 --- /dev/null +++ b/third_party/gpui/src/view.rs @@ -0,0 +1,373 @@ +use crate::{ + AnyElement, AnyEntity, AnyWeakEntity, App, Bounds, ContentMask, Context, Element, ElementId, + Entity, EntityId, GlobalElementId, InspectorElementId, IntoElement, LayoutId, PaintIndex, + Pixels, PrepaintStateIndex, Render, Style, StyleRefinement, TextStyle, WeakEntity, +}; +use crate::{Empty, Window}; +use anyhow::Result; +use collections::FxHashSet; +use refineable::Refineable; +use std::mem; +use std::rc::Rc; +use std::{any::TypeId, fmt, ops::Range}; + +struct AnyViewState { + prepaint_range: Range, + paint_range: Range, + cache_key: ViewCacheKey, + accessed_entities: FxHashSet, +} + +#[derive(Default)] +struct ViewCacheKey { + bounds: Bounds, + content_mask: ContentMask, + text_style: TextStyle, +} + +impl Element for Entity { + type RequestLayoutState = AnyElement; + type PrepaintState = (); + + fn id(&self) -> Option { + Some(ElementId::View(self.entity_id())) + } + + fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let mut element = self.update(cx, |view, cx| view.render(window, cx).into_any_element()); + let layout_id = window.with_rendered_view(self.entity_id(), |window| { + element.request_layout(window, cx) + }); + (layout_id, element) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _: Bounds, + element: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) { + window.set_view_id(self.entity_id()); + window.with_rendered_view(self.entity_id(), |window| element.prepaint(window, cx)); + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _: Bounds, + element: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + window.with_rendered_view(self.entity_id(), |window| element.paint(window, cx)); + } +} + +/// A dynamically-typed handle to a view, which can be downcast to a [Entity] for a specific type. +#[derive(Clone, Debug)] +pub struct AnyView { + entity: AnyEntity, + render: fn(&AnyView, &mut Window, &mut App) -> AnyElement, + cached_style: Option>, +} + +impl From> for AnyView { + fn from(value: Entity) -> Self { + AnyView { + entity: value.into_any(), + render: any_view::render::, + cached_style: None, + } + } +} + +impl AnyView { + /// Indicate that this view should be cached when using it as an element. + /// When using this method, the view's previous layout and paint will be recycled from the previous frame if [Context::notify] has not been called since it was rendered. + /// The one exception is when [Window::refresh] is called, in which case caching is ignored. + pub fn cached(mut self, style: StyleRefinement) -> Self { + self.cached_style = Some(style.into()); + self + } + + /// Convert this to a weak handle. + pub fn downgrade(&self) -> AnyWeakView { + AnyWeakView { + entity: self.entity.downgrade(), + render: self.render, + } + } + + /// Convert this to a [Entity] of a specific type. + /// If this handle does not contain a view of the specified type, returns itself in an `Err` variant. + pub fn downcast(self) -> Result, Self> { + match self.entity.downcast() { + Ok(entity) => Ok(entity), + Err(entity) => Err(Self { + entity, + render: self.render, + cached_style: self.cached_style, + }), + } + } + + /// Gets the [TypeId] of the underlying view. + pub fn entity_type(&self) -> TypeId { + self.entity.entity_type + } + + /// Gets the entity id of this handle. + pub fn entity_id(&self) -> EntityId { + self.entity.entity_id() + } +} + +impl PartialEq for AnyView { + fn eq(&self, other: &Self) -> bool { + self.entity == other.entity + } +} + +impl Eq for AnyView {} + +impl Element for AnyView { + type RequestLayoutState = Option; + type PrepaintState = Option; + + fn id(&self) -> Option { + Some(ElementId::View(self.entity_id())) + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + window.with_rendered_view(self.entity_id(), |window| { + // Disable caching when inspecting so that mouse_hit_test has all hitboxes. + let caching_disabled = window.is_inspector_picking(cx); + match self.cached_style.as_ref() { + Some(style) if !caching_disabled => { + let mut root_style = Style::default(); + root_style.refine(style); + let layout_id = window.request_layout(root_style, None, cx); + (layout_id, None) + } + _ => { + let mut element = (self.render)(self, window, cx); + let layout_id = element.request_layout(window, cx); + (layout_id, Some(element)) + } + } + }) + } + + fn prepaint( + &mut self, + global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + element: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Option { + window.set_view_id(self.entity_id()); + window.with_rendered_view(self.entity_id(), |window| { + if let Some(mut element) = element.take() { + element.prepaint(window, cx); + return Some(element); + } + + window.with_element_state::( + global_id.unwrap(), + |element_state, window| { + let content_mask = window.content_mask(); + let text_style = window.text_style(); + + if let Some(mut element_state) = element_state + && element_state.cache_key.bounds == bounds + && element_state.cache_key.content_mask == content_mask + && element_state.cache_key.text_style == text_style + && !window.dirty_views.contains(&self.entity_id()) + && !window.refreshing + { + let prepaint_start = window.prepaint_index(); + window.reuse_prepaint(element_state.prepaint_range.clone()); + cx.entities + .extend_accessed(&element_state.accessed_entities); + let prepaint_end = window.prepaint_index(); + element_state.prepaint_range = prepaint_start..prepaint_end; + + return (None, element_state); + } + + let refreshing = mem::replace(&mut window.refreshing, true); + let prepaint_start = window.prepaint_index(); + let (mut element, accessed_entities) = cx.detect_accessed_entities(|cx| { + let mut element = (self.render)(self, window, cx); + element.layout_as_root(bounds.size.into(), window, cx); + element.prepaint_at(bounds.origin, window, cx); + element + }); + + let prepaint_end = window.prepaint_index(); + window.refreshing = refreshing; + + ( + Some(element), + AnyViewState { + accessed_entities, + prepaint_range: prepaint_start..prepaint_end, + paint_range: PaintIndex::default()..PaintIndex::default(), + cache_key: ViewCacheKey { + bounds, + content_mask, + text_style, + }, + }, + ) + }, + ) + }) + } + + fn paint( + &mut self, + global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _: &mut Self::RequestLayoutState, + element: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + window.with_rendered_view(self.entity_id(), |window| { + let caching_disabled = window.is_inspector_picking(cx); + if self.cached_style.is_some() && !caching_disabled { + window.with_element_state::( + global_id.unwrap(), + |element_state, window| { + let mut element_state = element_state.unwrap(); + + let paint_start = window.paint_index(); + + if let Some(element) = element { + let refreshing = mem::replace(&mut window.refreshing, true); + element.paint(window, cx); + window.refreshing = refreshing; + } else { + window.reuse_paint(element_state.paint_range.clone()); + } + + let paint_end = window.paint_index(); + element_state.paint_range = paint_start..paint_end; + + ((), element_state) + }, + ) + } else { + element.as_mut().unwrap().paint(window, cx); + } + }); + } +} + +impl IntoElement for Entity { + type Element = Entity; + + fn into_element(self) -> Self::Element { + self + } +} + +impl IntoElement for AnyView { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +/// A weak, dynamically-typed view handle that does not prevent the view from being released. +pub struct AnyWeakView { + entity: AnyWeakEntity, + render: fn(&AnyView, &mut Window, &mut App) -> AnyElement, +} + +impl AnyWeakView { + /// Convert to a strongly-typed handle if the referenced view has not yet been released. + pub fn upgrade(&self) -> Option { + let entity = self.entity.upgrade()?; + Some(AnyView { + entity, + render: self.render, + cached_style: None, + }) + } +} + +impl From> for AnyWeakView { + fn from(view: WeakEntity) -> Self { + AnyWeakView { + entity: view.into(), + render: any_view::render::, + } + } +} + +impl PartialEq for AnyWeakView { + fn eq(&self, other: &Self) -> bool { + self.entity == other.entity + } +} + +impl std::fmt::Debug for AnyWeakView { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("AnyWeakView") + .field("entity_id", &self.entity.entity_id) + .finish_non_exhaustive() + } +} + +mod any_view { + use crate::{AnyElement, AnyView, App, IntoElement, Render, Window}; + + pub(crate) fn render( + view: &AnyView, + window: &mut Window, + cx: &mut App, + ) -> AnyElement { + let view = view.clone().downcast::().unwrap(); + view.update(cx, |view, cx| view.render(window, cx).into_any_element()) + } +} + +/// A view that renders nothing +pub struct EmptyView; + +impl Render for EmptyView { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + Empty + } +} diff --git a/third_party/gpui/src/window.rs b/third_party/gpui/src/window.rs new file mode 100644 index 0000000..6d74a0e --- /dev/null +++ b/third_party/gpui/src/window.rs @@ -0,0 +1,5103 @@ +#[cfg(any(feature = "inspector", debug_assertions))] +use crate::Inspector; +use crate::{ + Action, AnyDrag, AnyElement, AnyImageCache, AnyTooltip, AnyView, App, AppContext, Arena, Asset, + AsyncWindowContext, AvailableSpace, Background, BorderStyle, Bounds, BoxShadow, Capslock, + Context, Corners, CursorStyle, Decorations, DevicePixels, DispatchActionListener, + DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, + FileDropEvent, FontId, Global, GlobalElementId, GlyphId, GpuSpecs, Hsla, InputHandler, IsZero, + KeyBinding, KeyContext, KeyDownEvent, KeyEvent, Keystroke, KeystrokeEvent, LayoutId, + LineLayoutIndex, Modifiers, ModifiersChangedEvent, MonochromeSprite, MouseButton, MouseEvent, + MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, + PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptButton, PromptLevel, Quad, + Render, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge, + SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS_X, SUBPIXEL_VARIANTS_Y, ScaledPixels, Scene, Shadow, + SharedString, Size, StrikethroughStyle, Style, SubscriberSet, Subscription, SystemWindowTab, + SystemWindowTabController, TabStopMap, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement, + TransformationMatrix, Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance, + WindowBounds, WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem, + point, prelude::*, px, rems, size, transparent_black, +}; +use anyhow::{Context as _, Result, anyhow}; +use collections::{FxHashMap, FxHashSet}; +#[cfg(target_os = "macos")] +use core_video::pixel_buffer::CVPixelBuffer; +use derive_more::{Deref, DerefMut}; +use futures::FutureExt; +use futures::channel::oneshot; +use itertools::FoldWhile::{Continue, Done}; +use itertools::Itertools; +use parking_lot::RwLock; +use raw_window_handle::{HandleError, HasDisplayHandle, HasWindowHandle}; +use refineable::Refineable; +use slotmap::SlotMap; +use smallvec::SmallVec; +use std::{ + any::{Any, TypeId}, + borrow::Cow, + cell::{Cell, RefCell}, + cmp, + fmt::{Debug, Display}, + hash::{Hash, Hasher}, + marker::PhantomData, + mem, + ops::{DerefMut, Range}, + rc::Rc, + sync::{ + Arc, Weak, + atomic::{AtomicUsize, Ordering::SeqCst}, + }, + time::{Duration, Instant}, +}; +use util::post_inc; +use util::{ResultExt, measure}; +use uuid::Uuid; + +mod prompts; + +use crate::util::atomic_incr_if_not_zero; +pub use prompts::*; + +pub(crate) const DEFAULT_WINDOW_SIZE: Size = size(px(1536.), px(864.)); + +/// Represents the two different phases when dispatching events. +#[derive(Default, Copy, Clone, Debug, Eq, PartialEq)] +pub enum DispatchPhase { + /// After the capture phase comes the bubble phase, in which mouse event listeners are + /// invoked front to back and keyboard event listeners are invoked from the focused element + /// to the root of the element tree. This is the phase you'll most commonly want to use when + /// registering event listeners. + #[default] + Bubble, + /// During the initial capture phase, mouse event listeners are invoked back to front, and keyboard + /// listeners are invoked from the root of the tree downward toward the focused element. This phase + /// is used for special purposes such as clearing the "pressed" state for click events. If + /// you stop event propagation during this phase, you need to know what you're doing. Handlers + /// outside of the immediate region may rely on detecting non-local events during this phase. + Capture, +} + +impl DispatchPhase { + /// Returns true if this represents the "bubble" phase. + #[inline] + pub fn bubble(self) -> bool { + self == DispatchPhase::Bubble + } + + /// Returns true if this represents the "capture" phase. + #[inline] + pub fn capture(self) -> bool { + self == DispatchPhase::Capture + } +} + +struct WindowInvalidatorInner { + pub dirty: bool, + pub draw_phase: DrawPhase, + pub dirty_views: FxHashSet, +} + +#[derive(Clone)] +pub(crate) struct WindowInvalidator { + inner: Rc>, +} + +impl WindowInvalidator { + pub fn new() -> Self { + WindowInvalidator { + inner: Rc::new(RefCell::new(WindowInvalidatorInner { + dirty: true, + draw_phase: DrawPhase::None, + dirty_views: FxHashSet::default(), + })), + } + } + + pub fn invalidate_view(&self, entity: EntityId, cx: &mut App) -> bool { + let mut inner = self.inner.borrow_mut(); + inner.dirty_views.insert(entity); + if inner.draw_phase == DrawPhase::None { + inner.dirty = true; + cx.push_effect(Effect::Notify { emitter: entity }); + true + } else { + false + } + } + + pub fn is_dirty(&self) -> bool { + self.inner.borrow().dirty + } + + pub fn set_dirty(&self, dirty: bool) { + self.inner.borrow_mut().dirty = dirty + } + + pub fn set_phase(&self, phase: DrawPhase) { + self.inner.borrow_mut().draw_phase = phase + } + + pub fn take_views(&self) -> FxHashSet { + mem::take(&mut self.inner.borrow_mut().dirty_views) + } + + pub fn replace_views(&self, views: FxHashSet) { + self.inner.borrow_mut().dirty_views = views; + } + + pub fn not_drawing(&self) -> bool { + self.inner.borrow().draw_phase == DrawPhase::None + } + + #[track_caller] + pub fn debug_assert_paint(&self) { + debug_assert!( + matches!(self.inner.borrow().draw_phase, DrawPhase::Paint), + "this method can only be called during paint" + ); + } + + #[track_caller] + pub fn debug_assert_prepaint(&self) { + debug_assert!( + matches!(self.inner.borrow().draw_phase, DrawPhase::Prepaint), + "this method can only be called during request_layout, or prepaint" + ); + } + + #[track_caller] + pub fn debug_assert_paint_or_prepaint(&self) { + debug_assert!( + matches!( + self.inner.borrow().draw_phase, + DrawPhase::Paint | DrawPhase::Prepaint + ), + "this method can only be called during request_layout, prepaint, or paint" + ); + } +} + +type AnyObserver = Box bool + 'static>; + +pub(crate) type AnyWindowFocusListener = + Box bool + 'static>; + +pub(crate) struct WindowFocusEvent { + pub(crate) previous_focus_path: SmallVec<[FocusId; 8]>, + pub(crate) current_focus_path: SmallVec<[FocusId; 8]>, +} + +impl WindowFocusEvent { + pub fn is_focus_in(&self, focus_id: FocusId) -> bool { + !self.previous_focus_path.contains(&focus_id) && self.current_focus_path.contains(&focus_id) + } + + pub fn is_focus_out(&self, focus_id: FocusId) -> bool { + self.previous_focus_path.contains(&focus_id) && !self.current_focus_path.contains(&focus_id) + } +} + +/// This is provided when subscribing for `Context::on_focus_out` events. +pub struct FocusOutEvent { + /// A weak focus handle representing what was blurred. + pub blurred: WeakFocusHandle, +} + +slotmap::new_key_type! { + /// A globally unique identifier for a focusable element. + pub struct FocusId; +} + +thread_local! { + pub(crate) static ELEMENT_ARENA: RefCell = RefCell::new(Arena::new(1024 * 1024)); +} + +/// Returned when the element arena has been used and so must be cleared before the next draw. +#[must_use] +pub struct ArenaClearNeeded; + +impl ArenaClearNeeded { + /// Clear the element arena. + pub fn clear(self) { + ELEMENT_ARENA.with_borrow_mut(|element_arena| { + element_arena.clear(); + }); + } +} + +pub(crate) type FocusMap = RwLock>; +pub(crate) struct FocusRef { + pub(crate) ref_count: AtomicUsize, + pub(crate) tab_index: isize, + pub(crate) tab_stop: bool, +} + +impl FocusId { + /// Obtains whether the element associated with this handle is currently focused. + pub fn is_focused(&self, window: &Window) -> bool { + window.focus == Some(*self) + } + + /// Obtains whether the element associated with this handle contains the focused + /// element or is itself focused. + pub fn contains_focused(&self, window: &Window, cx: &App) -> bool { + window + .focused(cx) + .is_some_and(|focused| self.contains(focused.id, window)) + } + + /// Obtains whether the element associated with this handle is contained within the + /// focused element or is itself focused. + pub fn within_focused(&self, window: &Window, cx: &App) -> bool { + let focused = window.focused(cx); + focused.is_some_and(|focused| focused.id.contains(*self, window)) + } + + /// Obtains whether this handle contains the given handle in the most recently rendered frame. + pub(crate) fn contains(&self, other: Self, window: &Window) -> bool { + window + .rendered_frame + .dispatch_tree + .focus_contains(*self, other) + } +} + +/// A handle which can be used to track and manipulate the focused element in a window. +pub struct FocusHandle { + pub(crate) id: FocusId, + handles: Arc, + /// The index of this element in the tab order. + pub tab_index: isize, + /// Whether this element can be focused by tab navigation. + pub tab_stop: bool, +} + +impl std::fmt::Debug for FocusHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("FocusHandle({:?})", self.id)) + } +} + +impl FocusHandle { + pub(crate) fn new(handles: &Arc) -> Self { + let id = handles.write().insert(FocusRef { + ref_count: AtomicUsize::new(1), + tab_index: 0, + tab_stop: false, + }); + + Self { + id, + tab_index: 0, + tab_stop: false, + handles: handles.clone(), + } + } + + pub(crate) fn for_id(id: FocusId, handles: &Arc) -> Option { + let lock = handles.read(); + let focus = lock.get(id)?; + if atomic_incr_if_not_zero(&focus.ref_count) == 0 { + return None; + } + Some(Self { + id, + tab_index: focus.tab_index, + tab_stop: focus.tab_stop, + handles: handles.clone(), + }) + } + + /// Sets the tab index of the element associated with this handle. + pub fn tab_index(mut self, index: isize) -> Self { + self.tab_index = index; + if let Some(focus) = self.handles.write().get_mut(self.id) { + focus.tab_index = index; + } + self + } + + /// Sets whether the element associated with this handle is a tab stop. + /// + /// When `false`, the element will not be included in the tab order. + pub fn tab_stop(mut self, tab_stop: bool) -> Self { + self.tab_stop = tab_stop; + if let Some(focus) = self.handles.write().get_mut(self.id) { + focus.tab_stop = tab_stop; + } + self + } + + /// Converts this focus handle into a weak variant, which does not prevent it from being released. + pub fn downgrade(&self) -> WeakFocusHandle { + WeakFocusHandle { + id: self.id, + handles: Arc::downgrade(&self.handles), + } + } + + /// Moves the focus to the element associated with this handle. + pub fn focus(&self, window: &mut Window) { + window.focus(self) + } + + /// Obtains whether the element associated with this handle is currently focused. + pub fn is_focused(&self, window: &Window) -> bool { + self.id.is_focused(window) + } + + /// Obtains whether the element associated with this handle contains the focused + /// element or is itself focused. + pub fn contains_focused(&self, window: &Window, cx: &App) -> bool { + self.id.contains_focused(window, cx) + } + + /// Obtains whether the element associated with this handle is contained within the + /// focused element or is itself focused. + pub fn within_focused(&self, window: &Window, cx: &mut App) -> bool { + self.id.within_focused(window, cx) + } + + /// Obtains whether this handle contains the given handle in the most recently rendered frame. + pub fn contains(&self, other: &Self, window: &Window) -> bool { + self.id.contains(other.id, window) + } + + /// Dispatch an action on the element that rendered this focus handle + pub fn dispatch_action(&self, action: &dyn Action, window: &mut Window, cx: &mut App) { + if let Some(node_id) = window + .rendered_frame + .dispatch_tree + .focusable_node_id(self.id) + { + window.dispatch_action_on_node(node_id, action, cx) + } + } +} + +impl Clone for FocusHandle { + fn clone(&self) -> Self { + Self::for_id(self.id, &self.handles).unwrap() + } +} + +impl PartialEq for FocusHandle { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for FocusHandle {} + +impl Drop for FocusHandle { + fn drop(&mut self) { + self.handles + .read() + .get(self.id) + .unwrap() + .ref_count + .fetch_sub(1, SeqCst); + } +} + +/// A weak reference to a focus handle. +#[derive(Clone, Debug)] +pub struct WeakFocusHandle { + pub(crate) id: FocusId, + pub(crate) handles: Weak, +} + +impl WeakFocusHandle { + /// Attempts to upgrade the [WeakFocusHandle] to a [FocusHandle]. + pub fn upgrade(&self) -> Option { + let handles = self.handles.upgrade()?; + FocusHandle::for_id(self.id, &handles) + } +} + +impl PartialEq for WeakFocusHandle { + fn eq(&self, other: &WeakFocusHandle) -> bool { + self.id == other.id + } +} + +impl Eq for WeakFocusHandle {} + +impl PartialEq for WeakFocusHandle { + fn eq(&self, other: &FocusHandle) -> bool { + self.id == other.id + } +} + +impl PartialEq for FocusHandle { + fn eq(&self, other: &WeakFocusHandle) -> bool { + self.id == other.id + } +} + +/// Focusable allows users of your view to easily +/// focus it (using window.focus_view(cx, view)) +pub trait Focusable: 'static { + /// Returns the focus handle associated with this view. + fn focus_handle(&self, cx: &App) -> FocusHandle; +} + +impl Focusable for Entity { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.read(cx).focus_handle(cx) + } +} + +/// ManagedView is a view (like a Modal, Popover, Menu, etc.) +/// where the lifecycle of the view is handled by another view. +pub trait ManagedView: Focusable + EventEmitter + Render {} + +impl + Render> ManagedView for M {} + +/// Emitted by implementers of [`ManagedView`] to indicate the view should be dismissed, such as when a view is presented as a modal. +pub struct DismissEvent; + +type FrameCallback = Box; + +pub(crate) type AnyMouseListener = + Box; + +#[derive(Clone)] +pub(crate) struct CursorStyleRequest { + pub(crate) hitbox_id: Option, + pub(crate) style: CursorStyle, +} + +#[derive(Default, Eq, PartialEq)] +pub(crate) struct HitTest { + pub(crate) ids: SmallVec<[HitboxId; 8]>, + pub(crate) hover_hitbox_count: usize, +} + +/// A type of window control area that corresponds to the platform window. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WindowControlArea { + /// An area that allows dragging of the platform window. + Drag, + /// An area that allows closing of the platform window. + Close, + /// An area that allows maximizing of the platform window. + Max, + /// An area that allows minimizing of the platform window. + Min, +} + +/// An identifier for a [Hitbox] which also includes [HitboxBehavior]. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct HitboxId(u64); + +impl HitboxId { + /// Checks if the hitbox with this ID is currently hovered. Except when handling + /// `ScrollWheelEvent`, this is typically what you want when determining whether to handle mouse + /// events or paint hover styles. + /// + /// See [`Hitbox::is_hovered`] for details. + pub fn is_hovered(self, window: &Window) -> bool { + let hit_test = &window.mouse_hit_test; + for id in hit_test.ids.iter().take(hit_test.hover_hitbox_count) { + if self == *id { + return true; + } + } + false + } + + /// Checks if the hitbox with this ID contains the mouse and should handle scroll events. + /// Typically this should only be used when handling `ScrollWheelEvent`, and otherwise + /// `is_hovered` should be used. See the documentation of `Hitbox::is_hovered` for details about + /// this distinction. + pub fn should_handle_scroll(self, window: &Window) -> bool { + window.mouse_hit_test.ids.contains(&self) + } + + fn next(mut self) -> HitboxId { + HitboxId(self.0.wrapping_add(1)) + } +} + +/// A rectangular region that potentially blocks hitboxes inserted prior. +/// See [Window::insert_hitbox] for more details. +#[derive(Clone, Debug, Deref)] +pub struct Hitbox { + /// A unique identifier for the hitbox. + pub id: HitboxId, + /// The bounds of the hitbox. + #[deref] + pub bounds: Bounds, + /// The content mask when the hitbox was inserted. + pub content_mask: ContentMask, + /// Flags that specify hitbox behavior. + pub behavior: HitboxBehavior, +} + +impl Hitbox { + /// Checks if the hitbox is currently hovered. Except when handling `ScrollWheelEvent`, this is + /// typically what you want when determining whether to handle mouse events or paint hover + /// styles. + /// + /// This can return `false` even when the hitbox contains the mouse, if a hitbox in front of + /// this sets `HitboxBehavior::BlockMouse` (`InteractiveElement::occlude`) or + /// `HitboxBehavior::BlockMouseExceptScroll` (`InteractiveElement::block_mouse_except_scroll`). + /// + /// Handling of `ScrollWheelEvent` should typically use `should_handle_scroll` instead. + /// Concretely, this is due to use-cases like overlays that cause the elements under to be + /// non-interactive while still allowing scrolling. More abstractly, this is because + /// `is_hovered` is about element interactions directly under the mouse - mouse moves, clicks, + /// hover styling, etc. In contrast, scrolling is about finding the current outer scrollable + /// container. + pub fn is_hovered(&self, window: &Window) -> bool { + self.id.is_hovered(window) + } + + /// Checks if the hitbox contains the mouse and should handle scroll events. Typically this + /// should only be used when handling `ScrollWheelEvent`, and otherwise `is_hovered` should be + /// used. See the documentation of `Hitbox::is_hovered` for details about this distinction. + /// + /// This can return `false` even when the hitbox contains the mouse, if a hitbox in front of + /// this sets `HitboxBehavior::BlockMouse` (`InteractiveElement::occlude`). + pub fn should_handle_scroll(&self, window: &Window) -> bool { + self.id.should_handle_scroll(window) + } +} + +/// How the hitbox affects mouse behavior. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum HitboxBehavior { + /// Normal hitbox mouse behavior, doesn't affect mouse handling for other hitboxes. + #[default] + Normal, + + /// All hitboxes behind this hitbox will be ignored and so will have `hitbox.is_hovered() == + /// false` and `hitbox.should_handle_scroll() == false`. Typically for elements this causes + /// skipping of all mouse events, hover styles, and tooltips. This flag is set by + /// [`InteractiveElement::occlude`]. + /// + /// For mouse handlers that check those hitboxes, this behaves the same as registering a + /// bubble-phase handler for every mouse event type: + /// + /// ```ignore + /// window.on_mouse_event(move |_: &EveryMouseEventTypeHere, phase, window, cx| { + /// if phase == DispatchPhase::Capture && hitbox.is_hovered(window) { + /// cx.stop_propagation(); + /// } + /// }) + /// ``` + /// + /// This has effects beyond event handling - any use of hitbox checking, such as hover + /// styles and tooltops. These other behaviors are the main point of this mechanism. An + /// alternative might be to not affect mouse event handling - but this would allow + /// inconsistent UI where clicks and moves interact with elements that are not considered to + /// be hovered. + BlockMouse, + + /// All hitboxes behind this hitbox will have `hitbox.is_hovered() == false`, even when + /// `hitbox.should_handle_scroll() == true`. Typically for elements this causes all mouse + /// interaction except scroll events to be ignored - see the documentation of + /// [`Hitbox::is_hovered`] for details. This flag is set by + /// [`InteractiveElement::block_mouse_except_scroll`]. + /// + /// For mouse handlers that check those hitboxes, this behaves the same as registering a + /// bubble-phase handler for every mouse event type **except** `ScrollWheelEvent`: + /// + /// ```ignore + /// window.on_mouse_event(move |_: &EveryMouseEventTypeExceptScroll, phase, window, cx| { + /// if phase == DispatchPhase::Bubble && hitbox.should_handle_scroll(window) { + /// cx.stop_propagation(); + /// } + /// }) + /// ``` + /// + /// See the documentation of [`Hitbox::is_hovered`] for details of why `ScrollWheelEvent` is + /// handled differently than other mouse events. If also blocking these scroll events is + /// desired, then a `cx.stop_propagation()` handler like the one above can be used. + /// + /// This has effects beyond event handling - this affects any use of `is_hovered`, such as + /// hover styles and tooltops. These other behaviors are the main point of this mechanism. + /// An alternative might be to not affect mouse event handling - but this would allow + /// inconsistent UI where clicks and moves interact with elements that are not considered to + /// be hovered. + BlockMouseExceptScroll, +} + +/// An identifier for a tooltip. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct TooltipId(usize); + +impl TooltipId { + /// Checks if the tooltip is currently hovered. + pub fn is_hovered(&self, window: &Window) -> bool { + window + .tooltip_bounds + .as_ref() + .is_some_and(|tooltip_bounds| { + tooltip_bounds.id == *self + && tooltip_bounds.bounds.contains(&window.mouse_position()) + }) + } +} + +pub(crate) struct TooltipBounds { + id: TooltipId, + bounds: Bounds, +} + +#[derive(Clone)] +pub(crate) struct TooltipRequest { + id: TooltipId, + tooltip: AnyTooltip, +} + +pub(crate) struct DeferredDraw { + current_view: EntityId, + priority: usize, + parent_node: DispatchNodeId, + element_id_stack: SmallVec<[ElementId; 32]>, + text_style_stack: Vec, + element: Option, + absolute_offset: Point, + prepaint_range: Range, + paint_range: Range, +} + +pub(crate) struct Frame { + pub(crate) focus: Option, + pub(crate) window_active: bool, + pub(crate) element_states: FxHashMap<(GlobalElementId, TypeId), ElementStateBox>, + accessed_element_states: Vec<(GlobalElementId, TypeId)>, + pub(crate) mouse_listeners: Vec>, + pub(crate) dispatch_tree: DispatchTree, + pub(crate) scene: Scene, + pub(crate) hitboxes: Vec, + pub(crate) window_control_hitboxes: Vec<(WindowControlArea, Hitbox)>, + pub(crate) deferred_draws: Vec, + pub(crate) input_handlers: Vec>, + pub(crate) tooltip_requests: Vec>, + pub(crate) cursor_styles: Vec, + #[cfg(any(test, feature = "test-support"))] + pub(crate) debug_bounds: FxHashMap>, + #[cfg(any(feature = "inspector", debug_assertions))] + pub(crate) next_inspector_instance_ids: FxHashMap, usize>, + #[cfg(any(feature = "inspector", debug_assertions))] + pub(crate) inspector_hitboxes: FxHashMap, + pub(crate) tab_stops: TabStopMap, +} + +#[derive(Clone, Default)] +pub(crate) struct PrepaintStateIndex { + hitboxes_index: usize, + tooltips_index: usize, + deferred_draws_index: usize, + dispatch_tree_index: usize, + accessed_element_states_index: usize, + line_layout_index: LineLayoutIndex, +} + +#[derive(Clone, Default)] +pub(crate) struct PaintIndex { + scene_index: usize, + mouse_listeners_index: usize, + input_handlers_index: usize, + cursor_styles_index: usize, + accessed_element_states_index: usize, + tab_handle_index: usize, + line_layout_index: LineLayoutIndex, +} + +impl Frame { + pub(crate) fn new(dispatch_tree: DispatchTree) -> Self { + Frame { + focus: None, + window_active: false, + element_states: FxHashMap::default(), + accessed_element_states: Vec::new(), + mouse_listeners: Vec::new(), + dispatch_tree, + scene: Scene::default(), + hitboxes: Vec::new(), + window_control_hitboxes: Vec::new(), + deferred_draws: Vec::new(), + input_handlers: Vec::new(), + tooltip_requests: Vec::new(), + cursor_styles: Vec::new(), + + #[cfg(any(test, feature = "test-support"))] + debug_bounds: FxHashMap::default(), + + #[cfg(any(feature = "inspector", debug_assertions))] + next_inspector_instance_ids: FxHashMap::default(), + + #[cfg(any(feature = "inspector", debug_assertions))] + inspector_hitboxes: FxHashMap::default(), + tab_stops: TabStopMap::default(), + } + } + + pub(crate) fn clear(&mut self) { + self.element_states.clear(); + self.accessed_element_states.clear(); + self.mouse_listeners.clear(); + self.dispatch_tree.clear(); + self.scene.clear(); + self.input_handlers.clear(); + self.tooltip_requests.clear(); + self.cursor_styles.clear(); + self.hitboxes.clear(); + self.window_control_hitboxes.clear(); + self.deferred_draws.clear(); + self.tab_stops.clear(); + self.focus = None; + + #[cfg(any(feature = "inspector", debug_assertions))] + { + self.next_inspector_instance_ids.clear(); + self.inspector_hitboxes.clear(); + } + } + + pub(crate) fn cursor_style(&self, window: &Window) -> Option { + self.cursor_styles + .iter() + .rev() + .fold_while(None, |style, request| match request.hitbox_id { + None => Done(Some(request.style)), + Some(hitbox_id) => Continue( + style.or_else(|| hitbox_id.is_hovered(window).then_some(request.style)), + ), + }) + .into_inner() + } + + pub(crate) fn hit_test(&self, position: Point) -> HitTest { + let mut set_hover_hitbox_count = false; + let mut hit_test = HitTest::default(); + for hitbox in self.hitboxes.iter().rev() { + let bounds = hitbox.bounds.intersect(&hitbox.content_mask.bounds); + if bounds.contains(&position) { + hit_test.ids.push(hitbox.id); + if !set_hover_hitbox_count + && hitbox.behavior == HitboxBehavior::BlockMouseExceptScroll + { + hit_test.hover_hitbox_count = hit_test.ids.len(); + set_hover_hitbox_count = true; + } + if hitbox.behavior == HitboxBehavior::BlockMouse { + break; + } + } + } + if !set_hover_hitbox_count { + hit_test.hover_hitbox_count = hit_test.ids.len(); + } + hit_test + } + + pub(crate) fn focus_path(&self) -> SmallVec<[FocusId; 8]> { + self.focus + .map(|focus_id| self.dispatch_tree.focus_path(focus_id)) + .unwrap_or_default() + } + + pub(crate) fn finish(&mut self, prev_frame: &mut Self) { + for element_state_key in &self.accessed_element_states { + if let Some((element_state_key, element_state)) = + prev_frame.element_states.remove_entry(element_state_key) + { + self.element_states.insert(element_state_key, element_state); + } + } + + self.scene.finish(); + } +} + +/// Holds the state for a specific window. +pub struct Window { + pub(crate) handle: AnyWindowHandle, + pub(crate) invalidator: WindowInvalidator, + pub(crate) removed: bool, + pub(crate) platform_window: Box, + display_id: Option, + sprite_atlas: Arc, + text_system: Arc, + rem_size: Pixels, + /// The stack of override values for the window's rem size. + /// + /// This is used by `with_rem_size` to allow rendering an element tree with + /// a given rem size. + rem_size_override_stack: SmallVec<[Pixels; 8]>, + pub(crate) viewport_size: Size, + layout_engine: Option, + pub(crate) root: Option, + pub(crate) element_id_stack: SmallVec<[ElementId; 32]>, + pub(crate) text_style_stack: Vec, + pub(crate) rendered_entity_stack: Vec, + pub(crate) element_offset_stack: Vec>, + pub(crate) element_opacity: f32, + pub(crate) content_mask_stack: Vec>, + pub(crate) requested_autoscroll: Option>, + pub(crate) image_cache_stack: Vec, + pub(crate) rendered_frame: Frame, + pub(crate) next_frame: Frame, + next_hitbox_id: HitboxId, + pub(crate) next_tooltip_id: TooltipId, + pub(crate) tooltip_bounds: Option, + next_frame_callbacks: Rc>>, + pub(crate) dirty_views: FxHashSet, + focus_listeners: SubscriberSet<(), AnyWindowFocusListener>, + pub(crate) focus_lost_listeners: SubscriberSet<(), AnyObserver>, + default_prevented: bool, + mouse_position: Point, + mouse_hit_test: HitTest, + modifiers: Modifiers, + capslock: Capslock, + scale_factor: f32, + pub(crate) bounds_observers: SubscriberSet<(), AnyObserver>, + appearance: WindowAppearance, + pub(crate) appearance_observers: SubscriberSet<(), AnyObserver>, + active: Rc>, + hovered: Rc>, + pub(crate) needs_present: Rc>, + pub(crate) last_input_timestamp: Rc>, + pub(crate) refreshing: bool, + pub(crate) activation_observers: SubscriberSet<(), AnyObserver>, + pub(crate) focus: Option, + focus_enabled: bool, + pending_input: Option, + pending_modifier: ModifierState, + pub(crate) pending_input_observers: SubscriberSet<(), AnyObserver>, + prompt: Option, + pub(crate) client_inset: Option, + #[cfg(any(feature = "inspector", debug_assertions))] + inspector: Option>, +} + +#[derive(Clone, Debug, Default)] +struct ModifierState { + modifiers: Modifiers, + saw_keystroke: bool, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum DrawPhase { + None, + Prepaint, + Paint, + Focus, +} + +#[derive(Default, Debug)] +struct PendingInput { + keystrokes: SmallVec<[Keystroke; 1]>, + focus: Option, + timer: Option>, +} + +pub(crate) struct ElementStateBox { + pub(crate) inner: Box, + #[cfg(debug_assertions)] + pub(crate) type_name: &'static str, +} + +fn default_bounds(display_id: Option, cx: &mut App) -> Bounds { + const DEFAULT_WINDOW_OFFSET: Point = point(px(0.), px(35.)); + + // TODO, BUG: if you open a window with the currently active window + // on the stack, this will erroneously select the 'unwrap_or_else' + // code path + cx.active_window() + .and_then(|w| w.update(cx, |_, window, _| window.bounds()).ok()) + .map(|mut bounds| { + bounds.origin += DEFAULT_WINDOW_OFFSET; + bounds + }) + .unwrap_or_else(|| { + let display = display_id + .map(|id| cx.find_display(id)) + .unwrap_or_else(|| cx.primary_display()); + + display + .map(|display| display.default_bounds()) + .unwrap_or_else(|| Bounds::new(point(px(0.), px(0.)), DEFAULT_WINDOW_SIZE)) + }) +} + +impl Window { + pub(crate) fn new( + handle: AnyWindowHandle, + options: WindowOptions, + cx: &mut App, + ) -> Result { + let WindowOptions { + window_bounds, + titlebar, + focus, + show, + kind, + is_movable, + is_resizable, + is_minimizable, + display_id, + window_background, + app_id, + window_min_size, + window_decorations, + #[cfg_attr(not(target_os = "macos"), allow(unused_variables))] + tabbing_identifier, + } = options; + + let bounds = window_bounds + .map(|bounds| bounds.get_bounds()) + .unwrap_or_else(|| default_bounds(display_id, cx)); + let mut platform_window = cx.platform.open_window( + handle, + WindowParams { + bounds, + titlebar, + kind, + is_movable, + is_resizable, + is_minimizable, + focus, + show, + display_id, + window_min_size, + #[cfg(target_os = "macos")] + tabbing_identifier, + }, + )?; + + let tab_bar_visible = platform_window.tab_bar_visible(); + SystemWindowTabController::init_visible(cx, tab_bar_visible); + if let Some(tabs) = platform_window.tabbed_windows() { + SystemWindowTabController::add_tab(cx, handle.window_id(), tabs); + } + + let display_id = platform_window.display().map(|display| display.id()); + let sprite_atlas = platform_window.sprite_atlas(); + let mouse_position = platform_window.mouse_position(); + let modifiers = platform_window.modifiers(); + let capslock = platform_window.capslock(); + let content_size = platform_window.content_size(); + let scale_factor = platform_window.scale_factor(); + let appearance = platform_window.appearance(); + let text_system = Arc::new(WindowTextSystem::new(cx.text_system().clone())); + let invalidator = WindowInvalidator::new(); + let active = Rc::new(Cell::new(platform_window.is_active())); + let hovered = Rc::new(Cell::new(platform_window.is_hovered())); + let needs_present = Rc::new(Cell::new(false)); + let next_frame_callbacks: Rc>> = Default::default(); + let last_input_timestamp = Rc::new(Cell::new(Instant::now())); + + platform_window + .request_decorations(window_decorations.unwrap_or(WindowDecorations::Server)); + platform_window.set_background_appearance(window_background); + + if let Some(ref window_open_state) = window_bounds { + match window_open_state { + WindowBounds::Fullscreen(_) => platform_window.toggle_fullscreen(), + WindowBounds::Maximized(_) => platform_window.zoom(), + WindowBounds::Windowed(_) => {} + } + } + + platform_window.on_close(Box::new({ + let window_id = handle.window_id(); + let mut cx = cx.to_async(); + move || { + let _ = handle.update(&mut cx, |_, window, _| window.remove_window()); + let _ = cx.update(|cx| { + SystemWindowTabController::remove_tab(cx, window_id); + }); + } + })); + platform_window.on_request_frame(Box::new({ + let mut cx = cx.to_async(); + let invalidator = invalidator.clone(); + let active = active.clone(); + let needs_present = needs_present.clone(); + let next_frame_callbacks = next_frame_callbacks.clone(); + let last_input_timestamp = last_input_timestamp.clone(); + move |request_frame_options| { + let next_frame_callbacks = next_frame_callbacks.take(); + if !next_frame_callbacks.is_empty() { + handle + .update(&mut cx, |_, window, cx| { + for callback in next_frame_callbacks { + callback(window, cx); + } + }) + .log_err(); + } + + // Keep presenting the current scene for 1 extra second since the + // last input to prevent the display from underclocking the refresh rate. + let needs_present = request_frame_options.require_presentation + || needs_present.get() + || (active.get() + && last_input_timestamp.get().elapsed() < Duration::from_secs(1)); + + if invalidator.is_dirty() || request_frame_options.force_render { + measure("frame duration", || { + handle + .update(&mut cx, |_, window, cx| { + let arena_clear_needed = window.draw(cx); + window.present(); + // drop the arena elements after present to reduce latency + arena_clear_needed.clear(); + }) + .log_err(); + }) + } else if needs_present { + handle + .update(&mut cx, |_, window, _| window.present()) + .log_err(); + } + + handle + .update(&mut cx, |_, window, _| { + window.complete_frame(); + }) + .log_err(); + } + })); + platform_window.on_resize(Box::new({ + let mut cx = cx.to_async(); + move |_, _| { + handle + .update(&mut cx, |_, window, cx| window.bounds_changed(cx)) + .log_err(); + } + })); + platform_window.on_moved(Box::new({ + let mut cx = cx.to_async(); + move || { + handle + .update(&mut cx, |_, window, cx| window.bounds_changed(cx)) + .log_err(); + } + })); + platform_window.on_appearance_changed(Box::new({ + let mut cx = cx.to_async(); + move || { + handle + .update(&mut cx, |_, window, cx| window.appearance_changed(cx)) + .log_err(); + } + })); + platform_window.on_active_status_change(Box::new({ + let mut cx = cx.to_async(); + move |active| { + handle + .update(&mut cx, |_, window, cx| { + window.active.set(active); + window.modifiers = window.platform_window.modifiers(); + window.capslock = window.platform_window.capslock(); + window + .activation_observers + .clone() + .retain(&(), |callback| callback(window, cx)); + + window.bounds_changed(cx); + window.refresh(); + + SystemWindowTabController::update_last_active(cx, window.handle.id); + }) + .log_err(); + } + })); + platform_window.on_hover_status_change(Box::new({ + let mut cx = cx.to_async(); + move |active| { + handle + .update(&mut cx, |_, window, _| { + window.hovered.set(active); + window.refresh(); + }) + .log_err(); + } + })); + platform_window.on_input({ + let mut cx = cx.to_async(); + Box::new(move |event| { + handle + .update(&mut cx, |_, window, cx| window.dispatch_event(event, cx)) + .log_err() + .unwrap_or(DispatchEventResult::default()) + }) + }); + platform_window.on_hit_test_window_control({ + let mut cx = cx.to_async(); + Box::new(move || { + handle + .update(&mut cx, |_, window, _cx| { + for (area, hitbox) in &window.rendered_frame.window_control_hitboxes { + if window.mouse_hit_test.ids.contains(&hitbox.id) { + return Some(*area); + } + } + None + }) + .log_err() + .unwrap_or(None) + }) + }); + platform_window.on_move_tab_to_new_window({ + let mut cx = cx.to_async(); + Box::new(move || { + handle + .update(&mut cx, |_, _window, cx| { + SystemWindowTabController::move_tab_to_new_window(cx, handle.window_id()); + }) + .log_err(); + }) + }); + platform_window.on_merge_all_windows({ + let mut cx = cx.to_async(); + Box::new(move || { + handle + .update(&mut cx, |_, _window, cx| { + SystemWindowTabController::merge_all_windows(cx, handle.window_id()); + }) + .log_err(); + }) + }); + platform_window.on_select_next_tab({ + let mut cx = cx.to_async(); + Box::new(move || { + handle + .update(&mut cx, |_, _window, cx| { + SystemWindowTabController::select_next_tab(cx, handle.window_id()); + }) + .log_err(); + }) + }); + platform_window.on_select_previous_tab({ + let mut cx = cx.to_async(); + Box::new(move || { + handle + .update(&mut cx, |_, _window, cx| { + SystemWindowTabController::select_previous_tab(cx, handle.window_id()) + }) + .log_err(); + }) + }); + platform_window.on_toggle_tab_bar({ + let mut cx = cx.to_async(); + Box::new(move || { + handle + .update(&mut cx, |_, window, cx| { + let tab_bar_visible = window.platform_window.tab_bar_visible(); + SystemWindowTabController::set_visible(cx, tab_bar_visible); + }) + .log_err(); + }) + }); + + if let Some(app_id) = app_id { + platform_window.set_app_id(&app_id); + } + + platform_window.map_window().unwrap(); + + Ok(Window { + handle, + invalidator, + removed: false, + platform_window, + display_id, + sprite_atlas, + text_system, + rem_size: px(16.), + rem_size_override_stack: SmallVec::new(), + viewport_size: content_size, + layout_engine: Some(TaffyLayoutEngine::new()), + root: None, + element_id_stack: SmallVec::default(), + text_style_stack: Vec::new(), + rendered_entity_stack: Vec::new(), + element_offset_stack: Vec::new(), + content_mask_stack: Vec::new(), + element_opacity: 1.0, + requested_autoscroll: None, + rendered_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())), + next_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())), + next_frame_callbacks, + next_hitbox_id: HitboxId(0), + next_tooltip_id: TooltipId::default(), + tooltip_bounds: None, + dirty_views: FxHashSet::default(), + focus_listeners: SubscriberSet::new(), + focus_lost_listeners: SubscriberSet::new(), + default_prevented: true, + mouse_position, + mouse_hit_test: HitTest::default(), + modifiers, + capslock, + scale_factor, + bounds_observers: SubscriberSet::new(), + appearance, + appearance_observers: SubscriberSet::new(), + active, + hovered, + needs_present, + last_input_timestamp, + refreshing: false, + activation_observers: SubscriberSet::new(), + focus: None, + focus_enabled: true, + pending_input: None, + pending_modifier: ModifierState::default(), + pending_input_observers: SubscriberSet::new(), + prompt: None, + client_inset: None, + image_cache_stack: Vec::new(), + #[cfg(any(feature = "inspector", debug_assertions))] + inspector: None, + }) + } + + pub(crate) fn new_focus_listener( + &self, + value: AnyWindowFocusListener, + ) -> (Subscription, impl FnOnce() + use<>) { + self.focus_listeners.insert((), value) + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub(crate) struct DispatchEventResult { + pub propagate: bool, + pub default_prevented: bool, +} + +/// Indicates which region of the window is visible. Content falling outside of this mask will not be +/// rendered. Currently, only rectangular content masks are supported, but we give the mask its own type +/// to leave room to support more complex shapes in the future. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[repr(C)] +pub struct ContentMask { + /// The bounds + pub bounds: Bounds

, +} + +impl ContentMask { + /// Scale the content mask's pixel units by the given scaling factor. + pub fn scale(&self, factor: f32) -> ContentMask { + ContentMask { + bounds: self.bounds.scale(factor), + } + } + + /// Intersect the content mask with the given content mask. + pub fn intersect(&self, other: &Self) -> Self { + let bounds = self.bounds.intersect(&other.bounds); + ContentMask { bounds } + } +} + +impl Window { + fn mark_view_dirty(&mut self, view_id: EntityId) { + // Mark ancestor views as dirty. If already in the `dirty_views` set, then all its ancestors + // should already be dirty. + for view_id in self + .rendered_frame + .dispatch_tree + .view_path(view_id) + .into_iter() + .rev() + { + if !self.dirty_views.insert(view_id) { + break; + } + } + } + + /// Registers a callback to be invoked when the window appearance changes. + pub fn observe_window_appearance( + &self, + mut callback: impl FnMut(&mut Window, &mut App) + 'static, + ) -> Subscription { + let (subscription, activate) = self.appearance_observers.insert( + (), + Box::new(move |window, cx| { + callback(window, cx); + true + }), + ); + activate(); + subscription + } + + /// Replaces the root entity of the window with a new one. + pub fn replace_root( + &mut self, + cx: &mut App, + build_view: impl FnOnce(&mut Window, &mut Context) -> E, + ) -> Entity + where + E: 'static + Render, + { + let view = cx.new(|cx| build_view(self, cx)); + self.root = Some(view.clone().into()); + self.refresh(); + view + } + + /// Returns the root entity of the window, if it has one. + pub fn root(&self) -> Option>> + where + E: 'static + Render, + { + self.root + .as_ref() + .map(|view| view.clone().downcast::().ok()) + } + + /// Obtain a handle to the window that belongs to this context. + pub fn window_handle(&self) -> AnyWindowHandle { + self.handle + } + + /// Mark the window as dirty, scheduling it to be redrawn on the next frame. + pub fn refresh(&mut self) { + if self.invalidator.not_drawing() { + self.refreshing = true; + self.invalidator.set_dirty(true); + } + } + + /// Close this window. + pub fn remove_window(&mut self) { + self.removed = true; + } + + /// Obtain the currently focused [`FocusHandle`]. If no elements are focused, returns `None`. + pub fn focused(&self, cx: &App) -> Option { + self.focus + .and_then(|id| FocusHandle::for_id(id, &cx.focus_handles)) + } + + /// Move focus to the element associated with the given [`FocusHandle`]. + pub fn focus(&mut self, handle: &FocusHandle) { + if !self.focus_enabled || self.focus == Some(handle.id) { + return; + } + + self.focus = Some(handle.id); + self.clear_pending_keystrokes(); + self.refresh(); + } + + /// Remove focus from all elements within this context's window. + pub fn blur(&mut self) { + if !self.focus_enabled { + return; + } + + self.focus = None; + self.refresh(); + } + + /// Blur the window and don't allow anything in it to be focused again. + pub fn disable_focus(&mut self) { + self.blur(); + self.focus_enabled = false; + } + + /// Move focus to next tab stop. + pub fn focus_next(&mut self) { + if !self.focus_enabled { + return; + } + + if let Some(handle) = self.rendered_frame.tab_stops.next(self.focus.as_ref()) { + self.focus(&handle) + } + } + + /// Move focus to previous tab stop. + pub fn focus_prev(&mut self) { + if !self.focus_enabled { + return; + } + + if let Some(handle) = self.rendered_frame.tab_stops.prev(self.focus.as_ref()) { + self.focus(&handle) + } + } + + /// Accessor for the text system. + pub fn text_system(&self) -> &Arc { + &self.text_system + } + + /// The current text style. Which is composed of all the style refinements provided to `with_text_style`. + pub fn text_style(&self) -> TextStyle { + let mut style = TextStyle::default(); + for refinement in &self.text_style_stack { + style.refine(refinement); + } + style + } + + /// Check if the platform window is maximized + /// On some platforms (namely Windows) this is different than the bounds being the size of the display + pub fn is_maximized(&self) -> bool { + self.platform_window.is_maximized() + } + + /// request a certain window decoration (Wayland) + pub fn request_decorations(&self, decorations: WindowDecorations) { + self.platform_window.request_decorations(decorations); + } + + /// Start a window resize operation (Wayland) + pub fn start_window_resize(&self, edge: ResizeEdge) { + self.platform_window.start_window_resize(edge); + } + + /// Return the `WindowBounds` to indicate that how a window should be opened + /// after it has been closed + pub fn window_bounds(&self) -> WindowBounds { + self.platform_window.window_bounds() + } + + /// Return the `WindowBounds` excluding insets (Wayland and X11) + pub fn inner_window_bounds(&self) -> WindowBounds { + self.platform_window.inner_window_bounds() + } + + /// Dispatch the given action on the currently focused element. + pub fn dispatch_action(&mut self, action: Box, cx: &mut App) { + let focus_id = self.focused(cx).map(|handle| handle.id); + + let window = self.handle; + cx.defer(move |cx| { + window + .update(cx, |_, window, cx| { + let node_id = window.focus_node_id_in_rendered_frame(focus_id); + window.dispatch_action_on_node(node_id, action.as_ref(), cx); + }) + .log_err(); + }) + } + + pub(crate) fn dispatch_keystroke_observers( + &mut self, + event: &dyn Any, + action: Option>, + context_stack: Vec, + cx: &mut App, + ) { + let Some(key_down_event) = event.downcast_ref::() else { + return; + }; + + cx.keystroke_observers.clone().retain(&(), move |callback| { + (callback)( + &KeystrokeEvent { + keystroke: key_down_event.keystroke.clone(), + action: action.as_ref().map(|action| action.boxed_clone()), + context_stack: context_stack.clone(), + }, + self, + cx, + ) + }); + } + + pub(crate) fn dispatch_keystroke_interceptors( + &mut self, + event: &dyn Any, + context_stack: Vec, + cx: &mut App, + ) { + let Some(key_down_event) = event.downcast_ref::() else { + return; + }; + + cx.keystroke_interceptors + .clone() + .retain(&(), move |callback| { + (callback)( + &KeystrokeEvent { + keystroke: key_down_event.keystroke.clone(), + action: None, + context_stack: context_stack.clone(), + }, + self, + cx, + ) + }); + } + + /// Schedules the given function to be run at the end of the current effect cycle, allowing entities + /// that are currently on the stack to be returned to the app. + pub fn defer(&self, cx: &mut App, f: impl FnOnce(&mut Window, &mut App) + 'static) { + let handle = self.handle; + cx.defer(move |cx| { + handle.update(cx, |_, window, cx| f(window, cx)).ok(); + }); + } + + /// Subscribe to events emitted by a entity. + /// The entity to which you're subscribing must implement the [`EventEmitter`] trait. + /// The callback will be invoked a handle to the emitting entity, the event, and a window context for the current window. + pub fn observe( + &mut self, + observed: &Entity, + cx: &mut App, + mut on_notify: impl FnMut(Entity, &mut Window, &mut App) + 'static, + ) -> Subscription { + let entity_id = observed.entity_id(); + let observed = observed.downgrade(); + let window_handle = self.handle; + cx.new_observer( + entity_id, + Box::new(move |cx| { + window_handle + .update(cx, |_, window, cx| { + if let Some(handle) = observed.upgrade() { + on_notify(handle, window, cx); + true + } else { + false + } + }) + .unwrap_or(false) + }), + ) + } + + /// Subscribe to events emitted by a entity. + /// The entity to which you're subscribing must implement the [`EventEmitter`] trait. + /// The callback will be invoked a handle to the emitting entity, the event, and a window context for the current window. + pub fn subscribe( + &mut self, + entity: &Entity, + cx: &mut App, + mut on_event: impl FnMut(Entity, &Evt, &mut Window, &mut App) + 'static, + ) -> Subscription + where + Emitter: EventEmitter, + Evt: 'static, + { + let entity_id = entity.entity_id(); + let handle = entity.downgrade(); + let window_handle = self.handle; + cx.new_subscription( + entity_id, + ( + TypeId::of::(), + Box::new(move |event, cx| { + window_handle + .update(cx, |_, window, cx| { + if let Some(entity) = handle.upgrade() { + let event = event.downcast_ref().expect("invalid event type"); + on_event(entity, event, window, cx); + true + } else { + false + } + }) + .unwrap_or(false) + }), + ), + ) + } + + /// Register a callback to be invoked when the given `Entity` is released. + pub fn observe_release( + &self, + entity: &Entity, + cx: &mut App, + mut on_release: impl FnOnce(&mut T, &mut Window, &mut App) + 'static, + ) -> Subscription + where + T: 'static, + { + let entity_id = entity.entity_id(); + let window_handle = self.handle; + let (subscription, activate) = cx.release_listeners.insert( + entity_id, + Box::new(move |entity, cx| { + let entity = entity.downcast_mut().expect("invalid entity type"); + let _ = window_handle.update(cx, |_, window, cx| on_release(entity, window, cx)); + }), + ); + activate(); + subscription + } + + /// Creates an [`AsyncWindowContext`], which has a static lifetime and can be held across + /// await points in async code. + pub fn to_async(&self, cx: &App) -> AsyncWindowContext { + AsyncWindowContext::new_context(cx.to_async(), self.handle) + } + + /// Schedule the given closure to be run directly after the current frame is rendered. + pub fn on_next_frame(&self, callback: impl FnOnce(&mut Window, &mut App) + 'static) { + RefCell::borrow_mut(&self.next_frame_callbacks).push(Box::new(callback)); + } + + /// Schedule a frame to be drawn on the next animation frame. + /// + /// This is useful for elements that need to animate continuously, such as a video player or an animated GIF. + /// It will cause the window to redraw on the next frame, even if no other changes have occurred. + /// + /// If called from within a view, it will notify that view on the next frame. Otherwise, it will refresh the entire window. + pub fn request_animation_frame(&self) { + let entity = self.current_view(); + self.on_next_frame(move |_, cx| cx.notify(entity)); + } + + /// Spawn the future returned by the given closure on the application thread pool. + /// The closure is provided a handle to the current window and an `AsyncWindowContext` for + /// use within your future. + #[track_caller] + pub fn spawn(&self, cx: &App, f: AsyncFn) -> Task + where + R: 'static, + AsyncFn: AsyncFnOnce(&mut AsyncWindowContext) -> R + 'static, + { + let handle = self.handle; + cx.spawn(async move |app| { + let mut async_window_cx = AsyncWindowContext::new_context(app.clone(), handle); + f(&mut async_window_cx).await + }) + } + + fn bounds_changed(&mut self, cx: &mut App) { + self.scale_factor = self.platform_window.scale_factor(); + self.viewport_size = self.platform_window.content_size(); + self.display_id = self.platform_window.display().map(|display| display.id()); + + self.refresh(); + + self.bounds_observers + .clone() + .retain(&(), |callback| callback(self, cx)); + } + + /// Returns the bounds of the current window in the global coordinate space, which could span across multiple displays. + pub fn bounds(&self) -> Bounds { + self.platform_window.bounds() + } + + /// Set the content size of the window. + pub fn resize(&mut self, size: Size) { + self.platform_window.resize(size); + } + + /// Returns whether or not the window is currently fullscreen + pub fn is_fullscreen(&self) -> bool { + self.platform_window.is_fullscreen() + } + + pub(crate) fn appearance_changed(&mut self, cx: &mut App) { + self.appearance = self.platform_window.appearance(); + + self.appearance_observers + .clone() + .retain(&(), |callback| callback(self, cx)); + } + + /// Returns the appearance of the current window. + pub fn appearance(&self) -> WindowAppearance { + self.appearance + } + + /// Returns the size of the drawable area within the window. + pub fn viewport_size(&self) -> Size { + self.viewport_size + } + + /// Returns whether this window is focused by the operating system (receiving key events). + pub fn is_window_active(&self) -> bool { + self.active.get() + } + + /// Returns whether this window is considered to be the window + /// that currently owns the mouse cursor. + /// On mac, this is equivalent to `is_window_active`. + pub fn is_window_hovered(&self) -> bool { + if cfg!(any( + target_os = "windows", + target_os = "linux", + target_os = "freebsd" + )) { + self.hovered.get() + } else { + self.is_window_active() + } + } + + /// Toggle zoom on the window. + pub fn zoom_window(&self) { + self.platform_window.zoom(); + } + + /// Opens the native title bar context menu, useful when implementing client side decorations (Wayland and X11) + pub fn show_window_menu(&self, position: Point) { + self.platform_window.show_window_menu(position) + } + + /// Tells the compositor to take control of window movement (Wayland and X11) + /// + /// Events may not be received during a move operation. + pub fn start_window_move(&self) { + self.platform_window.start_window_move() + } + + /// When using client side decorations, set this to the width of the invisible decorations (Wayland and X11) + pub fn set_client_inset(&mut self, inset: Pixels) { + self.client_inset = Some(inset); + self.platform_window.set_client_inset(inset); + } + + /// Returns the client_inset value by [`Self::set_client_inset`]. + pub fn client_inset(&self) -> Option { + self.client_inset + } + + /// Returns whether the title bar window controls need to be rendered by the application (Wayland and X11) + pub fn window_decorations(&self) -> Decorations { + self.platform_window.window_decorations() + } + + /// Returns which window controls are currently visible (Wayland) + pub fn window_controls(&self) -> WindowControls { + self.platform_window.window_controls() + } + + /// Updates the window's title at the platform level. + pub fn set_window_title(&mut self, title: &str) { + self.platform_window.set_title(title); + } + + /// Sets the application identifier. + pub fn set_app_id(&mut self, app_id: &str) { + self.platform_window.set_app_id(app_id); + } + + /// Sets the window background appearance. + pub fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { + self.platform_window + .set_background_appearance(background_appearance); + } + + /// Mark the window as dirty at the platform level. + pub fn set_window_edited(&mut self, edited: bool) { + self.platform_window.set_edited(edited); + } + + /// Determine the display on which the window is visible. + pub fn display(&self, cx: &App) -> Option> { + cx.platform + .displays() + .into_iter() + .find(|display| Some(display.id()) == self.display_id) + } + + /// Show the platform character palette. + pub fn show_character_palette(&self) { + self.platform_window.show_character_palette(); + } + + /// The scale factor of the display associated with the window. For example, it could + /// return 2.0 for a "retina" display, indicating that each logical pixel should actually + /// be rendered as two pixels on screen. + pub fn scale_factor(&self) -> f32 { + self.scale_factor + } + + /// The size of an em for the base font of the application. Adjusting this value allows the + /// UI to scale, just like zooming a web page. + pub fn rem_size(&self) -> Pixels { + self.rem_size_override_stack + .last() + .copied() + .unwrap_or(self.rem_size) + } + + /// Sets the size of an em for the base font of the application. Adjusting this value allows the + /// UI to scale, just like zooming a web page. + pub fn set_rem_size(&mut self, rem_size: impl Into) { + self.rem_size = rem_size.into(); + } + + /// Acquire a globally unique identifier for the given ElementId. + /// Only valid for the duration of the provided closure. + pub fn with_global_id( + &mut self, + element_id: ElementId, + f: impl FnOnce(&GlobalElementId, &mut Self) -> R, + ) -> R { + self.element_id_stack.push(element_id); + let global_id = GlobalElementId(self.element_id_stack.clone()); + let result = f(&global_id, self); + self.element_id_stack.pop(); + result + } + + /// Executes the provided function with the specified rem size. + /// + /// This method must only be called as part of element drawing. + pub fn with_rem_size(&mut self, rem_size: Option>, f: F) -> R + where + F: FnOnce(&mut Self) -> R, + { + self.invalidator.debug_assert_paint_or_prepaint(); + + if let Some(rem_size) = rem_size { + self.rem_size_override_stack.push(rem_size.into()); + let result = f(self); + self.rem_size_override_stack.pop(); + result + } else { + f(self) + } + } + + /// The line height associated with the current text style. + pub fn line_height(&self) -> Pixels { + self.text_style().line_height_in_pixels(self.rem_size()) + } + + /// Call to prevent the default action of an event. Currently only used to prevent + /// parent elements from becoming focused on mouse down. + pub fn prevent_default(&mut self) { + self.default_prevented = true; + } + + /// Obtain whether default has been prevented for the event currently being dispatched. + pub fn default_prevented(&self) -> bool { + self.default_prevented + } + + /// Determine whether the given action is available along the dispatch path to the currently focused element. + pub fn is_action_available(&self, action: &dyn Action, cx: &mut App) -> bool { + let node_id = + self.focus_node_id_in_rendered_frame(self.focused(cx).map(|handle| handle.id)); + self.rendered_frame + .dispatch_tree + .is_action_available(action, node_id) + } + + /// The position of the mouse relative to the window. + pub fn mouse_position(&self) -> Point { + self.mouse_position + } + + /// The current state of the keyboard's modifiers + pub fn modifiers(&self) -> Modifiers { + self.modifiers + } + + /// The current state of the keyboard's capslock + pub fn capslock(&self) -> Capslock { + self.capslock + } + + fn complete_frame(&self) { + self.platform_window.completed_frame(); + } + + /// Produces a new frame and assigns it to `rendered_frame`. To actually show + /// the contents of the new [`Scene`], use [`Self::present`]. + #[profiling::function] + pub fn draw(&mut self, cx: &mut App) -> ArenaClearNeeded { + self.invalidate_entities(); + cx.entities.clear_accessed(); + debug_assert!(self.rendered_entity_stack.is_empty()); + self.invalidator.set_dirty(false); + self.requested_autoscroll = None; + + // Restore the previously-used input handler. + if let Some(input_handler) = self.platform_window.take_input_handler() { + self.rendered_frame.input_handlers.push(Some(input_handler)); + } + self.draw_roots(cx); + self.dirty_views.clear(); + self.next_frame.window_active = self.active.get(); + + // Register requested input handler with the platform window. + if let Some(input_handler) = self.next_frame.input_handlers.pop() { + self.platform_window + .set_input_handler(input_handler.unwrap()); + } + + self.layout_engine.as_mut().unwrap().clear(); + self.text_system().finish_frame(); + self.next_frame.finish(&mut self.rendered_frame); + + self.invalidator.set_phase(DrawPhase::Focus); + let previous_focus_path = self.rendered_frame.focus_path(); + let previous_window_active = self.rendered_frame.window_active; + mem::swap(&mut self.rendered_frame, &mut self.next_frame); + self.next_frame.clear(); + let current_focus_path = self.rendered_frame.focus_path(); + let current_window_active = self.rendered_frame.window_active; + + if previous_focus_path != current_focus_path + || previous_window_active != current_window_active + { + if !previous_focus_path.is_empty() && current_focus_path.is_empty() { + self.focus_lost_listeners + .clone() + .retain(&(), |listener| listener(self, cx)); + } + + let event = WindowFocusEvent { + previous_focus_path: if previous_window_active { + previous_focus_path + } else { + Default::default() + }, + current_focus_path: if current_window_active { + current_focus_path + } else { + Default::default() + }, + }; + self.focus_listeners + .clone() + .retain(&(), |listener| listener(&event, self, cx)); + } + + debug_assert!(self.rendered_entity_stack.is_empty()); + self.record_entities_accessed(cx); + self.reset_cursor_style(cx); + self.refreshing = false; + self.invalidator.set_phase(DrawPhase::None); + self.needs_present.set(true); + + ArenaClearNeeded + } + + fn record_entities_accessed(&mut self, cx: &mut App) { + let mut entities_ref = cx.entities.accessed_entities.borrow_mut(); + let mut entities = mem::take(entities_ref.deref_mut()); + drop(entities_ref); + let handle = self.handle; + cx.record_entities_accessed( + handle, + // Try moving window invalidator into the Window + self.invalidator.clone(), + &entities, + ); + let mut entities_ref = cx.entities.accessed_entities.borrow_mut(); + mem::swap(&mut entities, entities_ref.deref_mut()); + } + + fn invalidate_entities(&mut self) { + let mut views = self.invalidator.take_views(); + for entity in views.drain() { + self.mark_view_dirty(entity); + } + self.invalidator.replace_views(views); + } + + #[profiling::function] + fn present(&self) { + self.platform_window.draw(&self.rendered_frame.scene); + self.needs_present.set(false); + profiling::finish_frame!(); + } + + fn draw_roots(&mut self, cx: &mut App) { + self.invalidator.set_phase(DrawPhase::Prepaint); + self.tooltip_bounds.take(); + + let _inspector_width: Pixels = rems(30.0).to_pixels(self.rem_size()); + let root_size = { + #[cfg(any(feature = "inspector", debug_assertions))] + { + if self.inspector.is_some() { + let mut size = self.viewport_size; + size.width = (size.width - _inspector_width).max(px(0.0)); + size + } else { + self.viewport_size + } + } + #[cfg(not(any(feature = "inspector", debug_assertions)))] + { + self.viewport_size + } + }; + + // Layout all root elements. + let mut root_element = self.root.as_ref().unwrap().clone().into_any(); + root_element.prepaint_as_root(Point::default(), root_size.into(), self, cx); + + #[cfg(any(feature = "inspector", debug_assertions))] + let inspector_element = self.prepaint_inspector(_inspector_width, cx); + + let mut sorted_deferred_draws = + (0..self.next_frame.deferred_draws.len()).collect::>(); + sorted_deferred_draws.sort_by_key(|ix| self.next_frame.deferred_draws[*ix].priority); + self.prepaint_deferred_draws(&sorted_deferred_draws, cx); + + let mut prompt_element = None; + let mut active_drag_element = None; + let mut tooltip_element = None; + if let Some(prompt) = self.prompt.take() { + let mut element = prompt.view.any_view().into_any(); + element.prepaint_as_root(Point::default(), root_size.into(), self, cx); + prompt_element = Some(element); + self.prompt = Some(prompt); + } else if let Some(active_drag) = cx.active_drag.take() { + let mut element = active_drag.view.clone().into_any(); + let offset = self.mouse_position() - active_drag.cursor_offset; + element.prepaint_as_root(offset, AvailableSpace::min_size(), self, cx); + active_drag_element = Some(element); + cx.active_drag = Some(active_drag); + } else { + tooltip_element = self.prepaint_tooltip(cx); + } + + self.mouse_hit_test = self.next_frame.hit_test(self.mouse_position); + + // Now actually paint the elements. + self.invalidator.set_phase(DrawPhase::Paint); + root_element.paint(self, cx); + + #[cfg(any(feature = "inspector", debug_assertions))] + self.paint_inspector(inspector_element, cx); + + self.paint_deferred_draws(&sorted_deferred_draws, cx); + + if let Some(mut prompt_element) = prompt_element { + prompt_element.paint(self, cx); + } else if let Some(mut drag_element) = active_drag_element { + drag_element.paint(self, cx); + } else if let Some(mut tooltip_element) = tooltip_element { + tooltip_element.paint(self, cx); + } + + #[cfg(any(feature = "inspector", debug_assertions))] + self.paint_inspector_hitbox(cx); + } + + fn prepaint_tooltip(&mut self, cx: &mut App) -> Option { + // Use indexing instead of iteration to avoid borrowing self for the duration of the loop. + for tooltip_request_index in (0..self.next_frame.tooltip_requests.len()).rev() { + let Some(Some(tooltip_request)) = self + .next_frame + .tooltip_requests + .get(tooltip_request_index) + .cloned() + else { + log::error!("Unexpectedly absent TooltipRequest"); + continue; + }; + let mut element = tooltip_request.tooltip.view.clone().into_any(); + let mouse_position = tooltip_request.tooltip.mouse_position; + let tooltip_size = element.layout_as_root(AvailableSpace::min_size(), self, cx); + + let mut tooltip_bounds = + Bounds::new(mouse_position + point(px(1.), px(1.)), tooltip_size); + let window_bounds = Bounds { + origin: Point::default(), + size: self.viewport_size(), + }; + + if tooltip_bounds.right() > window_bounds.right() { + let new_x = mouse_position.x - tooltip_bounds.size.width - px(1.); + if new_x >= Pixels::ZERO { + tooltip_bounds.origin.x = new_x; + } else { + tooltip_bounds.origin.x = cmp::max( + Pixels::ZERO, + tooltip_bounds.origin.x - tooltip_bounds.right() - window_bounds.right(), + ); + } + } + + if tooltip_bounds.bottom() > window_bounds.bottom() { + let new_y = mouse_position.y - tooltip_bounds.size.height - px(1.); + if new_y >= Pixels::ZERO { + tooltip_bounds.origin.y = new_y; + } else { + tooltip_bounds.origin.y = cmp::max( + Pixels::ZERO, + tooltip_bounds.origin.y - tooltip_bounds.bottom() - window_bounds.bottom(), + ); + } + } + + // It's possible for an element to have an active tooltip while not being painted (e.g. + // via the `visible_on_hover` method). Since mouse listeners are not active in this + // case, instead update the tooltip's visibility here. + let is_visible = + (tooltip_request.tooltip.check_visible_and_update)(tooltip_bounds, self, cx); + if !is_visible { + continue; + } + + self.with_absolute_element_offset(tooltip_bounds.origin, |window| { + element.prepaint(window, cx) + }); + + self.tooltip_bounds = Some(TooltipBounds { + id: tooltip_request.id, + bounds: tooltip_bounds, + }); + return Some(element); + } + None + } + + fn prepaint_deferred_draws(&mut self, deferred_draw_indices: &[usize], cx: &mut App) { + assert_eq!(self.element_id_stack.len(), 0); + + let mut deferred_draws = mem::take(&mut self.next_frame.deferred_draws); + for deferred_draw_ix in deferred_draw_indices { + let deferred_draw = &mut deferred_draws[*deferred_draw_ix]; + self.element_id_stack + .clone_from(&deferred_draw.element_id_stack); + self.text_style_stack + .clone_from(&deferred_draw.text_style_stack); + self.next_frame + .dispatch_tree + .set_active_node(deferred_draw.parent_node); + + let prepaint_start = self.prepaint_index(); + if let Some(element) = deferred_draw.element.as_mut() { + self.with_rendered_view(deferred_draw.current_view, |window| { + window.with_absolute_element_offset(deferred_draw.absolute_offset, |window| { + element.prepaint(window, cx) + }); + }) + } else { + self.reuse_prepaint(deferred_draw.prepaint_range.clone()); + } + let prepaint_end = self.prepaint_index(); + deferred_draw.prepaint_range = prepaint_start..prepaint_end; + } + assert_eq!( + self.next_frame.deferred_draws.len(), + 0, + "cannot call defer_draw during deferred drawing" + ); + self.next_frame.deferred_draws = deferred_draws; + self.element_id_stack.clear(); + self.text_style_stack.clear(); + } + + fn paint_deferred_draws(&mut self, deferred_draw_indices: &[usize], cx: &mut App) { + assert_eq!(self.element_id_stack.len(), 0); + + let mut deferred_draws = mem::take(&mut self.next_frame.deferred_draws); + for deferred_draw_ix in deferred_draw_indices { + let mut deferred_draw = &mut deferred_draws[*deferred_draw_ix]; + self.element_id_stack + .clone_from(&deferred_draw.element_id_stack); + self.next_frame + .dispatch_tree + .set_active_node(deferred_draw.parent_node); + + let paint_start = self.paint_index(); + if let Some(element) = deferred_draw.element.as_mut() { + self.with_rendered_view(deferred_draw.current_view, |window| { + element.paint(window, cx); + }) + } else { + self.reuse_paint(deferred_draw.paint_range.clone()); + } + let paint_end = self.paint_index(); + deferred_draw.paint_range = paint_start..paint_end; + } + self.next_frame.deferred_draws = deferred_draws; + self.element_id_stack.clear(); + } + + pub(crate) fn prepaint_index(&self) -> PrepaintStateIndex { + PrepaintStateIndex { + hitboxes_index: self.next_frame.hitboxes.len(), + tooltips_index: self.next_frame.tooltip_requests.len(), + deferred_draws_index: self.next_frame.deferred_draws.len(), + dispatch_tree_index: self.next_frame.dispatch_tree.len(), + accessed_element_states_index: self.next_frame.accessed_element_states.len(), + line_layout_index: self.text_system.layout_index(), + } + } + + pub(crate) fn reuse_prepaint(&mut self, range: Range) { + self.next_frame.hitboxes.extend( + self.rendered_frame.hitboxes[range.start.hitboxes_index..range.end.hitboxes_index] + .iter() + .cloned(), + ); + self.next_frame.tooltip_requests.extend( + self.rendered_frame.tooltip_requests + [range.start.tooltips_index..range.end.tooltips_index] + .iter_mut() + .map(|request| request.take()), + ); + self.next_frame.accessed_element_states.extend( + self.rendered_frame.accessed_element_states[range.start.accessed_element_states_index + ..range.end.accessed_element_states_index] + .iter() + .map(|(id, type_id)| (GlobalElementId(id.0.clone()), *type_id)), + ); + self.text_system + .reuse_layouts(range.start.line_layout_index..range.end.line_layout_index); + + let reused_subtree = self.next_frame.dispatch_tree.reuse_subtree( + range.start.dispatch_tree_index..range.end.dispatch_tree_index, + &mut self.rendered_frame.dispatch_tree, + self.focus, + ); + + if reused_subtree.contains_focus() { + self.next_frame.focus = self.focus; + } + + self.next_frame.deferred_draws.extend( + self.rendered_frame.deferred_draws + [range.start.deferred_draws_index..range.end.deferred_draws_index] + .iter() + .map(|deferred_draw| DeferredDraw { + current_view: deferred_draw.current_view, + parent_node: reused_subtree.refresh_node_id(deferred_draw.parent_node), + element_id_stack: deferred_draw.element_id_stack.clone(), + text_style_stack: deferred_draw.text_style_stack.clone(), + priority: deferred_draw.priority, + element: None, + absolute_offset: deferred_draw.absolute_offset, + prepaint_range: deferred_draw.prepaint_range.clone(), + paint_range: deferred_draw.paint_range.clone(), + }), + ); + } + + pub(crate) fn paint_index(&self) -> PaintIndex { + PaintIndex { + scene_index: self.next_frame.scene.len(), + mouse_listeners_index: self.next_frame.mouse_listeners.len(), + input_handlers_index: self.next_frame.input_handlers.len(), + cursor_styles_index: self.next_frame.cursor_styles.len(), + accessed_element_states_index: self.next_frame.accessed_element_states.len(), + tab_handle_index: self.next_frame.tab_stops.paint_index(), + line_layout_index: self.text_system.layout_index(), + } + } + + pub(crate) fn reuse_paint(&mut self, range: Range) { + self.next_frame.cursor_styles.extend( + self.rendered_frame.cursor_styles + [range.start.cursor_styles_index..range.end.cursor_styles_index] + .iter() + .cloned(), + ); + self.next_frame.input_handlers.extend( + self.rendered_frame.input_handlers + [range.start.input_handlers_index..range.end.input_handlers_index] + .iter_mut() + .map(|handler| handler.take()), + ); + self.next_frame.mouse_listeners.extend( + self.rendered_frame.mouse_listeners + [range.start.mouse_listeners_index..range.end.mouse_listeners_index] + .iter_mut() + .map(|listener| listener.take()), + ); + self.next_frame.accessed_element_states.extend( + self.rendered_frame.accessed_element_states[range.start.accessed_element_states_index + ..range.end.accessed_element_states_index] + .iter() + .map(|(id, type_id)| (GlobalElementId(id.0.clone()), *type_id)), + ); + self.next_frame.tab_stops.replay( + &self.rendered_frame.tab_stops.insertion_history + [range.start.tab_handle_index..range.end.tab_handle_index], + ); + + self.text_system + .reuse_layouts(range.start.line_layout_index..range.end.line_layout_index); + self.next_frame.scene.replay( + range.start.scene_index..range.end.scene_index, + &self.rendered_frame.scene, + ); + } + + /// Push a text style onto the stack, and call a function with that style active. + /// Use [`Window::text_style`] to get the current, combined text style. This method + /// should only be called as part of element drawing. + pub fn with_text_style(&mut self, style: Option, f: F) -> R + where + F: FnOnce(&mut Self) -> R, + { + self.invalidator.debug_assert_paint_or_prepaint(); + if let Some(style) = style { + self.text_style_stack.push(style); + let result = f(self); + self.text_style_stack.pop(); + result + } else { + f(self) + } + } + + /// Updates the cursor style at the platform level. This method should only be called + /// during the prepaint phase of element drawing. + pub fn set_cursor_style(&mut self, style: CursorStyle, hitbox: &Hitbox) { + self.invalidator.debug_assert_paint(); + self.next_frame.cursor_styles.push(CursorStyleRequest { + hitbox_id: Some(hitbox.id), + style, + }); + } + + /// Updates the cursor style for the entire window at the platform level. A cursor + /// style using this method will have precedence over any cursor style set using + /// `set_cursor_style`. This method should only be called during the prepaint + /// phase of element drawing. + pub fn set_window_cursor_style(&mut self, style: CursorStyle) { + self.invalidator.debug_assert_paint(); + self.next_frame.cursor_styles.push(CursorStyleRequest { + hitbox_id: None, + style, + }) + } + + /// Sets a tooltip to be rendered for the upcoming frame. This method should only be called + /// during the paint phase of element drawing. + pub fn set_tooltip(&mut self, tooltip: AnyTooltip) -> TooltipId { + self.invalidator.debug_assert_prepaint(); + let id = TooltipId(post_inc(&mut self.next_tooltip_id.0)); + self.next_frame + .tooltip_requests + .push(Some(TooltipRequest { id, tooltip })); + id + } + + /// Invoke the given function with the given content mask after intersecting it + /// with the current mask. This method should only be called during element drawing. + pub fn with_content_mask( + &mut self, + mask: Option>, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + self.invalidator.debug_assert_paint_or_prepaint(); + if let Some(mask) = mask { + let mask = mask.intersect(&self.content_mask()); + self.content_mask_stack.push(mask); + let result = f(self); + self.content_mask_stack.pop(); + result + } else { + f(self) + } + } + + /// Updates the global element offset relative to the current offset. This is used to implement + /// scrolling. This method should only be called during the prepaint phase of element drawing. + pub fn with_element_offset( + &mut self, + offset: Point, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + self.invalidator.debug_assert_prepaint(); + + if offset.is_zero() { + return f(self); + }; + + let abs_offset = self.element_offset() + offset; + self.with_absolute_element_offset(abs_offset, f) + } + + /// Updates the global element offset based on the given offset. This is used to implement + /// drag handles and other manual painting of elements. This method should only be called during + /// the prepaint phase of element drawing. + pub fn with_absolute_element_offset( + &mut self, + offset: Point, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + self.invalidator.debug_assert_prepaint(); + self.element_offset_stack.push(offset); + let result = f(self); + self.element_offset_stack.pop(); + result + } + + pub(crate) fn with_element_opacity( + &mut self, + opacity: Option, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + self.invalidator.debug_assert_paint_or_prepaint(); + + let Some(opacity) = opacity else { + return f(self); + }; + + let previous_opacity = self.element_opacity; + self.element_opacity = previous_opacity * opacity; + let result = f(self); + self.element_opacity = previous_opacity; + result + } + + /// Perform prepaint on child elements in a "retryable" manner, so that any side effects + /// of prepaints can be discarded before prepainting again. This is used to support autoscroll + /// where we need to prepaint children to detect the autoscroll bounds, then adjust the + /// element offset and prepaint again. See [`crate::List`] for an example. This method should only be + /// called during the prepaint phase of element drawing. + pub fn transact(&mut self, f: impl FnOnce(&mut Self) -> Result) -> Result { + self.invalidator.debug_assert_prepaint(); + let index = self.prepaint_index(); + let result = f(self); + if result.is_err() { + self.next_frame.hitboxes.truncate(index.hitboxes_index); + self.next_frame + .tooltip_requests + .truncate(index.tooltips_index); + self.next_frame + .deferred_draws + .truncate(index.deferred_draws_index); + self.next_frame + .dispatch_tree + .truncate(index.dispatch_tree_index); + self.next_frame + .accessed_element_states + .truncate(index.accessed_element_states_index); + self.text_system.truncate_layouts(index.line_layout_index); + } + result + } + + /// When you call this method during [`Element::prepaint`], containing elements will attempt to + /// scroll to cause the specified bounds to become visible. When they decide to autoscroll, they will call + /// [`Element::prepaint`] again with a new set of bounds. See [`crate::List`] for an example of an element + /// that supports this method being called on the elements it contains. This method should only be + /// called during the prepaint phase of element drawing. + pub fn request_autoscroll(&mut self, bounds: Bounds) { + self.invalidator.debug_assert_prepaint(); + self.requested_autoscroll = Some(bounds); + } + + /// This method can be called from a containing element such as [`crate::List`] to support the autoscroll behavior + /// described in [`Self::request_autoscroll`]. + pub fn take_autoscroll(&mut self) -> Option> { + self.invalidator.debug_assert_prepaint(); + self.requested_autoscroll.take() + } + + /// Asynchronously load an asset, if the asset hasn't finished loading this will return None. + /// Your view will be re-drawn once the asset has finished loading. + /// + /// Note that the multiple calls to this method will only result in one `Asset::load` call at a + /// time. + pub fn use_asset(&mut self, source: &A::Source, cx: &mut App) -> Option { + let (task, is_first) = cx.fetch_asset::(source); + task.clone().now_or_never().or_else(|| { + if is_first { + let entity_id = self.current_view(); + self.spawn(cx, { + let task = task.clone(); + async move |cx| { + task.await; + + cx.on_next_frame(move |_, cx| { + cx.notify(entity_id); + }); + } + }) + .detach(); + } + + None + }) + } + + /// Asynchronously load an asset, if the asset hasn't finished loading or doesn't exist this will return None. + /// Your view will not be re-drawn once the asset has finished loading. + /// + /// Note that the multiple calls to this method will only result in one `Asset::load` call at a + /// time. + pub fn get_asset(&mut self, source: &A::Source, cx: &mut App) -> Option { + let (task, _) = cx.fetch_asset::(source); + task.now_or_never() + } + /// Obtain the current element offset. This method should only be called during the + /// prepaint phase of element drawing. + pub fn element_offset(&self) -> Point { + self.invalidator.debug_assert_prepaint(); + self.element_offset_stack + .last() + .copied() + .unwrap_or_default() + } + + /// Obtain the current element opacity. This method should only be called during the + /// prepaint phase of element drawing. + #[inline] + pub(crate) fn element_opacity(&self) -> f32 { + self.invalidator.debug_assert_paint_or_prepaint(); + self.element_opacity + } + + /// Obtain the current content mask. This method should only be called during element drawing. + pub fn content_mask(&self) -> ContentMask { + self.invalidator.debug_assert_paint_or_prepaint(); + self.content_mask_stack + .last() + .cloned() + .unwrap_or_else(|| ContentMask { + bounds: Bounds { + origin: Point::default(), + size: self.viewport_size, + }, + }) + } + + /// Provide elements in the called function with a new namespace in which their identifiers must be unique. + /// This can be used within a custom element to distinguish multiple sets of child elements. + pub fn with_element_namespace( + &mut self, + element_id: impl Into, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + self.element_id_stack.push(element_id.into()); + let result = f(self); + self.element_id_stack.pop(); + result + } + + /// Use a piece of state that exists as long this element is being rendered in consecutive frames. + pub fn use_keyed_state( + &mut self, + key: impl Into, + cx: &mut App, + init: impl FnOnce(&mut Self, &mut Context) -> S, + ) -> Entity { + let current_view = self.current_view(); + self.with_global_id(key.into(), |global_id, window| { + window.with_element_state(global_id, |state: Option>, window| { + if let Some(state) = state { + (state.clone(), state) + } else { + let new_state = cx.new(|cx| init(window, cx)); + cx.observe(&new_state, move |_, cx| { + cx.notify(current_view); + }) + .detach(); + (new_state.clone(), new_state) + } + }) + }) + } + + /// Immediately push an element ID onto the stack. Useful for simplifying IDs in lists + pub fn with_id(&mut self, id: impl Into, f: impl FnOnce(&mut Self) -> R) -> R { + self.with_global_id(id.into(), |_, window| f(window)) + } + + /// Use a piece of state that exists as long this element is being rendered in consecutive frames, without needing to specify a key + /// + /// NOTE: This method uses the location of the caller to generate an ID for this state. + /// If this is not sufficient to identify your state (e.g. you're rendering a list item), + /// you can provide a custom ElementID using the `use_keyed_state` method. + #[track_caller] + pub fn use_state( + &mut self, + cx: &mut App, + init: impl FnOnce(&mut Self, &mut Context) -> S, + ) -> Entity { + self.use_keyed_state( + ElementId::CodeLocation(*core::panic::Location::caller()), + cx, + init, + ) + } + + /// Updates or initializes state for an element with the given id that lives across multiple + /// frames. If an element with this ID existed in the rendered frame, its state will be passed + /// to the given closure. The state returned by the closure will be stored so it can be referenced + /// when drawing the next frame. This method should only be called as part of element drawing. + pub fn with_element_state( + &mut self, + global_id: &GlobalElementId, + f: impl FnOnce(Option, &mut Self) -> (R, S), + ) -> R + where + S: 'static, + { + self.invalidator.debug_assert_paint_or_prepaint(); + + let key = (GlobalElementId(global_id.0.clone()), TypeId::of::()); + self.next_frame + .accessed_element_states + .push((GlobalElementId(key.0.clone()), TypeId::of::())); + + if let Some(any) = self + .next_frame + .element_states + .remove(&key) + .or_else(|| self.rendered_frame.element_states.remove(&key)) + { + let ElementStateBox { + inner, + #[cfg(debug_assertions)] + type_name, + } = any; + // Using the extra inner option to avoid needing to reallocate a new box. + let mut state_box = inner + .downcast::>() + .map_err(|_| { + #[cfg(debug_assertions)] + { + anyhow::anyhow!( + "invalid element state type for id, requested {:?}, actual: {:?}", + std::any::type_name::(), + type_name + ) + } + + #[cfg(not(debug_assertions))] + { + anyhow::anyhow!( + "invalid element state type for id, requested {:?}", + std::any::type_name::(), + ) + } + }) + .unwrap(); + + let state = state_box.take().expect( + "reentrant call to with_element_state for the same state type and element id", + ); + let (result, state) = f(Some(state), self); + state_box.replace(state); + self.next_frame.element_states.insert( + key, + ElementStateBox { + inner: state_box, + #[cfg(debug_assertions)] + type_name, + }, + ); + result + } else { + let (result, state) = f(None, self); + self.next_frame.element_states.insert( + key, + ElementStateBox { + inner: Box::new(Some(state)), + #[cfg(debug_assertions)] + type_name: std::any::type_name::(), + }, + ); + result + } + } + + /// A variant of `with_element_state` that allows the element's id to be optional. This is a convenience + /// method for elements where the element id may or may not be assigned. Prefer using `with_element_state` + /// when the element is guaranteed to have an id. + /// + /// The first option means 'no ID provided' + /// The second option means 'not yet initialized' + pub fn with_optional_element_state( + &mut self, + global_id: Option<&GlobalElementId>, + f: impl FnOnce(Option>, &mut Self) -> (R, Option), + ) -> R + where + S: 'static, + { + self.invalidator.debug_assert_paint_or_prepaint(); + + if let Some(global_id) = global_id { + self.with_element_state(global_id, |state, cx| { + let (result, state) = f(Some(state), cx); + let state = + state.expect("you must return some state when you pass some element id"); + (result, state) + }) + } else { + let (result, state) = f(None, self); + debug_assert!( + state.is_none(), + "you must not return an element state when passing None for the global id" + ); + result + } + } + + /// Executes the given closure within the context of a tab group. + #[inline] + pub fn with_tab_group(&mut self, index: Option, f: impl FnOnce(&mut Self) -> R) -> R { + if let Some(index) = index { + self.next_frame.tab_stops.begin_group(index); + let result = f(self); + self.next_frame.tab_stops.end_group(); + result + } else { + f(self) + } + } + + /// Defers the drawing of the given element, scheduling it to be painted on top of the currently-drawn tree + /// at a later time. The `priority` parameter determines the drawing order relative to other deferred elements, + /// with higher values being drawn on top. + /// + /// This method should only be called as part of the prepaint phase of element drawing. + pub fn defer_draw( + &mut self, + element: AnyElement, + absolute_offset: Point, + priority: usize, + ) { + self.invalidator.debug_assert_prepaint(); + let parent_node = self.next_frame.dispatch_tree.active_node_id().unwrap(); + self.next_frame.deferred_draws.push(DeferredDraw { + current_view: self.current_view(), + parent_node, + element_id_stack: self.element_id_stack.clone(), + text_style_stack: self.text_style_stack.clone(), + priority, + element: Some(element), + absolute_offset, + prepaint_range: PrepaintStateIndex::default()..PrepaintStateIndex::default(), + paint_range: PaintIndex::default()..PaintIndex::default(), + }); + } + + /// Creates a new painting layer for the specified bounds. A "layer" is a batch + /// of geometry that are non-overlapping and have the same draw order. This is typically used + /// for performance reasons. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_layer(&mut self, bounds: Bounds, f: impl FnOnce(&mut Self) -> R) -> R { + self.invalidator.debug_assert_paint(); + + let scale_factor = self.scale_factor(); + let content_mask = self.content_mask(); + let clipped_bounds = bounds.intersect(&content_mask.bounds); + if !clipped_bounds.is_empty() { + self.next_frame + .scene + .push_layer(clipped_bounds.scale(scale_factor)); + } + + let result = f(self); + + if !clipped_bounds.is_empty() { + self.next_frame.scene.pop_layer(); + } + + result + } + + /// Paint one or more drop shadows into the scene for the next frame at the current z-index. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_shadows( + &mut self, + bounds: Bounds, + corner_radii: Corners, + shadows: &[BoxShadow], + ) { + self.invalidator.debug_assert_paint(); + + let scale_factor = self.scale_factor(); + let content_mask = self.content_mask(); + let opacity = self.element_opacity(); + for shadow in shadows { + let shadow_bounds = (bounds + shadow.offset).dilate(shadow.spread_radius); + self.next_frame.scene.insert_primitive(Shadow { + order: 0, + blur_radius: shadow.blur_radius.scale(scale_factor), + bounds: shadow_bounds.scale(scale_factor), + content_mask: content_mask.scale(scale_factor), + corner_radii: corner_radii.scale(scale_factor), + color: shadow.color.opacity(opacity), + }); + } + } + + /// Paint one or more quads into the scene for the next frame at the current stacking context. + /// Quads are colored rectangular regions with an optional background, border, and corner radius. + /// see [`fill`], [`outline`], and [`quad`] to construct this type. + /// + /// This method should only be called as part of the paint phase of element drawing. + /// + /// Note that the `quad.corner_radii` are allowed to exceed the bounds, creating sharp corners + /// where the circular arcs meet. This will not display well when combined with dashed borders. + /// Use `Corners::clamp_radii_for_quad_size` if the radii should fit within the bounds. + pub fn paint_quad(&mut self, quad: PaintQuad) { + self.invalidator.debug_assert_paint(); + + let scale_factor = self.scale_factor(); + let content_mask = self.content_mask(); + let opacity = self.element_opacity(); + self.next_frame.scene.insert_primitive(Quad { + order: 0, + bounds: quad.bounds.scale(scale_factor), + content_mask: content_mask.scale(scale_factor), + background: quad.background.opacity(opacity), + border_color: quad.border_color.opacity(opacity), + corner_radii: quad.corner_radii.scale(scale_factor), + border_widths: quad.border_widths.scale(scale_factor), + border_style: quad.border_style, + }); + } + + /// Paint the given `Path` into the scene for the next frame at the current z-index. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_path(&mut self, mut path: Path, color: impl Into) { + self.invalidator.debug_assert_paint(); + + let scale_factor = self.scale_factor(); + let content_mask = self.content_mask(); + let opacity = self.element_opacity(); + path.content_mask = content_mask; + let color: Background = color.into(); + path.color = color.opacity(opacity); + self.next_frame + .scene + .insert_primitive(path.scale(scale_factor)); + } + + /// Paint an underline into the scene for the next frame at the current z-index. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_underline( + &mut self, + origin: Point, + width: Pixels, + style: &UnderlineStyle, + ) { + self.invalidator.debug_assert_paint(); + + let scale_factor = self.scale_factor(); + let height = if style.wavy { + style.thickness * 3. + } else { + style.thickness + }; + let bounds = Bounds { + origin, + size: size(width, height), + }; + let content_mask = self.content_mask(); + let element_opacity = self.element_opacity(); + + self.next_frame.scene.insert_primitive(Underline { + order: 0, + pad: 0, + bounds: bounds.scale(scale_factor), + content_mask: content_mask.scale(scale_factor), + color: style.color.unwrap_or_default().opacity(element_opacity), + thickness: style.thickness.scale(scale_factor), + wavy: if style.wavy { 1 } else { 0 }, + }); + } + + /// Paint a strikethrough into the scene for the next frame at the current z-index. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_strikethrough( + &mut self, + origin: Point, + width: Pixels, + style: &StrikethroughStyle, + ) { + self.invalidator.debug_assert_paint(); + + let scale_factor = self.scale_factor(); + let height = style.thickness; + let bounds = Bounds { + origin, + size: size(width, height), + }; + let content_mask = self.content_mask(); + let opacity = self.element_opacity(); + + self.next_frame.scene.insert_primitive(Underline { + order: 0, + pad: 0, + bounds: bounds.scale(scale_factor), + content_mask: content_mask.scale(scale_factor), + thickness: style.thickness.scale(scale_factor), + color: style.color.unwrap_or_default().opacity(opacity), + wavy: 0, + }); + } + + /// Paints a monochrome (non-emoji) glyph into the scene for the next frame at the current z-index. + /// + /// The y component of the origin is the baseline of the glyph. + /// You should generally prefer to use the [`ShapedLine::paint`](crate::ShapedLine::paint) or + /// [`WrappedLine::paint`](crate::WrappedLine::paint) methods in the [`TextSystem`](crate::TextSystem). + /// This method is only useful if you need to paint a single glyph that has already been shaped. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_glyph( + &mut self, + origin: Point, + font_id: FontId, + glyph_id: GlyphId, + font_size: Pixels, + color: Hsla, + ) -> Result<()> { + self.invalidator.debug_assert_paint(); + + let element_opacity = self.element_opacity(); + let scale_factor = self.scale_factor(); + let glyph_origin = origin.scale(scale_factor); + + let subpixel_variant = Point { + x: (glyph_origin.x.0.fract() * SUBPIXEL_VARIANTS_X as f32).floor() as u8, + y: (glyph_origin.y.0.fract() * SUBPIXEL_VARIANTS_Y as f32).floor() as u8, + }; + let params = RenderGlyphParams { + font_id, + glyph_id, + font_size, + subpixel_variant, + scale_factor, + is_emoji: false, + }; + + let raster_bounds = self.text_system().raster_bounds(¶ms)?; + if !raster_bounds.is_zero() { + let tile = self + .sprite_atlas + .get_or_insert_with(¶ms.clone().into(), &mut || { + let (size, bytes) = self.text_system().rasterize_glyph(¶ms)?; + Ok(Some((size, Cow::Owned(bytes)))) + })? + .expect("Callback above only errors or returns Some"); + let bounds = Bounds { + origin: glyph_origin.map(|px| px.floor()) + raster_bounds.origin.map(Into::into), + size: tile.bounds.size.map(Into::into), + }; + let content_mask = self.content_mask().scale(scale_factor); + self.next_frame.scene.insert_primitive(MonochromeSprite { + order: 0, + pad: 0, + bounds, + content_mask, + color: color.opacity(element_opacity), + tile, + transformation: TransformationMatrix::unit(), + }); + } + Ok(()) + } + + /// Paints an emoji glyph into the scene for the next frame at the current z-index. + /// + /// The y component of the origin is the baseline of the glyph. + /// You should generally prefer to use the [`ShapedLine::paint`](crate::ShapedLine::paint) or + /// [`WrappedLine::paint`](crate::WrappedLine::paint) methods in the [`TextSystem`](crate::TextSystem). + /// This method is only useful if you need to paint a single emoji that has already been shaped. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_emoji( + &mut self, + origin: Point, + font_id: FontId, + glyph_id: GlyphId, + font_size: Pixels, + ) -> Result<()> { + self.invalidator.debug_assert_paint(); + + let scale_factor = self.scale_factor(); + let glyph_origin = origin.scale(scale_factor); + let params = RenderGlyphParams { + font_id, + glyph_id, + font_size, + // We don't render emojis with subpixel variants. + subpixel_variant: Default::default(), + scale_factor, + is_emoji: true, + }; + + let raster_bounds = self.text_system().raster_bounds(¶ms)?; + if !raster_bounds.is_zero() { + let tile = self + .sprite_atlas + .get_or_insert_with(¶ms.clone().into(), &mut || { + let (size, bytes) = self.text_system().rasterize_glyph(¶ms)?; + Ok(Some((size, Cow::Owned(bytes)))) + })? + .expect("Callback above only errors or returns Some"); + + let bounds = Bounds { + origin: glyph_origin.map(|px| px.floor()) + raster_bounds.origin.map(Into::into), + size: tile.bounds.size.map(Into::into), + }; + let content_mask = self.content_mask().scale(scale_factor); + let opacity = self.element_opacity(); + + self.next_frame.scene.insert_primitive(PolychromeSprite { + order: 0, + pad: 0, + grayscale: false, + bounds, + corner_radii: Default::default(), + content_mask, + tile, + opacity, + }); + } + Ok(()) + } + + /// Paint a monochrome SVG into the scene for the next frame at the current stacking context. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_svg( + &mut self, + bounds: Bounds, + path: SharedString, + transformation: TransformationMatrix, + color: Hsla, + cx: &App, + ) -> Result<()> { + self.invalidator.debug_assert_paint(); + + let element_opacity = self.element_opacity(); + let scale_factor = self.scale_factor(); + + let bounds = bounds.scale(scale_factor); + let params = RenderSvgParams { + path, + size: bounds.size.map(|pixels| { + DevicePixels::from((pixels.0 * SMOOTH_SVG_SCALE_FACTOR).ceil() as i32) + }), + }; + + let Some(tile) = + self.sprite_atlas + .get_or_insert_with(¶ms.clone().into(), &mut || { + let Some((size, bytes)) = cx.svg_renderer.render(¶ms)? else { + return Ok(None); + }; + Ok(Some((size, Cow::Owned(bytes)))) + })? + else { + return Ok(()); + }; + let content_mask = self.content_mask().scale(scale_factor); + let svg_bounds = Bounds { + origin: bounds.center() + - Point::new( + ScaledPixels(tile.bounds.size.width.0 as f32 / SMOOTH_SVG_SCALE_FACTOR / 2.), + ScaledPixels(tile.bounds.size.height.0 as f32 / SMOOTH_SVG_SCALE_FACTOR / 2.), + ), + size: tile + .bounds + .size + .map(|value| ScaledPixels(value.0 as f32 / SMOOTH_SVG_SCALE_FACTOR)), + }; + + self.next_frame.scene.insert_primitive(MonochromeSprite { + order: 0, + pad: 0, + bounds: svg_bounds + .map_origin(|origin| origin.round()) + .map_size(|size| size.ceil()), + content_mask, + color: color.opacity(element_opacity), + tile, + transformation, + }); + + Ok(()) + } + + /// Paint an image into the scene for the next frame at the current z-index. + /// This method will panic if the frame_index is not valid + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_image( + &mut self, + bounds: Bounds, + corner_radii: Corners, + data: Arc, + frame_index: usize, + grayscale: bool, + ) -> Result<()> { + self.invalidator.debug_assert_paint(); + + let scale_factor = self.scale_factor(); + let bounds = bounds.scale(scale_factor); + let params = RenderImageParams { + image_id: data.id, + frame_index, + }; + + let tile = self + .sprite_atlas + .get_or_insert_with(¶ms.into(), &mut || { + Ok(Some(( + data.size(frame_index), + Cow::Borrowed( + data.as_bytes(frame_index) + .expect("It's the caller's job to pass a valid frame index"), + ), + ))) + })? + .expect("Callback above only returns Some"); + let content_mask = self.content_mask().scale(scale_factor); + let corner_radii = corner_radii.scale(scale_factor); + let opacity = self.element_opacity(); + + self.next_frame.scene.insert_primitive(PolychromeSprite { + order: 0, + pad: 0, + grayscale, + bounds: bounds + .map_origin(|origin| origin.floor()) + .map_size(|size| size.ceil()), + content_mask, + corner_radii, + tile, + opacity, + }); + Ok(()) + } + + /// Paint a surface into the scene for the next frame at the current z-index. + /// + /// This method should only be called as part of the paint phase of element drawing. + #[cfg(target_os = "macos")] + pub fn paint_surface(&mut self, bounds: Bounds, image_buffer: CVPixelBuffer) { + use crate::PaintSurface; + + self.invalidator.debug_assert_paint(); + + let scale_factor = self.scale_factor(); + let bounds = bounds.scale(scale_factor); + let content_mask = self.content_mask().scale(scale_factor); + self.next_frame.scene.insert_primitive(PaintSurface { + order: 0, + bounds, + content_mask, + image_buffer, + }); + } + + /// Removes an image from the sprite atlas. + pub fn drop_image(&mut self, data: Arc) -> Result<()> { + for frame_index in 0..data.frame_count() { + let params = RenderImageParams { + image_id: data.id, + frame_index, + }; + + self.sprite_atlas.remove(¶ms.clone().into()); + } + + Ok(()) + } + + /// Add a node to the layout tree for the current frame. Takes the `Style` of the element for which + /// layout is being requested, along with the layout ids of any children. This method is called during + /// calls to the [`Element::request_layout`] trait method and enables any element to participate in layout. + /// + /// This method should only be called as part of the request_layout or prepaint phase of element drawing. + #[must_use] + pub fn request_layout( + &mut self, + style: Style, + children: impl IntoIterator, + cx: &mut App, + ) -> LayoutId { + self.invalidator.debug_assert_prepaint(); + + cx.layout_id_buffer.clear(); + cx.layout_id_buffer.extend(children); + let rem_size = self.rem_size(); + let scale_factor = self.scale_factor(); + + self.layout_engine.as_mut().unwrap().request_layout( + style, + rem_size, + scale_factor, + &cx.layout_id_buffer, + ) + } + + /// Add a node to the layout tree for the current frame. Instead of taking a `Style` and children, + /// this variant takes a function that is invoked during layout so you can use arbitrary logic to + /// determine the element's size. One place this is used internally is when measuring text. + /// + /// The given closure is invoked at layout time with the known dimensions and available space and + /// returns a `Size`. + /// + /// This method should only be called as part of the request_layout or prepaint phase of element drawing. + pub fn request_measured_layout< + F: FnMut(Size>, Size, &mut Window, &mut App) -> Size + + 'static, + >( + &mut self, + style: Style, + measure: F, + ) -> LayoutId { + self.invalidator.debug_assert_prepaint(); + + let rem_size = self.rem_size(); + let scale_factor = self.scale_factor(); + self.layout_engine + .as_mut() + .unwrap() + .request_measured_layout(style, rem_size, scale_factor, measure) + } + + /// Compute the layout for the given id within the given available space. + /// This method is called for its side effect, typically by the framework prior to painting. + /// After calling it, you can request the bounds of the given layout node id or any descendant. + /// + /// This method should only be called as part of the prepaint phase of element drawing. + pub fn compute_layout( + &mut self, + layout_id: LayoutId, + available_space: Size, + cx: &mut App, + ) { + self.invalidator.debug_assert_prepaint(); + + let mut layout_engine = self.layout_engine.take().unwrap(); + layout_engine.compute_layout(layout_id, available_space, self, cx); + self.layout_engine = Some(layout_engine); + } + + /// Obtain the bounds computed for the given LayoutId relative to the window. This method will usually be invoked by + /// GPUI itself automatically in order to pass your element its `Bounds` automatically. + /// + /// This method should only be called as part of element drawing. + pub fn layout_bounds(&mut self, layout_id: LayoutId) -> Bounds { + self.invalidator.debug_assert_prepaint(); + + let scale_factor = self.scale_factor(); + let mut bounds = self + .layout_engine + .as_mut() + .unwrap() + .layout_bounds(layout_id, scale_factor) + .map(Into::into); + bounds.origin += self.element_offset(); + bounds + } + + /// This method should be called during `prepaint`. You can use + /// the returned [Hitbox] during `paint` or in an event handler + /// to determine whether the inserted hitbox was the topmost. + /// + /// This method should only be called as part of the prepaint phase of element drawing. + pub fn insert_hitbox(&mut self, bounds: Bounds, behavior: HitboxBehavior) -> Hitbox { + self.invalidator.debug_assert_prepaint(); + + let content_mask = self.content_mask(); + let mut id = self.next_hitbox_id; + self.next_hitbox_id = self.next_hitbox_id.next(); + let hitbox = Hitbox { + id, + bounds, + content_mask, + behavior, + }; + self.next_frame.hitboxes.push(hitbox.clone()); + hitbox + } + + /// Set a hitbox which will act as a control area of the platform window. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn insert_window_control_hitbox(&mut self, area: WindowControlArea, hitbox: Hitbox) { + self.invalidator.debug_assert_paint(); + self.next_frame.window_control_hitboxes.push((area, hitbox)); + } + + /// Sets the key context for the current element. This context will be used to translate + /// keybindings into actions. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn set_key_context(&mut self, context: KeyContext) { + self.invalidator.debug_assert_paint(); + self.next_frame.dispatch_tree.set_key_context(context); + } + + /// Sets the focus handle for the current element. This handle will be used to manage focus state + /// and keyboard event dispatch for the element. + /// + /// This method should only be called as part of the prepaint phase of element drawing. + pub fn set_focus_handle(&mut self, focus_handle: &FocusHandle, _: &App) { + self.invalidator.debug_assert_prepaint(); + if focus_handle.is_focused(self) { + self.next_frame.focus = Some(focus_handle.id); + } + self.next_frame.dispatch_tree.set_focus_id(focus_handle.id); + } + + /// Sets the view id for the current element, which will be used to manage view caching. + /// + /// This method should only be called as part of element prepaint. We plan on removing this + /// method eventually when we solve some issues that require us to construct editor elements + /// directly instead of always using editors via views. + pub fn set_view_id(&mut self, view_id: EntityId) { + self.invalidator.debug_assert_prepaint(); + self.next_frame.dispatch_tree.set_view_id(view_id); + } + + /// Get the entity ID for the currently rendering view + pub fn current_view(&self) -> EntityId { + self.invalidator.debug_assert_paint_or_prepaint(); + self.rendered_entity_stack.last().copied().unwrap() + } + + pub(crate) fn with_rendered_view( + &mut self, + id: EntityId, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + self.rendered_entity_stack.push(id); + let result = f(self); + self.rendered_entity_stack.pop(); + result + } + + /// Executes the provided function with the specified image cache. + pub fn with_image_cache(&mut self, image_cache: Option, f: F) -> R + where + F: FnOnce(&mut Self) -> R, + { + if let Some(image_cache) = image_cache { + self.image_cache_stack.push(image_cache); + let result = f(self); + self.image_cache_stack.pop(); + result + } else { + f(self) + } + } + + /// Sets an input handler, such as [`ElementInputHandler`][element_input_handler], which interfaces with the + /// platform to receive textual input with proper integration with concerns such + /// as IME interactions. This handler will be active for the upcoming frame until the following frame is + /// rendered. + /// + /// This method should only be called as part of the paint phase of element drawing. + /// + /// [element_input_handler]: crate::ElementInputHandler + pub fn handle_input( + &mut self, + focus_handle: &FocusHandle, + input_handler: impl InputHandler, + cx: &App, + ) { + self.invalidator.debug_assert_paint(); + + if focus_handle.is_focused(self) { + let cx = self.to_async(cx); + self.next_frame + .input_handlers + .push(Some(PlatformInputHandler::new(cx, Box::new(input_handler)))); + } + } + + /// Register a mouse event listener on the window for the next frame. The type of event + /// is determined by the first parameter of the given listener. When the next frame is rendered + /// the listener will be cleared. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn on_mouse_event( + &mut self, + mut handler: impl FnMut(&Event, DispatchPhase, &mut Window, &mut App) + 'static, + ) { + self.invalidator.debug_assert_paint(); + + self.next_frame.mouse_listeners.push(Some(Box::new( + move |event: &dyn Any, phase: DispatchPhase, window: &mut Window, cx: &mut App| { + if let Some(event) = event.downcast_ref() { + handler(event, phase, window, cx) + } + }, + ))); + } + + /// Register a key event listener on the window for the next frame. The type of event + /// is determined by the first parameter of the given listener. When the next frame is rendered + /// the listener will be cleared. + /// + /// This is a fairly low-level method, so prefer using event handlers on elements unless you have + /// a specific need to register a global listener. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn on_key_event( + &mut self, + listener: impl Fn(&Event, DispatchPhase, &mut Window, &mut App) + 'static, + ) { + self.invalidator.debug_assert_paint(); + + self.next_frame.dispatch_tree.on_key_event(Rc::new( + move |event: &dyn Any, phase, window: &mut Window, cx: &mut App| { + if let Some(event) = event.downcast_ref::() { + listener(event, phase, window, cx) + } + }, + )); + } + + /// Register a modifiers changed event listener on the window for the next frame. + /// + /// This is a fairly low-level method, so prefer using event handlers on elements unless you have + /// a specific need to register a global listener. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn on_modifiers_changed( + &mut self, + listener: impl Fn(&ModifiersChangedEvent, &mut Window, &mut App) + 'static, + ) { + self.invalidator.debug_assert_paint(); + + self.next_frame.dispatch_tree.on_modifiers_changed(Rc::new( + move |event: &ModifiersChangedEvent, window: &mut Window, cx: &mut App| { + listener(event, window, cx) + }, + )); + } + + /// Register a listener to be called when the given focus handle or one of its descendants receives focus. + /// This does not fire if the given focus handle - or one of its descendants - was previously focused. + /// Returns a subscription and persists until the subscription is dropped. + pub fn on_focus_in( + &mut self, + handle: &FocusHandle, + cx: &mut App, + mut listener: impl FnMut(&mut Window, &mut App) + 'static, + ) -> Subscription { + let focus_id = handle.id; + let (subscription, activate) = + self.new_focus_listener(Box::new(move |event, window, cx| { + if event.is_focus_in(focus_id) { + listener(window, cx); + } + true + })); + cx.defer(move |_| activate()); + subscription + } + + /// Register a listener to be called when the given focus handle or one of its descendants loses focus. + /// Returns a subscription and persists until the subscription is dropped. + pub fn on_focus_out( + &mut self, + handle: &FocusHandle, + cx: &mut App, + mut listener: impl FnMut(FocusOutEvent, &mut Window, &mut App) + 'static, + ) -> Subscription { + let focus_id = handle.id; + let (subscription, activate) = + self.new_focus_listener(Box::new(move |event, window, cx| { + if let Some(blurred_id) = event.previous_focus_path.last().copied() + && event.is_focus_out(focus_id) + { + let event = FocusOutEvent { + blurred: WeakFocusHandle { + id: blurred_id, + handles: Arc::downgrade(&cx.focus_handles), + }, + }; + listener(event, window, cx) + } + true + })); + cx.defer(move |_| activate()); + subscription + } + + fn reset_cursor_style(&self, cx: &mut App) { + // Set the cursor only if we're the active window. + if self.is_window_hovered() { + let style = self + .rendered_frame + .cursor_style(self) + .unwrap_or(CursorStyle::Arrow); + cx.platform.set_cursor_style(style); + } + } + + /// Dispatch a given keystroke as though the user had typed it. + /// You can create a keystroke with Keystroke::parse(""). + pub fn dispatch_keystroke(&mut self, keystroke: Keystroke, cx: &mut App) -> bool { + let keystroke = keystroke.with_simulated_ime(); + let result = self.dispatch_event( + PlatformInput::KeyDown(KeyDownEvent { + keystroke: keystroke.clone(), + is_held: false, + }), + cx, + ); + if !result.propagate { + return true; + } + + if let Some(input) = keystroke.key_char + && let Some(mut input_handler) = self.platform_window.take_input_handler() + { + input_handler.dispatch_input(&input, self, cx); + self.platform_window.set_input_handler(input_handler); + return true; + } + + false + } + + /// Return a key binding string for an action, to display in the UI. Uses the highest precedence + /// binding for the action (last binding added to the keymap). + pub fn keystroke_text_for(&self, action: &dyn Action) -> String { + self.highest_precedence_binding_for_action(action) + .map(|binding| { + binding + .keystrokes() + .iter() + .map(ToString::to_string) + .collect::>() + .join(" ") + }) + .unwrap_or_else(|| action.name().to_string()) + } + + /// Dispatch a mouse or keyboard event on the window. + #[profiling::function] + pub fn dispatch_event(&mut self, event: PlatformInput, cx: &mut App) -> DispatchEventResult { + self.last_input_timestamp.set(Instant::now()); + // Handlers may set this to false by calling `stop_propagation`. + cx.propagate_event = true; + // Handlers may set this to true by calling `prevent_default`. + self.default_prevented = false; + + let event = match event { + // Track the mouse position with our own state, since accessing the platform + // API for the mouse position can only occur on the main thread. + PlatformInput::MouseMove(mouse_move) => { + self.mouse_position = mouse_move.position; + self.modifiers = mouse_move.modifiers; + PlatformInput::MouseMove(mouse_move) + } + PlatformInput::MouseDown(mouse_down) => { + self.mouse_position = mouse_down.position; + self.modifiers = mouse_down.modifiers; + PlatformInput::MouseDown(mouse_down) + } + PlatformInput::MouseUp(mouse_up) => { + self.mouse_position = mouse_up.position; + self.modifiers = mouse_up.modifiers; + PlatformInput::MouseUp(mouse_up) + } + PlatformInput::MouseExited(mouse_exited) => { + self.modifiers = mouse_exited.modifiers; + PlatformInput::MouseExited(mouse_exited) + } + PlatformInput::ModifiersChanged(modifiers_changed) => { + self.modifiers = modifiers_changed.modifiers; + self.capslock = modifiers_changed.capslock; + PlatformInput::ModifiersChanged(modifiers_changed) + } + PlatformInput::ScrollWheel(scroll_wheel) => { + self.mouse_position = scroll_wheel.position; + self.modifiers = scroll_wheel.modifiers; + PlatformInput::ScrollWheel(scroll_wheel) + } + // Translate dragging and dropping of external files from the operating system + // to internal drag and drop events. + PlatformInput::FileDrop(file_drop) => match file_drop { + FileDropEvent::Entered { position, paths } => { + self.mouse_position = position; + if cx.active_drag.is_none() { + cx.active_drag = Some(AnyDrag { + value: Arc::new(paths.clone()), + view: cx.new(|_| paths).into(), + cursor_offset: position, + cursor_style: None, + }); + } + PlatformInput::MouseMove(MouseMoveEvent { + position, + pressed_button: Some(MouseButton::Left), + modifiers: Modifiers::default(), + }) + } + FileDropEvent::Pending { position } => { + self.mouse_position = position; + PlatformInput::MouseMove(MouseMoveEvent { + position, + pressed_button: Some(MouseButton::Left), + modifiers: Modifiers::default(), + }) + } + FileDropEvent::Submit { position } => { + cx.activate(true); + self.mouse_position = position; + PlatformInput::MouseUp(MouseUpEvent { + button: MouseButton::Left, + position, + modifiers: Modifiers::default(), + click_count: 1, + }) + } + FileDropEvent::Exited => { + cx.active_drag.take(); + PlatformInput::FileDrop(FileDropEvent::Exited) + } + }, + PlatformInput::KeyDown(_) | PlatformInput::KeyUp(_) => event, + }; + + if let Some(any_mouse_event) = event.mouse_event() { + self.dispatch_mouse_event(any_mouse_event, cx); + } else if let Some(any_key_event) = event.keyboard_event() { + self.dispatch_key_event(any_key_event, cx); + } + + DispatchEventResult { + propagate: cx.propagate_event, + default_prevented: self.default_prevented, + } + } + + fn dispatch_mouse_event(&mut self, event: &dyn Any, cx: &mut App) { + let hit_test = self.rendered_frame.hit_test(self.mouse_position()); + if hit_test != self.mouse_hit_test { + self.mouse_hit_test = hit_test; + self.reset_cursor_style(cx); + } + + #[cfg(any(feature = "inspector", debug_assertions))] + if self.is_inspector_picking(cx) { + self.handle_inspector_mouse_event(event, cx); + // When inspector is picking, all other mouse handling is skipped. + return; + } + + let mut mouse_listeners = mem::take(&mut self.rendered_frame.mouse_listeners); + + // Capture phase, events bubble from back to front. Handlers for this phase are used for + // special purposes, such as detecting events outside of a given Bounds. + for listener in &mut mouse_listeners { + let listener = listener.as_mut().unwrap(); + listener(event, DispatchPhase::Capture, self, cx); + if !cx.propagate_event { + break; + } + } + + // Bubble phase, where most normal handlers do their work. + if cx.propagate_event { + for listener in mouse_listeners.iter_mut().rev() { + let listener = listener.as_mut().unwrap(); + listener(event, DispatchPhase::Bubble, self, cx); + if !cx.propagate_event { + break; + } + } + } + + self.rendered_frame.mouse_listeners = mouse_listeners; + + if cx.has_active_drag() { + if event.is::() { + // If this was a mouse move event, redraw the window so that the + // active drag can follow the mouse cursor. + self.refresh(); + } else if event.is::() { + // If this was a mouse up event, cancel the active drag and redraw + // the window. + cx.active_drag = None; + self.refresh(); + } + } + } + + fn dispatch_key_event(&mut self, event: &dyn Any, cx: &mut App) { + if self.invalidator.is_dirty() { + self.draw(cx).clear(); + } + + let node_id = self.focus_node_id_in_rendered_frame(self.focus); + let dispatch_path = self.rendered_frame.dispatch_tree.dispatch_path(node_id); + + let mut keystroke: Option = None; + + if let Some(event) = event.downcast_ref::() { + if event.modifiers.number_of_modifiers() == 0 + && self.pending_modifier.modifiers.number_of_modifiers() == 1 + && !self.pending_modifier.saw_keystroke + { + let key = match self.pending_modifier.modifiers { + modifiers if modifiers.shift => Some("shift"), + modifiers if modifiers.control => Some("control"), + modifiers if modifiers.alt => Some("alt"), + modifiers if modifiers.platform => Some("platform"), + modifiers if modifiers.function => Some("function"), + _ => None, + }; + if let Some(key) = key { + keystroke = Some(Keystroke { + key: key.to_string(), + key_char: None, + modifiers: Modifiers::default(), + }); + } + } + + if self.pending_modifier.modifiers.number_of_modifiers() == 0 + && event.modifiers.number_of_modifiers() == 1 + { + self.pending_modifier.saw_keystroke = false + } + self.pending_modifier.modifiers = event.modifiers + } else if let Some(key_down_event) = event.downcast_ref::() { + self.pending_modifier.saw_keystroke = true; + keystroke = Some(key_down_event.keystroke.clone()); + } + + let Some(keystroke) = keystroke else { + self.finish_dispatch_key_event(event, dispatch_path, self.context_stack(), cx); + return; + }; + + cx.propagate_event = true; + self.dispatch_keystroke_interceptors(event, self.context_stack(), cx); + if !cx.propagate_event { + self.finish_dispatch_key_event(event, dispatch_path, self.context_stack(), cx); + return; + } + + let mut currently_pending = self.pending_input.take().unwrap_or_default(); + if currently_pending.focus.is_some() && currently_pending.focus != self.focus { + currently_pending = PendingInput::default(); + } + + let match_result = self.rendered_frame.dispatch_tree.dispatch_key( + currently_pending.keystrokes, + keystroke, + &dispatch_path, + ); + + if !match_result.to_replay.is_empty() { + self.replay_pending_input(match_result.to_replay, cx); + cx.propagate_event = true; + } + + if !match_result.pending.is_empty() { + currently_pending.keystrokes = match_result.pending; + currently_pending.focus = self.focus; + currently_pending.timer = Some(self.spawn(cx, async move |cx| { + cx.background_executor.timer(Duration::from_secs(1)).await; + cx.update(move |window, cx| { + let Some(currently_pending) = window + .pending_input + .take() + .filter(|pending| pending.focus == window.focus) + else { + return; + }; + + let node_id = window.focus_node_id_in_rendered_frame(window.focus); + let dispatch_path = window.rendered_frame.dispatch_tree.dispatch_path(node_id); + + let to_replay = window + .rendered_frame + .dispatch_tree + .flush_dispatch(currently_pending.keystrokes, &dispatch_path); + + window.pending_input_changed(cx); + window.replay_pending_input(to_replay, cx) + }) + .log_err(); + })); + self.pending_input = Some(currently_pending); + self.pending_input_changed(cx); + cx.propagate_event = false; + return; + } + + for binding in match_result.bindings { + self.dispatch_action_on_node(node_id, binding.action.as_ref(), cx); + if !cx.propagate_event { + self.dispatch_keystroke_observers( + event, + Some(binding.action), + match_result.context_stack, + cx, + ); + self.pending_input_changed(cx); + return; + } + } + + self.finish_dispatch_key_event(event, dispatch_path, match_result.context_stack, cx); + self.pending_input_changed(cx); + } + + fn finish_dispatch_key_event( + &mut self, + event: &dyn Any, + dispatch_path: SmallVec<[DispatchNodeId; 32]>, + context_stack: Vec, + cx: &mut App, + ) { + self.dispatch_key_down_up_event(event, &dispatch_path, cx); + if !cx.propagate_event { + return; + } + + self.dispatch_modifiers_changed_event(event, &dispatch_path, cx); + if !cx.propagate_event { + return; + } + + self.dispatch_keystroke_observers(event, None, context_stack, cx); + } + + fn pending_input_changed(&mut self, cx: &mut App) { + self.pending_input_observers + .clone() + .retain(&(), |callback| callback(self, cx)); + } + + fn dispatch_key_down_up_event( + &mut self, + event: &dyn Any, + dispatch_path: &SmallVec<[DispatchNodeId; 32]>, + cx: &mut App, + ) { + // Capture phase + for node_id in dispatch_path { + let node = self.rendered_frame.dispatch_tree.node(*node_id); + + for key_listener in node.key_listeners.clone() { + key_listener(event, DispatchPhase::Capture, self, cx); + if !cx.propagate_event { + return; + } + } + } + + // Bubble phase + for node_id in dispatch_path.iter().rev() { + // Handle low level key events + let node = self.rendered_frame.dispatch_tree.node(*node_id); + for key_listener in node.key_listeners.clone() { + key_listener(event, DispatchPhase::Bubble, self, cx); + if !cx.propagate_event { + return; + } + } + } + } + + fn dispatch_modifiers_changed_event( + &mut self, + event: &dyn Any, + dispatch_path: &SmallVec<[DispatchNodeId; 32]>, + cx: &mut App, + ) { + let Some(event) = event.downcast_ref::() else { + return; + }; + for node_id in dispatch_path.iter().rev() { + let node = self.rendered_frame.dispatch_tree.node(*node_id); + for listener in node.modifiers_changed_listeners.clone() { + listener(event, self, cx); + if !cx.propagate_event { + return; + } + } + } + } + + /// Determine whether a potential multi-stroke key binding is in progress on this window. + pub fn has_pending_keystrokes(&self) -> bool { + self.pending_input.is_some() + } + + pub(crate) fn clear_pending_keystrokes(&mut self) { + self.pending_input.take(); + } + + /// Returns the currently pending input keystrokes that might result in a multi-stroke key binding. + pub fn pending_input_keystrokes(&self) -> Option<&[Keystroke]> { + self.pending_input + .as_ref() + .map(|pending_input| pending_input.keystrokes.as_slice()) + } + + fn replay_pending_input(&mut self, replays: SmallVec<[Replay; 1]>, cx: &mut App) { + let node_id = self.focus_node_id_in_rendered_frame(self.focus); + let dispatch_path = self.rendered_frame.dispatch_tree.dispatch_path(node_id); + + 'replay: for replay in replays { + let event = KeyDownEvent { + keystroke: replay.keystroke.clone(), + is_held: false, + }; + + cx.propagate_event = true; + for binding in replay.bindings { + self.dispatch_action_on_node(node_id, binding.action.as_ref(), cx); + if !cx.propagate_event { + self.dispatch_keystroke_observers( + &event, + Some(binding.action), + Vec::default(), + cx, + ); + continue 'replay; + } + } + + self.dispatch_key_down_up_event(&event, &dispatch_path, cx); + if !cx.propagate_event { + continue 'replay; + } + if let Some(input) = replay.keystroke.key_char.as_ref().cloned() + && let Some(mut input_handler) = self.platform_window.take_input_handler() + { + input_handler.dispatch_input(&input, self, cx); + self.platform_window.set_input_handler(input_handler) + } + } + } + + fn focus_node_id_in_rendered_frame(&self, focus_id: Option) -> DispatchNodeId { + focus_id + .and_then(|focus_id| { + self.rendered_frame + .dispatch_tree + .focusable_node_id(focus_id) + }) + .unwrap_or_else(|| self.rendered_frame.dispatch_tree.root_node_id()) + } + + fn dispatch_action_on_node( + &mut self, + node_id: DispatchNodeId, + action: &dyn Action, + cx: &mut App, + ) { + let dispatch_path = self.rendered_frame.dispatch_tree.dispatch_path(node_id); + + // Capture phase for global actions. + cx.propagate_event = true; + if let Some(mut global_listeners) = cx + .global_action_listeners + .remove(&action.as_any().type_id()) + { + for listener in &global_listeners { + listener(action.as_any(), DispatchPhase::Capture, cx); + if !cx.propagate_event { + break; + } + } + + global_listeners.extend( + cx.global_action_listeners + .remove(&action.as_any().type_id()) + .unwrap_or_default(), + ); + + cx.global_action_listeners + .insert(action.as_any().type_id(), global_listeners); + } + + if !cx.propagate_event { + return; + } + + // Capture phase for window actions. + for node_id in &dispatch_path { + let node = self.rendered_frame.dispatch_tree.node(*node_id); + for DispatchActionListener { + action_type, + listener, + } in node.action_listeners.clone() + { + let any_action = action.as_any(); + if action_type == any_action.type_id() { + listener(any_action, DispatchPhase::Capture, self, cx); + + if !cx.propagate_event { + return; + } + } + } + } + + // Bubble phase for window actions. + for node_id in dispatch_path.iter().rev() { + let node = self.rendered_frame.dispatch_tree.node(*node_id); + for DispatchActionListener { + action_type, + listener, + } in node.action_listeners.clone() + { + let any_action = action.as_any(); + if action_type == any_action.type_id() { + cx.propagate_event = false; // Actions stop propagation by default during the bubble phase + listener(any_action, DispatchPhase::Bubble, self, cx); + + if !cx.propagate_event { + return; + } + } + } + } + + // Bubble phase for global actions. + if let Some(mut global_listeners) = cx + .global_action_listeners + .remove(&action.as_any().type_id()) + { + for listener in global_listeners.iter().rev() { + cx.propagate_event = false; // Actions stop propagation by default during the bubble phase + + listener(action.as_any(), DispatchPhase::Bubble, cx); + if !cx.propagate_event { + break; + } + } + + global_listeners.extend( + cx.global_action_listeners + .remove(&action.as_any().type_id()) + .unwrap_or_default(), + ); + + cx.global_action_listeners + .insert(action.as_any().type_id(), global_listeners); + } + } + + /// Register the given handler to be invoked whenever the global of the given type + /// is updated. + pub fn observe_global( + &mut self, + cx: &mut App, + f: impl Fn(&mut Window, &mut App) + 'static, + ) -> Subscription { + let window_handle = self.handle; + let (subscription, activate) = cx.global_observers.insert( + TypeId::of::(), + Box::new(move |cx| { + window_handle + .update(cx, |_, window, cx| f(window, cx)) + .is_ok() + }), + ); + cx.defer(move |_| activate()); + subscription + } + + /// Focus the current window and bring it to the foreground at the platform level. + pub fn activate_window(&self) { + self.platform_window.activate(); + } + + /// Minimize the current window at the platform level. + pub fn minimize_window(&self) { + self.platform_window.minimize(); + } + + /// Toggle full screen status on the current window at the platform level. + pub fn toggle_fullscreen(&self) { + self.platform_window.toggle_fullscreen(); + } + + /// Updates the IME panel position suggestions for languages like japanese, chinese. + pub fn invalidate_character_coordinates(&self) { + self.on_next_frame(|window, cx| { + if let Some(mut input_handler) = window.platform_window.take_input_handler() { + if let Some(bounds) = input_handler.selected_bounds(window, cx) { + window.platform_window.update_ime_position(bounds); + } + window.platform_window.set_input_handler(input_handler); + } + }); + } + + /// Present a platform dialog. + /// The provided message will be presented, along with buttons for each answer. + /// When a button is clicked, the returned Receiver will receive the index of the clicked button. + pub fn prompt( + &mut self, + level: PromptLevel, + message: &str, + detail: Option<&str>, + answers: &[T], + cx: &mut App, + ) -> oneshot::Receiver + where + T: Clone + Into, + { + let prompt_builder = cx.prompt_builder.take(); + let Some(prompt_builder) = prompt_builder else { + unreachable!("Re-entrant window prompting is not supported by GPUI"); + }; + + let answers = answers + .iter() + .map(|answer| answer.clone().into()) + .collect::>(); + + let receiver = match &prompt_builder { + PromptBuilder::Default => self + .platform_window + .prompt(level, message, detail, &answers) + .unwrap_or_else(|| { + self.build_custom_prompt(&prompt_builder, level, message, detail, &answers, cx) + }), + PromptBuilder::Custom(_) => { + self.build_custom_prompt(&prompt_builder, level, message, detail, &answers, cx) + } + }; + + cx.prompt_builder = Some(prompt_builder); + + receiver + } + + fn build_custom_prompt( + &mut self, + prompt_builder: &PromptBuilder, + level: PromptLevel, + message: &str, + detail: Option<&str>, + answers: &[PromptButton], + cx: &mut App, + ) -> oneshot::Receiver { + let (sender, receiver) = oneshot::channel(); + let handle = PromptHandle::new(sender); + let handle = (prompt_builder)(level, message, detail, answers, handle, self, cx); + self.prompt = Some(handle); + receiver + } + + /// Returns the current context stack. + pub fn context_stack(&self) -> Vec { + let node_id = self.focus_node_id_in_rendered_frame(self.focus); + let dispatch_tree = &self.rendered_frame.dispatch_tree; + dispatch_tree + .dispatch_path(node_id) + .iter() + .filter_map(move |&node_id| dispatch_tree.node(node_id).context.clone()) + .collect() + } + + /// Returns all available actions for the focused element. + pub fn available_actions(&self, cx: &App) -> Vec> { + let node_id = self.focus_node_id_in_rendered_frame(self.focus); + let mut actions = self.rendered_frame.dispatch_tree.available_actions(node_id); + for action_type in cx.global_action_listeners.keys() { + if let Err(ix) = actions.binary_search_by_key(action_type, |a| a.as_any().type_id()) { + let action = cx.actions.build_action_type(action_type).ok(); + if let Some(action) = action { + actions.insert(ix, action); + } + } + } + actions + } + + /// Returns key bindings that invoke an action on the currently focused element. Bindings are + /// returned in the order they were added. For display, the last binding should take precedence. + pub fn bindings_for_action(&self, action: &dyn Action) -> Vec { + self.rendered_frame + .dispatch_tree + .bindings_for_action(action, &self.rendered_frame.dispatch_tree.context_stack) + } + + /// Returns the highest precedence key binding that invokes an action on the currently focused + /// element. This is more efficient than getting the last result of `bindings_for_action`. + pub fn highest_precedence_binding_for_action(&self, action: &dyn Action) -> Option { + self.rendered_frame + .dispatch_tree + .highest_precedence_binding_for_action( + action, + &self.rendered_frame.dispatch_tree.context_stack, + ) + } + + /// Returns the key bindings for an action in a context. + pub fn bindings_for_action_in_context( + &self, + action: &dyn Action, + context: KeyContext, + ) -> Vec { + let dispatch_tree = &self.rendered_frame.dispatch_tree; + dispatch_tree.bindings_for_action(action, &[context]) + } + + /// Returns the highest precedence key binding for an action in a context. This is more + /// efficient than getting the last result of `bindings_for_action_in_context`. + pub fn highest_precedence_binding_for_action_in_context( + &self, + action: &dyn Action, + context: KeyContext, + ) -> Option { + let dispatch_tree = &self.rendered_frame.dispatch_tree; + dispatch_tree.highest_precedence_binding_for_action(action, &[context]) + } + + /// Returns any bindings that would invoke an action on the given focus handle if it were + /// focused. Bindings are returned in the order they were added. For display, the last binding + /// should take precedence. + pub fn bindings_for_action_in( + &self, + action: &dyn Action, + focus_handle: &FocusHandle, + ) -> Vec { + let dispatch_tree = &self.rendered_frame.dispatch_tree; + let Some(context_stack) = self.context_stack_for_focus_handle(focus_handle) else { + return vec![]; + }; + dispatch_tree.bindings_for_action(action, &context_stack) + } + + /// Returns the highest precedence key binding that would invoke an action on the given focus + /// handle if it were focused. This is more efficient than getting the last result of + /// `bindings_for_action_in`. + pub fn highest_precedence_binding_for_action_in( + &self, + action: &dyn Action, + focus_handle: &FocusHandle, + ) -> Option { + let dispatch_tree = &self.rendered_frame.dispatch_tree; + let context_stack = self.context_stack_for_focus_handle(focus_handle)?; + dispatch_tree.highest_precedence_binding_for_action(action, &context_stack) + } + + fn context_stack_for_focus_handle( + &self, + focus_handle: &FocusHandle, + ) -> Option> { + let dispatch_tree = &self.rendered_frame.dispatch_tree; + let node_id = dispatch_tree.focusable_node_id(focus_handle.id)?; + let context_stack: Vec<_> = dispatch_tree + .dispatch_path(node_id) + .into_iter() + .filter_map(|node_id| dispatch_tree.node(node_id).context.clone()) + .collect(); + Some(context_stack) + } + + /// Returns a generic event listener that invokes the given listener with the view and context associated with the given view handle. + pub fn listener_for( + &self, + view: &Entity, + f: impl Fn(&mut V, &E, &mut Window, &mut Context) + 'static, + ) -> impl Fn(&E, &mut Window, &mut App) + 'static { + let view = view.downgrade(); + move |e: &E, window: &mut Window, cx: &mut App| { + view.update(cx, |view, cx| f(view, e, window, cx)).ok(); + } + } + + /// Returns a generic handler that invokes the given handler with the view and context associated with the given view handle. + pub fn handler_for) + 'static>( + &self, + entity: &Entity, + f: Callback, + ) -> impl Fn(&mut Window, &mut App) + 'static { + let entity = entity.downgrade(); + move |window: &mut Window, cx: &mut App| { + entity.update(cx, |entity, cx| f(entity, window, cx)).ok(); + } + } + + /// Register a callback that can interrupt the closing of the current window based the returned boolean. + /// If the callback returns false, the window won't be closed. + pub fn on_window_should_close( + &self, + cx: &App, + f: impl Fn(&mut Window, &mut App) -> bool + 'static, + ) { + let mut cx = self.to_async(cx); + self.platform_window.on_should_close(Box::new(move || { + cx.update(|window, cx| f(window, cx)).unwrap_or(true) + })) + } + + /// Register an action listener on the window for the next frame. The type of action + /// is determined by the first parameter of the given listener. When the next frame is rendered + /// the listener will be cleared. + /// + /// This is a fairly low-level method, so prefer using action handlers on elements unless you have + /// a specific need to register a global listener. + pub fn on_action( + &mut self, + action_type: TypeId, + listener: impl Fn(&dyn Any, DispatchPhase, &mut Window, &mut App) + 'static, + ) { + self.next_frame + .dispatch_tree + .on_action(action_type, Rc::new(listener)); + } + + /// Register an action listener on the window for the next frame if the condition is true. + /// The type of action is determined by the first parameter of the given listener. + /// When the next frame is rendered the listener will be cleared. + /// + /// This is a fairly low-level method, so prefer using action handlers on elements unless you have + /// a specific need to register a global listener. + pub fn on_action_when( + &mut self, + condition: bool, + action_type: TypeId, + listener: impl Fn(&dyn Any, DispatchPhase, &mut Window, &mut App) + 'static, + ) { + if condition { + self.next_frame + .dispatch_tree + .on_action(action_type, Rc::new(listener)); + } + } + + /// Read information about the GPU backing this window. + /// Currently returns None on Mac and Windows. + pub fn gpu_specs(&self) -> Option { + self.platform_window.gpu_specs() + } + + /// Perform titlebar double-click action. + /// This is macOS specific. + pub fn titlebar_double_click(&self) { + self.platform_window.titlebar_double_click(); + } + + /// Gets the window's title at the platform level. + /// This is macOS specific. + pub fn window_title(&self) -> String { + self.platform_window.get_title() + } + + /// Returns a list of all tabbed windows and their titles. + /// This is macOS specific. + pub fn tabbed_windows(&self) -> Option> { + self.platform_window.tabbed_windows() + } + + /// Returns the tab bar visibility. + /// This is macOS specific. + pub fn tab_bar_visible(&self) -> bool { + self.platform_window.tab_bar_visible() + } + + /// Merges all open windows into a single tabbed window. + /// This is macOS specific. + pub fn merge_all_windows(&self) { + self.platform_window.merge_all_windows() + } + + /// Moves the tab to a new containing window. + /// This is macOS specific. + pub fn move_tab_to_new_window(&self) { + self.platform_window.move_tab_to_new_window() + } + + /// Shows or hides the window tab overview. + /// This is macOS specific. + pub fn toggle_window_tab_overview(&self) { + self.platform_window.toggle_window_tab_overview() + } + + /// Sets the tabbing identifier for the window. + /// This is macOS specific. + pub fn set_tabbing_identifier(&self, tabbing_identifier: Option) { + self.platform_window + .set_tabbing_identifier(tabbing_identifier) + } + + /// Toggles the inspector mode on this window. + #[cfg(any(feature = "inspector", debug_assertions))] + pub fn toggle_inspector(&mut self, cx: &mut App) { + self.inspector = match self.inspector { + None => Some(cx.new(|_| Inspector::new())), + Some(_) => None, + }; + self.refresh(); + } + + /// Returns true if the window is in inspector mode. + pub fn is_inspector_picking(&self, _cx: &App) -> bool { + #[cfg(any(feature = "inspector", debug_assertions))] + { + if let Some(inspector) = &self.inspector { + return inspector.read(_cx).is_picking(); + } + } + false + } + + /// Executes the provided function with mutable access to an inspector state. + #[cfg(any(feature = "inspector", debug_assertions))] + pub fn with_inspector_state( + &mut self, + _inspector_id: Option<&crate::InspectorElementId>, + cx: &mut App, + f: impl FnOnce(&mut Option, &mut Self) -> R, + ) -> R { + if let Some(inspector_id) = _inspector_id + && let Some(inspector) = &self.inspector + { + let inspector = inspector.clone(); + let active_element_id = inspector.read(cx).active_element_id(); + if Some(inspector_id) == active_element_id { + return inspector.update(cx, |inspector, _cx| { + inspector.with_active_element_state(self, f) + }); + } + } + f(&mut None, self) + } + + #[cfg(any(feature = "inspector", debug_assertions))] + pub(crate) fn build_inspector_element_id( + &mut self, + path: crate::InspectorElementPath, + ) -> crate::InspectorElementId { + self.invalidator.debug_assert_paint_or_prepaint(); + let path = Rc::new(path); + let next_instance_id = self + .next_frame + .next_inspector_instance_ids + .entry(path.clone()) + .or_insert(0); + let instance_id = *next_instance_id; + *next_instance_id += 1; + crate::InspectorElementId { path, instance_id } + } + + #[cfg(any(feature = "inspector", debug_assertions))] + fn prepaint_inspector(&mut self, inspector_width: Pixels, cx: &mut App) -> Option { + if let Some(inspector) = self.inspector.take() { + let mut inspector_element = AnyView::from(inspector.clone()).into_any_element(); + inspector_element.prepaint_as_root( + point(self.viewport_size.width - inspector_width, px(0.0)), + size(inspector_width, self.viewport_size.height).into(), + self, + cx, + ); + self.inspector = Some(inspector); + Some(inspector_element) + } else { + None + } + } + + #[cfg(any(feature = "inspector", debug_assertions))] + fn paint_inspector(&mut self, mut inspector_element: Option, cx: &mut App) { + if let Some(mut inspector_element) = inspector_element { + inspector_element.paint(self, cx); + }; + } + + /// Registers a hitbox that can be used for inspector picking mode, allowing users to select and + /// inspect UI elements by clicking on them. + #[cfg(any(feature = "inspector", debug_assertions))] + pub fn insert_inspector_hitbox( + &mut self, + hitbox_id: HitboxId, + inspector_id: Option<&crate::InspectorElementId>, + cx: &App, + ) { + self.invalidator.debug_assert_paint_or_prepaint(); + if !self.is_inspector_picking(cx) { + return; + } + if let Some(inspector_id) = inspector_id { + self.next_frame + .inspector_hitboxes + .insert(hitbox_id, inspector_id.clone()); + } + } + + #[cfg(any(feature = "inspector", debug_assertions))] + fn paint_inspector_hitbox(&mut self, cx: &App) { + if let Some(inspector) = self.inspector.as_ref() { + let inspector = inspector.read(cx); + if let Some((hitbox_id, _)) = self.hovered_inspector_hitbox(inspector, &self.next_frame) + && let Some(hitbox) = self + .next_frame + .hitboxes + .iter() + .find(|hitbox| hitbox.id == hitbox_id) + { + self.paint_quad(crate::fill(hitbox.bounds, crate::rgba(0x61afef4d))); + } + } + } + + #[cfg(any(feature = "inspector", debug_assertions))] + fn handle_inspector_mouse_event(&mut self, event: &dyn Any, cx: &mut App) { + let Some(inspector) = self.inspector.clone() else { + return; + }; + if event.downcast_ref::().is_some() { + inspector.update(cx, |inspector, _cx| { + if let Some((_, inspector_id)) = + self.hovered_inspector_hitbox(inspector, &self.rendered_frame) + { + inspector.hover(inspector_id, self); + } + }); + } else if event.downcast_ref::().is_some() { + inspector.update(cx, |inspector, _cx| { + if let Some((_, inspector_id)) = + self.hovered_inspector_hitbox(inspector, &self.rendered_frame) + { + inspector.select(inspector_id, self); + } + }); + } else if let Some(event) = event.downcast_ref::() { + // This should be kept in sync with SCROLL_LINES in x11 platform. + const SCROLL_LINES: f32 = 3.0; + const SCROLL_PIXELS_PER_LAYER: f32 = 36.0; + let delta_y = event + .delta + .pixel_delta(px(SCROLL_PIXELS_PER_LAYER / SCROLL_LINES)) + .y; + if let Some(inspector) = self.inspector.clone() { + inspector.update(cx, |inspector, _cx| { + if let Some(depth) = inspector.pick_depth.as_mut() { + *depth += f32::from(delta_y) / SCROLL_PIXELS_PER_LAYER; + let max_depth = self.mouse_hit_test.ids.len() as f32 - 0.5; + if *depth < 0.0 { + *depth = 0.0; + } else if *depth > max_depth { + *depth = max_depth; + } + if let Some((_, inspector_id)) = + self.hovered_inspector_hitbox(inspector, &self.rendered_frame) + { + inspector.set_active_element_id(inspector_id, self); + } + } + }); + } + } + } + + #[cfg(any(feature = "inspector", debug_assertions))] + fn hovered_inspector_hitbox( + &self, + inspector: &Inspector, + frame: &Frame, + ) -> Option<(HitboxId, crate::InspectorElementId)> { + if let Some(pick_depth) = inspector.pick_depth { + let depth = (pick_depth as i64).try_into().unwrap_or(0); + let max_skipped = self.mouse_hit_test.ids.len().saturating_sub(1); + let skip_count = (depth as usize).min(max_skipped); + for hitbox_id in self.mouse_hit_test.ids.iter().skip(skip_count) { + if let Some(inspector_id) = frame.inspector_hitboxes.get(hitbox_id) { + return Some((*hitbox_id, inspector_id.clone())); + } + } + } + None + } + + /// For testing: set the current modifier keys state. + /// This does not generate any events. + #[cfg(any(test, feature = "test-support"))] + pub fn set_modifiers(&mut self, modifiers: Modifiers) { + self.modifiers = modifiers; + } +} + +// #[derive(Clone, Copy, Eq, PartialEq, Hash)] +slotmap::new_key_type! { + /// A unique identifier for a window. + pub struct WindowId; +} + +impl WindowId { + /// Converts this window ID to a `u64`. + pub fn as_u64(&self) -> u64 { + self.0.as_ffi() + } +} + +impl From for WindowId { + fn from(value: u64) -> Self { + WindowId(slotmap::KeyData::from_ffi(value)) + } +} + +/// A handle to a window with a specific root view type. +/// Note that this does not keep the window alive on its own. +#[derive(Deref, DerefMut)] +pub struct WindowHandle { + #[deref] + #[deref_mut] + pub(crate) any_handle: AnyWindowHandle, + state_type: PhantomData V>, +} + +impl Debug for WindowHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WindowHandle") + .field("any_handle", &self.any_handle.id.as_u64()) + .finish() + } +} + +impl WindowHandle { + /// Creates a new handle from a window ID. + /// This does not check if the root type of the window is `V`. + pub fn new(id: WindowId) -> Self { + WindowHandle { + any_handle: AnyWindowHandle { + id, + state_type: TypeId::of::(), + }, + state_type: PhantomData, + } + } + + /// Get the root view out of this window. + /// + /// This will fail if the window is closed or if the root view's type does not match `V`. + #[cfg(any(test, feature = "test-support"))] + pub fn root(&self, cx: &mut C) -> Result> + where + C: AppContext, + { + crate::Flatten::flatten(cx.update_window(self.any_handle, |root_view, _, _| { + root_view + .downcast::() + .map_err(|_| anyhow!("the type of the window's root view has changed")) + })) + } + + /// Updates the root view of this window. + /// + /// This will fail if the window has been closed or if the root view's type does not match + pub fn update( + &self, + cx: &mut C, + update: impl FnOnce(&mut V, &mut Window, &mut Context) -> R, + ) -> Result + where + C: AppContext, + { + cx.update_window(self.any_handle, |root_view, window, cx| { + let view = root_view + .downcast::() + .map_err(|_| anyhow!("the type of the window's root view has changed"))?; + + Ok(view.update(cx, |view, cx| update(view, window, cx))) + })? + } + + /// Read the root view out of this window. + /// + /// This will fail if the window is closed or if the root view's type does not match `V`. + pub fn read<'a>(&self, cx: &'a App) -> Result<&'a V> { + let x = cx + .windows + .get(self.id) + .and_then(|window| { + window + .as_deref() + .and_then(|window| window.root.clone()) + .map(|root_view| root_view.downcast::()) + }) + .context("window not found")? + .map_err(|_| anyhow!("the type of the window's root view has changed"))?; + + Ok(x.read(cx)) + } + + /// Read the root view out of this window, with a callback + /// + /// This will fail if the window is closed or if the root view's type does not match `V`. + pub fn read_with(&self, cx: &C, read_with: impl FnOnce(&V, &App) -> R) -> Result + where + C: AppContext, + { + cx.read_window(self, |root_view, cx| read_with(root_view.read(cx), cx)) + } + + /// Read the root view pointer off of this window. + /// + /// This will fail if the window is closed or if the root view's type does not match `V`. + pub fn entity(&self, cx: &C) -> Result> + where + C: AppContext, + { + cx.read_window(self, |root_view, _cx| root_view) + } + + /// Check if this window is 'active'. + /// + /// Will return `None` if the window is closed or currently + /// borrowed. + pub fn is_active(&self, cx: &mut App) -> Option { + cx.update_window(self.any_handle, |_, window, _| window.is_window_active()) + .ok() + } +} + +impl Copy for WindowHandle {} + +impl Clone for WindowHandle { + fn clone(&self) -> Self { + *self + } +} + +impl PartialEq for WindowHandle { + fn eq(&self, other: &Self) -> bool { + self.any_handle == other.any_handle + } +} + +impl Eq for WindowHandle {} + +impl Hash for WindowHandle { + fn hash(&self, state: &mut H) { + self.any_handle.hash(state); + } +} + +impl From> for AnyWindowHandle { + fn from(val: WindowHandle) -> Self { + val.any_handle + } +} + +/// A handle to a window with any root view type, which can be downcast to a window with a specific root view type. +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +pub struct AnyWindowHandle { + pub(crate) id: WindowId, + state_type: TypeId, +} + +impl AnyWindowHandle { + /// Get the ID of this window. + pub fn window_id(&self) -> WindowId { + self.id + } + + /// Attempt to convert this handle to a window handle with a specific root view type. + /// If the types do not match, this will return `None`. + pub fn downcast(&self) -> Option> { + if TypeId::of::() == self.state_type { + Some(WindowHandle { + any_handle: *self, + state_type: PhantomData, + }) + } else { + None + } + } + + /// Updates the state of the root view of this window. + /// + /// This will fail if the window has been closed. + pub fn update( + self, + cx: &mut C, + update: impl FnOnce(AnyView, &mut Window, &mut App) -> R, + ) -> Result + where + C: AppContext, + { + cx.update_window(self, update) + } + + /// Read the state of the root view of this window. + /// + /// This will fail if the window has been closed. + pub fn read(self, cx: &C, read: impl FnOnce(Entity, &App) -> R) -> Result + where + C: AppContext, + T: 'static, + { + let view = self + .downcast::() + .context("the type of the window's root view has changed")?; + + cx.read_window(&view, read) + } +} + +impl HasWindowHandle for Window { + fn window_handle(&self) -> Result, HandleError> { + self.platform_window.window_handle() + } +} + +impl HasDisplayHandle for Window { + fn display_handle( + &self, + ) -> std::result::Result, HandleError> { + self.platform_window.display_handle() + } +} + +/// An identifier for an [`Element`]. +/// +/// Can be constructed with a string, a number, or both, as well +/// as other internal representations. +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum ElementId { + /// The ID of a View element + View(EntityId), + /// An integer ID. + Integer(u64), + /// A string based ID. + Name(SharedString), + /// A UUID. + Uuid(Uuid), + /// An ID that's equated with a focus handle. + FocusHandle(FocusId), + /// A combination of a name and an integer. + NamedInteger(SharedString, u64), + /// A path. + Path(Arc), + /// A code location. + CodeLocation(core::panic::Location<'static>), + /// A labeled child of an element. + NamedChild(Box, SharedString), +} + +impl ElementId { + /// Constructs an `ElementId::NamedInteger` from a name and `usize`. + pub fn named_usize(name: impl Into, integer: usize) -> ElementId { + Self::NamedInteger(name.into(), integer as u64) + } +} + +impl Display for ElementId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ElementId::View(entity_id) => write!(f, "view-{}", entity_id)?, + ElementId::Integer(ix) => write!(f, "{}", ix)?, + ElementId::Name(name) => write!(f, "{}", name)?, + ElementId::FocusHandle(_) => write!(f, "FocusHandle")?, + ElementId::NamedInteger(s, i) => write!(f, "{}-{}", s, i)?, + ElementId::Uuid(uuid) => write!(f, "{}", uuid)?, + ElementId::Path(path) => write!(f, "{}", path.display())?, + ElementId::CodeLocation(location) => write!(f, "{}", location)?, + ElementId::NamedChild(id, name) => write!(f, "{}-{}", id, name)?, + } + + Ok(()) + } +} + +impl TryInto for ElementId { + type Error = anyhow::Error; + + fn try_into(self) -> anyhow::Result { + if let ElementId::Name(name) = self { + Ok(name) + } else { + anyhow::bail!("element id is not string") + } + } +} + +impl From for ElementId { + fn from(id: usize) -> Self { + ElementId::Integer(id as u64) + } +} + +impl From for ElementId { + fn from(id: i32) -> Self { + Self::Integer(id as u64) + } +} + +impl From for ElementId { + fn from(name: SharedString) -> Self { + ElementId::Name(name) + } +} + +impl From> for ElementId { + fn from(path: Arc) -> Self { + ElementId::Path(path) + } +} + +impl From<&'static str> for ElementId { + fn from(name: &'static str) -> Self { + ElementId::Name(name.into()) + } +} + +impl<'a> From<&'a FocusHandle> for ElementId { + fn from(handle: &'a FocusHandle) -> Self { + ElementId::FocusHandle(handle.id) + } +} + +impl From<(&'static str, EntityId)> for ElementId { + fn from((name, id): (&'static str, EntityId)) -> Self { + ElementId::NamedInteger(name.into(), id.as_u64()) + } +} + +impl From<(&'static str, usize)> for ElementId { + fn from((name, id): (&'static str, usize)) -> Self { + ElementId::NamedInteger(name.into(), id as u64) + } +} + +impl From<(SharedString, usize)> for ElementId { + fn from((name, id): (SharedString, usize)) -> Self { + ElementId::NamedInteger(name, id as u64) + } +} + +impl From<(&'static str, u64)> for ElementId { + fn from((name, id): (&'static str, u64)) -> Self { + ElementId::NamedInteger(name.into(), id) + } +} + +impl From for ElementId { + fn from(value: Uuid) -> Self { + Self::Uuid(value) + } +} + +impl From<(&'static str, u32)> for ElementId { + fn from((name, id): (&'static str, u32)) -> Self { + ElementId::NamedInteger(name.into(), id.into()) + } +} + +impl> From<(ElementId, T)> for ElementId { + fn from((id, name): (ElementId, T)) -> Self { + ElementId::NamedChild(Box::new(id), name.into()) + } +} + +impl From<&'static core::panic::Location<'static>> for ElementId { + fn from(location: &'static core::panic::Location<'static>) -> Self { + ElementId::CodeLocation(*location) + } +} + +/// A rectangle to be rendered in the window at the given position and size. +/// Passed as an argument [`Window::paint_quad`]. +#[derive(Clone)] +pub struct PaintQuad { + /// The bounds of the quad within the window. + pub bounds: Bounds, + /// The radii of the quad's corners. + pub corner_radii: Corners, + /// The background color of the quad. + pub background: Background, + /// The widths of the quad's borders. + pub border_widths: Edges, + /// The color of the quad's borders. + pub border_color: Hsla, + /// The style of the quad's borders. + pub border_style: BorderStyle, +} + +impl PaintQuad { + /// Sets the corner radii of the quad. + pub fn corner_radii(self, corner_radii: impl Into>) -> Self { + PaintQuad { + corner_radii: corner_radii.into(), + ..self + } + } + + /// Sets the border widths of the quad. + pub fn border_widths(self, border_widths: impl Into>) -> Self { + PaintQuad { + border_widths: border_widths.into(), + ..self + } + } + + /// Sets the border color of the quad. + pub fn border_color(self, border_color: impl Into) -> Self { + PaintQuad { + border_color: border_color.into(), + ..self + } + } + + /// Sets the background color of the quad. + pub fn background(self, background: impl Into) -> Self { + PaintQuad { + background: background.into(), + ..self + } + } +} + +/// Creates a quad with the given parameters. +pub fn quad( + bounds: Bounds, + corner_radii: impl Into>, + background: impl Into, + border_widths: impl Into>, + border_color: impl Into, + border_style: BorderStyle, +) -> PaintQuad { + PaintQuad { + bounds, + corner_radii: corner_radii.into(), + background: background.into(), + border_widths: border_widths.into(), + border_color: border_color.into(), + border_style, + } +} + +/// Creates a filled quad with the given bounds and background color. +pub fn fill(bounds: impl Into>, background: impl Into) -> PaintQuad { + PaintQuad { + bounds: bounds.into(), + corner_radii: (0.).into(), + background: background.into(), + border_widths: (0.).into(), + border_color: transparent_black(), + border_style: BorderStyle::default(), + } +} + +/// Creates a rectangle outline with the given bounds, border color, and a 1px border width +pub fn outline( + bounds: impl Into>, + border_color: impl Into, + border_style: BorderStyle, +) -> PaintQuad { + PaintQuad { + bounds: bounds.into(), + corner_radii: (0.).into(), + background: transparent_black().into(), + border_widths: (1.).into(), + border_color: border_color.into(), + border_style, + } +} diff --git a/third_party/gpui/src/window/prompts.rs b/third_party/gpui/src/window/prompts.rs new file mode 100644 index 0000000..778ee1d --- /dev/null +++ b/third_party/gpui/src/window/prompts.rs @@ -0,0 +1,231 @@ +use std::ops::Deref; + +use futures::channel::oneshot; + +use crate::{ + AnyView, App, AppContext as _, Context, Entity, EventEmitter, FocusHandle, Focusable, + InteractiveElement, IntoElement, ParentElement, PromptButton, PromptLevel, Render, + StatefulInteractiveElement, Styled, div, opaque_grey, white, +}; + +use super::Window; + +/// The event emitted when a prompt's option is selected. +/// The usize is the index of the selected option, from the actions +/// passed to the prompt. +pub struct PromptResponse(pub usize); + +/// A prompt that can be rendered in the window. +pub trait Prompt: EventEmitter + Focusable {} + +impl + Focusable> Prompt for V {} + +/// A handle to a prompt that can be used to interact with it. +pub struct PromptHandle { + sender: oneshot::Sender, +} + +impl PromptHandle { + pub(crate) fn new(sender: oneshot::Sender) -> Self { + Self { sender } + } + + /// Construct a new prompt handle from a view of the appropriate types + pub fn with_view( + self, + view: Entity, + window: &mut Window, + cx: &mut App, + ) -> RenderablePromptHandle { + let mut sender = Some(self.sender); + let previous_focus = window.focused(cx); + let window_handle = window.window_handle(); + cx.subscribe(&view, move |_: Entity, e: &PromptResponse, cx| { + if let Some(sender) = sender.take() { + sender.send(e.0).ok(); + window_handle + .update(cx, |_, window, _cx| { + window.prompt.take(); + if let Some(previous_focus) = &previous_focus { + window.focus(previous_focus); + } + }) + .ok(); + } + }) + .detach(); + + window.focus(&view.focus_handle(cx)); + + RenderablePromptHandle { + view: Box::new(view), + } + } +} + +/// A prompt handle capable of being rendered in a window. +pub struct RenderablePromptHandle { + pub(crate) view: Box, +} + +/// Use this function in conjunction with [App::set_prompt_builder] to force +/// GPUI to always use the fallback prompt renderer. +pub fn fallback_prompt_renderer( + level: PromptLevel, + message: &str, + detail: Option<&str>, + actions: &[PromptButton], + handle: PromptHandle, + window: &mut Window, + cx: &mut App, +) -> RenderablePromptHandle { + let renderer = cx.new(|cx| FallbackPromptRenderer { + _level: level, + message: message.to_string(), + detail: detail.map(ToString::to_string), + actions: actions.to_vec(), + focus: cx.focus_handle(), + }); + + handle.with_view(renderer, window, cx) +} + +/// The default GPUI fallback for rendering prompts, when the platform doesn't support it. +pub struct FallbackPromptRenderer { + _level: PromptLevel, + message: String, + detail: Option, + actions: Vec, + focus: FocusHandle, +} + +impl Render for FallbackPromptRenderer { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let prompt = div() + .cursor_default() + .track_focus(&self.focus) + .w_72() + .bg(white()) + .rounded_lg() + .overflow_hidden() + .p_3() + .child( + div() + .w_full() + .flex() + .flex_row() + .justify_around() + .child(div().overflow_hidden().child(self.message.clone())), + ) + .children(self.detail.clone().map(|detail| { + div() + .w_full() + .flex() + .flex_row() + .justify_around() + .text_sm() + .mb_2() + .child(div().child(detail)) + })) + .children(self.actions.iter().enumerate().map(|(ix, action)| { + div() + .flex() + .flex_row() + .justify_around() + .border_1() + .border_color(opaque_grey(0.2, 0.5)) + .mt_1() + .rounded_xs() + .cursor_pointer() + .text_sm() + .child(action.label().clone()) + .id(ix) + .on_click(cx.listener(move |_, _, _, cx| { + cx.emit(PromptResponse(ix)); + })) + })); + + div() + .size_full() + .child( + div() + .size_full() + .bg(opaque_grey(0.5, 0.6)) + .absolute() + .top_0() + .left_0(), + ) + .child( + div() + .size_full() + .absolute() + .top_0() + .left_0() + .flex() + .flex_col() + .justify_around() + .child( + div() + .w_full() + .flex() + .flex_row() + .justify_around() + .child(prompt), + ), + ) + } +} + +impl EventEmitter for FallbackPromptRenderer {} + +impl Focusable for FallbackPromptRenderer { + fn focus_handle(&self, _: &crate::App) -> FocusHandle { + self.focus.clone() + } +} + +pub(crate) trait PromptViewHandle { + fn any_view(&self) -> AnyView; +} + +impl PromptViewHandle for Entity { + fn any_view(&self) -> AnyView { + self.clone().into() + } +} + +pub(crate) enum PromptBuilder { + Default, + Custom( + Box< + dyn Fn( + PromptLevel, + &str, + Option<&str>, + &[PromptButton], + PromptHandle, + &mut Window, + &mut App, + ) -> RenderablePromptHandle, + >, + ), +} + +impl Deref for PromptBuilder { + type Target = dyn Fn( + PromptLevel, + &str, + Option<&str>, + &[PromptButton], + PromptHandle, + &mut Window, + &mut App, + ) -> RenderablePromptHandle; + + fn deref(&self) -> &Self::Target { + match self { + Self::Default => &fallback_prompt_renderer, + Self::Custom(f) => f.as_ref(), + } + } +}