re-re-fork (#28)
@@ -1,233 +1,151 @@
|
||||
[workspace]
|
||||
members = [
|
||||
".",
|
||||
"tooling/macros",
|
||||
"./crates/gpui/",
|
||||
"./crates/gpui_web/",
|
||||
"./crates/gpui_wgpu/",
|
||||
"./crates/gpui_linux/",
|
||||
"./crates/gpui_macos/",
|
||||
"./crates/gpui_tokio/",
|
||||
"./crates/gpui_macros/",
|
||||
"./crates/gpui_windows/",
|
||||
"./crates/gpui_platform/",
|
||||
"./crates/gpui_shared_string/",
|
||||
]
|
||||
default-members = [
|
||||
".",
|
||||
"./crates/gpui/",
|
||||
]
|
||||
resolver = "3"
|
||||
|
||||
[package]
|
||||
name = "gpui-ce"
|
||||
version = "0.3.3"
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
description = "A community fork of Zed's GPU-accelerated UI framework"
|
||||
repository = "https://github.com/gpui-ce/gpui-ce"
|
||||
publish = true
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://gpui-ce.github.io/"
|
||||
readme = "README.md"
|
||||
keywords = ["desktop", "gui", "immediate"]
|
||||
categories = ["gui"]
|
||||
autoexamples = false
|
||||
|
||||
[features]
|
||||
default = ["font-kit", "wayland", "x11", "windows-manifest"]
|
||||
test-support = [
|
||||
"leak-detection",
|
||||
"backtrace",
|
||||
"collections/test-support",
|
||||
"util/test-support",
|
||||
"http_client/test-support",
|
||||
"wayland",
|
||||
"x11",
|
||||
]
|
||||
inspector = ["gpui-ce-macros/inspector"]
|
||||
leak-detection = ["backtrace"]
|
||||
runtime_shaders = []
|
||||
wayland = [
|
||||
"bitflags",
|
||||
"ashpd/wayland",
|
||||
"font-kit",
|
||||
"calloop-wayland-source",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-cursor",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols-plasma",
|
||||
"wayland-protocols-wlr",
|
||||
"filedescriptor",
|
||||
"xkbcommon",
|
||||
"open",
|
||||
]
|
||||
x11 = [
|
||||
"ashpd",
|
||||
"font-kit",
|
||||
"as-raw-xcb-connection",
|
||||
"x11rb",
|
||||
"xkbcommon",
|
||||
"xim",
|
||||
"x11-clipboard",
|
||||
"filedescriptor",
|
||||
"open",
|
||||
]
|
||||
windows-manifest = []
|
||||
|
||||
[lib]
|
||||
name = "gpui"
|
||||
path = "src/gpui.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
[workspace.dependencies]
|
||||
accesskit = "0.24.0"
|
||||
accesskit_macos = "0.26.0"
|
||||
accesskit_unix = "0.21.0"
|
||||
accesskit_windows = "0.32.1"
|
||||
anyhow = "1.0.86"
|
||||
async-task = "4.7"
|
||||
backtrace = { version = "0.3", optional = true }
|
||||
circular-buffer = "1.0"
|
||||
collections = { package = "gpui_collections", version = "0.2.2" }
|
||||
ctor = "0.4.0"
|
||||
derive_more = "0.99.17"
|
||||
etagere = "0.2"
|
||||
futures = "0.3"
|
||||
futures-concurrency = "7"
|
||||
gpui-ce-macros = { path = "./tooling/macros", features = ["inspector"] }
|
||||
http_client = { package = "gpui_http_client", version = "0.2.2" }
|
||||
backtrace = "0.3"
|
||||
bitflags = "2.6.0"
|
||||
collections = { git = "https://github.com/zed-industries/zed", version = "0.1.0" }
|
||||
ctor = "1.0.6"
|
||||
derive_more = { version = "2.1.1", features = [
|
||||
"add",
|
||||
"add_assign",
|
||||
"deref",
|
||||
"deref_mut",
|
||||
"display",
|
||||
"from_str",
|
||||
"mul",
|
||||
"mul_assign",
|
||||
"not",
|
||||
] }
|
||||
futures = "0.3.32"
|
||||
futures-concurrency = "7.7.1"
|
||||
http_client = { git = "https://github.com/zed-industries/zed" }
|
||||
image = "0.25.1"
|
||||
inventory = "0.3.19"
|
||||
itertools = "0.14.0"
|
||||
libc = "0.2"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
|
||||
lyon = "1.0"
|
||||
num_cpus = "1.13"
|
||||
parking = "2.0.0"
|
||||
parking_lot = "0.12.1"
|
||||
pin-project = "1.1.10"
|
||||
postage = { version = "0.5", features = ["futures-traits"] }
|
||||
proptest = { git = "https://github.com/proptest-rs/proptest", rev = "3dca198a8fef1b32e3a66f1e1897c955b4dc5b5b", features = ["attr-macro"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
profiling = "1"
|
||||
proptest = "1"
|
||||
rand = "0.9"
|
||||
raw-window-handle = "0.6"
|
||||
refineable = { package = "gpui_refineable", version = "0.2.2" }
|
||||
resvg = { version = "0.45.0", default-features = false, features = [
|
||||
"text",
|
||||
"system-fonts",
|
||||
"memmap-fonts",
|
||||
] }
|
||||
regex = "1.5"
|
||||
refineable = { git = "https://github.com/zed-industries/zed" }
|
||||
scheduler = { git = "https://github.com/zed-industries/zed" }
|
||||
util_macros = { git = "https://github.com/zed-industries/zed" }
|
||||
schemars = { version = "1.0", features = ["indexmap2"] }
|
||||
seahash = "4.1"
|
||||
semver = { version = "1.0", features = ["serde"] }
|
||||
serde = { version = "1.0.221", features = ["derive", "rc"] }
|
||||
serde_json = { version = "1.0.144", features = ["preserve_order", "raw_value"] }
|
||||
slotmap = "1.0.6"
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = "2.0"
|
||||
spin = "0.10.0"
|
||||
smallvec = { version = "1.6", features = ["union", "const_new"] }
|
||||
async-channel = "2.5.0"
|
||||
stacksafe = "0.1"
|
||||
strum = { version = "0.27.2", features = ["derive"] }
|
||||
sum_tree = { package = "gpui_sum_tree", version = "0.2.2" }
|
||||
taffy = "=0.9.0"
|
||||
sum_tree = { git = "https://github.com/zed-industries/zed" }
|
||||
thiserror = "2.0.12"
|
||||
util = { package = "gpui_util", version = "0.2.2" }
|
||||
util_macros = { package = "gpui_util_macros", version = "0.2.2" }
|
||||
usvg = { version = "0.45.0", default-features = false }
|
||||
url = "2"
|
||||
hdrhistogram = "7"
|
||||
pollster = "0.4.0"
|
||||
url = "2.2"
|
||||
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
|
||||
waker-fn = "1.2.0"
|
||||
web-time = "1"
|
||||
async-channel = "2"
|
||||
pollster = "0.4"
|
||||
chrono = "0.4"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
block = "0.1"
|
||||
web-time = "1.1.0"
|
||||
cocoa = "=0.26.0"
|
||||
cocoa-foundation = "=0.2.0"
|
||||
core-foundation = "=0.10.0"
|
||||
core-foundation-sys = "0.8.6"
|
||||
core-graphics = "0.24"
|
||||
core-text = "21"
|
||||
core-video = { version = "0.4.3", features = ["metal"] }
|
||||
dispatch2 = "0.3.1"
|
||||
flume = "0.11"
|
||||
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "110523127440aefb11ce0cf280ae7c5071337ec5", package = "zed-font-kit", version = "0.14.1-zed", optional = true }
|
||||
foreign-types = "0.5"
|
||||
mach2 = "0.5"
|
||||
metal = "0.29"
|
||||
core-video = { version = "0.5.2", features = ["metal"] }
|
||||
media = { git = "https://github.com/zed-industries/zed" }
|
||||
objc = "0.2"
|
||||
objc2 = { version = "0.6", optional = true }
|
||||
objc2-metal = { version = "0.3", optional = true }
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))'.dependencies]
|
||||
pathfinder_geometry = "0.5"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
|
||||
flume = "0.11"
|
||||
oo7 = { version = "0.6", default-features = false, features = [
|
||||
"async-std",
|
||||
"native_crypto",
|
||||
mach2 = "0.5"
|
||||
metal = "0.33"
|
||||
scap = { git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197176fb19cd0f9b180dd36eaac", default-features = false, package = "zed-scap", version = "0.0.8-zed" }
|
||||
env_logger = "0.11"
|
||||
unicode-segmentation = "1.10"
|
||||
reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
||||
wasm-bindgen = "0.2.120"
|
||||
heck = "0.5"
|
||||
proc-macro2 = "1.0.93"
|
||||
syn = { version = "2.0.101", features = ["full", "extra-traits", "visit-mut"] }
|
||||
quote = "1.0.9"
|
||||
ashpd = { version = "0.13", default-features = false, features = [
|
||||
"async-io",
|
||||
"notification",
|
||||
"open_uri",
|
||||
"file_chooser",
|
||||
"settings",
|
||||
"trash"
|
||||
] }
|
||||
wgpu = "24"
|
||||
cosmic-text = "0.17.0"
|
||||
swash = "0.2.6"
|
||||
bytemuck = "1"
|
||||
libc = "0.2"
|
||||
smol = "2.0"
|
||||
util = { git = "https://github.com/zed-industries/zed" }
|
||||
wgpu = { git = "https://github.com/zed-industries/wgpu.git", rev = "357a0c56e0070480ad9daea5d2eaa83150b79e88" }
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
objc2-app-kit = { version = "0.3", default-features = false, features = [ "NSGraphics" ] }
|
||||
semver = { version = "1.0", features = ["serde"] }
|
||||
windows-core = "0.61"
|
||||
tokio = { version = "1" }
|
||||
gpui_util = { git = "https://github.com/zed-industries/zed" }
|
||||
|
||||
ashpd = { version = "0.11", default-features = false, features = ["async-std"], optional = true }
|
||||
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "110523127440aefb11ce0cf280ae7c5071337ec5", package = "zed-font-kit", version = "0.14.1-zed", features = [
|
||||
"source-fontconfig-dlopen",
|
||||
], optional = true }
|
||||
gpui = { path = "./crates/gpui/" }
|
||||
gpui_platform = { path = "./crates/gpui_platform/" }
|
||||
gpui_linux = { path = "./crates/gpui_linux/" }
|
||||
gpui_macos = { path = "./crates/gpui_macos/" }
|
||||
gpui_windows = { path = "./crates/gpui_windows/" }
|
||||
gpui_web = { path = "./crates/gpui_web/" }
|
||||
|
||||
calloop = "0.14.3"
|
||||
filedescriptor = { version = "0.8.2", optional = true }
|
||||
open = { version = "5.2.0", optional = true }
|
||||
gpui_wgpu = { path = "./crates/gpui_wgpu/" }
|
||||
|
||||
calloop-wayland-source = { version = "0.4.1", optional = true }
|
||||
wayland-backend = { version = "0.3.3", features = [
|
||||
"client_system",
|
||||
"dlopen",
|
||||
], optional = true }
|
||||
wayland-client = { version = "0.31.11", optional = true }
|
||||
wayland-cursor = { version = "0.31.11", optional = true }
|
||||
wayland-protocols = { version = "0.32.9", features = [
|
||||
"client",
|
||||
"staging",
|
||||
"unstable",
|
||||
], optional = true }
|
||||
wayland-protocols-plasma = { version = "0.3.9", features = [
|
||||
"client",
|
||||
], optional = true }
|
||||
wayland-protocols-wlr = { version = "0.3.9", features = [
|
||||
"client",
|
||||
], optional = true }
|
||||
gpui_macros = { path = "./crates/gpui_macros/" }
|
||||
gpui_shared_string = { path = "./crates/gpui_shared_string/" }
|
||||
gpui_tokio = { path = "./crates/gpui_tokio/" }
|
||||
|
||||
as-raw-xcb-connection = { version = "1", optional = true }
|
||||
x11rb = { version = "0.13.1", features = [
|
||||
"allow-unsafe-code",
|
||||
"xkb",
|
||||
"randr",
|
||||
"xinput",
|
||||
"cursor",
|
||||
"resource_manager",
|
||||
"sync",
|
||||
"dri3",
|
||||
], optional = true }
|
||||
xkbcommon = { version = "0.8.0", features = [
|
||||
"wayland",
|
||||
"x11",
|
||||
], optional = true }
|
||||
xim = { git = "https://github.com/zed-industries/xim-rs.git", rev = "16f35a2c881b815a2b6cdfd6687988e84f8447d8", features = [
|
||||
"x11rb-xcb",
|
||||
"x11rb-client",
|
||||
], package = "zed-xim", version = "0.4.0-zed", optional = true }
|
||||
x11-clipboard = { version = "0.9.3", optional = true }
|
||||
bitflags = { version = "2.6.0", optional = true }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
flume = "0.11"
|
||||
rand = "0.9"
|
||||
windows = { version = "0.61", features = [
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.61"
|
||||
features = [
|
||||
"Foundation_Numerics",
|
||||
"Globalization_DateTimeFormatting",
|
||||
"Storage_Search",
|
||||
"Storage_Streams",
|
||||
"System_Threading",
|
||||
"UI_ViewManagement",
|
||||
"Wdk_System_SystemServices",
|
||||
"Win32_Foundation",
|
||||
"Win32_Globalization",
|
||||
"Win32_Graphics_Direct3D",
|
||||
"Win32_Graphics_Direct3D11",
|
||||
"Win32_Graphics_Direct3D_Fxc",
|
||||
"Win32_Graphics_DirectComposition",
|
||||
"Win32_Graphics_DirectWrite",
|
||||
"Win32_Graphics_DirectManipulation",
|
||||
"Win32_Graphics_Dwm",
|
||||
"Win32_Graphics_Dxgi",
|
||||
"Win32_Graphics_Dxgi_Common",
|
||||
@@ -242,6 +160,7 @@ windows = { version = "0.61", features = [
|
||||
"Win32_System_Com",
|
||||
"Win32_System_Com_StructuredStorage",
|
||||
"Win32_System_Console",
|
||||
"Win32_System_Diagnostics_Debug",
|
||||
"Win32_System_DataExchange",
|
||||
"Win32_System_IO",
|
||||
"Win32_System_LibraryLoader",
|
||||
@@ -249,6 +168,7 @@ windows = { version = "0.61", features = [
|
||||
"Win32_System_Ole",
|
||||
"Win32_System_Performance",
|
||||
"Win32_System_Pipes",
|
||||
"Win32_System_RestartManager",
|
||||
"Win32_System_SystemInformation",
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_System_Threading",
|
||||
@@ -258,195 +178,18 @@ windows = { version = "0.61", features = [
|
||||
"Win32_UI_HiDpi",
|
||||
"Win32_UI_Input_Ime",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_UI_Input_Pointer",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_UI_Shell_Common",
|
||||
"Win32_UI_Shell_PropertiesSystem",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
windows-core = "0.61"
|
||||
windows-numerics = "0.2"
|
||||
windows-registry = "0.5"
|
||||
"Win32_Media",
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
backtrace = "0.3"
|
||||
collections = { package = "gpui_collections", version = "0.2.2", features = ["test-support"] }
|
||||
env_logger = "0.11"
|
||||
http_client = { package = "gpui_http_client", version = "0.2.2", features = ["test-support"] }
|
||||
lyon = { version = "1.0", features = ["extra"] }
|
||||
pretty_assertions = { version = "1.3.0", features = ["unstable"] }
|
||||
proptest = "1"
|
||||
rand = "0.9"
|
||||
unicode-segmentation = "1.10"
|
||||
util = { package = "gpui_util", version = "0.2.2", features = ["test-support"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.build-dependencies]
|
||||
embed-resource = "3.0"
|
||||
windows-registry = "0.5"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.build-dependencies]
|
||||
cbindgen = { version = "0.28.0", default-features = false }
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.build-dependencies]
|
||||
naga = { version = "29.0", features = ["wgsl-in"] }
|
||||
|
||||
# ============================================================================
|
||||
# Learn Examples - Educational examples for learning GPUI
|
||||
# ============================================================================
|
||||
|
||||
[[example]]
|
||||
name = "interactive_elements"
|
||||
path = "examples/learn/interactive_elements.rs"
|
||||
|
||||
[[example]]
|
||||
name = "creating_components"
|
||||
path = "examples/learn/creating_components.rs"
|
||||
|
||||
[[example]]
|
||||
name = "layout"
|
||||
path = "examples/learn/layout.rs"
|
||||
|
||||
[[example]]
|
||||
name = "styling"
|
||||
path = "examples/learn/styling.rs"
|
||||
|
||||
[[example]]
|
||||
name = "async_tasks"
|
||||
path = "examples/learn/async_tasks.rs"
|
||||
|
||||
[[example]]
|
||||
name = "custom_drawing"
|
||||
path = "examples/learn/custom_drawing.rs"
|
||||
|
||||
[[example]]
|
||||
name = "animation"
|
||||
path = "examples/learn/animation.rs"
|
||||
|
||||
[[example]]
|
||||
name = "text"
|
||||
path = "examples/learn/text.rs"
|
||||
|
||||
# ============================================================================
|
||||
# Bench Examples - Performance benchmarks
|
||||
# ============================================================================
|
||||
|
||||
[[example]]
|
||||
name = "data_table"
|
||||
path = "examples/bench/data_table.rs"
|
||||
|
||||
[[example]]
|
||||
name = "paths_bench"
|
||||
path = "examples/bench/paths_bench.rs"
|
||||
|
||||
[[example]]
|
||||
name = "pattern"
|
||||
path = "examples/bench/pattern.rs"
|
||||
|
||||
[[example]]
|
||||
name = "shadow"
|
||||
path = "examples/bench/shadow.rs"
|
||||
|
||||
# ============================================================================
|
||||
# Legacy Examples - Original examples (to be updated)
|
||||
# ============================================================================
|
||||
|
||||
[[example]]
|
||||
name = "focus_visible"
|
||||
path = "examples/legacy/focus_visible.rs"
|
||||
|
||||
[[example]]
|
||||
name = "gif_viewer"
|
||||
path = "examples/legacy/gif_viewer.rs"
|
||||
|
||||
[[example]]
|
||||
name = "gradient"
|
||||
path = "examples/legacy/gradient.rs"
|
||||
|
||||
[[example]]
|
||||
name = "hello_world"
|
||||
path = "examples/legacy/hello_world.rs"
|
||||
|
||||
[[example]]
|
||||
name = "image_loading"
|
||||
path = "examples/legacy/image_loading.rs"
|
||||
|
||||
[[example]]
|
||||
name = "input"
|
||||
path = "examples/legacy/input.rs"
|
||||
|
||||
[[example]]
|
||||
name = "layer_shell"
|
||||
path = "examples/legacy/layer_shell.rs"
|
||||
|
||||
[[example]]
|
||||
name = "on_window_close_quit"
|
||||
path = "examples/legacy/on_window_close_quit.rs"
|
||||
|
||||
[[example]]
|
||||
name = "opacity"
|
||||
path = "examples/legacy/opacity.rs"
|
||||
|
||||
[[example]]
|
||||
name = "scrollable"
|
||||
path = "examples/legacy/scrollable.rs"
|
||||
|
||||
[[example]]
|
||||
name = "svg"
|
||||
path = "examples/legacy/svg/svg.rs"
|
||||
|
||||
[[example]]
|
||||
name = "tab_stop"
|
||||
path = "examples/legacy/tab_stop.rs"
|
||||
|
||||
[[example]]
|
||||
name = "tree"
|
||||
path = "examples/legacy/tree.rs"
|
||||
|
||||
[[example]]
|
||||
name = "uniform_list"
|
||||
path = "examples/legacy/uniform_list.rs"
|
||||
|
||||
[[example]]
|
||||
name = "window"
|
||||
path = "examples/legacy/window.rs"
|
||||
|
||||
[[example]]
|
||||
name = "window_positioning"
|
||||
path = "examples/legacy/window_positioning.rs"
|
||||
|
||||
[[example]]
|
||||
name = "window_shadow"
|
||||
path = "examples/legacy/window_shadow.rs"
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = "unpacked"
|
||||
incremental = false
|
||||
codegen-units = 16
|
||||
|
||||
[profile.dev.build-override]
|
||||
codegen-units = 16
|
||||
|
||||
[profile.dev.package]
|
||||
quote = { opt-level = 3 }
|
||||
syn = { opt-level = 3 }
|
||||
proc-macro2 = { opt-level = 3 }
|
||||
taffy = { opt-level = 3 }
|
||||
resvg = { opt-level = 3 }
|
||||
|
||||
[profile.release]
|
||||
debug = "limited"
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
|
||||
[profile.release-fast]
|
||||
inherits = "release"
|
||||
debug = "full"
|
||||
lto = false
|
||||
codegen-units = 16
|
||||
|
||||
[lints.rust]
|
||||
[workspace.lints.rust]
|
||||
unexpected_cfgs = { level = "allow" }
|
||||
|
||||
[lints.clippy]
|
||||
[workspace.lints.clippy]
|
||||
dbg_macro = "deny"
|
||||
todo = "deny"
|
||||
declare_interior_mutable_const = "deny"
|
||||
@@ -459,4 +202,3 @@ single_range_in_vec_init = "allow"
|
||||
too_many_arguments = "allow"
|
||||
large_enum_variant = "allow"
|
||||
nonminimal_bool = "allow"
|
||||
|
||||
|
||||
@@ -7,12 +7,19 @@ A community fork of [GPUI](https://gpui.rs), Zed's GPU-accelerated UI framework.
|
||||
```toml
|
||||
[dependencies]
|
||||
gpui = { package = "gpui-ce", version = "0.3" }
|
||||
gpui_platform = { git = "https://github.com/gpui-ce/gpui-ce" }
|
||||
|
||||
# for test support...
|
||||
[dev-dependencies]
|
||||
gpui = { package = "gpui-ce", version = "0.3", features = ["test-support"] }
|
||||
```
|
||||
|
||||
or for using the git version
|
||||
|
||||
```toml
|
||||
gpui = { package = "gpui", git = "https://github.com/gpui-ce/gpui-ce" }
|
||||
```
|
||||
|
||||
Then use `gpui::{import}` as normal.
|
||||
|
||||
---
|
||||
@@ -24,7 +31,7 @@ todo: rewrite below...
|
||||
GPUI is a hybrid immediate and retained mode, GPU accelerated, UI framework
|
||||
for Rust, designed to support a wide variety of applications.
|
||||
|
||||
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.
|
||||
Everything in GPUI starts with an `Application`. You can create one with `gpui_platform::application()`, 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
|
||||
|
||||
@@ -75,4 +82,3 @@ In addition to the systems above, GPUI provides a range of smaller services that
|
||||
- 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 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).
|
||||
|
||||
|
||||
@@ -1,421 +0,0 @@
|
||||
#![allow(clippy::disallowed_methods, reason = "build scripts are exempt")]
|
||||
|
||||
use std::env;
|
||||
|
||||
fn main() {
|
||||
println!("cargo::rustc-check-cfg=cfg(gles)");
|
||||
|
||||
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||
|
||||
match target_os.as_str() {
|
||||
"macos" => {
|
||||
#[cfg(target_os = "macos")]
|
||||
macos::build();
|
||||
}
|
||||
"windows" => {
|
||||
#[cfg(target_os = "windows")]
|
||||
windows::build();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos {
|
||||
use std::{
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use cbindgen::Config;
|
||||
|
||||
pub fn build() {
|
||||
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_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();
|
||||
|
||||
// Source files that define types used in shaders
|
||||
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) {
|
||||
fn stitch_header(header: &Path, shader_path: &Path) -> std::io::Result<PathBuf> {
|
||||
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(shader_source_path);
|
||||
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 {
|
||||
pub fn build() {
|
||||
#[cfg(not(debug_assertions))]
|
||||
shader_compilation::compile_shaders();
|
||||
|
||||
#[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();
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
mod shader_compilation {
|
||||
use std::{
|
||||
fs,
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
process::{self, Command},
|
||||
};
|
||||
|
||||
pub 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());
|
||||
|
||||
let fxc_path = find_fxc_compiler();
|
||||
|
||||
let modules = [
|
||||
"quad",
|
||||
"shadow",
|
||||
"path_rasterization",
|
||||
"path_sprite",
|
||||
"underline",
|
||||
"monochrome_sprite",
|
||||
"subpixel_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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn find_latest_windows_sdk_binary(
|
||||
binary: &str,
|
||||
) -> Result<Option<PathBuf>, Box<dyn std::error::Error>> {
|
||||
let key = windows_registry::LOCAL_MACHINE
|
||||
.open("SOFTWARE\\WOW6432Node\\Microsoft\\Microsoft SDKs\\Windows\\v10.0")?;
|
||||
|
||||
let install_folder: String = key.get_string("InstallationFolder")?;
|
||||
let install_folder_bin = Path::new(&install_folder).join("bin");
|
||||
|
||||
let mut versions: Vec<_> = std::fs::read_dir(&install_folder_bin)?
|
||||
.flatten()
|
||||
.filter(|entry| entry.path().is_dir())
|
||||
.filter_map(|entry| entry.file_name().into_string().ok())
|
||||
.collect();
|
||||
|
||||
versions.sort_by_key(|s| {
|
||||
s.split('.')
|
||||
.filter_map(|p| p.parse().ok())
|
||||
.collect::<Vec<u32>>()
|
||||
});
|
||||
|
||||
let arch = match std::env::consts::ARCH {
|
||||
"x86_64" => "x64",
|
||||
"aarch64" => "arm64",
|
||||
_ => Err(format!(
|
||||
"Unsupported architecture: {}",
|
||||
std::env::consts::ARCH
|
||||
))?,
|
||||
};
|
||||
|
||||
if let Some(highest_version) = versions.last() {
|
||||
return Ok(Some(
|
||||
install_folder_bin
|
||||
.join(highest_version)
|
||||
.join(arch)
|
||||
.join(binary),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn find_fxc_compiler() -> String {
|
||||
if let Ok(path) = std::env::var("GPUI_FXC_PATH")
|
||||
&& Path::new(&path).exists()
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
if let Ok(Some(path)) = find_latest_windows_sdk_binary("fxc.exe") {
|
||||
return path.to_string_lossy().into_owned();
|
||||
}
|
||||
|
||||
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,
|
||||
) {
|
||||
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);
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
[package]
|
||||
name = "gpui"
|
||||
version = "0.2.2"
|
||||
edition.workspace = true
|
||||
authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
description = "Zed's GPU-accelerated UI framework"
|
||||
repository = "https://github.com/zed-industries/zed"
|
||||
publish = true
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://gpui.rs"
|
||||
readme = "README.md"
|
||||
keywords = ["desktop", "gui", "immediate"]
|
||||
categories = ["gui"]
|
||||
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
default = ["font-kit", "wayland", "x11", "windows-manifest"]
|
||||
test-support = [
|
||||
"leak-detection",
|
||||
"collections/test-support",
|
||||
"http_client/test-support",
|
||||
"wayland",
|
||||
"x11",
|
||||
"proptest",
|
||||
]
|
||||
inspector = ["gpui_macros/inspector"]
|
||||
leak-detection = ["backtrace"]
|
||||
wayland = [
|
||||
"bitflags",
|
||||
]
|
||||
x11 = [
|
||||
"scap?/x11",
|
||||
]
|
||||
screen-capture = [
|
||||
"scap",
|
||||
]
|
||||
windows-manifest = ["dep:embed-resource"]
|
||||
input-latency-histogram = ["dep:hdrhistogram"]
|
||||
|
||||
[lib]
|
||||
path = "src/gpui.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
accesskit.workspace = true
|
||||
anyhow.workspace = true
|
||||
async-task = "4.7"
|
||||
backtrace = { workspace = true, optional = true }
|
||||
bitflags = { workspace = true, optional = true }
|
||||
|
||||
collections.workspace = true
|
||||
ctor.workspace = true
|
||||
derive_more.workspace = true
|
||||
etagere = "0.2"
|
||||
futures.workspace = true
|
||||
futures-concurrency.workspace = true
|
||||
gpui_macros.workspace = true
|
||||
gpui_shared_string.workspace = true
|
||||
http_client.workspace = true
|
||||
image.workspace = true
|
||||
inventory.workspace = true
|
||||
itertools.workspace = true
|
||||
log.workspace = true
|
||||
num_cpus = "1.13"
|
||||
parking = "2.0.0"
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
proptest = { workspace = true, optional = true }
|
||||
chrono.workspace = true
|
||||
profiling.workspace = true
|
||||
rand.workspace = true
|
||||
raw-window-handle = "0.6"
|
||||
regex.workspace = true
|
||||
refineable.workspace = true
|
||||
scheduler.workspace = true
|
||||
resvg = { version = "0.45.0", default-features = false, features = [
|
||||
"text",
|
||||
"system-fonts",
|
||||
"memmap-fonts",
|
||||
"raster-images"
|
||||
] }
|
||||
usvg = { version = "0.45.0", default-features = false }
|
||||
ttf-parser = "0.25"
|
||||
util_macros.workspace = true
|
||||
schemars.workspace = true
|
||||
seahash = "4.1"
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
slotmap.workspace = true
|
||||
smallvec.workspace = true
|
||||
async-channel.workspace = true
|
||||
stacksafe.workspace = true
|
||||
strum.workspace = true
|
||||
sum_tree.workspace = true
|
||||
taffy = "=0.10.1"
|
||||
thiserror.workspace = true
|
||||
gpui_util.workspace = true
|
||||
hdrhistogram = { workspace = true, optional = true }
|
||||
waker-fn = "1.2.0"
|
||||
lyon = "1.0"
|
||||
pin-project = "1.1.10"
|
||||
spin = "0.10.0"
|
||||
pollster.workspace = true
|
||||
url.workspace = true
|
||||
uuid.workspace = true
|
||||
web-time.workspace = true
|
||||
|
||||
[target.'cfg(target_family = "wasm")'.dependencies]
|
||||
getrandom = { version = "0.3.4", features = ["wasm_js"] }
|
||||
uuid = { workspace = true, features = ["js"] }
|
||||
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
block = "0.1"
|
||||
cocoa.workspace = true
|
||||
cocoa-foundation.workspace = true
|
||||
core-foundation.workspace = true
|
||||
core-foundation-sys.workspace = true
|
||||
core-graphics = "0.24"
|
||||
core-video.workspace = true
|
||||
core-text = "21"
|
||||
# WARNING: If you change this, you must also publish a new version of zed-font-kit to crates.io
|
||||
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "94b0f28166665e8fd2f53ff6d268a14955c82269", package = "zed-font-kit", version = "0.14.1-zed", optional = true }
|
||||
foreign-types = "0.5"
|
||||
log.workspace = true
|
||||
media.workspace = true
|
||||
objc.workspace = true
|
||||
objc2 = { version = "0.6", optional = true }
|
||||
objc2-metal = { version = "0.3", optional = true }
|
||||
mach2.workspace = true
|
||||
#TODO: replace with "objc2"
|
||||
metal.workspace = true
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))'.dependencies]
|
||||
pathfinder_geometry = "0.5"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))'.dependencies]
|
||||
scap = { workspace = true, optional = true }
|
||||
|
||||
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = { version = "0.61", features = ["Win32_Foundation"] }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
backtrace.workspace = true
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
gpui_platform = { workspace = true, features = ["font-kit", "wayland", "x11"] }
|
||||
gpui_util = { workspace = true }
|
||||
lyon = { version = "1.0", features = ["extra"] }
|
||||
proptest = { workspace = true }
|
||||
rand.workspace = true
|
||||
scheduler = { workspace = true, features = ["test-support"] }
|
||||
unicode-segmentation = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_family = "wasm"))'.dev-dependencies]
|
||||
http_client = { workspace = true, features = ["test-support"] }
|
||||
reqwest_client = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[target.'cfg(target_family = "wasm")'.dev-dependencies]
|
||||
wasm-bindgen = { workspace = true }
|
||||
gpui_web.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
embed-resource = { version = "3.0", optional = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.build-dependencies]
|
||||
bindgen = "0.71"
|
||||
cbindgen = { version = "0.28.0", default-features = false }
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Learn Examples - Educational examples for learning GPUI
|
||||
# ============================================================================
|
||||
|
||||
[[example]]
|
||||
name = "interactive_elements"
|
||||
path = "examples/learn/interactive_elements.rs"
|
||||
|
||||
[[example]]
|
||||
name = "creating_components"
|
||||
path = "examples/learn/creating_components.rs"
|
||||
|
||||
[[example]]
|
||||
name = "layout"
|
||||
path = "examples/learn/layout.rs"
|
||||
|
||||
[[example]]
|
||||
name = "styling"
|
||||
path = "examples/learn/styling.rs"
|
||||
|
||||
[[example]]
|
||||
name = "async_tasks"
|
||||
path = "examples/learn/async_tasks.rs"
|
||||
|
||||
[[example]]
|
||||
name = "custom_drawing"
|
||||
path = "examples/learn/custom_drawing.rs"
|
||||
|
||||
[[example]]
|
||||
name = "animation"
|
||||
path = "examples/learn/animation.rs"
|
||||
|
||||
[[example]]
|
||||
name = "text"
|
||||
path = "examples/learn/text.rs"
|
||||
|
||||
# ============================================================================
|
||||
# Bench Examples - Performance benchmarks
|
||||
# ============================================================================
|
||||
|
||||
[[example]]
|
||||
name = "data_table"
|
||||
path = "examples/bench/data_table.rs"
|
||||
|
||||
[[example]]
|
||||
name = "paths_bench"
|
||||
path = "examples/bench/paths_bench.rs"
|
||||
|
||||
[[example]]
|
||||
name = "pattern"
|
||||
path = "examples/bench/pattern.rs"
|
||||
|
||||
[[example]]
|
||||
name = "shadow"
|
||||
path = "examples/bench/shadow.rs"
|
||||
|
||||
# ============================================================================
|
||||
# Legacy Examples - Original examples (to be updated)
|
||||
# ============================================================================
|
||||
|
||||
[[example]]
|
||||
name = "focus_visible"
|
||||
path = "examples/legacy/focus_visible.rs"
|
||||
|
||||
[[example]]
|
||||
name = "gif_viewer"
|
||||
path = "examples/legacy/gif_viewer.rs"
|
||||
|
||||
[[example]]
|
||||
name = "gradient"
|
||||
path = "examples/legacy/gradient.rs"
|
||||
|
||||
[[example]]
|
||||
name = "hello_world"
|
||||
path = "examples/legacy/hello_world.rs"
|
||||
|
||||
[[example]]
|
||||
name = "image_loading"
|
||||
path = "examples/legacy/image_loading.rs"
|
||||
|
||||
[[example]]
|
||||
name = "input"
|
||||
path = "examples/legacy/input.rs"
|
||||
|
||||
[[example]]
|
||||
name = "layer_shell"
|
||||
path = "examples/legacy/layer_shell.rs"
|
||||
|
||||
[[example]]
|
||||
name = "on_window_close_quit"
|
||||
path = "examples/legacy/on_window_close_quit.rs"
|
||||
|
||||
[[example]]
|
||||
name = "opacity"
|
||||
path = "examples/legacy/opacity.rs"
|
||||
|
||||
[[example]]
|
||||
name = "scrollable"
|
||||
path = "examples/legacy/scrollable.rs"
|
||||
|
||||
[[example]]
|
||||
name = "svg"
|
||||
path = "examples/legacy/svg/svg.rs"
|
||||
|
||||
[[example]]
|
||||
name = "tab_stop"
|
||||
path = "examples/legacy/tab_stop.rs"
|
||||
|
||||
[[example]]
|
||||
name = "tree"
|
||||
path = "examples/legacy/tree.rs"
|
||||
|
||||
[[example]]
|
||||
name = "uniform_list"
|
||||
path = "examples/legacy/uniform_list.rs"
|
||||
|
||||
[[example]]
|
||||
name = "window"
|
||||
path = "examples/legacy/window.rs"
|
||||
|
||||
[[example]]
|
||||
name = "window_positioning"
|
||||
path = "examples/legacy/window_positioning.rs"
|
||||
|
||||
[[example]]
|
||||
name = "window_shadow"
|
||||
path = "examples/legacy/window_shadow.rs"
|
||||
@@ -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
|
||||
@@ -0,0 +1,67 @@
|
||||
# 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](_ownership_and_data_flow)
|
||||
- [Accessibility](_accessibility)
|
||||
|
||||
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](<https://en.wikipedia.org/wiki/Register_(sociolinguistics)>) 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 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).
|
||||
@@ -0,0 +1,23 @@
|
||||
#![allow(clippy::disallowed_methods, reason = "build scripts are exempt")]
|
||||
|
||||
fn main() {
|
||||
println!("cargo::rustc-check-cfg=cfg(gles)");
|
||||
|
||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||
|
||||
if target_os == "windows" {
|
||||
#[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();
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::{ops::Range, rc::Rc, time::Duration};
|
||||
|
||||
use gpui::{
|
||||
App, Application, Bounds, Context, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point,
|
||||
Render, SharedString, UniformListScrollHandle, Window, WindowBounds, WindowOptions, canvas,
|
||||
div, point, prelude::*, px, rgb, size, uniform_list,
|
||||
canvas, div, point, prelude::*, px, rgb, size, uniform_list, App, Application, Bounds, Context,
|
||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, Render, SharedString,
|
||||
UniformListScrollHandle, Window, WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
const TOTAL_ITEMS: usize = 10000;
|
||||
@@ -447,7 +447,7 @@ impl Render for DataTable {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
focus: true,
|
||||
@@ -1,7 +1,7 @@
|
||||
use gpui::{
|
||||
Application, Background, Bounds, ColorSpace, Context, Path, PathBuilder, Pixels, Render,
|
||||
TitlebarOptions, Window, WindowBounds, WindowOptions, canvas, div, linear_color_stop,
|
||||
linear_gradient, point, prelude::*, px, rgb, size,
|
||||
canvas, div, linear_color_stop, linear_gradient, point, prelude::*, px, rgb, size, Application,
|
||||
Background, Bounds, ColorSpace, Context, Path, PathBuilder, Pixels, Render, TitlebarOptions,
|
||||
Window, WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
const DEFAULT_WINDOW_WIDTH: Pixels = px(1024.0);
|
||||
@@ -69,7 +69,7 @@ impl Render for PaintingViewer {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx| {
|
||||
gpui_platform::application().run(|cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
titlebar: Some(TitlebarOptions {
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{
|
||||
App, AppContext, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div,
|
||||
linear_color_stop, linear_gradient, pattern_slash, prelude::*, px, rgb, size,
|
||||
div, linear_color_stop, linear_gradient, pattern_slash, prelude::*, px, rgb, size, App,
|
||||
AppContext, Application, Bounds, Context, Window, WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
struct PatternExample;
|
||||
@@ -99,7 +99,7 @@ impl Render for PatternExample {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{
|
||||
App, Application, Bounds, BoxShadow, Context, Div, SharedString, Window, WindowBounds,
|
||||
WindowOptions, current_platform, div, hsla, point, prelude::*, px, relative, rgb, size,
|
||||
current_platform, div, hsla, point, prelude::*, px, relative, rgb, size, App, Application,
|
||||
Bounds, BoxShadow, Context, Div, SharedString, Window, WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
struct Shadow {}
|
||||
@@ -569,7 +569,7 @@ impl Render for Shadow {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::with_platform(current_platform(false)).run(|cx: &mut App| {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(1000.0), px(800.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
@@ -15,9 +15,9 @@ use std::time::Duration;
|
||||
use anyhow::Result;
|
||||
use gpui::colors::Colors;
|
||||
use gpui::{
|
||||
bounce, div, ease_in_out, linear, percentage, prelude::*, px, rgb, size as gpui_size, svg,
|
||||
Animation, AnimationExt as _, App, Application, AssetSource, Bounds, Context, Hsla,
|
||||
SharedString, Transformation, Window, WindowBounds, WindowOptions, bounce, div, ease_in_out,
|
||||
linear, percentage, prelude::*, px, rgb, size as gpui_size, svg,
|
||||
SharedString, Transformation, Window, WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
struct Assets {}
|
||||
@@ -261,7 +261,7 @@ fn section(colors: &Colors, title: &'static str, content: impl IntoElement) -> i
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new()
|
||||
gpui_platform::application()
|
||||
.with_assets(Assets {})
|
||||
.run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, gpui_size(px(500.), px(650.)), cx);
|
||||
@@ -14,8 +14,8 @@ use std::time::Duration;
|
||||
|
||||
use gpui::colors::Colors;
|
||||
use gpui::{
|
||||
App, Application, Bounds, Context, Entity, Render, Task, Window, WindowBounds, WindowOptions,
|
||||
div, prelude::*, px, rgb, size,
|
||||
div, prelude::*, px, rgb, size, App, Application, Bounds, Context, Entity, Render, Task,
|
||||
Window, WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
// Example 1: Simple Foreground Task
|
||||
@@ -138,24 +138,22 @@ impl CancellableTaskDemo {
|
||||
self.counting_task = None;
|
||||
cx.notify();
|
||||
} else {
|
||||
self.counting_task = Some(cx.spawn(async move |this, cx| {
|
||||
loop {
|
||||
cx.background_spawn(async {
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
self.counting_task = Some(cx.spawn(async move |this, cx| loop {
|
||||
cx.background_spawn(async {
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
})
|
||||
.await;
|
||||
|
||||
let should_continue = this
|
||||
.update(cx, |this, cx| {
|
||||
this.counter += 1;
|
||||
cx.notify();
|
||||
true
|
||||
})
|
||||
.await;
|
||||
.unwrap_or(false);
|
||||
|
||||
let should_continue = this
|
||||
.update(cx, |this, cx| {
|
||||
this.counter += 1;
|
||||
cx.notify();
|
||||
true
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
if !should_continue {
|
||||
break;
|
||||
}
|
||||
if !should_continue {
|
||||
break;
|
||||
}
|
||||
}));
|
||||
cx.notify();
|
||||
@@ -546,7 +544,7 @@ fn progress_bar(colors: &Colors, progress: u32) -> impl IntoElement {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(550.), px(850.)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
@@ -13,8 +13,8 @@ mod example_prelude;
|
||||
use example_prelude::init_example;
|
||||
use gpui::colors::Colors;
|
||||
use gpui::{
|
||||
App, Application, Bounds, Context, Entity, IntoElement, Render, RenderOnce, Window,
|
||||
WindowBounds, WindowOptions, div, prelude::*, px, rgb, size,
|
||||
div, prelude::*, px, rgb, size, App, Application, Bounds, Context, Entity, IntoElement, Render,
|
||||
RenderOnce, Window, WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -419,7 +419,7 @@ impl Render for CreatingComponentsExample {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(700.), px(400.)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
@@ -9,9 +9,9 @@
|
||||
|
||||
use gpui::colors::Colors;
|
||||
use gpui::{
|
||||
App, Application, Bounds, Context, Hsla, MouseButton, MouseDownEvent, MouseMoveEvent,
|
||||
MouseUpEvent, Path, PathBuilder, Pixels, Point, Render, Rgba, Window, WindowBounds,
|
||||
WindowOptions, canvas, div, fill, point, prelude::*, px, rgb, size,
|
||||
canvas, div, fill, point, prelude::*, px, rgb, size, App, Application, Bounds, Context, Hsla,
|
||||
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, PathBuilder, Pixels, Point,
|
||||
Render, Rgba, Window, WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
#[path = "../prelude.rs"]
|
||||
@@ -464,7 +464,7 @@ fn section(
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(550.), px(800.)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
@@ -13,9 +13,9 @@ mod example_prelude;
|
||||
use example_prelude::init_example;
|
||||
use gpui::colors::Colors;
|
||||
use gpui::{
|
||||
App, Application, Bounds, ClickEvent, Context, Entity, Half, Hsla, IntoElement, MouseButton,
|
||||
MouseMoveEvent, Pixels, Point, Render, Window, WindowBounds, WindowOptions, div, prelude::*,
|
||||
px, rgb, size,
|
||||
div, prelude::*, px, rgb, size, App, Application, Bounds, ClickEvent, Context, Entity, Half,
|
||||
Hsla, IntoElement, MouseButton, MouseMoveEvent, Pixels, Point, Render, Window, WindowBounds,
|
||||
WindowOptions,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -552,7 +552,7 @@ impl Render for InteractiveElementsExample {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(700.), px(650.)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
@@ -12,8 +12,8 @@ mod example_prelude;
|
||||
use example_prelude::init_example;
|
||||
use gpui::colors::Colors;
|
||||
use gpui::{
|
||||
App, Application, Bounds, Context, Div, Hsla, Render, Rgba, Window, WindowBounds,
|
||||
WindowOptions, div, prelude::*, px, size,
|
||||
div, prelude::*, px, size, App, Application, Bounds, Context, Div, Hsla, Render, Rgba, Window,
|
||||
WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
// Helper: Colored block for visualization
|
||||
@@ -484,7 +484,7 @@ fn section(colors: &Colors, title: &'static str, content: impl IntoElement) -> i
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
gpui_platform::application().run(|cx| {
|
||||
let bounds = Bounds::centered(None, size(px(650.), px(700.)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
use gpui::colors::Colors;
|
||||
use gpui::{
|
||||
App, Application, Bounds, Context, FocusHandle, Hsla, KeyBinding, Menu, MenuItem, Render, Rgba,
|
||||
Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, rgb, size,
|
||||
actions, div, prelude::*, px, rgb, size, App, Application, Bounds, Context, FocusHandle, Hsla,
|
||||
KeyBinding, Menu, MenuItem, Render, Rgba, Window, WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
actions!(styling_example, [Quit, Tab, TabPrev]);
|
||||
@@ -480,7 +480,7 @@ fn color_swatch(colors: &Colors, name: &'static str, color: Rgba) -> impl IntoEl
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
cx.activate(true);
|
||||
cx.on_action(|_: &Quit, cx| cx.quit());
|
||||
cx.bind_keys([
|
||||
@@ -14,9 +14,9 @@ mod example_prelude;
|
||||
|
||||
use example_prelude::init_example;
|
||||
use gpui::{
|
||||
App, Application, Bounds, Context, FontStyle, FontWeight, Hsla, Render, StyledText,
|
||||
TextOverflow, Window, WindowBounds, WindowOptions, colors::Colors, current_platform, div,
|
||||
prelude::*, px, relative, rgb, size,
|
||||
colors::Colors, current_platform, div, prelude::*, px, relative, rgb, size, App, Application,
|
||||
Bounds, Context, FontStyle, FontWeight, Hsla, Render, StyledText, TextOverflow, Window,
|
||||
WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
// Text Styling Examples
|
||||
@@ -567,7 +567,7 @@ fn section(colors: &Colors, title: &'static str, content: impl IntoElement) -> i
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::with_platform(current_platform(false)).run(|cx: &mut App| {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(650.), px(900.)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{
|
||||
App, Application, Bounds, Context, Div, ElementId, FocusHandle, KeyBinding, SharedString,
|
||||
Stateful, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, size,
|
||||
actions, div, prelude::*, px, size, App, Application, Bounds, Context, Div, ElementId,
|
||||
FocusHandle, KeyBinding, SharedString, Stateful, Window, WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
actions!(example, [Tab, TabPrev, Quit]);
|
||||
@@ -192,7 +192,7 @@ impl Render for Example {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("tab", Tab, None),
|
||||
KeyBinding::new("shift-tab", TabPrev, None),
|
||||
@@ -1,4 +1,4 @@
|
||||
use gpui::{App, Application, Context, Render, Window, WindowOptions, div, img, prelude::*};
|
||||
use gpui::{div, img, prelude::*, App, Application, Context, Render, Window, WindowOptions};
|
||||
use std::path::PathBuf;
|
||||
|
||||
struct GifViewer {
|
||||
@@ -24,7 +24,7 @@ impl Render for GifViewer {
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
Application::new().run(|cx: &mut App| {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
let gif_path =
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples/image/black-cat-typing.gif");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{
|
||||
App, Application, Bounds, ColorSpace, Context, Half, Render, Window, WindowOptions, canvas,
|
||||
div, linear_color_stop, linear_gradient, point, prelude::*, px, size,
|
||||
canvas, div, linear_color_stop, linear_gradient, point, prelude::*, px, size, App, Application,
|
||||
Bounds, ColorSpace, Context, Half, Render, Window, WindowOptions,
|
||||
};
|
||||
|
||||
struct GradientViewer {
|
||||
@@ -243,7 +243,7 @@ impl Render for GradientViewer {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
focus: true,
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{
|
||||
App, Application, Bounds, Context, SharedString, Window, WindowBounds, WindowOptions, div,
|
||||
prelude::*, px, rgb, size,
|
||||
div, prelude::*, px, rgb, size, App, Application, Bounds, Context, SharedString, Window,
|
||||
WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
struct HelloWorld {
|
||||
@@ -87,7 +87,7 @@ impl Render for HelloWorld {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 748 B After Width: | Height: | Size: 748 B |
|
Before Width: | Height: | Size: 4.3 MiB After Width: | Height: | Size: 4.3 MiB |
|
Before Width: | Height: | Size: 746 B After Width: | Height: | Size: 746 B |
@@ -4,9 +4,9 @@ use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::{
|
||||
App, AppContext, Application, AssetSource, Bounds, Context, ImageSource, KeyBinding, Menu,
|
||||
MenuItem, Point, SharedString, SharedUri, TitlebarOptions, Window, WindowBounds, WindowOptions,
|
||||
actions, div, img, prelude::*, px, rgb, size,
|
||||
actions, div, img, prelude::*, px, rgb, size, App, AppContext, Application, AssetSource,
|
||||
Bounds, Context, ImageSource, KeyBinding, Menu, MenuItem, Point, SharedString, SharedUri,
|
||||
TitlebarOptions, Window, WindowBounds, WindowOptions,
|
||||
};
|
||||
use reqwest_client::ReqwestClient;
|
||||
|
||||
@@ -150,7 +150,7 @@ fn main() {
|
||||
|
||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
|
||||
Application::new()
|
||||
gpui_platform::application()
|
||||
.with_assets(Assets {
|
||||
base: manifest_dir.join("examples"),
|
||||
})
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::{path::Path, sync::Arc, time::Duration};
|
||||
|
||||
use gpui::{
|
||||
Animation, AnimationExt, App, Application, Asset, AssetLogger, AssetSource, Bounds, Context,
|
||||
Hsla, ImageAssetLoader, ImageCacheError, ImgResourceLoader, LOADING_DELAY, Length, RenderImage,
|
||||
Resource, SharedString, Window, WindowBounds, WindowOptions, black, div, img, prelude::*,
|
||||
pulsating_between, px, red, size,
|
||||
black, div, img, prelude::*, pulsating_between, px, red, size, Animation, AnimationExt, App,
|
||||
Application, Asset, AssetLogger, AssetSource, Bounds, Context, Hsla, ImageAssetLoader,
|
||||
ImageCacheError, ImgResourceLoader, Length, RenderImage, Resource, SharedString, Window,
|
||||
WindowBounds, WindowOptions, LOADING_DELAY,
|
||||
};
|
||||
|
||||
struct Assets {}
|
||||
@@ -193,7 +193,7 @@ impl Render for ImageLoadingExample {
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
Application::new()
|
||||
gpui_platform::application()
|
||||
.with_assets(Assets {})
|
||||
.run(|cx: &mut App| {
|
||||
let options = WindowOptions {
|
||||
@@ -682,7 +682,7 @@ impl Render for InputExample {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("backspace", Backspace, None),
|
||||
@@ -11,22 +11,20 @@ mod example {
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use gpui::{
|
||||
App, Application, Bounds, Context, FontWeight, Size, Window, WindowBackgroundAppearance,
|
||||
WindowBounds, WindowKind, WindowOptions, div, layer_shell::*, point, prelude::*, px, rems,
|
||||
rgba, white,
|
||||
div, layer_shell::*, point, prelude::*, px, rems, rgba, white, App, Application, Bounds,
|
||||
Context, FontWeight, Size, Window, WindowBackgroundAppearance, WindowBounds, WindowKind,
|
||||
WindowOptions,
|
||||
};
|
||||
|
||||
struct LayerShellExample;
|
||||
|
||||
impl LayerShellExample {
|
||||
fn new(cx: &mut Context<Self>) -> Self {
|
||||
cx.spawn(async move |this, cx| {
|
||||
loop {
|
||||
let _ = this.update(cx, |_, cx| cx.notify());
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(500))
|
||||
.await;
|
||||
}
|
||||
cx.spawn(async move |this, cx| loop {
|
||||
let _ = this.update(cx, |_, cx| cx.notify());
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(500))
|
||||
.await;
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -60,7 +58,7 @@ mod example {
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
titlebar: None,
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{
|
||||
App, Application, Bounds, Context, FocusHandle, KeyBinding, Window, WindowBounds,
|
||||
WindowOptions, actions, div, prelude::*, px, rgb, size,
|
||||
actions, div, prelude::*, px, rgb, size, App, Application, Bounds, Context, FocusHandle,
|
||||
KeyBinding, Window, WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
actions!(example, [CloseWindow]);
|
||||
@@ -35,7 +35,7 @@ impl Render for ExampleWindow {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
let mut bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
|
||||
|
||||
cx.bind_keys([KeyBinding::new("cmd-w", CloseWindow, None)]);
|
||||
@@ -2,8 +2,8 @@ use std::{fs, path::PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::{
|
||||
App, Application, AssetSource, Bounds, BoxShadow, ClickEvent, Context, SharedString, Task,
|
||||
Window, WindowBounds, WindowOptions, div, hsla, img, point, prelude::*, px, rgb, size, svg,
|
||||
div, hsla, img, point, prelude::*, px, rgb, size, svg, App, Application, AssetSource, Bounds,
|
||||
BoxShadow, ClickEvent, Context, SharedString, Task, Window, WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
struct Assets {
|
||||
@@ -156,7 +156,7 @@ impl Render for HelloWorld {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new()
|
||||
gpui_platform::application()
|
||||
.with_assets(Assets {
|
||||
base: PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples"),
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{
|
||||
App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px,
|
||||
size,
|
||||
div, prelude::*, px, size, App, Application, Bounds, Context, Window, WindowBounds,
|
||||
WindowOptions,
|
||||
};
|
||||
|
||||
struct Scrollable {}
|
||||
@@ -45,7 +45,7 @@ impl Render for Scrollable {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
@@ -3,8 +3,8 @@ use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::{
|
||||
App, Application, AssetSource, Bounds, Context, SharedString, Window, WindowBounds,
|
||||
WindowOptions, current_platform, div, prelude::*, px, rgb, size, svg,
|
||||
current_platform, div, prelude::*, px, rgb, size, svg, App, Application, AssetSource, Bounds,
|
||||
Context, SharedString, Window, WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
struct Assets {
|
||||
@@ -68,7 +68,7 @@ impl Render for SvgExample {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::with_platform(current_platform(false))
|
||||
gpui_platform::application()
|
||||
.with_assets(Assets {
|
||||
base: PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples"),
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{
|
||||
App, Application, Bounds, Context, Div, ElementId, FocusHandle, KeyBinding, SharedString,
|
||||
Stateful, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, size,
|
||||
actions, div, prelude::*, px, size, App, Application, Bounds, Context, Div, ElementId,
|
||||
FocusHandle, KeyBinding, SharedString, Stateful, Window, WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
actions!(example, [Tab, TabPrev]);
|
||||
@@ -134,7 +134,7 @@ impl Render for Example {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("tab", Tab, None),
|
||||
KeyBinding::new("shift-tab", TabPrev, None),
|
||||
@@ -3,8 +3,8 @@
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use gpui::{
|
||||
App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px,
|
||||
size,
|
||||
div, prelude::*, px, size, App, Application, Bounds, Context, Window, WindowBounds,
|
||||
WindowOptions,
|
||||
};
|
||||
|
||||
struct Tree {}
|
||||
@@ -32,7 +32,7 @@ impl Render for Tree {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{
|
||||
App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px,
|
||||
rgb, size, uniform_list,
|
||||
div, prelude::*, px, rgb, size, uniform_list, App, Application, Bounds, Context, Window,
|
||||
WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
struct UniformListExample {}
|
||||
@@ -36,7 +36,7 @@ impl Render for UniformListExample {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{
|
||||
App, Application, Bounds, Context, KeyBinding, PromptButton, PromptLevel, Window, WindowBounds,
|
||||
WindowKind, WindowOptions, actions, div, prelude::*, px, rgb, size,
|
||||
actions, div, prelude::*, px, rgb, size, App, Application, Bounds, Context, KeyBinding,
|
||||
PromptButton, PromptLevel, Window, WindowBounds, WindowKind, WindowOptions,
|
||||
};
|
||||
|
||||
struct SubWindow {
|
||||
@@ -241,7 +241,7 @@ impl Render for WindowDemo {
|
||||
actions!(window, [Quit]);
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx);
|
||||
|
||||
cx.open_window(
|
||||
@@ -1,7 +1,7 @@
|
||||
use gpui::{
|
||||
App, Application, Bounds, Context, DisplayId, Hsla, Pixels, SharedString, Size, Window,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowKind, WindowOptions, div, point, prelude::*,
|
||||
px, rgb,
|
||||
div, point, prelude::*, px, rgb, App, Application, Bounds, Context, DisplayId, Hsla, Pixels,
|
||||
SharedString, Size, Window, WindowBackgroundAppearance, WindowBounds, WindowKind,
|
||||
WindowOptions,
|
||||
};
|
||||
|
||||
struct WindowContent {
|
||||
@@ -68,7 +68,7 @@ fn build_window_options(display_id: DisplayId, bounds: Bounds<Pixels>) -> Window
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
// Create several new windows, positioned in the top right corner of each screen
|
||||
let size = Size {
|
||||
width: px(350.),
|
||||
@@ -1,8 +1,8 @@
|
||||
use gpui::{
|
||||
App, Application, Bounds, Context, CursorStyle, Decorations, HitboxBehavior, Hsla, MouseButton,
|
||||
black, canvas, div, green, point, prelude::*, px, rgb, size, transparent_black, white, App,
|
||||
Application, Bounds, Context, CursorStyle, Decorations, HitboxBehavior, Hsla, MouseButton,
|
||||
Pixels, Point, ResizeEdge, Size, Window, WindowBackgroundAppearance, WindowBounds,
|
||||
WindowDecorations, WindowOptions, black, canvas, div, green, point, prelude::*, px, rgb, size,
|
||||
transparent_black, white,
|
||||
WindowDecorations, WindowOptions,
|
||||
};
|
||||
|
||||
struct WindowShadow {}
|
||||
@@ -203,7 +203,7 @@ fn resize_edge(pos: Point<Pixels>, shadow_size: Pixels, size: Size<Pixels>) -> O
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
@@ -8,7 +8,7 @@
|
||||
//! use example_prelude::init_example;
|
||||
//! ```
|
||||
|
||||
use gpui::{App, KeyBinding, Menu, MenuItem, SharedString, actions};
|
||||
use gpui::{actions, App, KeyBinding, Menu, MenuItem, SharedString};
|
||||
|
||||
actions!(example, [Quit, CloseWindow]);
|
||||
|
||||
@@ -33,7 +33,7 @@ pub fn init_example(cx: &mut App, name: impl Into<SharedString>) {
|
||||
}]);
|
||||
|
||||
// Quit the app when all windows are closed
|
||||
cx.on_window_closed(|cx| {
|
||||
cx.on_window_closed(|cx, _| {
|
||||
if cx.windows().is_empty() {
|
||||
cx.quit();
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
<heapType xmlns="http://schemas.microsoft.com/SMI/2020/WindowsSettings">SegmentHeap</heapType>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
<dependency>
|
||||
@@ -0,0 +1,243 @@
|
||||
//! # Accessibility in GPUI
|
||||
//!
|
||||
//! "Accessibility" refers to the ability of your application to be used by all
|
||||
//! users, regardless of disability status. There are many aspects, all important, including:
|
||||
//! - Ensuring sufficient text contrast.
|
||||
//! - Providing a mechanism to disable animations.
|
||||
//! - Providing a mechanism to increase text sizes.
|
||||
//! - etc.
|
||||
//!
|
||||
//! This guide is focused on **programmatic accessibility**. This allows
|
||||
//! assistive technology, such as screen readers or Braille displays, to inspect
|
||||
//! and interact with your app.
|
||||
//!
|
||||
//! GPUI integrates with [AccessKit] to provide programmatic accessibility
|
||||
//! features (referred to as simply "accessibility" for the rest of this guide).
|
||||
//!
|
||||
//! A minimal example can be found in the `examples/a11y` directory.
|
||||
//!
|
||||
//! ## Background
|
||||
//!
|
||||
//! Accessibility support is based on two key capabilities:
|
||||
//! - Exposing information about the current UI state to assistive technology.
|
||||
//! - Responding to actions requested by assistive technology.
|
||||
//!
|
||||
//! For example, a screen reader might want to announce to the user that a new
|
||||
//! button has appeared. The user may then want to use a voice control program
|
||||
//! to press that button.
|
||||
//!
|
||||
//! ### IDs in GPUI - [`ElementId`] and [`GlobalElementId`]
|
||||
//!
|
||||
//! In GPUI, each [`Element`] can have an [`id`][Element::id]:
|
||||
//! ```rust
|
||||
//! # use gpui::*;
|
||||
//! let div_with_id = div().id("my-id").child(text!("hello"));
|
||||
//!
|
||||
//! // IDs are optional
|
||||
//! let div_without_id = div().child(text!("hello"));
|
||||
//! ```
|
||||
//!
|
||||
//! [`Element`]s with IDs are also assigned a [`GlobalElementId`]. This global
|
||||
//! ID is formed by composing all the non-`None` IDs of its ancestors. For
|
||||
//! example:
|
||||
//! ```rust
|
||||
//! # use gpui::*;
|
||||
//! let inner = div().id("inner-id");
|
||||
//! let middle = div().child(inner); // no ID
|
||||
//! let outer = div().id("outer-id").child(middle);
|
||||
//! ```
|
||||
//! In this example, `inner`s global ID is (roughly speaking) `["outer-id",
|
||||
//! "inner-id"]`.
|
||||
//!
|
||||
//! Since `middle` doesn't have an ID itself, it has no global ID.
|
||||
//!
|
||||
//! [`GlobalElementId`]s should be unique per-frame. Duplicate global IDs in the
|
||||
//! same frame will likely cause bugs.
|
||||
//!
|
||||
//! ### IDs and accessibility
|
||||
//!
|
||||
//! When GPUI renders a frame, it walks your UI tree, and finds nodes with
|
||||
//! global IDs, and informs assistive technology about this node.
|
||||
//!
|
||||
//! In order for nodes to be reported, they must also have a non-`None`
|
||||
//! [`role`][Element::a11y_role]. This is used to inform assistive technology
|
||||
//! what *sort* of node it is (button, label, table, etc.). You can use
|
||||
//! [`div().id(...).role()`][StatefulInteractiveElement::role] to set the role.
|
||||
//!
|
||||
//! Nodes with the same global ID *across frames* are considered to be "the
|
||||
//! same" node. For example:
|
||||
//! ```rust
|
||||
//! # use gpui::*;
|
||||
//! // The UI in frame 1
|
||||
//! let frame_1 = div()
|
||||
//! .id("parent")
|
||||
//! .role(Role::Button)
|
||||
//! .child(
|
||||
//! div()
|
||||
//! .id("id-1")
|
||||
//! .role(Role::Label)
|
||||
//! .child(text!("hello"))
|
||||
//! );
|
||||
//!
|
||||
//! // The UI on the next frame
|
||||
//! let frame_2 = div()
|
||||
//! .id("parent")
|
||||
//! .role(Role::Button)
|
||||
//! .child(
|
||||
//! div()
|
||||
//! .id("id-2") // <- different ID
|
||||
//! .role(Role::Label)
|
||||
//! .child(text!("hello"))
|
||||
//! );
|
||||
//! ```
|
||||
//! Logically, the UI has not changed. But the screen reader has no way of
|
||||
//! knowing that both child [`div`]s are "the same". So assistive technology
|
||||
//! will interpret this as one node being removed, and another node being added.
|
||||
//! This can be very disorienting for users, since announcements typically only
|
||||
//! happen when something has *meaningfully* changed.
|
||||
//!
|
||||
//! In other words, by controlling the ID of an element, you can control whether
|
||||
//! a change to a UI element is considered meaningful. You can also control
|
||||
//! whether elements are reported to assistive technology *at all* by setting
|
||||
//! the [`role`][Element::a11y_role], since nodes with no role are not reported.
|
||||
//!
|
||||
//! #### IDs and text
|
||||
//!
|
||||
//! Special care must be taken when dealing with text.
|
||||
//!
|
||||
//! GPUI provides the [`text!`] macro, which wraps strings in the [`Text`] type,
|
||||
//! but automatically derives an ID. Usually, this is what you want. However,
|
||||
//! the way it generates its ID is subtle and perhaps surprising.
|
||||
//!
|
||||
//! The ID of an invocation of the [`text!`] macro is derived from the
|
||||
//! **location in the source code of that invocation**. For example:
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use gpui::*;
|
||||
//! let a = text!("a");
|
||||
//! let b = text!("b");
|
||||
//!
|
||||
//! // Different source locations, different IDs
|
||||
//! assert_ne!(a.id(), b.id());
|
||||
//!
|
||||
//! // However:
|
||||
//!
|
||||
//! fn make_text(s: &str) -> Text { text!(s) }
|
||||
//!
|
||||
//! let a = make_text("a");
|
||||
//! let b = make_text("b");
|
||||
//!
|
||||
//! // Both `a` and `b` are produced by the same `text!` invocation, so the IDs
|
||||
//! // are the same
|
||||
//! assert_eq!(a.id(), b.id());
|
||||
//! ```
|
||||
//! This can produce surprising behaviour. For example, this footgun:
|
||||
//! ```rust
|
||||
//! # use gpui::*;
|
||||
//! let todos = vec!["eat lunch", "drink water", "go to gym"];
|
||||
//! let todo_divs = todos.into_iter().map(|todo| {
|
||||
//! text!(todo)
|
||||
//! });
|
||||
//!
|
||||
//! div()
|
||||
//! .id("todo-list")
|
||||
//! .role(Role::Document)
|
||||
//! .children(todo_divs); // ERROR: multiple nodes with the same global ID
|
||||
//! ```
|
||||
//!
|
||||
//! Here, when we map the iterator, since we have only written [`text!`] once,
|
||||
//! there is only one ID. And since they have the same ancestors and the same
|
||||
//! ID, they will have the same global ID. In release builds, this will mean
|
||||
//! some nodes get silently dropped!
|
||||
//!
|
||||
//! To fix this, you can set an ID:
|
||||
//! ```rust
|
||||
//! # use gpui::*;
|
||||
//! let todos = vec!["eat lunch", "drink water", "go to gym"];
|
||||
//! let todo_divs = todos.into_iter().enumerate().map(|(index, todo)| {
|
||||
//! text!(todo).with_id(index) // OR `text(id = index, todo)`
|
||||
//! });
|
||||
//!
|
||||
//! div()
|
||||
//! .id("todo-list")
|
||||
//! .role(Role::Document)
|
||||
//! .children(todo_divs);
|
||||
//! ```
|
||||
//! Another possible solution is to wrap the [`text!`] in another node that
|
||||
//! *does* have a unique global ID. For example:
|
||||
//! ```rust
|
||||
//! # use gpui::*;
|
||||
//! let todos = vec!["eat lunch", "drink water", "go to gym"];
|
||||
//! let todo_divs = todos.into_iter().enumerate().map(|(index, todo)| {
|
||||
//! div().id(index).child(text!(todo))
|
||||
//! });
|
||||
//!
|
||||
//! div()
|
||||
//! .id("todo-list")
|
||||
//! .role(Role::Document)
|
||||
//! .children(todo_divs);
|
||||
//! ```
|
||||
//! Since the AccessKit [`NodeId`][accesskit::NodeId] is derived from the global
|
||||
//! ID, and the global ID takes into account the IDs of all ancestors, this
|
||||
//! works too.
|
||||
//!
|
||||
//! Occasionally, you will need to create a [`Text`] element with *no* ID. You
|
||||
//! can achieve this with [`Text::new_inaccessible`]. If you are creating a
|
||||
//! custom UI component (e.g. a button), you may want this so that you can set a
|
||||
//! label property on a parent [`div`] without duplicating the text in the
|
||||
//! accessibility tree.
|
||||
//!
|
||||
//! ### Handling actions
|
||||
//!
|
||||
//! Assistive technology can dispatch actions to the UI. While many users of
|
||||
//! assistive technology use traditional input devices (e.g. a keyboard), some
|
||||
//! use more specialized systems. For example, users with limited mobility may
|
||||
//! use voice control to interact with your app.
|
||||
//!
|
||||
//! When a user dispatches an action, it is dispatched *to a specific node*. It
|
||||
//! is your responsibility to tell the UI elements how they should respond when
|
||||
//! a request comes in.
|
||||
//!
|
||||
//! Note, these actions are **totally unrelated** to GPUI's [`Action`] trait.
|
||||
//! AccessKit exposes [`accesskit::Action`]. In GPUI, this is re-exported as
|
||||
//! [`AccessibleAction`].
|
||||
//!
|
||||
//! To respond to an accessible action, use
|
||||
//! [`div().on_a11y_action()`][InteractiveElement::on_a11y_action]:
|
||||
//! ```rust,ignore
|
||||
//! div()
|
||||
//! .id("my-slider")
|
||||
//! .role(Role::Slider)
|
||||
//! .on_a11y_action(AccessibleAction::Increment, |_extra, _window, _cx| {
|
||||
//! position += 1;
|
||||
//! cx.notify();
|
||||
//! })
|
||||
//! .child(my_cool_slider());
|
||||
//! ```
|
||||
//!
|
||||
//! Note that some common actions are automatically registered. For example,
|
||||
//! [`.on_click()`][StatefulInteractiveElement::on_click] adds an
|
||||
//! [`AccessibleAction::Click`] handler that calls the click handler.
|
||||
//!
|
||||
//! ## Further reading
|
||||
//!
|
||||
//! Designing high-quality accessible interfaces can be challenging, in the same
|
||||
//! way that designing high-quality traditional interfaces can be. The
|
||||
//! following pages have useful information:
|
||||
//!
|
||||
//! - [AccessKit]: The cross-platform accessibility toolkit GPUI uses
|
||||
//! internally.
|
||||
//! - [MDN WAI-ARIA basics][mdn-aria]: Introduction to roles, properties, and
|
||||
//! states.
|
||||
//! - [ARIA Authoring Practices Guide][apg]: W3C patterns for accessible
|
||||
//! widgets.
|
||||
//!
|
||||
//! Note that, while GPUI mimics web APIs, it doesn't necessarily behave
|
||||
//! *exactly* as a web browser would with the same attributes.
|
||||
//!
|
||||
//! [AccessKit]: https://accesskit.dev/
|
||||
//! [mdn-aria]: https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Accessibility/WAI-ARIA_basics
|
||||
//! [apg]: https://www.w3.org/WAI/ARIA/apg/
|
||||
|
||||
#[cfg(doc)]
|
||||
use crate::*; // so I don't have to qualify every type :)
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::scheduler::Instant;
|
||||
use scheduler::Instant;
|
||||
use std::{
|
||||
any::{TypeId, type_name},
|
||||
cell::{BorrowMutError, Cell, Ref, RefCell, RefMut},
|
||||
@@ -26,6 +26,7 @@ pub use async_context::*;
|
||||
use collections::{FxHashMap, FxHashSet, HashMap, VecDeque};
|
||||
pub use context::*;
|
||||
pub use entity_map::*;
|
||||
use gpui_util::{ResultExt, debug_panic};
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use headless_app_context::*;
|
||||
use http_client::{HttpClient, Url};
|
||||
@@ -34,7 +35,6 @@ use smallvec::SmallVec;
|
||||
pub use test_app::*;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use test_context::*;
|
||||
use util::{ResultExt, debug_panic};
|
||||
#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
|
||||
pub use visual_test_context::*;
|
||||
|
||||
@@ -49,7 +49,8 @@ use crate::{
|
||||
PlatformKeyboardMapper, Point, Priority, PromptBuilder, PromptButton, PromptHandle,
|
||||
PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource,
|
||||
SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextRenderingMode, TextSystem,
|
||||
ThermalState, Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
|
||||
ThermalState, Window, WindowAppearance, WindowButtonLayout, WindowHandle, WindowId,
|
||||
WindowInvalidator,
|
||||
colors::{Colors, GlobalColors},
|
||||
hash, init_app_menus,
|
||||
};
|
||||
@@ -141,11 +142,6 @@ pub struct Application(Rc<AppCell>);
|
||||
/// 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 default platform for the current OS.
|
||||
pub fn new() -> Self {
|
||||
Self::with_platform(crate::current_platform(false))
|
||||
}
|
||||
|
||||
/// Builds an app with a caller-provided platform implementation.
|
||||
pub fn with_platform(platform: Rc<dyn Platform>) -> Self {
|
||||
Self(App::new_app(
|
||||
@@ -155,6 +151,21 @@ impl Application {
|
||||
))
|
||||
}
|
||||
|
||||
/// Builds an app with accessibility (AccessKit) integration forcibly
|
||||
/// disabled.
|
||||
///
|
||||
/// In this mode, accessibility APIs (e.g.
|
||||
/// [`div().role()`][crate::StatefulInteractiveElement::role]) silently
|
||||
/// no-op.
|
||||
///
|
||||
/// See the [accessibility guide](crate::_accessibility) for an overview of
|
||||
/// the features this disables.
|
||||
pub fn new_inaccessible(platform: Rc<dyn Platform>) -> Self {
|
||||
let this = Self::with_platform(platform);
|
||||
this.0.borrow_mut().accessibility_force_disabled = true;
|
||||
this
|
||||
}
|
||||
|
||||
/// Assigns the source of assets for the application.
|
||||
pub fn with_assets(self, asset_source: impl AssetSource) -> Self {
|
||||
let mut context_lock = self.0.borrow_mut();
|
||||
@@ -245,7 +256,7 @@ type Listener = Box<dyn FnMut(&dyn Any, &mut App) -> bool + 'static>;
|
||||
pub(crate) type KeystrokeObserver =
|
||||
Box<dyn FnMut(&KeystrokeEvent, &mut Window, &mut App) -> bool + 'static>;
|
||||
type QuitHandler = Box<dyn FnOnce(&mut App) -> LocalBoxFuture<'static, ()> + 'static>;
|
||||
type WindowClosedHandler = Box<dyn FnMut(&mut App)>;
|
||||
type WindowClosedHandler = Box<dyn FnMut(&mut App, WindowId)>;
|
||||
type ReleaseListener = Box<dyn FnOnce(&mut dyn Any, &mut App) + 'static>;
|
||||
type NewEntityListener = Box<dyn FnMut(AnyEntity, &mut Option<&mut Window>, &mut App) + 'static>;
|
||||
|
||||
@@ -261,6 +272,22 @@ pub enum QuitMode {
|
||||
Explicit,
|
||||
}
|
||||
|
||||
/// Controls when GPUI hides the mouse cursor in response to keyboard input.
|
||||
///
|
||||
/// Restoration on mouse motion is handled by the platform layer; this enum
|
||||
/// only describes the policy for *triggering* a hide.
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub enum CursorHideMode {
|
||||
/// Never hide the cursor automatically.
|
||||
Never,
|
||||
/// Hide on character-producing key presses (typing).
|
||||
OnTyping,
|
||||
/// Hide on character-producing key presses, *and* when a key binding
|
||||
/// resolves to an action that consumes the keystroke.
|
||||
#[default]
|
||||
OnTypingAndAction,
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct SystemWindowTab {
|
||||
@@ -642,6 +669,7 @@ pub struct App {
|
||||
pub(crate) window_invalidators_by_entity:
|
||||
FxHashMap<EntityId, FxHashMap<WindowId, WindowInvalidator>>,
|
||||
pub(crate) tracked_entities: FxHashMap<WindowId, FxHashSet<EntityId>>,
|
||||
pub(crate) current_window_by_entity: FxHashMap<EntityId, WindowId>,
|
||||
#[cfg(any(feature = "inspector", debug_assertions))]
|
||||
pub(crate) inspector_renderer: Option<crate::InspectorRenderer>,
|
||||
#[cfg(any(feature = "inspector", debug_assertions))]
|
||||
@@ -652,6 +680,10 @@ pub struct App {
|
||||
|
||||
pub(crate) window_update_stack: Vec<WindowId>,
|
||||
pub(crate) mode: GpuiMode,
|
||||
pub(crate) cursor_hide_mode: CursorHideMode,
|
||||
/// Whether the app was created by [`Application::new_inaccessible`]. No
|
||||
/// accesskit APIs will be called when this flag is set.
|
||||
pub(crate) accessibility_force_disabled: bool,
|
||||
flushing_effects: bool,
|
||||
pending_updates: usize,
|
||||
quit_mode: QuitMode,
|
||||
@@ -719,6 +751,7 @@ impl App {
|
||||
observers: SubscriberSet::new(),
|
||||
tracked_entities: FxHashMap::default(),
|
||||
window_invalidators_by_entity: FxHashMap::default(),
|
||||
current_window_by_entity: FxHashMap::default(),
|
||||
event_listeners: SubscriberSet::new(),
|
||||
release_listeners: SubscriberSet::new(),
|
||||
keystroke_observers: SubscriberSet::new(),
|
||||
@@ -739,6 +772,8 @@ impl App {
|
||||
inspector_element_registry: InspectorElementRegistry::default(),
|
||||
quit_mode: QuitMode::default(),
|
||||
quitting: false,
|
||||
cursor_hide_mode: CursorHideMode::default(),
|
||||
accessibility_force_disabled: false,
|
||||
|
||||
#[cfg(any(test, feature = "test-support", debug_assertions))]
|
||||
name: None,
|
||||
@@ -879,6 +914,27 @@ impl App {
|
||||
self.platform.quit();
|
||||
}
|
||||
|
||||
/// Returns the current policy for hiding the cursor in response to
|
||||
/// keyboard input.
|
||||
pub fn cursor_hide_mode(&self) -> CursorHideMode {
|
||||
self.cursor_hide_mode
|
||||
}
|
||||
|
||||
/// Sets the policy controlling when GPUI hides the cursor in response
|
||||
/// to keyboard input.
|
||||
pub fn set_cursor_hide_mode(&mut self, mode: CursorHideMode) {
|
||||
self.cursor_hide_mode = mode;
|
||||
}
|
||||
|
||||
/// Returns whether the cursor is currently visible according to the
|
||||
/// platform. This will report `false` after a keyboard input has hidden
|
||||
/// the cursor and the user has not yet moved the mouse to restore it.
|
||||
///
|
||||
/// See [`App::set_cursor_hide_mode`].
|
||||
pub fn is_cursor_visible(&self) -> bool {
|
||||
self.platform.is_cursor_visible()
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
@@ -956,6 +1012,8 @@ impl App {
|
||||
.entry(*entity)
|
||||
.or_default()
|
||||
.insert(window_handle.id, invalidator.clone());
|
||||
self.current_window_by_entity
|
||||
.insert(*entity, window_handle.id);
|
||||
}
|
||||
tracked_entities.clear();
|
||||
tracked_entities.extend(entities.iter().copied());
|
||||
@@ -1182,6 +1240,11 @@ impl App {
|
||||
self.platform.window_appearance()
|
||||
}
|
||||
|
||||
/// Returns the window button layout configuration when supported.
|
||||
pub fn button_layout(&self) -> Option<WindowButtonLayout> {
|
||||
self.platform.button_layout()
|
||||
}
|
||||
|
||||
/// Reads data from the platform clipboard.
|
||||
pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
self.platform.read_from_clipboard()
|
||||
@@ -1457,6 +1520,8 @@ impl App {
|
||||
for (entity_id, mut entity) in dropped {
|
||||
self.observers.remove(&entity_id);
|
||||
self.event_listeners.remove(&entity_id);
|
||||
self.window_invalidators_by_entity.remove(&entity_id);
|
||||
self.current_window_by_entity.remove(&entity_id);
|
||||
for release_callback in self.release_listeners.remove(&entity_id) {
|
||||
release_callback(entity.as_mut(), self);
|
||||
}
|
||||
@@ -1533,6 +1598,13 @@ impl App {
|
||||
tid: TypeId,
|
||||
window: Option<WindowId>,
|
||||
) {
|
||||
// Seed the entity's current window from its creation context so
|
||||
// `with_window` resolves correctly before the entity has ever been
|
||||
// rendered.
|
||||
if let Some(id) = window {
|
||||
self.current_window_by_entity.insert(entity.entity_id(), id);
|
||||
}
|
||||
|
||||
self.new_entity_observers.clone().retain(&tid, |observer| {
|
||||
if let Some(id) = window {
|
||||
self.update_window_id(id, {
|
||||
@@ -1547,7 +1619,28 @@ impl App {
|
||||
});
|
||||
}
|
||||
|
||||
fn update_window_id<T, F>(&mut self, id: WindowId, update: F) -> Result<T>
|
||||
/// Run `f` against the entity's *current* window — the most recently
|
||||
/// rendered window that referenced the entity, or its creation window if
|
||||
/// it has yet to be rendered. Returns `None` if the entity has no
|
||||
/// current window, or if that window has been closed, or if it is
|
||||
/// already on the update stack.
|
||||
pub fn with_window<R>(
|
||||
&mut self,
|
||||
entity_id: EntityId,
|
||||
f: impl FnOnce(&mut Window, &mut App) -> R,
|
||||
) -> Option<R> {
|
||||
let window_id = *self.current_window_by_entity.get(&entity_id)?;
|
||||
self.update_window_id(window_id, |_, window, cx| f(window, cx))
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn ensure_window(&mut self, entity_id: EntityId, window: WindowId) {
|
||||
self.current_window_by_entity
|
||||
.entry(entity_id)
|
||||
.or_insert(window);
|
||||
}
|
||||
|
||||
pub(crate) fn update_window_id<T, F>(&mut self, id: WindowId, update: F) -> Result<T>
|
||||
where
|
||||
F: FnOnce(AnyView, &mut Window, &mut App) -> T,
|
||||
{
|
||||
@@ -1564,9 +1657,21 @@ impl App {
|
||||
if window.removed {
|
||||
cx.window_handles.remove(&id);
|
||||
cx.windows.remove(id);
|
||||
if let Some(tracked) = cx.tracked_entities.remove(&id) {
|
||||
for entity_id in tracked {
|
||||
if let Some(windows) =
|
||||
cx.window_invalidators_by_entity.get_mut(&entity_id)
|
||||
{
|
||||
windows.remove(&id);
|
||||
}
|
||||
if cx.current_window_by_entity.get(&entity_id) == Some(&id) {
|
||||
cx.current_window_by_entity.remove(&entity_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cx.window_closed_observers.clone().retain(&(), |callback| {
|
||||
callback(cx);
|
||||
callback(cx, id);
|
||||
true
|
||||
});
|
||||
|
||||
@@ -1679,8 +1784,7 @@ impl App {
|
||||
self.globals_by_type
|
||||
.get(&TypeId::of::<G>())
|
||||
.map(|any_state| any_state.downcast_ref::<G>().unwrap())
|
||||
.with_context(|| format!("no state of type {} exists", type_name::<G>()))
|
||||
.unwrap()
|
||||
.unwrap_or_else(|| panic!("no state of type {} exists", type_name::<G>()))
|
||||
}
|
||||
|
||||
/// Access the global of the given type if a value has been assigned.
|
||||
@@ -1698,8 +1802,7 @@ impl App {
|
||||
self.globals_by_type
|
||||
.get_mut(&global_type)
|
||||
.and_then(|any_state| any_state.downcast_mut::<G>())
|
||||
.with_context(|| format!("no state of type {} exists", type_name::<G>()))
|
||||
.unwrap()
|
||||
.unwrap_or_else(|| panic!("no state of type {} exists", type_name::<G>()))
|
||||
}
|
||||
|
||||
/// Access the global of the given type mutably. A default value is assigned if a global of this type has not
|
||||
@@ -1734,7 +1837,7 @@ impl App {
|
||||
*self
|
||||
.globals_by_type
|
||||
.remove(&global_type)
|
||||
.unwrap_or_else(|| panic!("no global added for {}", std::any::type_name::<G>()))
|
||||
.unwrap_or_else(|| panic!("no global added for {}", type_name::<G>()))
|
||||
.downcast()
|
||||
.unwrap()
|
||||
}
|
||||
@@ -2040,7 +2143,10 @@ impl App {
|
||||
|
||||
/// 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 {
|
||||
pub fn on_window_closed(
|
||||
&self,
|
||||
mut on_closed: impl FnMut(&mut App, WindowId) + 'static,
|
||||
) -> Subscription {
|
||||
let (subscription, activate) = self.window_closed_observers.insert((), Box::new(on_closed));
|
||||
activate();
|
||||
subscription
|
||||
@@ -2279,13 +2385,27 @@ impl App {
|
||||
.or_default(),
|
||||
);
|
||||
|
||||
if window_invalidators.is_empty() {
|
||||
// `window_invalidators_by_entity` is monotonic, so an entry alone
|
||||
// doesn't mean the window is currently rendering the entity. Filter
|
||||
// through `tracked_entities` to keep invalidation tight to windows
|
||||
// that actually display this entity right now.
|
||||
let live_invalidators: SmallVec<[WindowInvalidator; 2]> = window_invalidators
|
||||
.iter()
|
||||
.filter(|(window_id, _)| {
|
||||
self.tracked_entities
|
||||
.get(window_id)
|
||||
.is_some_and(|set| set.contains(&entity_id))
|
||||
})
|
||||
.map(|(_, invalidator)| invalidator.clone())
|
||||
.collect();
|
||||
|
||||
if live_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() {
|
||||
for invalidator in &live_invalidators {
|
||||
invalidator.invalidate_view(entity_id, self);
|
||||
}
|
||||
}
|
||||
@@ -2356,13 +2476,12 @@ impl AppContext for App {
|
||||
let entity = build_entity(&mut Context::new_context(cx, slot.downgrade()));
|
||||
|
||||
cx.push_effect(Effect::EntityCreated {
|
||||
entity: handle.clone().into_any(),
|
||||
entity: handle.into_any(),
|
||||
tid: TypeId::of::<T>(),
|
||||
window: cx.window_update_stack.last().cloned(),
|
||||
});
|
||||
|
||||
cx.entities.insert(slot, entity);
|
||||
handle
|
||||
cx.entities.insert(slot, entity)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2422,6 +2541,14 @@ impl AppContext for App {
|
||||
self.update_window_id(handle.id, update)
|
||||
}
|
||||
|
||||
fn with_window<R>(
|
||||
&mut self,
|
||||
entity_id: EntityId,
|
||||
f: impl FnOnce(&mut Window, &mut App) -> R,
|
||||
) -> Option<R> {
|
||||
App::with_window(self, entity_id, f)
|
||||
}
|
||||
|
||||
fn read_window<T, R>(
|
||||
&self,
|
||||
window: &WindowHandle<T>,
|
||||
@@ -2578,10 +2705,6 @@ pub struct KeystrokeEvent {
|
||||
struct NullHttpClient;
|
||||
|
||||
impl HttpClient for NullHttpClient {
|
||||
fn type_name(&self) -> &'static str {
|
||||
"NullHttpClient"
|
||||
}
|
||||
|
||||
fn send(
|
||||
&self,
|
||||
_req: http_client::Request<http_client::AsyncBody>,
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{
|
||||
AnyView, AnyWindowHandle, App, AppCell, AppContext, BackgroundExecutor, BorrowAppContext,
|
||||
Entity, EventEmitter, Focusable, ForegroundExecutor, Global, GpuiBorrow, PromptButton,
|
||||
PromptLevel, Render, Reservation, Result, Subscription, Task, VisualContext, Window,
|
||||
WindowHandle,
|
||||
Entity, EntityId, EventEmitter, Focusable, ForegroundExecutor, Global, GpuiBorrow,
|
||||
PromptButton, PromptLevel, Render, Reservation, Result, Subscription, Task, VisualContext,
|
||||
Window, WindowHandle,
|
||||
};
|
||||
use anyhow::{Context as _, bail};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
@@ -94,6 +94,19 @@ impl AppContext for AsyncApp {
|
||||
lock.update_window(window, f)
|
||||
}
|
||||
|
||||
fn with_window<R>(
|
||||
&mut self,
|
||||
entity_id: EntityId,
|
||||
f: impl FnOnce(&mut Window, &mut App) -> R,
|
||||
) -> Option<R> {
|
||||
let app = self.app.upgrade()?;
|
||||
let mut lock = app.try_borrow_mut().ok()?;
|
||||
if lock.quitting {
|
||||
return None;
|
||||
}
|
||||
lock.with_window(entity_id, f)
|
||||
}
|
||||
|
||||
fn read_window<T, R>(
|
||||
&self,
|
||||
window: &WindowHandle<T>,
|
||||
@@ -253,10 +266,10 @@ impl AsyncApp {
|
||||
&self,
|
||||
entity: &WeakEntity<T>,
|
||||
f: Callback,
|
||||
) -> util::Deferred<impl FnOnce() + use<T, Callback>> {
|
||||
) -> gpui_util::Deferred<impl FnOnce() + use<T, Callback>> {
|
||||
let entity = entity.clone();
|
||||
let mut cx = self.clone();
|
||||
util::defer(move || {
|
||||
gpui_util::defer(move || {
|
||||
entity.update(&mut cx, f).ok();
|
||||
})
|
||||
}
|
||||
@@ -365,7 +378,21 @@ impl AppContext for AsyncWindowContext {
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.app.new(build_entity)
|
||||
let mut build_entity = Some(build_entity);
|
||||
match self.app.update_window(self.window, |_, _, cx| {
|
||||
cx.new(
|
||||
build_entity
|
||||
.take()
|
||||
.expect("build_entity is taken exactly once"),
|
||||
)
|
||||
}) {
|
||||
Ok(entity) => entity,
|
||||
Err(_) => self.app.new(
|
||||
build_entity
|
||||
.take()
|
||||
.expect("update_window returned Err without invoking the closure"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn reserve_entity<T: 'static>(&mut self) -> Reservation<T> {
|
||||
@@ -377,7 +404,19 @@ impl AppContext for AsyncWindowContext {
|
||||
reservation: Reservation<T>,
|
||||
build_entity: impl FnOnce(&mut Context<T>) -> T,
|
||||
) -> Entity<T> {
|
||||
self.app.insert_entity(reservation, build_entity)
|
||||
let mut args = Some((reservation, build_entity));
|
||||
match self.app.update_window(self.window, |_, _, cx| {
|
||||
let (reservation, build_entity) = args.take().expect("args are taken exactly once");
|
||||
cx.insert_entity(reservation, build_entity)
|
||||
}) {
|
||||
Ok(entity) => entity,
|
||||
Err(_) => {
|
||||
let (reservation, build_entity) = args
|
||||
.take()
|
||||
.expect("update_window returned Err without invoking the closure");
|
||||
self.app.insert_entity(reservation, build_entity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_entity<T: 'static, R>(
|
||||
@@ -409,6 +448,14 @@ impl AppContext for AsyncWindowContext {
|
||||
self.app.update_window(window, update)
|
||||
}
|
||||
|
||||
fn with_window<R>(
|
||||
&mut self,
|
||||
entity_id: EntityId,
|
||||
f: impl FnOnce(&mut Window, &mut App) -> R,
|
||||
) -> Option<R> {
|
||||
self.app.with_window(entity_id, f)
|
||||
}
|
||||
|
||||
fn read_window<T, R>(
|
||||
&self,
|
||||
window: &WindowHandle<T>,
|
||||
@@ -457,9 +504,12 @@ impl VisualContext for AsyncWindowContext {
|
||||
view: &Entity<T>,
|
||||
update: impl FnOnce(&mut T, &mut Window, &mut Context<T>) -> R,
|
||||
) -> Result<R> {
|
||||
self.app.update_window(self.window, |_, window, cx| {
|
||||
view.update(cx, |entity, cx| update(entity, window, cx))
|
||||
})
|
||||
let view = view.clone();
|
||||
self.app
|
||||
.with_window(view.entity_id(), |window, app| {
|
||||
view.update(app, |entity, cx| update(entity, window, cx))
|
||||
})
|
||||
.context("entity has no current window")
|
||||
}
|
||||
|
||||
fn replace_root_view<V>(
|
||||
@@ -5,6 +5,7 @@ use crate::{
|
||||
};
|
||||
use anyhow::Result;
|
||||
use futures::FutureExt;
|
||||
use gpui_util::Deferred;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
borrow::{Borrow, BorrowMut},
|
||||
@@ -12,7 +13,6 @@ use std::{
|
||||
ops,
|
||||
sync::Arc,
|
||||
};
|
||||
use util::Deferred;
|
||||
|
||||
use super::{App, AsyncWindowContext, Entity, KeystrokeEvent};
|
||||
|
||||
@@ -278,7 +278,7 @@ impl<'a, T: 'static> Context<'a, T> {
|
||||
) -> Deferred<impl FnOnce()> {
|
||||
let this = self.weak_entity();
|
||||
let mut cx = self.to_async();
|
||||
util::defer(move || {
|
||||
gpui_util::defer(move || {
|
||||
this.update(&mut cx, f).ok();
|
||||
})
|
||||
}
|
||||
@@ -307,9 +307,13 @@ impl<'a, T: 'static> Context<'a, T> {
|
||||
window: &Window,
|
||||
f: impl FnOnce(&mut T, &mut Window, &mut Context<T>) + 'static,
|
||||
) {
|
||||
let view = self.entity();
|
||||
window.defer(self, move |window, cx| {
|
||||
view.update(cx, |view, cx| f(view, window, cx))
|
||||
let view = self.weak_entity();
|
||||
let entity_id = self.entity_id();
|
||||
self.ensure_window(entity_id, window.handle.id);
|
||||
self.app.defer(move |cx| {
|
||||
cx.with_window(entity_id, |window, cx| {
|
||||
view.update(cx, |view, cx| f(view, window, cx)).ok();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -326,25 +330,21 @@ impl<'a, T: 'static> Context<'a, T> {
|
||||
{
|
||||
let observed_id = observed.entity_id();
|
||||
let observed = observed.downgrade();
|
||||
let window_handle = window.handle;
|
||||
let observer = self.weak_entity();
|
||||
let observer_id = self.entity_id();
|
||||
self.ensure_window(observer_id, window.handle.id);
|
||||
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)
|
||||
let Some((observer, observed)) = observer.upgrade().zip(observed.upgrade()) else {
|
||||
return false;
|
||||
};
|
||||
cx.with_window(observer_id, |window, cx| {
|
||||
observer.update(cx, |observer, cx| {
|
||||
on_notify(observer, observed, window, cx);
|
||||
});
|
||||
});
|
||||
true
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -363,28 +363,25 @@ impl<'a, T: 'static> Context<'a, T> {
|
||||
Evt: 'static,
|
||||
{
|
||||
let emitter = emitter.downgrade();
|
||||
let window_handle = window.handle;
|
||||
let subscriber = self.weak_entity();
|
||||
let subscriber_id = self.entity_id();
|
||||
self.ensure_window(subscriber_id, window.handle.id);
|
||||
self.new_subscription(
|
||||
emitter.entity_id(),
|
||||
(
|
||||
TypeId::of::<Evt>(),
|
||||
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)
|
||||
let Some((subscriber, emitter)) = subscriber.upgrade().zip(emitter.upgrade())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let event = event.downcast_ref().expect("invalid event type");
|
||||
cx.with_window(subscriber_id, |window, cx| {
|
||||
subscriber.update(cx, |subscriber, cx| {
|
||||
on_event(subscriber, &emitter, event, window, cx);
|
||||
});
|
||||
});
|
||||
true
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -479,6 +476,24 @@ impl<'a, T: 'static> Context<'a, T> {
|
||||
subscription
|
||||
}
|
||||
|
||||
/// Registers a callback to be invoked when the window button layout changes.
|
||||
pub fn observe_button_layout_changed(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
mut callback: impl FnMut(&mut T, &mut Window, &mut Context<T>) + 'static,
|
||||
) -> Subscription {
|
||||
let view = self.weak_entity();
|
||||
let (subscription, activate) = window.button_layout_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.
|
||||
@@ -817,6 +832,15 @@ impl<T> AppContext for Context<'_, T> {
|
||||
self.app.update_window(window, update)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn with_window<R>(
|
||||
&mut self,
|
||||
entity_id: EntityId,
|
||||
f: impl FnOnce(&mut Window, &mut App) -> R,
|
||||
) -> Option<R> {
|
||||
self.app.with_window(entity_id, f)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read_window<U, R>(
|
||||
&self,
|
||||
@@ -20,7 +20,7 @@ use std::{
|
||||
};
|
||||
|
||||
use super::Context;
|
||||
use crate::local_util::atomic_incr_if_not_zero;
|
||||
use crate::util::atomic_incr_if_not_zero;
|
||||
#[cfg(any(test, feature = "leak-detection"))]
|
||||
use collections::HashMap;
|
||||
|
||||
@@ -795,14 +795,13 @@ impl<T: 'static> WeakEntity<T> {
|
||||
update: impl FnOnce(&mut T, &mut Window, &mut Context<T>) -> R,
|
||||
) -> Result<R>
|
||||
where
|
||||
C: VisualContext,
|
||||
C: AppContext,
|
||||
{
|
||||
let window = cx.window_handle();
|
||||
let entity = self.upgrade().context("entity released")?;
|
||||
|
||||
window.update(cx, |_, window, cx| {
|
||||
entity.update(cx, |entity, cx| update(entity, window, cx))
|
||||
cx.with_window(entity.entity_id(), |window, app| {
|
||||
entity.update(app, |entity, cx| update(entity, window, cx))
|
||||
})
|
||||
.context("entity has no current window")
|
||||
}
|
||||
|
||||
/// Reads the entity referenced by this handle with the given function if
|
||||
@@ -894,6 +893,9 @@ pub(crate) struct HandleId {
|
||||
/// created, all participating strong entities in this cycle will effectively
|
||||
/// leak as they cannot be released anymore.
|
||||
///
|
||||
/// Cycles can also happen if an entity owns a task or subscription that it
|
||||
/// itself owns a strong reference to the entity again.
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// You can use `WeakEntity::assert_released` or `AnyWeakEntity::assert_released`
|
||||
@@ -919,7 +921,7 @@ pub(crate) struct HandleId {
|
||||
/// ```
|
||||
///
|
||||
/// This will capture and display backtraces for each leaked handle, helping you
|
||||
/// identify where handles were created but not released.
|
||||
/// identify where leaked handles were created.
|
||||
///
|
||||
/// # How It Works
|
||||
///
|
||||
@@ -962,7 +964,7 @@ impl LeakDetector {
|
||||
entity_id: EntityId,
|
||||
type_name: Option<&'static str>,
|
||||
) -> HandleId {
|
||||
let id = util::post_inc(&mut self.next_handle_id);
|
||||
let id = gpui_util::post_inc(&mut self.next_handle_id);
|
||||
let handle_id = HandleId { id };
|
||||
let handles = self
|
||||
.entity_handles
|
||||
@@ -1002,11 +1004,13 @@ impl LeakDetector {
|
||||
/// otherwise it suggests setting the environment variable to get more info.
|
||||
pub fn assert_released(&mut self, entity_id: EntityId) {
|
||||
use std::fmt::Write as _;
|
||||
|
||||
if let Some(data) = self.entity_handles.remove(&entity_id) {
|
||||
let mut out = String::new();
|
||||
for (_, backtrace) in data.handles {
|
||||
if let Some(mut backtrace) = backtrace {
|
||||
backtrace.resolve();
|
||||
let backtrace = BacktraceFormatter(backtrace);
|
||||
writeln!(out, "Leaked handle:\n{:?}", backtrace).unwrap();
|
||||
} else {
|
||||
writeln!(
|
||||
@@ -1016,7 +1020,7 @@ impl LeakDetector {
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
panic!("{out}");
|
||||
panic!("Handles for {} leaked:\n{out}", data.type_name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1054,6 +1058,7 @@ impl LeakDetector {
|
||||
if let Some(backtrace) = backtrace {
|
||||
let mut backtrace = backtrace.clone();
|
||||
backtrace.resolve();
|
||||
let backtrace = BacktraceFormatter(backtrace);
|
||||
writeln!(
|
||||
out,
|
||||
"Leaked handle for entity {} ({entity_id:?}):\n{:?}",
|
||||
@@ -1091,6 +1096,7 @@ impl Drop for LeakDetector {
|
||||
for (_handle, backtrace) in data.handles {
|
||||
if let Some(mut backtrace) = backtrace {
|
||||
backtrace.resolve();
|
||||
let backtrace = BacktraceFormatter(backtrace);
|
||||
writeln!(
|
||||
out,
|
||||
"Leaked handle for entity {} ({entity_id:?}):\n{:?}",
|
||||
@@ -1111,6 +1117,71 @@ impl Drop for LeakDetector {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "leak-detection"))]
|
||||
struct BacktraceFormatter(backtrace::Backtrace);
|
||||
|
||||
#[cfg(any(test, feature = "leak-detection"))]
|
||||
impl fmt::Debug for BacktraceFormatter {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use backtrace::{BacktraceFmt, BytesOrWideString, PrintFmt};
|
||||
|
||||
let style = if fmt.alternate() {
|
||||
PrintFmt::Full
|
||||
} else {
|
||||
PrintFmt::Short
|
||||
};
|
||||
|
||||
// When printing paths we try to strip the cwd if it exists, otherwise
|
||||
// we just print the path as-is. Note that we also only do this for the
|
||||
// short format, because if it's full we presumably want to print
|
||||
// everything.
|
||||
let cwd = std::env::current_dir();
|
||||
let mut print_path = move |fmt: &mut fmt::Formatter<'_>, path: BytesOrWideString<'_>| {
|
||||
let path = path.into_path_buf();
|
||||
if style != PrintFmt::Full {
|
||||
if let Ok(cwd) = &cwd {
|
||||
if let Ok(suffix) = path.strip_prefix(cwd) {
|
||||
return fmt::Display::fmt(&suffix.display(), fmt);
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt::Display::fmt(&path.display(), fmt)
|
||||
};
|
||||
|
||||
let mut f = BacktraceFmt::new(fmt, style, &mut print_path);
|
||||
f.add_context()?;
|
||||
let mut strip = true;
|
||||
for frame in self.0.frames() {
|
||||
if let [symbol, ..] = frame.symbols()
|
||||
&& let Some(name) = symbol.name()
|
||||
&& let Some(filename) = name.as_str()
|
||||
{
|
||||
match filename {
|
||||
"test::run_test_in_process"
|
||||
| "scheduler::executor::spawn_local_with_source_location::impl$1::poll<core::pin::Pin<alloc::boxed::Box<dyn$<core::future::future::Future<assoc$<Output,enum2$<core::result::Result<workspace::OpenResult,anyhow::Error> > > > >,alloc::alloc::Global> > >" => {
|
||||
strip = true
|
||||
}
|
||||
"gpui::app::entity_map::LeakDetector::handle_created" => {
|
||||
strip = false;
|
||||
continue;
|
||||
}
|
||||
"zed::main" => {
|
||||
strip = true;
|
||||
f.frame().backtrace_frame(frame)?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if strip {
|
||||
continue;
|
||||
}
|
||||
f.frame().backtrace_frame(frame)?;
|
||||
}
|
||||
f.finish()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::EntityMap;
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
use crate::{
|
||||
AnyView, AnyWindowHandle, App, AppCell, AppContext, AssetSource, BackgroundExecutor, Bounds,
|
||||
Context, Entity, ForegroundExecutor, Global, Pixels, PlatformHeadlessRenderer,
|
||||
Context, Entity, EntityId, ForegroundExecutor, Global, Pixels, PlatformHeadlessRenderer,
|
||||
PlatformTextSystem, Render, Reservation, Size, Task, TestDispatcher, TestPlatform, TextSystem,
|
||||
Window, WindowBounds, WindowHandle, WindowOptions,
|
||||
app::{GpuiBorrow, GpuiMode},
|
||||
@@ -246,6 +246,15 @@ impl AppContext for HeadlessAppContext {
|
||||
lock.update_window(window, f)
|
||||
}
|
||||
|
||||
fn with_window<R>(
|
||||
&mut self,
|
||||
entity_id: EntityId,
|
||||
f: impl FnOnce(&mut Window, &mut App) -> R,
|
||||
) -> Option<R> {
|
||||
let mut lock = self.app.borrow_mut();
|
||||
lock.with_window(entity_id, f)
|
||||
}
|
||||
|
||||
fn read_window<T, R>(
|
||||
&self,
|
||||
window: &WindowHandle<T>,
|
||||
@@ -1,9 +1,9 @@
|
||||
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,
|
||||
Element, Empty, EntityId, 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, app::GpuiMode, window::ElementArenaScope,
|
||||
};
|
||||
@@ -84,6 +84,15 @@ impl AppContext for TestAppContext {
|
||||
lock.update_window(window, f)
|
||||
}
|
||||
|
||||
fn with_window<R>(
|
||||
&mut self,
|
||||
entity_id: EntityId,
|
||||
f: impl FnOnce(&mut Window, &mut App) -> R,
|
||||
) -> Option<R> {
|
||||
let mut lock = self.app.borrow_mut();
|
||||
lock.with_window(entity_id, f)
|
||||
}
|
||||
|
||||
fn read_window<T, R>(
|
||||
&self,
|
||||
window: &WindowHandle<T>,
|
||||
@@ -193,12 +202,6 @@ impl TestAppContext {
|
||||
&self.foreground_executor
|
||||
}
|
||||
|
||||
#[expect(clippy::wrong_self_convention)]
|
||||
fn new<T: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T> {
|
||||
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<R>(&self, f: impl FnOnce(&mut App) -> R) -> R {
|
||||
let mut cx = self.app.borrow_mut();
|
||||
@@ -333,6 +336,20 @@ impl TestAppContext {
|
||||
self.test_platform.simulate_new_path_selection(select_path);
|
||||
}
|
||||
|
||||
/// Simulates responding to a `prompt_for_paths` ("Open") dialog.
|
||||
pub fn simulate_path_prompt_response(
|
||||
&self,
|
||||
select_paths: impl FnOnce(&crate::PathPromptOptions) -> Option<Vec<std::path::PathBuf>>,
|
||||
) {
|
||||
self.test_platform
|
||||
.simulate_path_prompt_response(select_paths);
|
||||
}
|
||||
|
||||
/// Returns true if there's a path selection dialog pending.
|
||||
pub fn did_prompt_for_paths(&self) -> bool {
|
||||
self.test_platform.did_prompt_for_paths()
|
||||
}
|
||||
|
||||
/// Simulates clicking a button in an platform-level alert dialog.
|
||||
#[track_caller]
|
||||
pub fn simulate_prompt_answer(&self, button: &str) {
|
||||
@@ -735,6 +752,16 @@ impl VisualTestContext {
|
||||
self.cx.test_window(self.window).0.lock().title.clone()
|
||||
}
|
||||
|
||||
/// Read the document path off the window (set by `Window#set_document_path`)
|
||||
pub fn document_path(&mut self) -> Option<std::path::PathBuf> {
|
||||
self.cx
|
||||
.test_window(self.window)
|
||||
.0
|
||||
.lock()
|
||||
.document_path
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Simulate a sequence of keystrokes `cx.simulate_keystrokes("cmd-p escape")`
|
||||
/// Automatically runs until parked.
|
||||
pub fn simulate_keystrokes(&mut self, keystrokes: &str) {
|
||||
@@ -930,7 +957,9 @@ impl VisualTestContext {
|
||||
|
||||
impl AppContext for VisualTestContext {
|
||||
fn new<T: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T> {
|
||||
self.cx.new(build_entity)
|
||||
self.window
|
||||
.update(&mut self.cx, |_, _, cx| cx.new(build_entity))
|
||||
.expect("window was unexpectedly closed")
|
||||
}
|
||||
|
||||
fn reserve_entity<T: 'static>(&mut self) -> crate::Reservation<T> {
|
||||
@@ -942,7 +971,11 @@ impl AppContext for VisualTestContext {
|
||||
reservation: crate::Reservation<T>,
|
||||
build_entity: impl FnOnce(&mut Context<T>) -> T,
|
||||
) -> Entity<T> {
|
||||
self.cx.insert_entity(reservation, build_entity)
|
||||
self.window
|
||||
.update(&mut self.cx, |_, _, cx| {
|
||||
cx.insert_entity(reservation, build_entity)
|
||||
})
|
||||
.expect("window was unexpectedly closed")
|
||||
}
|
||||
|
||||
fn update_entity<T, R>(
|
||||
@@ -977,6 +1010,14 @@ impl AppContext for VisualTestContext {
|
||||
self.cx.update_window(window, f)
|
||||
}
|
||||
|
||||
fn with_window<R>(
|
||||
&mut self,
|
||||
entity_id: EntityId,
|
||||
f: impl FnOnce(&mut Window, &mut App) -> R,
|
||||
) -> Option<R> {
|
||||
self.cx.with_window(entity_id, f)
|
||||
}
|
||||
|
||||
fn read_window<T, R>(
|
||||
&self,
|
||||
window: &WindowHandle<T>,
|
||||
@@ -1027,11 +1068,14 @@ impl VisualContext for VisualTestContext {
|
||||
view: &Entity<V>,
|
||||
update: impl FnOnce(&mut V, &mut Window, &mut Context<V>) -> R,
|
||||
) -> R {
|
||||
self.window
|
||||
.update(&mut self.cx, |_, window, cx| {
|
||||
view.update(cx, |v, cx| update(v, window, cx))
|
||||
let view = view.clone();
|
||||
self.cx
|
||||
.app
|
||||
.borrow_mut()
|
||||
.with_window(view.entity_id(), |window, app| {
|
||||
view.update(app, |v, cx| update(v, window, cx))
|
||||
})
|
||||
.expect("window was unexpectedly closed")
|
||||
.expect("entity has no current window; use `update` instead of `update_in`")
|
||||
}
|
||||
|
||||
fn replace_root_view<V>(
|
||||
@@ -1068,3 +1112,54 @@ impl AnyWindowHandle {
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{PathPromptOptions, TestAppContext};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_simulate_path_prompt_response(cx: &mut TestAppContext) {
|
||||
assert!(!cx.did_prompt_for_paths());
|
||||
|
||||
let receiver = cx.update(|cx| {
|
||||
cx.prompt_for_paths(PathPromptOptions {
|
||||
files: false,
|
||||
directories: true,
|
||||
multiple: true,
|
||||
prompt: None,
|
||||
})
|
||||
});
|
||||
assert!(cx.did_prompt_for_paths());
|
||||
|
||||
let selected = vec![PathBuf::from("/a"), PathBuf::from("/b")];
|
||||
cx.simulate_path_prompt_response({
|
||||
let selected = selected.clone();
|
||||
move |options| {
|
||||
assert!(options.multiple);
|
||||
Some(selected)
|
||||
}
|
||||
});
|
||||
assert!(!cx.did_prompt_for_paths());
|
||||
|
||||
let response = receiver.await.unwrap().unwrap();
|
||||
assert_eq!(response, Some(selected));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_simulate_path_prompt_cancellation(cx: &mut TestAppContext) {
|
||||
let receiver = cx.update(|cx| {
|
||||
cx.prompt_for_paths(PathPromptOptions {
|
||||
files: true,
|
||||
directories: false,
|
||||
multiple: false,
|
||||
prompt: None,
|
||||
})
|
||||
});
|
||||
|
||||
cx.simulate_path_prompt_response(|_options| None);
|
||||
|
||||
let response = receiver.await.unwrap().unwrap();
|
||||
assert_eq!(response, None);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::{
|
||||
Action, AnyView, AnyWindowHandle, App, AppCell, AppContext, AssetSource, BackgroundExecutor,
|
||||
Bounds, ClipboardItem, Context, Entity, ForegroundExecutor, Global, InputEvent, Keystroke,
|
||||
Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Platform, Point,
|
||||
Render, Result, Size, Task, TestDispatcher, TextSystem, VisualTestPlatform, Window,
|
||||
WindowBounds, WindowHandle, WindowOptions, app::GpuiMode,
|
||||
Bounds, ClipboardItem, Context, Entity, EntityId, ForegroundExecutor, Global, InputEvent,
|
||||
Keystroke, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
|
||||
Platform, Point, Render, Result, Size, Task, TestDispatcher, TextSystem, VisualTestPlatform,
|
||||
Window, WindowBounds, WindowHandle, WindowOptions, app::GpuiMode,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use image::RgbaImage;
|
||||
@@ -446,6 +446,15 @@ impl AppContext for VisualTestAppContext {
|
||||
lock.update_window(window, f)
|
||||
}
|
||||
|
||||
fn with_window<R>(
|
||||
&mut self,
|
||||
entity_id: EntityId,
|
||||
f: impl FnOnce(&mut Window, &mut App) -> R,
|
||||
) -> Option<R> {
|
||||
let mut lock = self.app.borrow_mut();
|
||||
lock.with_window(entity_id, f)
|
||||
}
|
||||
|
||||
fn read_window<T, R>(
|
||||
&self,
|
||||
window: &WindowHandle<T>,
|
||||
@@ -77,8 +77,13 @@ impl RenderImage {
|
||||
|
||||
/// Get the size of this image, in pixels.
|
||||
pub fn size(&self, frame_index: usize) -> Size<DevicePixels> {
|
||||
let (width, height) = self.data[frame_index].buffer().dimensions();
|
||||
size(width.into(), height.into())
|
||||
self.data
|
||||
.get(frame_index)
|
||||
.map(|frame| {
|
||||
let (width, height) = frame.buffer().dimensions();
|
||||
size(width.into(), height.into())
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get the size of this image, in pixels for display, adjusted for the scale factor.
|
||||
@@ -89,7 +94,10 @@ impl RenderImage {
|
||||
|
||||
/// Get the delay of this frame from the previous
|
||||
pub fn delay(&self, frame_index: usize) -> Delay {
|
||||
self.data[frame_index].delay()
|
||||
self.data
|
||||
.get(frame_index)
|
||||
.map(|frame| frame.delay())
|
||||
.unwrap_or(Delay::from_numer_denom_ms(100, 1))
|
||||
}
|
||||
|
||||
/// Get the number of frames for this image.
|
||||
@@ -102,7 +110,24 @@ 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))
|
||||
.field("size", &self.data.first().map(|f| f.buffer().dimensions()))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
#[test]
|
||||
fn empty_render_image_does_not_panic() {
|
||||
let image = RenderImage::new(SmallVec::new());
|
||||
assert_eq!(image.frame_count(), 0);
|
||||
assert_eq!(image.size(0), Size::default());
|
||||
assert_eq!(image.as_bytes(0), None);
|
||||
assert_eq!(image.render_size(0), Size::default());
|
||||
assert_eq!(image.delay(0), Delay::from_numer_denom_ms(100, 1));
|
||||
let _ = format!("{image:?}");
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ use std::{
|
||||
cmp,
|
||||
fmt::Debug,
|
||||
ops::{Add, Sub},
|
||||
ptr::NonNull,
|
||||
};
|
||||
|
||||
/// Maximum children per internal node (R-tree style branching factor).
|
||||
@@ -30,7 +31,7 @@ where
|
||||
/// Reusable stack for tree traversal during insertion.
|
||||
insert_path: Vec<usize>,
|
||||
/// Reusable stack for search operations.
|
||||
search_stack: Vec<usize>,
|
||||
search_stack: Vec<NonNull<Node<U>>>,
|
||||
}
|
||||
|
||||
/// A node in the bounds tree.
|
||||
@@ -150,12 +151,14 @@ where
|
||||
|
||||
// Slow path: search the tree
|
||||
self.search_stack.clear();
|
||||
self.search_stack.push(root_idx);
|
||||
self.search_stack.push(NonNull::from(&self.nodes[root_idx]));
|
||||
|
||||
let mut max_found = 0u32;
|
||||
|
||||
while let Some(node_idx) = self.search_stack.pop() {
|
||||
let node = &self.nodes[node_idx];
|
||||
while let Some(node) = self.search_stack.pop() {
|
||||
// SAFETY: `node` is guaranteed to be valid as the `nodes` stack is unmodified in this function
|
||||
// and the `search_stack` only contains pointers from this function call.
|
||||
let node = unsafe { node.as_ref() };
|
||||
|
||||
// Pruning: skip if this subtree can't improve our result
|
||||
if node.max_order <= max_found {
|
||||
@@ -174,11 +177,14 @@ where
|
||||
NodeKind::Internal { children } => {
|
||||
// Children are maintained with highest max_order at the end.
|
||||
// Push in forward order to highest (last) is popped first.
|
||||
for &child_idx in children.as_slice() {
|
||||
if self.nodes[child_idx].max_order > max_found {
|
||||
self.search_stack.push(child_idx);
|
||||
}
|
||||
}
|
||||
self.search_stack.extend(
|
||||
children
|
||||
.as_slice()
|
||||
.iter()
|
||||
.map(|&child_idx| &self.nodes[child_idx])
|
||||
.filter(|node| node.max_order > max_found)
|
||||
.map(NonNull::from),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -283,6 +283,32 @@ pub struct Hsla {
|
||||
pub a: f32,
|
||||
}
|
||||
|
||||
#[cfg(feature = "proptest")]
|
||||
mod property {
|
||||
use super::Hsla;
|
||||
use proptest::prelude::*;
|
||||
|
||||
impl Hsla {
|
||||
/// Proptest [`Strategy`] that produces opaque colors (i.e. alpha = 1).
|
||||
///
|
||||
/// For truly arbitrary colors, use the [`Arbitrary`] implementation.
|
||||
pub fn opaque_strategy() -> impl Strategy<Value = Self> {
|
||||
(0.0f32..=1.0, 0.0f32..=1.0, 0.0f32..=1.0).prop_map(|(h, s, l)| Hsla { h, s, l, a: 1. })
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for Hsla {
|
||||
type Strategy = BoxedStrategy<Self>;
|
||||
type Parameters = ();
|
||||
|
||||
fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
|
||||
(0.0f32..=1.0, 0.0f32..=1.0, 0.0f32..=1.0, 0.0f32..=1.0)
|
||||
.prop_map(|(h, s, l, a)| Hsla { h, s, l, a })
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Hsla {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.h
|
||||
@@ -34,7 +34,7 @@
|
||||
use crate::{
|
||||
App, ArenaBox, AvailableSpace, Bounds, Context, DispatchNodeId, ElementId, FocusHandle,
|
||||
InspectorElementId, LayoutId, Pixels, Point, SharedString, Size, Style, Window,
|
||||
local_util::FluentBuilder, window::with_element_arena,
|
||||
util::FluentBuilder, window::with_element_arena,
|
||||
};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use std::{
|
||||
@@ -103,6 +103,22 @@ pub trait Element: 'static + IntoElement {
|
||||
cx: &mut App,
|
||||
);
|
||||
|
||||
/// Returns the accessible role for this element, if any.
|
||||
/// Elements that return `None` are not included in the accessibility tree.
|
||||
///
|
||||
/// Note: inclusion in accessibility tree requires non-`None` [`id`][Element::id].
|
||||
///
|
||||
/// See the [accessibility guide](crate::_accessibility) for an overview.
|
||||
fn a11y_role(&self) -> Option<accesskit::Role> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Write accessibility properties to the given node.
|
||||
/// Called only when `a11y_role()` returns `Some`.
|
||||
///
|
||||
/// See the [accessibility guide](crate::_accessibility) for an overview.
|
||||
fn write_a11y_info(&self, _node: &mut accesskit::Node) {}
|
||||
|
||||
/// Convert this element into a dynamically-typed [`AnyElement`].
|
||||
fn into_any(self) -> AnyElement {
|
||||
AnyElement::new(self)
|
||||
@@ -302,6 +318,15 @@ impl Display for GlobalElementId {
|
||||
}
|
||||
}
|
||||
|
||||
impl GlobalElementId {
|
||||
pub(crate) fn accesskit_node_id(&self) -> accesskit::NodeId {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut hasher = std::hash::DefaultHasher::default();
|
||||
self.hash(&mut hasher);
|
||||
accesskit::NodeId(hasher.finish())
|
||||
}
|
||||
}
|
||||
|
||||
trait ElementObject {
|
||||
fn inner_element(&mut self) -> &mut dyn Any;
|
||||
|
||||
@@ -431,6 +456,26 @@ impl<E: Element> Drawable<E> {
|
||||
}
|
||||
|
||||
let bounds = window.layout_bounds(layout_id);
|
||||
let mut pushed_a11y_node = false;
|
||||
if window.a11y.is_active() {
|
||||
if let Some(global_id) = global_id.as_ref() {
|
||||
if let Some(role) = self.element.a11y_role() {
|
||||
let node_id = global_id.accesskit_node_id();
|
||||
let mut node = accesskit::Node::new(role);
|
||||
let scale = window.scale_factor();
|
||||
node.set_bounds(accesskit::Rect {
|
||||
x0: (bounds.origin.x.0 * scale) as f64,
|
||||
y0: (bounds.origin.y.0 * scale) as f64,
|
||||
x1: ((bounds.origin.x.0 + bounds.size.width.0) * scale) as f64,
|
||||
y1: ((bounds.origin.y.0 + bounds.size.height.0) * scale) as f64,
|
||||
});
|
||||
self.element.write_a11y_info(&mut node);
|
||||
window.a11y.node_bounds.insert(node_id, bounds);
|
||||
pushed_a11y_node = window.a11y.nodes.push(node_id, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let node_id = window.next_frame.dispatch_tree.push_node();
|
||||
let prepaint = self.element.prepaint(
|
||||
global_id.as_ref(),
|
||||
@@ -442,6 +487,10 @@ impl<E: Element> Drawable<E> {
|
||||
);
|
||||
window.next_frame.dispatch_tree.pop_node();
|
||||
|
||||
if pushed_a11y_node {
|
||||
window.a11y.nodes.pop();
|
||||
}
|
||||
|
||||
if global_id.is_some() {
|
||||
window.element_id_stack.pop();
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{
|
||||
AnyElement, App, Axis, Bounds, Corner, Display, Edges, Element, GlobalElementId,
|
||||
Anchor, AnyElement, App, Axis, Bounds, Display, Edges, Element, GlobalElementId,
|
||||
InspectorElementId, IntoElement, LayoutId, ParentElement, Pixels, Point, Position, Size, Style,
|
||||
Window, point, px,
|
||||
};
|
||||
@@ -15,7 +15,7 @@ pub struct AnchoredState {
|
||||
/// will avoid overflowing the window bounds.
|
||||
pub struct Anchored {
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
anchor_corner: Corner,
|
||||
anchor: Anchor,
|
||||
fit_mode: AnchoredFitMode,
|
||||
anchor_position: Option<Point<Pixels>>,
|
||||
position_mode: AnchoredPositionMode,
|
||||
@@ -27,7 +27,7 @@ pub struct Anchored {
|
||||
pub fn anchored() -> Anchored {
|
||||
Anchored {
|
||||
children: SmallVec::new(),
|
||||
anchor_corner: Corner::TopLeft,
|
||||
anchor: Anchor::TopLeft,
|
||||
fit_mode: AnchoredFitMode::SwitchAnchor,
|
||||
anchor_position: None,
|
||||
position_mode: AnchoredPositionMode::Window,
|
||||
@@ -37,8 +37,8 @@ pub fn anchored() -> Anchored {
|
||||
|
||||
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;
|
||||
pub fn anchor(mut self, anchor: Anchor) -> Self {
|
||||
self.anchor = anchor;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -132,19 +132,17 @@ impl Element for Anchored {
|
||||
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<Pixels> = (child_max - child_min).into();
|
||||
let children_bounds = request_layout
|
||||
.child_layout_ids
|
||||
.iter()
|
||||
.map(|id| window.layout_bounds(*id))
|
||||
.reduce(|acc, bounds| acc.union(&bounds))
|
||||
.unwrap();
|
||||
|
||||
let (origin, mut desired) = self.position_mode.get_position_and_bounds(
|
||||
self.anchor_position,
|
||||
self.anchor_corner,
|
||||
size,
|
||||
self.anchor,
|
||||
children_bounds.size,
|
||||
bounds,
|
||||
self.offset,
|
||||
);
|
||||
@@ -155,25 +153,25 @@ impl Element for Anchored {
|
||||
};
|
||||
|
||||
if self.fit_mode == AnchoredFitMode::SwitchAnchor {
|
||||
let mut anchor_corner = self.anchor_corner;
|
||||
let mut anchor = self.anchor;
|
||||
|
||||
if desired.left() < limits.left() || desired.right() > limits.right() {
|
||||
let switched = Bounds::from_corner_and_size(
|
||||
anchor_corner.other_side_corner_along(Axis::Horizontal),
|
||||
let switched = Bounds::from_anchor_and_size(
|
||||
anchor.other_side_along(Axis::Horizontal),
|
||||
origin,
|
||||
size,
|
||||
children_bounds.size,
|
||||
);
|
||||
if !(switched.left() < limits.left() || switched.right() > limits.right()) {
|
||||
anchor_corner = anchor_corner.other_side_corner_along(Axis::Horizontal);
|
||||
anchor = anchor.other_side_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),
|
||||
let switched = Bounds::from_anchor_and_size(
|
||||
anchor.other_side_along(Axis::Vertical),
|
||||
origin,
|
||||
size,
|
||||
children_bounds.size,
|
||||
);
|
||||
if !(switched.top() < limits.top() || switched.bottom() > limits.bottom()) {
|
||||
desired = switched;
|
||||
@@ -264,7 +262,7 @@ impl AnchoredPositionMode {
|
||||
fn get_position_and_bounds(
|
||||
&self,
|
||||
anchor_position: Option<Point<Pixels>>,
|
||||
anchor_corner: Corner,
|
||||
anchor: Anchor,
|
||||
size: Size<Pixels>,
|
||||
bounds: Bounds<Pixels>,
|
||||
offset: Option<Point<Pixels>>,
|
||||
@@ -274,14 +272,13 @@ impl AnchoredPositionMode {
|
||||
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);
|
||||
let bounds = Bounds::from_anchor_and_size(anchor, 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,
|
||||
let bounds = Bounds::from_anchor_and_size(
|
||||
anchor,
|
||||
bounds.origin + anchor_position + offset,
|
||||
size,
|
||||
);
|
||||
@@ -290,3 +287,112 @@ impl AnchoredPositionMode {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
Context, Pixels, PlatformInput, Point, TestAppContext, Window, deferred, div, point,
|
||||
prelude::*, px, size,
|
||||
};
|
||||
|
||||
struct AnchoredTestView {
|
||||
position: Point<Pixels>,
|
||||
}
|
||||
|
||||
impl Render for AnchoredTestView {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().size_full().child(
|
||||
div()
|
||||
.id("scroll-container")
|
||||
.overflow_y_scroll()
|
||||
.size_full()
|
||||
.child(div().h(px(2000.)).w_full())
|
||||
.child(
|
||||
deferred(
|
||||
super::anchored()
|
||||
.snap_to_window()
|
||||
.position(self.position)
|
||||
.child(
|
||||
div()
|
||||
.id("menu")
|
||||
.debug_selector(|| "MENU".into())
|
||||
.w(px(200.))
|
||||
.h(px(300.)),
|
||||
),
|
||||
)
|
||||
.with_priority(1),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_anchored_position_without_scroll(cx: &mut TestAppContext) {
|
||||
let window = cx.open_window(size(px(800.), px(600.)), |_, _| AnchoredTestView {
|
||||
position: point(px(100.), px(100.)),
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let menu_bounds = window
|
||||
.update(cx, |_, window, _| {
|
||||
window.rendered_frame.debug_bounds.get("MENU").copied()
|
||||
})
|
||||
.unwrap()
|
||||
.expect("MENU debug bounds not found");
|
||||
|
||||
assert_eq!(menu_bounds.origin, point(px(100.), px(100.)));
|
||||
assert_eq!(menu_bounds.size, size(px(200.), px(300.)));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_anchored_position_when_scrolled(cx: &mut TestAppContext) {
|
||||
let window = cx.open_window(size(px(800.), px(600.)), |_, _| AnchoredTestView {
|
||||
position: point(px(100.), px(100.)),
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
window
|
||||
.update(cx, |_, window, cx| {
|
||||
let event = gpui::ScrollWheelEvent {
|
||||
position: point(px(400.), px(300.)),
|
||||
delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-1000.))),
|
||||
..Default::default()
|
||||
};
|
||||
window.dispatch_event(PlatformInput::ScrollWheel(event), cx);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let menu_bounds = window
|
||||
.update(cx, |_, window, _| {
|
||||
window.rendered_frame.debug_bounds.get("MENU").copied()
|
||||
})
|
||||
.unwrap()
|
||||
.expect("MENU debug bounds not found");
|
||||
|
||||
assert_eq!(menu_bounds.origin, point(px(100.), px(100.)));
|
||||
assert_eq!(menu_bounds.size, size(px(200.), px(300.)));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_anchored_snaps_to_window(cx: &mut TestAppContext) {
|
||||
let window = cx.open_window(size(px(800.), px(600.)), |_, _| AnchoredTestView {
|
||||
position: point(px(100.), px(500.)),
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let menu_bounds = window
|
||||
.update(cx, |_, window, _| {
|
||||
window.rendered_frame.debug_bounds.get("MENU").copied()
|
||||
})
|
||||
.unwrap()
|
||||
.expect("MENU debug bounds not found");
|
||||
|
||||
assert_eq!(menu_bounds.origin, point(px(100.), px(300.)));
|
||||
assert_eq!(menu_bounds.size, size(px(200.), px(300.)));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::scheduler::Instant;
|
||||
use scheduler::Instant;
|
||||
use std::{rc::Rc, time::Duration};
|
||||
|
||||
use crate::{
|
||||
@@ -15,19 +15,19 @@
|
||||
//! 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.
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
use crate::PinchEvent;
|
||||
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, MousePressureEvent, MouseUpEvent,
|
||||
Overflow, ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style,
|
||||
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, MousePressureEvent, MouseUpEvent, Overflow,
|
||||
ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style,
|
||||
StyleRefinement, Styled, Task, TooltipId, Visibility, Window, WindowControlArea, point, px,
|
||||
size,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use gpui_util::ResultExt;
|
||||
use refineable::Refineable;
|
||||
use smallvec::SmallVec;
|
||||
use stacksafe::{StackSafe, stacksafe};
|
||||
@@ -42,7 +42,6 @@ use std::{
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
use super::ImageCacheProvider;
|
||||
|
||||
@@ -357,11 +356,7 @@ impl Interactivity {
|
||||
|
||||
/// Bind the given callback to pinch gesture events during the bubble phase.
|
||||
///
|
||||
/// Note: This event is only available on macOS and Wayland (Linux).
|
||||
/// On Windows, pinch gestures are simulated as scroll wheel events with Ctrl held.
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub fn on_pinch(&mut self, listener: impl Fn(&PinchEvent, &mut Window, &mut App) + 'static) {
|
||||
self.pinch_listeners
|
||||
.push(Box::new(move |event, phase, hitbox, window, cx| {
|
||||
@@ -373,11 +368,7 @@ impl Interactivity {
|
||||
|
||||
/// Bind the given callback to pinch gesture events during the capture phase.
|
||||
///
|
||||
/// Note: This event is only available on macOS and Wayland (Linux).
|
||||
/// On Windows, pinch gestures are simulated as scroll wheel events with Ctrl held.
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub fn capture_pinch(
|
||||
&mut self,
|
||||
listener: impl Fn(&PinchEvent, &mut Window, &mut App) + 'static,
|
||||
@@ -675,15 +666,9 @@ impl Interactivity {
|
||||
self.hitbox_behavior = HitboxBehavior::BlockMouseExceptScroll;
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
fn has_pinch_listeners(&self) -> bool {
|
||||
!self.pinch_listeners.is_empty()
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||
fn has_pinch_listeners(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for elements that want to use the standard GPUI event handlers that don't
|
||||
@@ -753,7 +738,7 @@ pub trait InteractiveElement: Sized {
|
||||
fn key_context<C, E>(mut self, key_context: C) -> Self
|
||||
where
|
||||
C: TryInto<KeyContext, Error = E>,
|
||||
E: Debug,
|
||||
E: std::fmt::Display,
|
||||
{
|
||||
if let Some(key_context) = key_context.try_into().log_err() {
|
||||
self.interactivity().key_context = Some(key_context);
|
||||
@@ -957,11 +942,7 @@ pub trait InteractiveElement: Sized {
|
||||
/// Bind the given callback to pinch gesture events during the bubble phase.
|
||||
/// The fluent API equivalent to [`Interactivity::on_pinch`].
|
||||
///
|
||||
/// Note: This event is only available on macOS and Wayland (Linux).
|
||||
/// On Windows, pinch gestures are simulated as scroll wheel events with Ctrl held.
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
fn on_pinch(mut self, listener: impl Fn(&PinchEvent, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.interactivity().on_pinch(listener);
|
||||
self
|
||||
@@ -970,11 +951,7 @@ pub trait InteractiveElement: Sized {
|
||||
/// Bind the given callback to pinch gesture events during the capture phase.
|
||||
/// The fluent API equivalent to [`Interactivity::capture_pinch`].
|
||||
///
|
||||
/// Note: This event is only available on macOS and Wayland (Linux).
|
||||
/// On Windows, pinch gestures are simulated as scroll wheel events with Ctrl held.
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
fn capture_pinch(
|
||||
mut self,
|
||||
listener: impl Fn(&PinchEvent, &mut Window, &mut App) + 'static,
|
||||
@@ -1198,6 +1175,124 @@ pub trait InteractiveElement: Sized {
|
||||
/// A trait for elements that want to use the standard GPUI interactivity features
|
||||
/// that require state.
|
||||
pub trait StatefulInteractiveElement: InteractiveElement {
|
||||
/// Set the accessible role for this element.
|
||||
///
|
||||
/// See the [accessibility guide](crate::_accessibility) for an overview.
|
||||
fn role(mut self, role: accesskit::Role) -> Self {
|
||||
debug_assert!(
|
||||
role != accesskit::Role::GenericContainer,
|
||||
"GenericContainer is filtered out of the a11y tree and has no effect"
|
||||
);
|
||||
self.interactivity().override_role = Some(role);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the accessible label for this element.
|
||||
fn aria_label(mut self, label: impl Into<SharedString>) -> Self {
|
||||
self.interactivity().aria_label = Some(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the selected state for this element.
|
||||
fn aria_selected(mut self, selected: bool) -> Self {
|
||||
self.interactivity().aria_selected = Some(selected);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the expanded state for this element.
|
||||
fn aria_expanded(mut self, expanded: bool) -> Self {
|
||||
self.interactivity().aria_expanded = Some(expanded);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the toggled state for this element.
|
||||
fn aria_toggled(mut self, toggled: accesskit::Toggled) -> Self {
|
||||
self.interactivity().aria_toggled = Some(toggled);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the numeric value for this element.
|
||||
fn aria_numeric_value(mut self, value: f64) -> Self {
|
||||
self.interactivity().aria_numeric_value = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the minimum numeric value for this element.
|
||||
fn aria_min_numeric_value(mut self, value: f64) -> Self {
|
||||
self.interactivity().aria_min_numeric_value = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the maximum numeric value for this element.
|
||||
fn aria_max_numeric_value(mut self, value: f64) -> Self {
|
||||
self.interactivity().aria_max_numeric_value = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the orientation of this element.
|
||||
fn aria_orientation(mut self, orientation: accesskit::Orientation) -> Self {
|
||||
self.interactivity().aria_orientation = Some(orientation);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the heading level of this element.
|
||||
fn aria_level(mut self, level: usize) -> Self {
|
||||
self.interactivity().aria_level = Some(level);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the position in set of this element.
|
||||
fn aria_position_in_set(mut self, position: usize) -> Self {
|
||||
self.interactivity().aria_position_in_set = Some(position);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the size of set for this element.
|
||||
fn aria_size_of_set(mut self, size: usize) -> Self {
|
||||
self.interactivity().aria_size_of_set = Some(size);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the row index for this element.
|
||||
fn aria_row_index(mut self, index: usize) -> Self {
|
||||
self.interactivity().aria_row_index = Some(index);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the column index for this element.
|
||||
fn aria_column_index(mut self, index: usize) -> Self {
|
||||
self.interactivity().aria_column_index = Some(index);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the row count for this element.
|
||||
fn aria_row_count(mut self, count: usize) -> Self {
|
||||
self.interactivity().aria_row_count = Some(count);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the column count for this element.
|
||||
fn aria_column_count(mut self, count: usize) -> Self {
|
||||
self.interactivity().aria_column_count = Some(count);
|
||||
self
|
||||
}
|
||||
|
||||
/// Register a handler for an accessibility action on this element.
|
||||
/// The handler is called when a screen reader requests the given action.
|
||||
///
|
||||
/// See the [accessibility guide](crate::_accessibility) for an overview.
|
||||
fn on_a11y_action(
|
||||
mut self,
|
||||
action: accesskit::Action,
|
||||
listener: impl FnMut(Option<&accesskit::ActionData>, &mut crate::Window, &mut crate::App)
|
||||
+ 'static,
|
||||
) -> Self {
|
||||
self.interactivity()
|
||||
.a11y_action_listeners
|
||||
.push((action, Box::new(listener)));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set this element to focusable.
|
||||
fn focusable(mut self) -> Self {
|
||||
self.interactivity().focusable = true;
|
||||
@@ -1223,15 +1318,6 @@ pub trait StatefulInteractiveElement: InteractiveElement {
|
||||
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<AbsoluteLength>) -> 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());
|
||||
@@ -1367,7 +1453,6 @@ pub(crate) type MouseMoveListener =
|
||||
pub(crate) type ScrollWheelListener =
|
||||
Box<dyn Fn(&ScrollWheelEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub(crate) type PinchListener =
|
||||
Box<dyn Fn(&PinchEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
|
||||
|
||||
@@ -1507,6 +1592,18 @@ impl Element for Div {
|
||||
self.interactivity.source_location()
|
||||
}
|
||||
|
||||
fn a11y_role(&self) -> Option<accesskit::Role> {
|
||||
// Nodes with `GenericContainer` should never be reported to accesskit.
|
||||
// Equivalent to an HTML div with no role.
|
||||
self.interactivity
|
||||
.override_role
|
||||
.filter(|role| *role != accesskit::Role::GenericContainer)
|
||||
}
|
||||
|
||||
fn write_a11y_info(&self, node: &mut accesskit::Node) {
|
||||
self.interactivity.write_a11y_info(node);
|
||||
}
|
||||
|
||||
#[stacksafe]
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
@@ -1725,7 +1822,6 @@ pub struct Interactivity {
|
||||
pub(crate) mouse_pressure_listeners: Vec<MousePressureListener>,
|
||||
pub(crate) mouse_move_listeners: Vec<MouseMoveListener>,
|
||||
pub(crate) scroll_wheel_listeners: Vec<ScrollWheelListener>,
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub(crate) pinch_listeners: Vec<PinchListener>,
|
||||
pub(crate) key_down_listeners: Vec<KeyDownListener>,
|
||||
pub(crate) key_up_listeners: Vec<KeyUpListener>,
|
||||
@@ -1744,6 +1840,25 @@ pub struct Interactivity {
|
||||
pub(crate) tab_group: bool,
|
||||
pub(crate) tab_stop: bool,
|
||||
|
||||
pub(crate) a11y_action_listeners:
|
||||
Vec<(accesskit::Action, crate::window::a11y::A11yActionListener)>,
|
||||
pub(crate) override_role: Option<accesskit::Role>,
|
||||
pub(crate) aria_label: Option<SharedString>,
|
||||
pub(crate) aria_selected: Option<bool>,
|
||||
pub(crate) aria_expanded: Option<bool>,
|
||||
pub(crate) aria_toggled: Option<accesskit::Toggled>,
|
||||
pub(crate) aria_numeric_value: Option<f64>,
|
||||
pub(crate) aria_min_numeric_value: Option<f64>,
|
||||
pub(crate) aria_max_numeric_value: Option<f64>,
|
||||
pub(crate) aria_orientation: Option<accesskit::Orientation>,
|
||||
pub(crate) aria_level: Option<usize>,
|
||||
pub(crate) aria_position_in_set: Option<usize>,
|
||||
pub(crate) aria_size_of_set: Option<usize>,
|
||||
pub(crate) aria_row_index: Option<usize>,
|
||||
pub(crate) aria_column_index: Option<usize>,
|
||||
pub(crate) aria_row_count: Option<usize>,
|
||||
pub(crate) aria_column_count: Option<usize>,
|
||||
|
||||
#[cfg(any(feature = "inspector", debug_assertions))]
|
||||
pub(crate) source_location: Option<&'static core::panic::Location<'static>>,
|
||||
|
||||
@@ -1864,6 +1979,16 @@ impl Interactivity {
|
||||
|
||||
if let Some(focus_handle) = self.tracked_focus_handle.as_ref() {
|
||||
window.set_focus_handle(focus_handle, cx);
|
||||
|
||||
if window.a11y.is_active() {
|
||||
if let Some(global_id) = global_id {
|
||||
let node_id = global_id.accesskit_node_id();
|
||||
window.a11y.focus_ids.insert(node_id, focus_handle.id);
|
||||
if focus_handle.is_focused(window) && window.a11y.nodes.has_node(node_id) {
|
||||
window.a11y.nodes.set_focus(node_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
window.with_optional_element_state::<InteractiveElementState, _>(
|
||||
global_id,
|
||||
@@ -2088,6 +2213,22 @@ impl Interactivity {
|
||||
}
|
||||
|
||||
self.paint_keyboard_listeners(window, cx);
|
||||
|
||||
if window.a11y.is_active() {
|
||||
if let Some(global_id) = global_id {
|
||||
if !self.a11y_action_listeners.is_empty() {
|
||||
let node_id = global_id.accesskit_node_id();
|
||||
for (action, listener) in
|
||||
self.a11y_action_listeners.drain(..)
|
||||
{
|
||||
window.on_a11y_action(
|
||||
node_id, action, listener,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f(&style, window, cx);
|
||||
|
||||
if let Some(_hitbox) = hitbox {
|
||||
@@ -2297,7 +2438,6 @@ impl Interactivity {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
for listener in self.pinch_listeners.drain(..) {
|
||||
let hitbox = hitbox.clone();
|
||||
window.on_mouse_event(move |event: &PinchEvent, phase, window, cx| {
|
||||
@@ -2892,6 +3032,63 @@ impl Interactivity {
|
||||
|
||||
style
|
||||
}
|
||||
|
||||
pub(crate) fn write_a11y_info(&self, node: &mut accesskit::Node) {
|
||||
if let Some(label) = &self.aria_label {
|
||||
node.set_label(label.to_string());
|
||||
}
|
||||
if let Some(selected) = self.aria_selected {
|
||||
node.set_selected(selected);
|
||||
}
|
||||
if let Some(expanded) = self.aria_expanded {
|
||||
node.set_expanded(expanded);
|
||||
}
|
||||
if let Some(toggled) = self.aria_toggled {
|
||||
node.set_toggled(toggled);
|
||||
}
|
||||
if let Some(value) = self.aria_numeric_value {
|
||||
node.set_numeric_value(value);
|
||||
}
|
||||
if let Some(value) = self.aria_min_numeric_value {
|
||||
node.set_min_numeric_value(value);
|
||||
}
|
||||
if let Some(value) = self.aria_max_numeric_value {
|
||||
node.set_max_numeric_value(value);
|
||||
}
|
||||
if let Some(orientation) = self.aria_orientation {
|
||||
node.set_orientation(orientation);
|
||||
}
|
||||
if let Some(level) = self.aria_level {
|
||||
node.set_level(level);
|
||||
}
|
||||
if let Some(position) = self.aria_position_in_set {
|
||||
node.set_position_in_set(position);
|
||||
}
|
||||
if let Some(size) = self.aria_size_of_set {
|
||||
node.set_size_of_set(size);
|
||||
}
|
||||
if let Some(index) = self.aria_row_index {
|
||||
node.set_row_index(index);
|
||||
}
|
||||
if let Some(index) = self.aria_column_index {
|
||||
node.set_column_index(index);
|
||||
}
|
||||
if let Some(count) = self.aria_row_count {
|
||||
node.set_row_count(count);
|
||||
}
|
||||
if let Some(count) = self.aria_column_count {
|
||||
node.set_column_count(count);
|
||||
}
|
||||
if !self.click_listeners.is_empty() {
|
||||
node.add_action(accesskit::Action::Click);
|
||||
}
|
||||
if self.tracked_focus_handle.is_some() || self.focusable {
|
||||
node.add_action(accesskit::Action::Focus);
|
||||
}
|
||||
for (action, _) in &self.a11y_action_listeners {
|
||||
node.add_action(*action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The per-frame state of an interactive element. Used for tracking stateful interactions like clicks
|
||||
@@ -3093,21 +3290,29 @@ fn handle_tooltip_mouse_move(
|
||||
}
|
||||
Action::ScheduleShow => {
|
||||
let delayed_show_task = window.spawn(cx, {
|
||||
let active_tooltip = active_tooltip.clone();
|
||||
let weak_active_tooltip = Rc::downgrade(active_tooltip);
|
||||
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;
|
||||
let Some(active_tooltip) = weak_active_tooltip.upgrade() else {
|
||||
return;
|
||||
};
|
||||
cx.update(|window, cx| {
|
||||
let new_tooltip =
|
||||
build_tooltip(window, cx).map(|(view, tooltip_is_hoverable)| {
|
||||
let active_tooltip = active_tooltip.clone();
|
||||
let weak_active_tooltip = Rc::downgrade(&active_tooltip);
|
||||
ActiveTooltip::Visible {
|
||||
tooltip: AnyTooltip {
|
||||
view,
|
||||
mouse_position: window.mouse_position(),
|
||||
check_visible_and_update: Rc::new(
|
||||
move |tooltip_bounds, window, cx| {
|
||||
let Some(active_tooltip) =
|
||||
weak_active_tooltip.upgrade()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
handle_tooltip_check_visible_and_update(
|
||||
&active_tooltip,
|
||||
tooltip_is_hoverable,
|
||||
@@ -3186,11 +3391,14 @@ fn handle_tooltip_check_visible_and_update(
|
||||
Action::Hide => clear_active_tooltip(active_tooltip, window),
|
||||
Action::ScheduleHide(tooltip) => {
|
||||
let delayed_hide_task = window.spawn(cx, {
|
||||
let active_tooltip = active_tooltip.clone();
|
||||
let weak_active_tooltip = Rc::downgrade(active_tooltip);
|
||||
async move |cx| {
|
||||
cx.background_executor()
|
||||
.timer(HOVERABLE_TOOLTIP_HIDE_DELAY)
|
||||
.await;
|
||||
let Some(active_tooltip) = weak_active_tooltip.upgrade() else {
|
||||
return;
|
||||
};
|
||||
if active_tooltip.borrow_mut().take().is_some() {
|
||||
cx.update(|window, _cx| window.refresh()).ok();
|
||||
}
|
||||
@@ -3287,6 +3495,14 @@ where
|
||||
self.element.source_location()
|
||||
}
|
||||
|
||||
fn a11y_role(&self) -> Option<accesskit::Role> {
|
||||
self.element.a11y_role()
|
||||
}
|
||||
|
||||
fn write_a11y_info(&self, node: &mut accesskit::Node) {
|
||||
self.element.write_a11y_info(node);
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
id: Option<&GlobalElementId>,
|
||||
@@ -3603,6 +3819,112 @@ impl ScrollHandle {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{AppContext as _, Context, InputEvent, MouseMoveEvent, TestAppContext};
|
||||
use std::rc::Weak;
|
||||
|
||||
struct TestTooltipView;
|
||||
|
||||
impl Render for TestTooltipView {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().w(px(20.)).h(px(20.)).child("tooltip")
|
||||
}
|
||||
}
|
||||
|
||||
type CapturedActiveTooltip = Rc<RefCell<Option<Weak<RefCell<Option<ActiveTooltip>>>>>>;
|
||||
|
||||
struct TooltipCaptureElement {
|
||||
child: AnyElement,
|
||||
captured_active_tooltip: CapturedActiveTooltip,
|
||||
}
|
||||
|
||||
impl IntoElement for TooltipCaptureElement {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for TooltipCaptureElement {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = ();
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
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) {
|
||||
(self.child.request_layout(window, cx), ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self::PrepaintState {
|
||||
self.child.prepaint(window, cx);
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
_prepaint: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
self.child.paint(window, cx);
|
||||
window.with_global_id("target".into(), |global_id, window| {
|
||||
window.with_element_state::<InteractiveElementState, _>(
|
||||
global_id,
|
||||
|state, _window| {
|
||||
let state = state.unwrap();
|
||||
*self.captured_active_tooltip.borrow_mut() =
|
||||
state.active_tooltip.as_ref().map(Rc::downgrade);
|
||||
((), state)
|
||||
},
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
struct TooltipOwner {
|
||||
captured_active_tooltip: CapturedActiveTooltip,
|
||||
}
|
||||
|
||||
impl Render for TooltipOwner {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
TooltipCaptureElement {
|
||||
child: div()
|
||||
.size_full()
|
||||
.child(
|
||||
div()
|
||||
.id("target")
|
||||
.w(px(50.))
|
||||
.h(px(50.))
|
||||
.tooltip(|_, cx| cx.new(|_| TestTooltipView).into()),
|
||||
)
|
||||
.into_any_element(),
|
||||
captured_active_tooltip: self.captured_active_tooltip.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scroll_handle_aligns_wide_children_to_left_edge() {
|
||||
@@ -3641,4 +3963,96 @@ mod tests {
|
||||
|
||||
assert_eq!(handle.offset().y, px(-25.));
|
||||
}
|
||||
|
||||
fn setup_tooltip_owner_test() -> (
|
||||
TestAppContext,
|
||||
crate::AnyWindowHandle,
|
||||
CapturedActiveTooltip,
|
||||
) {
|
||||
let mut test_app = TestAppContext::single();
|
||||
let captured_active_tooltip: CapturedActiveTooltip = Rc::new(RefCell::new(None));
|
||||
let window = test_app.add_window({
|
||||
let captured_active_tooltip = captured_active_tooltip.clone();
|
||||
move |_, _| TooltipOwner {
|
||||
captured_active_tooltip,
|
||||
}
|
||||
});
|
||||
let any_window = window.into();
|
||||
|
||||
test_app
|
||||
.update_window(any_window, |_, window, cx| {
|
||||
window.draw(cx).clear();
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
test_app
|
||||
.update_window(any_window, |_, window, cx| {
|
||||
window.dispatch_event(
|
||||
MouseMoveEvent {
|
||||
position: point(px(10.), px(10.)),
|
||||
modifiers: Default::default(),
|
||||
pressed_button: None,
|
||||
}
|
||||
.to_platform_input(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
test_app
|
||||
.update_window(any_window, |_, window, cx| {
|
||||
window.draw(cx).clear();
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
(test_app, any_window, captured_active_tooltip)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tooltip_waiting_for_show_is_released_when_its_owner_disappears() {
|
||||
let (mut test_app, any_window, captured_active_tooltip) = setup_tooltip_owner_test();
|
||||
|
||||
let weak_active_tooltip = captured_active_tooltip.borrow().clone().unwrap();
|
||||
let active_tooltip = weak_active_tooltip.upgrade().unwrap();
|
||||
assert!(matches!(
|
||||
active_tooltip.borrow().as_ref(),
|
||||
Some(ActiveTooltip::WaitingForShow { .. })
|
||||
));
|
||||
|
||||
test_app
|
||||
.update_window(any_window, |_, window, _| {
|
||||
window.remove_window();
|
||||
})
|
||||
.unwrap();
|
||||
test_app.run_until_parked();
|
||||
drop(active_tooltip);
|
||||
|
||||
assert!(weak_active_tooltip.upgrade().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tooltip_is_released_when_its_owner_disappears() {
|
||||
let (mut test_app, any_window, captured_active_tooltip) = setup_tooltip_owner_test();
|
||||
|
||||
let weak_active_tooltip = captured_active_tooltip.borrow().clone().unwrap();
|
||||
let active_tooltip = weak_active_tooltip.upgrade().unwrap();
|
||||
|
||||
test_app.dispatcher.advance_clock(TOOLTIP_SHOW_DELAY);
|
||||
test_app.run_until_parked();
|
||||
|
||||
assert!(matches!(
|
||||
active_tooltip.borrow().as_ref(),
|
||||
Some(ActiveTooltip::Visible { .. })
|
||||
));
|
||||
|
||||
test_app
|
||||
.update_window(any_window, |_, window, _| {
|
||||
window.remove_window();
|
||||
})
|
||||
.unwrap();
|
||||
test_app.run_until_parked();
|
||||
drop(active_tooltip);
|
||||
|
||||
assert!(weak_active_tooltip.upgrade().is_none());
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,13 @@ use crate::{
|
||||
};
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::scheduler::Instant;
|
||||
use futures::Future;
|
||||
use gpui_util::ResultExt;
|
||||
use image::{
|
||||
AnimationDecoder, DynamicImage, Frame, ImageError, ImageFormat, Rgba,
|
||||
codecs::{gif::GifDecoder, webp::WebPDecoder},
|
||||
};
|
||||
use scheduler::Instant;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
fs,
|
||||
@@ -23,7 +24,6 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use util::ResultExt;
|
||||
|
||||
use super::{Stateful, StatefulInteractiveElement};
|
||||
|
||||
@@ -294,7 +294,7 @@ impl Element for Img {
|
||||
})
|
||||
});
|
||||
|
||||
let frame_index = state.as_ref().map(|state| state.frame_index).unwrap_or(0);
|
||||
let mut frame_index = state.as_ref().map(|state| state.frame_index).unwrap_or(0);
|
||||
|
||||
let layout_id = self.interactivity.request_layout(
|
||||
global_id,
|
||||
@@ -312,26 +312,36 @@ impl Element for Img {
|
||||
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));
|
||||
let frame_count = data.frame_count();
|
||||
let max_frame_index = frame_count.saturating_sub(1);
|
||||
|
||||
if elapsed >= frame_duration {
|
||||
state.frame_index =
|
||||
(state.frame_index + 1) % frame_count;
|
||||
state.last_frame_time =
|
||||
Some(current_time - (elapsed - frame_duration));
|
||||
if let Some(state) = &mut state {
|
||||
state.frame_index = state.frame_index.min(max_frame_index);
|
||||
if frame_count > 1 {
|
||||
if window.is_window_active() {
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
state.last_frame_time = Some(current_time);
|
||||
state.last_frame_time = None;
|
||||
}
|
||||
} else {
|
||||
state.last_frame_time = None;
|
||||
}
|
||||
state.started_loading = None;
|
||||
frame_index = state.frame_index;
|
||||
}
|
||||
|
||||
let image_size = data.render_size(frame_index);
|
||||
@@ -365,7 +375,10 @@ impl Element for Img {
|
||||
};
|
||||
}
|
||||
|
||||
if global_id.is_some() && data.frame_count() > 1 {
|
||||
if global_id.is_some()
|
||||
&& data.frame_count() > 1
|
||||
&& window.is_window_active()
|
||||
{
|
||||
window.request_animation_frame();
|
||||
}
|
||||
}
|
||||
@@ -466,6 +479,9 @@ impl Element for Img {
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
if data.frame_count() == 0 {
|
||||
return;
|
||||
}
|
||||
let new_bounds = self
|
||||
.style
|
||||
.object_fit
|
||||
@@ -643,12 +659,26 @@ impl Asset for ImageAssetLoader {
|
||||
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);
|
||||
match frame {
|
||||
Ok(mut frame) => {
|
||||
// Convert from RGBA to BGRA.
|
||||
for pixel in frame.buffer_mut().chunks_exact_mut(4) {
|
||||
pixel.swap(0, 2);
|
||||
}
|
||||
frames.push(frame);
|
||||
}
|
||||
Err(err) => {
|
||||
log::debug!(
|
||||
"Skipping GIF frame in {source:?} due to decode error: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
frames.push(frame);
|
||||
}
|
||||
|
||||
if frames.is_empty() {
|
||||
return Err(ImageCacheError::Other(Arc::new(anyhow::anyhow!(
|
||||
"GIF could not be decoded: all frames failed ({source:?})"
|
||||
))));
|
||||
}
|
||||
|
||||
frames
|
||||
@@ -661,12 +691,26 @@ impl Asset for ImageAssetLoader {
|
||||
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);
|
||||
match frame {
|
||||
Ok(mut frame) => {
|
||||
// Convert from RGBA to BGRA.
|
||||
for pixel in frame.buffer_mut().chunks_exact_mut(4) {
|
||||
pixel.swap(0, 2);
|
||||
}
|
||||
frames.push(frame);
|
||||
}
|
||||
Err(err) => {
|
||||
log::debug!(
|
||||
"Skipping WebP frame in {source:?} due to decode error: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
frames.push(frame);
|
||||
}
|
||||
|
||||
if frames.is_empty() {
|
||||
return Err(ImageCacheError::Other(Arc::new(anyhow::anyhow!(
|
||||
"WebP could not be decoded: all frames failed ({source:?})"
|
||||
))));
|
||||
}
|
||||
|
||||
frames
|
||||
@@ -697,7 +741,7 @@ impl Asset for ImageAssetLoader {
|
||||
Ok(Arc::new(RenderImage::new(data)))
|
||||
} else {
|
||||
svg_renderer
|
||||
.render_single_frame(&bytes, 1.0, true)
|
||||
.render_single_frame(&bytes, 1.0)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
@@ -757,3 +801,63 @@ impl From<image::ImageError> for ImageCacheError {
|
||||
Self::Image(Arc::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{ParentElement as _, TestAppContext, canvas, div, point, px, size};
|
||||
use image::{Frame, ImageBuffer, Rgba};
|
||||
|
||||
const TEST_IMG_ID: &str = "test-img";
|
||||
|
||||
fn test_image(frame_count: usize) -> Arc<RenderImage> {
|
||||
let frame = Frame::new(ImageBuffer::from_pixel(1, 1, Rgba([0, 0, 0, 0])));
|
||||
Arc::new(RenderImage::new(SmallVec::from_iter(
|
||||
(0..frame_count).map(|_| frame.clone()),
|
||||
)))
|
||||
}
|
||||
|
||||
/// Overwrites the cached `frame_index` of the sibling `img` during paint.
|
||||
fn seed_frame_index(frame_index: usize) -> impl IntoElement {
|
||||
canvas(
|
||||
|_, _, _| (),
|
||||
move |_, _, window, _| {
|
||||
window.with_global_id(TEST_IMG_ID.into(), |id, window| {
|
||||
window.with_element_state::<ImgState, _>(id, |state, _| {
|
||||
let mut state = state.expect("img state should be initialized");
|
||||
state.frame_index = frame_index;
|
||||
((), state)
|
||||
});
|
||||
});
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn zero_frame_image_does_not_panic_on_paint(cx: &mut TestAppContext) {
|
||||
cx.add_empty_window()
|
||||
.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
|
||||
img(ImageSource::Render(test_image(0))).into_any_element()
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn stale_frame_index_is_clamped_when_image_changes(cx: &mut TestAppContext) {
|
||||
let window = cx.add_empty_window();
|
||||
|
||||
// Assert that a cached frame_index from a previous multi-frame image
|
||||
// does not cause an out-of-bounds panic when the image is replaced
|
||||
// with one that has fewer frames.
|
||||
window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
|
||||
div()
|
||||
.child(img(ImageSource::Render(test_image(5))).id(TEST_IMG_ID))
|
||||
.child(seed_frame_index(4))
|
||||
.into_any_element()
|
||||
});
|
||||
window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
|
||||
img(ImageSource::Render(test_image(1)))
|
||||
.id(TEST_IMG_ID)
|
||||
.into_any_element()
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use crate::{
|
||||
Interactivity, IntoElement, LayoutId, Pixels, Point, Radians, SharedString, Size,
|
||||
StyleRefinement, Styled, TransformationMatrix, Window, point, px, radians, size,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use gpui_util::ResultExt;
|
||||
|
||||
/// An SVG element.
|
||||
pub struct Svg {
|
||||
@@ -6,17 +6,250 @@ use crate::{
|
||||
WrappedLineLayout, register_tooltip_mouse_handlers, set_tooltip_on_window,
|
||||
};
|
||||
use anyhow::Context as _;
|
||||
use gpui_util::ResultExt;
|
||||
use itertools::Itertools;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::{Cell, RefCell},
|
||||
mem,
|
||||
ops::Range,
|
||||
ops::{Deref, DerefMut, Range},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
/// An [`Element`] that renders text.
|
||||
///
|
||||
/// In general, [`Text`] objects should be created via the [`text`] macro:
|
||||
/// ```rust
|
||||
/// # use gpui::*;
|
||||
/// # fn render() -> impl IntoElement {
|
||||
/// div().child(text!("hello"))
|
||||
/// # }
|
||||
/// ```
|
||||
/// ## IDs and Accessibility
|
||||
///
|
||||
/// [`Text`] elements have an ID. This ID is primarily used to produce nodes in
|
||||
/// the accessibility tree, which allows the text to be visible to screen
|
||||
/// readers and other assistive technologies.
|
||||
///
|
||||
/// This ID is stable across frames. If the same text, with the same ID, is
|
||||
/// present in two consecutive frames, no updates are reported to the screen
|
||||
/// reader. If the text changes, but the ID stays the same, then the screen
|
||||
/// reader will be notified that a text node's content has changed. **However**,
|
||||
/// if the ID changes, then the screen reader will be notified that a node has
|
||||
/// been removed, and a new node has been added.
|
||||
///
|
||||
/// When using the [`text`] macro, each invocation of the macro will get a
|
||||
/// unique ID, derived from its position in the source code (filename, line, and
|
||||
/// column). For example:
|
||||
/// ```rust
|
||||
/// # use gpui::*;
|
||||
/// let x = text!("hello");
|
||||
/// let y = text!("hello");
|
||||
/// // not equal, because different `text!` invocations produced them
|
||||
/// assert_ne!(x.id(), y.id());
|
||||
///
|
||||
/// fn make_text(s: &str) -> Text { text!(s) }
|
||||
/// let x = make_text("hello");
|
||||
/// let y = make_text("hello");
|
||||
/// // equal, because the same `text!` invocation produced them
|
||||
/// assert_eq!(x.id(), y.id());
|
||||
/// ```
|
||||
/// When the contents of an invocation of [`text`] do not change, this
|
||||
/// distinction is less relevant (with the caveat that you still need to take
|
||||
/// care to ensure that duplicate IDs do not appear).
|
||||
///
|
||||
/// However, when a [`text`] invocation's argument *does* change, you should
|
||||
/// consider whether this change should be reported as a node "updating its
|
||||
/// contents", or an old node being destroyed and a new node being created.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Text {
|
||||
id: Option<ElementId>,
|
||||
text: SharedString,
|
||||
}
|
||||
|
||||
impl Text {
|
||||
/// Create a new [`Text`] element with a specific ID.
|
||||
///
|
||||
/// If you want a unique ID to be assigned automatically, use the [`text`]
|
||||
/// macro. The docs for [`Text`] have more detail about choosing IDs.
|
||||
#[inline]
|
||||
pub const fn new(id: ElementId, text: SharedString) -> Self {
|
||||
Self { id: Some(id), text }
|
||||
}
|
||||
|
||||
/// Create a new [`Text`] element that is inaccessible to screen readers.
|
||||
///
|
||||
/// In order for text to be accessible to screen readers, it must have an ID
|
||||
/// provided. If you want text to be accessible, either use [`text`] to have
|
||||
/// an ID automatically assigned, or use [`Text::new`] to manually assign an
|
||||
/// ID.
|
||||
///
|
||||
/// This function is intended for use inside custom UI components, where
|
||||
/// accessible properties may be set on parent containers.
|
||||
#[inline]
|
||||
pub const fn new_inaccessible(text: SharedString) -> Self {
|
||||
Self { id: None, text }
|
||||
}
|
||||
|
||||
/// The ID of this [`Text`] element.
|
||||
#[inline]
|
||||
pub const fn id(&self) -> Option<&ElementId> {
|
||||
self.id.as_ref()
|
||||
}
|
||||
|
||||
/// Produce a new [`Text`] with the given `id`.
|
||||
pub fn with_id(mut self, id: impl Into<ElementId>) -> Self {
|
||||
self.id = Some(id.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// The text that this [`Text`] element will display.
|
||||
#[inline]
|
||||
pub const fn text(&self) -> &SharedString {
|
||||
&self.text
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Text {
|
||||
type Target = SharedString;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.text
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Text {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.text
|
||||
}
|
||||
}
|
||||
|
||||
/// Trivial hash function for the location information produced by the [`text`]
|
||||
/// macro. Not covered by semver guarantees. Performance is not particularly
|
||||
/// significant because it's only used on small strings in const contexts.
|
||||
#[doc(hidden)]
|
||||
pub const fn __hash_text_macro_location_unstable_do_not_use(s: &'static str) -> u64 {
|
||||
const BASIS: u64 = 0xcbf29ce484222325;
|
||||
const PRIME: u64 = 0x100000001b3;
|
||||
|
||||
let bytes = s.as_bytes();
|
||||
let mut hash = BASIS;
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
hash ^= bytes[i] as u64;
|
||||
hash = hash.wrapping_mul(PRIME);
|
||||
i += 1;
|
||||
}
|
||||
hash
|
||||
}
|
||||
|
||||
/// Create a new [`Text`] element.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use gpui::*;
|
||||
/// let a = text!("hello");
|
||||
/// let b = text!(id = "farewell-message", "hello");
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// Text created with this macro is *accessible*. The macro generates an ID
|
||||
/// based on the source location. See the docs for [`Text`] for a more in-depth
|
||||
/// explanation of the significance of the ID of a [`Text`] element.
|
||||
#[macro_export]
|
||||
macro_rules! text {
|
||||
(id = $id:expr, $text:expr) => {{ $crate::Text::new($id.into(), $text.into()) }};
|
||||
($text:expr) => {{
|
||||
const ID: &'static str = concat!(file!(), "/", line!(), ":", column!());
|
||||
const HASH: u64 = $crate::__hash_text_macro_location_unstable_do_not_use(ID);
|
||||
$crate::Text::new($crate::ElementId::Integer(HASH), $text.into())
|
||||
}};
|
||||
}
|
||||
|
||||
impl IntoElement for Text {
|
||||
type Element = Self;
|
||||
#[inline]
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for Text {
|
||||
type RequestLayoutState = TextLayout;
|
||||
type PrepaintState = ();
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn a11y_role(&self) -> Option<accesskit::Role> {
|
||||
if self.id.is_some() {
|
||||
Some(accesskit::Role::Label)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn write_a11y_info(&self, node: &mut accesskit::Node) {
|
||||
node.set_value(self.text.to_string());
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
<SharedString as Element>::request_layout(&mut self.text, id, inspector_id, window, cx)
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self::PrepaintState {
|
||||
<SharedString as Element>::prepaint(
|
||||
&mut self.text,
|
||||
id,
|
||||
inspector_id,
|
||||
bounds,
|
||||
request_layout,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
prepaint: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
<SharedString as Element>::paint(
|
||||
&mut self.text,
|
||||
id,
|
||||
inspector_id,
|
||||
bounds,
|
||||
request_layout,
|
||||
prepaint,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for &'static str {
|
||||
type RequestLayoutState = TextLayout;
|
||||
@@ -159,6 +392,7 @@ pub struct StyledText {
|
||||
text: SharedString,
|
||||
runs: Option<Vec<TextRun>>,
|
||||
delayed_highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
|
||||
delayed_font_family_overrides: Option<Vec<(Range<usize>, SharedString)>>,
|
||||
layout: TextLayout,
|
||||
}
|
||||
|
||||
@@ -169,6 +403,7 @@ impl StyledText {
|
||||
text: text.into(),
|
||||
runs: None,
|
||||
delayed_highlights: None,
|
||||
delayed_font_family_overrides: None,
|
||||
layout: TextLayout::default(),
|
||||
}
|
||||
}
|
||||
@@ -242,9 +477,54 @@ impl StyledText {
|
||||
runs
|
||||
}
|
||||
|
||||
/// Override the font family for specific byte ranges of the text.
|
||||
///
|
||||
/// This is resolved lazily at layout time, so the overrides are applied
|
||||
/// on top of the inherited text style from the parent element.
|
||||
/// Can be combined with [`with_highlights`](Self::with_highlights).
|
||||
///
|
||||
/// The overrides must be sorted by range start and non-overlapping.
|
||||
/// Each override range must fall on character boundaries.
|
||||
pub fn with_font_family_overrides(
|
||||
mut self,
|
||||
overrides: impl IntoIterator<Item = (Range<usize>, SharedString)>,
|
||||
) -> Self {
|
||||
self.delayed_font_family_overrides = Some(
|
||||
overrides
|
||||
.into_iter()
|
||||
.inspect(|(range, _)| {
|
||||
debug_assert!(self.text.is_char_boundary(range.start));
|
||||
debug_assert!(self.text.is_char_boundary(range.end));
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
fn apply_font_family_overrides(
|
||||
runs: &mut [TextRun],
|
||||
overrides: &[(Range<usize>, SharedString)],
|
||||
) {
|
||||
let mut byte_offset = 0;
|
||||
let mut override_idx = 0;
|
||||
for run in runs.iter_mut() {
|
||||
let run_end = byte_offset + run.len;
|
||||
while override_idx < overrides.len() && overrides[override_idx].0.end <= byte_offset {
|
||||
override_idx += 1;
|
||||
}
|
||||
if override_idx < overrides.len() {
|
||||
let (ref range, ref family) = overrides[override_idx];
|
||||
if byte_offset >= range.start && run_end <= range.end {
|
||||
run.font.family = family.clone();
|
||||
}
|
||||
}
|
||||
byte_offset = run_end;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the text runs for this piece of text.
|
||||
pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
|
||||
let mut text = &**self.text;
|
||||
let mut text = &*self.text;
|
||||
for run in &runs {
|
||||
text = text.get(run.len..).unwrap_or_else(|| {
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -278,12 +558,19 @@ impl Element for StyledText {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let runs = self.runs.take().or_else(|| {
|
||||
let font_family_overrides = self.delayed_font_family_overrides.take();
|
||||
let mut runs = self.runs.take().or_else(|| {
|
||||
self.delayed_highlights.take().map(|delayed_highlights| {
|
||||
Self::compute_runs(&self.text, &window.text_style(), delayed_highlights)
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(ref overrides) = font_family_overrides {
|
||||
let runs =
|
||||
runs.get_or_insert_with(|| vec![window.text_style().to_run(self.text.len())]);
|
||||
Self::apply_font_family_overrides(runs, overrides);
|
||||
}
|
||||
|
||||
let layout_id = self.layout.layout(self.text.clone(), runs, window, cx);
|
||||
(layout_id, ())
|
||||
}
|
||||
@@ -345,9 +632,11 @@ impl TextLayout {
|
||||
) -> 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 line_height = window.pixel_snap(
|
||||
text_style
|
||||
.line_height
|
||||
.to_pixels(font_size.into(), window.rem_size()),
|
||||
);
|
||||
|
||||
let runs = if let Some(runs) = runs {
|
||||
runs
|
||||
@@ -399,14 +688,27 @@ impl TextLayout {
|
||||
}
|
||||
|
||||
let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size);
|
||||
let (text, runs) = if let Some(truncate_width) = truncate_width {
|
||||
line_wrapper.truncate_line(
|
||||
text.clone(),
|
||||
truncate_width,
|
||||
&truncation_affix,
|
||||
&runs,
|
||||
truncate_from,
|
||||
)
|
||||
let (text, runs) = if truncate_width.is_some() {
|
||||
if let Some(max_lines) = text_style.line_clamp
|
||||
&& let Some(wrap_width) = wrap_width
|
||||
{
|
||||
line_wrapper.truncate_wrapped_line(
|
||||
text.clone(),
|
||||
wrap_width,
|
||||
max_lines,
|
||||
&truncation_affix,
|
||||
&runs,
|
||||
truncate_from,
|
||||
)
|
||||
} else {
|
||||
line_wrapper.truncate_line(
|
||||
text.clone(),
|
||||
truncate_width.unwrap_or(Pixels::MAX),
|
||||
&truncation_affix,
|
||||
&runs,
|
||||
truncate_from,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
(text.clone(), Cow::Borrowed(&*runs))
|
||||
};
|
||||
@@ -738,6 +1040,14 @@ impl Element for InteractiveText {
|
||||
None
|
||||
}
|
||||
|
||||
fn a11y_role(&self) -> Option<accesskit::Role> {
|
||||
Some(accesskit::Role::Label)
|
||||
}
|
||||
|
||||
fn write_a11y_info(&self, node: &mut accesskit::Node) {
|
||||
node.set_value(self.text.text.to_string());
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
@@ -940,6 +1250,8 @@ impl IntoElement for InteractiveText {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_into_element_for() {
|
||||
use crate::{ParentElement as _, SharedString, div};
|
||||
@@ -950,4 +1262,23 @@ mod tests {
|
||||
let _ = div().child(Cow::Borrowed("Cow"));
|
||||
let _ = div().child(SharedString::from("SharedString"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_macro_id() {
|
||||
// one call to `text!` = one id
|
||||
fn make_text_stable_id(happy: bool) -> Text {
|
||||
text!(if happy { "happy" } else { "sad" })
|
||||
}
|
||||
|
||||
// two calls to `text!` = two ids
|
||||
fn make_text_unstable_id(happy: bool) -> Text {
|
||||
if happy { text!("happy") } else { text!("sad") }
|
||||
}
|
||||
|
||||
assert_eq!(make_text_stable_id(false).id, make_text_stable_id(true).id);
|
||||
assert_ne!(
|
||||
make_text_unstable_id(false).id,
|
||||
make_text_unstable_id(true).id
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ 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,
|
||||
StyleRefinement, Styled, Window, point, px, size,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cell::RefCell, cmp, ops::Range, rc::Rc, usize};
|
||||
@@ -236,6 +236,18 @@ impl UniformListScrollHandle {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the list is scrolled to the end, or `None` if the list is
|
||||
/// not scrollable.
|
||||
pub fn is_scrolled_to_end(&self) -> Option<bool> {
|
||||
let state = self.0.borrow();
|
||||
let max_offset = state.base_handle.max_offset();
|
||||
if max_offset.y <= px(0.) {
|
||||
return None;
|
||||
}
|
||||
let offset = state.base_handle.offset();
|
||||
Some(-offset.y >= max_offset.y)
|
||||
}
|
||||
|
||||
/// Scroll to the bottom of the list.
|
||||
pub fn scroll_to_bottom(&self) {
|
||||
self.scroll_to_item(usize::MAX, ScrollStrategy::Bottom);
|
||||
@@ -1,23 +1,18 @@
|
||||
use crate::scheduler::Instant;
|
||||
use crate::scheduler::Scheduler;
|
||||
use crate::{App, PlatformDispatcher, PlatformScheduler};
|
||||
use futures::channel::mpsc;
|
||||
use futures::prelude::*;
|
||||
use std::{
|
||||
fmt::Debug, future::Future, marker::PhantomData, mem, pin::Pin, rc::Rc, sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use util::TryFutureExt;
|
||||
use gpui_util::{TryFutureExt, TryFutureExtBacktrace};
|
||||
use scheduler::Instant;
|
||||
use scheduler::Scheduler;
|
||||
use std::{future::Future, marker::PhantomData, mem, pin::Pin, rc::Rc, sync::Arc, time::Duration};
|
||||
|
||||
pub use crate::scheduler::{
|
||||
FallibleTask, ForegroundExecutor as SchedulerForegroundExecutor, Priority,
|
||||
};
|
||||
pub use scheduler::{FallibleTask, LocalExecutor as SchedulerLocalExecutor, Priority, Task};
|
||||
|
||||
/// A pointer to the executor that is currently running,
|
||||
/// for spawning background tasks.
|
||||
#[derive(Clone)]
|
||||
pub struct BackgroundExecutor {
|
||||
inner: crate::scheduler::BackgroundExecutor,
|
||||
inner: scheduler::BackgroundExecutor,
|
||||
dispatcher: Arc<dyn PlatformDispatcher>,
|
||||
}
|
||||
|
||||
@@ -25,90 +20,41 @@ pub struct BackgroundExecutor {
|
||||
/// for spawning tasks on the main thread.
|
||||
#[derive(Clone)]
|
||||
pub struct ForegroundExecutor {
|
||||
inner: crate::scheduler::ForegroundExecutor,
|
||||
inner: scheduler::LocalExecutor,
|
||||
dispatcher: Arc<dyn PlatformDispatcher>,
|
||||
not_send: PhantomData<Rc<()>>,
|
||||
}
|
||||
|
||||
/// Task is a primitive that allows work to happen in the background.
|
||||
/// Extension trait for `Task<Result<T, E>>` that adds `detach_and_log_err` with an `&App` context.
|
||||
///
|
||||
/// 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<T>(crate::scheduler::Task<T>);
|
||||
|
||||
impl<T> Task<T> {
|
||||
/// Creates a new task that will resolve with the value.
|
||||
pub fn ready(val: T) -> Self {
|
||||
Task(crate::scheduler::Task::ready(val))
|
||||
}
|
||||
|
||||
/// Returns true if the task has completed or was created with `Task::ready`.
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.0.is_ready()
|
||||
}
|
||||
|
||||
/// Detaching a task runs it to completion in the background.
|
||||
pub fn detach(self) {
|
||||
self.0.detach()
|
||||
}
|
||||
|
||||
/// Wraps a scheduler Task.
|
||||
pub fn from_scheduler(task: crate::scheduler::Task<T>) -> Self {
|
||||
Task(task)
|
||||
}
|
||||
|
||||
/// Converts this task into a fallible task that returns `Option<T>`.
|
||||
///
|
||||
/// Unlike the standard `Task<T>`, a [`FallibleTask`] will return `None`
|
||||
/// if the task was cancelled.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// // Background task that gracefully handles cancellation:
|
||||
/// cx.background_spawn(async move {
|
||||
/// let result = foreground_task.fallible().await;
|
||||
/// if let Some(value) = result {
|
||||
/// // Process the value
|
||||
/// }
|
||||
/// // If None, task was cancelled - just exit gracefully
|
||||
/// }).detach();
|
||||
/// ```
|
||||
pub fn fallible(self) -> FallibleTask<T> {
|
||||
self.0.fallible()
|
||||
}
|
||||
/// This trait is automatically implemented for all `Task<Result<T, E>>` types.
|
||||
pub trait TaskExt<T, E> {
|
||||
/// Run the task to completion in the background and log any errors that occur.
|
||||
fn detach_and_log_err(self, cx: &App);
|
||||
/// Like [`Self::detach_and_log_err`], but uses `{:?}` formatting on failure so `anyhow::Error`
|
||||
/// values emit their full backtrace. Prefer `detach_and_log_err` unless a backtrace is wanted.
|
||||
fn detach_and_log_err_with_backtrace(self, cx: &App);
|
||||
}
|
||||
|
||||
impl<T, E> Task<Result<T, E>>
|
||||
impl<T, E> TaskExt<T, E> for Task<Result<T, E>>
|
||||
where
|
||||
T: 'static,
|
||||
E: 'static + Debug,
|
||||
E: 'static + std::fmt::Display + std::fmt::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) {
|
||||
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<T> std::future::Future for Task<T> {
|
||||
type Output = T;
|
||||
|
||||
fn poll(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Self::Output> {
|
||||
// SAFETY: Task is a repr(transparent) wrapper around the scheduler Task,
|
||||
// and we're just projecting the pin through to the inner task.
|
||||
let inner = unsafe { self.map_unchecked_mut(|t| &mut t.0) };
|
||||
inner.poll(cx)
|
||||
#[track_caller]
|
||||
fn detach_and_log_err_with_backtrace(self, cx: &App) {
|
||||
let location = *core::panic::Location::caller();
|
||||
cx.foreground_executor()
|
||||
.spawn(self.log_tracked_err_with_backtrace(location))
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,15 +72,15 @@ impl BackgroundExecutor {
|
||||
let scheduler: Arc<dyn Scheduler> = Arc::new(PlatformScheduler::new(dispatcher.clone()));
|
||||
|
||||
Self {
|
||||
inner: crate::scheduler::BackgroundExecutor::new(scheduler),
|
||||
inner: scheduler::BackgroundExecutor::new(scheduler),
|
||||
dispatcher,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the underlying crate::scheduler::BackgroundExecutor.
|
||||
/// Returns the underlying scheduler::BackgroundExecutor.
|
||||
///
|
||||
/// This is used by Ex to pass the executor to thread/worktree code.
|
||||
pub fn scheduler_executor(&self) -> crate::scheduler::BackgroundExecutor {
|
||||
pub fn scheduler_executor(&self) -> scheduler::BackgroundExecutor {
|
||||
self.inner.clone()
|
||||
}
|
||||
|
||||
@@ -161,66 +107,12 @@ impl BackgroundExecutor {
|
||||
R: Send + 'static,
|
||||
{
|
||||
if priority == Priority::RealtimeAudio {
|
||||
Task::from_scheduler(self.inner.spawn_realtime(future))
|
||||
self.inner.spawn_realtime(future)
|
||||
} else {
|
||||
Task::from_scheduler(self.inner.spawn_with_priority(priority, future))
|
||||
self.inner.spawn_with_priority(priority, future)
|
||||
}
|
||||
}
|
||||
|
||||
/// Enqueues the given future to be run to completion on a background thread and blocking the current task on it.
|
||||
///
|
||||
/// This allows to spawn background work that borrows from its scope. Note that the supplied future will run to
|
||||
/// completion before the current task is resumed, even if the current task is slated for cancellation.
|
||||
pub async fn await_on_background<R>(&self, future: impl Future<Output = R> + Send) -> R
|
||||
where
|
||||
R: Send,
|
||||
{
|
||||
use crate::RunnableMeta;
|
||||
use parking_lot::{Condvar, Mutex};
|
||||
|
||||
struct NotifyOnDrop<'a>(&'a (Condvar, Mutex<bool>));
|
||||
|
||||
impl Drop for NotifyOnDrop<'_> {
|
||||
fn drop(&mut self) {
|
||||
*self.0.1.lock() = true;
|
||||
self.0.0.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
struct WaitOnDrop<'a>(&'a (Condvar, Mutex<bool>));
|
||||
|
||||
impl Drop for WaitOnDrop<'_> {
|
||||
fn drop(&mut self) {
|
||||
let mut done = self.0.1.lock();
|
||||
if !*done {
|
||||
self.0.0.wait(&mut done);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let dispatcher = self.dispatcher.clone();
|
||||
let location = core::panic::Location::caller();
|
||||
|
||||
let pair = &(Condvar::new(), Mutex::new(false));
|
||||
let _wait_guard = WaitOnDrop(pair);
|
||||
|
||||
let (runnable, task) = unsafe {
|
||||
async_task::Builder::new()
|
||||
.metadata(RunnableMeta { location })
|
||||
.spawn_unchecked(
|
||||
move |_| async {
|
||||
let _notify_guard = NotifyOnDrop(pair);
|
||||
future.await
|
||||
},
|
||||
move |runnable| {
|
||||
dispatcher.dispatch(runnable, Priority::default());
|
||||
},
|
||||
)
|
||||
};
|
||||
runnable.schedule();
|
||||
task.await
|
||||
}
|
||||
|
||||
/// 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)
|
||||
@@ -340,7 +232,7 @@ impl BackgroundExecutor {
|
||||
|
||||
/// In tests, returns the rng used by the dispatcher.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn rng(&self) -> crate::scheduler::SharedRng {
|
||||
pub fn rng(&self) -> scheduler::SharedRng {
|
||||
self.dispatcher.as_test().unwrap().scheduler().rng()
|
||||
}
|
||||
|
||||
@@ -386,18 +278,29 @@ impl ForegroundExecutor {
|
||||
)
|
||||
} else {
|
||||
let platform_scheduler = Arc::new(PlatformScheduler::new(dispatcher.clone()));
|
||||
let session_id = platform_scheduler.allocate_session_id();
|
||||
(platform_scheduler, session_id)
|
||||
let inner = platform_scheduler.foreground_executor();
|
||||
return Self {
|
||||
inner,
|
||||
dispatcher,
|
||||
not_send: PhantomData,
|
||||
};
|
||||
};
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
let (scheduler, session_id): (Arc<dyn Scheduler>, _) = {
|
||||
let inner = {
|
||||
let platform_scheduler = Arc::new(PlatformScheduler::new(dispatcher.clone()));
|
||||
let session_id = platform_scheduler.allocate_session_id();
|
||||
(platform_scheduler, session_id)
|
||||
platform_scheduler.foreground_executor()
|
||||
};
|
||||
|
||||
let inner = crate::scheduler::ForegroundExecutor::new(session_id, scheduler);
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
let inner = {
|
||||
let scheduler_for_dispatch = Arc::downgrade(&scheduler);
|
||||
scheduler::LocalExecutor::new(session_id, scheduler, move |runnable| {
|
||||
if let Some(scheduler) = scheduler_for_dispatch.upgrade() {
|
||||
scheduler.schedule_local(session_id, runnable);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
Self {
|
||||
inner,
|
||||
@@ -412,7 +315,7 @@ impl ForegroundExecutor {
|
||||
where
|
||||
R: 'static,
|
||||
{
|
||||
Task::from_scheduler(self.inner.spawn(future.boxed_local()))
|
||||
self.inner.spawn(future.boxed_local())
|
||||
}
|
||||
|
||||
/// Enqueues the given Task to run on the main thread with the given priority.
|
||||
@@ -426,7 +329,7 @@ impl ForegroundExecutor {
|
||||
R: 'static,
|
||||
{
|
||||
// Priority is ignored for foreground tasks - they run in order on the main thread
|
||||
Task::from_scheduler(self.inner.spawn(future))
|
||||
self.inner.spawn(future)
|
||||
}
|
||||
|
||||
/// Used by the test harness to run an async test in a synchronous fashion.
|
||||
@@ -472,7 +375,7 @@ impl ForegroundExecutor {
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn scheduler_executor(&self) -> SchedulerForegroundExecutor {
|
||||
pub fn scheduler_executor(&self) -> SchedulerLocalExecutor {
|
||||
self.inner.clone()
|
||||
}
|
||||
}
|
||||
@@ -826,24 +826,45 @@ where
|
||||
};
|
||||
Bounds { origin, size }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Bounds<T>
|
||||
where
|
||||
T: Sub<Output = T> + Half + Clone + Debug + Default + PartialEq,
|
||||
{
|
||||
/// 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<T>, size: Size<T>) -> Bounds<T> {
|
||||
pub fn from_anchor_and_size(corner: Anchor, origin: Point<T>, size: Size<T>) -> Bounds<T> {
|
||||
let origin = match corner {
|
||||
Corner::TopLeft => origin,
|
||||
Corner::TopRight => Point {
|
||||
Anchor::TopLeft => origin,
|
||||
Anchor::TopRight => Point {
|
||||
x: origin.x - size.width.clone(),
|
||||
y: origin.y,
|
||||
},
|
||||
Corner::BottomLeft => Point {
|
||||
Anchor::BottomLeft => Point {
|
||||
x: origin.x,
|
||||
y: origin.y - size.height.clone(),
|
||||
},
|
||||
Corner::BottomRight => Point {
|
||||
Anchor::BottomRight => Point {
|
||||
x: origin.x - size.width.clone(),
|
||||
y: origin.y - size.height.clone(),
|
||||
},
|
||||
Anchor::TopCenter => Point {
|
||||
x: origin.x - size.width.half(),
|
||||
y: origin.y,
|
||||
},
|
||||
Anchor::BottomCenter => Point {
|
||||
x: origin.x - size.width.half(),
|
||||
y: origin.y - size.height.clone(),
|
||||
},
|
||||
Anchor::LeftCenter => Point {
|
||||
x: origin.x,
|
||||
y: origin.y - size.height.half(),
|
||||
},
|
||||
Anchor::RightCenter => Point {
|
||||
x: origin.x - size.width.clone(),
|
||||
y: origin.y - size.height.half(),
|
||||
},
|
||||
};
|
||||
|
||||
Bounds { origin, size }
|
||||
@@ -864,6 +885,43 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Bounds<T>
|
||||
where
|
||||
T: Add<T, Output = T> + Half + Clone + Debug + Default + PartialEq,
|
||||
{
|
||||
/// Returns the top center point of the bounds.
|
||||
pub fn top_center(&self) -> Point<T> {
|
||||
Point {
|
||||
x: self.origin.x.clone() + self.size.width.half(),
|
||||
y: self.origin.y.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the bottom center point of the bounds.
|
||||
pub fn bottom_center(&self) -> Point<T> {
|
||||
Point {
|
||||
x: self.origin.x.clone() + self.size.width.half(),
|
||||
y: self.origin.y.clone() + self.size.height.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the left center point of the bounds.
|
||||
pub fn left_center(&self) -> Point<T> {
|
||||
Point {
|
||||
x: self.origin.x.clone(),
|
||||
y: self.origin.y.clone() + self.size.height.half(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the right center point of the bounds.
|
||||
pub fn right_center(&self) -> Point<T> {
|
||||
Point {
|
||||
x: self.origin.x.clone() + self.size.width.clone(),
|
||||
y: self.origin.y.clone() + self.size.height.half(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Bounds<T>
|
||||
where
|
||||
T: PartialOrd + Add<T, Output = T> + Clone + Debug + Default + PartialEq,
|
||||
@@ -1334,7 +1392,12 @@ where
|
||||
y: self.origin.y.clone() + self.size.height.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Bounds<T>
|
||||
where
|
||||
T: Add<T, Output = T> + Half + Clone + Debug + Default + PartialEq,
|
||||
{
|
||||
/// Returns the requested corner point of the bounds.
|
||||
///
|
||||
/// # Returns
|
||||
@@ -1344,20 +1407,24 @@ where
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use gpui::{Bounds, Corner, Point, Size};
|
||||
/// use gpui::{Bounds, Anchor, Point, Size};
|
||||
/// let bounds = Bounds {
|
||||
/// origin: Point { x: 0, y: 0 },
|
||||
/// size: Size { width: 10, height: 20 },
|
||||
/// };
|
||||
/// let bottom_left = bounds.corner(Corner::BottomLeft);
|
||||
/// let bottom_left = bounds.corner(Anchor::BottomLeft);
|
||||
/// assert_eq!(bottom_left, Point { x: 0, y: 20 });
|
||||
/// ```
|
||||
pub fn corner(&self, corner: Corner) -> Point<T> {
|
||||
pub fn corner(&self, corner: Anchor) -> Point<T> {
|
||||
match corner {
|
||||
Corner::TopLeft => self.origin.clone(),
|
||||
Corner::TopRight => self.top_right(),
|
||||
Corner::BottomLeft => self.bottom_left(),
|
||||
Corner::BottomRight => self.bottom_right(),
|
||||
Anchor::TopLeft => self.origin.clone(),
|
||||
Anchor::TopRight => self.top_right(),
|
||||
Anchor::BottomLeft => self.bottom_left(),
|
||||
Anchor::BottomRight => self.bottom_right(),
|
||||
Anchor::TopCenter => self.top_center(),
|
||||
Anchor::BottomCenter => self.bottom_center(),
|
||||
Anchor::LeftCenter => self.left_center(),
|
||||
Anchor::RightCenter => self.right_center(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2093,9 +2160,9 @@ impl From<Pixels> for Edges<Pixels> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Identifies a corner of a 2d box.
|
||||
/// Identifies a reference point on a 2D box, used to anchor positioned elements.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Corner {
|
||||
pub enum Anchor {
|
||||
/// The top left corner
|
||||
TopLeft,
|
||||
/// The top right corner
|
||||
@@ -2104,24 +2171,36 @@ pub enum Corner {
|
||||
BottomLeft,
|
||||
/// The bottom right corner
|
||||
BottomRight,
|
||||
/// The top center position
|
||||
TopCenter,
|
||||
/// The bottom center position
|
||||
BottomCenter,
|
||||
/// The left center position
|
||||
LeftCenter,
|
||||
/// The right center position
|
||||
RightCenter,
|
||||
}
|
||||
|
||||
impl Corner {
|
||||
impl Anchor {
|
||||
/// Returns the directly opposite corner.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use gpui::Corner;
|
||||
/// assert_eq!(Corner::TopLeft.opposite_corner(), Corner::BottomRight);
|
||||
/// # use gpui::Anchor;
|
||||
/// assert_eq!(Anchor::TopLeft.opposite(), Anchor::BottomRight);
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn opposite_corner(self) -> Self {
|
||||
pub fn opposite(self) -> Self {
|
||||
match self {
|
||||
Corner::TopLeft => Corner::BottomRight,
|
||||
Corner::TopRight => Corner::BottomLeft,
|
||||
Corner::BottomLeft => Corner::TopRight,
|
||||
Corner::BottomRight => Corner::TopLeft,
|
||||
Anchor::TopLeft => Anchor::BottomRight,
|
||||
Anchor::TopRight => Anchor::BottomLeft,
|
||||
Anchor::BottomLeft => Anchor::TopRight,
|
||||
Anchor::BottomRight => Anchor::TopLeft,
|
||||
Anchor::TopCenter => Anchor::BottomCenter,
|
||||
Anchor::BottomCenter => Anchor::TopCenter,
|
||||
Anchor::LeftCenter => Anchor::RightCenter,
|
||||
Anchor::RightCenter => Anchor::LeftCenter,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2130,27 +2209,44 @@ impl Corner {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use gpui::{Axis, Corner};
|
||||
/// let result = Corner::TopLeft.other_side_corner_along(Axis::Horizontal);
|
||||
/// assert_eq!(result, Corner::TopRight);
|
||||
/// # use gpui::{Axis, Anchor};
|
||||
/// let result = Anchor::TopLeft.other_side_along(Axis::Horizontal);
|
||||
/// assert_eq!(result, Anchor::TopRight);
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn other_side_corner_along(self, axis: Axis) -> Self {
|
||||
pub fn other_side_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,
|
||||
Anchor::TopLeft => Anchor::BottomLeft,
|
||||
Anchor::TopRight => Anchor::BottomRight,
|
||||
Anchor::BottomLeft => Anchor::TopLeft,
|
||||
Anchor::BottomRight => Anchor::TopRight,
|
||||
Anchor::TopCenter => Anchor::BottomCenter,
|
||||
Anchor::BottomCenter => Anchor::TopCenter,
|
||||
Anchor::LeftCenter => Anchor::LeftCenter,
|
||||
Anchor::RightCenter => Anchor::RightCenter,
|
||||
},
|
||||
Axis::Horizontal => match self {
|
||||
Corner::TopLeft => Corner::TopRight,
|
||||
Corner::TopRight => Corner::TopLeft,
|
||||
Corner::BottomLeft => Corner::BottomRight,
|
||||
Corner::BottomRight => Corner::BottomLeft,
|
||||
Anchor::TopLeft => Anchor::TopRight,
|
||||
Anchor::TopRight => Anchor::TopLeft,
|
||||
Anchor::BottomLeft => Anchor::BottomRight,
|
||||
Anchor::BottomRight => Anchor::BottomLeft,
|
||||
Anchor::TopCenter => Anchor::TopCenter,
|
||||
Anchor::BottomCenter => Anchor::BottomCenter,
|
||||
Anchor::LeftCenter => Anchor::RightCenter,
|
||||
Anchor::RightCenter => Anchor::LeftCenter,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if at the center.
|
||||
#[inline]
|
||||
pub fn is_center(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::TopCenter | Self::BottomCenter | Self::LeftCenter | Self::RightCenter
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the corners of a box in a 2D space, such as border radius.
|
||||
@@ -2172,7 +2268,7 @@ pub struct Corners<T: Clone + Debug + Default + PartialEq> {
|
||||
|
||||
impl<T> Corners<T>
|
||||
where
|
||||
T: Clone + Debug + Default + PartialEq,
|
||||
T: Add<T, Output = T> + Half + Clone + Debug + Default + PartialEq,
|
||||
{
|
||||
/// Constructs `Corners` where all sides are set to the same specified value.
|
||||
///
|
||||
@@ -2207,31 +2303,60 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the requested corner.
|
||||
/// Returns the requested corner value, supporting all eight corner positions.
|
||||
///
|
||||
/// For the four basic corners (TopLeft, TopRight, BottomLeft, BottomRight),
|
||||
/// this returns the corresponding field value directly.
|
||||
///
|
||||
/// For the center positions (TopCenter, BottomCenter, LeftCenter, RightCenter),
|
||||
/// this calculates the average of the two adjacent corners.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A `Point<T>` representing the corner requested by the parameter.
|
||||
/// A value of type `T` representing the corner requested by the parameter.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Basic corner positions:
|
||||
///
|
||||
/// ```
|
||||
/// # use gpui::{Corner, Corners};
|
||||
/// # use gpui::{Anchor, Corners};
|
||||
/// let corners = Corners {
|
||||
/// top_left: 1,
|
||||
/// top_right: 2,
|
||||
/// bottom_left: 3,
|
||||
/// bottom_right: 4
|
||||
/// top_left: 10,
|
||||
/// top_right: 20,
|
||||
/// bottom_left: 30,
|
||||
/// bottom_right: 40
|
||||
/// };
|
||||
/// assert_eq!(corners.corner(Corner::BottomLeft), 3);
|
||||
/// assert_eq!(corners.corner(Anchor::TopLeft), 10);
|
||||
/// assert_eq!(corners.corner(Anchor::BottomRight), 40);
|
||||
/// ```
|
||||
///
|
||||
/// Center positions (calculated as average of adjacent corners):
|
||||
///
|
||||
/// ```
|
||||
/// # use gpui::{Anchor, Corners};
|
||||
/// let corners = Corners {
|
||||
/// top_left: 10,
|
||||
/// top_right: 20,
|
||||
/// bottom_left: 30,
|
||||
/// bottom_right: 40
|
||||
/// };
|
||||
/// assert_eq!(corners.corner(Anchor::TopCenter), 15);
|
||||
/// assert_eq!(corners.corner(Anchor::BottomCenter), 35);
|
||||
/// assert_eq!(corners.corner(Anchor::LeftCenter), 20);
|
||||
/// assert_eq!(corners.corner(Anchor::RightCenter), 30);
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn corner(&self, corner: Corner) -> T {
|
||||
pub fn corner(&self, corner: Anchor) -> 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(),
|
||||
Anchor::TopLeft => self.top_left.clone(),
|
||||
Anchor::TopRight => self.top_right.clone(),
|
||||
Anchor::BottomLeft => self.bottom_left.clone(),
|
||||
Anchor::BottomRight => self.bottom_right.clone(),
|
||||
Anchor::TopCenter => (self.top_left.clone() + self.top_right.clone()).half(),
|
||||
Anchor::BottomCenter => (self.bottom_left.clone() + self.bottom_right.clone()).half(),
|
||||
Anchor::LeftCenter => (self.top_left.clone() + self.bottom_left.clone()).half(),
|
||||
Anchor::RightCenter => (self.top_right.clone() + self.bottom_right.clone()).half(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2337,7 +2462,7 @@ impl<T: Div<f32, Output = T> + Ord + Clone + Debug + Default + PartialEq> Corner
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Corner radii values clamped to fit.
|
||||
/// Anchor radii values clamped to fit.
|
||||
#[must_use]
|
||||
pub fn clamp_radii_for_quad_size(self, size: Size<T>) -> Corners<T> {
|
||||
let max = cmp::min(size.width, size.height) / 2.;
|
||||
@@ -5,8 +5,8 @@
|
||||
#![allow(unused_mut)] // False positives in platform specific code
|
||||
|
||||
extern crate self as gpui;
|
||||
extern crate gpui_ce_macros as gpui_macros;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub static GPUI_MANIFEST_DIR: &'static str = env!("CARGO_MANIFEST_DIR");
|
||||
#[macro_use]
|
||||
mod action;
|
||||
mod app;
|
||||
@@ -30,7 +30,6 @@ mod inspector;
|
||||
mod interactive;
|
||||
mod key_dispatch;
|
||||
mod keymap;
|
||||
mod local_util;
|
||||
mod path_builder;
|
||||
mod platform;
|
||||
pub mod prelude;
|
||||
@@ -40,9 +39,6 @@ pub mod profiler;
|
||||
#[expect(missing_docs)]
|
||||
pub mod queue;
|
||||
mod scene;
|
||||
/// The scheduler module provides task scheduling, execution, and timing primitives.
|
||||
pub mod scheduler;
|
||||
mod shared_string;
|
||||
mod shared_uri;
|
||||
mod style;
|
||||
mod styled;
|
||||
@@ -53,12 +49,15 @@ mod taffy;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
mod text_system;
|
||||
mod util;
|
||||
mod view;
|
||||
mod window;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use proptest;
|
||||
|
||||
#[cfg(doc)]
|
||||
pub mod _accessibility;
|
||||
#[cfg(doc)]
|
||||
pub mod _ownership_and_data_flow;
|
||||
|
||||
@@ -78,6 +77,9 @@ mod seal {
|
||||
pub trait Sealed {}
|
||||
}
|
||||
|
||||
pub use accesskit;
|
||||
pub use accesskit::Action as AccessibleAction;
|
||||
pub use accesskit::{Orientation, Role, Toggled};
|
||||
pub use action::*;
|
||||
pub use anyhow::Result;
|
||||
pub use app::*;
|
||||
@@ -92,16 +94,16 @@ pub use executor::*;
|
||||
pub use geometry::*;
|
||||
pub use global::*;
|
||||
pub use gpui_macros::{
|
||||
AppContext, IntoElement, Render, VisualContext, derive_inspector_reflection, register_action,
|
||||
test,
|
||||
AppContext, IntoElement, Render, VisualContext, property_test, register_action, test,
|
||||
};
|
||||
pub use gpui_shared_string::*;
|
||||
pub use gpui_util::arc_cow::ArcCow;
|
||||
pub use http_client;
|
||||
pub use input::*;
|
||||
pub use inspector::*;
|
||||
pub use interactive::*;
|
||||
use key_dispatch::*;
|
||||
pub use keymap::*;
|
||||
pub use local_util::{FutureExt, Timeout, command};
|
||||
pub use path_builder::*;
|
||||
pub use platform::*;
|
||||
pub use profiler::*;
|
||||
@@ -109,7 +111,6 @@ pub use profiler::*;
|
||||
pub use queue::{PriorityQueueReceiver, PriorityQueueSender};
|
||||
pub use refineable::*;
|
||||
pub use scene::*;
|
||||
pub use shared_string::*;
|
||||
pub use shared_uri::*;
|
||||
use std::{any::Any, future::Future};
|
||||
pub use style::*;
|
||||
@@ -122,10 +123,12 @@ pub use taffy::{AvailableSpace, LayoutId};
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use test::*;
|
||||
pub use text_system::*;
|
||||
pub use util::arc_cow::ArcCow;
|
||||
pub use util::{FutureExt, Timeout};
|
||||
pub use view::*;
|
||||
pub use window::*;
|
||||
|
||||
pub use pollster::block_on;
|
||||
|
||||
/// The context trait, allows the different contexts in GPUI to be used
|
||||
/// interchangeably for certain operations.
|
||||
pub trait AppContext {
|
||||
@@ -173,6 +176,16 @@ pub trait AppContext {
|
||||
where
|
||||
F: FnOnce(AnyView, &mut Window, &mut App) -> T;
|
||||
|
||||
/// Run `f` against the entity's *current* window — the most recently
|
||||
/// rendered window that referenced the entity. Returns `None` if the
|
||||
/// entity has no current window or that window is unavailable. See
|
||||
/// [`App::with_window`] for the underlying lookup.
|
||||
fn with_window<R>(
|
||||
&mut self,
|
||||
entity_id: EntityId,
|
||||
f: impl FnOnce(&mut Window, &mut App) -> R,
|
||||
) -> Option<R>;
|
||||
|
||||
/// Read a window off of the application context.
|
||||
fn read_window<T, R>(
|
||||
&self,
|
||||
@@ -187,4 +187,9 @@ impl<V: EntityInputHandler> InputHandler for ElementInputHandler<V> {
|
||||
self.view
|
||||
.update(cx, |view, cx| view.accepts_text_input(window, cx))
|
||||
}
|
||||
|
||||
fn prefers_ime_for_printable_keys(&mut self, window: &mut Window, cx: &mut App) -> bool {
|
||||
self.view
|
||||
.update(cx, |view, cx| view.accepts_text_input(window, cx))
|
||||
}
|
||||
}
|
||||
@@ -473,10 +473,7 @@ impl Default for ScrollDelta {
|
||||
/// A pinch gesture event from the platform, generated when the user performs
|
||||
/// a pinch-to-zoom gesture (typically on a trackpad).
|
||||
///
|
||||
/// Note: This event is only available on macOS and Wayland (Linux).
|
||||
/// On Windows, pinch gestures are simulated as scroll wheel events with Ctrl held.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub struct PinchEvent {
|
||||
/// The position of the pinch center on the window.
|
||||
pub position: Point<Pixels>,
|
||||
@@ -493,20 +490,15 @@ pub struct PinchEvent {
|
||||
pub phase: TouchPhase,
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
impl Sealed for PinchEvent {}
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
impl InputEvent for PinchEvent {
|
||||
fn to_platform_input(self) -> PlatformInput {
|
||||
PlatformInput::Pinch(self)
|
||||
}
|
||||
}
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
impl GestureEvent for PinchEvent {}
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
impl MouseEvent for PinchEvent {}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
impl Deref for PinchEvent {
|
||||
type Target = Modifiers;
|
||||
|
||||
@@ -675,7 +667,6 @@ pub enum PlatformInput {
|
||||
/// The scroll wheel was used.
|
||||
ScrollWheel(ScrollWheelEvent),
|
||||
/// A pinch gesture was performed.
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
Pinch(PinchEvent),
|
||||
/// Files were dragged and dropped onto the window.
|
||||
FileDrop(FileDropEvent),
|
||||
@@ -693,7 +684,6 @@ impl PlatformInput {
|
||||
PlatformInput::MousePressure(event) => Some(event),
|
||||
PlatformInput::MouseExited(event) => Some(event),
|
||||
PlatformInput::ScrollWheel(event) => Some(event),
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
PlatformInput::Pinch(event) => Some(event),
|
||||
PlatformInput::FileDrop(event) => Some(event),
|
||||
}
|
||||
@@ -710,7 +700,6 @@ impl PlatformInput {
|
||||
PlatformInput::MousePressure(_) => None,
|
||||
PlatformInput::MouseExited(_) => None,
|
||||
PlatformInput::ScrollWheel(_) => None,
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
PlatformInput::Pinch(_) => None,
|
||||
PlatformInput::FileDrop(_) => None,
|
||||
}
|
||||
@@ -2,21 +2,6 @@ mod app_menu;
|
||||
mod keyboard;
|
||||
mod keystroke;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) mod mac;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
pub(crate) mod linux;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) mod windows;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd", target_family = "wasm"))]
|
||||
pub(crate) mod wgpu;
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub(crate) mod web;
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "wayland"))]
|
||||
#[expect(missing_docs)]
|
||||
pub mod layer_shell;
|
||||
@@ -43,17 +28,17 @@ pub(crate) type PlatformScreenCaptureFrame = ();
|
||||
#[cfg(all(target_os = "macos", feature = "screen-capture"))]
|
||||
pub(crate) type PlatformScreenCaptureFrame = core_video::image_buffer::CVImageBuffer;
|
||||
|
||||
use crate::scheduler::Instant;
|
||||
pub use crate::scheduler::RunnableMeta;
|
||||
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, Priority, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Scene,
|
||||
ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer, SystemWindowTab, Task,
|
||||
ThreadTaskTimings, Window, WindowControlArea, hash, point, px, size,
|
||||
ForegroundExecutor, GlyphId, GpuSpecs, Hsla, ImageSource, Keymap, LineLayout, Pixels,
|
||||
PlatformInput, Point, Priority, RenderGlyphParams, RenderImage, RenderImageParams,
|
||||
RenderSvgParams, Scene, ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer,
|
||||
SystemWindowTab, Task, ThreadTaskTimings, Window, WindowControlArea, hash, point, px, size,
|
||||
};
|
||||
use anyhow::Result;
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
use anyhow::bail;
|
||||
use async_task::Runnable;
|
||||
use futures::channel::oneshot;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@@ -61,6 +46,8 @@ use image::RgbaImage;
|
||||
use image::codecs::gif::GifDecoder;
|
||||
use image::{AnimationDecoder as _, Frame};
|
||||
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
|
||||
use scheduler::Instant;
|
||||
pub use scheduler::RunnableMeta;
|
||||
use schemars::JsonSchema;
|
||||
use seahash::SeaHasher;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -93,81 +80,6 @@ pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream}
|
||||
#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
|
||||
pub use visual_test::VisualTestPlatform;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use mac::MacPlatform;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use windows::WindowsPlatform;
|
||||
|
||||
/// Returns the default [`Platform`] for the current OS.
|
||||
pub fn current_platform(headless: bool) -> Rc<dyn Platform> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Rc::new(mac::MacPlatform::new(headless))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
Rc::new(
|
||||
windows::WindowsPlatform::new(headless).expect("failed to initialize Windows platform"),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
{
|
||||
linux::current_platform(headless)
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
{
|
||||
let _ = headless;
|
||||
Rc::new(web::WebPlatform::new(true))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a background executor for the current platform.
|
||||
pub fn background_executor() -> crate::BackgroundExecutor {
|
||||
current_platform(true).background_executor()
|
||||
}
|
||||
|
||||
/// Creates a new application with the current platform.
|
||||
pub fn application() -> crate::Application {
|
||||
crate::Application::with_platform(current_platform(false))
|
||||
}
|
||||
|
||||
/// Creates a new headless application.
|
||||
pub fn headless() -> crate::Application {
|
||||
crate::Application::with_platform(current_platform(true))
|
||||
}
|
||||
|
||||
/// Unlike `application`, this function returns a single-threaded web application.
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub fn single_threaded_web() -> crate::Application {
|
||||
crate::Application::with_platform(Rc::new(web::WebPlatform::new(false)))
|
||||
}
|
||||
|
||||
/// Initializes panic hooks and logging for the web platform.
|
||||
/// Call this before running the application in a wasm_bindgen entrypoint.
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub fn web_init() {
|
||||
console_error_panic_hook::set_once();
|
||||
web::init_logging();
|
||||
}
|
||||
|
||||
/// Returns a new headless renderer for the current platform, if available.
|
||||
#[cfg(feature = "test-support")]
|
||||
pub fn current_headless_renderer() -> Option<Box<dyn crate::PlatformHeadlessRenderer>> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Some(Box::new(mac::metal_renderer::MetalHeadlessRenderer::new()))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(jk): return an enum instead of a string
|
||||
/// Return which compositor we're guessing we'll use.
|
||||
/// Does not attempt to connect to the given compositor.
|
||||
@@ -246,6 +158,11 @@ pub trait Platform: 'static {
|
||||
/// Returns the appearance of the application's windows.
|
||||
fn window_appearance(&self) -> WindowAppearance;
|
||||
|
||||
/// Returns the window button layout configuration when supported.
|
||||
fn button_layout(&self) -> Option<WindowButtonLayout> {
|
||||
None
|
||||
}
|
||||
|
||||
fn open_url(&self, url: &str);
|
||||
fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
|
||||
fn register_url_scheme(&self, url: &str) -> Task<Result<()>>;
|
||||
@@ -295,6 +212,14 @@ pub trait Platform: 'static {
|
||||
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
|
||||
|
||||
fn set_cursor_style(&self, style: CursorStyle);
|
||||
|
||||
/// Hides the mouse cursor until the user moves the mouse over one of
|
||||
/// this application's windows.
|
||||
fn hide_cursor_until_mouse_moves(&self);
|
||||
|
||||
/// Returns whether the mouse cursor is currently visible.
|
||||
fn is_cursor_visible(&self) -> bool;
|
||||
|
||||
fn should_auto_hide_scrollbars(&self) -> bool;
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
|
||||
@@ -401,22 +326,22 @@ pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
|
||||
|
||||
/// An opaque identifier for a hardware display
|
||||
#[derive(PartialEq, Eq, Hash, Copy, Clone)]
|
||||
pub struct DisplayId(pub(crate) u32);
|
||||
pub struct DisplayId(pub(crate) u64);
|
||||
|
||||
impl DisplayId {
|
||||
/// Create a new `DisplayId` from a raw platform display identifier.
|
||||
pub fn new(id: u32) -> Self {
|
||||
pub fn new(id: u64) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for DisplayId {
|
||||
fn from(id: u32) -> Self {
|
||||
impl From<u64> for DisplayId {
|
||||
fn from(id: u64) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DisplayId> for u32 {
|
||||
impl From<DisplayId> for u64 {
|
||||
fn from(id: DisplayId) -> Self {
|
||||
id.0
|
||||
}
|
||||
@@ -497,6 +422,145 @@ impl Default for WindowControls {
|
||||
}
|
||||
}
|
||||
|
||||
/// A window control button type used in [`WindowButtonLayout`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum WindowButton {
|
||||
/// The minimize button
|
||||
Minimize,
|
||||
/// The maximize button
|
||||
Maximize,
|
||||
/// The close button
|
||||
Close,
|
||||
}
|
||||
|
||||
impl WindowButton {
|
||||
/// Returns a stable element ID for rendering this button.
|
||||
pub fn id(&self) -> &'static str {
|
||||
match self {
|
||||
WindowButton::Minimize => "minimize",
|
||||
WindowButton::Maximize => "maximize",
|
||||
WindowButton::Close => "close",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
fn index(&self) -> usize {
|
||||
match self {
|
||||
WindowButton::Minimize => 0,
|
||||
WindowButton::Maximize => 1,
|
||||
WindowButton::Close => 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Maximum number of [`WindowButton`]s per side in the titlebar.
|
||||
pub const MAX_BUTTONS_PER_SIDE: usize = 3;
|
||||
|
||||
/// Describes which [`WindowButton`]s appear on each side of the titlebar.
|
||||
///
|
||||
/// On Linux, this is read from the desktop environment's configuration
|
||||
/// (e.g. GNOME's `gtk-decoration-layout` gsetting) via [`WindowButtonLayout::parse`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct WindowButtonLayout {
|
||||
/// Buttons on the left side of the titlebar.
|
||||
pub left: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
|
||||
/// Buttons on the right side of the titlebar.
|
||||
pub right: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
impl WindowButtonLayout {
|
||||
/// Returns Zed's built-in fallback button layout for Linux titlebars.
|
||||
pub fn linux_default() -> Self {
|
||||
Self {
|
||||
left: [None; MAX_BUTTONS_PER_SIDE],
|
||||
right: [
|
||||
Some(WindowButton::Minimize),
|
||||
Some(WindowButton::Maximize),
|
||||
Some(WindowButton::Close),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a GNOME-style `button-layout` string (e.g. `"close,minimize:maximize"`).
|
||||
pub fn parse(layout_string: &str) -> Result<Self> {
|
||||
fn parse_side(
|
||||
s: &str,
|
||||
seen_buttons: &mut [bool; MAX_BUTTONS_PER_SIDE],
|
||||
unrecognized: &mut Vec<String>,
|
||||
) -> [Option<WindowButton>; MAX_BUTTONS_PER_SIDE] {
|
||||
let mut result = [None; MAX_BUTTONS_PER_SIDE];
|
||||
let mut i = 0;
|
||||
for name in s.split(',') {
|
||||
let trimmed = name.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let button = match trimmed {
|
||||
"minimize" => Some(WindowButton::Minimize),
|
||||
"maximize" => Some(WindowButton::Maximize),
|
||||
"close" => Some(WindowButton::Close),
|
||||
other => {
|
||||
unrecognized.push(other.to_string());
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(button) = button {
|
||||
if seen_buttons[button.index()] {
|
||||
continue;
|
||||
}
|
||||
if let Some(slot) = result.get_mut(i) {
|
||||
*slot = Some(button);
|
||||
seen_buttons[button.index()] = true;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
let (left_str, right_str) = layout_string.split_once(':').unwrap_or(("", layout_string));
|
||||
let mut unrecognized = Vec::new();
|
||||
let mut seen_buttons = [false; MAX_BUTTONS_PER_SIDE];
|
||||
let layout = Self {
|
||||
left: parse_side(left_str, &mut seen_buttons, &mut unrecognized),
|
||||
right: parse_side(right_str, &mut seen_buttons, &mut unrecognized),
|
||||
};
|
||||
|
||||
if !unrecognized.is_empty()
|
||||
&& layout.left.iter().all(Option::is_none)
|
||||
&& layout.right.iter().all(Option::is_none)
|
||||
{
|
||||
bail!(
|
||||
"button layout string {:?} contains no valid buttons (unrecognized: {})",
|
||||
layout_string,
|
||||
unrecognized.join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
Ok(layout)
|
||||
}
|
||||
|
||||
/// Formats the layout back into a GNOME-style `button-layout` string.
|
||||
#[cfg(test)]
|
||||
pub fn format(&self) -> String {
|
||||
fn format_side(buttons: &[Option<WindowButton>; MAX_BUTTONS_PER_SIDE]) -> String {
|
||||
buttons
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|button| match button {
|
||||
WindowButton::Minimize => "minimize",
|
||||
WindowButton::Maximize => "maximize",
|
||||
WindowButton::Close => "close",
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
}
|
||||
|
||||
format!("{}:{}", format_side(&self.left), format_side(&self.right))
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -527,6 +591,16 @@ impl Tiling {
|
||||
}
|
||||
}
|
||||
|
||||
/// Callbacks for the accessibility adapter.
|
||||
pub struct A11yCallbacks {
|
||||
/// Called when the adapter is activated (a screen reader connects).
|
||||
pub activation: Box<dyn Fn() -> Option<accesskit::TreeUpdate> + Send + 'static>,
|
||||
/// Called when an action is requested by the screen reader.
|
||||
pub action: Box<dyn Fn(accesskit::ActionRequest) + Send + 'static>,
|
||||
/// Called when the adapter is deactivated (screen reader disconnects).
|
||||
pub deactivation: Box<dyn Fn() + Send + 'static>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
|
||||
#[expect(missing_docs)]
|
||||
pub struct RequestFrameOptions {
|
||||
@@ -578,6 +652,7 @@ pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>);
|
||||
fn on_close(&self, callback: Box<dyn FnOnce()>);
|
||||
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_button_layout_changed(&self, _callback: Box<dyn FnMut()>) {}
|
||||
fn draw(&self, scene: &Scene);
|
||||
fn completed_frame(&self) {}
|
||||
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
|
||||
@@ -594,6 +669,7 @@ pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
false
|
||||
}
|
||||
fn set_edited(&mut self, _edited: bool) {}
|
||||
fn set_document_path(&self, _path: Option<&std::path::Path>) {}
|
||||
fn show_character_palette(&self) {}
|
||||
fn titlebar_double_click(&self) {}
|
||||
fn on_move_tab_to_new_window(&self, _callback: Box<dyn FnMut()>) {}
|
||||
@@ -632,6 +708,17 @@ pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
|
||||
fn update_ime_position(&self, _bounds: Bounds<Pixels>);
|
||||
|
||||
fn play_system_bell(&self) {}
|
||||
|
||||
/// Initialize the accessibility adapter with callbacks.
|
||||
fn a11y_init(&self, _callbacks: A11yCallbacks) {}
|
||||
|
||||
/// Provide a TreeUpdate to the accessibility adapter.
|
||||
fn a11y_tree_update(&self, _tree_update: accesskit::TreeUpdate) {}
|
||||
|
||||
/// Inform the adapter of updated window bounds.
|
||||
fn a11y_update_window_bounds(&self) {}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
fn as_test(&mut self) -> Option<&mut TestWindow> {
|
||||
None
|
||||
@@ -666,7 +753,7 @@ pub trait PlatformHeadlessRenderer {
|
||||
pub type RunnableVariant = Runnable<RunnableMeta>;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub type TimerResolutionGuard = util::Deferred<Box<dyn FnOnce() + Send>>;
|
||||
pub type TimerResolutionGuard = gpui_util::Deferred<Box<dyn FnOnce() + Send>>;
|
||||
|
||||
/// This type is public so that our test macro can generate and use it, but it should not
|
||||
/// be considered part of our public API.
|
||||
@@ -686,7 +773,7 @@ pub trait PlatformDispatcher: Send + Sync {
|
||||
}
|
||||
|
||||
fn increase_timer_resolution(&self) -> TimerResolutionGuard {
|
||||
util::defer(Box::new(|| {}))
|
||||
gpui_util::defer(Box::new(|| {}))
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@@ -723,6 +810,10 @@ pub trait PlatformTextSystem: Send + Sync {
|
||||
/// Returns the recommended text rendering mode for the given font and size.
|
||||
fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
|
||||
-> TextRenderingMode;
|
||||
/// Returns the dilation level to use for a glyph painted in the given color.
|
||||
fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(missing_docs)]
|
||||
@@ -989,7 +1080,7 @@ impl<T> AtlasTextureList<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
#[expect(missing_docs)]
|
||||
pub struct AtlasTile {
|
||||
@@ -1185,6 +1276,13 @@ impl PlatformInputHandler {
|
||||
.update(|window, cx| self.handler.accepts_text_input(window, cx))
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
|
||||
self.cx
|
||||
.update(|window, cx| self.handler.prefers_ime_for_printable_keys(window, cx))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// A struct representing a selection in a text buffer, in UTF16 characters.
|
||||
@@ -1298,6 +1396,18 @@ pub trait InputHandler: 'static {
|
||||
fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Returns whether printable keys should be routed to the IME before keybinding
|
||||
/// matching when a non-ASCII input source (e.g. Japanese, Korean, Chinese IME)
|
||||
/// is active. This prevents multi-stroke keybindings like `jj` from intercepting
|
||||
/// keys that the IME should compose.
|
||||
///
|
||||
/// Defaults to `false`. The editor overrides this based on whether it expects
|
||||
/// character input (e.g. Vim insert mode returns `true`, normal mode returns `false`).
|
||||
/// The terminal keeps the default `false` so that raw keys reach the terminal process.
|
||||
fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// The variables that can be configured when creating a new window
|
||||
@@ -1346,6 +1456,9 @@ pub struct WindowOptions {
|
||||
/// Note that this may be ignored.
|
||||
pub window_decorations: Option<WindowDecorations>,
|
||||
|
||||
/// Icon image (X11 only)
|
||||
pub icon: Option<Arc<image::RgbaImage>>,
|
||||
|
||||
/// 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<String>,
|
||||
}
|
||||
@@ -1392,6 +1505,10 @@ pub struct WindowParams {
|
||||
#[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
|
||||
pub show: bool,
|
||||
|
||||
/// An image to set as the window icon (x11 only)
|
||||
#[cfg_attr(feature = "wayland", allow(dead_code))]
|
||||
pub icon: Option<Arc<image::RgbaImage>>,
|
||||
|
||||
#[cfg_attr(feature = "wayland", allow(dead_code))]
|
||||
pub display_id: Option<DisplayId>,
|
||||
|
||||
@@ -1452,6 +1569,7 @@ impl Default for WindowOptions {
|
||||
is_minimizable: true,
|
||||
display_id: None,
|
||||
window_background: WindowBackgroundAppearance::default(),
|
||||
icon: None,
|
||||
app_id: None,
|
||||
window_min_size: None,
|
||||
window_decorations: None,
|
||||
@@ -1727,9 +1845,6 @@ pub enum CursorStyle {
|
||||
/// 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,
|
||||
}
|
||||
|
||||
/// A clipboard item that should be copied to the clipboard
|
||||
@@ -1895,6 +2010,8 @@ pub enum ImageFormat {
|
||||
Tiff,
|
||||
/// .ico
|
||||
Ico,
|
||||
/// Netpbm image formats (.pbm, .ppm, .pgm).
|
||||
Pnm,
|
||||
}
|
||||
|
||||
impl ImageFormat {
|
||||
@@ -1909,20 +2026,25 @@ impl ImageFormat {
|
||||
ImageFormat::Bmp => "image/bmp",
|
||||
ImageFormat::Tiff => "image/tiff",
|
||||
ImageFormat::Ico => "image/ico",
|
||||
ImageFormat::Pnm => "image/x-portable-anymap",
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the ImageFormat for the given mime type
|
||||
/// Returns the ImageFormat for the given mime type, including known aliases.
|
||||
pub fn from_mime_type(mime_type: &str) -> Option<Self> {
|
||||
use strum::IntoEnumIterator;
|
||||
Self::iter()
|
||||
.find(|format| format.mime_type() == mime_type)
|
||||
.or_else(|| Self::from_mime_type_alias(mime_type))
|
||||
}
|
||||
|
||||
/// Non-canonical mime types that some producers use in the wild.
|
||||
/// Unlike `mime_type()` which returns the single canonical form,
|
||||
/// these are legacy or shortened variants we still need to recognize.
|
||||
fn from_mime_type_alias(mime_type: &str) -> Option<Self> {
|
||||
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),
|
||||
"image/ico" => Some(Self::Ico),
|
||||
"image/jpg" => Some(Self::Jpeg),
|
||||
"image/tif" => Some(Self::Tiff),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -2014,12 +2136,22 @@ impl Image {
|
||||
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);
|
||||
match frame {
|
||||
Ok(mut frame) => {
|
||||
// Convert from RGBA to BGRA.
|
||||
for pixel in frame.buffer_mut().chunks_exact_mut(4) {
|
||||
pixel.swap(0, 2);
|
||||
}
|
||||
frames.push(frame);
|
||||
}
|
||||
Err(err) => {
|
||||
log::debug!("Skipping GIF frame due to decode error: {err}");
|
||||
}
|
||||
}
|
||||
frames.push(frame);
|
||||
}
|
||||
|
||||
if frames.is_empty() {
|
||||
anyhow::bail!("GIF could not be decoded: all frames failed");
|
||||
}
|
||||
|
||||
frames
|
||||
@@ -2032,9 +2164,10 @@ impl Image {
|
||||
ImageFormat::Ico => frames_for_image(&self.bytes, image::ImageFormat::Ico)?,
|
||||
ImageFormat::Svg => {
|
||||
return svg_renderer
|
||||
.render_single_frame(&self.bytes, 1.0, false)
|
||||
.render_single_frame(&self.bytes, 1.0)
|
||||
.map_err(Into::into);
|
||||
}
|
||||
ImageFormat::Pnm => frames_for_image(&self.bytes, image::ImageFormat::Pnm)?,
|
||||
};
|
||||
|
||||
Ok(Arc::new(RenderImage::new(frames)))
|
||||
@@ -2113,3 +2246,209 @@ impl From<String> for ClipboardString {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod image_tests {
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
fn test_svg_image_to_image_data_converts_to_bgra() {
|
||||
let image = Image::from_bytes(
|
||||
ImageFormat::Svg,
|
||||
br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
|
||||
<rect width="1" height="1" fill="#38BDF8"/>
|
||||
</svg>"##
|
||||
.to_vec(),
|
||||
);
|
||||
|
||||
let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
|
||||
let bytes = render_image.as_bytes(0).unwrap();
|
||||
|
||||
for pixel in bytes.chunks_exact(4) {
|
||||
assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn test_window_button_layout_parse_standard() {
|
||||
let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
|
||||
assert_eq!(
|
||||
layout.left,
|
||||
[
|
||||
Some(WindowButton::Close),
|
||||
Some(WindowButton::Minimize),
|
||||
None
|
||||
]
|
||||
);
|
||||
assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_window_button_layout_parse_right_only() {
|
||||
let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap();
|
||||
assert_eq!(layout.left, [None, None, None]);
|
||||
assert_eq!(
|
||||
layout.right,
|
||||
[
|
||||
Some(WindowButton::Minimize),
|
||||
Some(WindowButton::Maximize),
|
||||
Some(WindowButton::Close)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_window_button_layout_parse_left_only() {
|
||||
let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap();
|
||||
assert_eq!(
|
||||
layout.left,
|
||||
[
|
||||
Some(WindowButton::Close),
|
||||
Some(WindowButton::Minimize),
|
||||
Some(WindowButton::Maximize)
|
||||
]
|
||||
);
|
||||
assert_eq!(layout.right, [None, None, None]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_window_button_layout_parse_with_whitespace() {
|
||||
let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap();
|
||||
assert_eq!(
|
||||
layout.left,
|
||||
[
|
||||
Some(WindowButton::Close),
|
||||
Some(WindowButton::Minimize),
|
||||
None
|
||||
]
|
||||
);
|
||||
assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_window_button_layout_parse_empty() {
|
||||
let layout = WindowButtonLayout::parse("").unwrap();
|
||||
assert_eq!(layout.left, [None, None, None]);
|
||||
assert_eq!(layout.right, [None, None, None]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_window_button_layout_parse_intentionally_empty() {
|
||||
let layout = WindowButtonLayout::parse(":").unwrap();
|
||||
assert_eq!(layout.left, [None, None, None]);
|
||||
assert_eq!(layout.right, [None, None, None]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_window_button_layout_parse_invalid_buttons() {
|
||||
let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap();
|
||||
assert_eq!(
|
||||
layout.left,
|
||||
[
|
||||
Some(WindowButton::Close),
|
||||
Some(WindowButton::Minimize),
|
||||
None
|
||||
]
|
||||
);
|
||||
assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_window_button_layout_parse_deduplicates_same_side_buttons() {
|
||||
let layout = WindowButtonLayout::parse("close,close,minimize").unwrap();
|
||||
assert_eq!(
|
||||
layout.right,
|
||||
[
|
||||
Some(WindowButton::Close),
|
||||
Some(WindowButton::Minimize),
|
||||
None
|
||||
]
|
||||
);
|
||||
assert_eq!(layout.format(), ":close,minimize");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_window_button_layout_parse_deduplicates_buttons_across_sides() {
|
||||
let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap();
|
||||
assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
|
||||
assert_eq!(
|
||||
layout.right,
|
||||
[
|
||||
Some(WindowButton::Maximize),
|
||||
Some(WindowButton::Minimize),
|
||||
None
|
||||
]
|
||||
);
|
||||
|
||||
let button_ids: Vec<_> = layout
|
||||
.left
|
||||
.iter()
|
||||
.chain(layout.right.iter())
|
||||
.flatten()
|
||||
.map(WindowButton::id)
|
||||
.collect();
|
||||
let unique_button_ids = button_ids.iter().copied().collect::<HashSet<_>>();
|
||||
assert_eq!(unique_button_ids.len(), button_ids.len());
|
||||
assert_eq!(layout.format(), "close:maximize,minimize");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_window_button_layout_parse_gnome_style() {
|
||||
let layout = WindowButtonLayout::parse("close").unwrap();
|
||||
assert_eq!(layout.left, [None, None, None]);
|
||||
assert_eq!(layout.right, [Some(WindowButton::Close), None, None]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_window_button_layout_parse_elementary_style() {
|
||||
let layout = WindowButtonLayout::parse("close:maximize").unwrap();
|
||||
assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
|
||||
assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_window_button_layout_round_trip() {
|
||||
let cases = [
|
||||
"close:minimize,maximize",
|
||||
"minimize,maximize,close:",
|
||||
":close",
|
||||
"close:",
|
||||
"close:maximize",
|
||||
":",
|
||||
];
|
||||
|
||||
for case in cases {
|
||||
let layout = WindowButtonLayout::parse(case).unwrap();
|
||||
assert_eq!(layout.format(), case, "Round-trip failed for: {}", case);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_window_button_layout_linux_default() {
|
||||
let layout = WindowButtonLayout::linux_default();
|
||||
assert_eq!(layout.left, [None, None, None]);
|
||||
assert_eq!(
|
||||
layout.right,
|
||||
[
|
||||
Some(WindowButton::Minimize),
|
||||
Some(WindowButton::Maximize),
|
||||
Some(WindowButton::Close)
|
||||
]
|
||||
);
|
||||
|
||||
let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap();
|
||||
assert_eq!(round_tripped, layout);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_window_button_layout_parse_all_invalid() {
|
||||
assert!(WindowButtonLayout::parse("asdfghjkl").is_err());
|
||||
}
|
||||
}
|
||||
@@ -126,7 +126,7 @@ fn start_default_target_screen_capture(
|
||||
) {
|
||||
// Due to use of blocking APIs, a dedicated thread is used.
|
||||
std::thread::spawn(|| {
|
||||
let start_result = util::maybe!({
|
||||
let start_result = gpui_util::maybe!({
|
||||
let mut capturer = new_scap_capturer(None)?;
|
||||
capturer.start_capture();
|
||||
let first_frame = capturer
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::scheduler::Instant;
|
||||
use crate::scheduler::{Clock, Scheduler, SessionId, TestScheduler, TestSchedulerConfig, Yield};
|
||||
use crate::{PlatformDispatcher, Priority, RunnableVariant};
|
||||
use scheduler::Instant;
|
||||
use scheduler::{Clock, Scheduler, SessionId, TestScheduler, TestSchedulerConfig, Yield};
|
||||
use std::{
|
||||
sync::{
|
||||
Arc,
|
||||
@@ -139,8 +139,7 @@ impl PlatformDispatcher for TestDispatcher {
|
||||
}
|
||||
|
||||
fn dispatch_on_main_thread(&self, runnable: RunnableVariant, _priority: Priority) {
|
||||
self.scheduler
|
||||
.schedule_foreground(self.session_id, runnable);
|
||||
self.scheduler.schedule_local(self.session_id, runnable);
|
||||
}
|
||||
|
||||
fn dispatch_after(&self, _duration: Duration, _runnable: RunnableVariant) {
|
||||