Vendor gpui-pre 0.3.8 unmodified (crates.io source)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
ba5c0798cb
commit
48a66ca6c1
162 files changed
+104826
-2
No files matched your search
Generated
-2
@@ -2966,8 +2966,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui-pre"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7f0bc3ebba070f8981b3f8e905a12f47d27794a30d029e59cdb9edf7a4b3360"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"anyhow",
|
||||
|
||||
@@ -67,4 +67,5 @@ ely-ui = { path = "crates/ely-ui" }
|
||||
all = { level = "warn", priority = -1 }
|
||||
|
||||
[patch.crates-io]
|
||||
gpui-pre = { path = "vendor/gpui-pre" }
|
||||
gpui-pre-windows = { path = "vendor/gpui-pre-windows" }
|
||||
+5167
File diff suppressed because it is too large.
Load diff
Vendored
+683
@@ -0,0 +1,683 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2024"
|
||||
name = "gpui-pre"
|
||||
version = "0.3.8"
|
||||
authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
build = "build.rs"
|
||||
publish = true
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "Zed's GPU-accelerated UI framework (gpui-pre snapshot of zed@279fe07)"
|
||||
homepage = "https://gpui.rs"
|
||||
readme = "README.md"
|
||||
keywords = [
|
||||
"desktop",
|
||||
"gui",
|
||||
"immediate",
|
||||
]
|
||||
categories = ["gui"]
|
||||
license = "Apache-2.0"
|
||||
repository = "https://github.com/zed-industries/zed"
|
||||
resolver = "2"
|
||||
|
||||
[package.metadata.cargo-shear]
|
||||
ignored = [
|
||||
"bindgen",
|
||||
"font-kit",
|
||||
"getrandom",
|
||||
"objc2",
|
||||
"objc2-metal",
|
||||
]
|
||||
ignored-paths = ["src/_ownership_and_data_flow.rs"]
|
||||
|
||||
[package.metadata.gpui-pre]
|
||||
zed-crate = "gpui"
|
||||
zed-version = "0.2.2"
|
||||
zed-rev = "279fe070bb389b79652e52065b2f001edcc0b11b"
|
||||
|
||||
[features]
|
||||
bench = ["bench-support"]
|
||||
bench-support = [
|
||||
"profiler",
|
||||
"dep:criterion",
|
||||
"dep:bench_metrics",
|
||||
]
|
||||
default = [
|
||||
"font-kit",
|
||||
"wayland",
|
||||
"x11",
|
||||
"windows-manifest",
|
||||
]
|
||||
inspector = ["gpui_macros/inspector"]
|
||||
leak-detection = []
|
||||
profiler = ["dep:hdrhistogram"]
|
||||
screen-capture = ["scap"]
|
||||
stacker = ["dep:stacksafe"]
|
||||
test-support = [
|
||||
"collections/test-support",
|
||||
"http_client/test-support",
|
||||
"wayland",
|
||||
"x11",
|
||||
"proptest",
|
||||
]
|
||||
wayland = []
|
||||
windows-manifest = ["dep:embed-resource"]
|
||||
x11 = ["scap?/x11"]
|
||||
|
||||
[lib]
|
||||
name = "gpui"
|
||||
path = "src/gpui.rs"
|
||||
|
||||
[[example]]
|
||||
name = "a11y"
|
||||
path = "examples/a11y.rs"
|
||||
|
||||
[[example]]
|
||||
name = "active_state_bug"
|
||||
path = "examples/active_state_bug.rs"
|
||||
|
||||
[[example]]
|
||||
name = "anchor"
|
||||
path = "examples/anchor.rs"
|
||||
|
||||
[[example]]
|
||||
name = "animation"
|
||||
path = "examples/animation.rs"
|
||||
|
||||
[[example]]
|
||||
name = "data_table"
|
||||
path = "examples/data_table.rs"
|
||||
|
||||
[[example]]
|
||||
name = "drag_drop"
|
||||
path = "examples/drag_drop.rs"
|
||||
|
||||
[[example]]
|
||||
name = "focus_visible"
|
||||
path = "examples/focus_visible.rs"
|
||||
|
||||
[[example]]
|
||||
name = "gif_viewer"
|
||||
path = "examples/gif_viewer.rs"
|
||||
|
||||
[[example]]
|
||||
name = "gradient"
|
||||
path = "examples/gradient.rs"
|
||||
|
||||
[[example]]
|
||||
name = "grid_layout"
|
||||
path = "examples/grid_layout.rs"
|
||||
|
||||
[[example]]
|
||||
name = "hello_world"
|
||||
path = "examples/hello_world.rs"
|
||||
|
||||
[[example]]
|
||||
name = "image"
|
||||
path = "examples/image/image.rs"
|
||||
|
||||
[[example]]
|
||||
name = "image_gallery"
|
||||
path = "examples/image_gallery.rs"
|
||||
|
||||
[[example]]
|
||||
name = "image_loading"
|
||||
path = "examples/image_loading.rs"
|
||||
|
||||
[[example]]
|
||||
name = "input"
|
||||
path = "examples/input.rs"
|
||||
|
||||
[[example]]
|
||||
name = "layer_shell"
|
||||
path = "examples/layer_shell.rs"
|
||||
|
||||
[[example]]
|
||||
name = "list_example"
|
||||
path = "examples/list_example.rs"
|
||||
|
||||
[[example]]
|
||||
name = "mouse_pressure"
|
||||
path = "examples/mouse_pressure.rs"
|
||||
|
||||
[[example]]
|
||||
name = "move_entity_between_windows"
|
||||
path = "examples/move_entity_between_windows.rs"
|
||||
|
||||
[[example]]
|
||||
name = "on_window_close_quit"
|
||||
path = "examples/on_window_close_quit.rs"
|
||||
|
||||
[[example]]
|
||||
name = "opacity"
|
||||
path = "examples/opacity.rs"
|
||||
|
||||
[[example]]
|
||||
name = "ownership_post"
|
||||
path = "examples/ownership_post.rs"
|
||||
|
||||
[[example]]
|
||||
name = "painting"
|
||||
path = "examples/painting.rs"
|
||||
|
||||
[[example]]
|
||||
name = "paths_bench"
|
||||
path = "examples/paths_bench.rs"
|
||||
|
||||
[[example]]
|
||||
name = "pattern"
|
||||
path = "examples/pattern.rs"
|
||||
|
||||
[[example]]
|
||||
name = "popover"
|
||||
path = "examples/popover.rs"
|
||||
|
||||
[[example]]
|
||||
name = "scrollable"
|
||||
path = "examples/scrollable.rs"
|
||||
|
||||
[[example]]
|
||||
name = "set_menus"
|
||||
path = "examples/set_menus.rs"
|
||||
|
||||
[[example]]
|
||||
name = "shadow"
|
||||
path = "examples/shadow.rs"
|
||||
|
||||
[[example]]
|
||||
name = "svg"
|
||||
path = "examples/svg/svg.rs"
|
||||
|
||||
[[example]]
|
||||
name = "system_notifications"
|
||||
path = "examples/system_notifications.rs"
|
||||
|
||||
[[example]]
|
||||
name = "tab_stop"
|
||||
path = "examples/tab_stop.rs"
|
||||
|
||||
[[example]]
|
||||
name = "testing"
|
||||
path = "examples/testing.rs"
|
||||
|
||||
[[example]]
|
||||
name = "text"
|
||||
path = "examples/text.rs"
|
||||
|
||||
[[example]]
|
||||
name = "text_layout"
|
||||
path = "examples/text_layout.rs"
|
||||
|
||||
[[example]]
|
||||
name = "text_wrapper"
|
||||
path = "examples/text_wrapper.rs"
|
||||
|
||||
[[example]]
|
||||
name = "toggle_headless"
|
||||
path = "examples/toggle_headless.rs"
|
||||
|
||||
[[example]]
|
||||
name = "tree"
|
||||
path = "examples/tree.rs"
|
||||
|
||||
[[example]]
|
||||
name = "uniform_list"
|
||||
path = "examples/uniform_list.rs"
|
||||
|
||||
[[example]]
|
||||
name = "view_example"
|
||||
path = "examples/view_example/view_example_main.rs"
|
||||
|
||||
[[example]]
|
||||
name = "window"
|
||||
path = "examples/window.rs"
|
||||
|
||||
[[example]]
|
||||
name = "window_movable"
|
||||
path = "examples/window_movable.rs"
|
||||
|
||||
[[example]]
|
||||
name = "window_positioning"
|
||||
path = "examples/window_positioning.rs"
|
||||
|
||||
[[example]]
|
||||
name = "window_shadow"
|
||||
path = "examples/window_shadow.rs"
|
||||
|
||||
[[test]]
|
||||
name = "action_macros"
|
||||
path = "tests/action_macros.rs"
|
||||
|
||||
[dependencies.accesskit]
|
||||
version = "0.24.0"
|
||||
features = ["enumn"]
|
||||
|
||||
[dependencies.anyhow]
|
||||
version = "1.0.86"
|
||||
|
||||
[dependencies.async-task]
|
||||
version = "4.7"
|
||||
|
||||
[dependencies.backtrace]
|
||||
version = "0.3"
|
||||
|
||||
[dependencies.bench_metrics]
|
||||
version = "=0.3.8"
|
||||
optional = true
|
||||
package = "gpui-pre-bench-metrics"
|
||||
|
||||
[dependencies.bitflags]
|
||||
version = "2.6.0"
|
||||
|
||||
[dependencies.chrono]
|
||||
version = "0.4"
|
||||
features = ["serde"]
|
||||
|
||||
[dependencies.collections]
|
||||
version = "=0.3.8"
|
||||
package = "gpui-pre-collections"
|
||||
|
||||
[dependencies.criterion]
|
||||
version = "0.8.2"
|
||||
features = ["html_reports"]
|
||||
optional = true
|
||||
|
||||
[dependencies.ctor]
|
||||
version = "1.0.12"
|
||||
|
||||
[dependencies.derive_more]
|
||||
version = "2.1.1"
|
||||
features = [
|
||||
"add",
|
||||
"add_assign",
|
||||
"deref",
|
||||
"deref_mut",
|
||||
"display",
|
||||
"from",
|
||||
"from_str",
|
||||
"mul",
|
||||
"mul_assign",
|
||||
"not",
|
||||
]
|
||||
|
||||
[dependencies.etagere]
|
||||
version = "0.2"
|
||||
|
||||
[dependencies.futures]
|
||||
version = "0.3.34"
|
||||
|
||||
[dependencies.futures-concurrency]
|
||||
version = "7.7.1"
|
||||
|
||||
[dependencies.gpui_macros]
|
||||
version = "=0.3.8"
|
||||
package = "gpui-pre-macros"
|
||||
|
||||
[dependencies.gpui_shared_string]
|
||||
version = "=0.3.8"
|
||||
package = "gpui-pre-shared-string"
|
||||
|
||||
[dependencies.gpui_util]
|
||||
version = "=0.3.8"
|
||||
package = "gpui-pre-util"
|
||||
|
||||
[dependencies.hdrhistogram]
|
||||
version = "7"
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.heapless]
|
||||
version = "0.9.2"
|
||||
|
||||
[dependencies.http_client]
|
||||
version = "=0.3.8"
|
||||
package = "gpui-pre-http-client"
|
||||
|
||||
[dependencies.image]
|
||||
version = "0.25.1"
|
||||
features = [
|
||||
"bmp",
|
||||
"dds",
|
||||
"exr",
|
||||
"ff",
|
||||
"gif",
|
||||
"hdr",
|
||||
"ico",
|
||||
"jpeg",
|
||||
"png",
|
||||
"pnm",
|
||||
"qoi",
|
||||
"rayon",
|
||||
"tga",
|
||||
"tiff",
|
||||
"webp",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[dependencies.inventory]
|
||||
version = "0.3.19"
|
||||
|
||||
[dependencies.itertools]
|
||||
version = "0.14.0"
|
||||
|
||||
[dependencies.log]
|
||||
version = "0.4.16"
|
||||
features = [
|
||||
"kv_unstable_serde",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[dependencies.lyon]
|
||||
version = "1.0"
|
||||
|
||||
[dependencies.num_cpus]
|
||||
version = "1.13"
|
||||
|
||||
[dependencies.parking]
|
||||
version = "2.0.0"
|
||||
|
||||
[dependencies.parking_lot]
|
||||
version = "0.12.1"
|
||||
|
||||
[dependencies.pin-project]
|
||||
version = "1.1.10"
|
||||
|
||||
[dependencies.pollster]
|
||||
version = "0.4.0"
|
||||
|
||||
[dependencies.postage]
|
||||
version = "0.5"
|
||||
features = ["futures-traits"]
|
||||
|
||||
[dependencies.profiling]
|
||||
version = "1"
|
||||
|
||||
[dependencies.proptest]
|
||||
version = "1"
|
||||
features = ["attr-macro"]
|
||||
optional = true
|
||||
package = "proptest"
|
||||
|
||||
[dependencies.rand]
|
||||
version = "0.9"
|
||||
|
||||
[dependencies.raw-window-handle]
|
||||
version = "0.6"
|
||||
|
||||
[dependencies.refineable]
|
||||
version = "=0.3.8"
|
||||
package = "gpui-pre-refineable"
|
||||
|
||||
[dependencies.regex]
|
||||
version = "1.5"
|
||||
|
||||
[dependencies.resvg]
|
||||
version = "0.46.0"
|
||||
features = [
|
||||
"text",
|
||||
"system-fonts",
|
||||
"memmap-fonts",
|
||||
"raster-images",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[dependencies.scheduler]
|
||||
version = "=0.3.8"
|
||||
package = "gpui-pre-scheduler"
|
||||
|
||||
[dependencies.schemars]
|
||||
version = "1.0"
|
||||
features = ["indexmap2"]
|
||||
|
||||
[dependencies.seahash]
|
||||
version = "4.1"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0.221"
|
||||
features = [
|
||||
"derive",
|
||||
"rc",
|
||||
]
|
||||
|
||||
[dependencies.serde_json]
|
||||
version = "1.0.144"
|
||||
features = [
|
||||
"preserve_order",
|
||||
"raw_value",
|
||||
]
|
||||
|
||||
[dependencies.slotmap]
|
||||
version = "1.0.6"
|
||||
|
||||
[dependencies.smallvec]
|
||||
version = "1.6"
|
||||
features = [
|
||||
"union",
|
||||
"const_new",
|
||||
]
|
||||
|
||||
[dependencies.spin]
|
||||
version = "0.10.0"
|
||||
|
||||
[dependencies.stacksafe]
|
||||
version = "1.0"
|
||||
optional = true
|
||||
|
||||
[dependencies.strum]
|
||||
version = "0.28"
|
||||
features = ["derive"]
|
||||
|
||||
[dependencies.sum_tree]
|
||||
version = "=0.3.8"
|
||||
package = "gpui-pre-sum-tree"
|
||||
|
||||
[dependencies.taffy]
|
||||
version = "0.13.0"
|
||||
|
||||
[dependencies.thiserror]
|
||||
version = "2.0.12"
|
||||
|
||||
[dependencies.ttf-parser]
|
||||
version = "0.25"
|
||||
|
||||
[dependencies.url]
|
||||
version = "2.2"
|
||||
|
||||
[dependencies.usvg]
|
||||
version = "0.46.0"
|
||||
default-features = false
|
||||
|
||||
[dependencies.util_macros]
|
||||
version = "=0.3.8"
|
||||
package = "gpui-pre-util-macros"
|
||||
|
||||
[dependencies.uuid]
|
||||
version = "1.1.2"
|
||||
features = [
|
||||
"v4",
|
||||
"v5",
|
||||
"v7",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[dependencies.waker-fn]
|
||||
version = "1.2.0"
|
||||
|
||||
[dependencies.web-time]
|
||||
version = "1.1.0"
|
||||
|
||||
[build-dependencies.embed-resource]
|
||||
version = "3.0"
|
||||
optional = true
|
||||
|
||||
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies.async-channel]
|
||||
version = "2.5.0"
|
||||
default-features = false
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies.libc]
|
||||
version = "0.2"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))'.dependencies.scap]
|
||||
version = "0.0.8-zed"
|
||||
optional = true
|
||||
default-features = false
|
||||
package = "zed-scap"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies.core-video]
|
||||
version = "0.5.2"
|
||||
features = ["metal"]
|
||||
|
||||
[target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies.async-channel]
|
||||
version = "2.5.0"
|
||||
|
||||
[target.'cfg(target_family = "wasm")'.dependencies.getrandom]
|
||||
version = "0.3.4"
|
||||
features = ["wasm_js"]
|
||||
|
||||
[target.'cfg(target_family = "wasm")'.dependencies.uuid]
|
||||
version = "1.1.2"
|
||||
features = [
|
||||
"v4",
|
||||
"v5",
|
||||
"v7",
|
||||
"serde",
|
||||
"js",
|
||||
]
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies.font-kit]
|
||||
version = "0.14.1-zed"
|
||||
optional = true
|
||||
package = "zed-font-kit"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies.log]
|
||||
version = "0.4.16"
|
||||
features = [
|
||||
"kv_unstable_serde",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies.objc2]
|
||||
version = "0.6"
|
||||
optional = true
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies.objc2-core-foundation]
|
||||
version = "0.3.2"
|
||||
default-features = false
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies.objc2-core-video]
|
||||
version = "0.3.2"
|
||||
features = [
|
||||
"CVBuffer",
|
||||
"CVImageBuffer",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies.objc2-metal]
|
||||
version = "0.3"
|
||||
optional = true
|
||||
|
||||
[target.'cfg(target_os = "macos")'.build-dependencies.bindgen]
|
||||
version = "0.72"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies.windows]
|
||||
version = "0.62"
|
||||
features = [
|
||||
"Data_Xml_Dom",
|
||||
"Foundation_Numerics",
|
||||
"Globalization_DateTimeFormatting",
|
||||
"Storage_Search",
|
||||
"Storage_Streams",
|
||||
"System_Threading",
|
||||
"UI_Notifications",
|
||||
"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",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_Graphics_Imaging",
|
||||
"Win32_Graphics_Hlsl",
|
||||
"Win32_Networking_WinSock",
|
||||
"Win32_Security",
|
||||
"Win32_Security_Credentials",
|
||||
"Win32_Security_Cryptography",
|
||||
"Win32_Storage_FileSystem",
|
||||
"Win32_Storage_Packaging_Appx",
|
||||
"Win32_System_Com",
|
||||
"Win32_System_Com_StructuredStorage",
|
||||
"Win32_System_Console",
|
||||
"Win32_System_Diagnostics_Debug",
|
||||
"Win32_System_DataExchange",
|
||||
"Win32_System_IO",
|
||||
"Win32_System_JobObjects",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Memory",
|
||||
"Win32_System_Ole",
|
||||
"Win32_System_Performance",
|
||||
"Win32_System_Pipes",
|
||||
"Win32_System_Power",
|
||||
"Win32_System_RestartManager",
|
||||
"Win32_System_SystemInformation",
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_Variant",
|
||||
"Win32_System_WinRT",
|
||||
"Win32_UI_Controls",
|
||||
"Win32_UI_HiDpi",
|
||||
"Win32_UI_Input_Ime",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_UI_Input_Pointer",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_UI_Shell_Common",
|
||||
"Win32_UI_Shell_PropertiesSystem",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_Media",
|
||||
"Win32_Foundation",
|
||||
"Win32_System_Power",
|
||||
"Win32_System_RemoteDesktop",
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_WindowsProgramming",
|
||||
]
|
||||
|
||||
[lints.clippy]
|
||||
dbg_macro = "deny"
|
||||
declare_interior_mutable_const = "deny"
|
||||
disallowed_methods = "deny"
|
||||
large_enum_variant = "allow"
|
||||
let_underscore_future = "allow"
|
||||
nonminimal_bool = "allow"
|
||||
redundant_clone = "deny"
|
||||
single_range_in_vec_init = "allow"
|
||||
todo = "deny"
|
||||
too_many_arguments = "allow"
|
||||
type_complexity = "allow"
|
||||
|
||||
[lints.clippy.style]
|
||||
level = "allow"
|
||||
priority = -1
|
||||
|
||||
[lints.rust.unexpected_cfgs]
|
||||
level = "allow"
|
||||
priority = 0
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
[package]
|
||||
name = "gpui-pre"
|
||||
version = "0.3.8"
|
||||
edition = { workspace = true }
|
||||
authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
description = "Zed's GPU-accelerated UI framework (gpui-pre snapshot of zed@279fe07)"
|
||||
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"]
|
||||
|
||||
[package.metadata.cargo-shear]
|
||||
ignored = ["bindgen", "font-kit", "getrandom", "objc2", "objc2-metal"]
|
||||
ignored-paths = ["src/_ownership_and_data_flow.rs"]
|
||||
|
||||
[package.metadata.gpui-pre]
|
||||
zed-crate = "gpui"
|
||||
zed-version = "0.2.2"
|
||||
zed-rev = "279fe070bb389b79652e52065b2f001edcc0b11b"
|
||||
|
||||
[lib]
|
||||
path = "src/gpui.rs"
|
||||
name = "gpui"
|
||||
|
||||
[features]
|
||||
default = ["font-kit", "wayland", "x11", "windows-manifest"]
|
||||
test-support = [
|
||||
"collections/test-support",
|
||||
"http_client/test-support",
|
||||
"wayland",
|
||||
"x11",
|
||||
"proptest",
|
||||
]
|
||||
bench-support = ["profiler", "dep:criterion", "dep:bench_metrics"]
|
||||
bench = ["bench-support"]
|
||||
inspector = ["gpui_macros/inspector"]
|
||||
leak-detection = []
|
||||
wayland = []
|
||||
x11 = ["scap?/x11"]
|
||||
screen-capture = ["scap"]
|
||||
windows-manifest = ["dep:embed-resource"]
|
||||
profiler = ["dep:hdrhistogram"]
|
||||
stacker = ["dep:stacksafe"]
|
||||
|
||||
[dependencies]
|
||||
accesskit = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
async-task = { workspace = true }
|
||||
backtrace = { workspace = true }
|
||||
bitflags = { workspace = true }
|
||||
collections = { workspace = true }
|
||||
bench_metrics = { workspace = true, optional = true }
|
||||
criterion = { workspace = true, optional = true }
|
||||
ctor = { workspace = true }
|
||||
derive_more = { workspace = true }
|
||||
etagere = { workspace = true }
|
||||
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 = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
refineable = { workspace = true }
|
||||
scheduler = { workspace = true }
|
||||
resvg = { workspace = true }
|
||||
usvg = { workspace = true }
|
||||
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 }
|
||||
stacksafe = { workspace = true, optional = true }
|
||||
strum = { workspace = true }
|
||||
sum_tree = { workspace = true }
|
||||
taffy = "0.13.0"
|
||||
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 }
|
||||
heapless = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
embed-resource = { version = "3.0", optional = true }
|
||||
|
||||
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
|
||||
async-channel = { version = "2.5.0", default-features = false }
|
||||
|
||||
[target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies]
|
||||
async-channel = { workspace = true }
|
||||
|
||||
[target.'cfg(target_family = "wasm")'.dependencies]
|
||||
getrandom = { version = "0.3.4", features = ["wasm_js"] }
|
||||
uuid = { workspace = true, features = ["js"] }
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
|
||||
core-video = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc2-core-foundation = { workspace = true }
|
||||
objc2-core-video = { workspace = true }
|
||||
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "94b0f28166665e8fd2f53ff6d268a14955c82269", package = "zed-font-kit", version = "0.14.1-zed", optional = true }
|
||||
log = { workspace = true }
|
||||
objc2 = { workspace = true, optional = true }
|
||||
objc2-metal = { version = "0.3", optional = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.build-dependencies]
|
||||
bindgen = "0.72"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))'.dependencies]
|
||||
scap = { workspace = true, optional = true }
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
|
||||
libc = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = { workspace = true, features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_System_Power",
|
||||
"Win32_System_RemoteDesktop",
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_WindowsProgramming",
|
||||
] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[[example]]
|
||||
name = "hello_world"
|
||||
path = "examples/hello_world.rs"
|
||||
|
||||
[[example]]
|
||||
name = "move_entity_between_windows"
|
||||
path = "examples/move_entity_between_windows.rs"
|
||||
|
||||
[[example]]
|
||||
name = "image"
|
||||
path = "examples/image/image.rs"
|
||||
|
||||
[[example]]
|
||||
name = "input"
|
||||
path = "examples/input.rs"
|
||||
|
||||
[[example]]
|
||||
name = "on_window_close_quit"
|
||||
path = "examples/on_window_close_quit.rs"
|
||||
|
||||
[[example]]
|
||||
name = "window_movable"
|
||||
path = "examples/window_movable.rs"
|
||||
|
||||
[[example]]
|
||||
name = "opacity"
|
||||
path = "examples/opacity.rs"
|
||||
|
||||
[[example]]
|
||||
name = "pattern"
|
||||
path = "examples/pattern.rs"
|
||||
|
||||
[[example]]
|
||||
name = "set_menus"
|
||||
path = "examples/set_menus.rs"
|
||||
|
||||
[[example]]
|
||||
name = "system_notifications"
|
||||
path = "examples/system_notifications.rs"
|
||||
|
||||
[[example]]
|
||||
name = "shadow"
|
||||
path = "examples/shadow.rs"
|
||||
|
||||
[[example]]
|
||||
name = "svg"
|
||||
path = "examples/svg/svg.rs"
|
||||
|
||||
[[example]]
|
||||
name = "tab_stop"
|
||||
path = "examples/tab_stop.rs"
|
||||
|
||||
[[example]]
|
||||
name = "text"
|
||||
path = "examples/text.rs"
|
||||
|
||||
[[example]]
|
||||
name = "text_wrapper"
|
||||
path = "examples/text_wrapper.rs"
|
||||
|
||||
[[example]]
|
||||
name = "tree"
|
||||
path = "examples/tree.rs"
|
||||
|
||||
[[example]]
|
||||
name = "uniform_list"
|
||||
path = "examples/uniform_list.rs"
|
||||
|
||||
[[example]]
|
||||
name = "window_shadow"
|
||||
path = "examples/window_shadow.rs"
|
||||
|
||||
[[example]]
|
||||
name = "grid_layout"
|
||||
path = "examples/grid_layout.rs"
|
||||
|
||||
[[example]]
|
||||
name = "list_example"
|
||||
path = "examples/list_example.rs"
|
||||
|
||||
[[example]]
|
||||
name = "mouse_pressure"
|
||||
path = "examples/mouse_pressure.rs"
|
||||
|
||||
[[example]]
|
||||
name = "a11y"
|
||||
path = "examples/a11y.rs"
|
||||
|
||||
[[example]]
|
||||
name = "view_example"
|
||||
path = "examples/view_example/view_example_main.rs"
|
||||
|
||||
[[example]]
|
||||
name = "toggle_headless"
|
||||
path = "examples/toggle_headless.rs"
|
||||
Vendored
+222
@@ -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
|
||||
Vendored
+98
@@ -0,0 +1,98 @@
|
||||
# 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. Add `gpui`, and optionally `gpui_platform`, to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
gpui = { version = "*" }
|
||||
gpui_platform = { version = "*", features = ["font-kit", "wayland", "x11"] }
|
||||
```
|
||||
|
||||
Everything in a standalone GPUI app starts with an `Application`. You can create one with `gpui_platform::application()`, which picks the windowing and text backends for the host OS, 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.
|
||||
|
||||
```rust,no_run
|
||||
use gpui::*;
|
||||
|
||||
fn main() {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
// ..
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### `gpui_platform`
|
||||
|
||||
The features on `gpui_platform` are platform-specific, so the list above is a safe cross-platform default. If you build for a single platform, you can trim it:
|
||||
|
||||
- **macOS** — Rendering uses Metal and is always available, but glyph rasterization needs `font-kit`. Without it, GPUI falls back to a placeholder text system that lays text out but renders no glyphs.
|
||||
|
||||
```toml
|
||||
gpui_platform = { version = "*", features = ["font-kit"] }
|
||||
```
|
||||
|
||||
- **Linux / FreeBSD** — enable at least one windowing backend for desktop windows: `wayland`, `x11`, or both. These features also compile the renderer and text system, so no separate text feature is needed.
|
||||
|
||||
```toml
|
||||
gpui_platform = { version = "*", features = ["wayland", "x11"] }
|
||||
```
|
||||
|
||||
- **Windows** — no features are required. Windowing uses Win32 and text uses DirectWrite. `font-kit` has no effect here.
|
||||
|
||||
### Additional Topics
|
||||
|
||||
- [Ownership and data flow](_ownership_and_data_flow)
|
||||
- [Accessibility](_accessibility)
|
||||
|
||||
### 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).
|
||||
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
#![allow(clippy::disallowed_methods, reason = "build scripts are exempt")]
|
||||
|
||||
fn main() {
|
||||
println!("cargo::rustc-check-cfg=cfg(gles)");
|
||||
|
||||
// Leak detection does bookkeeping on every entity handle, so `test-support`
|
||||
// does not enable it: that feature must stay cheap enough to compile into
|
||||
// every build. CI sets `GPUI_LEAK_DETECTION`; the `leak-detection` feature
|
||||
// turns it on explicitly.
|
||||
println!("cargo::rustc-check-cfg=cfg(gpui_leak_detection)");
|
||||
println!("cargo::rerun-if-env-changed=GPUI_LEAK_DETECTION");
|
||||
let requested_by_environment =
|
||||
std::env::var_os("GPUI_LEAK_DETECTION").is_some_and(|value| !value.is_empty());
|
||||
if requested_by_environment || std::env::var_os("CARGO_FEATURE_LEAK_DETECTION").is_some() {
|
||||
println!("cargo::rustc-cfg=gpui_leak_detection");
|
||||
}
|
||||
|
||||
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 resource_dir = std::path::Path::new("resources/windows");
|
||||
let manifest = resource_dir.join("gpui.manifest.xml");
|
||||
let rc_file = resource_dir.join("gpui.rc");
|
||||
println!("cargo:rerun-if-changed={}", manifest.display());
|
||||
println!("cargo:rerun-if-changed={}", rc_file.display());
|
||||
embed_resource::compile(rc_file, embed_resource::ParamsIncludeDirs([resource_dir]))
|
||||
.manifest_required()
|
||||
.unwrap();
|
||||
}
|
||||
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
# Contexts
|
||||
|
||||
GPUI makes extensive use of _context parameters_ (typically named `cx`) to provide access to application state and services. These contexts are references passed to functions, enabling interaction with global state, windows, entities, and system services.
|
||||
|
||||
---
|
||||
|
||||
## `App`
|
||||
|
||||
The root context granting access to the application's global state. This context owns all entities' data and can be used to read or update the data referenced by an `Entity<T>`.
|
||||
|
||||
## `Context<T>`
|
||||
|
||||
A context provided when interacting with an `Entity<T>`, with additional methods related to that specific entity such as notifying observers and emitting events. This context dereferences into `App`, meaning any function which can take an `App` reference can also take a `Context<T>` reference, allowing you to access the application's global state.
|
||||
|
||||
## `AsyncApp` and `AsyncWindowContext`
|
||||
|
||||
Whereas the above contexts are always passed to your code as references, you can call `to_async` on the reference to create an async context, which has a static lifetime and can be held across `await` points in async code. When you interact with entities with an async context, the calls become fallible, because the context may outlive the window or even the app itself.
|
||||
|
||||
## `TestAppContext`
|
||||
|
||||
These are similar to the async contexts above, but they panic if you attempt to access a non-existent app or window, and they also contain other features specific to tests.
|
||||
|
||||
---
|
||||
|
||||
# Non-Context Core Types
|
||||
|
||||
## `Window`
|
||||
|
||||
Provides access to the state of an application window. This type has a root view (an `Entity` implementing `Render`) which it can read/update, but since it is not a context, you must pass a `&mut App` (or a context which dereferences to it) to do so, along with other functions interacting with global state. You can obtain a `Window` from an `WindowHandle` by calling `WindowHandle::update`.
|
||||
|
||||
## `Entity<T>`
|
||||
|
||||
A handle to a structure requiring state. This data is owned by the `App` and can be accessed and modified via references to contexts. If `T` implements `Render`, then the entity is sometimes referred to as a view. Entities can be observed by other entities and windows, allowing a closure to be called when `notify` is called on the entity's `Context`.
|
||||
Vendored
+100
@@ -0,0 +1,100 @@
|
||||
# Key Dispatch
|
||||
|
||||
GPUI is designed for keyboard-first interactivity.
|
||||
|
||||
To expose functionality to the mouse, you render a button with a click handler.
|
||||
|
||||
To expose functionality to the keyboard, you bind an _action_ in a _key context_.
|
||||
|
||||
Actions are similar to framework-level events like `MouseDown`, `KeyDown`, etc, but you can define them yourself:
|
||||
|
||||
```rust
|
||||
mod menu {
|
||||
#[gpui::action]
|
||||
struct MoveUp;
|
||||
|
||||
#[gpui::action]
|
||||
struct MoveDown;
|
||||
}
|
||||
```
|
||||
|
||||
Actions are frequently unit structs, for which we have a macro. The above could also be written:
|
||||
|
||||
```rust
|
||||
mod menu {
|
||||
actions!(gpui, [MoveUp, MoveDown]);
|
||||
}
|
||||
```
|
||||
|
||||
Actions can also be more complex types:
|
||||
|
||||
```rust
|
||||
mod menu {
|
||||
#[gpui::action]
|
||||
struct Move {
|
||||
direction: Direction,
|
||||
select: bool,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To bind actions, chain `on_action` on to your element:
|
||||
|
||||
```rust
|
||||
impl Render for Menu {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.on_action(|this: &mut Menu, move: &MoveUp, window: &mut Window, cx: &mut Context<Menu>| {
|
||||
// ...
|
||||
})
|
||||
.on_action(|this, move: &MoveDown, cx| {
|
||||
// ...
|
||||
})
|
||||
.children(unimplemented!())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In order to bind keys to actions, you need to declare a _key context_ for part of the element tree by calling `key_context`.
|
||||
|
||||
```rust
|
||||
impl Render for Menu {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.key_context("menu")
|
||||
.on_action(|this: &mut Menu, move: &MoveUp, window: &mut Window, cx: &mut Context<Menu>| {
|
||||
// ...
|
||||
})
|
||||
.on_action(|this, move: &MoveDown, cx| {
|
||||
// ...
|
||||
})
|
||||
.children(unimplemented!())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now you can target your context in the keymap. Note how actions are identified in the keymap by their fully-qualified type name.
|
||||
|
||||
```json
|
||||
{
|
||||
"context": "menu",
|
||||
"bindings": {
|
||||
"up": "menu::MoveUp",
|
||||
"down": "menu::MoveDown"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you had opted for the more complex type definition, you'd provide the serialized representation of the action alongside the name:
|
||||
|
||||
```json
|
||||
{
|
||||
"context": "menu",
|
||||
"bindings": {
|
||||
"up": ["menu::Move", {direction: "up", select: false}]
|
||||
"down": ["menu::Move", {direction: "down", select: false}]
|
||||
"shift-up": ["menu::Move", {direction: "up", select: true}]
|
||||
"shift-down": ["menu::Move", {direction: "down", select: true}]
|
||||
}
|
||||
}
|
||||
```
|
||||
Vendored
+157
@@ -0,0 +1,157 @@
|
||||
# GPUI Examples
|
||||
|
||||
Examples can be run from the Zed repository root:
|
||||
|
||||
```sh
|
||||
cargo run -p gpui --example hello_world
|
||||
```
|
||||
|
||||
## Running in a browser
|
||||
|
||||
A selection of these examples is also served by the standalone web gallery:
|
||||
|
||||
```sh
|
||||
cd crates/gpui_web/examples/hello_web
|
||||
trunk serve
|
||||
```
|
||||
|
||||
Open <http://localhost:8080/> to choose an example:
|
||||
|
||||
| Path | Example |
|
||||
| --- | --- |
|
||||
| `/hello-world` | Hello world |
|
||||
| `/text` | Styled text |
|
||||
| `/text-layout` | Alignment and decorations |
|
||||
| `/text-wrapper` | Wrapping and truncation |
|
||||
| `/input` | Text input and selection |
|
||||
| `/dynamic-fonts` | Download a font on missing-glyph notifications |
|
||||
| `/prime-sieve` | Background task demo |
|
||||
|
||||
The server supports direct links and reloads at each path. Only the selected
|
||||
example's WASM module is loaded. Follow **All examples** to return to the gallery;
|
||||
navigation reloads the page so the previous app and its workers are released.
|
||||
|
||||
Install [Trunk](https://trunk-rs.github.io/trunk/) if needed. The gallery's toolchain file selects
|
||||
nightly Rust, the WASM target, and `rust-src`. Its Trunk configuration supplies the
|
||||
cross-origin-isolation headers needed for shared memory and watches the GPUI
|
||||
example and renderer sources for automatic rebuilds and browser reloads. Use a
|
||||
browser with WebGPU or WebGL2 support.
|
||||
|
||||
The gallery builds separate Cargo binaries directly from the existing example
|
||||
sources. To add another browser-compatible example, register its binary in the
|
||||
gallery's `Cargo.toml`, add an auxiliary Rust asset in `index.html`, and add its
|
||||
gallery link with the corresponding `data-module` name.
|
||||
|
||||
### Dynamic font loading
|
||||
|
||||
Open `/dynamic-fonts` to see `App::on_missing_glyphs` fetch a versioned
|
||||
[Noto Sans Devanagari TTF](https://fonts.gstatic.com/s/notosansdevanagari/v30/TuGoUUFzXI5FBtUq5a8bjKYTZjtRU6Sgv3NaV_SNmI0b8QQCQmHn6B2OHjbL_08AlXQly-A.ttf)
|
||||
from Google Fonts' CDN. The 219 KB font is licensed under the
|
||||
[SIL Open Font License](https://github.com/google/fonts/blob/main/ofl/notosansdevanagari/OFL.txt).
|
||||
The CDN permits cross-origin requests and caches the font for one year.
|
||||
No sample text is sent to the host.
|
||||
|
||||
The example bundles only IBM Plex Sans, downloads the fallback once on demand,
|
||||
calls `add_fonts`, and refreshes the windows to reshape the text. Emoji continues
|
||||
to use the default Canvas fallback. The page displays loading and failure states;
|
||||
use browser network throttling or block `fonts.gstatic.com` and reload to
|
||||
exercise them. Reload to retry a failed request.
|
||||
|
||||
This is a small script-to-font mapping, not a general font discovery service.
|
||||
For production, host versioned font assets on your own CDN and choose an explicit
|
||||
coverage, caching, and retry policy. Use raw TTF/OTF assets rather than passing
|
||||
Google Fonts CSS or WOFF2 responses directly to `add_fonts`.
|
||||
|
||||
### Canvas font fallback
|
||||
|
||||
The web text system uses browser fonts for eligible missing CJK and emoji
|
||||
graphemes. It renders each complete grapheme independently, retaining the primary
|
||||
font's baseline and line-spacing metrics. Contextual CJK spacing and arbitrary
|
||||
OpenType features are not reproduced. Other scripts remain on Cosmic Text.
|
||||
Browser font availability is assumed stable for the application's lifetime.
|
||||
|
||||
With the gallery server running, a browser smoke test checks native-font
|
||||
preservation, whole-grapheme rendering, emoji atlas reuse, and input/deletion:
|
||||
|
||||
```sh
|
||||
node crates/gpui_web/examples/hello_web/test_canvas_fallback.mjs
|
||||
```
|
||||
|
||||
This requires Node.js 22 or newer and Chrome/Chromium. On macOS it uses the
|
||||
standard Google Chrome application path; elsewhere it uses `chromium`. Set
|
||||
`CHROME` to override the executable, and pass an optional server URL as the first
|
||||
argument. Screenshots and logs are saved under the gallery's ignored `target`
|
||||
directory. The test intercepts clipboard writes rather than modifying the system
|
||||
clipboard.
|
||||
|
||||
## Where to start
|
||||
|
||||
- `hello_world` shows the basic shape of a GPUI application: create an
|
||||
`Application`, open a window, create a root view, and render a `div`.
|
||||
- `input` demonstrates text input, focus, selections, clipboard actions, and
|
||||
keyboard bindings.
|
||||
- `uniform_list` shows how to render a simple virtualized list.
|
||||
- `testing` demonstrates `#[gpui::test]`, `TestAppContext`, actions, focus, and
|
||||
window-based tests.
|
||||
|
||||
## Layout and styling
|
||||
|
||||
- `grid_layout` demonstrates CSS-grid-style layout.
|
||||
- `opacity` demonstrates opacity styling.
|
||||
- `pattern` shows patterned backgrounds.
|
||||
- `shadow` demonstrates box shadows.
|
||||
- `text` shows styled text rendering.
|
||||
- `text_layout` demonstrates text alignment, decoration, weights, and wrapping.
|
||||
- `text_wrapper` shows wrapping text content.
|
||||
|
||||
## Interaction
|
||||
|
||||
- `anchor` demonstrates anchored positioning.
|
||||
- `data_table` combines virtualized list rendering with table-style rows and a
|
||||
custom scrollbar.
|
||||
- `drag_drop` shows draggable elements and drop targets.
|
||||
- `focus_visible` demonstrates keyboard-visible focus styling.
|
||||
- `mouse_pressure` demonstrates pressure-sensitive pointer input where supported.
|
||||
- `popover` shows floating layers with `deferred` and `anchored`.
|
||||
- `scrollable` demonstrates scrollable content.
|
||||
- `tab_stop` shows keyboard tab navigation.
|
||||
|
||||
## Images, drawing, and animation
|
||||
|
||||
- `animation` demonstrates GPUI animations and animated SVG transforms.
|
||||
- `gif_viewer` shows GIF rendering.
|
||||
- `gradient` demonstrates linear gradients and color spaces.
|
||||
- `image` shows local and remote image loading, image sizing, and asset setup.
|
||||
- `image_gallery` demonstrates image caching and loading remote images.
|
||||
- `image_loading` shows image loading states and asset loading.
|
||||
- `painting` demonstrates custom drawing with paths and canvas.
|
||||
- `svg` shows SVG rendering.
|
||||
|
||||
## Windows and application behavior
|
||||
|
||||
- `move_entity_between_windows` shows moving an entity between windows.
|
||||
- `on_window_close_quit` demonstrates quitting when a window closes.
|
||||
- `set_menus` shows application menu setup.
|
||||
- `system_notifications` demonstrates posting, replacing, dismissing, and responding to operating-system notifications.
|
||||
- `window` demonstrates creating normal, dialog, popup, and floating windows.
|
||||
- `window_positioning` demonstrates window bounds and placement.
|
||||
- `window_shadow` demonstrates window shadow styling.
|
||||
|
||||
## Specialized examples
|
||||
|
||||
These examples are useful when working on GPUI itself, but they may not be the
|
||||
best starting point for new applications:
|
||||
|
||||
- `active_state_bug` is a focused active-state reproduction.
|
||||
- `toggle_headless` starts without a display and preserves its app and todo
|
||||
entities while attaching to or detaching from Wayland or X11. Run it on Linux
|
||||
with `cargo run -p gpui --example toggle_headless`; its stdin commands are
|
||||
`ls`, `create [message]`, `open`, `close`, and `quit`. `open` attaches to the
|
||||
display server in the process environment and opens the window; while headless,
|
||||
`open DISPLAY=:1` (or `WAYLAND_DISPLAY=… XDG_RUNTIME_DIR=…`) targets another one.
|
||||
`close` closes the window and returns to headless mode.
|
||||
- `layer_shell` demonstrates Linux layer-shell windows.
|
||||
- `list_example` demonstrates bottom-aligned list state and scrollbar behavior.
|
||||
- `ownership_post` supports the ownership and data-flow documentation.
|
||||
- `paths_bench` is a path rendering benchmark.
|
||||
- `tree` renders a deep tree of nested elements.
|
||||
Vendored
+272
@@ -0,0 +1,272 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
//! Accessibility (AccessKit) demo app.
|
||||
//!
|
||||
//! Run with: `cargo run -p gpui --example a11y`
|
||||
//!
|
||||
//! Or on Linux: `cargo run -p gpui --features gpui_platform/wayland,gpui_platform/x11 --example a11y`
|
||||
//!
|
||||
//! This app uses GPUI's accessibility APIs to attach structured information to
|
||||
//! the element tree, which allows assistive technology to see and interact with
|
||||
//! the UI programmatically.
|
||||
//!
|
||||
//! The app behaves as follows:
|
||||
//! - It opens a single window.
|
||||
//! - The window's title is "GPUI Accessibility Demo".
|
||||
//! - The window has a sequence of UI elements, stacked vertically:
|
||||
//! - A heading with the text "Accessibility Demo".
|
||||
//! - A row containing two elements:
|
||||
//! - A spin button (role `SpinButton`) labelled "Counter: <n>", where
|
||||
//! `<n>` is the current count. It supports `Increment` and `Decrement`
|
||||
//! accessible actions, and also increments on click. The numeric value
|
||||
//! is clamped to a minimum of 0.
|
||||
//! - A button labelled "Reset counter" that resets the count to 0.
|
||||
//! - A row containing two elements:
|
||||
//! - A switch, that can be toggled, and starts disabled. Toggling the switch
|
||||
//! does nothing.
|
||||
//! - The text "Enable feature".
|
||||
//! - A "to-do" list, with three items, each represented with a `Text` element:
|
||||
//! - "1. Write code"
|
||||
//! - "2. Run tests"
|
||||
//! - "3. Ship it"
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{
|
||||
AccessibleAction, App, Bounds, Context, FocusHandle, KeyBinding, Role, SharedString, Toggled,
|
||||
Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, rgb, size, text,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
actions!(a11y_example, [Tab, TabPrev]);
|
||||
|
||||
struct A11yDemo {
|
||||
focus_handle: FocusHandle,
|
||||
count: i32,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
impl A11yDemo {
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
window.focus(&focus_handle, cx);
|
||||
Self {
|
||||
focus_handle,
|
||||
count: 0,
|
||||
enabled: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for A11yDemo {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.id("root")
|
||||
.role(Role::Application)
|
||||
.aria_label("Accessibility Demo")
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action(cx.listener(|_, _: &Tab, window, cx| window.focus_next(cx)))
|
||||
.on_action(cx.listener(|_, _: &TabPrev, window, cx| window.focus_prev(cx)))
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_4()
|
||||
.p_4()
|
||||
.bg(rgb(0x1e1e2e))
|
||||
.text_color(rgb(0xcdd6f4))
|
||||
// Heading
|
||||
.child(
|
||||
div()
|
||||
.id("heading")
|
||||
.role(Role::Heading)
|
||||
.aria_level(1)
|
||||
.aria_label("Accessibility Demo")
|
||||
.text_xl()
|
||||
.font_weight(gpui::FontWeight::BOLD)
|
||||
.child(text!("Accessibility Demo")),
|
||||
)
|
||||
// Counter — uses a SpinButton role with Increment/Decrement
|
||||
// actions so screen readers can adjust the value directly.
|
||||
// Click also works via the built-in handler.
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.child(
|
||||
div()
|
||||
.id("counter")
|
||||
.focusable()
|
||||
.tab_stop(true)
|
||||
.role(Role::SpinButton)
|
||||
.aria_label(SharedString::from(format!("Counter: {}", self.count)))
|
||||
.aria_numeric_value(self.count as f64)
|
||||
.aria_min_numeric_value(0.0)
|
||||
.on_a11y_action(AccessibleAction::Increment, {
|
||||
let this = cx.entity().downgrade();
|
||||
move |_, _, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.count += 1;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.on_a11y_action(AccessibleAction::Decrement, {
|
||||
let this = cx.entity().downgrade();
|
||||
move |_, _, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.count = (this.count - 1).max(0);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.count += 1;
|
||||
cx.notify();
|
||||
}))
|
||||
.px_3()
|
||||
.py_1()
|
||||
.rounded_md()
|
||||
.bg(rgb(0x89b4fa))
|
||||
.text_color(rgb(0x1e1e2e))
|
||||
.cursor_pointer()
|
||||
.child(text!(format!("Count: {}", self.count))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("reset")
|
||||
.focusable()
|
||||
.tab_stop(true)
|
||||
.role(Role::Button)
|
||||
.aria_label("Reset counter")
|
||||
.px_3()
|
||||
.py_1()
|
||||
.rounded_md()
|
||||
.bg(rgb(0x585b70))
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.count = 0;
|
||||
cx.notify();
|
||||
}))
|
||||
.child(text!("Reset")),
|
||||
),
|
||||
)
|
||||
// A toggle switch
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.id("toggle")
|
||||
.focusable()
|
||||
.tab_stop(true)
|
||||
.role(Role::Switch)
|
||||
.aria_label("Enable feature")
|
||||
.aria_toggled(if self.enabled {
|
||||
Toggled::True
|
||||
} else {
|
||||
Toggled::False
|
||||
})
|
||||
.w(px(44.))
|
||||
.h(px(24.))
|
||||
.rounded_full()
|
||||
.cursor_pointer()
|
||||
.when(self.enabled, |el| el.bg(rgb(0x89b4fa)))
|
||||
.when(!self.enabled, |el| el.bg(rgb(0x585b70)))
|
||||
.child(
|
||||
div()
|
||||
.size(px(20.))
|
||||
.rounded_full()
|
||||
.bg(gpui::white())
|
||||
.mt(px(2.))
|
||||
.when(self.enabled, |el| el.ml(px(22.)))
|
||||
.when(!self.enabled, |el| el.ml(px(2.))),
|
||||
)
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.enabled = !this.enabled;
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
.child(text!("Enable feature")),
|
||||
)
|
||||
// A short list
|
||||
.child(
|
||||
div()
|
||||
.id("task-list")
|
||||
.role(Role::List)
|
||||
.aria_label("Tasks")
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.children(
|
||||
["Write code", "Run tests", "Ship it"]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, label)| {
|
||||
div()
|
||||
.id(("task", i))
|
||||
.role(Role::ListItem)
|
||||
.aria_label(SharedString::from(*label))
|
||||
.aria_position_in_set(i + 1)
|
||||
.aria_size_of_set(3)
|
||||
.py_1()
|
||||
.px_2()
|
||||
// Note: even though this `text!` macro
|
||||
// produces multiple elements, it doesn't
|
||||
// need its own unique ID because the parent
|
||||
// div has different IDs for each string.
|
||||
.child(text!(format!("{}. {}", i + 1, label)))
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("tab", Tab, None),
|
||||
KeyBinding::new("shift-tab", TabPrev, None),
|
||||
]);
|
||||
|
||||
let bounds = Bounds::centered(None, size(px(500.), px(400.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
titlebar: Some(gpui::TitlebarOptions {
|
||||
title: Some("GPUI Accessibility Demo".into()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| cx.new(|cx| A11yDemo::new(window, cx)),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
env_logger::builder()
|
||||
.filter_level(log::LevelFilter::Warn)
|
||||
.filter_module("gpui", log::LevelFilter::Info)
|
||||
.init();
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/// Click the button — the `.active()` background gets stuck on every other click.
|
||||
use gpui::*;
|
||||
use gpui_platform::application;
|
||||
|
||||
struct Example;
|
||||
|
||||
impl Render for Example {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
// Colors from Zed's default dark theme
|
||||
let bg = hsla(215. / 360., 0.12, 0.15, 1.);
|
||||
let text = hsla(221. / 360., 0.11, 0.86, 1.);
|
||||
let hover = hsla(225. / 360., 0.118, 0.267, 1.);
|
||||
let active = hsla(220. / 360., 0.118, 0.20, 1.);
|
||||
|
||||
div().bg(bg).size_full().p_1().child(
|
||||
div()
|
||||
.id("button")
|
||||
.px_2()
|
||||
.py_0p5()
|
||||
.rounded_md()
|
||||
.text_sm()
|
||||
.text_color(text)
|
||||
.hover(|s| s.bg(hover))
|
||||
.active(|s| s.bg(active))
|
||||
.on_click(|_, _, _| {})
|
||||
.child("Click me"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
application().run(|cx: &mut App| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
|
||||
None,
|
||||
size(px(200.), px(60.)),
|
||||
cx,
|
||||
))),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| cx.new(|_| Example),
|
||||
)
|
||||
.unwrap();
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
Vendored
+207
@@ -0,0 +1,207 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{
|
||||
Anchor, AnchoredPositionMode, App, Axis, Bounds, Context, Half as _, InteractiveElement,
|
||||
ParentElement, Pixels, Point, Render, SharedString, Size, Window, WindowBounds, WindowOptions,
|
||||
anchored, deferred, div, point, prelude::*, px, rgb, size,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
struct AnchorDemo {
|
||||
hovered_button: Option<usize>,
|
||||
}
|
||||
|
||||
struct ButtonDemo {
|
||||
label: SharedString,
|
||||
corner: Option<Anchor>,
|
||||
}
|
||||
|
||||
fn resolved_position(corner: Anchor, button_size: Size<Pixels>) -> Point<Pixels> {
|
||||
let offset = Point {
|
||||
x: px(0.),
|
||||
y: -button_size.height,
|
||||
};
|
||||
|
||||
offset
|
||||
+ match corner.other_side_along(Axis::Vertical) {
|
||||
Anchor::TopLeft => point(px(0.0), px(0.0)),
|
||||
Anchor::TopCenter => point(button_size.width.half(), px(0.0)),
|
||||
Anchor::TopRight => point(button_size.width, px(0.0)),
|
||||
Anchor::LeftCenter => point(button_size.width, button_size.height.half()),
|
||||
Anchor::RightCenter => point(px(0.), button_size.height.half()),
|
||||
Anchor::BottomLeft => point(px(0.0), button_size.height),
|
||||
Anchor::BottomCenter => point(button_size.width / 2.0, button_size.height),
|
||||
Anchor::BottomRight => point(button_size.width, button_size.height),
|
||||
}
|
||||
}
|
||||
|
||||
impl AnchorDemo {
|
||||
fn buttons() -> Vec<ButtonDemo> {
|
||||
vec![
|
||||
ButtonDemo {
|
||||
label: "TopLeft".into(),
|
||||
corner: Some(Anchor::TopLeft),
|
||||
},
|
||||
ButtonDemo {
|
||||
label: "TopCenter".into(),
|
||||
corner: Some(Anchor::TopCenter),
|
||||
},
|
||||
ButtonDemo {
|
||||
label: "TopRight".into(),
|
||||
corner: Some(Anchor::TopRight),
|
||||
},
|
||||
ButtonDemo {
|
||||
label: "LeftCenter".into(),
|
||||
corner: Some(Anchor::LeftCenter),
|
||||
},
|
||||
ButtonDemo {
|
||||
label: "Center".into(),
|
||||
corner: None,
|
||||
},
|
||||
ButtonDemo {
|
||||
label: "RightCenter".into(),
|
||||
corner: Some(Anchor::RightCenter),
|
||||
},
|
||||
ButtonDemo {
|
||||
label: "BottomLeft".into(),
|
||||
corner: Some(Anchor::BottomLeft),
|
||||
},
|
||||
ButtonDemo {
|
||||
label: "BottomCenter".into(),
|
||||
corner: Some(Anchor::BottomCenter),
|
||||
},
|
||||
ButtonDemo {
|
||||
label: "BottomRight".into(),
|
||||
corner: Some(Anchor::BottomRight),
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AnchorDemo {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let buttons = Self::buttons();
|
||||
let button_size = size(px(120.0), px(65.0));
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.bg(gpui::white())
|
||||
.gap_4()
|
||||
.p_10()
|
||||
.child("Popover with Anchor")
|
||||
.child(
|
||||
div()
|
||||
.size_128()
|
||||
.grid()
|
||||
.grid_cols(3)
|
||||
.gap_6()
|
||||
.relative()
|
||||
.children(buttons.iter().enumerate().map(|(index, button)| {
|
||||
let is_hovered = self.hovered_button == Some(index);
|
||||
let is_hoverable = button.corner.is_some();
|
||||
div()
|
||||
.relative()
|
||||
.child(
|
||||
div()
|
||||
.id(("button", index))
|
||||
.w(button_size.width)
|
||||
.h(button_size.height)
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.bg(gpui::white())
|
||||
.when(is_hoverable, |this| {
|
||||
this.border_1()
|
||||
.rounded_lg()
|
||||
.border_color(gpui::black())
|
||||
.hover(|style| {
|
||||
style.bg(gpui::black()).text_color(gpui::white())
|
||||
})
|
||||
.on_hover(cx.listener(
|
||||
move |this, hovered, _window, cx| {
|
||||
if *hovered {
|
||||
this.hovered_button = Some(index);
|
||||
} else if this.hovered_button == Some(index) {
|
||||
this.hovered_button = None;
|
||||
}
|
||||
cx.notify();
|
||||
},
|
||||
))
|
||||
.child(button.label.clone())
|
||||
}),
|
||||
)
|
||||
.when_some(self.hovered_button.filter(|_| is_hovered), |this, index| {
|
||||
let button = &buttons[index];
|
||||
let Some(corner) = button.corner else {
|
||||
return this;
|
||||
};
|
||||
|
||||
let position = resolved_position(corner, button_size);
|
||||
this.child(deferred(
|
||||
anchored()
|
||||
.anchor(corner)
|
||||
.position(position)
|
||||
.position_mode(AnchoredPositionMode::Local)
|
||||
.snap_to_window()
|
||||
.child(
|
||||
div()
|
||||
.py_0p5()
|
||||
.px_2()
|
||||
.bg(gpui::black().opacity(0.75))
|
||||
.text_color(rgb(0xffffff))
|
||||
.rounded_sm()
|
||||
.shadow_sm()
|
||||
.min_w(px(100.0))
|
||||
.text_sm()
|
||||
.child(button.label.clone()),
|
||||
),
|
||||
))
|
||||
})
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
|
||||
None,
|
||||
size(px(750.), px(600.)),
|
||||
cx,
|
||||
))),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| {
|
||||
cx.new(|_| AnchorDemo {
|
||||
hovered_button: None,
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
+373
@@ -0,0 +1,373 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::{
|
||||
Animation, AnimationExt as _, AnimationPhase, App, AssetSource, Bounds, Context, MouseButton,
|
||||
MouseDownEvent, MouseMoveEvent, Pixels, SharedString, SpringAnimation, SpringConfig,
|
||||
Transformation, Window, WindowBounds, WindowOptions, bounce, div, ease_in_out, percentage,
|
||||
prelude::*, px, relative, rgba, size, svg,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
struct Assets {}
|
||||
|
||||
impl AssetSource for Assets {
|
||||
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
|
||||
std::fs::read(path)
|
||||
.map(Into::into)
|
||||
.map_err(Into::into)
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
|
||||
Ok(std::fs::read_dir(path)?
|
||||
.filter_map(|entry| {
|
||||
Some(SharedString::from(
|
||||
entry.ok()?.path().to_string_lossy().into_owned(),
|
||||
))
|
||||
})
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
}
|
||||
|
||||
const ARROW_CIRCLE_SVG: &str = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/examples/image/arrow_circle.svg"
|
||||
);
|
||||
|
||||
struct AnimationExample {
|
||||
spring_phase: u8,
|
||||
spring_damping: f32,
|
||||
spring_damping_drag: Option<(Pixels, f32)>,
|
||||
}
|
||||
|
||||
impl Render for AnimationExample {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
const MINIMUM_DAMPING: f32 = 2.0;
|
||||
const MAXIMUM_DAMPING: f32 = 32.0;
|
||||
const DAMPING_SLIDER_WIDTH: Pixels = px(224.0);
|
||||
|
||||
let spring_phase = self.spring_phase;
|
||||
let spring_position = px(98.0 * f32::from(spring_phase));
|
||||
let spring_damping = self.spring_damping;
|
||||
let spring = SpringConfig::new(170.0, spring_damping, 1.0);
|
||||
let (_, damping_ratio) = spring.canonical();
|
||||
let damping_fraction =
|
||||
(spring_damping - MINIMUM_DAMPING) / (MAXIMUM_DAMPING - MINIMUM_DAMPING);
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.bg(gpui::white())
|
||||
.text_color(gpui::black())
|
||||
.justify_around()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.justify_around()
|
||||
.child(
|
||||
div()
|
||||
.id("content")
|
||||
.flex()
|
||||
.flex_col()
|
||||
.h(px(150.))
|
||||
.overflow_y_scroll()
|
||||
.w_full()
|
||||
.flex_1()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.text_xl()
|
||||
.gap_4()
|
||||
.child("Hello Animation")
|
||||
.child(
|
||||
div()
|
||||
.id("spring-demo")
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.p_2()
|
||||
.w(px(240.0))
|
||||
.rounded_md()
|
||||
.bg(rgba(0x00000010))
|
||||
.cursor_pointer()
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(|this, _, _, cx| {
|
||||
this.spring_phase = (this.spring_phase + 1) % 3;
|
||||
cx.notify();
|
||||
}),
|
||||
)
|
||||
.child(format!(
|
||||
"Target phase {spring_phase}: click rapidly to redirect momentum"
|
||||
))
|
||||
.child(
|
||||
div()
|
||||
.id("spring-damping-controls")
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.text_sm()
|
||||
.on_click(|_, _, cx| cx.stop_propagation())
|
||||
.child(format!(
|
||||
"Drag damping: {spring_damping:.1} (ζ {damping_ratio:.2})"
|
||||
))
|
||||
.child(
|
||||
div()
|
||||
.id("spring-damping")
|
||||
.relative()
|
||||
.h(px(20.0))
|
||||
.w(DAMPING_SLIDER_WIDTH)
|
||||
.cursor_pointer()
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(
|
||||
|this,
|
||||
event: &MouseDownEvent,
|
||||
_,
|
||||
cx| {
|
||||
this.spring_damping_drag = Some((
|
||||
event.position.x,
|
||||
this.spring_damping,
|
||||
));
|
||||
cx.stop_propagation();
|
||||
},
|
||||
),
|
||||
)
|
||||
.on_mouse_move(cx.listener(
|
||||
|this,
|
||||
event: &MouseMoveEvent,
|
||||
_,
|
||||
cx| {
|
||||
let Some((
|
||||
start_position,
|
||||
start_damping,
|
||||
)) = this.spring_damping_drag
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let delta = (event.position.x
|
||||
- start_position)
|
||||
/ DAMPING_SLIDER_WIDTH;
|
||||
this.spring_damping = (start_damping
|
||||
+ delta
|
||||
* (MAXIMUM_DAMPING
|
||||
- MINIMUM_DAMPING))
|
||||
.clamp(
|
||||
MINIMUM_DAMPING,
|
||||
MAXIMUM_DAMPING,
|
||||
);
|
||||
cx.stop_propagation();
|
||||
cx.notify();
|
||||
},
|
||||
))
|
||||
.on_mouse_up(
|
||||
MouseButton::Left,
|
||||
cx.listener(|this, _, _, cx| {
|
||||
this.spring_damping_drag = None;
|
||||
cx.stop_propagation();
|
||||
}),
|
||||
)
|
||||
.on_mouse_up_out(
|
||||
MouseButton::Left,
|
||||
cx.listener(|this, _, _, cx| {
|
||||
this.spring_damping_drag = None;
|
||||
cx.stop_propagation();
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top(px(8.0))
|
||||
.h(px(4.0))
|
||||
.w_full()
|
||||
.rounded_full()
|
||||
.bg(rgba(0x00000030)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top(px(8.0))
|
||||
.h(px(4.0))
|
||||
.w(relative(damping_fraction))
|
||||
.rounded_full()
|
||||
.bg(rgba(0x3b82f6ff)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top(px(3.0))
|
||||
.left(relative(damping_fraction))
|
||||
.ml(px(-7.0))
|
||||
.size(px(14.0))
|
||||
.rounded_full()
|
||||
.bg(rgba(0x2563ebff)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().relative().h(px(32.0)).w_full().child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_1()
|
||||
.size(px(24.0))
|
||||
.rounded_full()
|
||||
.bg(rgba(0x3b82f6ff))
|
||||
.cursor_pointer()
|
||||
.debug_selector(|| "spring-position".into())
|
||||
.with_spring(
|
||||
"spring-position",
|
||||
SpringAnimation::new(spring)
|
||||
.to(spring_position)
|
||||
.with_epsilon(0.25),
|
||||
|this, left| this.left(left),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().h(px(24.0)).rounded_sm().with_spring(
|
||||
"spring-phase",
|
||||
SpringAnimation::new(spring)
|
||||
.to(AnimationPhase(f32::from(spring_phase)))
|
||||
.with_epsilon(0.001),
|
||||
|this, phase| {
|
||||
let (width, color) = if phase.0 <= 1.0 {
|
||||
(
|
||||
phase.interpolate_between(
|
||||
0.0..=1.0,
|
||||
px(48.0),
|
||||
px(224.0),
|
||||
),
|
||||
phase.interpolate_between_clamped(
|
||||
0.0..=1.0,
|
||||
rgba(0xf97316ff),
|
||||
rgba(0x22c55eff),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
phase.interpolate_between(
|
||||
1.0..=2.0,
|
||||
px(224.0),
|
||||
px(96.0),
|
||||
),
|
||||
phase.interpolate_between_clamped(
|
||||
1.0..=2.0,
|
||||
rgba(0x22c55eff),
|
||||
rgba(0xa855f7ff),
|
||||
),
|
||||
)
|
||||
};
|
||||
this.w(width).bg(color)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
svg()
|
||||
.size_20()
|
||||
.overflow_hidden()
|
||||
.path(ARROW_CIRCLE_SVG)
|
||||
.text_color(gpui::black())
|
||||
.with_animation(
|
||||
"image_circle",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(bounce(ease_in_out)),
|
||||
|svg, delta| {
|
||||
svg.with_transformation(Transformation::rotate(
|
||||
percentage(delta),
|
||||
))
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.h(px(64.))
|
||||
.w_full()
|
||||
.p_2()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.border_t_1()
|
||||
.border_color(gpui::black().opacity(0.1))
|
||||
.bg(gpui::black().opacity(0.05))
|
||||
.child("Other Panel"),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().with_assets(Assets {}).run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
let options = WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
|
||||
None,
|
||||
size(px(300.), px(300.)),
|
||||
cx,
|
||||
))),
|
||||
..Default::default()
|
||||
};
|
||||
cx.open_window(options, |_, cx| {
|
||||
cx.activate(false);
|
||||
cx.new(|_| AnimationExample {
|
||||
spring_phase: 0,
|
||||
spring_damping: 14.0,
|
||||
spring_damping_drag: None,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "test-support"))]
|
||||
mod tests {
|
||||
use gpui::{Modifiers, TestAppContext};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
fn clicking_spring_position_changes_the_target(cx: &mut TestAppContext) {
|
||||
let (view, cx) = cx.add_window_view(|_, _| AnimationExample {
|
||||
spring_phase: 0,
|
||||
spring_damping: 14.0,
|
||||
spring_damping_drag: None,
|
||||
});
|
||||
cx.simulate_resize(size(px(300.0), px(300.0)));
|
||||
cx.run_until_parked();
|
||||
|
||||
let position_bounds = cx
|
||||
.debug_bounds("spring-position")
|
||||
.expect("spring position should be rendered");
|
||||
let position = position_bounds.center();
|
||||
cx.simulate_mouse_move(position, None, Modifiers::default());
|
||||
cx.simulate_click(position, Modifiers::default());
|
||||
cx.run_until_parked();
|
||||
|
||||
assert_eq!(cx.update(|_, cx| view.read(cx).spring_phase), 1);
|
||||
}
|
||||
}
|
||||
+494
@@ -0,0 +1,494 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use std::{ops::Range, rc::Rc, time::Duration};
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, Render,
|
||||
SharedString, UniformListScrollHandle, Window, WindowBounds, WindowOptions, canvas, div, point,
|
||||
prelude::*, px, rgb, size, uniform_list,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
const TOTAL_ITEMS: usize = 10000;
|
||||
const SCROLLBAR_THUMB_WIDTH: Pixels = px(8.);
|
||||
const SCROLLBAR_THUMB_HEIGHT: Pixels = px(100.);
|
||||
|
||||
pub struct Quote {
|
||||
name: SharedString,
|
||||
symbol: SharedString,
|
||||
last_done: f64,
|
||||
prev_close: f64,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
timestamp: Duration,
|
||||
volume: i64,
|
||||
turnover: f64,
|
||||
ttm: f64,
|
||||
market_cap: f64,
|
||||
float_cap: f64,
|
||||
shares: f64,
|
||||
pb: f64,
|
||||
pe: f64,
|
||||
eps: f64,
|
||||
dividend: f64,
|
||||
dividend_yield: f64,
|
||||
dividend_per_share: f64,
|
||||
dividend_date: SharedString,
|
||||
dividend_payment: f64,
|
||||
}
|
||||
|
||||
impl Quote {
|
||||
pub fn random() -> Self {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::rng();
|
||||
// simulate a base price in a realistic range
|
||||
let prev_close = rng.random_range(100.0..200.0);
|
||||
let change = rng.random_range(-5.0..5.0);
|
||||
let last_done = prev_close + change;
|
||||
let open = prev_close + rng.random_range(-3.0..3.0);
|
||||
let high = (prev_close + rng.random_range::<f64, _>(0.0..10.0)).max(open);
|
||||
let low = (prev_close - rng.random_range::<f64, _>(0.0..10.0)).min(open);
|
||||
let timestamp = Duration::from_secs(rng.random_range(0..86400));
|
||||
let volume = rng.random_range(1_000_000..100_000_000);
|
||||
let turnover = last_done * volume as f64;
|
||||
let symbol = {
|
||||
let mut ticker = String::new();
|
||||
if rng.random_bool(0.5) {
|
||||
ticker.push_str(&format!(
|
||||
"{:03}.{}",
|
||||
rng.random_range(100..1000),
|
||||
rng.random_range(0..10)
|
||||
));
|
||||
} else {
|
||||
ticker.push_str(&format!(
|
||||
"{}{}",
|
||||
rng.random_range('A'..='Z'),
|
||||
rng.random_range('A'..='Z')
|
||||
));
|
||||
}
|
||||
ticker.push_str(&format!(".{}", rng.random_range('A'..='Z')));
|
||||
ticker
|
||||
};
|
||||
let name = format!(
|
||||
"{} {} - #{}",
|
||||
symbol,
|
||||
rng.random_range(1..100),
|
||||
rng.random_range(10000..100000)
|
||||
);
|
||||
let ttm = rng.random_range(0.0..10.0);
|
||||
let market_cap = rng.random_range(1_000_000.0..10_000_000.0);
|
||||
let float_cap = market_cap + rng.random_range(1_000.0..10_000.0);
|
||||
let shares = rng.random_range(100.0..1000.0);
|
||||
let pb = market_cap / shares;
|
||||
let pe = market_cap / shares;
|
||||
let eps = market_cap / shares;
|
||||
let dividend = rng.random_range(0.0..10.0);
|
||||
let dividend_yield = rng.random_range(0.0..10.0);
|
||||
let dividend_per_share = rng.random_range(0.0..10.0);
|
||||
let dividend_date = SharedString::new(format!(
|
||||
"{}-{}-{}",
|
||||
rng.random_range(2000..2023),
|
||||
rng.random_range(1..12),
|
||||
rng.random_range(1..28)
|
||||
));
|
||||
let dividend_payment = rng.random_range(0.0..10.0);
|
||||
|
||||
Self {
|
||||
name: name.into(),
|
||||
symbol: symbol.into(),
|
||||
last_done,
|
||||
prev_close,
|
||||
open,
|
||||
high,
|
||||
low,
|
||||
timestamp,
|
||||
volume,
|
||||
turnover,
|
||||
pb,
|
||||
pe,
|
||||
eps,
|
||||
ttm,
|
||||
market_cap,
|
||||
float_cap,
|
||||
shares,
|
||||
dividend,
|
||||
dividend_yield,
|
||||
dividend_per_share,
|
||||
dividend_date,
|
||||
dividend_payment,
|
||||
}
|
||||
}
|
||||
|
||||
fn change(&self) -> f64 {
|
||||
(self.last_done - self.prev_close) / self.prev_close * 100.0
|
||||
}
|
||||
|
||||
fn change_color(&self) -> gpui::Hsla {
|
||||
if self.change() > 0.0 {
|
||||
gpui::green()
|
||||
} else {
|
||||
gpui::red()
|
||||
}
|
||||
}
|
||||
|
||||
fn turnover_ratio(&self) -> f64 {
|
||||
self.volume as f64 / self.turnover * 100.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct TableRow {
|
||||
ix: usize,
|
||||
quote: Rc<Quote>,
|
||||
}
|
||||
impl TableRow {
|
||||
fn new(ix: usize, quote: Rc<Quote>) -> Self {
|
||||
Self { ix, quote }
|
||||
}
|
||||
|
||||
fn render_cell(&self, key: &str, width: Pixels, color: gpui::Hsla) -> impl IntoElement {
|
||||
div()
|
||||
.whitespace_nowrap()
|
||||
.truncate()
|
||||
.w(width)
|
||||
.px_1()
|
||||
.child(match key {
|
||||
"id" => div().child(format!("{}", self.ix)),
|
||||
"symbol" => div().child(self.quote.symbol.clone()),
|
||||
"name" => div().child(self.quote.name.clone()),
|
||||
"last_done" => div()
|
||||
.text_color(color)
|
||||
.child(format!("{:.3}", self.quote.last_done)),
|
||||
"prev_close" => div()
|
||||
.text_color(color)
|
||||
.child(format!("{:.3}", self.quote.prev_close)),
|
||||
"change" => div()
|
||||
.text_color(color)
|
||||
.child(format!("{:.2}%", self.quote.change())),
|
||||
"timestamp" => div()
|
||||
.text_color(color)
|
||||
.child(format!("{:?}", self.quote.timestamp.as_secs())),
|
||||
"open" => div()
|
||||
.text_color(color)
|
||||
.child(format!("{:.2}", self.quote.open)),
|
||||
"low" => div()
|
||||
.text_color(color)
|
||||
.child(format!("{:.2}", self.quote.low)),
|
||||
"high" => div()
|
||||
.text_color(color)
|
||||
.child(format!("{:.2}", self.quote.high)),
|
||||
"ttm" => div()
|
||||
.text_color(color)
|
||||
.child(format!("{:.2}", self.quote.ttm)),
|
||||
"eps" => div()
|
||||
.text_color(color)
|
||||
.child(format!("{:.2}", self.quote.eps)),
|
||||
"market_cap" => {
|
||||
div().child(format!("{:.2} M", self.quote.market_cap / 1_000_000.0))
|
||||
}
|
||||
"float_cap" => div().child(format!("{:.2} M", self.quote.float_cap / 1_000_000.0)),
|
||||
"turnover" => div().child(format!("{:.2} M", self.quote.turnover / 1_000_000.0)),
|
||||
"volume" => div().child(format!("{:.2} M", self.quote.volume as f64 / 1_000_000.0)),
|
||||
"turnover_ratio" => div().child(format!("{:.2}%", self.quote.turnover_ratio())),
|
||||
"pe" => div().child(format!("{:.2}", self.quote.pe)),
|
||||
"pb" => div().child(format!("{:.2}", self.quote.pb)),
|
||||
"shares" => div().child(format!("{:.2}", self.quote.shares)),
|
||||
"dividend" => div().child(format!("{:.2}", self.quote.dividend)),
|
||||
"yield" => div().child(format!("{:.2}%", self.quote.dividend_yield)),
|
||||
"dividend_per_share" => {
|
||||
div().child(format!("{:.2}", self.quote.dividend_per_share))
|
||||
}
|
||||
"dividend_date" => div().child(format!("{}", self.quote.dividend_date)),
|
||||
"dividend_payment" => div().child(format!("{:.2}", self.quote.dividend_payment)),
|
||||
_ => div().child("--"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const FIELDS: [(&str, f32); 24] = [
|
||||
("id", 64.),
|
||||
("symbol", 64.),
|
||||
("name", 180.),
|
||||
("last_done", 80.),
|
||||
("prev_close", 80.),
|
||||
("open", 80.),
|
||||
("low", 80.),
|
||||
("high", 80.),
|
||||
("ttm", 50.),
|
||||
("market_cap", 96.),
|
||||
("float_cap", 96.),
|
||||
("turnover", 120.),
|
||||
("volume", 100.),
|
||||
("turnover_ratio", 96.),
|
||||
("pe", 64.),
|
||||
("pb", 64.),
|
||||
("eps", 64.),
|
||||
("shares", 96.),
|
||||
("dividend", 64.),
|
||||
("yield", 64.),
|
||||
("dividend_per_share", 64.),
|
||||
("dividend_date", 96.),
|
||||
("dividend_payment", 64.),
|
||||
("timestamp", 120.),
|
||||
];
|
||||
|
||||
impl RenderOnce for TableRow {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let color = self.quote.change_color();
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.border_b_1()
|
||||
.border_color(rgb(0xE0E0E0))
|
||||
.bg(if self.ix.is_multiple_of(2) {
|
||||
rgb(0xFFFFFF)
|
||||
} else {
|
||||
rgb(0xFAFAFA)
|
||||
})
|
||||
.py_0p5()
|
||||
.px_2()
|
||||
.children(FIELDS.map(|(key, width)| self.render_cell(key, px(width), color)))
|
||||
}
|
||||
}
|
||||
|
||||
struct DataTable {
|
||||
/// Use `Rc` to share the same quote data across multiple items, avoid cloning.
|
||||
quotes: Vec<Rc<Quote>>,
|
||||
visible_range: Range<usize>,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
/// The position in thumb bounds when dragging start mouse down.
|
||||
drag_position: Option<Point<Pixels>>,
|
||||
}
|
||||
|
||||
impl DataTable {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
quotes: Vec::new(),
|
||||
visible_range: 0..0,
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
drag_position: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn generate(&mut self) {
|
||||
self.quotes = (0..TOTAL_ITEMS).map(|_| Rc::new(Quote::random())).collect();
|
||||
}
|
||||
|
||||
fn table_bounds(&self) -> Bounds<Pixels> {
|
||||
self.scroll_handle.0.borrow().base_handle.bounds()
|
||||
}
|
||||
|
||||
fn scroll_top(&self) -> Pixels {
|
||||
self.scroll_handle.0.borrow().base_handle.offset().y
|
||||
}
|
||||
|
||||
fn scroll_height(&self) -> Pixels {
|
||||
self.scroll_handle
|
||||
.0
|
||||
.borrow()
|
||||
.last_item_size
|
||||
.unwrap_or_default()
|
||||
.contents
|
||||
.height
|
||||
}
|
||||
|
||||
fn render_scrollbar(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let scroll_height = self.scroll_height();
|
||||
let table_bounds = self.table_bounds();
|
||||
let table_height = table_bounds.size.height;
|
||||
if table_height == px(0.) {
|
||||
return div().id("scrollbar");
|
||||
}
|
||||
|
||||
let percentage = -self.scroll_top() / scroll_height;
|
||||
let offset_top = (table_height * percentage).clamp(
|
||||
px(4.),
|
||||
(table_height - SCROLLBAR_THUMB_HEIGHT - px(4.)).max(px(4.)),
|
||||
);
|
||||
let entity = cx.entity();
|
||||
let scroll_handle = self.scroll_handle.0.borrow().base_handle.clone();
|
||||
|
||||
div()
|
||||
.id("scrollbar")
|
||||
.absolute()
|
||||
.top(offset_top)
|
||||
.right_1()
|
||||
.h(SCROLLBAR_THUMB_HEIGHT)
|
||||
.w(SCROLLBAR_THUMB_WIDTH)
|
||||
.bg(rgb(0xC0C0C0))
|
||||
.hover(|this| this.bg(rgb(0xA0A0A0)))
|
||||
.rounded_lg()
|
||||
.child(
|
||||
canvas(
|
||||
|_, _, _| (),
|
||||
move |thumb_bounds, _, window, _| {
|
||||
window.on_mouse_event({
|
||||
let entity = entity.clone();
|
||||
move |ev: &MouseDownEvent, _, _, cx| {
|
||||
if !thumb_bounds.contains(&ev.position) {
|
||||
return;
|
||||
}
|
||||
|
||||
entity.update(cx, |this, _| {
|
||||
this.drag_position = Some(
|
||||
ev.position - thumb_bounds.origin - table_bounds.origin,
|
||||
);
|
||||
})
|
||||
}
|
||||
});
|
||||
window.on_mouse_event({
|
||||
let entity = entity.clone();
|
||||
move |_: &MouseUpEvent, _, _, cx| {
|
||||
entity.update(cx, |this, _| {
|
||||
this.drag_position = None;
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
window.on_mouse_event(move |ev: &MouseMoveEvent, _, _, cx| {
|
||||
if !ev.dragging() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(drag_pos) = entity.read(cx).drag_position else {
|
||||
return;
|
||||
};
|
||||
|
||||
let inside_offset = drag_pos.y;
|
||||
let percentage = ((ev.position.y - table_bounds.origin.y
|
||||
+ inside_offset)
|
||||
/ (table_bounds.size.height))
|
||||
.clamp(0., 1.);
|
||||
|
||||
let offset_y = ((scroll_height - table_bounds.size.height)
|
||||
* percentage)
|
||||
.clamp(px(0.), scroll_height - SCROLLBAR_THUMB_HEIGHT);
|
||||
scroll_handle.set_offset(point(px(0.), -offset_y));
|
||||
cx.notify(entity.entity_id());
|
||||
})
|
||||
},
|
||||
)
|
||||
.size_full(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for DataTable {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.bg(gpui::white())
|
||||
.text_sm()
|
||||
.size_full()
|
||||
.p_4()
|
||||
.gap_2()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(format!(
|
||||
"Total {} items, visible range: {:?}",
|
||||
self.quotes.len(),
|
||||
self.visible_range
|
||||
))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.flex_1()
|
||||
.overflow_hidden()
|
||||
.border_1()
|
||||
.border_color(rgb(0xE0E0E0))
|
||||
.rounded_sm()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.w_full()
|
||||
.overflow_hidden()
|
||||
.border_b_1()
|
||||
.border_color(rgb(0xE0E0E0))
|
||||
.text_color(rgb(0x555555))
|
||||
.bg(rgb(0xF0F0F0))
|
||||
.py_1()
|
||||
.px_2()
|
||||
.text_xs()
|
||||
.children(FIELDS.map(|(key, width)| {
|
||||
div()
|
||||
.whitespace_nowrap()
|
||||
.flex_shrink_0()
|
||||
.truncate()
|
||||
.px_1()
|
||||
.w(px(width))
|
||||
.child(key.replace("_", " ").to_uppercase())
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.relative()
|
||||
.size_full()
|
||||
.child(
|
||||
uniform_list(
|
||||
"items",
|
||||
self.quotes.len(),
|
||||
cx.processor(move |this, range: Range<usize>, _, _| {
|
||||
this.visible_range = range.clone();
|
||||
let mut items = Vec::with_capacity(range.end - range.start);
|
||||
for i in range {
|
||||
if let Some(quote) = this.quotes.get(i) {
|
||||
items.push(TableRow::new(i, quote.clone()));
|
||||
}
|
||||
}
|
||||
items
|
||||
}),
|
||||
)
|
||||
.size_full()
|
||||
.track_scroll(&self.scroll_handle),
|
||||
)
|
||||
.child(self.render_scrollbar(window, cx)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
focus: true,
|
||||
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
|
||||
None,
|
||||
size(px(1280.0), px(1000.0)),
|
||||
cx,
|
||||
))),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| {
|
||||
cx.new(|_| {
|
||||
let mut table = DataTable::new();
|
||||
table.generate();
|
||||
table
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, Half, Hsla, Pixels, Point, Window, WindowBounds, WindowOptions, div,
|
||||
prelude::*, px, rgb, size,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct DragInfo {
|
||||
ix: usize,
|
||||
color: Hsla,
|
||||
position: Point<Pixels>,
|
||||
}
|
||||
|
||||
impl DragInfo {
|
||||
fn new(ix: usize, color: Hsla) -> Self {
|
||||
Self {
|
||||
ix,
|
||||
color,
|
||||
position: Point::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn position(mut self, pos: Point<Pixels>) -> Self {
|
||||
self.position = pos;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for DragInfo {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
|
||||
let size = gpui::size(px(120.), px(50.));
|
||||
|
||||
div()
|
||||
.pl(self.position.x - size.width.half())
|
||||
.pt(self.position.y - size.height.half())
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.w(size.width)
|
||||
.h(size.height)
|
||||
.bg(self.color.opacity(0.5))
|
||||
.text_color(gpui::white())
|
||||
.text_xs()
|
||||
.shadow_md()
|
||||
.child(format!("Item {}", self.ix)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct DragDrop {
|
||||
drop_on: Option<DragInfo>,
|
||||
}
|
||||
|
||||
impl DragDrop {
|
||||
fn new() -> Self {
|
||||
Self { drop_on: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for DragDrop {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let items = [gpui::blue(), gpui::red(), gpui::green()];
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_5()
|
||||
.bg(gpui::white())
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.text_color(rgb(0x333333))
|
||||
.child(div().text_xl().text_center().child("Drop & Drop"))
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.mb_10()
|
||||
.justify_center()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.gap_4()
|
||||
.items_center()
|
||||
.children(items.into_iter().enumerate().map(|(ix, color)| {
|
||||
let drag_info = DragInfo::new(ix, color);
|
||||
|
||||
div()
|
||||
.id(("item", ix))
|
||||
.size_32()
|
||||
.flex()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.border_2()
|
||||
.border_color(color)
|
||||
.text_color(color)
|
||||
.cursor_move()
|
||||
.hover(|this| this.bg(color.opacity(0.2)))
|
||||
.child(format!("Item ({})", ix))
|
||||
.on_drag(drag_info, |info: &DragInfo, position, _, cx| {
|
||||
cx.new(|_| info.position(position))
|
||||
})
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("drop-target")
|
||||
.w_128()
|
||||
.h_32()
|
||||
.flex()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.border_3()
|
||||
.border_color(self.drop_on.map(|info| info.color).unwrap_or(gpui::black()))
|
||||
.when_some(self.drop_on, |this, info| this.bg(info.color.opacity(0.5)))
|
||||
.on_drop(cx.listener(|this, info: &DragInfo, _, _| {
|
||||
this.drop_on = Some(*info);
|
||||
}))
|
||||
.child("Drop items here"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
let bounds = Bounds::centered(None, size(px(800.), px(600.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| cx.new(|_| DragDrop::new()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#[cfg(target_family = "wasm")]
|
||||
use std::borrow::Cow;
|
||||
|
||||
use gpui::App;
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub fn load_fonts(cx: &App) -> bool {
|
||||
let fonts = [
|
||||
Cow::Borrowed(
|
||||
include_bytes!("../../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf")
|
||||
.as_slice(),
|
||||
),
|
||||
Cow::Borrowed(
|
||||
include_bytes!("../../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf")
|
||||
.as_slice(),
|
||||
),
|
||||
Cow::Borrowed(
|
||||
include_bytes!("../../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBold.ttf")
|
||||
.as_slice(),
|
||||
),
|
||||
Cow::Borrowed(
|
||||
include_bytes!("../../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBoldItalic.ttf")
|
||||
.as_slice(),
|
||||
),
|
||||
Cow::Borrowed(
|
||||
include_bytes!("../../../../assets/fonts/lilex/Lilex-Regular.ttf").as_slice(),
|
||||
),
|
||||
Cow::Borrowed(include_bytes!("../../../../assets/fonts/lilex/Lilex-Bold.ttf").as_slice()),
|
||||
Cow::Borrowed(include_bytes!("../../../../assets/fonts/lilex/Lilex-Italic.ttf").as_slice()),
|
||||
Cow::Borrowed(
|
||||
include_bytes!("../../../../assets/fonts/lilex/Lilex-BoldItalic.ttf").as_slice(),
|
||||
),
|
||||
];
|
||||
if let Err(error) = cx.text_system().add_fonts(fonts.into()) {
|
||||
web_sys::console::error_1(&format!("failed to load application fonts: {error:#}").into());
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub fn load_fonts(_cx: &App) -> bool {
|
||||
true
|
||||
}
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, Div, ElementId, FocusHandle, KeyBinding, SharedString, Stateful, Window,
|
||||
WindowBounds, WindowOptions, actions, div, prelude::*, px, size,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
actions!(example, [Tab, TabPrev, Quit]);
|
||||
|
||||
struct Example {
|
||||
focus_handle: FocusHandle,
|
||||
items: Vec<(FocusHandle, &'static str)>,
|
||||
message: SharedString,
|
||||
}
|
||||
|
||||
impl Example {
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let items = vec![
|
||||
(
|
||||
cx.focus_handle().tab_index(1).tab_stop(true),
|
||||
"Button with .focus() - always shows border when focused",
|
||||
),
|
||||
(
|
||||
cx.focus_handle().tab_index(2).tab_stop(true),
|
||||
"Button with .focus_visible() - only shows border with keyboard",
|
||||
),
|
||||
(
|
||||
cx.focus_handle().tab_index(3).tab_stop(true),
|
||||
"Button with both .focus() and .focus_visible()",
|
||||
),
|
||||
];
|
||||
|
||||
let focus_handle = cx.focus_handle();
|
||||
window.focus(&focus_handle, cx);
|
||||
|
||||
Self {
|
||||
focus_handle,
|
||||
items,
|
||||
message: SharedString::from(
|
||||
"Try clicking vs tabbing! Click shows no border, Tab shows border.",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
|
||||
window.focus_next(cx);
|
||||
self.message = SharedString::from("Pressed Tab - focus-visible border should appear!");
|
||||
}
|
||||
|
||||
fn on_tab_prev(&mut self, _: &TabPrev, window: &mut Window, cx: &mut Context<Self>) {
|
||||
window.focus_prev(cx);
|
||||
self.message =
|
||||
SharedString::from("Pressed Shift-Tab - focus-visible border should appear!");
|
||||
}
|
||||
|
||||
fn on_quit(&mut self, _: &Quit, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.quit();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Example {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn button_base(id: impl Into<ElementId>, label: &'static str) -> Stateful<Div> {
|
||||
div()
|
||||
.id(id)
|
||||
.h_16()
|
||||
.w_full()
|
||||
.flex()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.bg(gpui::rgb(0x2563eb))
|
||||
.text_color(gpui::white())
|
||||
.rounded_md()
|
||||
.cursor_pointer()
|
||||
.hover(|style| style.bg(gpui::rgb(0x1d4ed8)))
|
||||
.child(label)
|
||||
}
|
||||
|
||||
div()
|
||||
.id("app")
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action(cx.listener(Self::on_tab))
|
||||
.on_action(cx.listener(Self::on_tab_prev))
|
||||
.on_action(cx.listener(Self::on_quit))
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.p_8()
|
||||
.gap_6()
|
||||
.bg(gpui::rgb(0xf3f4f6))
|
||||
.child(
|
||||
div()
|
||||
.text_2xl()
|
||||
.font_weight(gpui::FontWeight::BOLD)
|
||||
.text_color(gpui::rgb(0x111827))
|
||||
.child("CSS focus-visible Demo"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.p_4()
|
||||
.rounded_md()
|
||||
.bg(gpui::rgb(0xdbeafe))
|
||||
.text_color(gpui::rgb(0x1e3a8a))
|
||||
.child(self.message.clone()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_4()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.font_weight(gpui::FontWeight::BOLD)
|
||||
.text_color(gpui::rgb(0x374151))
|
||||
.child("1. Regular .focus() - always visible:"),
|
||||
)
|
||||
.child(
|
||||
button_base("button1", self.items[0].1)
|
||||
.track_focus(&self.items[0].0)
|
||||
.focus(|style| {
|
||||
style.border_4().border_color(gpui::rgb(0xfbbf24))
|
||||
})
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.message =
|
||||
"Clicked button 1 - focus border is visible!".into();
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.font_weight(gpui::FontWeight::BOLD)
|
||||
.text_color(gpui::rgb(0x374151))
|
||||
.child("2. New .focus_visible() - only keyboard:"),
|
||||
)
|
||||
.child(
|
||||
button_base("button2", self.items[1].1)
|
||||
.track_focus(&self.items[1].0)
|
||||
.focus_visible(|style| {
|
||||
style.border_4().border_color(gpui::rgb(0x10b981))
|
||||
})
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.message =
|
||||
"Clicked button 2 - no border! Try Tab instead.".into();
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.font_weight(gpui::FontWeight::BOLD)
|
||||
.text_color(gpui::rgb(0x374151))
|
||||
.child(
|
||||
"3. Both .focus() (yellow) and .focus_visible() (green):",
|
||||
),
|
||||
)
|
||||
.child(
|
||||
button_base("button3", self.items[2].1)
|
||||
.track_focus(&self.items[2].0)
|
||||
.focus(|style| {
|
||||
style.border_4().border_color(gpui::rgb(0xfbbf24))
|
||||
})
|
||||
.focus_visible(|style| {
|
||||
style.border_4().border_color(gpui::rgb(0x10b981))
|
||||
})
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.message =
|
||||
"Clicked button 3 - yellow border. Tab shows green!"
|
||||
.into();
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("tab", Tab, None),
|
||||
KeyBinding::new("shift-tab", TabPrev, None),
|
||||
KeyBinding::new("cmd-q", Quit, None),
|
||||
]);
|
||||
|
||||
let bounds = Bounds::centered(None, size(px(800.), px(600.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| cx.new(|cx| Example::new(window, cx)),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{App, Context, Render, Window, WindowOptions, div, img, prelude::*};
|
||||
use gpui_platform::application;
|
||||
use std::path::PathBuf;
|
||||
|
||||
struct GifViewer {
|
||||
gif_path: PathBuf,
|
||||
}
|
||||
|
||||
impl GifViewer {
|
||||
fn new(gif_path: PathBuf) -> Self {
|
||||
Self { gif_path }
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for GifViewer {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().size_full().child(
|
||||
img(self.gif_path.clone())
|
||||
.size_full()
|
||||
.object_fit(gpui::ObjectFit::Contain)
|
||||
.id("gif"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
let gif_path =
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples/image/black-cat-typing.gif");
|
||||
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
focus: true,
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| cx.new(|_| GifViewer::new(gif_path)),
|
||||
)
|
||||
.unwrap();
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
Vendored
+278
@@ -0,0 +1,278 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, ColorSpace, Context, Half, Render, Window, WindowOptions, canvas, div,
|
||||
linear_color_stop, linear_gradient, point, prelude::*, px, size,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
struct GradientViewer {
|
||||
color_space: ColorSpace,
|
||||
}
|
||||
|
||||
impl GradientViewer {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
color_space: ColorSpace::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for GradientViewer {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let color_space = self.color_space;
|
||||
|
||||
div()
|
||||
.bg(gpui::white())
|
||||
.size_full()
|
||||
.p_4()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child("Gradient Examples")
|
||||
.child(
|
||||
div().flex().gap_2().items_center().child(
|
||||
div()
|
||||
.id("method")
|
||||
.flex()
|
||||
.px_3()
|
||||
.py_1()
|
||||
.text_sm()
|
||||
.bg(gpui::black())
|
||||
.text_color(gpui::white())
|
||||
.child(format!("{}", color_space))
|
||||
.active(|this| this.opacity(0.8))
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.color_space = match this.color_space {
|
||||
ColorSpace::Oklab => ColorSpace::Srgb,
|
||||
ColorSpace::Srgb => ColorSpace::Oklab,
|
||||
};
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.gap_3()
|
||||
.child(
|
||||
div()
|
||||
.size_full()
|
||||
.rounded_xl()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.bg(gpui::red())
|
||||
.text_color(gpui::white())
|
||||
.child("Solid Color"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.size_full()
|
||||
.rounded_xl()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.bg(gpui::blue())
|
||||
.text_color(gpui::white())
|
||||
.child("Solid Color"),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.gap_3()
|
||||
.h_24()
|
||||
.text_color(gpui::white())
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
45.,
|
||||
linear_color_stop(gpui::red(), 0.),
|
||||
linear_color_stop(gpui::blue(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
135.,
|
||||
linear_color_stop(gpui::red(), 0.),
|
||||
linear_color_stop(gpui::green(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
225.,
|
||||
linear_color_stop(gpui::green(), 0.),
|
||||
linear_color_stop(gpui::blue(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
315.,
|
||||
linear_color_stop(gpui::green(), 0.),
|
||||
linear_color_stop(gpui::yellow(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.gap_3()
|
||||
.h_24()
|
||||
.text_color(gpui::white())
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
0.,
|
||||
linear_color_stop(gpui::red(), 0.),
|
||||
linear_color_stop(gpui::white(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
90.,
|
||||
linear_color_stop(gpui::blue(), 0.),
|
||||
linear_color_stop(gpui::white(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
180.,
|
||||
linear_color_stop(gpui::green(), 0.),
|
||||
linear_color_stop(gpui::white(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
360.,
|
||||
linear_color_stop(gpui::yellow(), 0.),
|
||||
linear_color_stop(gpui::white(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
0.,
|
||||
linear_color_stop(gpui::green(), 0.05),
|
||||
linear_color_stop(gpui::yellow(), 0.95),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
90.,
|
||||
linear_color_stop(gpui::blue(), 0.05),
|
||||
linear_color_stop(gpui::red(), 0.95),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.gap_3()
|
||||
.child(
|
||||
div().flex().flex_1().gap_3().child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
90.,
|
||||
linear_color_stop(gpui::blue(), 0.5),
|
||||
linear_color_stop(gpui::red(), 0.5),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
180.,
|
||||
linear_color_stop(gpui::green(), 0.),
|
||||
linear_color_stop(gpui::blue(), 0.5),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
),
|
||||
)
|
||||
.child(div().h_24().child(canvas(
|
||||
move |_, _, _| {},
|
||||
move |bounds, _, window, _| {
|
||||
let size = size(bounds.size.width * 0.8, px(80.));
|
||||
let square_bounds = Bounds {
|
||||
origin: point(
|
||||
bounds.size.width.half() - size.width.half(),
|
||||
bounds.origin.y,
|
||||
),
|
||||
size,
|
||||
};
|
||||
let height = square_bounds.size.height;
|
||||
let horizontal_offset = height;
|
||||
let vertical_offset = px(30.);
|
||||
let mut builder = gpui::PathBuilder::fill();
|
||||
builder.move_to(square_bounds.bottom_left());
|
||||
builder
|
||||
.line_to(square_bounds.origin + point(horizontal_offset, vertical_offset));
|
||||
builder.line_to(
|
||||
square_bounds.top_right() + point(-horizontal_offset, vertical_offset),
|
||||
);
|
||||
|
||||
builder.line_to(square_bounds.bottom_right());
|
||||
builder.line_to(square_bounds.bottom_left());
|
||||
let path = builder.build().unwrap();
|
||||
window.paint_path(
|
||||
path,
|
||||
linear_gradient(
|
||||
180.,
|
||||
linear_color_stop(gpui::red(), 0.),
|
||||
linear_color_stop(gpui::blue(), 1.),
|
||||
)
|
||||
.color_space(color_space),
|
||||
);
|
||||
},
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
focus: true,
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| cx.new(|_| GradientViewer::new()),
|
||||
)
|
||||
.unwrap();
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, Hsla, Window, WindowBounds, WindowOptions, container_query, div,
|
||||
prelude::*, px, rgb, size,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
// https://en.wikipedia.org/wiki/Holy_grail_(web_design)
|
||||
//
|
||||
// Resize the window: the layout is chosen by `container_query` based on the
|
||||
// measured size of the container, collapsing to a single stacked column when
|
||||
// it becomes too narrow for the three-column grid.
|
||||
struct HolyGrailExample {}
|
||||
|
||||
impl Render for HolyGrailExample {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
container_query(|container_size, _window, _cx| {
|
||||
let block = |color: Hsla| {
|
||||
div()
|
||||
.size_full()
|
||||
.bg(color)
|
||||
.border_1()
|
||||
.border_dashed()
|
||||
.rounded_md()
|
||||
.border_color(gpui::white())
|
||||
.items_center()
|
||||
};
|
||||
|
||||
let header = block(gpui::white()).child(format!("Header — {}", container_size.width));
|
||||
let table_of_contents = block(gpui::red()).child("Table of contents");
|
||||
let content = block(gpui::green()).child("Content");
|
||||
let ad = block(gpui::blue()).child("AD :(").text_color(gpui::white());
|
||||
let footer = block(gpui::black())
|
||||
.text_color(gpui::white())
|
||||
.child("Footer");
|
||||
|
||||
let container = div().gap_1().bg(rgb(0x505050)).shadow_lg().size_full();
|
||||
|
||||
if container_size.width < px(400.) {
|
||||
container
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(header.h_12().flex_none())
|
||||
.child(table_of_contents.h_20().flex_none())
|
||||
.child(content.flex_1())
|
||||
.child(ad.h_20().flex_none())
|
||||
.child(footer.h_12().flex_none())
|
||||
} else {
|
||||
container
|
||||
.grid()
|
||||
.grid_cols(5)
|
||||
.grid_rows(5)
|
||||
.child(header.row_span(1).col_span_full())
|
||||
.child(table_of_contents.col_span(1).h_56())
|
||||
.child(content.col_span(3).row_span(3))
|
||||
.child(ad.col_span(1).row_span(3))
|
||||
.child(footer.row_span(1).col_span_full())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| cx.new(|_| HolyGrailExample {}),
|
||||
)
|
||||
.unwrap();
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, SharedString, Window, WindowBounds, WindowOptions, div, prelude::*, px,
|
||||
rgb, size,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
struct HelloWorld {
|
||||
text: SharedString,
|
||||
}
|
||||
|
||||
impl Render for HelloWorld {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.bg(rgb(0x505050))
|
||||
.size(px(500.0))
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.shadow_lg()
|
||||
.border_1()
|
||||
.border_color(rgb(0x0000ff))
|
||||
.text_xl()
|
||||
.text_color(rgb(0xffffff))
|
||||
.child(format!("Hello, {}!", self.text))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.size_8()
|
||||
.bg(gpui::red())
|
||||
.border_1()
|
||||
.border_dashed()
|
||||
.rounded_md()
|
||||
.border_color(gpui::white()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.size_8()
|
||||
.bg(gpui::green())
|
||||
.border_1()
|
||||
.border_dashed()
|
||||
.rounded_md()
|
||||
.border_color(gpui::white()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.size_8()
|
||||
.bg(gpui::blue())
|
||||
.border_1()
|
||||
.border_dashed()
|
||||
.rounded_md()
|
||||
.border_color(gpui::white()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.size_8()
|
||||
.bg(gpui::yellow())
|
||||
.border_1()
|
||||
.border_dashed()
|
||||
.rounded_md()
|
||||
.border_color(gpui::white()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.size_8()
|
||||
.bg(gpui::black())
|
||||
.border_1()
|
||||
.border_dashed()
|
||||
.rounded_md()
|
||||
.rounded_md()
|
||||
.border_color(gpui::white()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.size_8()
|
||||
.bg(gpui::white())
|
||||
.border_1()
|
||||
.border_dashed()
|
||||
.rounded_md()
|
||||
.border_color(gpui::black()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| {
|
||||
cx.new(|_| HelloWorld {
|
||||
text: "World".into(),
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 164 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 8C3 6.67392 3.52678 5.40215 4.46446 4.46447C5.40214 3.52679 6.67391 3.00001 7.99999 3.00001C9.39779 3.00527 10.7394 3.55069 11.7444 4.52223L13 5.77778" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13 3.00001V5.77778H10.2222" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13 8C13 9.32608 12.4732 10.5978 11.5355 11.5355C10.5978 12.4732 9.32607 13 7.99999 13C6.60219 12.9947 5.26054 12.4493 4.25555 11.4778L3 10.2222" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5.77777 10.2222H3V13" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 748 B |
Binary file not shown.
|
After Width: | Height: | Size: 4.3 MiB |
+13
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="480" viewBox="0 0 480 480" fill="none"
|
||||
stroke="currentColor" stroke-width="40" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="lucide lucide-grip">
|
||||
<circle cx="240" cy="240" r="50" stroke="#44403c" />
|
||||
<circle cx="100" cy="100" r="20" stroke="#65a30d" />
|
||||
<circle cx="240" cy="100" r="30" stroke="#dc2626" />
|
||||
<circle cx="380" cy="100" r="20" stroke="#d97706" />
|
||||
<circle cx="380" cy="240" r="30" stroke="#06b6d4" />
|
||||
<circle cx="100" cy="240" r="30" stroke="#3b82f666" />
|
||||
<circle cx="240" cy="380" r="30" stroke="#7c3aed" />
|
||||
<circle cx="380" cy="380" r="20" stroke="#c026d3" />
|
||||
<circle cx="100" cy="380" r="20" stroke="#e11d4866" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 746 B |
Binary file not shown.
|
After Width: | Height: | Size: 378 B |
+223
@@ -0,0 +1,223 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "../example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::{
|
||||
App, AppContext, AssetSource, Bounds, Context, ImageSource, KeyBinding, Menu, MenuItem, Point,
|
||||
SharedString, SharedUri, TitlebarOptions, Window, WindowBounds, WindowOptions, actions, div,
|
||||
img, prelude::*, px, rgb, size,
|
||||
};
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use reqwest_client::ReqwestClient;
|
||||
|
||||
struct Assets {
|
||||
base: PathBuf,
|
||||
}
|
||||
|
||||
impl AssetSource for Assets {
|
||||
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
|
||||
fs::read(self.base.join(path))
|
||||
.map(|data| Some(std::borrow::Cow::Owned(data)))
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
|
||||
fs::read_dir(self.base.join(path))
|
||||
.map(|entries| {
|
||||
entries
|
||||
.filter_map(|entry| {
|
||||
entry
|
||||
.ok()
|
||||
.and_then(|entry| entry.file_name().into_string().ok())
|
||||
.map(SharedString::from)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct ImageContainer {
|
||||
text: SharedString,
|
||||
src: ImageSource,
|
||||
}
|
||||
|
||||
impl ImageContainer {
|
||||
pub fn new(text: impl Into<SharedString>, src: impl Into<ImageSource>) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
src: src.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ImageContainer {
|
||||
fn render(self, _window: &mut Window, _: &mut App) -> impl IntoElement {
|
||||
div().child(
|
||||
div()
|
||||
.flex_row()
|
||||
.size_full()
|
||||
.gap_4()
|
||||
.child(self.text)
|
||||
.child(img(self.src).size(px(256.0))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct ImageShowcase {
|
||||
local_resource: Arc<std::path::Path>,
|
||||
remote_resource: SharedUri,
|
||||
asset_resource: SharedString,
|
||||
}
|
||||
|
||||
impl Render for ImageShowcase {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.id("main")
|
||||
.bg(gpui::white())
|
||||
.overflow_y_scroll()
|
||||
.p_5()
|
||||
.size_full()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.gap_8()
|
||||
.child(img(
|
||||
"https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg",
|
||||
))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.gap_8()
|
||||
.child(ImageContainer::new(
|
||||
"Image loaded from a local file with EXIF orientation",
|
||||
self.local_resource.clone(),
|
||||
))
|
||||
.child(ImageContainer::new(
|
||||
"Image loaded from a remote resource",
|
||||
self.remote_resource.clone(),
|
||||
))
|
||||
.child(ImageContainer::new(
|
||||
"Image loaded from an asset",
|
||||
self.asset_resource.clone(),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.gap_8()
|
||||
.child(
|
||||
div()
|
||||
.flex_col()
|
||||
.child("Auto Width")
|
||||
.child(img("https://picsum.photos/800/400").h(px(180.))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_col()
|
||||
.child("Auto Height")
|
||||
.child(img("https://picsum.photos/800/400").w(px(180.))),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.w_full()
|
||||
.border_1()
|
||||
.border_color(rgb(0xC0C0C0))
|
||||
.child("image with max width 100%")
|
||||
.child(img("https://picsum.photos/800/400").max_w_full()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
actions!(image, [Quit]);
|
||||
|
||||
fn run_example() {
|
||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
let app = gpui_platform::application();
|
||||
#[cfg(target_family = "wasm")]
|
||||
let app = gpui_platform::application();
|
||||
app.with_assets(Assets {
|
||||
base: manifest_dir.join("examples"),
|
||||
})
|
||||
.run(move |cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
let http_client = ReqwestClient::user_agent("gpui example").unwrap();
|
||||
cx.set_http_client(Arc::new(http_client));
|
||||
}
|
||||
|
||||
cx.activate(true);
|
||||
cx.on_action(|_: &Quit, cx| cx.quit());
|
||||
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
|
||||
cx.set_menus(vec![Menu {
|
||||
name: "Image".into(),
|
||||
items: vec![MenuItem::action("Quit", Quit)],
|
||||
disabled: false,
|
||||
}]);
|
||||
|
||||
let window_options = WindowOptions {
|
||||
titlebar: Some(TitlebarOptions {
|
||||
title: Some(SharedString::from("Image Example")),
|
||||
appears_transparent: false,
|
||||
..Default::default()
|
||||
}),
|
||||
|
||||
window_bounds: Some(WindowBounds::Windowed(Bounds {
|
||||
size: size(px(1100.), px(600.)),
|
||||
origin: Point::new(px(200.), px(200.)),
|
||||
})),
|
||||
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
cx.open_window(window_options, |_, cx| {
|
||||
cx.new(|_| ImageShowcase {
|
||||
// Relative path to your root project path
|
||||
local_resource: manifest_dir
|
||||
.join("examples/image/exif-orientation-rotate-180.jpg")
|
||||
.into(),
|
||||
remote_resource: "https://picsum.photos/800/400".into(),
|
||||
asset_resource: "image/color.svg".into(),
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
+299
@@ -0,0 +1,299 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{
|
||||
App, AppContext, Bounds, ClickEvent, Context, ElementId, Entity, ImageCache,
|
||||
ImageCacheProvider, KeyBinding, Menu, MenuItem, RetainAllImageCache, SharedString,
|
||||
TitlebarOptions, Window, WindowBounds, WindowOptions, actions, div, hash, image_cache, img,
|
||||
prelude::*, px, rgb, size,
|
||||
};
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use reqwest_client::ReqwestClient;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
const IMAGES_IN_GALLERY: usize = 30;
|
||||
|
||||
struct ImageGallery {
|
||||
image_key: String,
|
||||
items_count: usize,
|
||||
total_count: usize,
|
||||
image_cache: Entity<RetainAllImageCache>,
|
||||
}
|
||||
|
||||
impl ImageGallery {
|
||||
fn on_next_image(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.image_cache
|
||||
.update(cx, |image_cache, cx| image_cache.clear(window, cx));
|
||||
|
||||
let t = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis();
|
||||
|
||||
self.image_key = format!("{}", t);
|
||||
self.total_count += self.items_count;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ImageGallery {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let image_url: SharedString =
|
||||
format!("https://picsum.photos/400/200?t={}", self.image_key).into();
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.text_color(gpui::white())
|
||||
.child("Manually managed image cache:")
|
||||
.child(
|
||||
div()
|
||||
.image_cache(self.image_cache.clone())
|
||||
.id("main")
|
||||
.text_color(gpui::black())
|
||||
.bg(rgb(0xE9E9E9))
|
||||
.overflow_y_scroll()
|
||||
.p_4()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_between()
|
||||
.child(format!(
|
||||
"Example to show images and test memory usage (Rendered: {} images).",
|
||||
self.total_count
|
||||
))
|
||||
.child(
|
||||
div()
|
||||
.id("btn")
|
||||
.py_1()
|
||||
.px_4()
|
||||
.bg(gpui::black())
|
||||
.hover(|this| this.opacity(0.8))
|
||||
.text_color(gpui::white())
|
||||
.text_center()
|
||||
.w_40()
|
||||
.child("Next Photos")
|
||||
.on_click(cx.listener(Self::on_next_image)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("image-gallery")
|
||||
.flex()
|
||||
.flex_row()
|
||||
.flex_wrap()
|
||||
.gap_x_4()
|
||||
.gap_y_2()
|
||||
.justify_around()
|
||||
.children(
|
||||
(0..self.items_count)
|
||||
.map(|ix| img(format!("{}-{}", image_url, ix)).size_20()),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
"Automatically managed image cache:"
|
||||
)
|
||||
.child(image_cache(simple_lru_cache("lru-cache", IMAGES_IN_GALLERY)).child(
|
||||
div()
|
||||
.id("main")
|
||||
.bg(rgb(0xE9E9E9))
|
||||
.text_color(gpui::black())
|
||||
.overflow_y_scroll()
|
||||
.p_4()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.id("image-gallery")
|
||||
.flex()
|
||||
.flex_row()
|
||||
.flex_wrap()
|
||||
.gap_x_4()
|
||||
.gap_y_2()
|
||||
.justify_around()
|
||||
.children(
|
||||
(0..self.items_count)
|
||||
.map(|ix| img(format!("{}-{}", image_url, ix)).size_20()),
|
||||
),
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn simple_lru_cache(id: impl Into<ElementId>, max_items: usize) -> SimpleLruCacheProvider {
|
||||
SimpleLruCacheProvider {
|
||||
id: id.into(),
|
||||
max_items,
|
||||
}
|
||||
}
|
||||
|
||||
struct SimpleLruCacheProvider {
|
||||
id: ElementId,
|
||||
max_items: usize,
|
||||
}
|
||||
|
||||
impl ImageCacheProvider for SimpleLruCacheProvider {
|
||||
fn provide(&mut self, window: &mut Window, cx: &mut App) -> gpui::AnyImageCache {
|
||||
window
|
||||
.with_global_id(self.id.clone(), |global_id, window| {
|
||||
window.with_element_state::<Entity<SimpleLruCache>, _>(
|
||||
global_id,
|
||||
|lru_cache, _window| {
|
||||
let mut lru_cache = lru_cache.unwrap_or_else(|| {
|
||||
cx.new(|cx| SimpleLruCache::new(self.max_items, cx))
|
||||
});
|
||||
if lru_cache.read(cx).max_items != self.max_items {
|
||||
lru_cache = cx.new(|cx| SimpleLruCache::new(self.max_items, cx));
|
||||
}
|
||||
(lru_cache.clone(), lru_cache)
|
||||
},
|
||||
)
|
||||
})
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
struct SimpleLruCache {
|
||||
max_items: usize,
|
||||
usages: Vec<u64>,
|
||||
cache: HashMap<u64, gpui::ImageCacheItem>,
|
||||
}
|
||||
|
||||
impl SimpleLruCache {
|
||||
fn new(max_items: usize, cx: &mut Context<Self>) -> Self {
|
||||
cx.on_release(|simple_cache, cx| {
|
||||
for (_, item) in std::mem::take(&mut simple_cache.cache) {
|
||||
if let Some(Ok(image)) = item.get() {
|
||||
cx.drop_image(image, None);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
max_items,
|
||||
usages: Vec::with_capacity(max_items),
|
||||
cache: HashMap::with_capacity(max_items),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ImageCache for SimpleLruCache {
|
||||
fn load(
|
||||
&mut self,
|
||||
resource: &gpui::Resource,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<Result<Arc<gpui::RenderImage>, gpui::ImageCacheError>> {
|
||||
assert_eq!(self.usages.len(), self.cache.len());
|
||||
assert!(self.cache.len() <= self.max_items);
|
||||
|
||||
let hash = hash(resource);
|
||||
|
||||
if let Some(item) = self.cache.get_mut(&hash) {
|
||||
let current_ix = self
|
||||
.usages
|
||||
.iter()
|
||||
.position(|item| *item == hash)
|
||||
.expect("cache and usages must stay in sync");
|
||||
self.usages.remove(current_ix);
|
||||
self.usages.insert(0, hash);
|
||||
|
||||
return item.use_image(window);
|
||||
}
|
||||
|
||||
if self.usages.len() == self.max_items {
|
||||
let oldest = self.usages.pop().unwrap();
|
||||
let image = self
|
||||
.cache
|
||||
.remove(&oldest)
|
||||
.expect("cache and usages must be in sync");
|
||||
if let Some(Ok(image)) = image.get() {
|
||||
cx.drop_image(image, Some(window));
|
||||
}
|
||||
}
|
||||
let item = gpui::ImageCacheItem::new(resource, cx);
|
||||
let result = item.use_image(window);
|
||||
self.cache.insert(hash, item);
|
||||
self.usages.insert(0, hash);
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
actions!(image, [Quit]);
|
||||
|
||||
fn run_example() {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
let app = gpui_platform::application();
|
||||
#[cfg(target_family = "wasm")]
|
||||
let app = gpui_platform::single_threaded_web();
|
||||
|
||||
app.run(move |cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
let http_client = ReqwestClient::user_agent("gpui example").unwrap();
|
||||
cx.set_http_client(Arc::new(http_client));
|
||||
}
|
||||
|
||||
cx.activate(true);
|
||||
cx.on_action(|_: &Quit, cx| cx.quit());
|
||||
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
|
||||
cx.set_menus([Menu::new("Image Gallery").items([MenuItem::action("Quit", Quit)])]);
|
||||
|
||||
let window_options = WindowOptions {
|
||||
titlebar: Some(TitlebarOptions {
|
||||
title: Some(SharedString::from("Image Gallery")),
|
||||
appears_transparent: false,
|
||||
..Default::default()
|
||||
}),
|
||||
|
||||
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
|
||||
None,
|
||||
size(px(1100.), px(860.)),
|
||||
cx,
|
||||
))),
|
||||
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
cx.open_window(window_options, |_, cx| {
|
||||
cx.new(|ctx| ImageGallery {
|
||||
image_key: "".into(),
|
||||
items_count: IMAGES_IN_GALLERY,
|
||||
total_count: 0,
|
||||
image_cache: RetainAllImageCache::new(ctx),
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use std::{path::Path, sync::Arc, time::Duration};
|
||||
|
||||
use gpui::{
|
||||
Animation, AnimationExt, App, 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,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
struct Assets {}
|
||||
|
||||
impl AssetSource for Assets {
|
||||
fn load(&self, path: &str) -> anyhow::Result<Option<std::borrow::Cow<'static, [u8]>>> {
|
||||
std::fs::read(path)
|
||||
.map(Into::into)
|
||||
.map_err(Into::into)
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
fn list(&self, path: &str) -> anyhow::Result<Vec<SharedString>> {
|
||||
Ok(std::fs::read_dir(path)?
|
||||
.filter_map(|entry| {
|
||||
Some(SharedString::from(
|
||||
entry.ok()?.path().to_string_lossy().into_owned(),
|
||||
))
|
||||
})
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
}
|
||||
|
||||
const IMAGE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/image/app-icon.png");
|
||||
|
||||
#[derive(Copy, Clone, Hash)]
|
||||
struct LoadImageParameters {
|
||||
timeout: Duration,
|
||||
fail: bool,
|
||||
}
|
||||
|
||||
struct LoadImageWithParameters {}
|
||||
|
||||
impl Asset for LoadImageWithParameters {
|
||||
type Source = LoadImageParameters;
|
||||
|
||||
type Output = Result<Arc<RenderImage>, ImageCacheError>;
|
||||
|
||||
fn load(
|
||||
parameters: Self::Source,
|
||||
cx: &mut App,
|
||||
) -> impl std::future::Future<Output = Self::Output> + Send + 'static {
|
||||
let timer = cx.background_executor().timer(parameters.timeout);
|
||||
let data = AssetLogger::<ImageAssetLoader>::load(
|
||||
Resource::Path(Path::new(IMAGE).to_path_buf().into()),
|
||||
cx,
|
||||
);
|
||||
async move {
|
||||
timer.await;
|
||||
if parameters.fail {
|
||||
log::error!("Intentionally failed to load image");
|
||||
Err(anyhow::anyhow!("Failed to load image").into())
|
||||
} else {
|
||||
data.await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ImageLoadingExample {}
|
||||
|
||||
impl ImageLoadingExample {
|
||||
fn loading_element() -> impl IntoElement {
|
||||
div().size_full().flex_none().p_0p5().rounded_xs().child(
|
||||
div().size_full().with_animation(
|
||||
"loading-bg",
|
||||
Animation::new(Duration::from_secs(3))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.04, 0.24)),
|
||||
move |this, delta| this.bg(black().opacity(delta)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn fallback_element() -> impl IntoElement {
|
||||
let fallback_color: Hsla = black().opacity(0.5);
|
||||
|
||||
div().size_full().flex_none().p_0p5().child(
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.rounded_xs()
|
||||
.text_sm()
|
||||
.text_color(fallback_color)
|
||||
.border_1()
|
||||
.border_color(fallback_color)
|
||||
.child("?"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ImageLoadingExample {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().flex().flex_col().size_full().justify_around().child(
|
||||
div().flex().flex_row().w_full().justify_around().child(
|
||||
div()
|
||||
.flex()
|
||||
.bg(gpui::white())
|
||||
.size(Length::Definite(px(300.0).into()))
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.child({
|
||||
let image_source = LoadImageParameters {
|
||||
timeout: LOADING_DELAY.saturating_sub(Duration::from_millis(25)),
|
||||
fail: false,
|
||||
};
|
||||
|
||||
// Load within the 'loading delay', should not show loading fallback
|
||||
img(move |window: &mut Window, cx: &mut App| {
|
||||
window.use_asset::<LoadImageWithParameters>(&image_source, cx)
|
||||
})
|
||||
.id("image-1")
|
||||
.border_1()
|
||||
.size_12()
|
||||
.with_fallback(|| Self::fallback_element().into_any_element())
|
||||
.border_color(red())
|
||||
.with_loading(|| Self::loading_element().into_any_element())
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.remove_asset::<LoadImageWithParameters>(&image_source);
|
||||
})
|
||||
})
|
||||
.child({
|
||||
// Load after a long delay
|
||||
let image_source = LoadImageParameters {
|
||||
timeout: Duration::from_secs(5),
|
||||
fail: false,
|
||||
};
|
||||
|
||||
img(move |window: &mut Window, cx: &mut App| {
|
||||
window.use_asset::<LoadImageWithParameters>(&image_source, cx)
|
||||
})
|
||||
.id("image-2")
|
||||
.with_fallback(|| Self::fallback_element().into_any_element())
|
||||
.with_loading(|| Self::loading_element().into_any_element())
|
||||
.size_12()
|
||||
.border_1()
|
||||
.border_color(red())
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.remove_asset::<LoadImageWithParameters>(&image_source);
|
||||
})
|
||||
})
|
||||
.child({
|
||||
// Fail to load image after a long delay
|
||||
let image_source = LoadImageParameters {
|
||||
timeout: Duration::from_secs(5),
|
||||
fail: true,
|
||||
};
|
||||
|
||||
// Fail to load after a long delay
|
||||
img(move |window: &mut Window, cx: &mut App| {
|
||||
window.use_asset::<LoadImageWithParameters>(&image_source, cx)
|
||||
})
|
||||
.id("image-3")
|
||||
.with_fallback(|| Self::fallback_element().into_any_element())
|
||||
.with_loading(|| Self::loading_element().into_any_element())
|
||||
.size_12()
|
||||
.border_1()
|
||||
.border_color(red())
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.remove_asset::<LoadImageWithParameters>(&image_source);
|
||||
})
|
||||
})
|
||||
.child({
|
||||
// Ensure that the normal image loader doesn't spam logs
|
||||
let image_source = Path::new(
|
||||
"this/file/really/shouldn't/exist/or/won't/be/an/image/I/hope",
|
||||
)
|
||||
.to_path_buf();
|
||||
img(image_source.clone())
|
||||
.id("image-4")
|
||||
.border_1()
|
||||
.size_12()
|
||||
.with_fallback(|| Self::fallback_element().into_any_element())
|
||||
.border_color(red())
|
||||
.with_loading(|| Self::loading_element().into_any_element())
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.remove_asset::<ImgResourceLoader>(&image_source.clone().into());
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().with_assets(Assets {}).run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
let options = WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
|
||||
None,
|
||||
size(px(300.), px(300.)),
|
||||
cx,
|
||||
))),
|
||||
..Default::default()
|
||||
};
|
||||
cx.open_window(options, |_, cx| {
|
||||
cx.activate(false);
|
||||
cx.new(|_| ImageLoadingExample {})
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
Vendored
+784
@@ -0,0 +1,784 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, ClipboardItem, Context, CursorStyle, ElementId, ElementInputHandler, Entity,
|
||||
EntityInputHandler, FocusHandle, Focusable, GlobalElementId, KeyBinding, Keystroke, LayoutId,
|
||||
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point,
|
||||
ShapedLine, SharedString, Style, TextRun, UTF16Selection, UnderlineStyle, Window, WindowBounds,
|
||||
WindowOptions, actions, black, div, fill, hsla, opaque_grey, point, prelude::*, px, relative,
|
||||
rgb, rgba, size, white, yellow,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
use unicode_segmentation::*;
|
||||
|
||||
actions!(
|
||||
text_input,
|
||||
[
|
||||
Backspace,
|
||||
Delete,
|
||||
Left,
|
||||
Right,
|
||||
SelectLeft,
|
||||
SelectRight,
|
||||
SelectAll,
|
||||
Home,
|
||||
End,
|
||||
ShowCharacterPalette,
|
||||
Paste,
|
||||
Cut,
|
||||
Copy,
|
||||
Quit,
|
||||
]
|
||||
);
|
||||
|
||||
struct TextInput {
|
||||
focus_handle: FocusHandle,
|
||||
content: SharedString,
|
||||
placeholder: SharedString,
|
||||
selected_range: Range<usize>,
|
||||
selection_reversed: bool,
|
||||
marked_range: Option<Range<usize>>,
|
||||
last_layout: Option<ShapedLine>,
|
||||
last_bounds: Option<Bounds<Pixels>>,
|
||||
is_selecting: bool,
|
||||
}
|
||||
|
||||
impl TextInput {
|
||||
fn left(&mut self, _: &Left, _: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.selected_range.is_empty() {
|
||||
self.move_to(self.previous_boundary(self.cursor_offset()), cx);
|
||||
} else {
|
||||
self.move_to(self.selected_range.start, cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn right(&mut self, _: &Right, _: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.selected_range.is_empty() {
|
||||
self.move_to(self.next_boundary(self.selected_range.end), cx);
|
||||
} else {
|
||||
self.move_to(self.selected_range.end, cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.select_to(self.previous_boundary(self.cursor_offset()), cx);
|
||||
}
|
||||
|
||||
fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.select_to(self.next_boundary(self.cursor_offset()), cx);
|
||||
}
|
||||
|
||||
fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.move_to(0, cx);
|
||||
self.select_to(self.content.len(), cx)
|
||||
}
|
||||
|
||||
fn home(&mut self, _: &Home, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.move_to(0, cx);
|
||||
}
|
||||
|
||||
fn end(&mut self, _: &End, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.move_to(self.content.len(), cx);
|
||||
}
|
||||
|
||||
fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.selected_range.is_empty() {
|
||||
let prev = self.previous_boundary(self.cursor_offset());
|
||||
if self.cursor_offset() == prev {
|
||||
window.play_system_bell();
|
||||
return;
|
||||
}
|
||||
self.select_to(prev, cx)
|
||||
}
|
||||
self.replace_text_in_range(None, "", window, cx)
|
||||
}
|
||||
|
||||
fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.selected_range.is_empty() {
|
||||
let next = self.next_boundary(self.cursor_offset());
|
||||
if self.cursor_offset() == next {
|
||||
window.play_system_bell();
|
||||
return;
|
||||
}
|
||||
self.select_to(next, cx)
|
||||
}
|
||||
self.replace_text_in_range(None, "", window, cx)
|
||||
}
|
||||
|
||||
fn on_mouse_down(
|
||||
&mut self,
|
||||
event: &MouseDownEvent,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.is_selecting = true;
|
||||
|
||||
if event.modifiers.shift {
|
||||
self.select_to(self.index_for_mouse_position(event.position), cx);
|
||||
} else {
|
||||
self.move_to(self.index_for_mouse_position(event.position), cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn on_mouse_up(&mut self, _: &MouseUpEvent, _window: &mut Window, _: &mut Context<Self>) {
|
||||
self.is_selecting = false;
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, event: &MouseMoveEvent, _: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.is_selecting {
|
||||
self.select_to(self.index_for_mouse_position(event.position), cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn show_character_palette(
|
||||
&mut self,
|
||||
_: &ShowCharacterPalette,
|
||||
window: &mut Window,
|
||||
_: &mut Context<Self>,
|
||||
) {
|
||||
window.show_character_palette();
|
||||
}
|
||||
|
||||
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) {
|
||||
self.replace_text_in_range(None, &text.replace("\n", " "), window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
|
||||
if !self.selected_range.is_empty() {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||
self.content[self.selected_range.clone()].to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if !self.selected_range.is_empty() {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||
self.content[self.selected_range.clone()].to_string(),
|
||||
));
|
||||
self.replace_text_in_range(None, "", window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn move_to(&mut self, offset: usize, cx: &mut Context<Self>) {
|
||||
self.selected_range = offset..offset;
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
fn cursor_offset(&self) -> usize {
|
||||
if self.selection_reversed {
|
||||
self.selected_range.start
|
||||
} else {
|
||||
self.selected_range.end
|
||||
}
|
||||
}
|
||||
|
||||
fn index_for_mouse_position(&self, position: Point<Pixels>) -> usize {
|
||||
if self.content.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let (Some(bounds), Some(line)) = (self.last_bounds.as_ref(), self.last_layout.as_ref())
|
||||
else {
|
||||
return 0;
|
||||
};
|
||||
if position.y < bounds.top() {
|
||||
return 0;
|
||||
}
|
||||
if position.y > bounds.bottom() {
|
||||
return self.content.len();
|
||||
}
|
||||
line.closest_index_for_x(position.x - bounds.left())
|
||||
}
|
||||
|
||||
fn select_to(&mut self, offset: usize, cx: &mut Context<Self>) {
|
||||
if self.selection_reversed {
|
||||
self.selected_range.start = offset
|
||||
} else {
|
||||
self.selected_range.end = offset
|
||||
};
|
||||
if self.selected_range.end < self.selected_range.start {
|
||||
self.selection_reversed = !self.selection_reversed;
|
||||
self.selected_range = self.selected_range.end..self.selected_range.start;
|
||||
}
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
fn offset_from_utf16(&self, offset: usize) -> usize {
|
||||
let mut utf8_offset = 0;
|
||||
let mut utf16_count = 0;
|
||||
|
||||
for ch in self.content.chars() {
|
||||
if utf16_count >= offset {
|
||||
break;
|
||||
}
|
||||
utf16_count += ch.len_utf16();
|
||||
utf8_offset += ch.len_utf8();
|
||||
}
|
||||
|
||||
utf8_offset
|
||||
}
|
||||
|
||||
fn offset_to_utf16(&self, offset: usize) -> usize {
|
||||
let mut utf16_offset = 0;
|
||||
let mut utf8_count = 0;
|
||||
|
||||
for ch in self.content.chars() {
|
||||
if utf8_count >= offset {
|
||||
break;
|
||||
}
|
||||
utf8_count += ch.len_utf8();
|
||||
utf16_offset += ch.len_utf16();
|
||||
}
|
||||
|
||||
utf16_offset
|
||||
}
|
||||
|
||||
fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
|
||||
self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
|
||||
}
|
||||
|
||||
fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
|
||||
self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
|
||||
}
|
||||
|
||||
fn previous_boundary(&self, offset: usize) -> usize {
|
||||
self.content
|
||||
.grapheme_indices(true)
|
||||
.rev()
|
||||
.find_map(|(idx, _)| (idx < offset).then_some(idx))
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn next_boundary(&self, offset: usize) -> usize {
|
||||
self.content
|
||||
.grapheme_indices(true)
|
||||
.find_map(|(idx, _)| (idx > offset).then_some(idx))
|
||||
.unwrap_or(self.content.len())
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.content = "".into();
|
||||
self.selected_range = 0..0;
|
||||
self.selection_reversed = false;
|
||||
self.marked_range = None;
|
||||
self.last_layout = None;
|
||||
self.last_bounds = None;
|
||||
self.is_selecting = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl EntityInputHandler for TextInput {
|
||||
fn text_for_range(
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
actual_range: &mut Option<Range<usize>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<String> {
|
||||
let range = self.range_from_utf16(&range_utf16);
|
||||
actual_range.replace(self.range_to_utf16(&range));
|
||||
Some(self.content[range].to_string())
|
||||
}
|
||||
|
||||
fn selected_text_range(
|
||||
&mut self,
|
||||
_ignore_disabled_input: bool,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<UTF16Selection> {
|
||||
Some(UTF16Selection {
|
||||
range: self.range_to_utf16(&self.selected_range),
|
||||
reversed: self.selection_reversed,
|
||||
})
|
||||
}
|
||||
|
||||
fn marked_text_range(
|
||||
&self,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<Range<usize>> {
|
||||
self.marked_range
|
||||
.as_ref()
|
||||
.map(|range| self.range_to_utf16(range))
|
||||
}
|
||||
|
||||
fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
|
||||
self.marked_range = None;
|
||||
}
|
||||
|
||||
fn replace_text_in_range(
|
||||
&mut self,
|
||||
range_utf16: Option<Range<usize>>,
|
||||
new_text: &str,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let range = range_utf16
|
||||
.as_ref()
|
||||
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
||||
.or(self.marked_range.clone())
|
||||
.unwrap_or(self.selected_range.clone());
|
||||
|
||||
self.content =
|
||||
(self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
|
||||
.into();
|
||||
self.selected_range = range.start + new_text.len()..range.start + new_text.len();
|
||||
self.marked_range.take();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn replace_and_mark_text_in_range(
|
||||
&mut self,
|
||||
range_utf16: Option<Range<usize>>,
|
||||
new_text: &str,
|
||||
new_selected_range_utf16: Option<Range<usize>>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let range = range_utf16
|
||||
.as_ref()
|
||||
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
||||
.or(self.marked_range.clone())
|
||||
.unwrap_or(self.selected_range.clone());
|
||||
|
||||
self.content =
|
||||
(self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
|
||||
.into();
|
||||
if !new_text.is_empty() {
|
||||
self.marked_range = Some(range.start..range.start + new_text.len());
|
||||
} else {
|
||||
self.marked_range = None;
|
||||
}
|
||||
self.selected_range = new_selected_range_utf16
|
||||
.as_ref()
|
||||
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
||||
.map(|new_range| new_range.start + range.start..new_range.end + range.end)
|
||||
.unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn bounds_for_range(
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<Bounds<Pixels>> {
|
||||
let last_layout = self.last_layout.as_ref()?;
|
||||
let range = self.range_from_utf16(&range_utf16);
|
||||
Some(Bounds::from_corners(
|
||||
point(
|
||||
bounds.left() + last_layout.x_for_index(range.start),
|
||||
bounds.top(),
|
||||
),
|
||||
point(
|
||||
bounds.left() + last_layout.x_for_index(range.end),
|
||||
bounds.bottom(),
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
fn character_index_for_point(
|
||||
&mut self,
|
||||
point: gpui::Point<Pixels>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<usize> {
|
||||
let line_point = self.last_bounds?.localize(&point)?;
|
||||
let last_layout = self.last_layout.as_ref()?;
|
||||
|
||||
assert_eq!(last_layout.text, self.content);
|
||||
let utf8_index = last_layout.index_for_x(point.x - line_point.x)?;
|
||||
Some(self.offset_to_utf16(utf8_index))
|
||||
}
|
||||
}
|
||||
|
||||
struct TextElement {
|
||||
input: Entity<TextInput>,
|
||||
}
|
||||
|
||||
struct PrepaintState {
|
||||
line: Option<ShapedLine>,
|
||||
cursor: Option<PaintQuad>,
|
||||
selection: Option<PaintQuad>,
|
||||
}
|
||||
|
||||
impl IntoElement for TextElement {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for TextElement {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = 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<&gpui::InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let mut style = Style::default();
|
||||
style.size.width = relative(1.).into();
|
||||
style.size.height = window.line_height().into();
|
||||
(window.request_layout(style, [], cx), ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&gpui::InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self::PrepaintState {
|
||||
let input = self.input.read(cx);
|
||||
let content = input.content.clone();
|
||||
let selected_range = input.selected_range.clone();
|
||||
let cursor = input.cursor_offset();
|
||||
let style = window.text_style();
|
||||
|
||||
let (display_text, text_color) = if content.is_empty() {
|
||||
(input.placeholder.clone(), hsla(0., 0., 0., 0.2))
|
||||
} else {
|
||||
(content, style.color)
|
||||
};
|
||||
|
||||
let run = TextRun {
|
||||
len: display_text.len(),
|
||||
font: style.font(),
|
||||
color: text_color,
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
};
|
||||
let runs = if let Some(marked_range) = input.marked_range.as_ref() {
|
||||
vec![
|
||||
TextRun {
|
||||
len: marked_range.start,
|
||||
..run.clone()
|
||||
},
|
||||
TextRun {
|
||||
len: marked_range.end - marked_range.start,
|
||||
underline: Some(UnderlineStyle {
|
||||
color: Some(run.color),
|
||||
thickness: px(1.0),
|
||||
wavy: false,
|
||||
}),
|
||||
..run.clone()
|
||||
},
|
||||
TextRun {
|
||||
len: display_text.len() - marked_range.end,
|
||||
..run
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|run| run.len > 0)
|
||||
.collect()
|
||||
} else {
|
||||
vec![run]
|
||||
};
|
||||
|
||||
let font_size = style.font_size.to_pixels(window.rem_size());
|
||||
let line = window
|
||||
.text_system()
|
||||
.shape_line(display_text, font_size, &runs, None);
|
||||
|
||||
let cursor_pos = line.x_for_index(cursor);
|
||||
let (selection, cursor) = if selected_range.is_empty() {
|
||||
(
|
||||
None,
|
||||
Some(fill(
|
||||
Bounds::new(
|
||||
point(bounds.left() + cursor_pos, bounds.top()),
|
||||
size(px(2.), bounds.bottom() - bounds.top()),
|
||||
),
|
||||
gpui::blue(),
|
||||
)),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
Some(fill(
|
||||
Bounds::from_corners(
|
||||
point(
|
||||
bounds.left() + line.x_for_index(selected_range.start),
|
||||
bounds.top(),
|
||||
),
|
||||
point(
|
||||
bounds.left() + line.x_for_index(selected_range.end),
|
||||
bounds.bottom(),
|
||||
),
|
||||
),
|
||||
rgba(0x3311ff30),
|
||||
)),
|
||||
None,
|
||||
)
|
||||
};
|
||||
PrepaintState {
|
||||
line: Some(line),
|
||||
cursor,
|
||||
selection,
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&gpui::InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
prepaint: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let focus_handle = self.input.read(cx).focus_handle.clone();
|
||||
window.handle_input(
|
||||
&focus_handle,
|
||||
ElementInputHandler::new(bounds, self.input.clone()),
|
||||
cx,
|
||||
);
|
||||
if let Some(selection) = prepaint.selection.take() {
|
||||
window.paint_quad(selection)
|
||||
}
|
||||
let line = prepaint.line.take().unwrap();
|
||||
line.paint(
|
||||
bounds.origin,
|
||||
window.line_height(),
|
||||
gpui::TextAlign::Left,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if focus_handle.is_focused(window)
|
||||
&& let Some(cursor) = prepaint.cursor.take()
|
||||
{
|
||||
window.paint_quad(cursor);
|
||||
}
|
||||
|
||||
self.input.update(cx, |input, _cx| {
|
||||
input.last_layout = Some(line);
|
||||
input.last_bounds = Some(bounds);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for TextInput {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.key_context("TextInput")
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.cursor(CursorStyle::IBeam)
|
||||
.on_action(cx.listener(Self::backspace))
|
||||
.on_action(cx.listener(Self::delete))
|
||||
.on_action(cx.listener(Self::left))
|
||||
.on_action(cx.listener(Self::right))
|
||||
.on_action(cx.listener(Self::select_left))
|
||||
.on_action(cx.listener(Self::select_right))
|
||||
.on_action(cx.listener(Self::select_all))
|
||||
.on_action(cx.listener(Self::home))
|
||||
.on_action(cx.listener(Self::end))
|
||||
.on_action(cx.listener(Self::show_character_palette))
|
||||
.on_action(cx.listener(Self::paste))
|
||||
.on_action(cx.listener(Self::cut))
|
||||
.on_action(cx.listener(Self::copy))
|
||||
.on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
|
||||
.on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
|
||||
.on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up))
|
||||
.on_mouse_move(cx.listener(Self::on_mouse_move))
|
||||
.bg(rgb(0xeeeeee))
|
||||
.line_height(px(30.))
|
||||
.text_size(px(24.))
|
||||
.child(
|
||||
div()
|
||||
.h(px(30. + 4. * 2.))
|
||||
.w_full()
|
||||
.p(px(4.))
|
||||
.bg(white())
|
||||
.child(TextElement { input: cx.entity() }),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for TextInput {
|
||||
fn focus_handle(&self, _: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
struct InputExample {
|
||||
text_input: Entity<TextInput>,
|
||||
recent_keystrokes: Vec<Keystroke>,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Focusable for InputExample {
|
||||
fn focus_handle(&self, _: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl InputExample {
|
||||
fn on_reset_click(&mut self, _: &MouseUpEvent, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.recent_keystrokes.clear();
|
||||
self.text_input
|
||||
.update(cx, |text_input, _cx| text_input.reset());
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for InputExample {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.bg(rgb(0xaaaaaa))
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.child(
|
||||
div()
|
||||
.bg(white())
|
||||
.border_b_1()
|
||||
.border_color(black())
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_between()
|
||||
.child(format!("Keyboard {}", cx.keyboard_layout().name()))
|
||||
.child(
|
||||
div()
|
||||
.border_1()
|
||||
.border_color(black())
|
||||
.px_2()
|
||||
.bg(yellow())
|
||||
.child("Reset")
|
||||
.hover(|style| {
|
||||
style
|
||||
.bg(yellow().blend(opaque_grey(0.5, 0.5)))
|
||||
.cursor_pointer()
|
||||
})
|
||||
.on_mouse_up(MouseButton::Left, cx.listener(Self::on_reset_click)),
|
||||
),
|
||||
)
|
||||
.child(self.text_input.clone())
|
||||
.children(self.recent_keystrokes.iter().rev().map(|ks| {
|
||||
format!(
|
||||
"{:} {}",
|
||||
ks.unparse(),
|
||||
if let Some(key_char) = ks.key_char.as_ref() {
|
||||
format!("-> {:?}", key_char)
|
||||
} else {
|
||||
"".to_owned()
|
||||
}
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("backspace", Backspace, None),
|
||||
KeyBinding::new("delete", Delete, None),
|
||||
KeyBinding::new("left", Left, None),
|
||||
KeyBinding::new("right", Right, None),
|
||||
KeyBinding::new("shift-left", SelectLeft, None),
|
||||
KeyBinding::new("shift-right", SelectRight, None),
|
||||
KeyBinding::new("cmd-a", SelectAll, None),
|
||||
KeyBinding::new("cmd-v", Paste, None),
|
||||
KeyBinding::new("cmd-c", Copy, None),
|
||||
KeyBinding::new("cmd-x", Cut, None),
|
||||
KeyBinding::new("home", Home, None),
|
||||
KeyBinding::new("end", End, None),
|
||||
KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None),
|
||||
]);
|
||||
|
||||
let window = cx
|
||||
.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| {
|
||||
let text_input = cx.new(|cx| TextInput {
|
||||
focus_handle: cx.focus_handle(),
|
||||
content: "".into(),
|
||||
placeholder: "Type here...".into(),
|
||||
selected_range: 0..0,
|
||||
selection_reversed: false,
|
||||
marked_range: None,
|
||||
last_layout: None,
|
||||
last_bounds: None,
|
||||
is_selecting: false,
|
||||
});
|
||||
cx.new(|cx| InputExample {
|
||||
text_input,
|
||||
recent_keystrokes: vec![],
|
||||
focus_handle: cx.focus_handle(),
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let view = window.update(cx, |_, _, cx| cx.entity()).unwrap();
|
||||
cx.observe_keystrokes(move |ev, _, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
view.recent_keystrokes.push(ev.keystroke.clone());
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
cx.on_keyboard_layout_change({
|
||||
move |cx| {
|
||||
window.update(cx, |_, _, cx| cx.notify()).ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
window
|
||||
.update(cx, |view, window, cx| {
|
||||
window.focus(&view.text_input.focus_handle(cx), cx);
|
||||
cx.activate(true);
|
||||
})
|
||||
.unwrap();
|
||||
cx.on_action(|_: &Quit, cx| cx.quit());
|
||||
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
fn run_example() {
|
||||
#[cfg(all(target_os = "linux", feature = "wayland"))]
|
||||
example::main();
|
||||
|
||||
#[cfg(not(all(target_os = "linux", feature = "wayland")))]
|
||||
panic!("This example requires the `wayland` feature and a linux system.");
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "wayland"))]
|
||||
mod example {
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, FontWeight, Size, Window, WindowBackgroundAppearance, WindowBounds,
|
||||
WindowKind, WindowOptions, div, layer_shell::*, point, prelude::*, px, rems, rgba, white,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
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;
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
LayerShellExample
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for LayerShellExample {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
let hours = (now / 3600) % 24;
|
||||
let minutes = (now / 60) % 60;
|
||||
let seconds = now % 60;
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_size(rems(4.5))
|
||||
.font_weight(FontWeight::EXTRA_BOLD)
|
||||
.text_color(white())
|
||||
.bg(rgba(0x0000044))
|
||||
.rounded_xl()
|
||||
.child(format!("{:02}:{:02}:{:02}", hours, minutes, seconds))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
application().run(|cx: &mut App| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
titlebar: None,
|
||||
window_bounds: Some(WindowBounds::Windowed(Bounds {
|
||||
origin: point(px(0.), px(0.)),
|
||||
size: Size::new(px(500.), px(200.)),
|
||||
})),
|
||||
app_id: Some("gpui-layer-shell-example".to_string()),
|
||||
window_background: WindowBackgroundAppearance::Transparent,
|
||||
kind: WindowKind::LayerShell(LayerShellOptions {
|
||||
namespace: "gpui".to_string(),
|
||||
anchor: Anchor::LEFT | Anchor::RIGHT | Anchor::BOTTOM,
|
||||
margin: Some((px(0.), px(0.), px(40.), px(0.))),
|
||||
keyboard_interactivity: KeyboardInteractivity::None,
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| cx.new(LayerShellExample::new),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, ListAlignment, ListState, Render, Window, WindowBounds, WindowOptions,
|
||||
div, list, prelude::*, px, rgb, size,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
const ITEM_COUNT: usize = 40;
|
||||
const SCROLLBAR_WIDTH: f32 = 12.;
|
||||
|
||||
struct BottomListDemo {
|
||||
list_state: ListState,
|
||||
}
|
||||
|
||||
impl BottomListDemo {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
list_state: ListState::new(ITEM_COUNT, ListAlignment::Bottom, px(500.)).measure_all(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for BottomListDemo {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let max_offset = self.list_state.max_offset_for_scrollbar().y;
|
||||
let current_offset = -self.list_state.scroll_px_offset_for_scrollbar().y;
|
||||
|
||||
let viewport_height = self.list_state.viewport_bounds().size.height;
|
||||
|
||||
let raw_fraction = if max_offset > px(0.) {
|
||||
current_offset / max_offset
|
||||
} else {
|
||||
0.
|
||||
};
|
||||
|
||||
let total_height = viewport_height + max_offset;
|
||||
let thumb_height = if total_height > px(0.) {
|
||||
px(viewport_height.as_f32() * viewport_height.as_f32() / total_height.as_f32())
|
||||
.max(px(30.))
|
||||
} else {
|
||||
px(30.)
|
||||
};
|
||||
|
||||
let track_space = viewport_height - thumb_height;
|
||||
let thumb_top = track_space * raw_fraction;
|
||||
|
||||
let bug_detected = raw_fraction > 1.0;
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.bg(rgb(0xFFFFFF))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.p_4()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.child(format!(
|
||||
"offset: {:.0} / max: {:.0} | fraction: {:.3}",
|
||||
current_offset.as_f32(),
|
||||
max_offset.as_f32(),
|
||||
raw_fraction,
|
||||
))
|
||||
.child(
|
||||
div()
|
||||
.text_color(if bug_detected {
|
||||
rgb(0xCC0000)
|
||||
} else {
|
||||
rgb(0x008800)
|
||||
})
|
||||
.child(if bug_detected {
|
||||
format!(
|
||||
"BUG: fraction is {:.3} (> 1.0) — thumb is off-track!",
|
||||
raw_fraction
|
||||
)
|
||||
} else {
|
||||
"OK: fraction <= 1.0 — thumb is within track.".to_string()
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.overflow_hidden()
|
||||
.border_1()
|
||||
.border_color(rgb(0xCCCCCC))
|
||||
.rounded_sm()
|
||||
.child(
|
||||
list(self.list_state.clone(), |index, _window, _cx| {
|
||||
let height = px(30. + (index % 5) as f32 * 10.);
|
||||
div()
|
||||
.h(height)
|
||||
.w_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.px_3()
|
||||
.border_b_1()
|
||||
.border_color(rgb(0xEEEEEE))
|
||||
.bg(if index % 2 == 0 {
|
||||
rgb(0xFAFAFA)
|
||||
} else {
|
||||
rgb(0xFFFFFF)
|
||||
})
|
||||
.text_sm()
|
||||
.child(format!("Item {index}"))
|
||||
.into_any()
|
||||
})
|
||||
.flex_1(),
|
||||
)
|
||||
// Scrollbar track
|
||||
.child(
|
||||
div()
|
||||
.w(px(SCROLLBAR_WIDTH))
|
||||
.h_full()
|
||||
.flex_shrink_0()
|
||||
.bg(rgb(0xE0E0E0))
|
||||
.relative()
|
||||
.child(
|
||||
// Thumb — position is unclamped to expose the bug
|
||||
div()
|
||||
.absolute()
|
||||
.top(thumb_top)
|
||||
.w_full()
|
||||
.h(thumb_height)
|
||||
.bg(if bug_detected {
|
||||
rgb(0xCC0000)
|
||||
} else {
|
||||
rgb(0x888888)
|
||||
})
|
||||
.rounded_sm(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
let bounds = Bounds::centered(None, size(px(400.), px(500.)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
focus: true,
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| cx.new(|_| BottomListDemo::new()),
|
||||
)
|
||||
.unwrap();
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, MousePressureEvent, PressureStage, Window, WindowBounds, WindowOptions,
|
||||
div, prelude::*, px, rgb, size,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
struct MousePressureExample {
|
||||
pressure_stage: PressureStage,
|
||||
pressure_amount: f32,
|
||||
}
|
||||
|
||||
impl Render for MousePressureExample {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.bg(rgb(0x505050))
|
||||
.size(px(500.0))
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.shadow_lg()
|
||||
.border_1()
|
||||
.border_color(rgb(0x0000ff))
|
||||
.text_xl()
|
||||
.text_color(rgb(0xffffff))
|
||||
.child(format!("Pressure stage: {:?}", self.pressure_stage))
|
||||
.child(format!("Pressure amount: {:.2}", self.pressure_amount))
|
||||
.on_mouse_pressure(cx.listener(Self::on_mouse_pressure))
|
||||
}
|
||||
}
|
||||
|
||||
impl MousePressureExample {
|
||||
fn on_mouse_pressure(
|
||||
&mut self,
|
||||
pressure_event: &MousePressureEvent,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.pressure_amount = pressure_event.pressure;
|
||||
self.pressure_stage = pressure_event.stage;
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
|
||||
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| {
|
||||
cx.new(|_| MousePressureExample {
|
||||
pressure_stage: PressureStage::Zero,
|
||||
pressure_amount: 0.0,
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
//! An entity registers callbacks via the `_in` API family and then gets
|
||||
//! re-hosted in a new window via a click. The point of the example is to
|
||||
//! demonstrate that callbacks dispatched after the move correctly target the
|
||||
//! entity's *current* window rather than the window it was in at
|
||||
//! registration time.
|
||||
//!
|
||||
//! To run: cargo run -p gpui --example move_entity_between_windows
|
||||
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::{
|
||||
App, AppContext as _, Bounds, Context, EventEmitter, MouseButton, Render, SharedString,
|
||||
Subscription, Task, Window, WindowBounds, WindowOptions, div, prelude::*, px, rgb, size,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
struct MoveToNewWindow;
|
||||
|
||||
struct HelloWorld {
|
||||
text: SharedString,
|
||||
tick_count: u32,
|
||||
move_count: u32,
|
||||
_tasks: Vec<Task<()>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl EventEmitter<MoveToNewWindow> for HelloWorld {}
|
||||
|
||||
impl HelloWorld {
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let self_entity = cx.entity();
|
||||
|
||||
let task = cx.spawn_in(window, async move |this, cx| {
|
||||
loop {
|
||||
cx.background_executor().timer(Duration::from_secs(1)).await;
|
||||
let result = this.update_in(cx, |this, window, _cx| {
|
||||
this.tick_count += 1;
|
||||
println!(
|
||||
"tick #{} fired in entity's current window {}",
|
||||
this.tick_count,
|
||||
window.window_handle().window_id().as_u64(),
|
||||
);
|
||||
});
|
||||
if let Err(err) = result {
|
||||
println!("tick task giving up: {err}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let subscription = cx.subscribe_in::<_, MoveToNewWindow>(
|
||||
&self_entity,
|
||||
window,
|
||||
move |this, _emitter, _event, window, cx| {
|
||||
let entered_window_id = window.window_handle().window_id().as_u64();
|
||||
println!(
|
||||
"MoveToNewWindow handler fired in entity's current window {entered_window_id}",
|
||||
);
|
||||
|
||||
this.move_count += 1;
|
||||
cx.notify();
|
||||
|
||||
let entity = cx.entity();
|
||||
let old_window = window.window_handle();
|
||||
cx.defer(move |cx| {
|
||||
let bounds = Bounds::centered(None, size(px(500.0), px(500.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
move |_, _| entity,
|
||||
)
|
||||
.expect("failed to open new window");
|
||||
old_window
|
||||
.update(cx, |_, window, _| window.remove_window())
|
||||
.ok();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Self {
|
||||
text: "World".into(),
|
||||
tick_count: 0,
|
||||
move_count: 0,
|
||||
_tasks: vec![task],
|
||||
_subscriptions: vec![subscription],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for HelloWorld {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let window_id = window.window_handle().window_id().as_u64();
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.bg(rgb(0x505050))
|
||||
.size(px(500.0))
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.text_xl()
|
||||
.text_color(rgb(0xffffff))
|
||||
.child(format!("Hello, {}!", self.text))
|
||||
.child(format!("Rendering in window: {window_id}"))
|
||||
.child(format!("Ticks observed by entity: {}", self.tick_count))
|
||||
.child(format!("Moves observed by entity: {}", self.move_count))
|
||||
.child(
|
||||
div()
|
||||
.px_4()
|
||||
.py_2()
|
||||
.bg(rgb(0x4040ff))
|
||||
.rounded_md()
|
||||
.child("Move me to a new window")
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(|_this, _, _window, cx| {
|
||||
cx.emit(MoveToNewWindow);
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
let bounds = Bounds::centered(None, size(px(500.0), px(500.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| cx.new(|cx| HelloWorld::new(window, cx)),
|
||||
)
|
||||
.unwrap();
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, FocusHandle, KeyBinding, Window, WindowBounds, WindowOptions, actions,
|
||||
div, prelude::*, px, rgb, size,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
actions!(example, [CloseWindow]);
|
||||
|
||||
struct ExampleWindow {
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Render for ExampleWindow {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.on_action(|_: &CloseWindow, window, _| {
|
||||
window.remove_window();
|
||||
})
|
||||
.track_focus(&self.focus_handle)
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.bg(rgb(0x505050))
|
||||
.size(px(500.0))
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.shadow_lg()
|
||||
.border_1()
|
||||
.border_color(rgb(0x0000ff))
|
||||
.text_xl()
|
||||
.text_color(rgb(0xffffff))
|
||||
.child(
|
||||
"Closing this window with cmd-w or the traffic lights should quit the application!",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
let mut bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
|
||||
|
||||
cx.bind_keys([KeyBinding::new("cmd-w", CloseWindow, None)]);
|
||||
cx.on_window_closed(|cx, _window_id| {
|
||||
if cx.windows().is_empty() {
|
||||
cx.quit();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| {
|
||||
cx.activate(false);
|
||||
cx.new(|cx| {
|
||||
let focus_handle = cx.focus_handle();
|
||||
focus_handle.focus(window, cx);
|
||||
ExampleWindow { focus_handle }
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
bounds.origin.x += bounds.size.width;
|
||||
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| {
|
||||
cx.new(|cx| {
|
||||
let focus_handle = cx.focus_handle();
|
||||
focus_handle.focus(window, cx);
|
||||
ExampleWindow { focus_handle }
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
Vendored
+195
@@ -0,0 +1,195 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::{
|
||||
App, AssetSource, Bounds, BoxShadow, ClickEvent, Context, SharedString, Task, Window,
|
||||
WindowBounds, WindowOptions, div, hsla, img, prelude::*, px, rgb, size, svg,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
struct Assets {
|
||||
base: PathBuf,
|
||||
}
|
||||
|
||||
impl AssetSource for Assets {
|
||||
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
|
||||
fs::read(self.base.join(path))
|
||||
.map(|data| Some(std::borrow::Cow::Owned(data)))
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
|
||||
fs::read_dir(self.base.join(path))
|
||||
.map(|entries| {
|
||||
entries
|
||||
.filter_map(|entry| {
|
||||
entry
|
||||
.ok()
|
||||
.and_then(|entry| entry.file_name().into_string().ok())
|
||||
.map(SharedString::from)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
|
||||
struct HelloWorld {
|
||||
_task: Option<Task<()>>,
|
||||
opacity: f32,
|
||||
animating: bool,
|
||||
}
|
||||
|
||||
impl HelloWorld {
|
||||
fn new(_window: &mut Window, _: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
_task: None,
|
||||
opacity: 0.5,
|
||||
animating: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn start_animation(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.opacity = 0.0;
|
||||
self.animating = true;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for HelloWorld {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
if self.animating {
|
||||
self.opacity += 0.005;
|
||||
if self.opacity >= 1.0 {
|
||||
self.animating = false;
|
||||
self.opacity = 1.0;
|
||||
} else {
|
||||
window.request_animation_frame();
|
||||
}
|
||||
}
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.size_full()
|
||||
.bg(rgb(0xe0e0e0))
|
||||
.text_xl()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.size_full()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.border_1()
|
||||
.text_color(gpui::blue())
|
||||
.child(div().child("This is background text.")),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("panel")
|
||||
.on_click(cx.listener(Self::start_animation))
|
||||
.absolute()
|
||||
.top_8()
|
||||
.left_8()
|
||||
.right_8()
|
||||
.bottom_8()
|
||||
.opacity(self.opacity)
|
||||
.flex()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.bg(gpui::white())
|
||||
.border_3()
|
||||
.border_color(gpui::red())
|
||||
.text_color(gpui::yellow())
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.size(px(300.))
|
||||
.bg(gpui::blue())
|
||||
.border_3()
|
||||
.border_color(gpui::black())
|
||||
.shadow(vec![
|
||||
BoxShadow::new(px(10.0), px(10.0), hsla(0.0, 0.0, 0.0, 0.5))
|
||||
.blur_radius(px(1.0))
|
||||
.spread_radius(px(5.0)),
|
||||
])
|
||||
.child(img("image/app-icon.png").size_8())
|
||||
.child("Opacity Panel (Click to test)")
|
||||
.child(
|
||||
div()
|
||||
.id("deep-level-text")
|
||||
.flex()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.p_4()
|
||||
.bg(gpui::black())
|
||||
.text_color(gpui::white())
|
||||
.text_decoration_2()
|
||||
.text_decoration_wavy()
|
||||
.text_decoration_color(gpui::red())
|
||||
.child(format!("opacity: {:.1}", self.opacity)),
|
||||
)
|
||||
.child(
|
||||
svg()
|
||||
.path("image/arrow_circle.svg")
|
||||
.text_color(gpui::black())
|
||||
.text_2xl()
|
||||
.size_8(),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.children(["🎊", "✈️", "🎉", "🎈", "🎁", "🎂"].map(|emoji| {
|
||||
div()
|
||||
.child(emoji.to_string())
|
||||
.hover(|style| style.opacity(0.5))
|
||||
})),
|
||||
)
|
||||
.child(img("image/black-cat-typing.gif").size_12()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application()
|
||||
.with_assets(Assets {
|
||||
base: PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples"),
|
||||
})
|
||||
.run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
let bounds = Bounds::centered(None, size(px(500.0), px(500.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| cx.new(|cx| HelloWorld::new(window, cx)),
|
||||
)
|
||||
.unwrap();
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{App, Context, Entity, EventEmitter, prelude::*};
|
||||
use gpui_platform::application;
|
||||
|
||||
struct Counter {
|
||||
count: usize,
|
||||
}
|
||||
|
||||
struct Change {
|
||||
increment: usize,
|
||||
}
|
||||
|
||||
impl EventEmitter<Change> for Counter {}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
let counter: Entity<Counter> = cx.new(|_cx| Counter { count: 0 });
|
||||
let subscriber = cx.new(|cx: &mut Context<Counter>| {
|
||||
cx.subscribe(&counter, |subscriber, _emitter, event, _cx| {
|
||||
subscriber.count += event.increment * 2;
|
||||
})
|
||||
.detach();
|
||||
|
||||
Counter {
|
||||
count: counter.read(cx).count * 2,
|
||||
}
|
||||
});
|
||||
|
||||
counter.update(cx, |counter, cx| {
|
||||
counter.count += 2;
|
||||
cx.notify();
|
||||
cx.emit(Change { increment: 2 });
|
||||
});
|
||||
|
||||
assert_eq!(subscriber.read(cx).count, 4);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
Vendored
+478
@@ -0,0 +1,478 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{
|
||||
Background, Bounds, ColorSpace, Context, MouseDownEvent, Path, PathBuilder, PathStyle, Pixels,
|
||||
Point, Render, StrokeOptions, Window, WindowOptions, canvas, div, linear_color_stop,
|
||||
linear_gradient, point, prelude::*, px, quad, rgb, size,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
struct PaintingViewer {
|
||||
default_lines: Vec<(Path<Pixels>, Background)>,
|
||||
background_quads: Vec<(Bounds<Pixels>, Background)>,
|
||||
lines: Vec<Vec<Point<Pixels>>>,
|
||||
start: Point<Pixels>,
|
||||
dashed: bool,
|
||||
_painting: bool,
|
||||
}
|
||||
|
||||
impl PaintingViewer {
|
||||
fn new(_window: &mut Window, _cx: &mut Context<Self>) -> Self {
|
||||
let mut lines = vec![];
|
||||
|
||||
// Black squares beneath transparent paths.
|
||||
let background_quads = vec![
|
||||
(
|
||||
Bounds {
|
||||
origin: point(px(70.), px(70.)),
|
||||
size: size(px(40.), px(40.)),
|
||||
},
|
||||
gpui::black().into(),
|
||||
),
|
||||
(
|
||||
Bounds {
|
||||
origin: point(px(170.), px(70.)),
|
||||
size: size(px(40.), px(40.)),
|
||||
},
|
||||
gpui::black().into(),
|
||||
),
|
||||
(
|
||||
Bounds {
|
||||
origin: point(px(270.), px(70.)),
|
||||
size: size(px(40.), px(40.)),
|
||||
},
|
||||
gpui::black().into(),
|
||||
),
|
||||
(
|
||||
Bounds {
|
||||
origin: point(px(370.), px(70.)),
|
||||
size: size(px(40.), px(40.)),
|
||||
},
|
||||
gpui::black().into(),
|
||||
),
|
||||
(
|
||||
Bounds {
|
||||
origin: point(px(450.), px(50.)),
|
||||
size: size(px(80.), px(80.)),
|
||||
},
|
||||
gpui::black().into(),
|
||||
),
|
||||
];
|
||||
|
||||
// 50% opaque red path that extends across black quad.
|
||||
let mut builder = PathBuilder::fill();
|
||||
builder.move_to(point(px(50.), px(50.)));
|
||||
builder.line_to(point(px(130.), px(50.)));
|
||||
builder.line_to(point(px(130.), px(130.)));
|
||||
builder.line_to(point(px(50.), px(130.)));
|
||||
builder.close();
|
||||
let path = builder.build().unwrap();
|
||||
let red = rgb(0xFF0000).alpha(0.5);
|
||||
lines.push((path, red.into()));
|
||||
|
||||
// 50% opaque blue path that extends across black quad.
|
||||
let mut builder = PathBuilder::fill();
|
||||
builder.move_to(point(px(150.), px(50.)));
|
||||
builder.line_to(point(px(230.), px(50.)));
|
||||
builder.line_to(point(px(230.), px(130.)));
|
||||
builder.line_to(point(px(150.), px(130.)));
|
||||
builder.close();
|
||||
let path = builder.build().unwrap();
|
||||
let blue = rgb(0x0000FF).alpha(0.5);
|
||||
lines.push((path, blue.into()));
|
||||
|
||||
// 50% opaque green path that extends across black quad.
|
||||
let mut builder = PathBuilder::fill();
|
||||
builder.move_to(point(px(250.), px(50.)));
|
||||
builder.line_to(point(px(330.), px(50.)));
|
||||
builder.line_to(point(px(330.), px(130.)));
|
||||
builder.line_to(point(px(250.), px(130.)));
|
||||
builder.close();
|
||||
let path = builder.build().unwrap();
|
||||
let green = rgb(0x00FF00).alpha(0.5);
|
||||
lines.push((path, green.into()));
|
||||
|
||||
// 50% opaque black path that extends across black quad.
|
||||
let mut builder = PathBuilder::fill();
|
||||
builder.move_to(point(px(350.), px(50.)));
|
||||
builder.line_to(point(px(430.), px(50.)));
|
||||
builder.line_to(point(px(430.), px(130.)));
|
||||
builder.line_to(point(px(350.), px(130.)));
|
||||
builder.close();
|
||||
let path = builder.build().unwrap();
|
||||
let black = rgb(0x000000).alpha(0.5);
|
||||
lines.push((path, black.into()));
|
||||
|
||||
// Two 50% opaque red circles overlapping - center should be darker red
|
||||
let mut builder = PathBuilder::fill();
|
||||
let center = point(px(530.), px(85.));
|
||||
let radius = px(30.);
|
||||
builder.move_to(point(center.x + radius, center.y));
|
||||
builder.arc_to(
|
||||
point(radius, radius),
|
||||
px(0.),
|
||||
false,
|
||||
false,
|
||||
point(center.x - radius, center.y),
|
||||
);
|
||||
builder.arc_to(
|
||||
point(radius, radius),
|
||||
px(0.),
|
||||
false,
|
||||
false,
|
||||
point(center.x + radius, center.y),
|
||||
);
|
||||
builder.close();
|
||||
let path = builder.build().unwrap();
|
||||
let red1 = rgb(0xFF0000).alpha(0.5);
|
||||
lines.push((path, red1.into()));
|
||||
|
||||
let mut builder = PathBuilder::fill();
|
||||
let center = point(px(570.), px(85.));
|
||||
let radius = px(30.);
|
||||
builder.move_to(point(center.x + radius, center.y));
|
||||
builder.arc_to(
|
||||
point(radius, radius),
|
||||
px(0.),
|
||||
false,
|
||||
false,
|
||||
point(center.x - radius, center.y),
|
||||
);
|
||||
builder.arc_to(
|
||||
point(radius, radius),
|
||||
px(0.),
|
||||
false,
|
||||
false,
|
||||
point(center.x + radius, center.y),
|
||||
);
|
||||
builder.close();
|
||||
let path = builder.build().unwrap();
|
||||
let red2 = rgb(0xFF0000).alpha(0.5);
|
||||
lines.push((path, red2.into()));
|
||||
|
||||
// draw a Rust logo
|
||||
let mut builder = lyon::path::Path::svg_builder();
|
||||
lyon::extra::rust_logo::build_logo_path(&mut builder);
|
||||
// move down the Path
|
||||
let mut builder: PathBuilder = builder.into();
|
||||
builder.translate(point(px(10.), px(200.)));
|
||||
builder.scale(0.9);
|
||||
let path = builder.build().unwrap();
|
||||
lines.push((path, gpui::black().into()));
|
||||
|
||||
// draw a lightening bolt ⚡
|
||||
let mut builder = PathBuilder::fill();
|
||||
builder.add_polygon(
|
||||
&[
|
||||
point(px(150.), px(300.)),
|
||||
point(px(200.), px(225.)),
|
||||
point(px(200.), px(275.)),
|
||||
point(px(250.), px(200.)),
|
||||
],
|
||||
false,
|
||||
);
|
||||
let path = builder.build().unwrap();
|
||||
lines.push((path, rgb(0x1d4ed8).into()));
|
||||
|
||||
// draw a ⭐
|
||||
let mut builder = PathBuilder::fill();
|
||||
builder.move_to(point(px(350.), px(200.)));
|
||||
builder.line_to(point(px(370.), px(260.)));
|
||||
builder.line_to(point(px(430.), px(260.)));
|
||||
builder.line_to(point(px(380.), px(300.)));
|
||||
builder.line_to(point(px(400.), px(360.)));
|
||||
builder.line_to(point(px(350.), px(320.)));
|
||||
builder.line_to(point(px(300.), px(360.)));
|
||||
builder.line_to(point(px(320.), px(300.)));
|
||||
builder.line_to(point(px(270.), px(260.)));
|
||||
builder.line_to(point(px(330.), px(260.)));
|
||||
builder.line_to(point(px(350.), px(200.)));
|
||||
let path = builder.build().unwrap();
|
||||
lines.push((
|
||||
path,
|
||||
linear_gradient(
|
||||
180.,
|
||||
linear_color_stop(rgb(0xFACC15), 0.7),
|
||||
linear_color_stop(rgb(0xD56D0C), 1.),
|
||||
)
|
||||
.color_space(ColorSpace::Oklab),
|
||||
));
|
||||
|
||||
// draw linear gradient
|
||||
let square_bounds = Bounds {
|
||||
origin: point(px(450.), px(200.)),
|
||||
size: size(px(200.), px(80.)),
|
||||
};
|
||||
let height = square_bounds.size.height;
|
||||
let horizontal_offset = height;
|
||||
let vertical_offset = px(30.);
|
||||
let mut builder = PathBuilder::fill();
|
||||
builder.move_to(square_bounds.bottom_left());
|
||||
builder.curve_to(
|
||||
square_bounds.origin + point(horizontal_offset, vertical_offset),
|
||||
square_bounds.origin + point(px(0.0), vertical_offset),
|
||||
);
|
||||
builder.line_to(square_bounds.top_right() + point(-horizontal_offset, vertical_offset));
|
||||
builder.curve_to(
|
||||
square_bounds.bottom_right(),
|
||||
square_bounds.top_right() + point(px(0.0), vertical_offset),
|
||||
);
|
||||
builder.line_to(square_bounds.bottom_left());
|
||||
let path = builder.build().unwrap();
|
||||
lines.push((
|
||||
path,
|
||||
linear_gradient(
|
||||
180.,
|
||||
linear_color_stop(gpui::blue(), 0.4),
|
||||
linear_color_stop(gpui::red(), 1.),
|
||||
),
|
||||
));
|
||||
|
||||
// draw a pie chart
|
||||
let center = point(px(96.), px(96.));
|
||||
let pie_center = point(px(775.), px(255.));
|
||||
let segments = [
|
||||
(
|
||||
point(px(871.), px(255.)),
|
||||
point(px(747.), px(163.)),
|
||||
rgb(0x1374e9),
|
||||
),
|
||||
(
|
||||
point(px(747.), px(163.)),
|
||||
point(px(679.), px(263.)),
|
||||
rgb(0xe13527),
|
||||
),
|
||||
(
|
||||
point(px(679.), px(263.)),
|
||||
point(px(754.), px(349.)),
|
||||
rgb(0x0751ce),
|
||||
),
|
||||
(
|
||||
point(px(754.), px(349.)),
|
||||
point(px(854.), px(310.)),
|
||||
rgb(0x209742),
|
||||
),
|
||||
(
|
||||
point(px(854.), px(310.)),
|
||||
point(px(871.), px(255.)),
|
||||
rgb(0xfbc10a),
|
||||
),
|
||||
];
|
||||
|
||||
for (start, end, color) in segments {
|
||||
let mut builder = PathBuilder::fill();
|
||||
builder.move_to(start);
|
||||
builder.arc_to(center, px(0.), false, false, end);
|
||||
builder.line_to(pie_center);
|
||||
builder.close();
|
||||
let path = builder.build().unwrap();
|
||||
lines.push((path, color.into()));
|
||||
}
|
||||
|
||||
// draw a wave
|
||||
let options = StrokeOptions::default()
|
||||
.with_line_width(1.)
|
||||
.with_line_join(lyon::path::LineJoin::Bevel);
|
||||
let mut builder = PathBuilder::stroke(px(1.)).with_style(PathStyle::Stroke(options));
|
||||
builder.move_to(point(px(40.), px(420.)));
|
||||
for i in 1..50 {
|
||||
builder.line_to(point(
|
||||
px(40.0 + i as f32 * 10.0),
|
||||
px(420.0 + (i as f32 * 10.0).sin() * 40.0),
|
||||
));
|
||||
}
|
||||
let path = builder.build().unwrap();
|
||||
lines.push((path, gpui::green().into()));
|
||||
|
||||
Self {
|
||||
default_lines: lines.clone(),
|
||||
background_quads,
|
||||
lines: vec![],
|
||||
start: point(px(0.), px(0.)),
|
||||
dashed: false,
|
||||
_painting: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn clear(&mut self, cx: &mut Context<Self>) {
|
||||
self.lines.clear();
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn button(
|
||||
text: &str,
|
||||
cx: &mut Context<PaintingViewer>,
|
||||
on_click: impl Fn(&mut PaintingViewer, &mut Context<PaintingViewer>) + 'static,
|
||||
) -> impl IntoElement {
|
||||
div()
|
||||
.id(text.to_string())
|
||||
.child(text.to_string())
|
||||
.bg(gpui::black())
|
||||
.text_color(gpui::white())
|
||||
.active(|this| this.opacity(0.8))
|
||||
.flex()
|
||||
.px_3()
|
||||
.py_1()
|
||||
.on_click(cx.listener(move |this, _, _, cx| on_click(this, cx)))
|
||||
}
|
||||
|
||||
impl Render for PaintingViewer {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let default_lines = self.default_lines.clone();
|
||||
let background_quads = self.background_quads.clone();
|
||||
let lines = self.lines.clone();
|
||||
let dashed = self.dashed;
|
||||
|
||||
div()
|
||||
.bg(gpui::white())
|
||||
.size_full()
|
||||
.p_4()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child("Mouse down any point and drag to draw lines (Hold on shift key to draw straight lines)")
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_x_2()
|
||||
.child(button(
|
||||
if dashed { "Solid" } else { "Dashed" },
|
||||
cx,
|
||||
move |this, _| this.dashed = !dashed,
|
||||
))
|
||||
.child(button("Clear", cx, |this, cx| this.clear(cx))),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.size_full()
|
||||
.child(
|
||||
canvas(
|
||||
move |_, _, _| {},
|
||||
move |_, _, window, _| {
|
||||
// First draw background quads
|
||||
for (bounds, color) in background_quads.iter() {
|
||||
window.paint_quad(quad(
|
||||
*bounds,
|
||||
px(0.),
|
||||
*color,
|
||||
px(0.),
|
||||
gpui::transparent_black(),
|
||||
Default::default(),
|
||||
));
|
||||
}
|
||||
|
||||
// Then draw the default paths on top
|
||||
for (path, color) in default_lines {
|
||||
window.paint_path(path, color);
|
||||
}
|
||||
|
||||
for points in lines {
|
||||
if points.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut builder = PathBuilder::stroke(px(1.));
|
||||
if dashed {
|
||||
builder = builder.dash_array(&[px(4.), px(2.)]);
|
||||
}
|
||||
for (i, p) in points.into_iter().enumerate() {
|
||||
if i == 0 {
|
||||
builder.move_to(p);
|
||||
} else {
|
||||
builder.line_to(p);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(path) = builder.build() {
|
||||
window.paint_path(path, gpui::black());
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.size_full(),
|
||||
)
|
||||
.on_mouse_down(
|
||||
gpui::MouseButton::Left,
|
||||
cx.listener(|this, ev: &MouseDownEvent, _, _| {
|
||||
this._painting = true;
|
||||
this.start = ev.position;
|
||||
let path = vec![ev.position];
|
||||
this.lines.push(path);
|
||||
}),
|
||||
)
|
||||
.on_mouse_move(cx.listener(|this, ev: &gpui::MouseMoveEvent, _, cx| {
|
||||
if !this._painting {
|
||||
return;
|
||||
}
|
||||
|
||||
let is_shifted = ev.modifiers.shift;
|
||||
let mut pos = ev.position;
|
||||
// When holding shift, draw a straight line
|
||||
if is_shifted {
|
||||
let dx = pos.x - this.start.x;
|
||||
let dy = pos.y - this.start.y;
|
||||
if dx.abs() > dy.abs() {
|
||||
pos.y = this.start.y;
|
||||
} else {
|
||||
pos.x = this.start.x;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(path) = this.lines.last_mut() {
|
||||
path.push(pos);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}))
|
||||
.on_mouse_up(
|
||||
gpui::MouseButton::Left,
|
||||
cx.listener(|this, _, _, _| {
|
||||
this._painting = false;
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
focus: true,
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| cx.new(|cx| PaintingViewer::new(window, cx)),
|
||||
)
|
||||
.unwrap();
|
||||
cx.on_window_closed(|cx, _window_id| {
|
||||
cx.quit();
|
||||
})
|
||||
.detach();
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{
|
||||
Background, Bounds, ColorSpace, Context, Path, PathBuilder, Pixels, Render, TitlebarOptions,
|
||||
Window, WindowBounds, WindowOptions, canvas, div, linear_color_stop, linear_gradient, point,
|
||||
prelude::*, px, rgb, size,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
const DEFAULT_WINDOW_WIDTH: Pixels = px(1024.0);
|
||||
const DEFAULT_WINDOW_HEIGHT: Pixels = px(768.0);
|
||||
|
||||
struct PaintingViewer {
|
||||
default_lines: Vec<(Path<Pixels>, Background)>,
|
||||
_painting: bool,
|
||||
}
|
||||
|
||||
impl PaintingViewer {
|
||||
fn new(_window: &mut Window, _cx: &mut Context<Self>) -> Self {
|
||||
let mut lines = vec![];
|
||||
|
||||
// draw a lightening bolt ⚡
|
||||
for _ in 0..2000 {
|
||||
// draw a ⭐
|
||||
let mut builder = PathBuilder::fill();
|
||||
builder.move_to(point(px(350.), px(100.)));
|
||||
builder.line_to(point(px(370.), px(160.)));
|
||||
builder.line_to(point(px(430.), px(160.)));
|
||||
builder.line_to(point(px(380.), px(200.)));
|
||||
builder.line_to(point(px(400.), px(260.)));
|
||||
builder.line_to(point(px(350.), px(220.)));
|
||||
builder.line_to(point(px(300.), px(260.)));
|
||||
builder.line_to(point(px(320.), px(200.)));
|
||||
builder.line_to(point(px(270.), px(160.)));
|
||||
builder.line_to(point(px(330.), px(160.)));
|
||||
builder.line_to(point(px(350.), px(100.)));
|
||||
let path = builder.build().unwrap();
|
||||
lines.push((
|
||||
path,
|
||||
linear_gradient(
|
||||
180.,
|
||||
linear_color_stop(rgb(0xFACC15), 0.7),
|
||||
linear_color_stop(rgb(0xD56D0C), 1.),
|
||||
)
|
||||
.color_space(ColorSpace::Oklab),
|
||||
));
|
||||
}
|
||||
|
||||
Self {
|
||||
default_lines: lines,
|
||||
_painting: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for PaintingViewer {
|
||||
fn render(&mut self, window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
window.request_animation_frame();
|
||||
let lines = self.default_lines.clone();
|
||||
div().size_full().child(
|
||||
canvas(
|
||||
move |_, _, _| {},
|
||||
move |_, _, window, _| {
|
||||
for (path, color) in lines {
|
||||
window.paint_path(path, color);
|
||||
}
|
||||
},
|
||||
)
|
||||
.size_full(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
titlebar: Some(TitlebarOptions {
|
||||
title: Some("Vulkan".into()),
|
||||
..Default::default()
|
||||
}),
|
||||
focus: true,
|
||||
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
|
||||
None,
|
||||
size(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT),
|
||||
cx,
|
||||
))),
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| cx.new(|cx| PaintingViewer::new(window, cx)),
|
||||
)
|
||||
.unwrap();
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
Vendored
+136
@@ -0,0 +1,136 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{
|
||||
App, AppContext, Bounds, Context, Window, WindowBounds, WindowOptions, div, linear_color_stop,
|
||||
linear_gradient, pattern_slash, prelude::*, px, rgb, size,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
struct PatternExample;
|
||||
|
||||
impl Render for PatternExample {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.bg(rgb(0xffffff))
|
||||
.size(px(600.0))
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.shadow_lg()
|
||||
.text_xl()
|
||||
.text_color(rgb(0x000000))
|
||||
.child("Pattern Example")
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.border_1()
|
||||
.border_color(gpui::blue())
|
||||
.child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash(
|
||||
gpui::red(),
|
||||
18.0 / 4.0,
|
||||
18.0 / 4.0,
|
||||
)))
|
||||
.child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash(
|
||||
gpui::red(),
|
||||
18.0 / 4.0,
|
||||
18.0 / 4.0,
|
||||
)))
|
||||
.child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash(
|
||||
gpui::red(),
|
||||
18.0 / 4.0,
|
||||
18.0 / 4.0,
|
||||
)))
|
||||
.child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash(
|
||||
gpui::red(),
|
||||
18.0 / 4.0,
|
||||
18.0 / 2.0,
|
||||
))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.border_1()
|
||||
.border_color(gpui::blue())
|
||||
.bg(gpui::green().opacity(0.16))
|
||||
.child("Elements the same height should align")
|
||||
.child(div().w(px(256.0)).h(px(56.0)).bg(pattern_slash(
|
||||
gpui::red(),
|
||||
56.0 / 6.0,
|
||||
56.0 / 6.0,
|
||||
)))
|
||||
.child(div().w(px(256.0)).h(px(56.0)).bg(pattern_slash(
|
||||
gpui::green(),
|
||||
56.0 / 6.0,
|
||||
56.0 / 6.0,
|
||||
)))
|
||||
.child(div().w(px(256.0)).h(px(56.0)).bg(pattern_slash(
|
||||
gpui::blue(),
|
||||
56.0 / 6.0,
|
||||
56.0 / 6.0,
|
||||
)))
|
||||
.child(div().w(px(256.0)).h(px(26.0)).bg(pattern_slash(
|
||||
gpui::yellow(),
|
||||
56.0 / 6.0,
|
||||
56.0 / 6.0,
|
||||
))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.border_1()
|
||||
.border_color(gpui::blue())
|
||||
.w(px(240.0))
|
||||
.h(px(40.0))
|
||||
.bg(gpui::red()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.border_1()
|
||||
.border_color(gpui::blue())
|
||||
.w(px(240.0))
|
||||
.h(px(40.0))
|
||||
.bg(linear_gradient(
|
||||
45.,
|
||||
linear_color_stop(gpui::red(), 0.),
|
||||
linear_color_stop(gpui::blue(), 1.),
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|_window, cx| cx.new(|_cx| PatternExample),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
Vendored
+197
@@ -0,0 +1,197 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{
|
||||
Anchor, App, Context, Div, Hsla, Stateful, Window, WindowOptions, anchored, deferred, div,
|
||||
prelude::*, px,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
/// An example show use deferred to create a floating layers.
|
||||
struct HelloWorld {
|
||||
open: bool,
|
||||
secondary_open: bool,
|
||||
}
|
||||
|
||||
fn button(id: &'static str) -> Stateful<Div> {
|
||||
div()
|
||||
.id(id)
|
||||
.bg(gpui::black())
|
||||
.text_color(gpui::white())
|
||||
.px_3()
|
||||
.py_1()
|
||||
}
|
||||
|
||||
fn popover() -> Div {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.shadow_lg()
|
||||
.p_3()
|
||||
.rounded_md()
|
||||
.bg(gpui::white())
|
||||
.text_color(gpui::black())
|
||||
.border_1()
|
||||
.text_sm()
|
||||
.border_color(gpui::black().opacity(0.1))
|
||||
}
|
||||
|
||||
fn line(color: Hsla) -> Div {
|
||||
div().w(px(480.)).h_2().bg(color.opacity(0.25))
|
||||
}
|
||||
|
||||
impl HelloWorld {
|
||||
fn render_secondary_popover(
|
||||
&mut self,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
button("secondary-btn")
|
||||
.mt_2()
|
||||
.child("Child Popover")
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.secondary_open = true;
|
||||
cx.notify();
|
||||
}))
|
||||
.when(self.secondary_open, |this| {
|
||||
this.child(
|
||||
// Now GPUI supports nested deferred!
|
||||
deferred(
|
||||
anchored()
|
||||
.anchor(Anchor::TopLeft)
|
||||
.snap_to_window_with_margin(px(8.))
|
||||
.child(
|
||||
popover()
|
||||
.child("This is second level Popover with nested deferred!")
|
||||
.bg(gpui::white())
|
||||
.border_color(gpui::blue())
|
||||
.on_mouse_down_out(cx.listener(|this, _, _, cx| {
|
||||
this.secondary_open = false;
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
)
|
||||
.priority(2),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for HelloWorld {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.size_full()
|
||||
.bg(gpui::white())
|
||||
.text_color(gpui::black())
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.gap_4()
|
||||
.child(
|
||||
button("popover0").child("Opened Popover").child(
|
||||
deferred(
|
||||
anchored()
|
||||
.anchor(Anchor::TopLeft)
|
||||
.snap_to_window_with_margin(px(8.))
|
||||
.child(popover().w_96().gap_3().child(
|
||||
"This is a default opened Popover, \
|
||||
we can use deferred to render it \
|
||||
in a floating layer.",
|
||||
)),
|
||||
)
|
||||
.priority(0),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
button("popover1")
|
||||
.child("Open Popover")
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.open = true;
|
||||
cx.notify();
|
||||
}))
|
||||
.when(self.open, |this| {
|
||||
this.child(
|
||||
deferred(
|
||||
anchored()
|
||||
.anchor(Anchor::TopLeft)
|
||||
.snap_to_window_with_margin(px(8.))
|
||||
.child(
|
||||
popover()
|
||||
.w_96()
|
||||
.gap_3()
|
||||
.child(
|
||||
"This is first level Popover, \
|
||||
we can use deferred to render it \
|
||||
in a floating layer.\n\
|
||||
Click outside to close.",
|
||||
)
|
||||
.when(!self.secondary_open, |this| {
|
||||
this.on_mouse_down_out(cx.listener(
|
||||
|this, _, _, cx| {
|
||||
this.open = false;
|
||||
cx.notify();
|
||||
},
|
||||
))
|
||||
})
|
||||
// Here we need render popover after the content
|
||||
// to ensure it will be on top layer.
|
||||
.child(
|
||||
self.render_secondary_popover(window, cx),
|
||||
),
|
||||
),
|
||||
)
|
||||
.priority(1),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
"Here is an example text rendered, \
|
||||
to ensure the Popover will float above this contents.",
|
||||
)
|
||||
.children([
|
||||
line(gpui::red()),
|
||||
line(gpui::yellow()),
|
||||
line(gpui::blue()),
|
||||
line(gpui::green()),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
cx.open_window(WindowOptions::default(), |_, cx| {
|
||||
cx.new(|_| HelloWorld {
|
||||
open: false,
|
||||
secondary_open: false,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{App, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px, size};
|
||||
use gpui_platform::application;
|
||||
|
||||
struct Scrollable {}
|
||||
|
||||
impl Render for Scrollable {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.size_full()
|
||||
.id("vertical")
|
||||
.p_4()
|
||||
.overflow_scroll()
|
||||
.bg(gpui::white())
|
||||
.child("Example for test 2 way scroll in nested layout")
|
||||
.child(
|
||||
div()
|
||||
.h(px(5000.))
|
||||
.border_1()
|
||||
.border_color(gpui::blue())
|
||||
.bg(gpui::blue().opacity(0.05))
|
||||
.p_4()
|
||||
.child(
|
||||
div()
|
||||
.mb_5()
|
||||
.w_full()
|
||||
.id("horizontal")
|
||||
.overflow_scroll()
|
||||
.child(
|
||||
div()
|
||||
.w(px(2000.))
|
||||
.h(px(150.))
|
||||
.bg(gpui::green().opacity(0.1))
|
||||
.hover(|this| this.bg(gpui::green().opacity(0.2)))
|
||||
.border_1()
|
||||
.border_color(gpui::green())
|
||||
.p_4()
|
||||
.child("Scroll Horizontal"),
|
||||
),
|
||||
)
|
||||
.child("Scroll Vertical"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| cx.new(|_| Scrollable {}),
|
||||
)
|
||||
.unwrap();
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{
|
||||
App, Context, Global, Menu, MenuItem, SharedString, SystemMenuType, Window, WindowOptions,
|
||||
actions, div, prelude::*,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
struct SetMenus;
|
||||
|
||||
impl Render for SetMenus {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.bg(gpui::white())
|
||||
.size_full()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.text_xl()
|
||||
.text_color(gpui::black())
|
||||
.child("Set Menus Example")
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
cx.set_global(AppState::new());
|
||||
|
||||
// Bring the menu bar to the foreground (so you can see the menu bar)
|
||||
cx.activate(true);
|
||||
// Register the `quit` function so it can be referenced
|
||||
// by the `MenuItem::action` in the menu bar
|
||||
cx.on_action(quit);
|
||||
cx.on_action(toggle_check);
|
||||
// Add menu items
|
||||
set_app_menus(cx);
|
||||
cx.open_window(WindowOptions::default(), |_, cx| cx.new(|_| SetMenus {}))
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum ViewMode {
|
||||
List,
|
||||
Grid,
|
||||
}
|
||||
|
||||
impl ViewMode {
|
||||
fn toggle(&mut self) {
|
||||
*self = match self {
|
||||
ViewMode::List => ViewMode::Grid,
|
||||
ViewMode::Grid => ViewMode::List,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<SharedString> for ViewMode {
|
||||
fn into(self) -> SharedString {
|
||||
match self {
|
||||
ViewMode::List => "List",
|
||||
ViewMode::Grid => "Grid",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
struct AppState {
|
||||
view_mode: ViewMode,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
view_mode: ViewMode::List,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Global for AppState {}
|
||||
|
||||
fn set_app_menus(cx: &mut App) {
|
||||
let app_state = cx.global::<AppState>();
|
||||
cx.set_menus([Menu::new("set_menus").items([
|
||||
MenuItem::os_submenu("Services", SystemMenuType::Services),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Disabled Item", gpui::NoAction).disabled(true),
|
||||
MenuItem::submenu(Menu::new("Disabled Submenu").disabled(true)),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("List Mode", ToggleCheck).checked(app_state.view_mode == ViewMode::List),
|
||||
MenuItem::submenu(
|
||||
Menu::new("Mode").items([
|
||||
MenuItem::action(ViewMode::List, ToggleCheck)
|
||||
.checked(app_state.view_mode == ViewMode::List),
|
||||
MenuItem::action(ViewMode::Grid, ToggleCheck)
|
||||
.checked(app_state.view_mode == ViewMode::Grid),
|
||||
]),
|
||||
),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Quit", Quit),
|
||||
])]);
|
||||
}
|
||||
|
||||
// Associate actions using the `actions!` macro (or `Action` derive macro)
|
||||
actions!(set_menus, [Quit, ToggleCheck]);
|
||||
|
||||
// Define the quit function that is registered with the App
|
||||
fn quit(_: &Quit, cx: &mut App) {
|
||||
println!("Gracefully quitting the application...");
|
||||
cx.quit();
|
||||
}
|
||||
|
||||
fn toggle_check(_: &ToggleCheck, cx: &mut App) {
|
||||
let app_state = cx.global_mut::<AppState>();
|
||||
app_state.view_mode.toggle();
|
||||
set_app_menus(cx);
|
||||
}
|
||||
Vendored
+622
@@ -0,0 +1,622 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, BoxShadow, Context, Div, SharedString, Window, WindowBounds, WindowOptions, div,
|
||||
hsla, prelude::*, px, relative, rgb, size,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
struct Shadow {}
|
||||
|
||||
impl Shadow {
|
||||
fn base() -> Div {
|
||||
div()
|
||||
.size_16()
|
||||
.bg(rgb(0xffffff))
|
||||
.rounded_full()
|
||||
.border_1()
|
||||
.border_color(hsla(0.0, 0.0, 0.0, 0.1))
|
||||
}
|
||||
|
||||
fn square() -> Div {
|
||||
div()
|
||||
.size_16()
|
||||
.bg(rgb(0xffffff))
|
||||
.border_1()
|
||||
.border_color(hsla(0.0, 0.0, 0.0, 0.1))
|
||||
}
|
||||
|
||||
fn rounded_small() -> Div {
|
||||
div()
|
||||
.size_16()
|
||||
.bg(rgb(0xffffff))
|
||||
.rounded(px(4.))
|
||||
.border_1()
|
||||
.border_color(hsla(0.0, 0.0, 0.0, 0.1))
|
||||
}
|
||||
|
||||
fn rounded_medium() -> Div {
|
||||
div()
|
||||
.size_16()
|
||||
.bg(rgb(0xffffff))
|
||||
.rounded(px(8.))
|
||||
.border_1()
|
||||
.border_color(hsla(0.0, 0.0, 0.0, 0.1))
|
||||
}
|
||||
|
||||
fn rounded_large() -> Div {
|
||||
div()
|
||||
.size_16()
|
||||
.bg(rgb(0xffffff))
|
||||
.rounded(px(12.))
|
||||
.border_1()
|
||||
.border_color(hsla(0.0, 0.0, 0.0, 0.1))
|
||||
}
|
||||
}
|
||||
|
||||
fn example(label: impl Into<SharedString>, example: impl IntoElement) -> impl IntoElement {
|
||||
let label = label.into();
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.w(relative(1. / 6.))
|
||||
.border_r_1()
|
||||
.border_color(hsla(0.0, 0.0, 0.0, 1.0))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.flex_1()
|
||||
.py_12()
|
||||
.child(example),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.border_t_1()
|
||||
.border_color(hsla(0.0, 0.0, 0.0, 1.0))
|
||||
.p_1()
|
||||
.flex()
|
||||
.items_center()
|
||||
.child(label),
|
||||
)
|
||||
}
|
||||
|
||||
impl Render for Shadow {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.id("shadow-example")
|
||||
.overflow_y_scroll()
|
||||
.bg(rgb(0xffffff))
|
||||
.size_full()
|
||||
.text_xs()
|
||||
.child(div().flex().flex_col().w_full().children(vec![
|
||||
div()
|
||||
.border_b_1()
|
||||
.border_color(hsla(0.0, 0.0, 0.0, 1.0))
|
||||
.flex()
|
||||
.flex_row()
|
||||
.children(vec![
|
||||
example(
|
||||
"Square",
|
||||
Shadow::square().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.5, 0.5, 0.3))
|
||||
.blur_radius(px(8.)),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Rounded 4",
|
||||
Shadow::rounded_small().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.5, 0.5, 0.3))
|
||||
.blur_radius(px(8.)),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Rounded 8",
|
||||
Shadow::rounded_medium().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.5, 0.5, 0.3))
|
||||
.blur_radius(px(8.)),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Rounded 16",
|
||||
Shadow::rounded_large().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.5, 0.5, 0.3))
|
||||
.blur_radius(px(8.)),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Circle",
|
||||
Shadow::base().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.5, 0.5, 0.3))
|
||||
.blur_radius(px(8.)),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
div()
|
||||
.border_b_1()
|
||||
.border_color(hsla(0.0, 0.0, 0.0, 1.0))
|
||||
.flex()
|
||||
.w_full()
|
||||
.children(vec![
|
||||
example("None", Shadow::base()),
|
||||
// 2Xsmall shadow
|
||||
example("2X Small", Shadow::base().shadow_2xs()),
|
||||
// Xsmall shadow
|
||||
example("Extra Small", Shadow::base().shadow_xs()),
|
||||
// Small shadow
|
||||
example("Small", Shadow::base().shadow_sm()),
|
||||
// Medium shadow
|
||||
example("Medium", Shadow::base().shadow_md()),
|
||||
// Large shadow
|
||||
example("Large", Shadow::base().shadow_lg()),
|
||||
example("Extra Large", Shadow::base().shadow_xl()),
|
||||
example("2X Large", Shadow::base().shadow_2xl()),
|
||||
]),
|
||||
// Horizontal list of increasing blur radii
|
||||
div()
|
||||
.border_b_1()
|
||||
.border_color(hsla(0.0, 0.0, 0.0, 1.0))
|
||||
.flex()
|
||||
.children(vec![
|
||||
example(
|
||||
"Blur 0",
|
||||
Shadow::base().shadow(vec![BoxShadow::new(
|
||||
px(0.),
|
||||
px(8.),
|
||||
hsla(0.0, 0.0, 0.0, 0.3),
|
||||
)]),
|
||||
),
|
||||
example(
|
||||
"Blur 2",
|
||||
Shadow::base().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3))
|
||||
.blur_radius(px(2.)),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Blur 4",
|
||||
Shadow::base().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3))
|
||||
.blur_radius(px(4.)),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Blur 8",
|
||||
Shadow::base().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3))
|
||||
.blur_radius(px(8.)),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Blur 16",
|
||||
Shadow::base().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3))
|
||||
.blur_radius(px(16.)),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
// Horizontal list of increasing spread radii
|
||||
div()
|
||||
.border_b_1()
|
||||
.border_color(hsla(0.0, 0.0, 0.0, 1.0))
|
||||
.flex()
|
||||
.children(vec![
|
||||
example(
|
||||
"Spread 0",
|
||||
Shadow::base().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3))
|
||||
.blur_radius(px(8.)),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Spread 2",
|
||||
Shadow::base().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3))
|
||||
.blur_radius(px(8.))
|
||||
.spread_radius(px(2.)),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Spread 4",
|
||||
Shadow::base().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3))
|
||||
.blur_radius(px(8.))
|
||||
.spread_radius(px(4.)),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Spread 8",
|
||||
Shadow::base().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3))
|
||||
.blur_radius(px(8.))
|
||||
.spread_radius(px(8.)),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Spread 16",
|
||||
Shadow::base().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3))
|
||||
.blur_radius(px(8.))
|
||||
.spread_radius(px(16.)),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
// Square spread examples
|
||||
div()
|
||||
.border_b_1()
|
||||
.border_color(hsla(0.0, 0.0, 0.0, 1.0))
|
||||
.flex()
|
||||
.children(vec![
|
||||
example(
|
||||
"Square Spread 0",
|
||||
Shadow::square().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3))
|
||||
.blur_radius(px(8.)),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Square Spread 8",
|
||||
Shadow::square().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3))
|
||||
.blur_radius(px(8.))
|
||||
.spread_radius(px(8.)),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Square Spread 16",
|
||||
Shadow::square().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3))
|
||||
.blur_radius(px(8.))
|
||||
.spread_radius(px(16.)),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
// Rounded large spread examples
|
||||
div()
|
||||
.border_b_1()
|
||||
.border_color(hsla(0.0, 0.0, 0.0, 1.0))
|
||||
.flex()
|
||||
.children(vec![
|
||||
example(
|
||||
"Rounded Large Spread 0",
|
||||
Shadow::rounded_large().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3))
|
||||
.blur_radius(px(8.)),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Rounded Large Spread 8",
|
||||
Shadow::rounded_large().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3))
|
||||
.blur_radius(px(8.))
|
||||
.spread_radius(px(8.)),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Rounded Large Spread 16",
|
||||
Shadow::rounded_large().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3))
|
||||
.blur_radius(px(8.))
|
||||
.spread_radius(px(16.)),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
// Directional shadows
|
||||
div()
|
||||
.border_b_1()
|
||||
.border_color(hsla(0.0, 0.0, 0.0, 1.0))
|
||||
.flex()
|
||||
.children(vec![
|
||||
example(
|
||||
"Left",
|
||||
Shadow::base().shadow(vec![
|
||||
BoxShadow::new(px(-8.), px(0.), hsla(0.0, 0.5, 0.5, 0.3))
|
||||
.blur_radius(px(8.)),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Right",
|
||||
Shadow::base().shadow(vec![
|
||||
BoxShadow::new(px(8.), px(0.), hsla(0.0, 0.5, 0.5, 0.3))
|
||||
.blur_radius(px(8.)),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Top",
|
||||
Shadow::base().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(-8.), hsla(0.0, 0.5, 0.5, 0.3))
|
||||
.blur_radius(px(8.)),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Bottom",
|
||||
Shadow::base().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.5, 0.5, 0.3))
|
||||
.blur_radius(px(8.)),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
// Square directional shadows
|
||||
div()
|
||||
.border_b_1()
|
||||
.border_color(hsla(0.0, 0.0, 0.0, 1.0))
|
||||
.flex()
|
||||
.children(vec![
|
||||
example(
|
||||
"Square Left",
|
||||
Shadow::square().shadow(vec![
|
||||
BoxShadow::new(px(-8.), px(0.), hsla(0.0, 0.5, 0.5, 0.3))
|
||||
.blur_radius(px(8.)),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Square Right",
|
||||
Shadow::square().shadow(vec![
|
||||
BoxShadow::new(px(8.), px(0.), hsla(0.0, 0.5, 0.5, 0.3))
|
||||
.blur_radius(px(8.)),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Square Top",
|
||||
Shadow::square().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(-8.), hsla(0.0, 0.5, 0.5, 0.3))
|
||||
.blur_radius(px(8.)),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Square Bottom",
|
||||
Shadow::square().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.5, 0.5, 0.3))
|
||||
.blur_radius(px(8.)),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
// Rounded large directional shadows
|
||||
div()
|
||||
.border_b_1()
|
||||
.border_color(hsla(0.0, 0.0, 0.0, 1.0))
|
||||
.flex()
|
||||
.children(vec![
|
||||
example(
|
||||
"Rounded Large Left",
|
||||
Shadow::rounded_large().shadow(vec![
|
||||
BoxShadow::new(px(-8.), px(0.), hsla(0.0, 0.5, 0.5, 0.3))
|
||||
.blur_radius(px(8.)),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Rounded Large Right",
|
||||
Shadow::rounded_large().shadow(vec![
|
||||
BoxShadow::new(px(8.), px(0.), hsla(0.0, 0.5, 0.5, 0.3))
|
||||
.blur_radius(px(8.)),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Rounded Large Top",
|
||||
Shadow::rounded_large().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(-8.), hsla(0.0, 0.5, 0.5, 0.3))
|
||||
.blur_radius(px(8.)),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Rounded Large Bottom",
|
||||
Shadow::rounded_large().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.5, 0.5, 0.3))
|
||||
.blur_radius(px(8.)),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
// Multiple shadows for different shapes
|
||||
div()
|
||||
.border_b_1()
|
||||
.border_color(hsla(0.0, 0.0, 0.0, 1.0))
|
||||
.flex()
|
||||
.children(vec![
|
||||
example(
|
||||
"Circle Multiple",
|
||||
Shadow::base().shadow(vec![
|
||||
BoxShadow::new(
|
||||
px(0.),
|
||||
px(-12.),
|
||||
hsla(0.0 / 360., 1.0, 0.5, 0.3),
|
||||
)
|
||||
.blur_radius(px(8.))
|
||||
.spread_radius(px(2.)),
|
||||
BoxShadow::new(
|
||||
px(12.),
|
||||
px(0.),
|
||||
hsla(60.0 / 360., 1.0, 0.5, 0.3),
|
||||
)
|
||||
.blur_radius(px(8.))
|
||||
.spread_radius(px(2.)),
|
||||
BoxShadow::new(
|
||||
px(0.),
|
||||
px(12.),
|
||||
hsla(120.0 / 360., 1.0, 0.5, 0.3),
|
||||
)
|
||||
.blur_radius(px(8.))
|
||||
.spread_radius(px(2.)),
|
||||
BoxShadow::new(
|
||||
px(-12.),
|
||||
px(0.),
|
||||
hsla(240.0 / 360., 1.0, 0.5, 0.3),
|
||||
)
|
||||
.blur_radius(px(8.))
|
||||
.spread_radius(px(2.)),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Square Multiple",
|
||||
Shadow::square().shadow(vec![
|
||||
BoxShadow::new(
|
||||
px(0.),
|
||||
px(-12.),
|
||||
hsla(0.0 / 360., 1.0, 0.5, 0.3),
|
||||
)
|
||||
.blur_radius(px(8.))
|
||||
.spread_radius(px(2.)),
|
||||
BoxShadow::new(
|
||||
px(12.),
|
||||
px(0.),
|
||||
hsla(60.0 / 360., 1.0, 0.5, 0.3),
|
||||
)
|
||||
.blur_radius(px(8.))
|
||||
.spread_radius(px(2.)),
|
||||
BoxShadow::new(
|
||||
px(0.),
|
||||
px(12.),
|
||||
hsla(120.0 / 360., 1.0, 0.5, 0.3),
|
||||
)
|
||||
.blur_radius(px(8.))
|
||||
.spread_radius(px(2.)),
|
||||
BoxShadow::new(
|
||||
px(-12.),
|
||||
px(0.),
|
||||
hsla(240.0 / 360., 1.0, 0.5, 0.3),
|
||||
)
|
||||
.blur_radius(px(8.))
|
||||
.spread_radius(px(2.)),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Rounded Large Multiple",
|
||||
Shadow::rounded_large().shadow(vec![
|
||||
BoxShadow::new(
|
||||
px(0.),
|
||||
px(-12.),
|
||||
hsla(0.0 / 360., 1.0, 0.5, 0.3),
|
||||
)
|
||||
.blur_radius(px(8.))
|
||||
.spread_radius(px(2.)),
|
||||
BoxShadow::new(
|
||||
px(12.),
|
||||
px(0.),
|
||||
hsla(60.0 / 360., 1.0, 0.5, 0.3),
|
||||
)
|
||||
.blur_radius(px(8.))
|
||||
.spread_radius(px(2.)),
|
||||
BoxShadow::new(
|
||||
px(0.),
|
||||
px(12.),
|
||||
hsla(120.0 / 360., 1.0, 0.5, 0.3),
|
||||
)
|
||||
.blur_radius(px(8.))
|
||||
.spread_radius(px(2.)),
|
||||
BoxShadow::new(
|
||||
px(-12.),
|
||||
px(0.),
|
||||
hsla(240.0 / 360., 1.0, 0.5, 0.3),
|
||||
)
|
||||
.blur_radius(px(8.))
|
||||
.spread_radius(px(2.)),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
// Inset shadows (CSS `box-shadow: inset ...`).
|
||||
div()
|
||||
.border_b_1()
|
||||
.border_color(hsla(0.0, 0.0, 0.0, 1.0))
|
||||
.flex()
|
||||
.w_full()
|
||||
.children(vec![
|
||||
example(
|
||||
"Inset basic",
|
||||
Shadow::base().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(0.), hsla(0.0, 0.0, 0.0, 0.5))
|
||||
.blur_radius(px(12.))
|
||||
.inset(),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Inset offset",
|
||||
Shadow::base().shadow(vec![
|
||||
BoxShadow::new(px(6.), px(6.), hsla(0.0, 0.0, 0.0, 0.5))
|
||||
.blur_radius(px(8.))
|
||||
.inset(),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Inset spread",
|
||||
Shadow::base().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(0.), hsla(0.0, 0.0, 0.0, 0.5))
|
||||
.blur_radius(px(4.))
|
||||
.spread_radius(px(8.))
|
||||
.inset(),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Inset rounded",
|
||||
Shadow::rounded_large().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(4.), hsla(0.0, 0.0, 0.0, 0.5))
|
||||
.blur_radius(px(10.))
|
||||
.spread_radius(px(2.))
|
||||
.inset(),
|
||||
]),
|
||||
),
|
||||
example(
|
||||
"Inset sharp",
|
||||
Shadow::square().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(0.), hsla(0.0, 0.0, 0.0, 0.6))
|
||||
.spread_radius(px(6.))
|
||||
.inset(),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
// Combined: drop + inset shadows on the same element.
|
||||
div()
|
||||
.border_b_1()
|
||||
.border_color(hsla(0.0, 0.0, 0.0, 1.0))
|
||||
.flex()
|
||||
.w_full()
|
||||
.children(vec![example(
|
||||
"Drop + Inset",
|
||||
Shadow::rounded_medium().shadow(vec![
|
||||
BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.25))
|
||||
.blur_radius(px(12.)),
|
||||
BoxShadow::new(px(0.), px(2.), hsla(0.0, 0.0, 0.0, 0.4))
|
||||
.blur_radius(px(4.))
|
||||
.inset(),
|
||||
]),
|
||||
)]),
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
let bounds = Bounds::centered(None, size(px(1000.0), px(800.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| cx.new(|_| Shadow {}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<!-- Retrieved from https://commons.wikimedia.org/wiki/Category:SVG_files#/media/File:Dragon_clip_art.svg -->
|
||||
<!-- License: CC BY-SA 3.0 -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="273.55759"
|
||||
height="297.81952"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.48.2 r9819"
|
||||
sodipodi:docname="New document 1">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.78061715"
|
||||
inkscape:cx="112.41895"
|
||||
inkscape:cy="87.896202"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:window-width="1024"
|
||||
inkscape:window-height="712"
|
||||
inkscape:window-x="-4"
|
||||
inkscape:window-y="-4"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(12.737105,20.750754)">
|
||||
<path
|
||||
style="fill:#ff0000;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="M 73.44622,98.502944 95.574248,123.45583 103.10719,106.5067 73.44622,75.904107 c -3.686689,-4.753593 -11.648074,-6.667124 -21.18641,-7.532946 2.554639,2.715713 6.322156,2.39923 6.591328,10.82861 0.824915,3.302369 -2.23752,5.956846 -8.945373,8.003754 C 38.790859,84.969312 29.325417,85.209295 20.7156,86.732716 10.937191,88.437808 3.3459273,92.876831 -1.4124273,100.85699 L 14.595082,96.148898 c 7.184903,-0.279702 11.921181,1.889221 10.3578,10.357802 -1.994238,5.38177 -5.541365,9.21065 -11.299418,10.82861 -13.89013312,3.69644 -14.20560941,10.40948 -17.4199368,16.47832 2.98162616,-2.35421 5.619737,-5.05194 12.2410366,-3.76648 C 12.049969,137.75238 6.2740844,144.2887 0,150.76275 c -8.2557969,11.86874 -11.815365,18.89318 -11.299418,29.19017 -3.600096,18.58594 5.3801474,23.59338 8.4745634,34.36906 C -3.7567158,203.36083 -1.3904441,200.64501 0,195.48962 l 7.0621365,-10.3578 c -0.8666538,6.37974 -2.2779903,12.39636 0.4708091,21.18641 12.3493154,22.89597 28.0783714,46.43569 15.0658914,64.50084 l 15.065891,-22.59883 c 5.315347,9.96887 6.398134,20.64317 25.894501,28.24854 l 26.836118,-1.41242 c 0,0 -66.854892,-105.46124 -67.79651,-107.34448 -0.941618,-1.88323 53.201428,-70.621364 53.201428,-70.621364 z"
|
||||
id="path3790"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccccccccccccccccccccccscc" />
|
||||
<path
|
||||
style="fill:#b4daff;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="M 142.47916,274.20076 160.07509,273.64393 96.045057,235.03758 65.913274,202.08095 58.851138,160.64975 90.866157,124.86825 68.26732,97.561326 c -12.595581,1.14506 -19.978043,4.375364 -26.36531,8.003754 -10.473991,7.25982 -17.726182,17.74144 -24.482073,28.71936 -6.479109,23.36435 -7.280245,49.16212 3.766473,80.03754 11.380348,21.89016 28.33967,38.20135 48.493337,51.31819 l 18.835517,9.59325 z"
|
||||
id="path3788"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccccccccccc" />
|
||||
<path
|
||||
style="fill:#ffa100;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="m 156.77943,275.05636 17.41994,-9.41619 0.94162,-16.47831 -25.4237,-4.70809 -12.71184,21.1864 z"
|
||||
id="path2998"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:#ffa100;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="m 149.24649,255.75318 c 0,0 6.12051,-17.41993 4.23728,-17.41993 -1.88324,0 -22.59884,-3.76648 -22.59884,-3.76648 l -17.89075,17.41994 24.95289,13.65346 z"
|
||||
id="path3000"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:#ffa100;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="m 95.103438,237.39163 21.186412,16.47832 16.00751,-9.88699 0.47081,-18.36156 -23.54046,-5.64971 0.47081,10.82861 z"
|
||||
id="path3002"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:#ffa100;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="m 90.866157,216.67603 2.824854,-11.77023 17.419939,12.71185 -1.88324,15.06589 -16.007508,5.1789 -20.244791,-18.36156 z"
|
||||
id="path3004"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:#ffa100;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="m 62.61761,202.55176 18.832364,-8.47457 13.653464,8.47457 -2.824854,16.0075 -19.303173,0 z"
|
||||
id="path3006"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:#ffa100;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="m 57.909519,180.42373 18.832364,0.47081 9.416183,10.3578 -5.178901,9.88699 -21.186409,-0.47081 z"
|
||||
id="path3008"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:#ffa100;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="m 56.497092,159.70813 18.361555,2.82485 6.591327,13.18266 -8.474563,9.88699 -16.949128,-5.1789 z"
|
||||
id="path3780"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:#ffa100;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="m 89.924538,126.28068 c -9.664322,8.18228 -14.377332,17.78125 -10.357799,33.42745 l -13.182656,8.00375 -9.886991,-8.47457 c 3.769665,-18.09309 15.869815,-31.15434 33.427446,-32.95663 z"
|
||||
id="path3786"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
style="fill:#80afb8;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="M 133.77127,121.15445 C 102.29445,108.16921 71.200271,103.60211 38.733769,68.843895 58.122127,89.497163 77.85045,109.22179 101.42657,118.35923 L 69.081877,100.38995 c 11.499182,15.61467 23.598373,27.80948 42.327623,31.94538 -11.24421,1.21671 -20.380767,-7.051 -29.549472,-15.17405 24.733762,29.06649 40.923142,28.22765 55.105772,20.36517 -8.16571,-3.14828 -15.52424,-7.10373 -21.56313,-12.37883 z"
|
||||
id="path3798"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccccccc" />
|
||||
<path
|
||||
style="fill:#ffaf29;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="M 138.56307,117.95991 128.58014,122.3524 87.051152,98.792686 C 67.454225,70.758132 70.300802,54.846161 60.696216,35.301249 68.447691,43.355436 74.77626,52.358226 85.453883,58.461647 79.819169,46.517664 80.927543,37.270916 80.262759,27.314905 c 3.166784,15.761986 9.719966,24.751173 15.972689,34.34128 -1.47816,-6.276965 -3.350278,-13.144867 -2.795221,-16.372006 5.304337,8.158007 10.295783,16.503749 20.764493,21.56313 3.59095,0.562069 10.55437,5.339728 18.36859,11.180882 7.24446,7.245148 12.72056,16.848114 16.77133,28.351519 z"
|
||||
id="path3808"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccccccccc" />
|
||||
<path
|
||||
style="fill:#00d7d6;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="m 113.8054,124.34899 15.57338,-3.59386 12.37883,-4.7918 6.78839,-10.78157 -0.79863,-6.389074 17.17064,13.976104 14.37542,7.18771 9.58361,13.9761 14.77474,-1.99659 c 4.39249,1.57017 8.78497,2.92164 13.17746,7.58703 5.76188,6.36887 11.20761,14.0023 17.96928,16.37201 4.23518,-1.76668 8.10347,-4.0837 14.37542,-2.79522 5.61406,0.5029 9.02747,3.75662 10.38225,9.58361 2.60388,11.36945 -3.06132,11.71345 -6.38908,15.17405 0.037,9.48753 -3.51318,15.38783 -8.78497,19.56654 -1.67328,-6.90259 -4.18558,-12.12712 -7.98635,-14.77473 -13.4304,-10.55772 -26.90659,-1.42587 -40.33104,-8.38566 -17.65083,-10.24469 -27.78384,-26.45774 -37.93513,-31.14674 -9.67041,-1.16902 -16.20698,-3.38266 -22.76109,-5.59045 z"
|
||||
id="path3810"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccccccccccccccccc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="m 154.53576,133.53328 c 7.83618,0.93458 14.84203,2.00755 16.37201,3.99317 12.61186,7.01602 21.03384,26.20016 31.54606,30.34811 -3.00497,-5.51683 -3.48439,-10.71796 1.99658,-15.17405 7.22535,-3.03546 10.55836,-0.45702 14.77474,2.3959 6.02262,6.9248 11.74338,16.2645 18.76791,15.17406"
|
||||
id="path3814"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="m 209.24222,167.47525 c 0.33375,-1.59661 0.12171,-3.55709 1.59727,-4.39249 0.98845,-1.35075 2.83593,-0.98344 4.39249,-1.19796 4.8086,3.66257 4.22462,5.91709 5.98975,8.78498"
|
||||
id="path3816"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="m 219.22515,144.31485 11.18088,11.18088 13.57679,8.78498"
|
||||
id="path3818"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:#80b7c4;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="m 146.94873,99.59132 c -8.40512,-14.108233 -7.32446,-24.976745 -9.98293,-38.334452 2.28959,7.959558 5.31902,15.17927 10.78157,19.96586 -5.86678,-10.515353 -3.24744,-21.030706 -1.99659,-31.546059 1.01072,9.17552 3.11766,18.229239 13.57679,26.354936 5.88809,4.549002 13.0362,8.257998 15.97269,14.774736 6.35347,8.175649 4.50901,17.892639 8.78497,23.160399 -1.99075,-8.86593 -0.24555,-12.75061 0.79864,-17.569958 0.68276,7.697158 4.15451,11.675658 7.98634,15.174058 l 0.39932,-15.174058 c 8.25592,15.027878 19.79844,10.971398 23.95903,28.750838 l -6.78839,8.38566 -9.58361,-1.19795 -6.38908,4.39249 -8.38566,-2.79522 -9.58361,-13.17747 z"
|
||||
id="path3820"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccccccccccccccc" />
|
||||
<path
|
||||
style="fill:#333333;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="m 163.10163,110.79268 c -0.23573,2.97072 13.46423,4.4681 19.60954,10.89419 2.70861,3.95943 5.16753,8.73304 8.71536,13.07303 -5.87363,0.0346 -11.0482,1.74698 -18.05324,-0.93378 -7.43617,-4.47099 -6.44315,-11.29641 -9.33788,-18.98702 z"
|
||||
id="path3822"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccc" />
|
||||
<path
|
||||
style="fill:#ffcd71;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="m 169.40234,116.31052 c 0.19359,4.0262 -0.47444,12.71073 6.79118,16.41203 3.5528,1.27665 7.10559,1.14632 10.65839,0.94322 -2.15752,-3.14407 -3.90555,-6.28813 -5.565,-9.4322 -2.11396,-2.77299 -6.25107,-5.40146 -11.88457,-7.92305 z"
|
||||
id="path3824"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
style="fill:#009d80;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="m 181.19259,123.29035 c -3.74814,1.60851 -4.45804,5.32041 -4.99907,9.14923 2.28898,-1.15726 4.81391,-2.06949 5.65932,-8.11169 z"
|
||||
id="path3826"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
style="fill:#de4000;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="m 137.41517,138.64674 15.74549,16.46119 -4.77136,20.03971 c -5.06394,19.66324 4.09464,25.78123 11.9284,33.16095 10.22958,3.59957 20.40736,7.66534 31.25241,5.72563 -6.95999,-6.56919 -15.51616,-8.88191 -20.75542,-20.03971 -6.49636,-7.22625 -3.74543,-16.18636 -0.23857,-25.28821 4.60728,-11.28719 1.0716,-14.4314 -2.38568,-17.65403 -7.16108,-6.52548 -8.39426,-6.27621 -11.45126,-8.11131 -6.42685,-0.0251 -14.02518,-3.48676 -19.32401,-4.29422 z"
|
||||
id="path3828"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccccccc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="m 153.87636,142.94096 c 3.90631,-1.87096 9.69299,16.23198 6.67991,19.80114 -5.63668,6.67696 -2.80544,19.95511 -0.23857,16.22262"
|
||||
id="path3830"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="csc" />
|
||||
<path
|
||||
style="fill:#ececec;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="m 147.43503,184.21322 c 5.98074,4.73827 16.11122,1.17708 25.76534,-1.43141 -7.03019,7.0566 -13.84727,16.88359 -21.47112,16.22263 l -4.77136,-7.87275 z"
|
||||
id="path3832"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
style="fill:#f2f2f2;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="m 173.67751,213.31852 8.11131,1.43141 c 6.06788,-12.85113 5.3157,-19.63998 5.9642,-27.67389 -3.126,10.99543 -11.45068,18.09185 -22.18683,23.37966 l 7.87275,4.5328 z"
|
||||
id="path3834"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccc" />
|
||||
<path
|
||||
style="fill:#ff6c00;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="m 134.31379,139.12387 5.48706,0.47714 13.12124,8.58844 -1.90854,21.70969 -3.10139,26.71962 20.51685,15.02978 24.09537,2.14712 c -7.59703,15.51076 -21.44479,13.73677 -24.33394,26.24248 1.27084,-6.44195 0.86471,-13.55469 5.00993,-18.84688 -8.47582,5.06966 -15.09323,4.5641 -22.42539,6.20277 4.22385,-2.68547 8.10205,-6.06224 11.68983,-10.01985 -5.80515,-0.60962 -11.61031,-2.01583 -17.41546,-4.5328 -8.64382,-4.18289 -9.62981,-5.93913 -11.9284,-8.11131 7.01942,0.86252 9.473,-1.31884 12.88267,-2.86281 -26.93647,-7.55398 -26.92199,-14.26574 -35.0695,-21.23256 l 20.51685,7.39561 c 4.4984,0.68777 6.63439,-1.32438 6.6799,-5.72563 -16.33876,-9.8522 -21.88964,-21.17548 -24.33393,-32.92239 0.59372,8.94131 30.81833,14.94779 28.38959,14.79122 -3.07926,-0.19851 -18.61897,-12.87576 -17.1769,-16.22262 4.13518,3.60847 8.27036,6.74347 12.40554,5.9642 4.34471,1.92284 4.85595,-1.2656 5.48706,-4.29423 -6.58133,-6.79299 -11.82628,-7.57225 -17.17689,-8.82701 z"
|
||||
id="path3836"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccccccccccccccccsccccc" />
|
||||
<path
|
||||
style="fill:#f2f2f2;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="m 203.49851,177.05618 c -1.12284,5.42482 -3.73922,9.99619 -9.30416,12.88267 6.07286,-0.34494 18.50532,-3.68719 24.09537,-12.88267 z"
|
||||
id="path3838"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
style="fill:#ececec;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="m 230.21812,178.96473 c 3.65804,1.99083 7.31609,3.07314 10.97413,9.30415 -6.98529,5.73834 -13.78235,11.6649 -26.95818,11.2127 8.7089,-1.5798 12.95652,-10.09936 15.98405,-20.51685 z"
|
||||
id="path3840"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
style="fill:#999999;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="m 186.79875,168.22917 c -1.59045,3.42787 -3.18091,5.13539 -4.77136,7.3956 4.18355,-0.76297 7.96823,-1.79187 10.25842,-3.81708 z"
|
||||
id="path3842"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
style="fill:#ca0000;fill-opacity:1;stroke:#130e13;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="m 155.78491,120.51557 c 0,0 -0.47714,-10.73556 -2.14712,-12.64411 -1.66997,-1.90854 -21.94825,-15.506918 -21.94825,-15.506918 -9.7295,-4.220532 -14.38334,-6.731213 -20.51685,-15.745488 2.90484,10.265925 7.92673,20.108438 17.1769,29.105296 -5.32802,-3.09071 -10.65604,-6.298919 -15.98406,-10.019856 2.26988,6.838946 5.33355,13.677896 15.74549,20.516846 10.6125,6.05766 18.85054,4.20042 27.67389,4.29423 z"
|
||||
id="path3846"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cscccccc" />
|
||||
<path
|
||||
style="fill:#000000;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="M 139.45292,84.438938 125.20773,71.693241 C 119.15983,58.804456 109.97182,43.346495 95.217856,23.334566 86.780901,17.513373 80.270569,10.150883 68.226968,7.2150074 60.058839,6.6898637 52.035767,7.0350652 43.110447,1.9667792 34.99844,-5.3656363 30.6958,-11.065466 28.115509,-16.027146 34.01411,-10.45225 40.197961,-4.0216063 45.359687,-0.65733498 52.482283,2.4482615 59.604878,2.589719 66.727474,3.466273 c 14.767928,1.2266725 22.683537,9.305664 32.239116,15.744685 16.18998,15.07105 25.9637,31.746167 35.98785,48.358675 -1.05571,-9.882878 -3.6074,-20.064949 -8.62209,-30.739623 -6.0246,-9.94338 -14.00687,-17.755415 -20.7603,-26.241142 C 95.75565,5.1501111 90.473401,1.2722747 75.724437,-6.2804367 70.112554,-8.4271577 66.785373,-15.143282 62.603866,-20.150754 l 13.120571,10.121583 c 12.287926,4.1420139 32.155153,15.4397069 38.237093,21.367786 12.3045,10.369959 16.369,17.443923 20.99291,24.741648 l 8.99696,25.116521 0.37488,16.494432 c 0,0 -7.87234,-18.743673 -7.87234,-17.244179 0,1.499494 2.99898,23.991901 2.99898,23.991901 z"
|
||||
id="path3848"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccccccccccccccccsc" />
|
||||
<path
|
||||
style="fill:#1a1a1a;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
|
||||
d="m 99.012729,134.48743 11.893421,6.24404 c -25.631941,9.87193 -51.204911,20.45152 -77.604572,21.11082 -8.62273,0.0124 -17.24546,-2.90897 -25.86819,-6.83871 L -8.3253946,139.83947 9.5147367,151.13822 c 7.9289473,3.73642 15.8578943,4.42414 23.7868413,5.94671 16.056118,-2.54274 32.112237,-4.68742 48.168355,-13.3801 z"
|
||||
id="path3850"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccccccc" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 20 KiB |
Vendored
+108
@@ -0,0 +1,108 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "../example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::{
|
||||
App, AssetSource, Bounds, Context, SharedString, Window, WindowBounds, WindowOptions, div,
|
||||
prelude::*, px, rgb, size, svg,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
struct Assets {
|
||||
base: PathBuf,
|
||||
}
|
||||
|
||||
impl AssetSource for Assets {
|
||||
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
|
||||
fs::read(self.base.join(path))
|
||||
.map(|data| Some(std::borrow::Cow::Owned(data)))
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
|
||||
fs::read_dir(self.base.join(path))
|
||||
.map(|entries| {
|
||||
entries
|
||||
.filter_map(|entry| {
|
||||
entry
|
||||
.ok()
|
||||
.and_then(|entry| entry.file_name().into_string().ok())
|
||||
.map(SharedString::from)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
}
|
||||
|
||||
struct SvgExample;
|
||||
|
||||
impl Render for SvgExample {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.size_full()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.gap_8()
|
||||
.bg(rgb(0xffffff))
|
||||
.child(
|
||||
svg()
|
||||
.path("svg/dragon.svg")
|
||||
.size_8()
|
||||
.text_color(rgb(0xff0000)),
|
||||
)
|
||||
.child(
|
||||
svg()
|
||||
.path("svg/dragon.svg")
|
||||
.size_8()
|
||||
.text_color(rgb(0x00ff00)),
|
||||
)
|
||||
.child(
|
||||
svg()
|
||||
.path("svg/dragon.svg")
|
||||
.size_8()
|
||||
.text_color(rgb(0x0000ff)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application()
|
||||
.with_assets(Assets {
|
||||
base: PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples"),
|
||||
})
|
||||
.run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| cx.new(|_| SvgExample),
|
||||
)
|
||||
.unwrap();
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
//! Demonstrates posting, replacing, dismissing, and responding to system notifications.
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, Div, SharedString, Stateful, SystemNotification,
|
||||
SystemNotificationAction, SystemNotificationResponse, Window, WindowBounds, WindowOptions, div,
|
||||
prelude::*, px, rgb, size,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
const NOTIFICATION_TAG: &str = "gpui-system-notification-example";
|
||||
|
||||
struct SystemNotificationExample {
|
||||
revision: usize,
|
||||
status: SharedString,
|
||||
}
|
||||
|
||||
impl Render for SystemNotificationExample {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.gap_4()
|
||||
.p_8()
|
||||
.bg(rgb(0x18181b))
|
||||
.text_color(rgb(0xf4f4f5))
|
||||
.child(div().text_2xl().child("GPUI system notifications"))
|
||||
.child(div().text_sm().text_color(rgb(0xa1a1aa)).child(
|
||||
"Post repeatedly to replace the notification with the same tag. Click the \
|
||||
notification body or an action button to send a response back to GPUI.",
|
||||
))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_3()
|
||||
.child(button("show", "Show or replace").on_click(cx.listener(
|
||||
|this, _, _, cx| {
|
||||
this.revision += 1;
|
||||
let revision = this.revision;
|
||||
cx.show_system_notification(SystemNotification {
|
||||
tag: NOTIFICATION_TAG.into(),
|
||||
title: format!("Example notification {revision}").into(),
|
||||
body: "This notification was posted by the GPUI example.".into(),
|
||||
actions: vec![
|
||||
SystemNotificationAction {
|
||||
id: "open".into(),
|
||||
label: "Open".into(),
|
||||
},
|
||||
SystemNotificationAction {
|
||||
id: "snooze".into(),
|
||||
label: "Snooze".into(),
|
||||
},
|
||||
],
|
||||
});
|
||||
this.status = format!("Posted notification revision {revision}").into();
|
||||
cx.notify();
|
||||
},
|
||||
)))
|
||||
.child(
|
||||
button("dismiss", "Dismiss").on_click(cx.listener(|this, _, _, cx| {
|
||||
cx.dismiss_system_notification(NOTIFICATION_TAG);
|
||||
this.status = "Dismissed the notification".into();
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.mt_2()
|
||||
.p_4()
|
||||
.rounded_md()
|
||||
.bg(rgb(0x27272a))
|
||||
.child(self.status.clone()),
|
||||
)
|
||||
.when(cfg!(target_os = "macos"), |this| {
|
||||
this.child(div().mt_2().text_xs().text_color(rgb(0x71717a)).child(
|
||||
"macOS only delivers notifications when this example runs from an app bundle.",
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn button(id: &'static str, label: &'static str) -> Stateful<Div> {
|
||||
div()
|
||||
.id(id)
|
||||
.px_4()
|
||||
.py_2()
|
||||
.rounded_md()
|
||||
.bg(rgb(0x3f3f46))
|
||||
.hover(|style| style.bg(rgb(0x52525b)))
|
||||
.active(|style| style.bg(rgb(0x71717a)))
|
||||
.cursor_pointer()
|
||||
.child(label)
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
cx.set_app_identity("dev.zed.gpui.system-notifications", "GPUI Notifications");
|
||||
|
||||
let view = cx.new(|_| SystemNotificationExample {
|
||||
revision: 0,
|
||||
status: "No notification posted yet".into(),
|
||||
});
|
||||
cx.on_system_notification_response({
|
||||
let view = view.clone();
|
||||
move |response, cx| {
|
||||
let SystemNotificationResponse { tag, action_id } = response;
|
||||
view.update(cx, |this, cx| {
|
||||
this.status = match action_id {
|
||||
Some(action_id) => {
|
||||
format!("Received action '{action_id}' for tag '{tag}'").into()
|
||||
}
|
||||
None => format!("Notification body clicked for tag '{tag}'").into(),
|
||||
};
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let bounds = Bounds::centered(None, size(px(560.), px(360.)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
titlebar: Some(gpui::TitlebarOptions {
|
||||
title: Some("System Notifications Example".into()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
move |_, _| view,
|
||||
)
|
||||
.expect("failed to open system notifications example window");
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
Vendored
+220
@@ -0,0 +1,220 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, Div, ElementId, FocusHandle, KeyBinding, SharedString, Stateful, Window,
|
||||
WindowBounds, WindowOptions, actions, div, prelude::*, px, size,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
actions!(example, [Tab, TabPrev]);
|
||||
|
||||
struct Example {
|
||||
focus_handle: FocusHandle,
|
||||
items: Vec<FocusHandle>,
|
||||
message: SharedString,
|
||||
}
|
||||
|
||||
impl Example {
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let items = vec![
|
||||
cx.focus_handle().tab_index(1).tab_stop(true),
|
||||
cx.focus_handle().tab_index(2).tab_stop(true),
|
||||
cx.focus_handle().tab_index(3).tab_stop(true),
|
||||
cx.focus_handle(),
|
||||
cx.focus_handle().tab_index(2).tab_stop(true),
|
||||
];
|
||||
|
||||
let focus_handle = cx.focus_handle();
|
||||
window.focus(&focus_handle, cx);
|
||||
|
||||
Self {
|
||||
focus_handle,
|
||||
items,
|
||||
message: SharedString::from("Press `Tab`, `Shift-Tab` to switch focus."),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
|
||||
window.focus_next(cx);
|
||||
self.message = SharedString::from("You have pressed `Tab`.");
|
||||
}
|
||||
|
||||
fn on_tab_prev(&mut self, _: &TabPrev, window: &mut Window, cx: &mut Context<Self>) {
|
||||
window.focus_prev(cx);
|
||||
self.message = SharedString::from("You have pressed `Shift-Tab`.");
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Example {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn tab_stop_style<T: Styled>(this: T) -> T {
|
||||
this.border_3().border_color(gpui::blue())
|
||||
}
|
||||
|
||||
fn button(id: impl Into<ElementId>) -> Stateful<Div> {
|
||||
div()
|
||||
.id(id)
|
||||
.h_10()
|
||||
.flex_1()
|
||||
.flex()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.border_1()
|
||||
.border_color(gpui::black())
|
||||
.bg(gpui::black())
|
||||
.text_color(gpui::white())
|
||||
.focus(tab_stop_style)
|
||||
.shadow_sm()
|
||||
}
|
||||
|
||||
div()
|
||||
.id("app")
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action(cx.listener(Self::on_tab))
|
||||
.on_action(cx.listener(Self::on_tab_prev))
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.p_4()
|
||||
.gap_3()
|
||||
.bg(gpui::white())
|
||||
.text_color(gpui::black())
|
||||
.child(self.message.clone())
|
||||
.children(
|
||||
self.items
|
||||
.clone()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, item_handle)| {
|
||||
div()
|
||||
.id(("item", ix))
|
||||
.track_focus(&item_handle)
|
||||
.h_10()
|
||||
.w_full()
|
||||
.flex()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.border_1()
|
||||
.border_color(gpui::black())
|
||||
.when(
|
||||
item_handle.tab_stop && item_handle.is_focused(window),
|
||||
tab_stop_style,
|
||||
)
|
||||
.map(|this| match item_handle.tab_stop {
|
||||
true => this
|
||||
.hover(|this| this.bg(gpui::black().opacity(0.1)))
|
||||
.child(format!("tab_index: {}", item_handle.tab_index)),
|
||||
false => this.opacity(0.4).child("tab_stop: false"),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.gap_3()
|
||||
.items_center()
|
||||
.child(
|
||||
button("el1")
|
||||
.tab_index(4)
|
||||
.child("Button 1")
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.message = "You have clicked Button 1.".into();
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
button("el2")
|
||||
.tab_index(5)
|
||||
.child("Button 2")
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.message = "You have clicked Button 2.".into();
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("group-1")
|
||||
.tab_index(6)
|
||||
.tab_group()
|
||||
.tab_stop(false)
|
||||
.child(
|
||||
button("group-1-button-1")
|
||||
.tab_index(1)
|
||||
.child("Tab index [6, 1]"),
|
||||
)
|
||||
.child(
|
||||
button("group-1-button-2")
|
||||
.tab_index(2)
|
||||
.child("Tab index [6, 2]"),
|
||||
)
|
||||
.child(
|
||||
button("group-1-button-3")
|
||||
.tab_index(3)
|
||||
.child("Tab index [6, 3]"),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("group-2")
|
||||
.tab_index(7)
|
||||
.tab_group()
|
||||
.tab_stop(false)
|
||||
.child(
|
||||
button("group-2-button-1")
|
||||
.tab_index(1)
|
||||
.child("Tab index [7, 1]"),
|
||||
)
|
||||
.child(
|
||||
button("group-2-button-2")
|
||||
.tab_index(2)
|
||||
.child("Tab index [7, 2]"),
|
||||
)
|
||||
.child(
|
||||
button("group-2-button-3")
|
||||
.tab_index(3)
|
||||
.child("Tab index [7, 3]"),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("tab", Tab, None),
|
||||
KeyBinding::new("shift-tab", TabPrev, None),
|
||||
]);
|
||||
|
||||
let bounds = Bounds::centered(None, size(px(800.), px(600.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| cx.new(|cx| Example::new(window, cx)),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
Vendored
+559
@@ -0,0 +1,559 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
//! Example demonstrating GPUI's testing infrastructure.
|
||||
//!
|
||||
//! When run normally, this displays an interactive counter window.
|
||||
//! The tests below demonstrate various GPUI testing patterns.
|
||||
//!
|
||||
//! Run the app: cargo run -p gpui --example testing
|
||||
//! Run tests: cargo test -p gpui --example testing --features test-support
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, FocusHandle, Focusable, Render, Task, Window, WindowBounds,
|
||||
WindowOptions, actions, div, prelude::*, px, rgb, size,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
actions!(counter, [Increment, Decrement]);
|
||||
|
||||
struct Counter {
|
||||
count: i32,
|
||||
focus_handle: FocusHandle,
|
||||
_subscription: gpui::Subscription,
|
||||
}
|
||||
|
||||
/// Event emitted by Counter
|
||||
struct CounterEvent;
|
||||
|
||||
impl gpui::EventEmitter<CounterEvent> for Counter {}
|
||||
|
||||
impl Counter {
|
||||
fn new(cx: &mut Context<Self>) -> Self {
|
||||
let subscription = cx.subscribe_self(|this: &mut Self, _event: &CounterEvent, _cx| {
|
||||
this.count = 999;
|
||||
});
|
||||
|
||||
Self {
|
||||
count: 0,
|
||||
focus_handle: cx.focus_handle(),
|
||||
_subscription: subscription,
|
||||
}
|
||||
}
|
||||
|
||||
fn increment(&mut self, _: &Increment, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.count += 1;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn decrement(&mut self, _: &Decrement, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.count -= 1;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn load(&self, cx: &mut Context<Self>) -> Task<()> {
|
||||
cx.spawn(async move |this, cx| {
|
||||
// Simulate loading data (e.g., from disk or network)
|
||||
this.update(cx, |counter, _| {
|
||||
counter.count = 100;
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
fn reload(&self, cx: &mut Context<Self>) {
|
||||
cx.spawn(async move |this, cx| {
|
||||
// Simulate reloading data in the background
|
||||
this.update(cx, |counter, _| {
|
||||
counter.count += 50;
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for Counter {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Counter {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.id("counter")
|
||||
.key_context("Counter")
|
||||
.on_action(cx.listener(Self::increment))
|
||||
.on_action(cx.listener(Self::decrement))
|
||||
.track_focus(&self.focus_handle)
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_4()
|
||||
.bg(rgb(0x1e1e2e))
|
||||
.size_full()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.child(
|
||||
div()
|
||||
.text_3xl()
|
||||
.text_color(rgb(0xcdd6f4))
|
||||
.child(format!("{}", self.count)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.id("decrement")
|
||||
.px_4()
|
||||
.py_2()
|
||||
.bg(rgb(0x313244))
|
||||
.hover(|s| s.bg(rgb(0x45475a)))
|
||||
.rounded_md()
|
||||
.cursor_pointer()
|
||||
.text_color(rgb(0xcdd6f4))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.decrement(&Decrement, window, cx)
|
||||
}))
|
||||
.child("−"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("increment")
|
||||
.px_4()
|
||||
.py_2()
|
||||
.bg(rgb(0x313244))
|
||||
.hover(|s| s.bg(rgb(0x45475a)))
|
||||
.rounded_md()
|
||||
.cursor_pointer()
|
||||
.text_color(rgb(0xcdd6f4))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.increment(&Increment, window, cx)
|
||||
}))
|
||||
.child("+"),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.id("load")
|
||||
.px_4()
|
||||
.py_2()
|
||||
.bg(rgb(0x313244))
|
||||
.hover(|s| s.bg(rgb(0x45475a)))
|
||||
.rounded_md()
|
||||
.cursor_pointer()
|
||||
.text_color(rgb(0xcdd6f4))
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.load(cx).detach();
|
||||
}))
|
||||
.child("Load"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("reload")
|
||||
.px_4()
|
||||
.py_2()
|
||||
.bg(rgb(0x313244))
|
||||
.hover(|s| s.bg(rgb(0x45475a)))
|
||||
.rounded_md()
|
||||
.cursor_pointer()
|
||||
.text_color(rgb(0xcdd6f4))
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.reload(cx);
|
||||
}))
|
||||
.child("Reload"),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(rgb(0x6c7086))
|
||||
.child("Press ↑/↓ or click buttons"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
cx.bind_keys([
|
||||
gpui::KeyBinding::new("up", Increment, Some("Counter")),
|
||||
gpui::KeyBinding::new("down", Decrement, Some("Counter")),
|
||||
]);
|
||||
|
||||
let bounds = Bounds::centered(None, size(px(300.), px(200.)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| {
|
||||
let counter = cx.new(|cx| Counter::new(cx));
|
||||
counter.focus_handle(cx).focus(window, cx);
|
||||
counter
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::{TestAppContext, VisualTestContext};
|
||||
use rand::prelude::*;
|
||||
|
||||
/// Here's a basic GPUI test. Just add the macro and take a TestAppContext as an argument!
|
||||
///
|
||||
/// Note that synchronous side effects run immediately after your "update*" calls complete.
|
||||
#[gpui::test]
|
||||
fn basic_testing(cx: &mut TestAppContext) {
|
||||
let counter = cx.new(|cx| Counter::new(cx));
|
||||
|
||||
counter.update(cx, |counter, _| {
|
||||
counter.count = 42;
|
||||
});
|
||||
|
||||
// Note that TestAppContext doesn't support `read(cx)`
|
||||
let updated = counter.read_with(cx, |counter, _| counter.count);
|
||||
assert_eq!(updated, 42);
|
||||
|
||||
// Emit an event - the subscriber will run immediately after the update finishes
|
||||
counter.update(cx, |_, cx| {
|
||||
cx.emit(CounterEvent);
|
||||
});
|
||||
|
||||
let count_after_update = counter.read_with(cx, |counter, _| counter.count);
|
||||
assert_eq!(
|
||||
count_after_update, 999,
|
||||
"Side effects should run after update completes"
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests which involve the window require you to construct a VisualTestContext.
|
||||
/// Just like synchronous side effects, the window will be drawn after every "update*"
|
||||
/// call, so you can test render-dependent behavior.
|
||||
#[gpui::test]
|
||||
fn test_counter_in_window(cx: &mut TestAppContext) {
|
||||
let window = cx.update(|cx| {
|
||||
cx.open_window(Default::default(), |_, cx| cx.new(|cx| Counter::new(cx)))
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
let mut cx = VisualTestContext::from_window(window.into(), cx);
|
||||
let counter = window.root(&mut cx).unwrap();
|
||||
|
||||
// Action dispatch depends on the element tree to resolve which action handler
|
||||
// to call, and this works exactly as you'd expect in a test.
|
||||
let focus_handle = counter.read_with(&cx, |counter, _| counter.focus_handle.clone());
|
||||
cx.update(|window, cx| {
|
||||
focus_handle.dispatch_action(&Increment, window, cx);
|
||||
});
|
||||
|
||||
let count_after = counter.read_with(&cx, |counter, _| counter.count);
|
||||
assert_eq!(
|
||||
count_after, 1,
|
||||
"Action dispatched via focus handle should increment"
|
||||
);
|
||||
}
|
||||
|
||||
/// GPUI tests can also be async, simply add the async keyword before the test.
|
||||
/// Note that the test executor is single thread, so async side effects (including
|
||||
/// background tasks) won't run until you explicitly yield control.
|
||||
#[gpui::test]
|
||||
async fn test_async_operations(cx: &mut TestAppContext) {
|
||||
let counter = cx.new(|cx| Counter::new(cx));
|
||||
|
||||
// Tasks can be awaited directly
|
||||
counter.update(cx, |counter, cx| counter.load(cx)).await;
|
||||
|
||||
let count = counter.read_with(cx, |counter, _| counter.count);
|
||||
assert_eq!(count, 100, "Load task should have set count to 100");
|
||||
|
||||
// But side effects don't run until you yield control
|
||||
counter.update(cx, |counter, cx| counter.reload(cx));
|
||||
|
||||
let count = counter.read_with(cx, |counter, _| counter.count);
|
||||
assert_eq!(count, 100, "Detached reload task shouldn't have run yet");
|
||||
|
||||
// This runs all pending tasks
|
||||
cx.run_until_parked();
|
||||
|
||||
let count = counter.read_with(cx, |counter, _| counter.count);
|
||||
assert_eq!(count, 150, "Reload task should have run after parking");
|
||||
}
|
||||
|
||||
/// Note that the test executor panics if you await a future that waits on
|
||||
/// something outside GPUI's control, like a reading a file or network IO.
|
||||
/// You should mock external systems where possible, as this feature can be used
|
||||
/// to detect potential deadlocks in your async code.
|
||||
///
|
||||
/// However, if you want to disable this check use `allow_parking()`
|
||||
#[gpui::test]
|
||||
async fn test_allow_parking(cx: &mut TestAppContext) {
|
||||
// Allow the thread to park
|
||||
cx.executor().allow_parking();
|
||||
|
||||
// Simulate an external system (like a file system) with an OS thread
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
tx.send(42).ok();
|
||||
});
|
||||
|
||||
// Without allow_parking(), this await would panic because GPUI's
|
||||
// scheduler runs out of tasks while waiting for the external thread.
|
||||
let result = rx.await.unwrap();
|
||||
assert_eq!(result, 42);
|
||||
}
|
||||
|
||||
/// GPUI also provides support for property testing, via the iterations flag
|
||||
#[gpui::test(iterations = 10)]
|
||||
fn test_counter_random_operations(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
let window = cx.update(|cx| {
|
||||
cx.open_window(Default::default(), |_, cx| cx.new(|cx| Counter::new(cx)))
|
||||
.unwrap()
|
||||
});
|
||||
let mut cx = VisualTestContext::from_window(window.into(), cx);
|
||||
|
||||
let counter = cx.new(|cx| Counter::new(cx));
|
||||
|
||||
// Perform random increments/decrements
|
||||
let mut expected = 0i32;
|
||||
for _ in 0..100 {
|
||||
if rng.random_bool(0.5) {
|
||||
expected += 1;
|
||||
counter.update_in(&mut cx, |counter, window, cx| {
|
||||
counter.increment(&Increment, window, cx)
|
||||
});
|
||||
} else {
|
||||
expected -= 1;
|
||||
counter.update_in(&mut cx, |counter, window, cx| {
|
||||
counter.decrement(&Decrement, window, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let actual = counter.read_with(&cx, |counter, _| counter.count);
|
||||
assert_eq!(
|
||||
actual, expected,
|
||||
"Counter should match expected after random ops"
|
||||
);
|
||||
}
|
||||
|
||||
/// Now, all of those tests are good, but GPUI also provides strong support for testing distributed systems.
|
||||
/// Let's setup a mock network and enhance the counter to send messages over it.
|
||||
mod distributed_systems {
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// The state of the mock network.
|
||||
struct MockNetworkState {
|
||||
ordering: Vec<i32>,
|
||||
a_to_b: Vec<i32>,
|
||||
b_to_a: Vec<i32>,
|
||||
}
|
||||
|
||||
/// A mock network that delivers messages between two peers.
|
||||
#[derive(Clone)]
|
||||
struct MockNetwork {
|
||||
state: Arc<Mutex<MockNetworkState>>,
|
||||
}
|
||||
|
||||
impl MockNetwork {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
state: Arc::new(Mutex::new(MockNetworkState {
|
||||
ordering: Vec::new(),
|
||||
a_to_b: Vec::new(),
|
||||
b_to_a: Vec::new(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
fn a_client(&self) -> NetworkClient {
|
||||
NetworkClient {
|
||||
network: self.clone(),
|
||||
is_a: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn b_client(&self) -> NetworkClient {
|
||||
NetworkClient {
|
||||
network: self.clone(),
|
||||
is_a: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A client handle for sending/receiving messages over the mock network.
|
||||
#[derive(Clone)]
|
||||
struct NetworkClient {
|
||||
network: MockNetwork,
|
||||
is_a: bool,
|
||||
}
|
||||
|
||||
// See, networking is easy!
|
||||
impl NetworkClient {
|
||||
fn send(&self, value: i32) {
|
||||
let mut network = self.network.state.lock().unwrap();
|
||||
network.ordering.push(value);
|
||||
if self.is_a {
|
||||
network.b_to_a.push(value);
|
||||
} else {
|
||||
network.a_to_b.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
fn receive_all(&self) -> Vec<i32> {
|
||||
let mut network = self.network.state.lock().unwrap();
|
||||
if self.is_a {
|
||||
network.a_to_b.drain(..).collect()
|
||||
} else {
|
||||
network.b_to_a.drain(..).collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use gpui::Context;
|
||||
|
||||
/// A networked counter that can send/receive over a mock network.
|
||||
struct NetworkedCounter {
|
||||
count: i32,
|
||||
client: NetworkClient,
|
||||
}
|
||||
|
||||
impl NetworkedCounter {
|
||||
fn new(client: NetworkClient) -> Self {
|
||||
Self { count: 0, client }
|
||||
}
|
||||
|
||||
/// Increment the counter and broadcast the change.
|
||||
fn increment(&mut self, delta: i32, cx: &mut Context<Self>) {
|
||||
self.count += delta;
|
||||
|
||||
cx.background_spawn({
|
||||
let client = self.client.clone();
|
||||
async move {
|
||||
client.send(delta);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// Process incoming increment requests.
|
||||
fn sync(&mut self) {
|
||||
for delta in self.client.receive_all() {
|
||||
self.count += delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use super::*;
|
||||
|
||||
/// You can simulate distributed systems with multiple app contexts, simply by adding
|
||||
/// additional parameters.
|
||||
#[gpui::test]
|
||||
fn test_app_sync(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
let network = MockNetwork::new();
|
||||
|
||||
let a = cx_a.new(|_| NetworkedCounter::new(network.a_client()));
|
||||
let b = cx_b.new(|_| NetworkedCounter::new(network.b_client()));
|
||||
|
||||
// B increments locally and broadcasts the delta
|
||||
b.update(cx_b, |b, cx| b.increment(42, cx));
|
||||
b.read_with(cx_b, |b, _| assert_eq!(b.count, 42)); // B's count is set immediately
|
||||
a.read_with(cx_a, |a, _| assert_eq!(a.count, 0)); // A's count is in a side effect
|
||||
|
||||
cx_b.run_until_parked(); // Send the delta from B
|
||||
a.update(cx_a, |a, _| a.sync()); // Receive the delta at A
|
||||
|
||||
b.read_with(cx_b, |b, _| assert_eq!(b.count, 42)); // Both counts now match
|
||||
a.read_with(cx_a, |a, _| assert_eq!(a.count, 42));
|
||||
}
|
||||
|
||||
/// Multiple apps can run concurrently, and to capture this each test app shares
|
||||
/// a dispatcher. Whenever you call `run_until_parked`, the dispatcher will randomly
|
||||
/// pick which app's tasks to run next. This allows you to test that your distributed code
|
||||
/// is robust to different execution orderings.
|
||||
#[gpui::test(iterations = 10)]
|
||||
fn test_random_interleaving(
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
mut rng: StdRng,
|
||||
) {
|
||||
let network = MockNetwork::new();
|
||||
|
||||
// Track execution order
|
||||
let mut original_order = Vec::new();
|
||||
let a = cx_a.new(|_| NetworkedCounter::new(MockNetwork::a_client(&network)));
|
||||
let b = cx_b.new(|_| NetworkedCounter::new(MockNetwork::b_client(&network)));
|
||||
|
||||
let num_operations: usize = rng.random_range(3..8);
|
||||
|
||||
for i in 0..num_operations {
|
||||
let i = i as i32;
|
||||
let which = rng.random_bool(0.5);
|
||||
|
||||
original_order.push(i);
|
||||
if which {
|
||||
b.update(cx_b, |b, cx| b.increment(i, cx));
|
||||
} else {
|
||||
a.update(cx_a, |a, cx| a.increment(i, cx));
|
||||
}
|
||||
}
|
||||
|
||||
// This will send all of the pending increment messages, from both a and b
|
||||
cx_a.run_until_parked();
|
||||
|
||||
a.update(cx_a, |a, _| a.sync());
|
||||
b.update(cx_b, |b, _| b.sync());
|
||||
|
||||
let a_count = a.read_with(cx_a, |a, _| a.count);
|
||||
let b_count = b.read_with(cx_b, |b, _| b.count);
|
||||
|
||||
assert_eq!(a_count, b_count, "A and B should have the same count");
|
||||
|
||||
// Nicely format the execution order output.
|
||||
// Run this test with `-- --nocapture` to see it!
|
||||
let actual = network.state.lock().unwrap().ordering.clone();
|
||||
let spawned: Vec<_> = original_order.iter().map(|n| format!("{}", n)).collect();
|
||||
let ran: Vec<_> = actual.iter().map(|n| format!("{}", n)).collect();
|
||||
let diff: Vec<_> = original_order
|
||||
.iter()
|
||||
.zip(actual.iter())
|
||||
.map(|(o, a)| {
|
||||
if o == a {
|
||||
" ".to_string()
|
||||
} else {
|
||||
"^".to_string()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
println!("spawned: [{}]", spawned.join(", "));
|
||||
println!("ran: [{}]", ran.join(", "));
|
||||
println!(" [{}]", diff.join(", "));
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+432
@@ -0,0 +1,432 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
ops::{Deref, DerefMut},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use gpui::{
|
||||
AbsoluteLength, App, Context, DefiniteLength, ElementId, Global, Hsla, Menu, SharedString,
|
||||
TextStyle, TitlebarOptions, Window, WindowBounds, WindowOptions, bounds, colors::DefaultColors,
|
||||
div, point, prelude::*, px, relative, rgb, size,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
use std::iter;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TextContext {
|
||||
font_size: f32,
|
||||
line_height: f32,
|
||||
type_scale: f32,
|
||||
}
|
||||
|
||||
impl Default for TextContext {
|
||||
fn default() -> Self {
|
||||
TextContext {
|
||||
font_size: 16.0,
|
||||
line_height: 1.3,
|
||||
type_scale: 1.33,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TextContext {
|
||||
pub fn get_global(cx: &App) -> &Arc<TextContext> {
|
||||
&cx.global::<GlobalTextContext>().0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GlobalTextContext(pub Arc<TextContext>);
|
||||
|
||||
impl Deref for GlobalTextContext {
|
||||
type Target = Arc<TextContext>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for GlobalTextContext {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Global for GlobalTextContext {}
|
||||
|
||||
pub trait ActiveTextContext {
|
||||
fn text_context(&self) -> &Arc<TextContext>;
|
||||
}
|
||||
|
||||
impl ActiveTextContext for App {
|
||||
fn text_context(&self) -> &Arc<TextContext> {
|
||||
&self.global::<GlobalTextContext>().0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct SpecimenTheme {
|
||||
pub bg: Hsla,
|
||||
pub fg: Hsla,
|
||||
}
|
||||
|
||||
impl Default for SpecimenTheme {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
bg: gpui::white(),
|
||||
fg: gpui::black(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SpecimenTheme {
|
||||
pub fn invert(&self) -> Self {
|
||||
Self {
|
||||
bg: self.fg,
|
||||
fg: self.bg,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, IntoElement)]
|
||||
struct Specimen {
|
||||
id: ElementId,
|
||||
scale: f32,
|
||||
text_style: Option<TextStyle>,
|
||||
string: SharedString,
|
||||
invert: bool,
|
||||
}
|
||||
|
||||
impl Specimen {
|
||||
pub fn new(id: usize) -> Self {
|
||||
let string = SharedString::new_static("The quick brown fox jumps over the lazy dog");
|
||||
let id_string = format!("specimen-{}", id);
|
||||
let id = ElementId::Name(id_string.into());
|
||||
Self {
|
||||
id,
|
||||
scale: 1.0,
|
||||
text_style: None,
|
||||
string,
|
||||
invert: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invert(mut self) -> Self {
|
||||
self.invert = !self.invert;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn scale(mut self, scale: f32) -> Self {
|
||||
self.scale = scale;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Specimen {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let rem_size = window.rem_size();
|
||||
let scale = self.scale;
|
||||
let global_style = cx.text_context();
|
||||
|
||||
let style_override = self.text_style;
|
||||
|
||||
let mut font_size = global_style.font_size;
|
||||
let mut line_height = global_style.line_height;
|
||||
|
||||
if let Some(style_override) = style_override {
|
||||
font_size = style_override.font_size.to_pixels(rem_size).into();
|
||||
line_height = match style_override.line_height {
|
||||
DefiniteLength::Absolute(absolute_len) => match absolute_len {
|
||||
AbsoluteLength::Rems(absolute_len) => absolute_len.to_pixels(rem_size).into(),
|
||||
AbsoluteLength::Pixels(absolute_len) => absolute_len.into(),
|
||||
},
|
||||
DefiniteLength::Fraction(value) => value,
|
||||
};
|
||||
}
|
||||
|
||||
let mut theme = SpecimenTheme::default();
|
||||
|
||||
if self.invert {
|
||||
theme = theme.invert();
|
||||
}
|
||||
|
||||
div()
|
||||
.id(self.id)
|
||||
.bg(theme.bg)
|
||||
.text_color(theme.fg)
|
||||
.text_size(px(font_size * scale))
|
||||
.line_height(relative(line_height))
|
||||
.p(px(10.0))
|
||||
.child(self.string)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, IntoElement)]
|
||||
struct CharacterGrid {
|
||||
scale: f32,
|
||||
invert: bool,
|
||||
text_style: Option<TextStyle>,
|
||||
}
|
||||
|
||||
impl CharacterGrid {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
scale: 1.0,
|
||||
invert: false,
|
||||
text_style: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scale(mut self, scale: f32) -> Self {
|
||||
self.scale = scale;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for CharacterGrid {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let mut theme = SpecimenTheme::default();
|
||||
|
||||
if self.invert {
|
||||
theme = theme.invert();
|
||||
}
|
||||
|
||||
let characters = vec![
|
||||
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "A", "B", "C", "D", "E", "F", "G",
|
||||
"H", "I", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y",
|
||||
"Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "p", "q",
|
||||
"r", "s", "t", "u", "v", "w", "x", "y", "z", "ẞ", "ſ", "ß", "ð", "Þ", "þ", "α", "β",
|
||||
"Γ", "γ", "Δ", "δ", "η", "θ", "ι", "κ", "Λ", "λ", "μ", "ν", "ξ", "π", "τ", "υ", "φ",
|
||||
"χ", "ψ", "∂", "а", "в", "Ж", "ж", "З", "з", "К", "к", "л", "м", "Н", "н", "Р", "р",
|
||||
"У", "у", "ф", "ч", "ь", "ы", "Э", "э", "Я", "я", "ij", "öẋ", ".,", "⣝⣑", "~", "*",
|
||||
"_", "^", "`", "'", "(", "{", "«", "#", "&", "@", "$", "¢", "%", "|", "?", "¶", "µ",
|
||||
"❮", "<=", "!=", "==", "--", "++", "=>", "->", "🏀", "🎊", "😍", "❤️", "👍", "👎",
|
||||
];
|
||||
|
||||
let columns = 20;
|
||||
let rows = characters.len().div_ceil(columns);
|
||||
|
||||
let grid_rows = (0..rows).map(|row_idx| {
|
||||
let start_idx = row_idx * columns;
|
||||
let end_idx = (start_idx + columns).min(characters.len());
|
||||
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.children((start_idx..end_idx).map(|i| {
|
||||
div()
|
||||
.text_center()
|
||||
.size(px(62.))
|
||||
.bg(theme.bg)
|
||||
.text_color(theme.fg)
|
||||
.text_size(px(24.0))
|
||||
.line_height(relative(1.0))
|
||||
.child(characters[i])
|
||||
}))
|
||||
.when(end_idx - start_idx < columns, |d| {
|
||||
d.children(
|
||||
iter::repeat_with(|| div().flex_1()).take(columns - (end_idx - start_idx)),
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
div().p_4().gap_2().flex().flex_col().children(grid_rows)
|
||||
}
|
||||
}
|
||||
|
||||
struct TextExample {
|
||||
next_id: usize,
|
||||
font_family: SharedString,
|
||||
}
|
||||
|
||||
impl TextExample {
|
||||
fn next_id(&mut self) -> usize {
|
||||
self.next_id += 1;
|
||||
self.next_id
|
||||
}
|
||||
|
||||
fn button(
|
||||
text: &str,
|
||||
cx: &mut Context<Self>,
|
||||
on_click: impl Fn(&mut Self, &mut Context<Self>) + 'static,
|
||||
) -> impl IntoElement {
|
||||
div()
|
||||
.id(text.to_string())
|
||||
.flex_none()
|
||||
.child(text.to_string())
|
||||
.bg(gpui::black())
|
||||
.text_color(gpui::white())
|
||||
.active(|this| this.opacity(0.8))
|
||||
.px_3()
|
||||
.py_1()
|
||||
.on_click(cx.listener(move |this, _, _, cx| on_click(this, cx)))
|
||||
}
|
||||
}
|
||||
|
||||
const FONT_FAMILIES: [&str; 5] = [
|
||||
".ZedMono",
|
||||
".SystemUIFont",
|
||||
"Menlo",
|
||||
"Monaco",
|
||||
"Courier New",
|
||||
];
|
||||
|
||||
impl Render for TextExample {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let tcx = cx.text_context();
|
||||
let colors = cx.default_colors().clone();
|
||||
|
||||
let font_size = tcx.font_size;
|
||||
let line_height = tcx.line_height;
|
||||
let type_scale = tcx.type_scale;
|
||||
|
||||
let step_down_2 = 1.0 / (type_scale * type_scale);
|
||||
let step_down_1 = 1.0 / type_scale;
|
||||
let base = 1.0;
|
||||
let step_up_1 = base * type_scale;
|
||||
let step_up_2 = step_up_1 * type_scale;
|
||||
let step_up_3 = step_up_2 * type_scale;
|
||||
let step_up_4 = step_up_3 * type_scale;
|
||||
let step_up_5 = step_up_4 * type_scale;
|
||||
let step_up_6 = step_up_5 * type_scale;
|
||||
|
||||
div()
|
||||
.font_family(self.font_family.clone())
|
||||
.size_full()
|
||||
.child(
|
||||
div()
|
||||
.bg(gpui::white())
|
||||
.border_b_1()
|
||||
.border_color(gpui::black())
|
||||
.p_3()
|
||||
.flex()
|
||||
.child(Self::button(&self.font_family, cx, |this, cx| {
|
||||
let new_family = FONT_FAMILIES
|
||||
.iter()
|
||||
.position(|f| *f == this.font_family.as_str())
|
||||
.map(|idx| FONT_FAMILIES[(idx + 1) % FONT_FAMILIES.len()])
|
||||
.unwrap_or(FONT_FAMILIES[0]);
|
||||
|
||||
this.font_family = SharedString::new(new_family);
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("text-example")
|
||||
.overflow_y_scroll()
|
||||
.overflow_x_hidden()
|
||||
.bg(rgb(0xffffff))
|
||||
.size_full()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.p_3()
|
||||
.text_size(px(font_size))
|
||||
.line_height(relative(line_height))
|
||||
.children([
|
||||
div().child("CJK: 中文「日本語」한글"),
|
||||
div().child(
|
||||
"Kana: が / か\u{3099}; Hangul: 각 / \u{1100}\u{1161}\u{11a8}",
|
||||
),
|
||||
div().child("Emoji: 😀 ❤️ ❤\u{fe0e} 👍🏽 🇯🇵 1\u{fe0f}\u{20e3} 👩💻 👨👩👧👦"),
|
||||
]),
|
||||
)
|
||||
.child(div().child(CharacterGrid::new().scale(base)))
|
||||
.child(
|
||||
div()
|
||||
.child(Specimen::new(self.next_id()).scale(step_down_2))
|
||||
.child(Specimen::new(self.next_id()).scale(step_down_2).invert())
|
||||
.child(Specimen::new(self.next_id()).scale(step_down_1))
|
||||
.child(Specimen::new(self.next_id()).scale(step_down_1).invert())
|
||||
.child(Specimen::new(self.next_id()).scale(base))
|
||||
.child(Specimen::new(self.next_id()).scale(base).invert())
|
||||
.child(Specimen::new(self.next_id()).scale(step_up_1))
|
||||
.child(Specimen::new(self.next_id()).scale(step_up_1).invert())
|
||||
.child(Specimen::new(self.next_id()).scale(step_up_2))
|
||||
.child(Specimen::new(self.next_id()).scale(step_up_2).invert())
|
||||
.child(Specimen::new(self.next_id()).scale(step_up_3))
|
||||
.child(Specimen::new(self.next_id()).scale(step_up_3).invert())
|
||||
.child(Specimen::new(self.next_id()).scale(step_up_4))
|
||||
.child(Specimen::new(self.next_id()).scale(step_up_4).invert())
|
||||
.child(Specimen::new(self.next_id()).scale(step_up_5))
|
||||
.child(Specimen::new(self.next_id()).scale(step_up_5).invert())
|
||||
.child(Specimen::new(self.next_id()).scale(step_up_6))
|
||||
.child(Specimen::new(self.next_id()).scale(step_up_6).invert()),
|
||||
),
|
||||
)
|
||||
.child(div().w(px(240.)).h_full().bg(colors.container))
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
cx.set_menus(vec![Menu {
|
||||
name: "GPUI Typography".into(),
|
||||
disabled: false,
|
||||
items: vec![],
|
||||
}]);
|
||||
|
||||
let fonts = [include_bytes!(
|
||||
"../../../assets/fonts/lilex/Lilex-Regular.ttf"
|
||||
)]
|
||||
.iter()
|
||||
.map(|b| Cow::Borrowed(&b[..]))
|
||||
.collect();
|
||||
|
||||
_ = cx.text_system().add_fonts(fonts);
|
||||
|
||||
cx.init_colors();
|
||||
cx.set_global(GlobalTextContext(Arc::new(TextContext::default())));
|
||||
|
||||
let window = cx
|
||||
.open_window(
|
||||
WindowOptions {
|
||||
titlebar: Some(TitlebarOptions {
|
||||
title: Some("GPUI Typography".into()),
|
||||
..Default::default()
|
||||
}),
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds(
|
||||
point(px(0.0), px(0.0)),
|
||||
size(px(920.), px(720.)),
|
||||
))),
|
||||
..Default::default()
|
||||
},
|
||||
|_window, cx| {
|
||||
cx.new(|_cx| TextExample {
|
||||
next_id: 0,
|
||||
font_family: ".ZedMono".into(),
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
window
|
||||
.update(cx, |_view, _window, cx| {
|
||||
cx.activate(true);
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, FontStyle, FontWeight, StyledText, Window, WindowBounds, WindowOptions,
|
||||
div, prelude::*, px, size,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
struct HelloWorld {}
|
||||
|
||||
impl Render for HelloWorld {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.bg(gpui::white())
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.p_4()
|
||||
.size_full()
|
||||
.child(div().child("Text left"))
|
||||
.child(div().text_center().child("Text center"))
|
||||
.child(div().text_right().child("Text right"))
|
||||
.child(div().text_decoration_1().child("Text left (underline)"))
|
||||
.child(
|
||||
div()
|
||||
.text_center()
|
||||
.text_decoration_1()
|
||||
.child("Text center (underline)"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_right()
|
||||
.text_decoration_1()
|
||||
.child("Text right (underline)"),
|
||||
)
|
||||
.child(div().line_through().child("Text left (line_through)"))
|
||||
.child(
|
||||
div()
|
||||
.text_center()
|
||||
.line_through()
|
||||
.child("Text center (line_through)"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_right()
|
||||
.line_through()
|
||||
.child("Text right (line_through)"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(
|
||||
div()
|
||||
.w(px(400.))
|
||||
.border_1()
|
||||
.border_color(gpui::blue())
|
||||
.p_1()
|
||||
.whitespace_nowrap()
|
||||
.overflow_hidden()
|
||||
.text_center()
|
||||
.child("A long non-wrapping text align center"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w_32()
|
||||
.border_1()
|
||||
.border_color(gpui::blue())
|
||||
.p_1()
|
||||
.whitespace_nowrap()
|
||||
.overflow_hidden()
|
||||
.text_right()
|
||||
.child("100%"),
|
||||
),
|
||||
)
|
||||
.child(div().flex().gap_2().justify_between().child(
|
||||
StyledText::new("ABCD").with_highlights([
|
||||
(0..1, FontWeight::EXTRA_BOLD.into()),
|
||||
(2..3, FontStyle::Italic.into()),
|
||||
]),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| cx.new(|_| HelloWorld {}),
|
||||
)
|
||||
.unwrap();
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, TextOverflow, Window, WindowBounds, WindowOptions, div, prelude::*, px,
|
||||
size,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
struct HelloWorld {}
|
||||
|
||||
impl Render for HelloWorld {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let text = "The longest word 你好世界这段是中文,こんにちはこの段落は日本語です in any of the major \
|
||||
English language dictionaries is pneumonoultramicroscopicsilicovolcanoconiosis, a word that \
|
||||
refers to a lung disease contracted from the inhalation of very fine silica particles, \
|
||||
a url https://github.com/zed-industries/zed/pull/35724?query=foo&bar=2, \
|
||||
specifically from a volcano; medically, it is the same as silicosis.";
|
||||
div()
|
||||
.id("page")
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.bg(gpui::white())
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.flex_shrink_0()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.border_1()
|
||||
.border_color(gpui::red())
|
||||
.text_ellipsis()
|
||||
.child("longer text in flex 1"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.border_1()
|
||||
.border_color(gpui::red())
|
||||
.text_ellipsis()
|
||||
.child("short flex"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.overflow_hidden()
|
||||
.border_1()
|
||||
.border_color(gpui::red())
|
||||
.text_ellipsis()
|
||||
.w_full()
|
||||
.child("A short text in normal div"),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.text_xl()
|
||||
.truncate()
|
||||
.border_1()
|
||||
.border_color(gpui::blue())
|
||||
.child("ELLIPSIS: ".to_owned() + text),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.text_xl()
|
||||
.overflow_hidden()
|
||||
.text_ellipsis()
|
||||
.line_clamp(2)
|
||||
.border_1()
|
||||
.border_color(gpui::blue())
|
||||
.child("ELLIPSIS 2 lines: ".to_owned() + text),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.text_xl()
|
||||
.overflow_hidden()
|
||||
.text_overflow(TextOverflow::Truncate("".into()))
|
||||
.border_1()
|
||||
.border_color(gpui::green())
|
||||
.child("TRUNCATE: ".to_owned() + text),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.text_xl()
|
||||
.overflow_hidden()
|
||||
.text_overflow(TextOverflow::Truncate("".into()))
|
||||
.line_clamp(3)
|
||||
.border_1()
|
||||
.border_color(gpui::green())
|
||||
.child("TRUNCATE 3 lines: ".to_owned() + text),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.text_xl()
|
||||
.whitespace_nowrap()
|
||||
.overflow_hidden()
|
||||
.border_1()
|
||||
.border_color(gpui::black())
|
||||
.child("NOWRAP: ".to_owned() + text),
|
||||
)
|
||||
.child(div().text_xl().w_full().child(text))
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| cx.new(|_| HelloWorld {}),
|
||||
)
|
||||
.unwrap();
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
+309
@@ -0,0 +1,309 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use std::{
|
||||
io::{self, BufRead as _},
|
||||
sync::mpsc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use gpui::{
|
||||
AnyWindowHandle, App, AppContext as _, AsyncApp, Context, Entity, GraphicalEnvironment,
|
||||
QuitMode, Render, Subscription, TitlebarOptions, Window, WindowOptions, WindowingRequest, div,
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
struct Todo {
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
struct Todos {
|
||||
items: Vec<Entity<Todo>>,
|
||||
window: Option<AnyWindowHandle>,
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
struct TodoWindow {
|
||||
todos: Entity<Todos>,
|
||||
_todos_subscription: Subscription,
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
impl Render for TodoWindow {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let rows = self
|
||||
.todos
|
||||
.read(cx)
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, todo)| div().child(describe_todo(index, todo, cx)))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.bg(gpui::rgb(0xffffff))
|
||||
.text_color(gpui::rgb(0x202020))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.p_4()
|
||||
.child("Switchable display todos")
|
||||
.child("Use the terminal: create [message], ls, close, open, quit")
|
||||
.when(rows.is_empty(), |view| {
|
||||
view.child("No todos yet. Type `create` in the terminal.")
|
||||
})
|
||||
.children(rows)
|
||||
}
|
||||
}
|
||||
|
||||
/// English, Chinese, Japanese and Korean for a word, then an emoji for it.
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
const SAMPLE_TODOS: [&str; 5] = [
|
||||
"milk / 牛奶 / 牛乳 / 우유 🥛",
|
||||
"apple / 苹果 / りんご / 사과 🍎",
|
||||
"cat / 猫 / ねこ / 고양이 🐱",
|
||||
"book / 书 / 本 / 책 📚",
|
||||
"rain / 雨 / あめ / 비 🌧️",
|
||||
];
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn describe_todo(index: usize, todo: &Entity<Todo>, cx: &App) -> String {
|
||||
let todo_reference: &Todo = todo.read(cx);
|
||||
format!(
|
||||
"{index} - {} - {:?} @{:p}",
|
||||
todo_reference.message,
|
||||
todo.entity_id(),
|
||||
todo_reference
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn print_todos(todos: &Entity<Todos>, cx: &App) {
|
||||
if todos.read(cx).items.is_empty() {
|
||||
println!("no todos");
|
||||
}
|
||||
for (index, todo) in todos.read(cx).items.iter().enumerate() {
|
||||
println!("{}", describe_todo(index, todo, cx));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn open_window(todos: &Entity<Todos>, cx: &mut App) -> anyhow::Result<()> {
|
||||
if todos.read(cx).window.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let options = WindowOptions {
|
||||
titlebar: Some(TitlebarOptions {
|
||||
title: Some("Switchable display todos".into()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let handle = cx.open_window(options, |_, cx| {
|
||||
cx.new(|cx| TodoWindow {
|
||||
todos: todos.clone(),
|
||||
_todos_subscription: cx.observe(todos, |_, _, cx| cx.notify()),
|
||||
})
|
||||
})?;
|
||||
// On macOS, making the window key does not activate the app over the terminal.
|
||||
cx.activate(true);
|
||||
handle.update(cx, |_, window, _| window.activate_window())?;
|
||||
todos.update(cx, |todos, _| todos.window = Some(handle.into()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Builds the environment for `open`: the given `NAME=value` pairs, or this process's
|
||||
/// environment when none are given.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn display_environment(arguments: &str) -> anyhow::Result<GraphicalEnvironment> {
|
||||
if arguments.trim().is_empty() {
|
||||
return Ok(GraphicalEnvironment::detect());
|
||||
}
|
||||
let mut environment = GraphicalEnvironment::default();
|
||||
for assignment in arguments.split_whitespace() {
|
||||
let (name, value) = assignment
|
||||
.split_once('=')
|
||||
.ok_or_else(|| anyhow::anyhow!("expected NAME=value, got {assignment:?}"))?;
|
||||
match name {
|
||||
"WAYLAND_DISPLAY" => environment.wayland_display = Some(value.into()),
|
||||
"DISPLAY" => environment.x11_display = Some(value.into()),
|
||||
"XDG_RUNTIME_DIR" => environment.xdg_runtime_dir = Some(value.into()),
|
||||
"XDG_ACTIVATION_TOKEN" => environment.activation_token = Some(value.into()),
|
||||
_ => anyhow::bail!("unknown display variable {name}"),
|
||||
}
|
||||
}
|
||||
Ok(environment)
|
||||
}
|
||||
|
||||
/// Builds the environment for `open`: `SESSION=id`, or this process's session when not given.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn display_environment(arguments: &str) -> anyhow::Result<GraphicalEnvironment> {
|
||||
let mut environment = GraphicalEnvironment::detect();
|
||||
for assignment in arguments.split_whitespace() {
|
||||
let session = assignment
|
||||
.strip_prefix("SESSION=")
|
||||
.ok_or_else(|| anyhow::anyhow!("expected SESSION=id, got {assignment:?}"))?;
|
||||
environment.session_id = Some(session.parse()?);
|
||||
}
|
||||
Ok(environment)
|
||||
}
|
||||
|
||||
/// Builds the environment for `open`, which carries nothing on this platform.
|
||||
#[cfg(not(any(target_os = "linux", target_os = "windows", target_family = "wasm")))]
|
||||
fn display_environment(_arguments: &str) -> anyhow::Result<GraphicalEnvironment> {
|
||||
Ok(GraphicalEnvironment::detect())
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn handle_command(
|
||||
command: &str,
|
||||
todos: &Entity<Todos>,
|
||||
cx: &mut App,
|
||||
) -> anyhow::Result<Option<WindowingRequest>> {
|
||||
let (name, arguments) = command.split_once(' ').unwrap_or((command, ""));
|
||||
match name {
|
||||
"ls" => print_todos(todos, cx),
|
||||
"create" => {
|
||||
// A sample with CJK text and an emoji, so that drawing todos exercises color glyph
|
||||
// rasterization, which uses the GPU devices a switch to windowed mode creates.
|
||||
let count = todos.read(cx).items.len();
|
||||
let sample = SAMPLE_TODOS[count % SAMPLE_TODOS.len()];
|
||||
let message = if arguments.trim().is_empty() {
|
||||
sample.to_owned()
|
||||
} else {
|
||||
format!("{arguments} · {sample}")
|
||||
};
|
||||
let todo = cx.new(|_| Todo { message });
|
||||
todos.update(cx, |todos, cx| {
|
||||
todos.items.push(todo);
|
||||
println!("todo added, total {}", todos.items.len());
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
"open" => {
|
||||
return Ok(Some(WindowingRequest::Windowed(display_environment(
|
||||
arguments,
|
||||
)?)));
|
||||
}
|
||||
"close" => {
|
||||
let window = todos.update(cx, |todos, _| todos.window.take());
|
||||
if let Some(window) = window {
|
||||
window.update(cx, |_, window, _| window.remove_window())?;
|
||||
}
|
||||
return Ok(Some(WindowingRequest::Headless));
|
||||
}
|
||||
"quit" => cx.quit(),
|
||||
"" => {}
|
||||
_ => println!("{USAGE}"),
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
const USAGE: &str = "commands: ls | create [message] | open [DISPLAY=… WAYLAND_DISPLAY=… XDG_RUNTIME_DIR=… XDG_ACTIVATION_TOKEN=…] | close | quit";
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const USAGE: &str = "commands: ls | create [message] | open [SESSION=…] | close | quit";
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "windows", target_family = "wasm")))]
|
||||
const USAGE: &str = "commands: ls | create [message] | open | close | quit";
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
async fn run_command(
|
||||
command: String,
|
||||
todos: &Entity<Todos>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> anyhow::Result<()> {
|
||||
match cx.update(|cx| handle_command(&command, todos, cx))? {
|
||||
None => return Ok(()),
|
||||
Some(request @ WindowingRequest::Headless) => {
|
||||
if cx.update(|cx| cx.graphical_environment()).is_some() {
|
||||
cx.update(|cx| cx.request_windowing(request)).await?;
|
||||
}
|
||||
}
|
||||
Some(request @ WindowingRequest::Windowed(_)) => {
|
||||
// Switching fails while windowed, so `open` then just opens the window.
|
||||
if cx.update(|cx| cx.graphical_environment()).is_none() {
|
||||
cx.update(|cx| cx.request_windowing(request)).await?;
|
||||
}
|
||||
cx.update(|cx| open_window(todos, cx))?;
|
||||
}
|
||||
}
|
||||
let mode = cx.update(|cx| match cx.graphical_environment() {
|
||||
None => "headless".to_owned(),
|
||||
Some(_) => match cx.compositor_name() {
|
||||
"" => "windowed".to_owned(),
|
||||
compositor => compositor.to_owned(),
|
||||
},
|
||||
});
|
||||
println!("mode: {mode}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
#[cfg(target_os = "linux")]
|
||||
let application = gpui_platform::linux(gpui::WindowingModes::all());
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let application = gpui_platform::application();
|
||||
application
|
||||
.with_windowing(WindowingRequest::Headless)
|
||||
.with_quit_mode(QuitMode::Explicit)
|
||||
.run(|cx| {
|
||||
let todos = cx.new(|_| Todos {
|
||||
items: Vec::new(),
|
||||
window: None,
|
||||
});
|
||||
let window_closed_subscription = cx.on_window_closed({
|
||||
let todos = todos.clone();
|
||||
move |cx, window_id| {
|
||||
todos.update(cx, |todos, _| {
|
||||
if todos
|
||||
.window
|
||||
.as_ref()
|
||||
.is_some_and(|window| window.window_id() == window_id)
|
||||
{
|
||||
todos.window = None;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
let (command_sender, command_receiver) = mpsc::channel();
|
||||
std::thread::spawn(move || {
|
||||
for line in io::stdin().lock().lines() {
|
||||
match line {
|
||||
Ok(line) => {
|
||||
if command_sender.send(line).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("stdin error: {error}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
println!("{USAGE}");
|
||||
cx.spawn(async move |cx| {
|
||||
let _window_closed_subscription = window_closed_subscription;
|
||||
loop {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(25))
|
||||
.await;
|
||||
while let Ok(command) = command_receiver.try_recv() {
|
||||
if let Err(error) = run_command(command, &todos, cx).await {
|
||||
println!("error: {error:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
}
|
||||
Vendored
+64
@@ -0,0 +1,64 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
//! Renders a div with deep children hierarchy. This example is useful to exemplify that Zed can
|
||||
//! handle deep hierarchies (even though it cannot just yet!).
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use gpui::{App, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px, size};
|
||||
use gpui_platform::application;
|
||||
|
||||
struct Tree {}
|
||||
|
||||
static DEPTH: LazyLock<u64> = LazyLock::new(|| {
|
||||
std::env::var("GPUI_TREE_DEPTH")
|
||||
.ok()
|
||||
.and_then(|depth| depth.parse().ok())
|
||||
.unwrap_or_else(|| 50)
|
||||
});
|
||||
|
||||
impl Render for Tree {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
let mut depth = *DEPTH;
|
||||
static COLORS: [gpui::Hsla; 4] = [gpui::red(), gpui::blue(), gpui::green(), gpui::yellow()];
|
||||
let mut colors = COLORS.iter().cycle().copied();
|
||||
let mut next_div = || div().p_0p5().bg(colors.next().unwrap());
|
||||
let mut innermost_node = next_div();
|
||||
while depth > 0 {
|
||||
innermost_node = next_div().child(innermost_node);
|
||||
depth -= 1;
|
||||
}
|
||||
innermost_node
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| cx.new(|_| Tree {}),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px, rgb, size,
|
||||
uniform_list,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
struct UniformListExample {}
|
||||
|
||||
impl Render for UniformListExample {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().size_full().bg(rgb(0xffffff)).child(
|
||||
uniform_list(
|
||||
"entries",
|
||||
50,
|
||||
cx.processor(|_this, range, _window, _cx| {
|
||||
let mut items = Vec::new();
|
||||
for ix in range {
|
||||
let item = ix + 1;
|
||||
|
||||
items.push(
|
||||
div()
|
||||
.id(ix)
|
||||
.px_2()
|
||||
.cursor_pointer()
|
||||
.on_click(move |_event, _window, _cx| {
|
||||
println!("clicked Item {item:?}");
|
||||
})
|
||||
.child(format!("Item {item}")),
|
||||
);
|
||||
}
|
||||
items
|
||||
}),
|
||||
)
|
||||
.h_full(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| cx.new(|_| UniformListExample {}),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
@@ -0,0 +1,549 @@
|
||||
//! `Editor` — the workhorse entity. It owns the cursor, blink, focus, keyboard
|
||||
//! handling, and the specialized text-shaping renderer. The *text itself* lives
|
||||
//! in a shared `Entity<String>` it's handed at construction, so the value is
|
||||
//! readable/writable from outside while the editing machinery stays in here.
|
||||
//!
|
||||
//! This is the piece that proves the point: a text input is genuinely
|
||||
//! complicated, and `View` lets all of that complexity live in one entity that
|
||||
//! anything can embed.
|
||||
|
||||
use std::ops::Range;
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, ElementInputHandler, Entity, EntityInputHandler, FocusHandle, Focusable,
|
||||
InteractiveElement, LayoutId, PaintQuad, Pixels, ShapedLine, SharedString, Subscription, Task,
|
||||
TextRun, UTF16Selection, Window, fill, hsla, point, prelude::*, px, relative, size,
|
||||
};
|
||||
use unicode_segmentation::*;
|
||||
|
||||
use crate::{Backspace, Delete, End, Home, Left, Right};
|
||||
|
||||
pub struct Editor {
|
||||
pub value: Entity<String>,
|
||||
pub focus_handle: FocusHandle,
|
||||
pub cursor: usize,
|
||||
pub cursor_visible: bool,
|
||||
_blink_task: Task<()>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
/// An editor that owns its own string internally, seeded with `text`.
|
||||
/// Nothing to allocate or wire up at the call site.
|
||||
pub fn new(text: impl Into<String>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let value = cx.new(|_| text.into());
|
||||
Self::over(value, window, cx)
|
||||
}
|
||||
|
||||
/// An editor over a string *you* own, so the value is shared in and out.
|
||||
pub fn over(value: Entity<String>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
let focus_sub = cx.on_focus(&focus_handle, window, |this, _window, cx| {
|
||||
this.start_blink(cx);
|
||||
});
|
||||
let blur_sub = cx.on_blur(&focus_handle, window, |this, _window, cx| {
|
||||
this.stop_blink(cx);
|
||||
});
|
||||
|
||||
// The value is shared: anything can write it while we hold a cursor into
|
||||
// it. Observe it so external writes (a) clamp the cursor back onto a char
|
||||
// boundary before the next IME round-trip can slice out of bounds, and
|
||||
// (b) notify us, so an `editor.cached(..)` subtree re-renders — the cache
|
||||
// is keyed on *our* notify, not the value's.
|
||||
let value_sub = cx.observe(&value, |this, value, cx| {
|
||||
let content = value.read(cx);
|
||||
let mut cursor = this.cursor.min(content.len());
|
||||
while cursor > 0 && !content.is_char_boundary(cursor) {
|
||||
cursor -= 1;
|
||||
}
|
||||
this.cursor = cursor;
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
Self {
|
||||
value,
|
||||
focus_handle,
|
||||
cursor: 0,
|
||||
cursor_visible: false,
|
||||
_blink_task: Task::ready(()),
|
||||
_subscriptions: vec![focus_sub, blur_sub, value_sub],
|
||||
}
|
||||
}
|
||||
|
||||
/// The current text. Read this from anywhere to get the value out.
|
||||
pub fn text(&self, cx: &App) -> String {
|
||||
self.value.read(cx).clone()
|
||||
}
|
||||
|
||||
fn start_blink(&mut self, cx: &mut Context<Self>) {
|
||||
self.cursor_visible = true;
|
||||
self._blink_task = Self::spawn_blink_task(cx);
|
||||
}
|
||||
|
||||
fn stop_blink(&mut self, cx: &mut Context<Self>) {
|
||||
self.cursor_visible = false;
|
||||
self._blink_task = Task::ready(());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn spawn_blink_task(cx: &mut Context<Self>) -> Task<()> {
|
||||
cx.spawn(async move |this, cx| {
|
||||
loop {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(500))
|
||||
.await;
|
||||
let result = this.update(cx, |editor, cx| {
|
||||
editor.cursor_visible = !editor.cursor_visible;
|
||||
cx.notify();
|
||||
});
|
||||
if result.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn reset_blink(&mut self, cx: &mut Context<Self>) {
|
||||
self.cursor_visible = true;
|
||||
self._blink_task = Self::spawn_blink_task(cx);
|
||||
}
|
||||
|
||||
pub fn left(&mut self, _: &Left, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let content = self.text(cx);
|
||||
if self.cursor > 0 {
|
||||
self.cursor = previous_boundary(&content, self.cursor);
|
||||
}
|
||||
self.reset_blink(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn right(&mut self, _: &Right, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let content = self.text(cx);
|
||||
if self.cursor < content.len() {
|
||||
self.cursor = next_boundary(&content, self.cursor);
|
||||
}
|
||||
self.reset_blink(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn home(&mut self, _: &Home, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.cursor = 0;
|
||||
self.reset_blink(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn end(&mut self, _: &End, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.cursor = self.text(cx).len();
|
||||
self.reset_blink(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn backspace(&mut self, _: &Backspace, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let content = self.text(cx);
|
||||
if self.cursor > 0 {
|
||||
let prev = previous_boundary(&content, self.cursor);
|
||||
let cursor = self.cursor;
|
||||
self.value.update(cx, |s, cx| {
|
||||
s.drain(prev..cursor);
|
||||
cx.notify();
|
||||
});
|
||||
self.cursor = prev;
|
||||
}
|
||||
self.reset_blink(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn delete(&mut self, _: &Delete, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let content = self.text(cx);
|
||||
if self.cursor < content.len() {
|
||||
let next = next_boundary(&content, self.cursor);
|
||||
let cursor = self.cursor;
|
||||
self.value.update(cx, |s, cx| {
|
||||
s.drain(cursor..next);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
self.reset_blink(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn insert_newline(&mut self, cx: &mut Context<Self>) {
|
||||
let cursor = self.cursor;
|
||||
self.value.update(cx, |s, cx| {
|
||||
s.insert(cursor, '\n');
|
||||
cx.notify();
|
||||
});
|
||||
self.cursor += 1;
|
||||
self.reset_blink(cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn previous_boundary(content: &str, offset: usize) -> usize {
|
||||
content
|
||||
.grapheme_indices(true)
|
||||
.rev()
|
||||
.find_map(|(idx, _)| (idx < offset).then_some(idx))
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn next_boundary(content: &str, offset: usize) -> usize {
|
||||
content
|
||||
.grapheme_indices(true)
|
||||
.find_map(|(idx, _)| (idx > offset).then_some(idx))
|
||||
.unwrap_or(content.len())
|
||||
}
|
||||
|
||||
fn offset_from_utf16(content: &str, offset: usize) -> usize {
|
||||
let mut utf8_offset = 0;
|
||||
let mut utf16_count = 0;
|
||||
for ch in content.chars() {
|
||||
if utf16_count >= offset {
|
||||
break;
|
||||
}
|
||||
utf16_count += ch.len_utf16();
|
||||
utf8_offset += ch.len_utf8();
|
||||
}
|
||||
utf8_offset
|
||||
}
|
||||
|
||||
fn offset_to_utf16(content: &str, offset: usize) -> usize {
|
||||
let mut utf16_offset = 0;
|
||||
let mut utf8_count = 0;
|
||||
for ch in content.chars() {
|
||||
if utf8_count >= offset {
|
||||
break;
|
||||
}
|
||||
utf8_count += ch.len_utf8();
|
||||
utf16_offset += ch.len_utf16();
|
||||
}
|
||||
utf16_offset
|
||||
}
|
||||
|
||||
fn range_to_utf16(content: &str, range: &Range<usize>) -> Range<usize> {
|
||||
offset_to_utf16(content, range.start)..offset_to_utf16(content, range.end)
|
||||
}
|
||||
|
||||
fn range_from_utf16(content: &str, range_utf16: &Range<usize>) -> Range<usize> {
|
||||
offset_from_utf16(content, range_utf16.start)..offset_from_utf16(content, range_utf16.end)
|
||||
}
|
||||
|
||||
impl Focusable for Editor {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl EntityInputHandler for Editor {
|
||||
fn text_for_range(
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
actual_range: &mut Option<Range<usize>>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<String> {
|
||||
let content = self.text(cx);
|
||||
let range = range_from_utf16(&content, &range_utf16);
|
||||
actual_range.replace(range_to_utf16(&content, &range));
|
||||
Some(content[range].to_string())
|
||||
}
|
||||
|
||||
fn selected_text_range(
|
||||
&mut self,
|
||||
_ignore_disabled_input: bool,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<UTF16Selection> {
|
||||
let content = self.text(cx);
|
||||
let utf16_cursor = offset_to_utf16(&content, self.cursor);
|
||||
Some(UTF16Selection {
|
||||
range: utf16_cursor..utf16_cursor,
|
||||
reversed: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn marked_text_range(
|
||||
&self,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<Range<usize>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
|
||||
|
||||
fn replace_text_in_range(
|
||||
&mut self,
|
||||
range_utf16: Option<Range<usize>>,
|
||||
new_text: &str,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let content = self.text(cx);
|
||||
let range = range_utf16
|
||||
.as_ref()
|
||||
.map(|r| range_from_utf16(&content, r))
|
||||
.unwrap_or(self.cursor..self.cursor);
|
||||
|
||||
let new_content = content[..range.start].to_owned() + new_text + &content[range.end..];
|
||||
self.cursor = range.start + new_text.len();
|
||||
self.value.update(cx, |s, cx| {
|
||||
*s = new_content;
|
||||
cx.notify();
|
||||
});
|
||||
self.reset_blink(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn replace_and_mark_text_in_range(
|
||||
&mut self,
|
||||
range_utf16: Option<Range<usize>>,
|
||||
new_text: &str,
|
||||
_new_selected_range_utf16: Option<Range<usize>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.replace_text_in_range(range_utf16, new_text, window, cx);
|
||||
}
|
||||
|
||||
fn bounds_for_range(
|
||||
&mut self,
|
||||
_range_utf16: Range<usize>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<Bounds<Pixels>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn character_index_for_point(
|
||||
&mut self,
|
||||
_point: gpui::Point<Pixels>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::Render for Editor {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Editor>) -> impl IntoElement {
|
||||
EditorText {
|
||||
editor: cx.entity(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// EditorText — the specialized renderer: shapes the text and paints the cursor.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct EditorText {
|
||||
editor: Entity<Editor>,
|
||||
}
|
||||
|
||||
struct EditorTextPrepaint {
|
||||
lines: Vec<ShapedLine>,
|
||||
cursor: Option<PaintQuad>,
|
||||
}
|
||||
|
||||
impl IntoElement for EditorText {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for EditorText {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = EditorTextPrepaint;
|
||||
|
||||
fn id(&self) -> Option<gpui::ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&gpui::GlobalElementId>,
|
||||
_inspector_id: Option<&gpui::InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let editor = self.editor.read(cx);
|
||||
let content = editor.value.read(cx);
|
||||
let line_count = content.split('\n').count().max(1);
|
||||
let line_height = window.line_height();
|
||||
let mut style = gpui::Style::default();
|
||||
style.size.width = relative(1.).into();
|
||||
style.size.height = (line_height * line_count as f32).into();
|
||||
(window.request_layout(style, [], cx), ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&gpui::GlobalElementId>,
|
||||
_inspector_id: Option<&gpui::InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self::PrepaintState {
|
||||
let editor = self.editor.read(cx);
|
||||
let content = editor.value.read(cx).clone();
|
||||
let cursor_offset = editor.cursor;
|
||||
let cursor_visible = editor.cursor_visible;
|
||||
let is_focused = editor.focus_handle.is_focused(window);
|
||||
|
||||
let style = window.text_style();
|
||||
let text_color = style.color;
|
||||
let font_size = style.font_size.to_pixels(window.rem_size());
|
||||
let line_height = window.line_height();
|
||||
|
||||
let is_placeholder = content.is_empty();
|
||||
|
||||
let lines: Vec<ShapedLine> = if is_placeholder {
|
||||
let placeholder: SharedString = "Type here...".into();
|
||||
let run = TextRun {
|
||||
len: placeholder.len(),
|
||||
font: style.font(),
|
||||
color: hsla(0., 0., 0.5, 0.5),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
};
|
||||
vec![
|
||||
window
|
||||
.text_system()
|
||||
.shape_line(placeholder, font_size, &[run], None),
|
||||
]
|
||||
} else {
|
||||
content
|
||||
.split('\n')
|
||||
.map(|line_str| {
|
||||
let text: SharedString = SharedString::from(line_str.to_string());
|
||||
let run = TextRun {
|
||||
len: text.len(),
|
||||
font: style.font(),
|
||||
color: text_color,
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
};
|
||||
window
|
||||
.text_system()
|
||||
.shape_line(text, font_size, &[run], None)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
let cursor = if is_focused && cursor_visible && !is_placeholder {
|
||||
let (cursor_line, offset_in_line) = cursor_line_and_offset(&content, cursor_offset);
|
||||
let cursor_line = cursor_line.min(lines.len().saturating_sub(1));
|
||||
let cursor_x = lines[cursor_line].x_for_index(offset_in_line);
|
||||
Some(fill(
|
||||
Bounds::new(
|
||||
point(
|
||||
bounds.left() + cursor_x,
|
||||
bounds.top() + line_height * cursor_line as f32,
|
||||
),
|
||||
size(px(1.5), line_height),
|
||||
),
|
||||
text_color,
|
||||
))
|
||||
} else if is_focused && cursor_visible && is_placeholder {
|
||||
Some(fill(
|
||||
Bounds::new(
|
||||
point(bounds.left(), bounds.top()),
|
||||
size(px(1.5), line_height),
|
||||
),
|
||||
text_color,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
EditorTextPrepaint { lines, cursor }
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&gpui::GlobalElementId>,
|
||||
_inspector_id: Option<&gpui::InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
prepaint: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let focus_handle = self.editor.read(cx).focus_handle.clone();
|
||||
window.handle_input(
|
||||
&focus_handle,
|
||||
ElementInputHandler::new(bounds, self.editor.clone()),
|
||||
cx,
|
||||
);
|
||||
|
||||
let line_height = window.line_height();
|
||||
for (i, line) in prepaint.lines.iter().enumerate() {
|
||||
let origin = point(bounds.left(), bounds.top() + line_height * i as f32);
|
||||
line.paint(origin, line_height, gpui::TextAlign::Left, None, window, cx)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
if let Some(cursor) = prepaint.cursor.take() {
|
||||
window.paint_quad(cursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor_line_and_offset(content: &str, cursor: usize) -> (usize, usize) {
|
||||
let mut line_index = 0;
|
||||
let mut line_start = 0;
|
||||
for (i, ch) in content.char_indices() {
|
||||
if i >= cursor {
|
||||
break;
|
||||
}
|
||||
if ch == '\n' {
|
||||
line_index += 1;
|
||||
line_start = i + 1;
|
||||
}
|
||||
}
|
||||
(line_index, cursor - line_start)
|
||||
}
|
||||
|
||||
pub fn standard_actions<E: InteractiveElement>(editor: Entity<Editor>) -> impl FnOnce(E) -> E {
|
||||
move |element| {
|
||||
element
|
||||
.on_action({
|
||||
let editor = editor.clone();
|
||||
move |a: &Left, window, cx| editor.update(cx, |e, cx| e.left(a, window, cx))
|
||||
})
|
||||
.on_action({
|
||||
let editor = editor.clone();
|
||||
move |a: &Right, window, cx| editor.update(cx, |e, cx| e.right(a, window, cx))
|
||||
})
|
||||
.on_action({
|
||||
let editor = editor.clone();
|
||||
move |a: &Home, window, cx| editor.update(cx, |e, cx| e.home(a, window, cx))
|
||||
})
|
||||
.on_action({
|
||||
let editor = editor.clone();
|
||||
move |a: &End, window, cx| editor.update(cx, |e, cx| e.end(a, window, cx))
|
||||
})
|
||||
.on_action({
|
||||
let editor = editor.clone();
|
||||
move |a: &Backspace, window, cx| {
|
||||
editor.update(cx, |e, cx| e.backspace(a, window, cx))
|
||||
}
|
||||
})
|
||||
.on_action(move |a: &Delete, window, cx| {
|
||||
editor.update(cx, |e, cx| e.delete(a, window, cx))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
//! `Input` — a single-line text input. The shaping layer over `Editor`.
|
||||
//!
|
||||
//! Construct it two ways, depending on how much state you want to own:
|
||||
//! * `Input::new(value: Entity<String>)` — you hold just the string; the input
|
||||
//! allocates the `Editor` internally via `use_state`. Value readable, cursor hidden.
|
||||
//! * `Input::editor(editor: Entity<Editor>)` — you hold the editor; cursor/selection
|
||||
//! are now yours to read and drive too.
|
||||
//!
|
||||
//! Either way the chrome is identical. Because the string (or editor) is the
|
||||
//! input's *identity*, the internal `use_state(Editor)` is collision-safe across
|
||||
//! any number of inputs.
|
||||
|
||||
use gpui::{
|
||||
App, BoxShadow, CursorStyle, Entity, EntityId, Hsla, IntoElement, Pixels, StyleRefinement,
|
||||
Window, div, hsla, point, prelude::*, px, white,
|
||||
};
|
||||
|
||||
use crate::example_editor::{Editor, standard_actions};
|
||||
|
||||
enum Source {
|
||||
Value(Entity<String>),
|
||||
Editor(Entity<Editor>),
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Input {
|
||||
source: Source,
|
||||
width: Option<Pixels>,
|
||||
color: Option<Hsla>,
|
||||
}
|
||||
|
||||
impl Input {
|
||||
/// Backed by a bare string; the editor is allocated internally.
|
||||
pub fn new(value: Entity<String>) -> Self {
|
||||
Self {
|
||||
source: Source::Value(value),
|
||||
width: None,
|
||||
color: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Backed by an editor you own (so you can read/drive its cursor).
|
||||
pub fn editor(editor: Entity<Editor>) -> Self {
|
||||
Self {
|
||||
source: Source::Editor(editor),
|
||||
width: None,
|
||||
color: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn width(mut self, width: Pixels) -> Self {
|
||||
self.width = Some(width);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: Hsla) -> Self {
|
||||
self.color = Some(color);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::View for Input {
|
||||
fn entity_id(&self) -> Option<EntityId> {
|
||||
Some(match &self.source {
|
||||
Source::Value(value) => value.entity_id(),
|
||||
Source::Editor(editor) => editor.entity_id(),
|
||||
})
|
||||
}
|
||||
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
// Get the editor: use the one we were handed, or allocate it under our
|
||||
// own (string-derived) identity so it persists and never collides.
|
||||
let editor = match self.source {
|
||||
Source::Value(value) => {
|
||||
window.use_state(cx, move |window, cx| Editor::over(value, window, cx))
|
||||
}
|
||||
Source::Editor(editor) => editor,
|
||||
};
|
||||
|
||||
let focus_handle = editor.read(cx).focus_handle.clone();
|
||||
let is_focused = focus_handle.is_focused(window);
|
||||
let text_color = self.color.unwrap_or(hsla(0., 0., 0.1, 1.));
|
||||
let box_width = self.width.unwrap_or(px(300.));
|
||||
|
||||
let border = if is_focused {
|
||||
hsla(220. / 360., 0.8, 0.5, 1.)
|
||||
} else {
|
||||
hsla(0., 0., 0.75, 1.)
|
||||
};
|
||||
|
||||
div()
|
||||
.id("input")
|
||||
.key_context("TextInput")
|
||||
.track_focus(&focus_handle)
|
||||
.cursor(CursorStyle::IBeam)
|
||||
.map(standard_actions(editor.clone()))
|
||||
.w(box_width)
|
||||
.h(px(36.))
|
||||
.px(px(8.))
|
||||
.bg(white())
|
||||
.border_1()
|
||||
.border_color(border)
|
||||
.when(is_focused, |this| {
|
||||
this.shadow(vec![BoxShadow {
|
||||
color: hsla(220. / 360., 0.8, 0.5, 0.3),
|
||||
offset: point(px(0.), px(0.)),
|
||||
blur_radius: px(4.),
|
||||
spread_radius: px(1.),
|
||||
inset: false,
|
||||
}])
|
||||
})
|
||||
.rounded(px(4.))
|
||||
.overflow_hidden()
|
||||
.flex()
|
||||
.items_center()
|
||||
.line_height(px(20.))
|
||||
.text_size(px(14.))
|
||||
.text_color(text_color)
|
||||
.child(editor.cached(StyleRefinement::default().size_full()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
//! Tests for the input composition. Require the `test-support` feature:
|
||||
//!
|
||||
//! ```sh
|
||||
//! cargo test -p gpui --example view_example --features test-support
|
||||
//! ```
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::{Context, Entity, KeyBinding, TestAppContext, Window, prelude::*};
|
||||
|
||||
use crate::example_editor::Editor;
|
||||
use crate::example_input::Input;
|
||||
use crate::{Backspace, Delete, End, Home, Left, Right};
|
||||
|
||||
/// Two inputs, each backed by an editor we own (so the test can focus and
|
||||
/// read them). Proves data flows through the shared `String` and that
|
||||
/// sibling inputs stay isolated.
|
||||
struct Harness {
|
||||
a: Entity<Editor>,
|
||||
b: Entity<Editor>,
|
||||
}
|
||||
|
||||
impl Render for Harness {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
gpui::div()
|
||||
.child(Input::editor(self.a.clone()))
|
||||
.child(Input::editor(self.b.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
fn bind_keys(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("backspace", Backspace, None),
|
||||
KeyBinding::new("delete", Delete, None),
|
||||
KeyBinding::new("left", Left, None),
|
||||
KeyBinding::new("right", Right, None),
|
||||
KeyBinding::new("home", Home, None),
|
||||
KeyBinding::new("end", End, None),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
fn setup(
|
||||
cx: &mut TestAppContext,
|
||||
) -> (
|
||||
Entity<Editor>,
|
||||
Entity<String>,
|
||||
Entity<String>,
|
||||
&mut gpui::VisualTestContext,
|
||||
) {
|
||||
bind_keys(cx);
|
||||
|
||||
let (harness, cx) = cx.add_window_view(|window, cx| {
|
||||
let a_value = cx.new(|_| String::new());
|
||||
let b_value = cx.new(|_| String::new());
|
||||
let a = cx.new(|cx| Editor::over(a_value, window, cx));
|
||||
let b = cx.new(|cx| Editor::over(b_value, window, cx));
|
||||
Harness { a, b }
|
||||
});
|
||||
|
||||
let a = cx.read_entity(&harness, |h, _| h.a.clone());
|
||||
let b = cx.read_entity(&harness, |h, _| h.b.clone());
|
||||
let a_value = cx.read_entity(&a, |e, _| e.value.clone());
|
||||
let b_value = cx.read_entity(&b, |e, _| e.value.clone());
|
||||
|
||||
// Focus the first input's editor.
|
||||
cx.update(|window, cx| {
|
||||
let focus_handle = a.read(cx).focus_handle.clone();
|
||||
window.focus(&focus_handle, cx);
|
||||
});
|
||||
|
||||
(a, a_value, b_value, cx)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn typing_updates_the_shared_string(cx: &mut TestAppContext) {
|
||||
let (editor, a_value, _b_value, cx) = setup(cx);
|
||||
|
||||
cx.simulate_input("hello");
|
||||
|
||||
cx.read_entity(&a_value, |value, _| assert_eq!(value, "hello"));
|
||||
cx.read_entity(&editor, |editor, _| assert_eq!(editor.cursor, 5));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn sibling_inputs_are_isolated(cx: &mut TestAppContext) {
|
||||
let (_editor, a_value, b_value, cx) = setup(cx);
|
||||
|
||||
cx.simulate_input("x");
|
||||
|
||||
cx.read_entity(&a_value, |value, _| assert_eq!(value, "x"));
|
||||
cx.read_entity(&b_value, |value, _| {
|
||||
assert_eq!(value, "", "typing in input A must not touch input B")
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn external_writes_clamp_the_cursor(cx: &mut TestAppContext) {
|
||||
let (editor, a_value, _b_value, cx) = setup(cx);
|
||||
|
||||
cx.simulate_input("hello");
|
||||
cx.read_entity(&editor, |editor, _| assert_eq!(editor.cursor, 5));
|
||||
|
||||
// Write the shared value from outside the editor. The old cursor (5)
|
||||
// now points into the middle of a multi-byte character; the editor's
|
||||
// observation must clamp it back onto a boundary.
|
||||
cx.update(|_, cx| {
|
||||
a_value.update(cx, |value, cx| {
|
||||
*value = "日本".to_string();
|
||||
cx.notify();
|
||||
})
|
||||
});
|
||||
|
||||
cx.read_entity(&a_value, |value, _| assert_eq!(value, "日本"));
|
||||
cx.read_entity(&editor, |editor, _| {
|
||||
assert_eq!(editor.cursor, 3, "cursor must clamp to a char boundary");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn arrows_move_the_cursor(cx: &mut TestAppContext) {
|
||||
let (editor, _a_value, _b_value, cx) = setup(cx);
|
||||
|
||||
cx.simulate_input("abc");
|
||||
cx.read_entity(&editor, |editor, _| assert_eq!(editor.cursor, 3));
|
||||
|
||||
cx.simulate_keystrokes("left left");
|
||||
cx.read_entity(&editor, |editor, _| assert_eq!(editor.cursor, 1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
//! `TextArea` — a multi-line text box. Same `Editor` workhorse, taller chrome,
|
||||
//! and `Enter` inserts a newline instead of being ignored. Constructible from a
|
||||
//! string or an editor, exactly like [`Input`](crate::example_input::Input).
|
||||
|
||||
use gpui::{
|
||||
App, BoxShadow, CursorStyle, Entity, EntityId, Hsla, IntoElement, StyleRefinement, Window, div,
|
||||
hsla, point, prelude::*, px, white,
|
||||
};
|
||||
|
||||
use crate::Enter;
|
||||
use crate::example_editor::{Editor, standard_actions};
|
||||
|
||||
enum Source {
|
||||
Value(Entity<String>),
|
||||
Editor(Entity<Editor>),
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct TextArea {
|
||||
source: Source,
|
||||
rows: usize,
|
||||
color: Option<Hsla>,
|
||||
}
|
||||
|
||||
impl TextArea {
|
||||
pub fn new(value: Entity<String>, rows: usize) -> Self {
|
||||
Self {
|
||||
source: Source::Value(value),
|
||||
rows,
|
||||
color: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn editor(editor: Entity<Editor>, rows: usize) -> Self {
|
||||
Self {
|
||||
source: Source::Editor(editor),
|
||||
rows,
|
||||
color: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: Hsla) -> Self {
|
||||
self.color = Some(color);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::View for TextArea {
|
||||
fn entity_id(&self) -> Option<EntityId> {
|
||||
Some(match &self.source {
|
||||
Source::Value(value) => value.entity_id(),
|
||||
Source::Editor(editor) => editor.entity_id(),
|
||||
})
|
||||
}
|
||||
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let editor = match self.source {
|
||||
Source::Value(value) => {
|
||||
window.use_state(cx, move |window, cx| Editor::over(value, window, cx))
|
||||
}
|
||||
Source::Editor(editor) => editor,
|
||||
};
|
||||
|
||||
let focus_handle = editor.read(cx).focus_handle.clone();
|
||||
let is_focused = focus_handle.is_focused(window);
|
||||
let text_color = self.color.unwrap_or(hsla(0., 0., 0.1, 1.));
|
||||
let row_height = px(20.);
|
||||
let box_height = row_height * self.rows as f32 + px(16.);
|
||||
|
||||
let border = if is_focused {
|
||||
hsla(220. / 360., 0.8, 0.5, 1.)
|
||||
} else {
|
||||
hsla(0., 0., 0.75, 1.)
|
||||
};
|
||||
|
||||
div()
|
||||
.id("text-area")
|
||||
.key_context("TextInput")
|
||||
.track_focus(&focus_handle)
|
||||
.cursor(CursorStyle::IBeam)
|
||||
.map(standard_actions(editor.clone()))
|
||||
// Enter is the one binding that differs from a single-line input.
|
||||
.on_action({
|
||||
let editor = editor.clone();
|
||||
move |_: &Enter, _window, cx| editor.update(cx, |e, cx| e.insert_newline(cx))
|
||||
})
|
||||
.w(px(400.))
|
||||
.h(box_height)
|
||||
.p(px(8.))
|
||||
.bg(white())
|
||||
.border_1()
|
||||
.border_color(border)
|
||||
.when(is_focused, |this| {
|
||||
this.shadow(vec![BoxShadow {
|
||||
color: hsla(220. / 360., 0.8, 0.5, 0.3),
|
||||
offset: point(px(0.), px(0.)),
|
||||
blur_radius: px(4.),
|
||||
spread_radius: px(1.),
|
||||
inset: false,
|
||||
}])
|
||||
})
|
||||
.rounded(px(4.))
|
||||
.overflow_hidden()
|
||||
.line_height(row_height)
|
||||
.text_size(px(14.))
|
||||
.text_color(text_color)
|
||||
// The cache style is computed from the `rows` prop: change `rows` and
|
||||
// the editor's cached bounds change, busting its cache and re-laying
|
||||
// out the text. (`Input` just uses `size_full()` — nothing to vary.)
|
||||
.child(
|
||||
editor.cached(
|
||||
StyleRefinement::default()
|
||||
.w_full()
|
||||
.h(row_height * self.rows as f32),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
//! View example — composing a text input from the `View` primitives.
|
||||
//!
|
||||
//! The whole point: a text input is deceptively complicated, and `View` makes it
|
||||
//! easy to compose one. Three pieces, each shown in its own section:
|
||||
//!
|
||||
//! * `Editor` — the workhorse entity: cursor, blink, focus, keyboard, and a
|
||||
//! specialized text renderer. All the hard parts live here.
|
||||
//! * `String` — the data plane. `editor.text(cx)` / `value.read(cx)` get it out.
|
||||
//! * `Input` / `TextArea` — the shaping layer. Each takes a `String` (and grows
|
||||
//! the editor internally) OR an `Editor` (so you can read the cursor).
|
||||
//!
|
||||
//! Run: `cargo run -p gpui --example view_example`
|
||||
|
||||
#[path = "../example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
mod example_editor;
|
||||
mod example_input;
|
||||
mod example_text_area;
|
||||
|
||||
#[cfg(test)]
|
||||
mod example_tests;
|
||||
|
||||
use example_editor::Editor;
|
||||
use example_input::Input;
|
||||
use example_text_area::TextArea;
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, Div, Entity, IntoElement, KeyBinding, Render, SharedString, Window,
|
||||
WindowBounds, WindowOptions, actions, div, hsla, prelude::*, px, rgb, size,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
actions!(
|
||||
view_example,
|
||||
[Backspace, Delete, Left, Right, Home, End, Enter, Quit]
|
||||
);
|
||||
|
||||
/// A tiny stateless view that reads an editor's cursor and is composed *beside*
|
||||
/// the thing editing it — two views over one entity, zero wiring.
|
||||
#[derive(IntoElement)]
|
||||
struct CursorReadout {
|
||||
editor: Entity<Editor>,
|
||||
}
|
||||
|
||||
impl CursorReadout {
|
||||
fn new(editor: Entity<Editor>) -> Self {
|
||||
Self { editor }
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::RenderOnce for CursorReadout {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let cursor = self.editor.read(cx).cursor;
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(hsla(0., 0., 0.45, 1.))
|
||||
.child(SharedString::from(format!("cursor @ {cursor}")))
|
||||
}
|
||||
}
|
||||
|
||||
struct ViewExample;
|
||||
|
||||
impl ViewExample {
|
||||
fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ViewExample {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
// The data plane: plain strings, allocated at the top by the hook.
|
||||
let name = window.use_state(cx, |_, _| String::new());
|
||||
let email = window.use_state(cx, |_, _| String::from("me@example.com"));
|
||||
let bio = window.use_state(cx, |_, _| String::new());
|
||||
// Editors that own their own string internally — no extra wiring up top.
|
||||
let notes = window.use_state(cx, |window, cx| Editor::new("multi\nline", window, cx));
|
||||
let owned = window.use_state(cx, |window, cx| Editor::new("editable", window, cx));
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.bg(rgb(0xf0f0f0))
|
||||
.p(px(24.))
|
||||
.gap(px(24.))
|
||||
.child(
|
||||
section("Inputs — from a String (cursor stays internal)")
|
||||
.child(Input::new(name).width(px(320.)))
|
||||
.child(
|
||||
Input::new(email)
|
||||
.width(px(320.))
|
||||
.color(hsla(0., 0., 0.3, 1.)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
section("Input — from an Editor (read its cursor beside it)").child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap(px(12.))
|
||||
.child(Input::editor(owned.clone()).width(px(320.)))
|
||||
.child(CursorReadout::new(owned)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
section("Text areas — from a String, or from an Editor")
|
||||
.child(TextArea::new(bio, 3))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_start()
|
||||
.gap(px(12.))
|
||||
.child(TextArea::editor(notes.clone(), 3).color(hsla(
|
||||
250. / 360.,
|
||||
0.7,
|
||||
0.4,
|
||||
1.,
|
||||
)))
|
||||
.child(CursorReadout::new(notes)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A labeled vertical section.
|
||||
fn section(title: &str) -> Div {
|
||||
div().flex().flex_col().gap(px(8.)).child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(hsla(0., 0., 0.3, 1.))
|
||||
.child(SharedString::from(title.to_string())),
|
||||
)
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
let bounds = Bounds::centered(None, size(px(560.0), px(480.0)), cx);
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("backspace", Backspace, None),
|
||||
KeyBinding::new("delete", Delete, None),
|
||||
KeyBinding::new("left", Left, None),
|
||||
KeyBinding::new("right", Right, None),
|
||||
KeyBinding::new("home", Home, None),
|
||||
KeyBinding::new("end", End, None),
|
||||
KeyBinding::new("enter", Enter, None),
|
||||
KeyBinding::new("cmd-q", Quit, None),
|
||||
]);
|
||||
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| cx.new(|_| ViewExample::new()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
cx.on_action(|_: &Quit, cx| cx.quit());
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
Vendored
+355
@@ -0,0 +1,355 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, KeyBinding, PromptButton, PromptLevel, Window, WindowBounds, WindowKind,
|
||||
WindowOptions, actions, div, prelude::*, px, rgb, size,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
struct SubWindow {
|
||||
custom_titlebar: bool,
|
||||
is_dialog: bool,
|
||||
}
|
||||
|
||||
fn button(text: &str, on_click: impl Fn(&mut Window, &mut App) + 'static) -> impl IntoElement {
|
||||
div()
|
||||
.id(text.to_string())
|
||||
.flex_none()
|
||||
.px_2()
|
||||
.bg(rgb(0xf7f7f7))
|
||||
.active(|this| this.opacity(0.85))
|
||||
.border_1()
|
||||
.border_color(rgb(0xe0e0e0))
|
||||
.rounded_sm()
|
||||
.cursor_pointer()
|
||||
.child(text.to_string())
|
||||
.on_click(move |_, window, cx| on_click(window, cx))
|
||||
}
|
||||
|
||||
impl Render for SubWindow {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let window_bounds =
|
||||
WindowBounds::Windowed(Bounds::centered(None, size(px(250.0), px(200.0)), cx));
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.bg(rgb(0xffffff))
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.when(self.custom_titlebar, |cx| {
|
||||
cx.child(
|
||||
div()
|
||||
.flex()
|
||||
.h(px(32.))
|
||||
.px_4()
|
||||
.bg(gpui::blue())
|
||||
.text_color(gpui::white())
|
||||
.w_full()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.size_full()
|
||||
.child("Custom Titlebar"),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.p_8()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child("SubWindow")
|
||||
.when(self.is_dialog, |div| {
|
||||
div.child(button("Open Nested Dialog", move |_, cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(window_bounds),
|
||||
kind: WindowKind::Dialog,
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: true,
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}))
|
||||
})
|
||||
.child(button("Close", |window, _| {
|
||||
window.remove_window();
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct WindowDemo {}
|
||||
|
||||
impl Render for WindowDemo {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let window_bounds =
|
||||
WindowBounds::Windowed(Bounds::centered(None, size(px(300.0), px(300.0)), cx));
|
||||
|
||||
div()
|
||||
.p_4()
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
.bg(rgb(0xffffff))
|
||||
.size_full()
|
||||
.justify_center()
|
||||
.content_center()
|
||||
.gap_2()
|
||||
.child(button("Normal", move |_, cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(window_bounds),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}))
|
||||
.child(button("Popup", move |_, cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(window_bounds),
|
||||
kind: WindowKind::PopUp,
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}))
|
||||
.child(button("Floating", move |_, cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(window_bounds),
|
||||
kind: WindowKind::Floating,
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}))
|
||||
.child(button("Dialog", move |_, cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(window_bounds),
|
||||
kind: WindowKind::Dialog,
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: true,
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}))
|
||||
.child(button("Custom Titlebar", move |_, cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
titlebar: None,
|
||||
window_bounds: Some(window_bounds),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: true,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}))
|
||||
.child(button("Invisible", move |_, cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
show: false,
|
||||
window_bounds: Some(window_bounds),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}))
|
||||
.child(button("Unmovable", move |_, cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
is_movable: false,
|
||||
titlebar: None,
|
||||
window_bounds: Some(window_bounds),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}))
|
||||
.child(button("Unresizable", move |_, cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
is_resizable: false,
|
||||
window_bounds: Some(window_bounds),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}))
|
||||
.child(button("Unminimizable", move |_, cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
is_minimizable: false,
|
||||
window_bounds: Some(window_bounds),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}))
|
||||
.child(button("Hide Application", |window, cx| {
|
||||
cx.hide();
|
||||
|
||||
// Restore the application after 3 seconds
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
cx.background_executor()
|
||||
.timer(std::time::Duration::from_secs(3))
|
||||
.await;
|
||||
cx.update(|_, cx| {
|
||||
cx.activate(false);
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}))
|
||||
.child(button("Resize", |window, _| {
|
||||
let content_size = window.bounds().size;
|
||||
window.resize(size(content_size.height, content_size.width));
|
||||
}))
|
||||
.child(button("Prompt", |window, cx| {
|
||||
let answer = window.prompt(
|
||||
PromptLevel::Info,
|
||||
"Are you sure?",
|
||||
None,
|
||||
&["OK", "Cancel"],
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.spawn(async move |_| {
|
||||
if answer.await.unwrap() == 0 {
|
||||
println!("You have clicked Ok");
|
||||
} else {
|
||||
println!("You have clicked Cancel");
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}))
|
||||
.child(button("Prompt (non-English)", |window, cx| {
|
||||
let answer = window.prompt(
|
||||
PromptLevel::Info,
|
||||
"Are you sure?",
|
||||
None,
|
||||
&[PromptButton::ok("确定"), PromptButton::cancel("取消")],
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.spawn(async move |_| {
|
||||
if answer.await.unwrap() == 0 {
|
||||
println!("You have clicked Ok");
|
||||
} else {
|
||||
println!("You have clicked Cancel");
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
actions!(window, [Quit]);
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx);
|
||||
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| {
|
||||
cx.new(|cx| {
|
||||
cx.observe_window_bounds(window, move |_, window, _| {
|
||||
println!("Window bounds changed: {:?}", window.bounds());
|
||||
})
|
||||
.detach();
|
||||
|
||||
WindowDemo {}
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
cx.activate(true);
|
||||
cx.on_action(|_: &Quit, cx| cx.quit());
|
||||
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, FocusHandle, Window, WindowBounds, WindowOptions, div, prelude::*, px,
|
||||
rgb, size,
|
||||
};
|
||||
use gpui::{SharedString, TitlebarOptions};
|
||||
use gpui_platform::application;
|
||||
|
||||
struct ExampleWindow {
|
||||
label: SharedString,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Render for ExampleWindow {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.bg(rgb(0x2e2e2e))
|
||||
.size_full()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.p_8()
|
||||
.text_lg()
|
||||
.text_color(rgb(0xffffff))
|
||||
.child(self.label.clone())
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(rgb(0xb0b0b0))
|
||||
.child("Try to drag the titlebar, and check the Window menu."),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn open_test_window(
|
||||
cx: &mut App,
|
||||
bounds: Bounds<gpui::Pixels>,
|
||||
label: &str,
|
||||
is_movable: bool,
|
||||
appears_transparent: bool,
|
||||
app_owns_titlebar_drag: bool,
|
||||
) {
|
||||
let label = SharedString::from(format!(
|
||||
"{label}\nis_movable: {is_movable}\n\
|
||||
appears_transparent: {appears_transparent}\n\
|
||||
app_owns_titlebar_drag: {app_owns_titlebar_drag}"
|
||||
));
|
||||
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
is_movable,
|
||||
app_owns_titlebar_drag,
|
||||
titlebar: Some(TitlebarOptions {
|
||||
title: Some(label.clone()),
|
||||
appears_transparent,
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| {
|
||||
cx.new(|cx| {
|
||||
let focus_handle = cx.focus_handle();
|
||||
focus_handle.focus(window, cx);
|
||||
ExampleWindow {
|
||||
label,
|
||||
focus_handle,
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
let window_size = size(px(420.), px(280.0));
|
||||
let base = Bounds::centered(None, window_size, cx);
|
||||
|
||||
// (label, is_movable, appears_transparent, app_owns_titlebar_drag, col, row)
|
||||
let windows = [
|
||||
("Native titlebar, movable", true, false, false, 0.0, 0.0),
|
||||
(
|
||||
"Native titlebar, NOT movable",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
1.0,
|
||||
0.0,
|
||||
),
|
||||
("Custom titlebar, movable", true, true, false, 0.0, 1.0),
|
||||
("Custom titlebar, NOT movable", false, true, false, 1.0, 1.0),
|
||||
];
|
||||
|
||||
for (label, is_movable, appears_transparent, app_owns_titlebar_drag, col, row) in windows {
|
||||
let mut bounds = base;
|
||||
bounds.origin.x += window_size.width * col;
|
||||
bounds.origin.y += window_size.height * row;
|
||||
open_test_window(
|
||||
cx,
|
||||
bounds,
|
||||
label,
|
||||
is_movable,
|
||||
appears_transparent,
|
||||
app_owns_titlebar_drag,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, DisplayId, Hsla, Pixels, SharedString, Size, Window,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowKind, WindowOptions, div, point, prelude::*,
|
||||
px, rgb,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
struct WindowContent {
|
||||
text: SharedString,
|
||||
bounds: Bounds<Pixels>,
|
||||
bg: Hsla,
|
||||
}
|
||||
|
||||
impl Render for WindowContent {
|
||||
fn render(&mut self, window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
let window_bounds = window.bounds();
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.bg(self.bg)
|
||||
.size_full()
|
||||
.items_center()
|
||||
.text_color(rgb(0xffffff))
|
||||
.child(self.text.clone())
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.text_sm()
|
||||
.items_center()
|
||||
.size_full()
|
||||
.child(format!(
|
||||
"origin: {}, {} size: {}, {}",
|
||||
self.bounds.origin.x,
|
||||
self.bounds.origin.y,
|
||||
self.bounds.size.width,
|
||||
self.bounds.size.height
|
||||
))
|
||||
.child(format!(
|
||||
"cx.bounds() origin: {}, {} size {}, {}",
|
||||
window_bounds.origin.x,
|
||||
window_bounds.origin.y,
|
||||
window_bounds.size.width,
|
||||
window_bounds.size.height
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn build_window_options(display_id: DisplayId, bounds: Bounds<Pixels>) -> WindowOptions {
|
||||
WindowOptions {
|
||||
// Set the bounds of the window in screen coordinates
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
// Specify the display_id to ensure the window is created on the correct screen
|
||||
display_id: Some(display_id),
|
||||
titlebar: None,
|
||||
window_background: WindowBackgroundAppearance::Transparent,
|
||||
focus: false,
|
||||
show: true,
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: false,
|
||||
app_id: None,
|
||||
window_min_size: None,
|
||||
window_decorations: None,
|
||||
tabbing_identifier: None,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
// Create several new windows, positioned in the top right corner of each screen
|
||||
let size = Size {
|
||||
width: px(350.),
|
||||
height: px(75.),
|
||||
};
|
||||
let margin_offset = px(150.);
|
||||
|
||||
for screen in cx.displays() {
|
||||
let bounds = Bounds {
|
||||
origin: point(margin_offset, margin_offset),
|
||||
size,
|
||||
};
|
||||
|
||||
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
|
||||
cx.new(|_| WindowContent {
|
||||
text: format!("Top Left {:?}", screen.id()).into(),
|
||||
bg: gpui::red(),
|
||||
bounds,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: screen.bounds().top_right()
|
||||
- point(size.width + margin_offset, -margin_offset),
|
||||
size,
|
||||
};
|
||||
|
||||
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
|
||||
cx.new(|_| WindowContent {
|
||||
text: format!("Top Right {:?}", screen.id()).into(),
|
||||
bg: gpui::red(),
|
||||
bounds,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: screen.bounds().bottom_left()
|
||||
- point(-margin_offset, size.height + margin_offset),
|
||||
size,
|
||||
};
|
||||
|
||||
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
|
||||
cx.new(|_| WindowContent {
|
||||
text: format!("Bottom Left {:?}", screen.id()).into(),
|
||||
bg: gpui::blue(),
|
||||
bounds,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: screen.bounds().bottom_right()
|
||||
- point(size.width + margin_offset, size.height + margin_offset),
|
||||
size,
|
||||
};
|
||||
|
||||
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
|
||||
cx.new(|_| WindowContent {
|
||||
text: format!("Bottom Right {:?}", screen.id()).into(),
|
||||
bg: gpui::blue(),
|
||||
bounds,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: point(screen.bounds().center().x - size.center().x, margin_offset),
|
||||
size,
|
||||
};
|
||||
|
||||
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
|
||||
cx.new(|_| WindowContent {
|
||||
text: format!("Top Center {:?}", screen.id()).into(),
|
||||
bg: gpui::black(),
|
||||
bounds,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: point(margin_offset, screen.bounds().center().y - size.center().y),
|
||||
size,
|
||||
};
|
||||
|
||||
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
|
||||
cx.new(|_| WindowContent {
|
||||
text: format!("Left Center {:?}", screen.id()).into(),
|
||||
bg: gpui::black(),
|
||||
bounds,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: point(
|
||||
screen.bounds().center().x - size.center().x,
|
||||
screen.bounds().center().y - size.center().y,
|
||||
),
|
||||
size,
|
||||
};
|
||||
|
||||
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
|
||||
cx.new(|_| WindowContent {
|
||||
text: format!("Center {:?}", screen.id()).into(),
|
||||
bg: gpui::black(),
|
||||
bounds,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: point(
|
||||
screen.bounds().size.width - size.width - margin_offset,
|
||||
screen.bounds().center().y - size.center().y,
|
||||
),
|
||||
size,
|
||||
};
|
||||
|
||||
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
|
||||
cx.new(|_| WindowContent {
|
||||
text: format!("Right Center {:?}", screen.id()).into(),
|
||||
bg: gpui::black(),
|
||||
bounds,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: point(
|
||||
screen.bounds().center().x - size.center().x,
|
||||
screen.bounds().size.height - size.height - margin_offset,
|
||||
),
|
||||
size,
|
||||
};
|
||||
|
||||
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
|
||||
cx.new(|_| WindowContent {
|
||||
text: format!("Bottom Center {:?}", screen.id()).into(),
|
||||
bg: gpui::black(),
|
||||
bounds,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
#[path = "example_support/fonts.rs"]
|
||||
mod example_support;
|
||||
|
||||
use gpui::{
|
||||
App, 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,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
struct WindowShadow {}
|
||||
|
||||
// Things to do:
|
||||
// 1. We need a way of calculating which edge or corner the mouse is on,
|
||||
// and then dispatch on that
|
||||
// 2. We need to improve the shadow rendering significantly
|
||||
// 3. We need to implement the techniques in here in Zed
|
||||
|
||||
impl Render for WindowShadow {
|
||||
fn render(&mut self, window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let decorations = window.window_decorations();
|
||||
let rounding = px(10.0);
|
||||
let shadow_size = px(10.0);
|
||||
let border_size = px(1.0);
|
||||
let grey = rgb(0x808080);
|
||||
window.set_client_inset(shadow_size);
|
||||
|
||||
div()
|
||||
.id("window-backdrop")
|
||||
.bg(transparent_black())
|
||||
.map(|div| match decorations {
|
||||
Decorations::Server => div,
|
||||
Decorations::Client { tiling, .. } => div
|
||||
.bg(gpui::transparent_black())
|
||||
.child(
|
||||
canvas(
|
||||
|_bounds, window, _cx| {
|
||||
window.insert_hitbox(
|
||||
Bounds::new(
|
||||
point(px(0.0), px(0.0)),
|
||||
window.window_bounds().get_bounds().size,
|
||||
),
|
||||
HitboxBehavior::Normal,
|
||||
)
|
||||
},
|
||||
move |_bounds, hitbox, window, _cx| {
|
||||
let mouse = window.mouse_position();
|
||||
let size = window.window_bounds().get_bounds().size;
|
||||
let Some(edge) = resize_edge(mouse, shadow_size, size) else {
|
||||
return;
|
||||
};
|
||||
window.set_cursor_style(
|
||||
match edge {
|
||||
ResizeEdge::Top | ResizeEdge::Bottom => {
|
||||
CursorStyle::ResizeUpDown
|
||||
}
|
||||
ResizeEdge::Left | ResizeEdge::Right => {
|
||||
CursorStyle::ResizeLeftRight
|
||||
}
|
||||
ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
|
||||
CursorStyle::ResizeUpLeftDownRight
|
||||
}
|
||||
ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
|
||||
CursorStyle::ResizeUpRightDownLeft
|
||||
}
|
||||
},
|
||||
&hitbox,
|
||||
);
|
||||
},
|
||||
)
|
||||
.size_full()
|
||||
.absolute(),
|
||||
)
|
||||
.when(!(tiling.top || tiling.right), |div| {
|
||||
div.rounded_tr(rounding)
|
||||
})
|
||||
.when(!(tiling.top || tiling.left), |div| div.rounded_tl(rounding))
|
||||
.when(!tiling.top, |div| div.pt(shadow_size))
|
||||
.when(!tiling.bottom, |div| div.pb(shadow_size))
|
||||
.when(!tiling.left, |div| div.pl(shadow_size))
|
||||
.when(!tiling.right, |div| div.pr(shadow_size))
|
||||
.on_mouse_move(|_e, window, _cx| window.refresh())
|
||||
.on_mouse_down(MouseButton::Left, move |e, window, _cx| {
|
||||
let size = window.window_bounds().get_bounds().size;
|
||||
let pos = e.position;
|
||||
|
||||
match resize_edge(pos, shadow_size, size) {
|
||||
Some(edge) => window.start_window_resize(edge),
|
||||
None => window.start_window_move(),
|
||||
};
|
||||
}),
|
||||
})
|
||||
.size_full()
|
||||
.child(
|
||||
div()
|
||||
.cursor(CursorStyle::Arrow)
|
||||
.map(|div| match decorations {
|
||||
Decorations::Server => div,
|
||||
Decorations::Client { tiling } => div
|
||||
.border_color(grey)
|
||||
.when(!(tiling.top || tiling.right), |div| {
|
||||
div.rounded_tr(rounding)
|
||||
})
|
||||
.when(!(tiling.top || tiling.left), |div| div.rounded_tl(rounding))
|
||||
.when(!tiling.top, |div| div.border_t(border_size))
|
||||
.when(!tiling.bottom, |div| div.border_b(border_size))
|
||||
.when(!tiling.left, |div| div.border_l(border_size))
|
||||
.when(!tiling.right, |div| div.border_r(border_size))
|
||||
.when(!tiling.is_tiled(), |div| {
|
||||
div.shadow(vec![
|
||||
gpui::BoxShadow::new(
|
||||
px(0.),
|
||||
px(0.),
|
||||
Hsla {
|
||||
h: 0.,
|
||||
s: 0.,
|
||||
l: 0.,
|
||||
a: 0.4,
|
||||
},
|
||||
)
|
||||
.blur_radius(shadow_size / 2.),
|
||||
])
|
||||
}),
|
||||
})
|
||||
.on_mouse_move(|_e, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.bg(gpui::rgb(0xCCCCFF))
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.justify_around()
|
||||
.child(
|
||||
div().w_full().flex().flex_row().justify_around().child(
|
||||
div()
|
||||
.flex()
|
||||
.bg(white())
|
||||
.size(px(300.0))
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.shadow_lg()
|
||||
.border_1()
|
||||
.border_color(rgb(0x0000ff))
|
||||
.text_xl()
|
||||
.text_color(rgb(0xffffff))
|
||||
.child(
|
||||
div()
|
||||
.id("hello")
|
||||
.w(px(200.0))
|
||||
.h(px(100.0))
|
||||
.bg(green())
|
||||
.shadow(vec![
|
||||
gpui::BoxShadow::new(
|
||||
px(0.),
|
||||
px(0.),
|
||||
Hsla {
|
||||
h: 0.,
|
||||
s: 0.,
|
||||
l: 0.,
|
||||
a: 1.0,
|
||||
},
|
||||
)
|
||||
.blur_radius(px(20.0)),
|
||||
])
|
||||
.map(|div| match decorations {
|
||||
Decorations::Server => div,
|
||||
Decorations::Client { .. } => div
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
|_e, window, _| {
|
||||
window.start_window_move();
|
||||
},
|
||||
)
|
||||
.on_click(|e, window, _| {
|
||||
if e.is_right_click() {
|
||||
window.show_window_menu(e.position());
|
||||
}
|
||||
})
|
||||
.text_color(black())
|
||||
.child("this is the custom titlebar"),
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn resize_edge(pos: Point<Pixels>, shadow_size: Pixels, size: Size<Pixels>) -> Option<ResizeEdge> {
|
||||
let edge = if pos.y < shadow_size && pos.x < shadow_size {
|
||||
ResizeEdge::TopLeft
|
||||
} else if pos.y < shadow_size && pos.x > size.width - shadow_size {
|
||||
ResizeEdge::TopRight
|
||||
} else if pos.y < shadow_size {
|
||||
ResizeEdge::Top
|
||||
} else if pos.y > size.height - shadow_size && pos.x < shadow_size {
|
||||
ResizeEdge::BottomLeft
|
||||
} else if pos.y > size.height - shadow_size && pos.x > size.width - shadow_size {
|
||||
ResizeEdge::BottomRight
|
||||
} else if pos.y > size.height - shadow_size {
|
||||
ResizeEdge::Bottom
|
||||
} else if pos.x < shadow_size {
|
||||
ResizeEdge::Left
|
||||
} else if pos.x > size.width - shadow_size {
|
||||
ResizeEdge::Right
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
Some(edge)
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
if !example_support::load_fonts(cx) {
|
||||
return;
|
||||
}
|
||||
let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
window_background: WindowBackgroundAppearance::Opaque,
|
||||
window_decorations: Some(WindowDecorations::Client),
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| {
|
||||
cx.new(|cx| {
|
||||
cx.observe_window_appearance(window, |_, window, _| {
|
||||
window.refresh();
|
||||
})
|
||||
.detach();
|
||||
WindowShadow {}
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<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>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity
|
||||
type='win32'
|
||||
name='Microsoft.Windows.Common-Controls'
|
||||
version='6.0.0.0'
|
||||
processorArchitecture='*'
|
||||
publicKeyToken='6595b64144ccf1df'
|
||||
/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
</assembly>
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
#define RT_MANIFEST 24
|
||||
1 RT_MANIFEST "gpui.manifest.xml"
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
//! # 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. Docs for contributors working on accessibility
|
||||
//! support can be found in the `a11y` module's doc comment.
|
||||
//!
|
||||
//! 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.
|
||||
//!
|
||||
//! ## Synthetic children
|
||||
//!
|
||||
//! Sometimes, a custom [`Element`] may want to appear as if it is really made
|
||||
//! of multiple nodes. For example, a totally hypothetical custom text editor
|
||||
//! element may want to have [`Role::TextInput`], while presenting children
|
||||
//! consisting of [`Role::TextRun`]s.
|
||||
//!
|
||||
//! This is possible using [`Element::a11y_synthetic_children`]. For example:
|
||||
//! ```rust,ignore
|
||||
//! # use gpui::*;
|
||||
//! impl Element for MyCustomTextField {
|
||||
//!
|
||||
//! // ...
|
||||
//!
|
||||
//! fn a11y_role(&self) -> Option<Role> {
|
||||
//! Some(Role::TextInput)
|
||||
//! }
|
||||
//!
|
||||
//! fn a11y_synthetic_children(
|
||||
//! &mut self,
|
||||
//! _prepaint: &mut Self::PrepaintState,
|
||||
//! builder: &mut A11ySubtreeBuilder,
|
||||
//! ) {
|
||||
//! // Create the synthetic child node
|
||||
//! let mut run = accesskit::Node::new(Role::TextRun);
|
||||
//! run.set_value(self.text.clone());
|
||||
//! run.set_character_lengths(
|
||||
//! self.text.chars().map(|c| c.len_utf8() as u8).collect::<Vec<_>>(),
|
||||
//! );
|
||||
//!
|
||||
//! // Insert it as a child of `MyCustomTextField`
|
||||
//! let run_id = builder.synthetic_node_id(0);
|
||||
//! builder.push_child(run_id, run);
|
||||
//!
|
||||
//! // You can also mutate the parent (i.e. the `MyCustomTextField`)
|
||||
//! let caret = accesskit::TextPosition {
|
||||
//! node: run_id,
|
||||
//! character_index: self.cursor,
|
||||
//! };
|
||||
//! builder.parent_node().set_text_selection(accesskit::TextSelection {
|
||||
//! anchor: caret,
|
||||
//! focus: caret,
|
||||
//! });
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Notably, synthetic children are added *after* an element is
|
||||
//! [prepainted][Element::prepaint], so prepaint state can be used (for example,
|
||||
//! to determine what is visible on screen).
|
||||
//!
|
||||
//! ## 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 :)
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
//! In GPUI, every model or view in the application is actually owned by a single top-level object called the `App`. When a new entity or view is created (referred to collectively as _entities_), the application is given ownership of their state to enable their participation in a variety of app services and interaction with other entities.
|
||||
//!
|
||||
//! To illustrate, consider the trivial app below. We start the app by calling `run` with a callback, which is passed a reference to the `App` that owns all the state for the application. This `App` is our gateway to all application-level services, such as opening windows, presenting dialogs, etc. It also has an `insert_entity` method, which is called below to create an entity and give ownership of it to the application.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use gpui::{App, AppContext, Application, Entity};
|
||||
//! # struct Counter {
|
||||
//! # count: usize,
|
||||
//! # }
|
||||
//! gpui_platform::application().run(|cx: &mut App| {
|
||||
//! let _counter: Entity<Counter> = cx.new(|_cx| Counter { count: 0 });
|
||||
//! // ...
|
||||
//! });
|
||||
//! ```
|
||||
//!
|
||||
//! The call to `new_entity` returns an _entity handle_, which carries a type parameter based on the type of object it references. By itself, this `Entity<Counter>` handle doesn't provide access to the entity's state. It's merely an inert identifier plus a compile-time type tag, and it maintains a reference counted pointer to the underlying `Counter` object that is owned by the app.
|
||||
//!
|
||||
//! Much like an `Rc` from the Rust standard library, this reference count is incremented when the handle is cloned and decremented when it is dropped to enable shared ownership over the underlying model, but unlike an `Rc` it only provides access to the model's state when a reference to an `App` is available. The handle doesn't truly _own_ the state, but it can be used to access the state from its true owner, the `App`. Stripping away some of the setup code for brevity:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use gpui::{App, AppContext, Application, Context, Entity};
|
||||
//! # struct Counter {
|
||||
//! # count: usize,
|
||||
//! # }
|
||||
//! gpui_platform::application().run(|cx: &mut App| {
|
||||
//! let counter: Entity<Counter> = cx.new(|_cx| Counter { count: 0 });
|
||||
//! // Call `update` to access the model's state.
|
||||
//! counter.update(cx, |counter: &mut Counter, _cx: &mut Context<Counter>| {
|
||||
//! counter.count += 1;
|
||||
//! });
|
||||
//! });
|
||||
//! ```
|
||||
//!
|
||||
//! To update the counter, we call `update` on the handle, passing the context reference and a callback. The callback is yielded a mutable reference to the counter, which can be used to manipulate state.
|
||||
//!
|
||||
//! The callback is also provided a second `Context<Counter>` reference. This reference is similar to the `App` reference provided to the `run` callback. A `Context` is actually a wrapper around the `App`, including some additional data to indicate which particular entity it is tied to; in this case the counter.
|
||||
//!
|
||||
//! In addition to the application-level services provided by `App`, a `Context` provides access to entity-level services. For example, it can be used it to inform observers of this entity that its state has changed. Let's add that to our example, by calling `cx.notify()`.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use gpui::{App, AppContext, Application, Entity};
|
||||
//! # struct Counter {
|
||||
//! # count: usize,
|
||||
//! # }
|
||||
//! gpui_platform::application().run(|cx: &mut App| {
|
||||
//! let counter: Entity<Counter> = cx.new(|_cx| Counter { count: 0 });
|
||||
//! counter.update(cx, |counter, cx| {
|
||||
//! counter.count += 1;
|
||||
//! cx.notify(); // Notify observers
|
||||
//! });
|
||||
//! });
|
||||
//! ```
|
||||
//!
|
||||
//! Next, these notifications need to be observed and reacted to. Before updating the counter, we'll construct a second counter that observes it. Whenever the first counter changes, twice its count is assigned to the second counter. Note how `observe` is called on the `Context` belonging to our second counter to arrange for it to be notified whenever the first counter notifies. The call to `observe` returns a `Subscription`, which is `detach`ed to preserve this behavior for as long as both counters exist. We could also store this subscription and drop it at a time of our choosing to cancel this behavior.
|
||||
//!
|
||||
//! The `observe` callback is passed a mutable reference to the observer and a _handle_ to the observed counter, whose state we access with the `read` method.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use gpui::{App, AppContext, Application, Entity, prelude::*};
|
||||
//! # struct Counter {
|
||||
//! # count: usize,
|
||||
//! # }
|
||||
//! gpui_platform::application().run(|cx: &mut App| {
|
||||
//! let first_counter: Entity<Counter> = cx.new(|_cx| Counter { count: 0 });
|
||||
//!
|
||||
//! let second_counter = cx.new(|cx: &mut Context<Counter>| {
|
||||
//! // Note we can set up the callback before the Counter is even created!
|
||||
//! cx.observe(
|
||||
//! &first_counter,
|
||||
//! |second: &mut Counter, first: Entity<Counter>, cx| {
|
||||
//! second.count = first.read(cx).count * 2;
|
||||
//! },
|
||||
//! )
|
||||
//! .detach();
|
||||
//!
|
||||
//! Counter { count: 0 }
|
||||
//! });
|
||||
//!
|
||||
//! first_counter.update(cx, |counter, cx| {
|
||||
//! counter.count += 1;
|
||||
//! cx.notify();
|
||||
//! });
|
||||
//!
|
||||
//! assert_eq!(second_counter.read(cx).count, 2);
|
||||
//! });
|
||||
//! ```
|
||||
//!
|
||||
//! After updating the first counter, it can be noted that the observing counter's state is maintained according to our subscription.
|
||||
//!
|
||||
//! In addition to `observe` and `notify`, which indicate that an entity's state has changed, GPUI also offers `subscribe` and `emit`, which enables entities to emit typed events. To opt into this system, the emitting object must implement the `EventEmitter` trait.
|
||||
//!
|
||||
//! Let's introduce a new event type called `CounterChangeEvent`, then indicate that `Counter` can emit this type of event:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use gpui::EventEmitter;
|
||||
//! # struct Counter {
|
||||
//! # count: usize,
|
||||
//! # }
|
||||
//! struct CounterChangeEvent {
|
||||
//! increment: usize,
|
||||
//! }
|
||||
//!
|
||||
//! impl EventEmitter<CounterChangeEvent> for Counter {}
|
||||
//! ```
|
||||
//!
|
||||
//! Next, the example should be updated, replacing the observation with a subscription. Whenever the counter is incremented, a `Change` event is emitted to indicate the magnitude of the increase.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use gpui::{App, AppContext, Application, Context, Entity, EventEmitter};
|
||||
//! # struct Counter {
|
||||
//! # count: usize,
|
||||
//! # }
|
||||
//! # struct CounterChangeEvent {
|
||||
//! # increment: usize,
|
||||
//! # }
|
||||
//! # impl EventEmitter<CounterChangeEvent> for Counter {}
|
||||
//! gpui_platform::application().run(|cx: &mut App| {
|
||||
//! let first_counter: Entity<Counter> = cx.new(|_cx| Counter { count: 0 });
|
||||
//!
|
||||
//! let second_counter = cx.new(|cx: &mut Context<Counter>| {
|
||||
//! // Note we can set up the callback before the Counter is even created!
|
||||
//! cx.subscribe(&first_counter, |second: &mut Counter, _first: Entity<Counter>, event, _cx| {
|
||||
//! second.count += event.increment * 2;
|
||||
//! })
|
||||
//! .detach();
|
||||
//!
|
||||
//! Counter {
|
||||
//! count: first_counter.read(cx).count * 2,
|
||||
//! }
|
||||
//! });
|
||||
//!
|
||||
//! first_counter.update(cx, |first, cx| {
|
||||
//! first.count += 2;
|
||||
//! cx.emit(CounterChangeEvent { increment: 2 });
|
||||
//! cx.notify();
|
||||
//! });
|
||||
//!
|
||||
//! assert_eq!(second_counter.read(cx).count, 4);
|
||||
//! });
|
||||
//! ```
|
||||
Vendored
+459
@@ -0,0 +1,459 @@
|
||||
// Modified for gpui-pre (snapshot of zed@279fe07): the `actions!` derive paths are crate-relative.
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::{HashMap, TypeIdHashMap};
|
||||
pub use gpui_macros::Action;
|
||||
pub use no_action::{NoAction, Unbind, is_no_action, is_unbind};
|
||||
use serde_json::json;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
fmt::Display,
|
||||
};
|
||||
|
||||
/// Defines and registers unit structs that can be used as actions. For more complex data types, derive `Action`.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```
|
||||
/// use gpui::actions;
|
||||
/// actions!(editor, [MoveUp, MoveDown, MoveLeft, MoveRight, Newline]);
|
||||
/// ```
|
||||
///
|
||||
/// This will create actions with names like `editor::MoveUp`, `editor::MoveDown`, etc.
|
||||
///
|
||||
/// The namespace argument `editor` can also be omitted, though it is required for Zed actions.
|
||||
#[macro_export]
|
||||
macro_rules! actions {
|
||||
($namespace:path, [ $( $(#[$attr:meta])* $name:ident),* $(,)? ]) => {
|
||||
$(
|
||||
#[derive(::std::clone::Clone, ::std::cmp::PartialEq, ::std::default::Default, ::std::fmt::Debug, $crate::Action)]
|
||||
#[action(namespace = $namespace)]
|
||||
$(#[$attr])*
|
||||
pub struct $name;
|
||||
)*
|
||||
};
|
||||
([ $( $(#[$attr:meta])* $name:ident),* $(,)? ]) => {
|
||||
$(
|
||||
#[derive(::std::clone::Clone, ::std::cmp::PartialEq, ::std::default::Default, ::std::fmt::Debug, $crate::Action)]
|
||||
$(#[$attr])*
|
||||
pub struct $name;
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
/// Actions are used to implement keyboard-driven UI. When you declare an action, you can bind keys
|
||||
/// to the action in the keymap and listeners for that action in the element tree.
|
||||
///
|
||||
/// To declare a list of simple actions, you can use the actions! macro, which defines a simple unit
|
||||
/// struct action for each listed action name in the given namespace.
|
||||
///
|
||||
/// ```
|
||||
/// use gpui::actions;
|
||||
/// actions!(editor, [MoveUp, MoveDown, MoveLeft, MoveRight, Newline]);
|
||||
/// ```
|
||||
///
|
||||
/// Registering the actions with the same name will result in a panic during `App` creation.
|
||||
///
|
||||
/// # Derive Macro
|
||||
///
|
||||
/// More complex data types can also be actions, by using the derive macro for `Action`:
|
||||
///
|
||||
/// ```
|
||||
/// use gpui::Action;
|
||||
/// #[derive(Clone, PartialEq, serde::Deserialize, schemars::JsonSchema, Action)]
|
||||
/// #[action(namespace = editor)]
|
||||
/// pub struct SelectNext {
|
||||
/// pub replace_newest: bool,
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The derive macro for `Action` requires that the type implement `Clone` and `PartialEq`. It also
|
||||
/// requires `serde::Deserialize` and `schemars::JsonSchema` unless `#[action(no_json)]` is
|
||||
/// specified. In Zed these trait impls are used to load keymaps from JSON.
|
||||
///
|
||||
/// Multiple arguments separated by commas may be specified in `#[action(...)]`:
|
||||
///
|
||||
/// - `namespace = some_namespace` sets the namespace. In Zed this is required.
|
||||
///
|
||||
/// - `name = "ActionName"` overrides the action's name. This must not contain `::`.
|
||||
///
|
||||
/// - `no_json` causes the `build` method to always error and `action_json_schema` to return `None`,
|
||||
/// and allows actions not implement `serde::Serialize` and `schemars::JsonSchema`.
|
||||
///
|
||||
/// - `no_register` skips registering the action. This is useful for implementing the `Action` trait
|
||||
/// while not supporting invocation by name or JSON deserialization.
|
||||
///
|
||||
/// - `deprecated_aliases = ["editor::SomeAction"]` specifies deprecated old names for the action.
|
||||
/// These action names should *not* correspond to any actions that are registered. These old names
|
||||
/// can then still be used to refer to invoke this action. In Zed, the keymap JSON schema will
|
||||
/// accept these old names and provide warnings.
|
||||
///
|
||||
/// - `deprecated = "Message about why this action is deprecation"` specifies a deprecation message.
|
||||
/// In Zed, the keymap JSON schema will cause this to be displayed as a warning.
|
||||
///
|
||||
/// # Manual Implementation
|
||||
///
|
||||
/// If you want to control the behavior of the action trait manually, you can use the lower-level
|
||||
/// `#[register_action]` macro, which only generates the code needed to register your action before
|
||||
/// `main`.
|
||||
///
|
||||
/// ```
|
||||
/// use gpui::{SharedString, register_action};
|
||||
/// #[derive(Clone, PartialEq, Eq, serde::Deserialize, schemars::JsonSchema)]
|
||||
/// pub struct Paste {
|
||||
/// pub content: SharedString,
|
||||
/// }
|
||||
///
|
||||
/// impl gpui::Action for Paste {
|
||||
/// # fn boxed_clone(&self) -> Box<dyn gpui::Action> { unimplemented!()}
|
||||
/// # fn partial_eq(&self, other: &dyn gpui::Action) -> bool { unimplemented!() }
|
||||
/// # fn name(&self) -> &'static str { "Paste" }
|
||||
/// # fn name_for_type() -> &'static str { "Paste" }
|
||||
/// # fn build(value: serde_json::Value) -> anyhow::Result<Box<dyn gpui::Action>> {
|
||||
/// # unimplemented!()
|
||||
/// # }
|
||||
/// }
|
||||
///
|
||||
/// register_action!(Paste);
|
||||
/// ```
|
||||
pub trait Action: Any + Send {
|
||||
/// Clone the action into a new box
|
||||
fn boxed_clone(&self) -> Box<dyn Action>;
|
||||
|
||||
/// Do a partial equality check on this action and the other
|
||||
fn partial_eq(&self, action: &dyn Action) -> bool;
|
||||
|
||||
/// Get the name of this action, for displaying in UI
|
||||
fn name(&self) -> &'static str;
|
||||
|
||||
/// Get the name of this action type (static)
|
||||
fn name_for_type() -> &'static str
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Build this action from a JSON value. This is used to construct actions from the keymap.
|
||||
/// A value of `{}` will be passed for actions that don't have any parameters.
|
||||
fn build(value: serde_json::Value) -> Result<Box<dyn Action>>
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Optional JSON schema for the action's input data.
|
||||
fn action_json_schema(_: &mut schemars::SchemaGenerator) -> Option<schemars::Schema>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
None
|
||||
}
|
||||
|
||||
/// A list of alternate, deprecated names for this action. These names can still be used to
|
||||
/// invoke the action. In Zed, the keymap JSON schema will accept these old names and provide
|
||||
/// warnings.
|
||||
fn deprecated_aliases() -> &'static [&'static str]
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
&[]
|
||||
}
|
||||
|
||||
/// Returns the deprecation message for this action, if any. In Zed, the keymap JSON schema will
|
||||
/// cause this to be displayed as a warning.
|
||||
fn deprecation_message() -> Option<&'static str>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
None
|
||||
}
|
||||
|
||||
/// The documentation for this action, if any. When using the derive macro for actions
|
||||
/// this will be automatically generated from the doc comments on the action struct.
|
||||
fn documentation() -> Option<&'static str>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for dyn Action {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("dyn Action")
|
||||
.field("name", &self.name())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl dyn Action {
|
||||
/// Type-erase Action type.
|
||||
pub fn as_any(&self) -> &dyn Any {
|
||||
self as &dyn Any
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use
|
||||
/// markdown to display it.
|
||||
#[derive(Debug)]
|
||||
pub enum ActionBuildError {
|
||||
/// Indicates that an action with this name has not been registered.
|
||||
NotFound {
|
||||
/// Name of the action that was not found.
|
||||
name: String,
|
||||
},
|
||||
/// Indicates that an error occurred while building the action, typically a JSON deserialization
|
||||
/// error.
|
||||
BuildError {
|
||||
/// Name of the action that was attempting to be built.
|
||||
name: String,
|
||||
/// Error that occurred while building the action.
|
||||
error: anyhow::Error,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::error::Error for ActionBuildError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
ActionBuildError::NotFound { .. } => None,
|
||||
ActionBuildError::BuildError { error, .. } => error.source(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ActionBuildError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ActionBuildError::NotFound { name } => {
|
||||
write!(f, "Didn't find an action named \"{name}\"")
|
||||
}
|
||||
ActionBuildError::BuildError { name, error } => {
|
||||
write!(f, "Error while building action \"{name}\": {error}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ActionBuilder = fn(json: serde_json::Value) -> anyhow::Result<Box<dyn Action>>;
|
||||
|
||||
pub(crate) struct ActionRegistry {
|
||||
by_name: HashMap<&'static str, ActionData>,
|
||||
names_by_type_id: TypeIdHashMap<&'static str>,
|
||||
all_names: Vec<&'static str>, // So we can return a static slice.
|
||||
deprecated_aliases: HashMap<&'static str, &'static str>, // deprecated name -> preferred name
|
||||
deprecation_messages: HashMap<&'static str, &'static str>, // action name -> deprecation message
|
||||
documentation: HashMap<&'static str, &'static str>, // action name -> documentation
|
||||
}
|
||||
|
||||
impl Default for ActionRegistry {
|
||||
fn default() -> Self {
|
||||
let mut this = ActionRegistry {
|
||||
by_name: Default::default(),
|
||||
names_by_type_id: Default::default(),
|
||||
documentation: Default::default(),
|
||||
all_names: Default::default(),
|
||||
deprecated_aliases: Default::default(),
|
||||
deprecation_messages: Default::default(),
|
||||
};
|
||||
|
||||
this.load_actions();
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
struct ActionData {
|
||||
pub build: ActionBuilder,
|
||||
pub json_schema: fn(&mut schemars::SchemaGenerator) -> Option<schemars::Schema>,
|
||||
}
|
||||
|
||||
/// This type must be public so that our macros can build it in other crates.
|
||||
/// But this is an implementation detail and should not be used directly.
|
||||
#[doc(hidden)]
|
||||
pub struct MacroActionBuilder(pub fn() -> MacroActionData);
|
||||
|
||||
/// This type must be public so that our macros can build it in other crates.
|
||||
/// But this is an implementation detail and should not be used directly.
|
||||
#[doc(hidden)]
|
||||
pub struct MacroActionData {
|
||||
pub name: &'static str,
|
||||
pub type_id: TypeId,
|
||||
pub build: ActionBuilder,
|
||||
pub json_schema: fn(&mut schemars::SchemaGenerator) -> Option<schemars::Schema>,
|
||||
pub deprecated_aliases: &'static [&'static str],
|
||||
pub deprecation_message: Option<&'static str>,
|
||||
pub documentation: Option<&'static str>,
|
||||
}
|
||||
|
||||
inventory::collect!(MacroActionBuilder);
|
||||
|
||||
impl ActionRegistry {
|
||||
/// Load all registered actions into the registry.
|
||||
pub(crate) fn load_actions(&mut self) {
|
||||
for builder in inventory::iter::<MacroActionBuilder> {
|
||||
let action = builder.0();
|
||||
self.insert_action(action);
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_action(&mut self, action: MacroActionData) {
|
||||
let name = action.name;
|
||||
if self.by_name.contains_key(name) {
|
||||
panic!(
|
||||
"Action with name `{name}` already registered \
|
||||
(might be registered in `#[action(deprecated_aliases = [...])]`."
|
||||
);
|
||||
}
|
||||
self.by_name.insert(
|
||||
name,
|
||||
ActionData {
|
||||
build: action.build,
|
||||
json_schema: action.json_schema,
|
||||
},
|
||||
);
|
||||
for &alias in action.deprecated_aliases {
|
||||
if self.by_name.contains_key(alias) {
|
||||
panic!(
|
||||
"Action with name `{alias}` already registered. \
|
||||
`{alias}` is specified in `#[action(deprecated_aliases = [...])]` for action `{name}`."
|
||||
);
|
||||
}
|
||||
self.by_name.insert(
|
||||
alias,
|
||||
ActionData {
|
||||
build: action.build,
|
||||
json_schema: action.json_schema,
|
||||
},
|
||||
);
|
||||
self.deprecated_aliases.insert(alias, name);
|
||||
self.all_names.push(alias);
|
||||
}
|
||||
self.names_by_type_id.insert(action.type_id, name);
|
||||
self.all_names.push(name);
|
||||
if let Some(deprecation_msg) = action.deprecation_message {
|
||||
self.deprecation_messages.insert(name, deprecation_msg);
|
||||
}
|
||||
if let Some(documentation) = action.documentation {
|
||||
self.documentation.insert(name, documentation);
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
|
||||
pub fn build_action_type(&self, type_id: &TypeId) -> Result<Box<dyn Action>> {
|
||||
let name = self
|
||||
.names_by_type_id
|
||||
.get(type_id)
|
||||
.with_context(|| format!("no action type registered for {type_id:?}"))?;
|
||||
|
||||
Ok(self.build_action(name, None)?)
|
||||
}
|
||||
|
||||
#[cfg(feature = "profiler")]
|
||||
pub(crate) fn try_resolve_action(&self, type_id: &TypeId) -> Option<&'static str> {
|
||||
self.names_by_type_id.get(type_id).copied()
|
||||
}
|
||||
|
||||
/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
|
||||
pub fn build_action(
|
||||
&self,
|
||||
name: &str,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> std::result::Result<Box<dyn Action>, ActionBuildError> {
|
||||
let build_action = self
|
||||
.by_name
|
||||
.get(name)
|
||||
.ok_or_else(|| ActionBuildError::NotFound {
|
||||
name: name.to_owned(),
|
||||
})?
|
||||
.build;
|
||||
(build_action)(params.unwrap_or_else(|| json!({}))).map_err(|e| {
|
||||
ActionBuildError::BuildError {
|
||||
name: name.to_owned(),
|
||||
error: e,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn all_action_names(&self) -> &[&'static str] {
|
||||
self.all_names.as_slice()
|
||||
}
|
||||
|
||||
pub fn action_schemas(
|
||||
&self,
|
||||
generator: &mut schemars::SchemaGenerator,
|
||||
) -> Vec<(&'static str, Option<schemars::Schema>)> {
|
||||
// Use the order from all_names so that the resulting schema has sensible order.
|
||||
self.all_names
|
||||
.iter()
|
||||
.map(|name| {
|
||||
let action_data = self
|
||||
.by_name
|
||||
.get(name)
|
||||
.expect("All actions in all_names should be registered");
|
||||
(*name, (action_data.json_schema)(generator))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub fn action_schema_by_name(
|
||||
&self,
|
||||
name: &str,
|
||||
generator: &mut schemars::SchemaGenerator,
|
||||
) -> Option<Option<schemars::Schema>> {
|
||||
self.by_name
|
||||
.get(name)
|
||||
.map(|action_data| (action_data.json_schema)(generator))
|
||||
}
|
||||
|
||||
pub fn deprecated_aliases(&self) -> &HashMap<&'static str, &'static str> {
|
||||
&self.deprecated_aliases
|
||||
}
|
||||
|
||||
pub fn deprecation_messages(&self) -> &HashMap<&'static str, &'static str> {
|
||||
&self.deprecation_messages
|
||||
}
|
||||
|
||||
pub fn documentation(&self) -> &HashMap<&'static str, &'static str> {
|
||||
&self.documentation
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a list of all the registered actions.
|
||||
/// Useful for transforming the list of available actions into a
|
||||
/// format suited for static analysis such as in validating keymaps, or
|
||||
/// generating documentation.
|
||||
pub fn generate_list_of_all_registered_actions() -> impl Iterator<Item = MacroActionData> {
|
||||
inventory::iter::<MacroActionBuilder>
|
||||
.into_iter()
|
||||
.map(|builder| builder.0())
|
||||
}
|
||||
|
||||
mod no_action {
|
||||
use crate as gpui;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
actions!(
|
||||
zed,
|
||||
[
|
||||
/// Action with special handling which unbinds the keybinding this is associated with,
|
||||
/// if it is the highest precedence match.
|
||||
NoAction
|
||||
]
|
||||
);
|
||||
|
||||
/// Action with special handling which unbinds later bindings for the same keystrokes when they
|
||||
/// dispatch the named action, regardless of that action's context.
|
||||
///
|
||||
/// In keymap JSON this is written as:
|
||||
///
|
||||
/// `["zed::Unbind", "editor::NewLine"]`
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize, JsonSchema, gpui::Action)]
|
||||
#[action(namespace = zed)]
|
||||
pub struct Unbind(pub gpui::SharedString);
|
||||
|
||||
/// Returns whether or not this action represents a removed key binding.
|
||||
pub fn is_no_action(action: &dyn gpui::Action) -> bool {
|
||||
action.as_any().is::<NoAction>()
|
||||
}
|
||||
|
||||
/// Returns whether or not this action represents an unbind marker.
|
||||
pub fn is_unbind(action: &dyn gpui::Action) -> bool {
|
||||
action.as_any().is::<Unbind>()
|
||||
}
|
||||
}
|
||||
Vendored
+3607
File diff suppressed because it is too large.
Load diff
+557
@@ -0,0 +1,557 @@
|
||||
use crate::{
|
||||
AnyView, AnyWindowHandle, App, AppCell, AppContext, BackgroundExecutor, BorrowAppContext,
|
||||
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};
|
||||
use futures::channel::oneshot;
|
||||
use futures::future::FutureExt;
|
||||
use std::{future::Future, rc::Weak};
|
||||
|
||||
use super::{Context, WeakEntity};
|
||||
|
||||
/// An async-friendly version of [App] with a static lifetime so it can be held across `await` points in async code.
|
||||
/// You're provided with an instance when calling [App::spawn], and you can also create one with [App::to_async].
|
||||
///
|
||||
/// Internally, this holds a weak reference to an `App`. Methods will panic if the app has been dropped,
|
||||
/// but this should not happen in practice when using foreground tasks spawned via `cx.spawn()`,
|
||||
/// as the executor checks if the app is alive before running each task.
|
||||
#[derive(Clone)]
|
||||
pub struct AsyncApp {
|
||||
pub(crate) app: Weak<AppCell>,
|
||||
pub(crate) background_executor: BackgroundExecutor,
|
||||
pub(crate) foreground_executor: ForegroundExecutor,
|
||||
}
|
||||
|
||||
impl AsyncApp {
|
||||
fn app(&self) -> std::rc::Rc<AppCell> {
|
||||
self.app
|
||||
.upgrade()
|
||||
.expect("app was released before async operation completed")
|
||||
}
|
||||
}
|
||||
|
||||
impl AppContext for AsyncApp {
|
||||
fn new<T: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T> {
|
||||
let app = self.app();
|
||||
let mut app = app.borrow_mut();
|
||||
app.new(build_entity)
|
||||
}
|
||||
|
||||
fn reserve_entity<T: 'static>(&mut self) -> Reservation<T> {
|
||||
let app = self.app();
|
||||
let mut app = app.borrow_mut();
|
||||
app.reserve_entity()
|
||||
}
|
||||
|
||||
fn insert_entity<T: 'static>(
|
||||
&mut self,
|
||||
reservation: Reservation<T>,
|
||||
build_entity: impl FnOnce(&mut Context<T>) -> T,
|
||||
) -> Entity<T> {
|
||||
let app = self.app();
|
||||
let mut app = app.borrow_mut();
|
||||
app.insert_entity(reservation, build_entity)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn update_entity<T: 'static, R>(
|
||||
&mut self,
|
||||
handle: &Entity<T>,
|
||||
update: impl FnOnce(&mut T, &mut Context<T>) -> R,
|
||||
) -> R {
|
||||
let app = self.app();
|
||||
let mut app = app.borrow_mut();
|
||||
app.update_entity(handle, update)
|
||||
}
|
||||
|
||||
fn as_mut<'a, T>(&'a mut self, _handle: &Entity<T>) -> GpuiBorrow<'a, T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
panic!("Cannot as_mut with an async context. Try calling update() first")
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn read_entity<T, R>(&self, handle: &Entity<T>, callback: impl FnOnce(&T, &App) -> R) -> R
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
let app = self.app();
|
||||
let lock = app.borrow();
|
||||
lock.read_entity(handle, callback)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
|
||||
where
|
||||
F: FnOnce(AnyView, &mut Window, &mut App) -> T,
|
||||
{
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
let mut lock = app.try_borrow_mut()?;
|
||||
if lock.quitting {
|
||||
bail!("app is quitting");
|
||||
}
|
||||
lock.update_window(window, f)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
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>,
|
||||
read: impl FnOnce(Entity<T>, &App) -> R,
|
||||
) -> Result<R>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
let lock = app.borrow();
|
||||
if lock.quitting {
|
||||
bail!("app is quitting");
|
||||
}
|
||||
lock.read_window(window, read)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
|
||||
where
|
||||
R: Send + 'static,
|
||||
{
|
||||
self.background_executor.spawn(future)
|
||||
}
|
||||
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
|
||||
where
|
||||
G: Global,
|
||||
{
|
||||
let app = self.app();
|
||||
let mut lock = app.borrow_mut();
|
||||
lock.update(|this| this.read_global(callback))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncApp {
|
||||
/// Schedules all windows in the application to be redrawn.
|
||||
pub fn refresh(&self) {
|
||||
let app = self.app();
|
||||
let mut lock = app.borrow_mut();
|
||||
// A direct call would leave the refresh effect queued, which cannot wake
|
||||
// a platform render loop that has already parked.
|
||||
lock.update(|cx| cx.refresh_windows());
|
||||
}
|
||||
|
||||
/// Get an executor which can be used to spawn futures in the background.
|
||||
pub fn background_executor(&self) -> &BackgroundExecutor {
|
||||
&self.background_executor
|
||||
}
|
||||
|
||||
/// Whether this app runs on the deterministic test scheduler. See
|
||||
/// [`BackgroundExecutor::is_test`].
|
||||
pub fn is_test(&self) -> bool {
|
||||
self.background_executor.is_test()
|
||||
}
|
||||
|
||||
/// Get an executor which can be used to spawn futures in the foreground.
|
||||
pub fn foreground_executor(&self) -> &ForegroundExecutor {
|
||||
&self.foreground_executor
|
||||
}
|
||||
|
||||
/// Invoke the given function in the context of the app, then flush any effects produced during its invocation.
|
||||
#[inline(always)]
|
||||
pub fn update<R>(&self, f: impl FnOnce(&mut App) -> R) -> R {
|
||||
let app = self.app();
|
||||
let mut lock = app.borrow_mut();
|
||||
lock.update(f)
|
||||
}
|
||||
|
||||
/// Arrange for the given callback to be invoked whenever the given entity emits an event of a given type.
|
||||
/// The callback is provided a handle to the emitting entity and a reference to the emitted event.
|
||||
pub fn subscribe<T, Event>(
|
||||
&mut self,
|
||||
entity: &Entity<T>,
|
||||
on_event: impl FnMut(Entity<T>, &Event, &mut App) + 'static,
|
||||
) -> Subscription
|
||||
where
|
||||
T: 'static + EventEmitter<Event>,
|
||||
Event: 'static,
|
||||
{
|
||||
let app = self.app();
|
||||
let mut lock = app.borrow_mut();
|
||||
lock.subscribe(entity, on_event)
|
||||
}
|
||||
|
||||
/// Open a window with the given options based on the root view returned by the given function.
|
||||
pub fn open_window<V>(
|
||||
&self,
|
||||
options: crate::WindowOptions,
|
||||
build_root_view: impl FnOnce(&mut Window, &mut App) -> Entity<V>,
|
||||
) -> Result<WindowHandle<V>>
|
||||
where
|
||||
V: 'static + Render,
|
||||
{
|
||||
let app = self.app();
|
||||
let mut lock = app.borrow_mut();
|
||||
if lock.quitting {
|
||||
bail!("app is quitting");
|
||||
}
|
||||
lock.open_window(options, build_root_view)
|
||||
}
|
||||
|
||||
/// Schedule a future to be polled in the foreground.
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn spawn<AsyncFn, R>(&self, f: AsyncFn) -> Task<R>
|
||||
where
|
||||
AsyncFn: AsyncFnOnce(&mut AsyncApp) -> R + 'static,
|
||||
R: 'static,
|
||||
{
|
||||
let mut cx = self.clone();
|
||||
self.foreground_executor
|
||||
.spawn(async move { f(&mut cx).await }.boxed_local())
|
||||
}
|
||||
|
||||
/// Determine whether global state of the specified type has been assigned.
|
||||
pub fn has_global<G: Global>(&self) -> bool {
|
||||
let app = self.app();
|
||||
let app = app.borrow_mut();
|
||||
app.has_global::<G>()
|
||||
}
|
||||
|
||||
/// Reads the global state of the specified type, passing it to the given callback.
|
||||
///
|
||||
/// Panics if no global state of the specified type has been assigned.
|
||||
#[inline(always)]
|
||||
pub fn read_global<G: Global, R>(&self, read: impl FnOnce(&G, &App) -> R) -> R {
|
||||
let app = self.app();
|
||||
let app = app.borrow_mut();
|
||||
read(app.global(), &app)
|
||||
}
|
||||
|
||||
/// Reads the global state of the specified type, passing it to the given callback.
|
||||
///
|
||||
/// Similar to [`AsyncApp::read_global`], but returns an error instead of panicking
|
||||
pub fn try_read_global<G: Global, R>(&self, read: impl FnOnce(&G, &App) -> R) -> Option<R> {
|
||||
let app = self.app();
|
||||
let app = app.borrow_mut();
|
||||
if app.quitting {
|
||||
return None;
|
||||
}
|
||||
Some(read(app.try_global()?, &app))
|
||||
}
|
||||
|
||||
/// Reads the global state of the specified type, passing it to the given callback.
|
||||
/// A default value is assigned if a global of this type has not yet been assigned.
|
||||
pub fn read_default_global<G: Global + Default, R>(
|
||||
&self,
|
||||
read: impl FnOnce(&G, &App) -> R,
|
||||
) -> R {
|
||||
let app = self.app();
|
||||
let mut app = app.borrow_mut();
|
||||
app.update(|cx| {
|
||||
cx.default_global::<G>();
|
||||
});
|
||||
read(app.global(), &app)
|
||||
}
|
||||
|
||||
/// A convenience method for [`App::update_global`](BorrowAppContext::update_global)
|
||||
/// for updating the global state of the specified type.
|
||||
pub fn update_global<G: Global, R>(&self, update: impl FnOnce(&mut G, &mut App) -> R) -> R {
|
||||
let app = self.app();
|
||||
let mut app = app.borrow_mut();
|
||||
app.update(|cx| cx.update_global(update))
|
||||
}
|
||||
|
||||
/// Run something using this entity and cx, when the returned struct is dropped
|
||||
pub fn on_drop<T: 'static, Callback: FnOnce(&mut T, &mut Context<T>) + 'static>(
|
||||
&self,
|
||||
entity: &WeakEntity<T>,
|
||||
f: Callback,
|
||||
) -> gpui_util::Deferred<impl FnOnce() + use<T, Callback>> {
|
||||
let entity = entity.clone();
|
||||
let mut cx = self.clone();
|
||||
gpui_util::defer(move || {
|
||||
entity.update(&mut cx, f).ok();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A cloneable, owned handle to the application context,
|
||||
/// composed with the window associated with the current task.
|
||||
#[derive(Clone, Deref, DerefMut)]
|
||||
pub struct AsyncWindowContext {
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
app: AsyncApp,
|
||||
window: AnyWindowHandle,
|
||||
}
|
||||
|
||||
impl AsyncWindowContext {
|
||||
pub(crate) fn new_context(app: AsyncApp, window: AnyWindowHandle) -> Self {
|
||||
Self { app, window }
|
||||
}
|
||||
|
||||
/// Get the handle of the window this context is associated with.
|
||||
pub fn window_handle(&self) -> AnyWindowHandle {
|
||||
self.window
|
||||
}
|
||||
|
||||
/// A convenience method for [`App::update_window`].
|
||||
#[inline(always)]
|
||||
pub fn update<R>(&mut self, update: impl FnOnce(&mut Window, &mut App) -> R) -> Result<R> {
|
||||
self.app
|
||||
.update_window(self.window, |_, window, cx| update(window, cx))
|
||||
}
|
||||
|
||||
/// A convenience method for [`App::update_window`].
|
||||
#[inline(always)]
|
||||
pub fn update_root<R>(
|
||||
&mut self,
|
||||
update: impl FnOnce(AnyView, &mut Window, &mut App) -> R,
|
||||
) -> Result<R> {
|
||||
self.app.update_window(self.window, update)
|
||||
}
|
||||
|
||||
/// A convenience method for [`Window::on_next_frame`].
|
||||
pub fn on_next_frame(&mut self, f: impl FnOnce(&mut Window, &mut App) + 'static) {
|
||||
self.app
|
||||
.update_window(self.window, |_, window, _| window.on_next_frame(f))
|
||||
.ok();
|
||||
}
|
||||
|
||||
/// A convenience method for [`App::global`].
|
||||
pub fn read_global<G: Global, R>(
|
||||
&mut self,
|
||||
read: impl FnOnce(&G, &Window, &App) -> R,
|
||||
) -> Result<R> {
|
||||
self.app
|
||||
.update_window(self.window, |_, window, cx| read(cx.global(), window, cx))
|
||||
}
|
||||
|
||||
/// A convenience method for [`App::update_global`](BorrowAppContext::update_global).
|
||||
/// for updating the global state of the specified type.
|
||||
pub fn update_global<G, R>(
|
||||
&mut self,
|
||||
update: impl FnOnce(&mut G, &mut Window, &mut App) -> R,
|
||||
) -> Result<R>
|
||||
where
|
||||
G: Global,
|
||||
{
|
||||
self.app.update_window(self.window, |_, window, cx| {
|
||||
cx.update_global(|global, cx| update(global, window, cx))
|
||||
})
|
||||
}
|
||||
|
||||
/// Schedule a future to be executed on the main thread. This is used for collecting
|
||||
/// the results of background tasks and updating the UI.
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn spawn<AsyncFn, R>(&self, f: AsyncFn) -> Task<R>
|
||||
where
|
||||
AsyncFn: AsyncFnOnce(&mut AsyncWindowContext) -> R + 'static,
|
||||
R: 'static,
|
||||
{
|
||||
let mut cx = self.clone();
|
||||
self.foreground_executor
|
||||
.spawn(async move { f(&mut cx).await }.boxed_local())
|
||||
}
|
||||
|
||||
/// Present a platform dialog.
|
||||
/// The provided message will be presented, along with buttons for each answer.
|
||||
/// When a button is clicked, the returned Receiver will receive the index of the clicked button.
|
||||
pub fn prompt<T>(
|
||||
&mut self,
|
||||
level: PromptLevel,
|
||||
message: &str,
|
||||
detail: Option<&str>,
|
||||
answers: &[T],
|
||||
) -> oneshot::Receiver<usize>
|
||||
where
|
||||
T: Clone + Into<PromptButton>,
|
||||
{
|
||||
self.app
|
||||
.update_window(self.window, |_, window, cx| {
|
||||
window.prompt(level, message, detail, answers, cx)
|
||||
})
|
||||
.unwrap_or_else(|_| oneshot::channel().1)
|
||||
}
|
||||
}
|
||||
|
||||
impl AppContext for AsyncWindowContext {
|
||||
fn new<T>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
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> {
|
||||
self.app.reserve_entity()
|
||||
}
|
||||
|
||||
fn insert_entity<T: 'static>(
|
||||
&mut self,
|
||||
reservation: Reservation<T>,
|
||||
build_entity: impl FnOnce(&mut Context<T>) -> T,
|
||||
) -> Entity<T> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn update_entity<T: 'static, R>(
|
||||
&mut self,
|
||||
handle: &Entity<T>,
|
||||
update: impl FnOnce(&mut T, &mut Context<T>) -> R,
|
||||
) -> R {
|
||||
self.app.update_entity(handle, update)
|
||||
}
|
||||
|
||||
fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> GpuiBorrow<'a, T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
panic!("Cannot use as_mut() from an async context, call `update`")
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn read_entity<T, R>(&self, handle: &Entity<T>, read: impl FnOnce(&T, &App) -> R) -> R
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.app.read_entity(handle, read)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn update_window<T, F>(&mut self, window: AnyWindowHandle, update: F) -> Result<T>
|
||||
where
|
||||
F: FnOnce(AnyView, &mut Window, &mut App) -> T,
|
||||
{
|
||||
self.app.update_window(window, update)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
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>,
|
||||
read: impl FnOnce(Entity<T>, &App) -> R,
|
||||
) -> Result<R>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.app.read_window(window, read)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
|
||||
where
|
||||
R: Send + 'static,
|
||||
{
|
||||
self.app.background_executor.spawn(future)
|
||||
}
|
||||
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
|
||||
where
|
||||
G: Global,
|
||||
{
|
||||
self.app.read_global(callback)
|
||||
}
|
||||
}
|
||||
|
||||
impl VisualContext for AsyncWindowContext {
|
||||
type Result<T> = Result<T>;
|
||||
|
||||
fn window_handle(&self) -> AnyWindowHandle {
|
||||
self.window
|
||||
}
|
||||
|
||||
fn new_window_entity<T: 'static>(
|
||||
&mut self,
|
||||
build_entity: impl FnOnce(&mut Window, &mut Context<T>) -> T,
|
||||
) -> Result<Entity<T>> {
|
||||
self.app.update_window(self.window, |_, window, cx| {
|
||||
cx.new(|cx| build_entity(window, cx))
|
||||
})
|
||||
}
|
||||
|
||||
fn update_window_entity<T: 'static, R>(
|
||||
&mut self,
|
||||
view: &Entity<T>,
|
||||
update: impl FnOnce(&mut T, &mut Window, &mut Context<T>) -> R,
|
||||
) -> Result<R> {
|
||||
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>(
|
||||
&mut self,
|
||||
build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,
|
||||
) -> Result<Entity<V>>
|
||||
where
|
||||
V: 'static + Render,
|
||||
{
|
||||
self.app.update_window(self.window, |_, window, cx| {
|
||||
window.replace_root(cx, build_view)
|
||||
})
|
||||
}
|
||||
|
||||
fn focus<V>(&mut self, view: &Entity<V>) -> Result<()>
|
||||
where
|
||||
V: Focusable,
|
||||
{
|
||||
self.app.update_window(self.window, |_, window, cx| {
|
||||
view.read(cx).focus_handle(cx).focus(window, cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
+1596
File diff suppressed because it is too large.
Load diff
Vendored
+959
@@ -0,0 +1,959 @@
|
||||
use crate::{
|
||||
AnyView, AnyWindowHandle, AppContext, AsyncApp, DispatchPhase, Effect, EntityId, EventEmitter,
|
||||
FocusHandle, FocusOutEvent, Focusable, Global, KeystrokeObserver, Priority, Reservation,
|
||||
SubscriberSet, Subscription, Task, WeakEntity, WeakFocusHandle, Window, WindowHandle,
|
||||
WindowVisibility,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use futures::FutureExt;
|
||||
use gpui_util::Deferred;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
borrow::{Borrow, BorrowMut},
|
||||
future::Future,
|
||||
ops,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use super::{App, AsyncWindowContext, Entity, KeystrokeEvent};
|
||||
|
||||
/// The app context, with specialized behavior for the given entity.
|
||||
pub struct Context<'a, T> {
|
||||
app: &'a mut App,
|
||||
entity_state: WeakEntity<T>,
|
||||
}
|
||||
|
||||
impl<'a, T> ops::Deref for Context<'a, T> {
|
||||
type Target = App;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.app
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> ops::DerefMut for Context<'a, T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.app
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: 'static> Context<'a, T> {
|
||||
pub(crate) fn new_context(app: &'a mut App, entity_state: WeakEntity<T>) -> Self {
|
||||
Self { app, entity_state }
|
||||
}
|
||||
|
||||
/// The entity id of the entity backing this context.
|
||||
pub fn entity_id(&self) -> EntityId {
|
||||
self.entity_state.entity_id
|
||||
}
|
||||
|
||||
/// Returns a handle to the entity belonging to this context.
|
||||
pub fn entity(&self) -> Entity<T> {
|
||||
self.weak_entity()
|
||||
.upgrade()
|
||||
.expect("The entity must be alive if we have a entity context")
|
||||
}
|
||||
|
||||
/// Returns a weak handle to the entity belonging to this context.
|
||||
pub fn weak_entity(&self) -> WeakEntity<T> {
|
||||
self.entity_state.clone()
|
||||
}
|
||||
|
||||
/// Arranges for the given function to be called whenever [`Context::notify`] is
|
||||
/// called with the given entity.
|
||||
pub fn observe<W>(
|
||||
&mut self,
|
||||
entity: &Entity<W>,
|
||||
mut on_notify: impl FnMut(&mut T, Entity<W>, &mut Context<T>) + 'static,
|
||||
) -> Subscription
|
||||
where
|
||||
T: 'static,
|
||||
W: 'static,
|
||||
{
|
||||
let observer = self.weak_entity();
|
||||
self.app.observe_internal(entity, move |entity, cx| {
|
||||
invoke_observer(&observer, entity, cx, &mut on_notify)
|
||||
})
|
||||
}
|
||||
|
||||
/// Observe changes to ourselves
|
||||
pub fn observe_self(
|
||||
&mut self,
|
||||
mut on_event: impl FnMut(&mut T, &mut Context<T>) + 'static,
|
||||
) -> Subscription
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
let this = self.entity();
|
||||
self.app.observe(&this, move |this, cx| {
|
||||
this.update(cx, |this, cx| on_event(this, cx))
|
||||
})
|
||||
}
|
||||
|
||||
/// Subscribe to an event type from another entity
|
||||
pub fn subscribe<T2, Evt>(
|
||||
&mut self,
|
||||
entity: &Entity<T2>,
|
||||
mut on_event: impl FnMut(&mut T, Entity<T2>, &Evt, &mut Context<T>) + 'static,
|
||||
) -> Subscription
|
||||
where
|
||||
T: 'static,
|
||||
T2: 'static + EventEmitter<Evt>,
|
||||
Evt: 'static,
|
||||
{
|
||||
let subscriber = self.weak_entity();
|
||||
self.app
|
||||
.subscribe_internal(entity, move |entity, event, cx| {
|
||||
invoke_subscriber(&subscriber, entity, event, cx, &mut on_event)
|
||||
})
|
||||
}
|
||||
|
||||
/// Subscribe to an event type from ourself
|
||||
pub fn subscribe_self<Evt>(
|
||||
&mut self,
|
||||
mut on_event: impl FnMut(&mut T, &Evt, &mut Context<T>) + 'static,
|
||||
) -> Subscription
|
||||
where
|
||||
T: 'static + EventEmitter<Evt>,
|
||||
Evt: 'static,
|
||||
{
|
||||
let this = self.entity();
|
||||
self.app.subscribe(&this, move |this, evt, cx| {
|
||||
this.update(cx, |this, cx| on_event(this, evt, cx))
|
||||
})
|
||||
}
|
||||
|
||||
/// Register a callback to be invoked when GPUI releases this entity.
|
||||
pub fn on_release(&self, on_release: impl FnOnce(&mut T, &mut App) + 'static) -> Subscription
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
let (subscription, activate) = self.app.release_listeners.insert(
|
||||
self.entity_state.entity_id,
|
||||
Box::new(move |this, cx| {
|
||||
let this = this.downcast_mut().expect("invalid entity type");
|
||||
on_release(this, cx);
|
||||
}),
|
||||
);
|
||||
activate();
|
||||
subscription
|
||||
}
|
||||
|
||||
/// Register a callback to be run on the release of another entity
|
||||
pub fn observe_release<T2>(
|
||||
&self,
|
||||
entity: &Entity<T2>,
|
||||
on_release: impl FnOnce(&mut T, &mut T2, &mut Context<T>) + 'static,
|
||||
) -> Subscription
|
||||
where
|
||||
T: Any,
|
||||
T2: 'static,
|
||||
{
|
||||
let entity_id = entity.entity_id();
|
||||
let this = self.weak_entity();
|
||||
let (subscription, activate) = self.app.release_listeners.insert(
|
||||
entity_id,
|
||||
Box::new(move |entity, cx| {
|
||||
let entity = entity.downcast_mut().expect("invalid entity type");
|
||||
if let Some(this) = this.upgrade() {
|
||||
this.update(cx, |this, cx| on_release(this, entity, cx));
|
||||
}
|
||||
}),
|
||||
);
|
||||
activate();
|
||||
subscription
|
||||
}
|
||||
|
||||
/// Register a callback to for updates to the given global
|
||||
pub fn observe_global<G: 'static>(
|
||||
&mut self,
|
||||
mut f: impl FnMut(&mut T, &mut Context<T>) + 'static,
|
||||
) -> Subscription
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
let handle = self.weak_entity();
|
||||
let (subscription, activate) = self.global_observers.insert(
|
||||
TypeId::of::<G>(),
|
||||
Box::new(move |cx| handle.update(cx, |view, cx| f(view, cx)).is_ok()),
|
||||
);
|
||||
self.defer(move |_| activate());
|
||||
subscription
|
||||
}
|
||||
|
||||
/// Register a callback to be invoked when the application is about to restart.
|
||||
pub fn on_app_restart(
|
||||
&self,
|
||||
mut on_restart: impl FnMut(&mut T, &mut App) + 'static,
|
||||
) -> Subscription
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
let handle = self.weak_entity();
|
||||
self.app.on_app_restart(move |cx| {
|
||||
handle.update(cx, |entity, cx| on_restart(entity, cx)).ok();
|
||||
})
|
||||
}
|
||||
|
||||
/// Arrange for the given function to be invoked whenever the application is quit.
|
||||
/// The future returned from this callback will be polled for up to [crate::SHUTDOWN_TIMEOUT] until the app fully quits.
|
||||
pub fn on_app_quit<Fut>(
|
||||
&self,
|
||||
mut on_quit: impl FnMut(&mut T, &mut Context<T>) -> Fut + 'static,
|
||||
) -> Subscription
|
||||
where
|
||||
Fut: 'static + Future<Output = ()>,
|
||||
T: 'static,
|
||||
{
|
||||
let handle = self.weak_entity();
|
||||
self.app.on_app_quit(move |cx| {
|
||||
let future = handle.update(cx, |entity, cx| on_quit(entity, cx)).ok();
|
||||
async move {
|
||||
if let Some(future) = future {
|
||||
future.await;
|
||||
}
|
||||
}
|
||||
.boxed_local()
|
||||
})
|
||||
}
|
||||
|
||||
/// Tell GPUI that this entity has changed and observers of it should be notified.
|
||||
pub fn notify(&mut self) {
|
||||
self.app.notify(self.entity_state.entity_id);
|
||||
}
|
||||
|
||||
/// Spawn the future returned by the given function.
|
||||
/// The function is provided a weak handle to the entity owned by this context and a context that can be held across await points.
|
||||
/// The returned task must be held or detached.
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn spawn<AsyncFn, R>(&self, f: AsyncFn) -> Task<R>
|
||||
where
|
||||
T: 'static,
|
||||
AsyncFn: AsyncFnOnce(WeakEntity<T>, &mut AsyncApp) -> R + 'static,
|
||||
R: 'static,
|
||||
{
|
||||
let this = self.weak_entity();
|
||||
self.app.spawn(async move |cx| f(this, cx).await)
|
||||
}
|
||||
|
||||
/// Convenience method for accessing view state in an event callback.
|
||||
///
|
||||
/// Many GPUI callbacks take the form of `Fn(&E, &mut Window, &mut App)`,
|
||||
/// but it's often useful to be able to access view state in these
|
||||
/// callbacks. This method provides a convenient way to do so.
|
||||
#[inline(always)]
|
||||
pub fn listener<E: ?Sized>(
|
||||
&self,
|
||||
listener: impl Fn(&mut T, &E, &mut Window, &mut Context<T>) + 'static,
|
||||
) -> impl Fn(&E, &mut Window, &mut App) + 'static {
|
||||
let view = self.entity().downgrade();
|
||||
move |event: &E, window: &mut Window, cx: &mut App| {
|
||||
invoke_listener(&view, window, cx, &|view, window, cx| {
|
||||
listener(view, event, window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience method for producing view state in a closure.
|
||||
/// See `listener` for more details.
|
||||
pub fn processor<E, R>(
|
||||
&self,
|
||||
f: impl Fn(&mut T, E, &mut Window, &mut Context<T>) -> R + 'static,
|
||||
) -> impl Fn(E, &mut Window, &mut App) -> R + 'static {
|
||||
let view = self.entity();
|
||||
move |e: E, window: &mut Window, cx: &mut App| {
|
||||
view.update(cx, |view, cx| f(view, e, window, cx))
|
||||
}
|
||||
}
|
||||
|
||||
/// Run something using this entity and cx, when the returned struct is dropped
|
||||
pub fn on_drop(
|
||||
&self,
|
||||
f: impl FnOnce(&mut T, &mut Context<T>) + 'static,
|
||||
) -> Deferred<impl FnOnce()> {
|
||||
let this = self.weak_entity();
|
||||
let mut cx = self.to_async();
|
||||
gpui_util::defer(move || {
|
||||
this.update(&mut cx, f).ok();
|
||||
})
|
||||
}
|
||||
|
||||
/// Focus the given view in the given window. View type is required to implement Focusable.
|
||||
pub fn focus_view<W: Focusable>(&mut self, view: &Entity<W>, window: &mut Window) {
|
||||
window.focus(&view.focus_handle(self), self);
|
||||
}
|
||||
|
||||
/// Sets a given callback to be run on the next frame.
|
||||
pub fn on_next_frame(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
f: impl FnOnce(&mut T, &mut Window, &mut Context<T>) + 'static,
|
||||
) where
|
||||
T: 'static,
|
||||
{
|
||||
let view = self.entity();
|
||||
window.on_next_frame(move |window, cx| view.update(cx, |view, cx| f(view, window, cx)));
|
||||
}
|
||||
|
||||
/// Schedules the given function to be run at the end of the current effect cycle, allowing entities
|
||||
/// that are currently on the stack to be returned to the app.
|
||||
pub fn defer_in(
|
||||
&mut self,
|
||||
window: &Window,
|
||||
f: impl FnOnce(&mut T, &mut Window, &mut Context<T>) + 'static,
|
||||
) {
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Observe another entity for changes to its state, as tracked by [`Context::notify`].
|
||||
pub fn observe_in<V2>(
|
||||
&mut self,
|
||||
observed: &Entity<V2>,
|
||||
window: &mut Window,
|
||||
mut on_notify: impl FnMut(&mut T, Entity<V2>, &mut Window, &mut Context<T>) + 'static,
|
||||
) -> Subscription
|
||||
where
|
||||
V2: 'static,
|
||||
T: 'static,
|
||||
{
|
||||
let observed_id = observed.entity_id();
|
||||
let observed = observed.downgrade();
|
||||
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| invoke_observer_in(&observer, &observed, cx, &mut on_notify)),
|
||||
)
|
||||
}
|
||||
|
||||
/// Subscribe to events emitted by another entity.
|
||||
/// The entity to which you're subscribing must implement the [`EventEmitter`] trait.
|
||||
/// The callback will be invoked with a reference to the current view, a handle to the emitting `Entity`, the event, a mutable reference to the `Window`, and the context for the entity.
|
||||
pub fn subscribe_in<Emitter, Evt>(
|
||||
&mut self,
|
||||
emitter: &Entity<Emitter>,
|
||||
window: &Window,
|
||||
mut on_event: impl FnMut(&mut T, &Entity<Emitter>, &Evt, &mut Window, &mut Context<T>) + 'static,
|
||||
) -> Subscription
|
||||
where
|
||||
Emitter: EventEmitter<Evt>,
|
||||
Evt: 'static,
|
||||
{
|
||||
let emitter = emitter.downgrade();
|
||||
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| {
|
||||
invoke_subscriber_in(&subscriber, &emitter, event, cx, &mut on_event)
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/// Register a callback to be invoked when the view is released.
|
||||
///
|
||||
/// The callback receives a handle to the view's window. This handle may be
|
||||
/// invalid, if the window was closed before the view was released.
|
||||
pub fn on_release_in(
|
||||
&mut self,
|
||||
window: &Window,
|
||||
on_release: impl FnOnce(&mut T, &mut Window, &mut App) + 'static,
|
||||
) -> Subscription {
|
||||
let entity = self.entity();
|
||||
self.app.observe_release_in(&entity, window, on_release)
|
||||
}
|
||||
|
||||
/// Register a callback to be invoked when the given Entity is released.
|
||||
pub fn observe_release_in<T2>(
|
||||
&self,
|
||||
observed: &Entity<T2>,
|
||||
window: &Window,
|
||||
mut on_release: impl FnMut(&mut T, &mut T2, &mut Window, &mut Context<T>) + 'static,
|
||||
) -> Subscription
|
||||
where
|
||||
T: 'static,
|
||||
T2: 'static,
|
||||
{
|
||||
let observer = self.weak_entity();
|
||||
self.app
|
||||
.observe_release_in(observed, window, move |observed, window, cx| {
|
||||
observer
|
||||
.update(cx, |observer, cx| {
|
||||
on_release(observer, observed, window, cx)
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
/// Register a callback to be invoked when the window is resized.
|
||||
pub fn observe_window_bounds(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
mut callback: impl FnMut(&mut T, &mut Window, &mut Context<T>) + 'static,
|
||||
) -> Subscription {
|
||||
let view = self.weak_entity();
|
||||
let (subscription, activate) = window.bounds_observers.insert(
|
||||
(),
|
||||
Box::new(move |window, cx| {
|
||||
view.update(cx, |view, cx| callback(view, window, cx))
|
||||
.is_ok()
|
||||
}),
|
||||
);
|
||||
activate();
|
||||
subscription
|
||||
}
|
||||
|
||||
/// Register a callback to be invoked when the window is activated or deactivated.
|
||||
pub fn observe_window_activation(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
mut callback: impl FnMut(&mut T, &mut Window, &mut Context<T>) + 'static,
|
||||
) -> Subscription {
|
||||
let view = self.weak_entity();
|
||||
let (subscription, activate) = window.activation_observers.insert(
|
||||
(),
|
||||
Box::new(move |window, cx| {
|
||||
view.update(cx, |view, cx| callback(view, window, cx))
|
||||
.is_ok()
|
||||
}),
|
||||
);
|
||||
activate();
|
||||
subscription
|
||||
}
|
||||
|
||||
/// Registers a callback to be invoked when the window's visibility changes
|
||||
/// (see [`WindowVisibility`]).
|
||||
pub fn observe_window_visibility(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
mut callback: impl FnMut(&mut T, WindowVisibility, &mut Window, &mut Context<T>) + 'static,
|
||||
) -> Subscription {
|
||||
let view = self.weak_entity();
|
||||
let (subscription, activate) = window.visibility_observers.insert(
|
||||
(),
|
||||
Box::new(move |visibility, window, cx| {
|
||||
view.update(cx, |view, cx| callback(view, visibility, window, cx))
|
||||
.is_ok()
|
||||
}),
|
||||
);
|
||||
activate();
|
||||
subscription
|
||||
}
|
||||
|
||||
/// Registers a callback to be invoked when the window appearance changes.
|
||||
pub fn observe_window_appearance(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
mut callback: impl FnMut(&mut T, &mut Window, &mut Context<T>) + 'static,
|
||||
) -> Subscription {
|
||||
let view = self.weak_entity();
|
||||
let (subscription, activate) = window.appearance_observers.insert(
|
||||
(),
|
||||
Box::new(move |window, cx| {
|
||||
view.update(cx, |view, cx| callback(view, window, cx))
|
||||
.is_ok()
|
||||
}),
|
||||
);
|
||||
activate();
|
||||
subscription
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Registers a callback for [`App::observe_keystrokes`] that updates this entity.
|
||||
pub fn observe_keystrokes(
|
||||
&mut self,
|
||||
mut f: impl FnMut(&mut T, &KeystrokeEvent, &mut Window, &mut Context<T>) + 'static,
|
||||
) -> Subscription {
|
||||
fn inner(
|
||||
keystroke_observers: &SubscriberSet<(), KeystrokeObserver>,
|
||||
handler: KeystrokeObserver,
|
||||
) -> Subscription {
|
||||
let (subscription, activate) = keystroke_observers.insert((), handler);
|
||||
activate();
|
||||
subscription
|
||||
}
|
||||
|
||||
let view = self.weak_entity();
|
||||
inner(
|
||||
&self.keystroke_observers,
|
||||
Box::new(move |event, window, cx| {
|
||||
if let Some(view) = view.upgrade() {
|
||||
view.update(cx, |view, cx| f(view, event, window, cx));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// Register a callback to be invoked when the window's pending input changes.
|
||||
pub fn observe_pending_input(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
mut callback: impl FnMut(&mut T, &mut Window, &mut Context<T>) + 'static,
|
||||
) -> Subscription {
|
||||
let view = self.weak_entity();
|
||||
let (subscription, activate) = window.pending_input_observers.insert(
|
||||
(),
|
||||
Box::new(move |window, cx| {
|
||||
view.update(cx, |view, cx| callback(view, window, cx))
|
||||
.is_ok()
|
||||
}),
|
||||
);
|
||||
activate();
|
||||
subscription
|
||||
}
|
||||
|
||||
/// Register a listener to be called when the given focus handle receives focus.
|
||||
/// Returns a subscription and persists until the subscription is dropped.
|
||||
pub fn on_focus(
|
||||
&mut self,
|
||||
handle: &FocusHandle,
|
||||
window: &mut Window,
|
||||
mut listener: impl FnMut(&mut T, &mut Window, &mut Context<T>) + 'static,
|
||||
) -> Subscription {
|
||||
let view = self.weak_entity();
|
||||
let focus_id = handle.id;
|
||||
let (subscription, activate) =
|
||||
window.new_focus_listener(Box::new(move |event, window, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
if event.previous_focus_path.last() != Some(&focus_id)
|
||||
&& event.current_focus_path.last() == Some(&focus_id)
|
||||
{
|
||||
listener(view, window, cx)
|
||||
}
|
||||
})
|
||||
.is_ok()
|
||||
}));
|
||||
self.defer(|_| activate());
|
||||
subscription
|
||||
}
|
||||
|
||||
/// Register a listener to be called when the given focus handle or one of its descendants receives focus.
|
||||
/// This does not fire if the given focus handle - or one of its descendants - was previously focused.
|
||||
/// Returns a subscription and persists until the subscription is dropped.
|
||||
pub fn on_focus_in(
|
||||
&mut self,
|
||||
handle: &FocusHandle,
|
||||
window: &mut Window,
|
||||
mut listener: impl FnMut(&mut T, &mut Window, &mut Context<T>) + 'static,
|
||||
) -> Subscription {
|
||||
let view = self.weak_entity();
|
||||
let focus_id = handle.id;
|
||||
let (subscription, activate) =
|
||||
window.new_focus_listener(Box::new(move |event, window, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
if event.is_focus_in(focus_id) {
|
||||
listener(view, window, cx)
|
||||
}
|
||||
})
|
||||
.is_ok()
|
||||
}));
|
||||
self.defer(|_| activate());
|
||||
subscription
|
||||
}
|
||||
|
||||
/// Register a listener to be called when the given focus handle loses focus.
|
||||
/// Returns a subscription and persists until the subscription is dropped.
|
||||
pub fn on_blur(
|
||||
&mut self,
|
||||
handle: &FocusHandle,
|
||||
window: &mut Window,
|
||||
mut listener: impl FnMut(&mut T, &mut Window, &mut Context<T>) + 'static,
|
||||
) -> Subscription {
|
||||
let view = self.weak_entity();
|
||||
let focus_id = handle.id;
|
||||
let (subscription, activate) =
|
||||
window.new_focus_listener(Box::new(move |event, window, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
if event.previous_focus_path.last() == Some(&focus_id)
|
||||
&& event.current_focus_path.last() != Some(&focus_id)
|
||||
{
|
||||
listener(view, window, cx)
|
||||
}
|
||||
})
|
||||
.is_ok()
|
||||
}));
|
||||
self.defer(|_| activate());
|
||||
subscription
|
||||
}
|
||||
|
||||
/// Register a listener to be called when nothing in the window has focus.
|
||||
/// This typically happens when the node that was focused is removed from the tree,
|
||||
/// and this callback lets you chose a default place to restore the users focus.
|
||||
/// Returns a subscription and persists until the subscription is dropped.
|
||||
pub fn on_focus_lost(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
mut listener: impl FnMut(&mut T, &mut Window, &mut Context<T>) + 'static,
|
||||
) -> Subscription {
|
||||
let view = self.weak_entity();
|
||||
let (subscription, activate) = window.focus_lost_listeners.insert(
|
||||
(),
|
||||
Box::new(move |window, cx| {
|
||||
view.update(cx, |view, cx| listener(view, window, cx))
|
||||
.is_ok()
|
||||
}),
|
||||
);
|
||||
self.defer(|_| activate());
|
||||
subscription
|
||||
}
|
||||
|
||||
/// Register a listener to be called when the given focus handle or one of its descendants loses focus.
|
||||
/// Returns a subscription and persists until the subscription is dropped.
|
||||
pub fn on_focus_out(
|
||||
&mut self,
|
||||
handle: &FocusHandle,
|
||||
window: &mut Window,
|
||||
mut listener: impl FnMut(&mut T, FocusOutEvent, &mut Window, &mut Context<T>) + 'static,
|
||||
) -> Subscription {
|
||||
let view = self.weak_entity();
|
||||
let focus_id = handle.id;
|
||||
let (subscription, activate) =
|
||||
window.new_focus_listener(Box::new(move |event, window, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
if let Some(blurred_id) = event.previous_focus_path.last().copied()
|
||||
&& event.is_focus_out(focus_id)
|
||||
{
|
||||
let event = FocusOutEvent {
|
||||
blurred: WeakFocusHandle {
|
||||
id: blurred_id,
|
||||
handles: Arc::downgrade(&cx.focus_handles),
|
||||
},
|
||||
};
|
||||
listener(view, event, window, cx)
|
||||
}
|
||||
})
|
||||
.is_ok()
|
||||
}));
|
||||
self.defer(|_| activate());
|
||||
subscription
|
||||
}
|
||||
|
||||
/// Schedule a future to be run asynchronously.
|
||||
/// The given callback is invoked with a [`WeakEntity<V>`] to avoid leaking the entity for a long-running process.
|
||||
/// It's also given an [`AsyncWindowContext`], which can be used to access the state of the entity across await points.
|
||||
/// The returned future will be polled on the main thread.
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn spawn_in<AsyncFn, R>(&self, window: &Window, f: AsyncFn) -> Task<R>
|
||||
where
|
||||
R: 'static,
|
||||
AsyncFn: AsyncFnOnce(WeakEntity<T>, &mut AsyncWindowContext) -> R + 'static,
|
||||
{
|
||||
let view = self.weak_entity();
|
||||
window.spawn(self, async move |cx| f(view, cx).await)
|
||||
}
|
||||
|
||||
/// Schedule a future to be run asynchronously with the given priority.
|
||||
/// The given callback is invoked with a [`WeakEntity<V>`] to avoid leaking the entity for a long-running process.
|
||||
/// It's also given an [`AsyncWindowContext`], which can be used to access the state of the entity across await points.
|
||||
/// The returned future will be polled on the main thread.
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn spawn_in_with_priority<AsyncFn, R>(
|
||||
&self,
|
||||
priority: Priority,
|
||||
window: &Window,
|
||||
f: AsyncFn,
|
||||
) -> Task<R>
|
||||
where
|
||||
R: 'static,
|
||||
AsyncFn: AsyncFnOnce(WeakEntity<T>, &mut AsyncWindowContext) -> R + 'static,
|
||||
{
|
||||
let view = self.weak_entity();
|
||||
window.spawn_with_priority(priority, self, async move |cx| f(view, cx).await)
|
||||
}
|
||||
|
||||
/// Register a callback to be invoked when the given global state changes.
|
||||
pub fn observe_global_in<G: Global>(
|
||||
&mut self,
|
||||
window: &Window,
|
||||
mut f: impl FnMut(&mut T, &mut Window, &mut Context<T>) + 'static,
|
||||
) -> Subscription {
|
||||
let window_handle = window.handle;
|
||||
let view = self.weak_entity();
|
||||
let (subscription, activate) = self.global_observers.insert(
|
||||
TypeId::of::<G>(),
|
||||
Box::new(move |cx| {
|
||||
// If the entity has been dropped, remove this observer.
|
||||
if view.upgrade().is_none() {
|
||||
return false;
|
||||
}
|
||||
// If the window is unavailable (e.g. temporarily taken during a
|
||||
// nested update, or already closed), skip this notification but
|
||||
// keep the observer alive so it can fire on future changes.
|
||||
let Ok(entity_alive) = window_handle.update(cx, |_, window, cx| {
|
||||
view.update(cx, |view, cx| f(view, window, cx)).is_ok()
|
||||
}) else {
|
||||
return true;
|
||||
};
|
||||
entity_alive
|
||||
}),
|
||||
);
|
||||
self.defer(move |_| activate());
|
||||
subscription
|
||||
}
|
||||
|
||||
/// Register a callback to be invoked when the given Action type is dispatched to the window.
|
||||
pub fn on_action(
|
||||
&mut self,
|
||||
action_type: TypeId,
|
||||
window: &mut Window,
|
||||
listener: impl Fn(&mut T, &dyn Any, DispatchPhase, &mut Window, &mut Context<T>) + 'static,
|
||||
) {
|
||||
let handle = self.weak_entity();
|
||||
window.on_action(action_type, move |action, phase, window, cx| {
|
||||
handle
|
||||
.update(cx, |view, cx| {
|
||||
listener(view, action, phase, window, cx);
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
}
|
||||
|
||||
/// Move focus to the current view, assuming it implements [`Focusable`].
|
||||
pub fn focus_self(&mut self, window: &mut Window)
|
||||
where
|
||||
T: Focusable,
|
||||
{
|
||||
let view = self.entity();
|
||||
window.defer(self, move |window, cx| {
|
||||
view.read(cx).focus_handle(cx).focus(window, cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Context<'_, T> {
|
||||
/// Emit an event of the specified type, which can be handled by other entities that have subscribed via `subscribe` methods on their respective contexts.
|
||||
pub fn emit<Evt>(&mut self, event: Evt)
|
||||
where
|
||||
T: EventEmitter<Evt>,
|
||||
Evt: 'static,
|
||||
{
|
||||
let event = self
|
||||
.event_arena
|
||||
.alloc(|| event)
|
||||
.map(|it| it as &mut dyn Any);
|
||||
self.app.pending_effects.push_back(Effect::Emit {
|
||||
emitter: self.entity_state.entity_id,
|
||||
event_type: TypeId::of::<Evt>(),
|
||||
event,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AppContext for Context<'_, T> {
|
||||
#[inline]
|
||||
fn new<U: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<U>) -> U) -> Entity<U> {
|
||||
self.app.new(build_entity)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn reserve_entity<U: 'static>(&mut self) -> Reservation<U> {
|
||||
self.app.reserve_entity()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn insert_entity<U: 'static>(
|
||||
&mut self,
|
||||
reservation: Reservation<U>,
|
||||
build_entity: impl FnOnce(&mut Context<U>) -> U,
|
||||
) -> Entity<U> {
|
||||
self.app.insert_entity(reservation, build_entity)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn update_entity<U: 'static, R>(
|
||||
&mut self,
|
||||
handle: &Entity<U>,
|
||||
update: impl FnOnce(&mut U, &mut Context<U>) -> R,
|
||||
) -> R {
|
||||
self.app.update_entity(handle, update)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn as_mut<'a, E>(&'a mut self, handle: &Entity<E>) -> super::GpuiBorrow<'a, E>
|
||||
where
|
||||
E: 'static,
|
||||
{
|
||||
self.app.as_mut(handle)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read_entity<U, R>(&self, handle: &Entity<U>, read: impl FnOnce(&U, &App) -> R) -> R
|
||||
where
|
||||
U: 'static,
|
||||
{
|
||||
self.app.read_entity(handle, read)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn update_window<R, F>(&mut self, window: AnyWindowHandle, update: F) -> Result<R>
|
||||
where
|
||||
F: FnOnce(AnyView, &mut Window, &mut App) -> R,
|
||||
{
|
||||
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,
|
||||
window: &WindowHandle<U>,
|
||||
read: impl FnOnce(Entity<U>, &App) -> R,
|
||||
) -> Result<R>
|
||||
where
|
||||
U: 'static,
|
||||
{
|
||||
self.app.read_window(window, read)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
|
||||
where
|
||||
R: Send + 'static,
|
||||
{
|
||||
self.app.background_executor.spawn(future)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
|
||||
where
|
||||
G: Global,
|
||||
{
|
||||
self.app.read_global(callback)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Borrow<App> for Context<'_, T> {
|
||||
fn borrow(&self) -> &App {
|
||||
self.app
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> BorrowMut<App> for Context<'_, T> {
|
||||
fn borrow_mut(&mut self) -> &mut App {
|
||||
self.app
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
fn invoke_observer<T: 'static, W: 'static>(
|
||||
observer: &WeakEntity<T>,
|
||||
observed: Entity<W>,
|
||||
cx: &mut App,
|
||||
on_notify: &mut dyn FnMut(&mut T, Entity<W>, &mut Context<T>),
|
||||
) -> bool {
|
||||
if let Some(observer) = observer.upgrade() {
|
||||
observer.update(cx, |observer, cx| on_notify(observer, observed, cx));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
fn invoke_subscriber<T: 'static, Emitter: 'static, Event: 'static>(
|
||||
subscriber: &WeakEntity<T>,
|
||||
emitter: Entity<Emitter>,
|
||||
event: &Event,
|
||||
cx: &mut App,
|
||||
on_event: &mut dyn FnMut(&mut T, Entity<Emitter>, &Event, &mut Context<T>),
|
||||
) -> bool {
|
||||
if let Some(subscriber) = subscriber.upgrade() {
|
||||
subscriber.update(cx, |subscriber, cx| {
|
||||
on_event(subscriber, emitter, event, cx)
|
||||
});
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
fn invoke_observer_in<T: 'static, W: 'static>(
|
||||
observer: &WeakEntity<T>,
|
||||
observed: &WeakEntity<W>,
|
||||
cx: &mut App,
|
||||
on_notify: &mut dyn FnMut(&mut T, Entity<W>, &mut Window, &mut Context<T>),
|
||||
) -> bool {
|
||||
let Some((observer, observed)) = observer.upgrade().zip(observed.upgrade()) else {
|
||||
return false;
|
||||
};
|
||||
cx.with_window(observer.entity_id(), |window, cx| {
|
||||
observer.update(cx, |observer, cx| {
|
||||
on_notify(observer, observed, window, cx);
|
||||
});
|
||||
});
|
||||
true
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
fn invoke_subscriber_in<T: 'static, Emitter: 'static, Event: 'static>(
|
||||
subscriber: &WeakEntity<T>,
|
||||
emitter: &WeakEntity<Emitter>,
|
||||
event: &dyn Any,
|
||||
cx: &mut App,
|
||||
on_event: &mut dyn FnMut(&mut T, &Entity<Emitter>, &Event, &mut Window, &mut Context<T>),
|
||||
) -> bool {
|
||||
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.entity_id(), |window, cx| {
|
||||
subscriber.update(cx, |subscriber, cx| {
|
||||
on_event(subscriber, &emitter, event, window, cx);
|
||||
});
|
||||
});
|
||||
true
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
fn invoke_listener<T: 'static>(
|
||||
view: &WeakEntity<T>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
listener: &dyn Fn(&mut T, &mut Window, &mut Context<T>),
|
||||
) {
|
||||
view.update(cx, |view, cx| listener(view, window, cx)).ok();
|
||||
}
|
||||
+1308
File diff suppressed because it is too large.
Load diff
+284
@@ -0,0 +1,284 @@
|
||||
//! Cross-platform headless app context for tests that need real text shaping.
|
||||
//!
|
||||
//! This replaces the macOS-only `HeadlessMetalAppContext` with a platform-neutral
|
||||
//! implementation backed by `TestPlatform`. Tests supply a real `PlatformTextSystem`
|
||||
//! (e.g. `DirectWriteTextSystem` on Windows, `MacTextSystem` on macOS) to get
|
||||
//! accurate glyph measurements while keeping everything else deterministic.
|
||||
//!
|
||||
//! Optionally, a renderer factory can be provided to enable real GPU rendering
|
||||
//! and screenshot capture via [`HeadlessAppContext::capture_screenshot`].
|
||||
|
||||
use crate::{
|
||||
AnyView, AnyWindowHandle, App, AppCell, AppContext, AssetSource, BackgroundExecutor, Bounds,
|
||||
Context, Entity, EntityId, ForegroundExecutor, Global, Pixels, PlatformHeadlessRenderer,
|
||||
PlatformTextSystem, Render, Reservation, Size, Task, TestDispatcher, TestPlatform, TextSystem,
|
||||
Window, WindowBounds, WindowHandle, WindowOptions,
|
||||
app::{GpuiBorrow, GpuiMode},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use image::RgbaImage;
|
||||
use std::{future::Future, rc::Rc, sync::Arc, time::Duration};
|
||||
|
||||
/// A cross-platform headless app context for tests that need real text shaping.
|
||||
///
|
||||
/// Unlike the old `HeadlessMetalAppContext`, this works on any platform. It uses
|
||||
/// `TestPlatform` for deterministic scheduling and accepts a pluggable
|
||||
/// `PlatformTextSystem` so tests get real glyph measurements.
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// ```ignore
|
||||
/// let text_system = Arc::new(gpui_wgpu::CosmicTextSystem::new("fallback"));
|
||||
/// let mut cx = HeadlessAppContext::with_platform(
|
||||
/// text_system,
|
||||
/// Arc::new(Assets),
|
||||
/// || gpui_platform::current_headless_renderer(),
|
||||
/// );
|
||||
/// ```
|
||||
pub struct HeadlessAppContext {
|
||||
/// The underlying app cell.
|
||||
pub app: Rc<AppCell>,
|
||||
/// The background executor for running async tasks.
|
||||
pub background_executor: BackgroundExecutor,
|
||||
/// The foreground executor for running tasks on the main thread.
|
||||
pub foreground_executor: ForegroundExecutor,
|
||||
dispatcher: TestDispatcher,
|
||||
text_system: Arc<TextSystem>,
|
||||
}
|
||||
|
||||
impl HeadlessAppContext {
|
||||
/// Creates a new headless app context with the given text system.
|
||||
pub fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
|
||||
Self::with_platform(platform_text_system, Arc::new(()), || Ok(None))
|
||||
}
|
||||
|
||||
/// Creates a new headless app context with a custom text system and asset source.
|
||||
pub fn with_asset_source(
|
||||
platform_text_system: Arc<dyn PlatformTextSystem>,
|
||||
asset_source: Arc<dyn AssetSource>,
|
||||
) -> Self {
|
||||
Self::with_platform(platform_text_system, asset_source, || Ok(None))
|
||||
}
|
||||
|
||||
/// Creates a new headless app context with the given text system, asset source,
|
||||
/// and an optional renderer factory for screenshot support.
|
||||
pub fn with_platform(
|
||||
platform_text_system: Arc<dyn PlatformTextSystem>,
|
||||
asset_source: Arc<dyn AssetSource>,
|
||||
renderer_factory: impl Fn() -> Result<Option<Box<dyn PlatformHeadlessRenderer>>> + 'static,
|
||||
) -> Self {
|
||||
let seed = std::env::var("SEED")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
let dispatcher = TestDispatcher::new(seed);
|
||||
let arc_dispatcher = Arc::new(dispatcher.clone());
|
||||
let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
|
||||
let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
|
||||
|
||||
let renderer_factory: Box<dyn Fn() -> Result<Option<Box<dyn PlatformHeadlessRenderer>>>> =
|
||||
Box::new(renderer_factory);
|
||||
let platform = TestPlatform::with_platform(
|
||||
background_executor.clone(),
|
||||
foreground_executor.clone(),
|
||||
platform_text_system.clone(),
|
||||
Some(renderer_factory),
|
||||
);
|
||||
|
||||
let text_system = Arc::new(TextSystem::new(platform_text_system));
|
||||
let http_client = http_client::FakeHttpClient::with_404_response();
|
||||
let app = App::new_app(platform, asset_source, http_client);
|
||||
app.borrow_mut().mode = GpuiMode::test();
|
||||
|
||||
Self {
|
||||
app,
|
||||
background_executor,
|
||||
foreground_executor,
|
||||
dispatcher,
|
||||
text_system,
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens a window for headless rendering.
|
||||
pub fn open_window<V: Render + 'static>(
|
||||
&mut self,
|
||||
size: Size<Pixels>,
|
||||
build_root: impl FnOnce(&mut Window, &mut App) -> Entity<V>,
|
||||
) -> Result<WindowHandle<V>> {
|
||||
use crate::{point, px};
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: point(px(0.0), px(0.0)),
|
||||
size,
|
||||
};
|
||||
|
||||
let mut cx = self.app.borrow_mut();
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
focus: false,
|
||||
show: false,
|
||||
..Default::default()
|
||||
},
|
||||
build_root,
|
||||
)
|
||||
}
|
||||
|
||||
/// Runs all pending tasks until parked.
|
||||
pub fn run_until_parked(&self) {
|
||||
self.dispatcher.run_until_parked();
|
||||
}
|
||||
|
||||
/// Advances the simulated clock.
|
||||
pub fn advance_clock(&self, duration: Duration) {
|
||||
self.dispatcher.advance_clock(duration);
|
||||
}
|
||||
|
||||
/// Enables parking mode, allowing blocking on real I/O (e.g., async asset loading).
|
||||
pub fn allow_parking(&self) {
|
||||
self.dispatcher.allow_parking();
|
||||
}
|
||||
|
||||
/// Disables parking mode, returning to deterministic test execution.
|
||||
pub fn forbid_parking(&self) {
|
||||
self.dispatcher.forbid_parking();
|
||||
}
|
||||
|
||||
/// Updates app state.
|
||||
pub fn update<R>(&mut self, f: impl FnOnce(&mut App) -> R) -> R {
|
||||
let mut app = self.app.borrow_mut();
|
||||
f(&mut app)
|
||||
}
|
||||
|
||||
/// Updates a window and calls draw to render.
|
||||
pub fn update_window<R>(
|
||||
&mut self,
|
||||
window: AnyWindowHandle,
|
||||
f: impl FnOnce(AnyView, &mut Window, &mut App) -> R,
|
||||
) -> Result<R> {
|
||||
let mut app = self.app.borrow_mut();
|
||||
app.update_window(window, f)
|
||||
}
|
||||
|
||||
/// Captures a screenshot from a window.
|
||||
///
|
||||
/// Requires that the context was created with a renderer factory that
|
||||
/// returns `Some` via [`HeadlessAppContext::with_platform`].
|
||||
pub fn capture_screenshot(&mut self, window: AnyWindowHandle) -> Result<RgbaImage> {
|
||||
let mut app = self.app.borrow_mut();
|
||||
app.update_window(window, |_, window, _| window.render_to_image())?
|
||||
}
|
||||
|
||||
/// Returns the text system.
|
||||
pub fn text_system(&self) -> &Arc<TextSystem> {
|
||||
&self.text_system
|
||||
}
|
||||
|
||||
/// Returns the background executor.
|
||||
pub fn background_executor(&self) -> &BackgroundExecutor {
|
||||
&self.background_executor
|
||||
}
|
||||
|
||||
/// Returns the foreground executor.
|
||||
pub fn foreground_executor(&self) -> &ForegroundExecutor {
|
||||
&self.foreground_executor
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for HeadlessAppContext {
|
||||
fn drop(&mut self) {
|
||||
// Shut down the app so windows are closed and entity handles are
|
||||
// released before the LeakDetector runs.
|
||||
self.app.borrow_mut().shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
impl AppContext for HeadlessAppContext {
|
||||
fn new<T: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T> {
|
||||
let mut app = self.app.borrow_mut();
|
||||
app.new(build_entity)
|
||||
}
|
||||
|
||||
fn reserve_entity<T: 'static>(&mut self) -> Reservation<T> {
|
||||
let mut app = self.app.borrow_mut();
|
||||
app.reserve_entity()
|
||||
}
|
||||
|
||||
fn insert_entity<T: 'static>(
|
||||
&mut self,
|
||||
reservation: Reservation<T>,
|
||||
build_entity: impl FnOnce(&mut Context<T>) -> T,
|
||||
) -> Entity<T> {
|
||||
let mut app = self.app.borrow_mut();
|
||||
app.insert_entity(reservation, build_entity)
|
||||
}
|
||||
|
||||
fn update_entity<T: 'static, R>(
|
||||
&mut self,
|
||||
handle: &Entity<T>,
|
||||
update: impl FnOnce(&mut T, &mut Context<T>) -> R,
|
||||
) -> R {
|
||||
let mut app = self.app.borrow_mut();
|
||||
app.update_entity(handle, update)
|
||||
}
|
||||
|
||||
fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> GpuiBorrow<'a, T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
panic!("Cannot use as_mut with HeadlessAppContext. Call update() instead.")
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(&self, handle: &Entity<T>, read: impl FnOnce(&T, &App) -> R) -> R
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
let app = self.app.borrow();
|
||||
app.read_entity(handle, read)
|
||||
}
|
||||
|
||||
fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
|
||||
where
|
||||
F: FnOnce(AnyView, &mut Window, &mut App) -> T,
|
||||
{
|
||||
let mut lock = self.app.borrow_mut();
|
||||
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>,
|
||||
read: impl FnOnce(Entity<T>, &App) -> R,
|
||||
) -> Result<R>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
let app = self.app.borrow();
|
||||
app.read_window(window, read)
|
||||
}
|
||||
|
||||
fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
|
||||
where
|
||||
R: Send + 'static,
|
||||
{
|
||||
self.background_executor.spawn(future)
|
||||
}
|
||||
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
|
||||
where
|
||||
G: Global,
|
||||
{
|
||||
let app = self.app.borrow();
|
||||
app.read_global(callback)
|
||||
}
|
||||
}
|
||||
Vendored
+645
@@ -0,0 +1,645 @@
|
||||
//! A clean testing API for GPUI applications.
|
||||
//!
|
||||
//! `TestApp` provides a simpler alternative to `TestAppContext` with:
|
||||
//! - Automatic effect flushing after updates
|
||||
//! - Clean window creation and inspection
|
||||
//! - Input simulation helpers
|
||||
//!
|
||||
//! # Example
|
||||
//! ```ignore
|
||||
//! #[test]
|
||||
//! fn test_my_view() {
|
||||
//! let mut app = TestApp::new();
|
||||
//!
|
||||
//! let mut window = app.open_window(|window, cx| {
|
||||
//! MyView::new(window, cx)
|
||||
//! });
|
||||
//!
|
||||
//! window.update(|view, window, cx| {
|
||||
//! view.do_something(cx);
|
||||
//! });
|
||||
//!
|
||||
//! // Check rendered state
|
||||
//! assert_eq!(window.title(), Some("Expected Title"));
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use crate::{
|
||||
AnyWindowHandle, App, AppCell, AppContext, AsyncApp, BackgroundExecutor, BorrowAppContext,
|
||||
Bounds, ClipboardItem, Context, Entity, ForegroundExecutor, Global, InputEvent, Keystroke,
|
||||
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Platform,
|
||||
PlatformTextSystem, Point, Render, Size, Task, TestDispatcher, TestPlatform, TextSystem,
|
||||
Window, WindowBounds, WindowHandle, WindowOptions, app::GpuiMode,
|
||||
};
|
||||
use std::{future::Future, rc::Rc, sync::Arc, time::Duration};
|
||||
|
||||
/// A test application context with a clean API.
|
||||
///
|
||||
/// Unlike `TestAppContext`, `TestApp` automatically flushes effects after
|
||||
/// each update and provides simpler window management.
|
||||
pub struct TestApp {
|
||||
app: Rc<AppCell>,
|
||||
platform: Rc<TestPlatform>,
|
||||
background_executor: BackgroundExecutor,
|
||||
foreground_executor: ForegroundExecutor,
|
||||
#[allow(dead_code)]
|
||||
dispatcher: TestDispatcher,
|
||||
text_system: Arc<TextSystem>,
|
||||
}
|
||||
|
||||
impl TestApp {
|
||||
/// Create a new test application.
|
||||
pub fn new() -> Self {
|
||||
Self::with_seed(0)
|
||||
}
|
||||
|
||||
/// Create a new test application with a specific random seed.
|
||||
pub fn with_seed(seed: u64) -> Self {
|
||||
Self::build(seed, None, Arc::new(()))
|
||||
}
|
||||
|
||||
/// Create a new test application with a custom text system for real font shaping.
|
||||
pub fn with_text_system(text_system: Arc<dyn PlatformTextSystem>) -> Self {
|
||||
Self::build(0, Some(text_system), Arc::new(()))
|
||||
}
|
||||
|
||||
/// Create a new test application with a custom text system and asset source.
|
||||
pub fn with_text_system_and_assets(
|
||||
text_system: Arc<dyn PlatformTextSystem>,
|
||||
asset_source: Arc<dyn crate::AssetSource>,
|
||||
) -> Self {
|
||||
Self::build(0, Some(text_system), asset_source)
|
||||
}
|
||||
|
||||
fn build(
|
||||
seed: u64,
|
||||
platform_text_system: Option<Arc<dyn PlatformTextSystem>>,
|
||||
asset_source: Arc<dyn crate::AssetSource>,
|
||||
) -> Self {
|
||||
let dispatcher = TestDispatcher::new(seed);
|
||||
let arc_dispatcher = Arc::new(dispatcher.clone());
|
||||
let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
|
||||
let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
|
||||
let platform = match platform_text_system.clone() {
|
||||
Some(ts) => TestPlatform::with_text_system(
|
||||
background_executor.clone(),
|
||||
foreground_executor.clone(),
|
||||
ts,
|
||||
),
|
||||
None => TestPlatform::new(background_executor.clone(), foreground_executor.clone()),
|
||||
};
|
||||
let http_client = http_client::FakeHttpClient::with_404_response();
|
||||
let text_system = Arc::new(TextSystem::new(
|
||||
platform_text_system.unwrap_or_else(|| platform.text_system.clone()),
|
||||
));
|
||||
|
||||
let app = App::new_app(platform.clone(), asset_source, http_client);
|
||||
app.borrow_mut().mode = GpuiMode::test();
|
||||
|
||||
Self {
|
||||
app,
|
||||
platform,
|
||||
background_executor,
|
||||
foreground_executor,
|
||||
dispatcher,
|
||||
text_system,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a closure with mutable access to the App context.
|
||||
/// Automatically runs until parked after the closure completes.
|
||||
pub fn update<R>(&mut self, f: impl FnOnce(&mut App) -> R) -> R {
|
||||
let result = {
|
||||
let mut app = self.app.borrow_mut();
|
||||
app.update(f)
|
||||
};
|
||||
self.run_until_parked();
|
||||
result
|
||||
}
|
||||
|
||||
/// Run a closure with read-only access to the App context.
|
||||
pub fn read<R>(&self, f: impl FnOnce(&App) -> R) -> R {
|
||||
let app = self.app.borrow();
|
||||
f(&app)
|
||||
}
|
||||
|
||||
/// Create a new entity in the app.
|
||||
pub fn new_entity<T: 'static>(
|
||||
&mut self,
|
||||
build: impl FnOnce(&mut Context<T>) -> T,
|
||||
) -> Entity<T> {
|
||||
self.update(|cx| cx.new(build))
|
||||
}
|
||||
|
||||
/// Update an entity.
|
||||
pub fn update_entity<T: 'static, R>(
|
||||
&mut self,
|
||||
entity: &Entity<T>,
|
||||
f: impl FnOnce(&mut T, &mut Context<T>) -> R,
|
||||
) -> R {
|
||||
self.update(|cx| entity.update(cx, f))
|
||||
}
|
||||
|
||||
/// Read an entity.
|
||||
pub fn read_entity<T: 'static, R>(
|
||||
&self,
|
||||
entity: &Entity<T>,
|
||||
f: impl FnOnce(&T, &App) -> R,
|
||||
) -> R {
|
||||
self.read(|cx| f(entity.read(cx), cx))
|
||||
}
|
||||
|
||||
/// Open a test window with the given root view, using maximized bounds.
|
||||
pub fn open_window<V: Render + 'static>(
|
||||
&mut self,
|
||||
build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,
|
||||
) -> TestAppWindow<V> {
|
||||
let bounds = self.read(|cx| Bounds::maximized(None, cx));
|
||||
let handle = self.update(|cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| cx.new(|cx| build_view(window, cx)),
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
TestAppWindow {
|
||||
handle,
|
||||
app: self.app.clone(),
|
||||
platform: self.platform.clone(),
|
||||
background_executor: self.background_executor.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Open a test window with specific options.
|
||||
pub fn open_window_with_options<V: Render + 'static>(
|
||||
&mut self,
|
||||
options: WindowOptions,
|
||||
build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,
|
||||
) -> TestAppWindow<V> {
|
||||
let handle = self.update(|cx| {
|
||||
cx.open_window(options, |window, cx| cx.new(|cx| build_view(window, cx)))
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
TestAppWindow {
|
||||
handle,
|
||||
app: self.app.clone(),
|
||||
platform: self.platform.clone(),
|
||||
background_executor: self.background_executor.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Run pending tasks until there's nothing left to do.
|
||||
pub fn run_until_parked(&self) {
|
||||
self.background_executor.run_until_parked();
|
||||
}
|
||||
|
||||
/// Advance the simulated clock by the given duration.
|
||||
pub fn advance_clock(&self, duration: Duration) {
|
||||
self.background_executor.advance_clock(duration);
|
||||
}
|
||||
|
||||
/// Spawn a future on the foreground executor.
|
||||
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncApp) -> Fut) -> Task<R>
|
||||
where
|
||||
Fut: Future<Output = R> + 'static,
|
||||
R: 'static,
|
||||
{
|
||||
self.foreground_executor.spawn(f(self.to_async()))
|
||||
}
|
||||
|
||||
/// Spawn a future on the background executor.
|
||||
pub fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
|
||||
where
|
||||
R: Send + 'static,
|
||||
{
|
||||
self.background_executor.spawn(future)
|
||||
}
|
||||
|
||||
/// Get an async handle to the app.
|
||||
pub fn to_async(&self) -> AsyncApp {
|
||||
AsyncApp {
|
||||
app: Rc::downgrade(&self.app),
|
||||
background_executor: self.background_executor.clone(),
|
||||
foreground_executor: self.foreground_executor.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the background executor.
|
||||
pub fn background_executor(&self) -> &BackgroundExecutor {
|
||||
&self.background_executor
|
||||
}
|
||||
|
||||
/// Get the foreground executor.
|
||||
pub fn foreground_executor(&self) -> &ForegroundExecutor {
|
||||
&self.foreground_executor
|
||||
}
|
||||
|
||||
/// Get the text system.
|
||||
pub fn text_system(&self) -> &Arc<TextSystem> {
|
||||
&self.text_system
|
||||
}
|
||||
|
||||
/// Check if a global of the given type exists.
|
||||
pub fn has_global<G: Global>(&self) -> bool {
|
||||
self.read(|cx| cx.has_global::<G>())
|
||||
}
|
||||
|
||||
/// Set a global value.
|
||||
pub fn set_global<G: Global>(&mut self, global: G) {
|
||||
self.update(|cx| cx.set_global(global));
|
||||
}
|
||||
|
||||
/// Read a global value.
|
||||
pub fn read_global<G: Global, R>(&self, f: impl FnOnce(&G, &App) -> R) -> R {
|
||||
self.read(|cx| f(cx.global(), cx))
|
||||
}
|
||||
|
||||
/// Update a global value.
|
||||
pub fn update_global<G: Global, R>(&mut self, f: impl FnOnce(&mut G, &mut App) -> R) -> R {
|
||||
self.update(|cx| cx.update_global(f))
|
||||
}
|
||||
|
||||
// Platform simulation methods
|
||||
|
||||
/// Write text to the simulated clipboard.
|
||||
pub fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
self.platform.write_to_clipboard(item);
|
||||
}
|
||||
|
||||
/// Read from the simulated clipboard.
|
||||
pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
self.platform.read_from_clipboard()
|
||||
}
|
||||
|
||||
/// Get URLs that have been opened via `cx.open_url()`.
|
||||
pub fn opened_url(&self) -> Option<String> {
|
||||
self.platform.opened_url.borrow().clone()
|
||||
}
|
||||
|
||||
/// Check if a file path prompt is pending.
|
||||
pub fn did_prompt_for_new_path(&self) -> bool {
|
||||
self.platform.did_prompt_for_new_path()
|
||||
}
|
||||
|
||||
/// Simulate answering a path selection dialog.
|
||||
pub fn simulate_new_path_selection(
|
||||
&self,
|
||||
select: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
|
||||
) {
|
||||
self.platform.simulate_new_path_selection(select);
|
||||
}
|
||||
|
||||
/// Check if a prompt dialog is pending.
|
||||
pub fn has_pending_prompt(&self) -> bool {
|
||||
self.platform.has_pending_prompt()
|
||||
}
|
||||
|
||||
/// Simulate answering a prompt dialog.
|
||||
pub fn simulate_prompt_answer(&self, button: &str) {
|
||||
self.platform.simulate_prompt_answer(button);
|
||||
}
|
||||
|
||||
/// Get all open windows.
|
||||
pub fn windows(&self) -> Vec<AnyWindowHandle> {
|
||||
self.read(|cx| cx.windows())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TestApp {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// A test window with inspection and simulation capabilities.
|
||||
pub struct TestAppWindow<V> {
|
||||
handle: WindowHandle<V>,
|
||||
app: Rc<AppCell>,
|
||||
platform: Rc<TestPlatform>,
|
||||
background_executor: BackgroundExecutor,
|
||||
}
|
||||
|
||||
impl<V: 'static + Render> TestAppWindow<V> {
|
||||
/// Get the window handle.
|
||||
pub fn handle(&self) -> WindowHandle<V> {
|
||||
self.handle
|
||||
}
|
||||
|
||||
/// Get the root view entity.
|
||||
pub fn root(&self) -> Entity<V> {
|
||||
let mut app = self.app.borrow_mut();
|
||||
let any_handle: AnyWindowHandle = self.handle.into();
|
||||
app.update_window(any_handle, |root_view, _, _| {
|
||||
root_view.downcast::<V>().expect("root view type mismatch")
|
||||
})
|
||||
.expect("window not found")
|
||||
}
|
||||
|
||||
/// Update the root view.
|
||||
pub fn update<R>(&mut self, f: impl FnOnce(&mut V, &mut Window, &mut Context<V>) -> R) -> R {
|
||||
let result = {
|
||||
let mut app = self.app.borrow_mut();
|
||||
let any_handle: AnyWindowHandle = self.handle.into();
|
||||
app.update_window(any_handle, |root_view, window, cx| {
|
||||
let view = root_view.downcast::<V>().expect("root view type mismatch");
|
||||
view.update(cx, |view, cx| f(view, window, cx))
|
||||
})
|
||||
.expect("window not found")
|
||||
};
|
||||
self.background_executor.run_until_parked();
|
||||
result
|
||||
}
|
||||
|
||||
/// Read the root view.
|
||||
pub fn read<R>(&self, f: impl FnOnce(&V, &App) -> R) -> R {
|
||||
let app = self.app.borrow();
|
||||
let view = self
|
||||
.app
|
||||
.borrow()
|
||||
.windows
|
||||
.get(self.handle.window_id())
|
||||
.and_then(|w| w.as_ref())
|
||||
.and_then(|w| w.root.clone())
|
||||
.and_then(|r| r.downcast::<V>().ok())
|
||||
.expect("window or root view not found");
|
||||
f(view.read(&app), &app)
|
||||
}
|
||||
|
||||
/// Get the window title.
|
||||
pub fn title(&self) -> Option<String> {
|
||||
let app = self.app.borrow();
|
||||
app.read_window(&self.handle, |_, _cx| {
|
||||
// TODO: expose title through Window API
|
||||
None
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Simulate a keystroke.
|
||||
pub fn simulate_keystroke(&mut self, keystroke: &str) {
|
||||
let keystroke = Keystroke::parse(keystroke).unwrap();
|
||||
{
|
||||
let mut app = self.app.borrow_mut();
|
||||
let any_handle: AnyWindowHandle = self.handle.into();
|
||||
app.update_window(any_handle, |_, window, cx| {
|
||||
window.dispatch_keystroke(keystroke, cx);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
self.background_executor.run_until_parked();
|
||||
}
|
||||
|
||||
/// Simulate multiple keystrokes (space-separated).
|
||||
pub fn simulate_keystrokes(&mut self, keystrokes: &str) {
|
||||
for keystroke in keystrokes.split(' ') {
|
||||
self.simulate_keystroke(keystroke);
|
||||
}
|
||||
}
|
||||
|
||||
/// Simulate typing text.
|
||||
pub fn simulate_input(&mut self, input: &str) {
|
||||
for char in input.chars() {
|
||||
self.simulate_keystroke(&char.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
/// Simulate a mouse move.
|
||||
pub fn simulate_mouse_move(&mut self, position: Point<Pixels>) {
|
||||
self.simulate_event(MouseMoveEvent {
|
||||
position,
|
||||
modifiers: Default::default(),
|
||||
pressed_button: None,
|
||||
});
|
||||
}
|
||||
|
||||
/// Simulate a mouse down event.
|
||||
pub fn simulate_mouse_down(&mut self, position: Point<Pixels>, button: MouseButton) {
|
||||
self.simulate_event(MouseDownEvent {
|
||||
position,
|
||||
button,
|
||||
modifiers: Default::default(),
|
||||
click_count: 1,
|
||||
first_mouse: false,
|
||||
});
|
||||
}
|
||||
|
||||
/// Simulate a mouse up event.
|
||||
pub fn simulate_mouse_up(&mut self, position: Point<Pixels>, button: MouseButton) {
|
||||
self.simulate_event(MouseUpEvent {
|
||||
position,
|
||||
button,
|
||||
modifiers: Default::default(),
|
||||
click_count: 1,
|
||||
});
|
||||
}
|
||||
|
||||
/// Simulate a click at the given position.
|
||||
pub fn simulate_click(&mut self, position: Point<Pixels>, button: MouseButton) {
|
||||
self.simulate_mouse_down(position, button);
|
||||
self.simulate_mouse_up(position, button);
|
||||
}
|
||||
|
||||
/// Simulate a scroll event.
|
||||
pub fn simulate_scroll(&mut self, position: Point<Pixels>, delta: Point<Pixels>) {
|
||||
self.simulate_event(crate::ScrollWheelEvent {
|
||||
position,
|
||||
delta: crate::ScrollDelta::Pixels(delta),
|
||||
modifiers: Default::default(),
|
||||
touch_phase: crate::TouchPhase::Moved,
|
||||
});
|
||||
}
|
||||
|
||||
/// Simulate an input event.
|
||||
pub fn simulate_event<E: InputEvent>(&mut self, event: E) {
|
||||
let platform_input = event.to_platform_input();
|
||||
{
|
||||
let mut app = self.app.borrow_mut();
|
||||
let any_handle: AnyWindowHandle = self.handle.into();
|
||||
app.update_window(any_handle, |_, window, cx| {
|
||||
window.dispatch_event(platform_input, cx);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
self.background_executor.run_until_parked();
|
||||
}
|
||||
|
||||
/// Simulate resizing the window.
|
||||
pub fn simulate_resize(&mut self, size: Size<Pixels>) {
|
||||
let window_id = self.handle.window_id();
|
||||
let mut app = self.app.borrow_mut();
|
||||
if let Some(Some(window)) = app.windows.get_mut(window_id) {
|
||||
if let Some(test_window) = window.platform_window.as_test() {
|
||||
test_window.simulate_resize(size);
|
||||
}
|
||||
}
|
||||
drop(app);
|
||||
self.background_executor.run_until_parked();
|
||||
}
|
||||
|
||||
/// Simulate the window moving to a display with a different scale factor.
|
||||
pub fn simulate_scale_factor_change(&mut self, scale_factor: f32) {
|
||||
let window_id = self.handle.window_id();
|
||||
let test_window = {
|
||||
let mut app = self.app.borrow_mut();
|
||||
app.windows
|
||||
.get_mut(window_id)
|
||||
.and_then(|window| window.as_mut())
|
||||
.and_then(|window| window.platform_window.as_test())
|
||||
.cloned()
|
||||
};
|
||||
// The resize callback needs to borrow the app again synchronously.
|
||||
if let Some(mut test_window) = test_window {
|
||||
test_window.simulate_scale_factor_change(scale_factor);
|
||||
}
|
||||
self.background_executor.run_until_parked();
|
||||
}
|
||||
|
||||
/// Force a redraw of the window.
|
||||
pub fn draw(&mut self) {
|
||||
let mut app = self.app.borrow_mut();
|
||||
let any_handle: AnyWindowHandle = self.handle.into();
|
||||
app.update_window(any_handle, |_, window, cx| {
|
||||
window.draw(cx).clear(cx);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Clone for TestAppWindow<V> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
handle: self.handle,
|
||||
app: self.app.clone(),
|
||||
platform: self.platform.clone(),
|
||||
background_executor: self.background_executor.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{FocusHandle, Focusable, div, prelude::*};
|
||||
|
||||
struct Counter {
|
||||
count: usize,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Counter {
|
||||
fn new(_window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
Self {
|
||||
count: 0,
|
||||
focus_handle,
|
||||
}
|
||||
}
|
||||
|
||||
fn increment(&mut self, _cx: &mut Context<Self>) {
|
||||
self.count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for Counter {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Counter {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().child(format!("Count: {}", self.count))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_usage() {
|
||||
let mut app = TestApp::new();
|
||||
|
||||
let mut window = app.open_window(Counter::new);
|
||||
|
||||
window.update(|counter, _window, cx| {
|
||||
counter.increment(cx);
|
||||
});
|
||||
|
||||
window.read(|counter, _| {
|
||||
assert_eq!(counter.count, 1);
|
||||
});
|
||||
|
||||
drop(window);
|
||||
app.update(|cx| cx.shutdown());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simulate_scale_factor_change() {
|
||||
let mut app = TestApp::new();
|
||||
let mut window = app.open_window(|_, _| crate::EmptyView);
|
||||
let viewport_size = window.update(|_, window, _| {
|
||||
assert_eq!(window.scale_factor(), 2.0);
|
||||
window.viewport_size()
|
||||
});
|
||||
|
||||
window.simulate_scale_factor_change(1.0);
|
||||
|
||||
window.update(|_, window, _| {
|
||||
assert_eq!(window.scale_factor(), 1.0);
|
||||
assert_eq!(window.viewport_size(), viewport_size);
|
||||
});
|
||||
|
||||
drop(window);
|
||||
app.update(|cx| cx.shutdown());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_entity_creation() {
|
||||
let mut app = TestApp::new();
|
||||
|
||||
let entity = app.new_entity(|cx| Counter {
|
||||
count: 42,
|
||||
focus_handle: cx.focus_handle(),
|
||||
});
|
||||
|
||||
app.read_entity(&entity, |counter, _| {
|
||||
assert_eq!(counter.count, 42);
|
||||
});
|
||||
|
||||
app.update_entity(&entity, |counter, _cx| {
|
||||
counter.count += 1;
|
||||
});
|
||||
|
||||
app.read_entity(&entity, |counter, _| {
|
||||
assert_eq!(counter.count, 43);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_globals() {
|
||||
let mut app = TestApp::new();
|
||||
|
||||
struct MyGlobal(String);
|
||||
impl Global for MyGlobal {}
|
||||
|
||||
assert!(!app.has_global::<MyGlobal>());
|
||||
|
||||
app.set_global(MyGlobal("hello".into()));
|
||||
|
||||
assert!(app.has_global::<MyGlobal>());
|
||||
|
||||
app.read_global::<MyGlobal, _>(|global, _| {
|
||||
assert_eq!(global.0, "hello");
|
||||
});
|
||||
|
||||
app.update_global::<MyGlobal, _>(|global, _| {
|
||||
global.0 = "world".into();
|
||||
});
|
||||
|
||||
app.read_global::<MyGlobal, _>(|global, _| {
|
||||
assert_eq!(global.0, "world");
|
||||
});
|
||||
}
|
||||
}
|
||||
+1439
File diff suppressed because it is too large.
Load diff
+484
@@ -0,0 +1,484 @@
|
||||
use crate::{
|
||||
Action, AnyView, AnyWindowHandle, App, AppCell, AppContext, AssetSource, BackgroundExecutor,
|
||||
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;
|
||||
use std::{future::Future, rc::Rc, sync::Arc, time::Duration};
|
||||
|
||||
/// A test context that uses real macOS rendering instead of mocked rendering.
|
||||
/// This is used for visual tests that need to capture actual screenshots.
|
||||
///
|
||||
/// Unlike `TestAppContext` which uses `TestPlatform` with mocked rendering,
|
||||
/// `VisualTestAppContext` uses the real `MacPlatform` to produce actual rendered output.
|
||||
///
|
||||
/// Windows created through this context are positioned off-screen (at coordinates like -10000, -10000)
|
||||
/// so they are invisible to the user but still fully rendered by the compositor.
|
||||
#[derive(Clone)]
|
||||
pub struct VisualTestAppContext {
|
||||
/// The underlying app cell
|
||||
pub app: Rc<AppCell>,
|
||||
/// The background executor for running async tasks
|
||||
pub background_executor: BackgroundExecutor,
|
||||
/// The foreground executor for running tasks on the main thread
|
||||
pub foreground_executor: ForegroundExecutor,
|
||||
/// The test dispatcher for deterministic task scheduling
|
||||
dispatcher: TestDispatcher,
|
||||
platform: Rc<dyn Platform>,
|
||||
text_system: Arc<TextSystem>,
|
||||
}
|
||||
|
||||
impl VisualTestAppContext {
|
||||
/// Creates a new `VisualTestAppContext` with real macOS platform rendering
|
||||
/// but deterministic task scheduling via TestDispatcher.
|
||||
///
|
||||
/// This provides:
|
||||
/// - Real Metal/compositor rendering for accurate screenshots
|
||||
/// - Deterministic task scheduling via TestDispatcher
|
||||
/// - Controllable time via `advance_clock`
|
||||
///
|
||||
/// Note: This uses a no-op asset source, so SVG icons won't render.
|
||||
/// Use `with_asset_source` to provide real assets for icon rendering.
|
||||
pub fn new(platform: Rc<dyn Platform>) -> Self {
|
||||
Self::with_asset_source(platform, Arc::new(()))
|
||||
}
|
||||
|
||||
/// Creates a new `VisualTestAppContext` with a custom asset source.
|
||||
///
|
||||
/// Use this when you need SVG icons to render properly in visual tests.
|
||||
/// Pass the real `Assets` struct to enable icon rendering.
|
||||
pub fn with_asset_source(
|
||||
platform: Rc<dyn Platform>,
|
||||
asset_source: Arc<dyn AssetSource>,
|
||||
) -> Self {
|
||||
// Use a seeded RNG for deterministic behavior
|
||||
let seed = std::env::var("SEED")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
// Create a visual test platform that combines real Mac rendering
|
||||
// with controllable TestDispatcher for deterministic task scheduling
|
||||
let platform = Rc::new(VisualTestPlatform::new(platform, seed));
|
||||
|
||||
// Get the dispatcher and executors from the platform
|
||||
let dispatcher = platform.dispatcher().clone();
|
||||
let background_executor = platform.background_executor();
|
||||
let foreground_executor = platform.foreground_executor();
|
||||
|
||||
let text_system = Arc::new(TextSystem::new(platform.text_system()));
|
||||
|
||||
let http_client = http_client::FakeHttpClient::with_404_response();
|
||||
|
||||
let mut app = App::new_app(platform.clone(), asset_source, http_client);
|
||||
app.borrow_mut().mode = GpuiMode::test();
|
||||
|
||||
Self {
|
||||
app,
|
||||
background_executor,
|
||||
foreground_executor,
|
||||
dispatcher,
|
||||
platform,
|
||||
text_system,
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens a window positioned off-screen for invisible rendering.
|
||||
///
|
||||
/// The window is positioned at (-10000, -10000) so it's not visible on any display,
|
||||
/// but it's still fully rendered by the compositor and can be captured via ScreenCaptureKit.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `size` - The size of the window to create
|
||||
/// * `build_root` - A closure that builds the root view for the window
|
||||
pub fn open_offscreen_window<V: Render + 'static>(
|
||||
&mut self,
|
||||
size: Size<Pixels>,
|
||||
build_root: impl FnOnce(&mut Window, &mut App) -> Entity<V>,
|
||||
) -> Result<WindowHandle<V>> {
|
||||
use crate::{point, px};
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: point(px(-10000.0), px(-10000.0)),
|
||||
size,
|
||||
};
|
||||
|
||||
let mut cx = self.app.borrow_mut();
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
focus: false,
|
||||
show: true,
|
||||
..Default::default()
|
||||
},
|
||||
build_root,
|
||||
)
|
||||
}
|
||||
|
||||
/// Opens an off-screen window with default size (1280x800).
|
||||
pub fn open_offscreen_window_default<V: Render + 'static>(
|
||||
&mut self,
|
||||
build_root: impl FnOnce(&mut Window, &mut App) -> Entity<V>,
|
||||
) -> Result<WindowHandle<V>> {
|
||||
use crate::{px, size};
|
||||
self.open_offscreen_window(size(px(1280.0), px(800.0)), build_root)
|
||||
}
|
||||
|
||||
/// Returns whether screen capture is supported on this platform.
|
||||
pub fn is_screen_capture_supported(&self) -> bool {
|
||||
self.platform.is_screen_capture_supported()
|
||||
}
|
||||
|
||||
/// Returns the text system used by this context.
|
||||
pub fn text_system(&self) -> &Arc<TextSystem> {
|
||||
&self.text_system
|
||||
}
|
||||
|
||||
/// Returns the background executor.
|
||||
pub fn executor(&self) -> BackgroundExecutor {
|
||||
self.background_executor.clone()
|
||||
}
|
||||
|
||||
/// Returns the foreground executor.
|
||||
pub fn foreground_executor(&self) -> ForegroundExecutor {
|
||||
self.foreground_executor.clone()
|
||||
}
|
||||
|
||||
/// Runs all pending foreground and background tasks until there's nothing left to do.
|
||||
/// This is essential for processing async operations like tooltip timers.
|
||||
pub fn run_until_parked(&self) {
|
||||
self.dispatcher.run_until_parked();
|
||||
}
|
||||
|
||||
/// Advances the simulated clock by the given duration and processes any tasks
|
||||
/// that become ready. This is essential for testing time-based behaviors like
|
||||
/// tooltip delays.
|
||||
pub fn advance_clock(&self, duration: Duration) {
|
||||
self.dispatcher.advance_clock(duration);
|
||||
}
|
||||
|
||||
/// Updates the app state.
|
||||
pub fn update<R>(&mut self, f: impl FnOnce(&mut App) -> R) -> R {
|
||||
let mut app = self.app.borrow_mut();
|
||||
f(&mut app)
|
||||
}
|
||||
|
||||
/// Reads from the app state.
|
||||
pub fn read<R>(&self, f: impl FnOnce(&App) -> R) -> R {
|
||||
let app = self.app.borrow();
|
||||
f(&app)
|
||||
}
|
||||
|
||||
/// Updates a window.
|
||||
pub fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
|
||||
where
|
||||
F: FnOnce(AnyView, &mut Window, &mut App) -> T,
|
||||
{
|
||||
let mut lock = self.app.borrow_mut();
|
||||
lock.update_window(window, f)
|
||||
}
|
||||
|
||||
/// Spawns a task on the foreground executor.
|
||||
pub fn spawn<F, R>(&self, f: F) -> Task<R>
|
||||
where
|
||||
F: Future<Output = R> + 'static,
|
||||
R: 'static,
|
||||
{
|
||||
self.foreground_executor.spawn(f)
|
||||
}
|
||||
|
||||
/// Checks if a global of type G exists.
|
||||
pub fn has_global<G: Global>(&self) -> bool {
|
||||
let app = self.app.borrow();
|
||||
app.has_global::<G>()
|
||||
}
|
||||
|
||||
/// Reads a global value.
|
||||
pub fn read_global<G: Global, R>(&self, f: impl FnOnce(&G, &App) -> R) -> R {
|
||||
let app = self.app.borrow();
|
||||
f(app.global::<G>(), &app)
|
||||
}
|
||||
|
||||
/// Sets a global value.
|
||||
pub fn set_global<G: Global>(&mut self, global: G) {
|
||||
let mut app = self.app.borrow_mut();
|
||||
app.set_global(global);
|
||||
}
|
||||
|
||||
/// Updates a global value.
|
||||
pub fn update_global<G: Global, R>(&mut self, f: impl FnOnce(&mut G, &mut App) -> R) -> R {
|
||||
let mut lock = self.app.borrow_mut();
|
||||
lock.update(|cx| {
|
||||
let mut global = cx.lease_global::<G>();
|
||||
let result = f(&mut global, cx);
|
||||
cx.end_global_lease(global);
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
/// Simulates a sequence of keystrokes on the given window.
|
||||
///
|
||||
/// Keystrokes are specified as a space-separated string, e.g., "cmd-p escape".
|
||||
pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) {
|
||||
for keystroke_text in keystrokes.split_whitespace() {
|
||||
let keystroke = Keystroke::parse(keystroke_text)
|
||||
.unwrap_or_else(|_| panic!("Invalid keystroke: {}", keystroke_text));
|
||||
self.dispatch_keystroke(window, keystroke);
|
||||
}
|
||||
self.run_until_parked();
|
||||
}
|
||||
|
||||
/// Dispatches a single keystroke to a window.
|
||||
pub fn dispatch_keystroke(&mut self, window: AnyWindowHandle, keystroke: Keystroke) {
|
||||
self.update_window(window, |_, window, cx| {
|
||||
window.dispatch_keystroke(keystroke, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
/// Simulates typing text input on the given window.
|
||||
pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) {
|
||||
for char in input.chars() {
|
||||
let key = char.to_string();
|
||||
let keystroke = Keystroke {
|
||||
modifiers: Modifiers::default(),
|
||||
key: key.clone(),
|
||||
key_char: Some(key),
|
||||
};
|
||||
self.dispatch_keystroke(window, keystroke);
|
||||
}
|
||||
self.run_until_parked();
|
||||
}
|
||||
|
||||
/// Simulates a mouse move event.
|
||||
pub fn simulate_mouse_move(
|
||||
&mut self,
|
||||
window: AnyWindowHandle,
|
||||
position: Point<Pixels>,
|
||||
button: impl Into<Option<MouseButton>>,
|
||||
modifiers: Modifiers,
|
||||
) {
|
||||
self.simulate_event(
|
||||
window,
|
||||
MouseMoveEvent {
|
||||
position,
|
||||
modifiers,
|
||||
pressed_button: button.into(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Simulates a mouse down event.
|
||||
pub fn simulate_mouse_down(
|
||||
&mut self,
|
||||
window: AnyWindowHandle,
|
||||
position: Point<Pixels>,
|
||||
button: MouseButton,
|
||||
modifiers: Modifiers,
|
||||
) {
|
||||
self.simulate_event(
|
||||
window,
|
||||
MouseDownEvent {
|
||||
position,
|
||||
modifiers,
|
||||
button,
|
||||
click_count: 1,
|
||||
first_mouse: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Simulates a mouse up event.
|
||||
pub fn simulate_mouse_up(
|
||||
&mut self,
|
||||
window: AnyWindowHandle,
|
||||
position: Point<Pixels>,
|
||||
button: MouseButton,
|
||||
modifiers: Modifiers,
|
||||
) {
|
||||
self.simulate_event(
|
||||
window,
|
||||
MouseUpEvent {
|
||||
position,
|
||||
modifiers,
|
||||
button,
|
||||
click_count: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Simulates a click (mouse down followed by mouse up).
|
||||
pub fn simulate_click(
|
||||
&mut self,
|
||||
window: AnyWindowHandle,
|
||||
position: Point<Pixels>,
|
||||
modifiers: Modifiers,
|
||||
) {
|
||||
self.simulate_mouse_down(window, position, MouseButton::Left, modifiers);
|
||||
self.simulate_mouse_up(window, position, MouseButton::Left, modifiers);
|
||||
}
|
||||
|
||||
/// Simulates an input event on the given window.
|
||||
pub fn simulate_event<E: InputEvent>(&mut self, window: AnyWindowHandle, event: E) {
|
||||
self.update_window(window, |_, window, cx| {
|
||||
window.dispatch_event(event.to_platform_input(), cx);
|
||||
})
|
||||
.ok();
|
||||
self.run_until_parked();
|
||||
}
|
||||
|
||||
/// Dispatches an action to the given window.
|
||||
pub fn dispatch_action(&mut self, window: AnyWindowHandle, action: impl Action) {
|
||||
self.update_window(window, |_, window, cx| {
|
||||
window.dispatch_action(action.boxed_clone(), cx);
|
||||
})
|
||||
.ok();
|
||||
self.run_until_parked();
|
||||
}
|
||||
|
||||
/// Writes to the clipboard.
|
||||
pub fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
self.platform.write_to_clipboard(item);
|
||||
}
|
||||
|
||||
/// Reads from the clipboard.
|
||||
pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
self.platform.read_from_clipboard()
|
||||
}
|
||||
|
||||
/// Waits for a condition to become true, with a timeout.
|
||||
pub async fn wait_for<T: 'static>(
|
||||
&mut self,
|
||||
entity: &Entity<T>,
|
||||
predicate: impl Fn(&T) -> bool,
|
||||
timeout: Duration,
|
||||
) -> Result<()> {
|
||||
let start = web_time::Instant::now();
|
||||
loop {
|
||||
{
|
||||
let app = self.app.borrow();
|
||||
if predicate(entity.read(&app)) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
if start.elapsed() > timeout {
|
||||
return Err(anyhow!("Timed out waiting for condition"));
|
||||
}
|
||||
|
||||
self.run_until_parked();
|
||||
self.background_executor
|
||||
.timer(Duration::from_millis(10))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Captures a screenshot of the specified window using direct texture capture.
|
||||
///
|
||||
/// This renders the scene to a Metal texture and reads the pixels directly,
|
||||
/// which does not require the window to be visible on screen.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn capture_screenshot(&mut self, window: AnyWindowHandle) -> Result<RgbaImage> {
|
||||
self.update_window(window, |_, window, _cx| window.render_to_image())?
|
||||
}
|
||||
|
||||
/// Waits for animations to complete by waiting a couple of frames.
|
||||
pub async fn wait_for_animations(&self) {
|
||||
self.background_executor
|
||||
.timer(Duration::from_millis(32))
|
||||
.await;
|
||||
self.run_until_parked();
|
||||
}
|
||||
}
|
||||
|
||||
impl AppContext for VisualTestAppContext {
|
||||
fn new<T: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T> {
|
||||
let mut app = self.app.borrow_mut();
|
||||
app.new(build_entity)
|
||||
}
|
||||
|
||||
fn reserve_entity<T: 'static>(&mut self) -> crate::Reservation<T> {
|
||||
let mut app = self.app.borrow_mut();
|
||||
app.reserve_entity()
|
||||
}
|
||||
|
||||
fn insert_entity<T: 'static>(
|
||||
&mut self,
|
||||
reservation: crate::Reservation<T>,
|
||||
build_entity: impl FnOnce(&mut Context<T>) -> T,
|
||||
) -> Entity<T> {
|
||||
let mut app = self.app.borrow_mut();
|
||||
app.insert_entity(reservation, build_entity)
|
||||
}
|
||||
|
||||
fn update_entity<T: 'static, R>(
|
||||
&mut self,
|
||||
handle: &Entity<T>,
|
||||
update: impl FnOnce(&mut T, &mut Context<T>) -> R,
|
||||
) -> R {
|
||||
let mut app = self.app.borrow_mut();
|
||||
app.update_entity(handle, update)
|
||||
}
|
||||
|
||||
fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> crate::GpuiBorrow<'a, T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
panic!("Cannot use as_mut with a visual test app context. Try calling update() first")
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(&self, handle: &Entity<T>, read: impl FnOnce(&T, &App) -> R) -> R
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
let app = self.app.borrow();
|
||||
app.read_entity(handle, read)
|
||||
}
|
||||
|
||||
fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
|
||||
where
|
||||
F: FnOnce(AnyView, &mut Window, &mut App) -> T,
|
||||
{
|
||||
let mut lock = self.app.borrow_mut();
|
||||
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>,
|
||||
read: impl FnOnce(Entity<T>, &App) -> R,
|
||||
) -> Result<R>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
let app = self.app.borrow();
|
||||
app.read_window(window, read)
|
||||
}
|
||||
|
||||
fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
|
||||
where
|
||||
R: Send + 'static,
|
||||
{
|
||||
self.background_executor.spawn(future)
|
||||
}
|
||||
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
|
||||
where
|
||||
G: Global,
|
||||
{
|
||||
let app = self.app.borrow();
|
||||
callback(app.global::<G>(), &app)
|
||||
}
|
||||
}
|
||||
Vendored
+401
@@ -0,0 +1,401 @@
|
||||
use std::{
|
||||
alloc::{self, handle_alloc_error},
|
||||
cell::Cell,
|
||||
num::NonZeroUsize,
|
||||
ops::{Deref, DerefMut},
|
||||
ptr::{self, NonNull},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
struct ArenaElement {
|
||||
value: *mut u8,
|
||||
drop: unsafe fn(*mut u8),
|
||||
}
|
||||
|
||||
impl Drop for ArenaElement {
|
||||
#[inline(always)]
|
||||
fn drop(&mut self) {
|
||||
unsafe { (self.drop)(self.value) };
|
||||
}
|
||||
}
|
||||
|
||||
struct Chunk {
|
||||
start: *mut u8,
|
||||
end: *mut u8,
|
||||
offset: *mut u8,
|
||||
}
|
||||
|
||||
impl Drop for Chunk {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
let chunk_size = self.end.offset_from_unsigned(self.start);
|
||||
// SAFETY: This succeeded during allocation.
|
||||
let layout = alloc::Layout::from_size_align_unchecked(chunk_size, 1);
|
||||
alloc::dealloc(self.start, layout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Chunk {
|
||||
fn new(chunk_size: NonZeroUsize) -> Self {
|
||||
// this only fails if chunk_size is unreasonably huge
|
||||
let layout = alloc::Layout::from_size_align(chunk_size.get(), 1).unwrap();
|
||||
let start = unsafe { alloc::alloc(layout) };
|
||||
if start.is_null() {
|
||||
handle_alloc_error(layout);
|
||||
}
|
||||
let end = unsafe { start.add(chunk_size.get()) };
|
||||
Self {
|
||||
start,
|
||||
end,
|
||||
offset: start,
|
||||
}
|
||||
}
|
||||
|
||||
fn allocate(&mut self, layout: alloc::Layout) -> Option<NonNull<u8>> {
|
||||
// Compute the allocation bounds in integer address space so that the
|
||||
// bounds check happens before any pointer offsetting. Offsetting a
|
||||
// pointer past the end of its allocation is undefined behavior even if
|
||||
// the result is never dereferenced (as happens on the chunk-spill
|
||||
// path), so `ptr::add` cannot be used until we know the result stays
|
||||
// in bounds. `checked_add` also handles the documented case where
|
||||
// `align_offset` returns `usize::MAX`.
|
||||
let base = self.offset.addr();
|
||||
let aligned_addr = base.checked_add(self.offset.align_offset(layout.align()))?;
|
||||
let next_addr = aligned_addr.checked_add(layout.size())?;
|
||||
|
||||
if next_addr <= self.end.addr() {
|
||||
let aligned = self.offset.with_addr(aligned_addr);
|
||||
self.offset = self.offset.with_addr(next_addr);
|
||||
NonNull::new(aligned)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.offset = self.start;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Arena {
|
||||
chunks: Vec<Chunk>,
|
||||
elements: Vec<ArenaElement>,
|
||||
valid: Rc<Cell<bool>>,
|
||||
current_chunk_index: usize,
|
||||
chunk_size: NonZeroUsize,
|
||||
scope_depth: usize,
|
||||
}
|
||||
|
||||
impl Drop for Arena {
|
||||
fn drop(&mut self) {
|
||||
self.force_clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl Arena {
|
||||
pub fn new(chunk_size: usize) -> Self {
|
||||
let chunk_size = NonZeroUsize::try_from(chunk_size).unwrap();
|
||||
Self {
|
||||
chunks: vec![Chunk::new(chunk_size)],
|
||||
elements: Vec::new(),
|
||||
valid: Rc::new(Cell::new(true)),
|
||||
current_chunk_index: 0,
|
||||
chunk_size,
|
||||
scope_depth: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn capacity(&self) -> usize {
|
||||
self.chunks.len() * self.chunk_size.get()
|
||||
}
|
||||
|
||||
/// Marks the start of a scope (e.g. a window draw) whose allocations must stay
|
||||
/// live until the scope ends, even if `clear` is called by a nested scope in
|
||||
/// the meantime.
|
||||
pub fn begin_scope(&mut self) {
|
||||
self.scope_depth += 1;
|
||||
}
|
||||
|
||||
/// Ends the innermost scope started with `begin_scope`.
|
||||
///
|
||||
/// Panics if no scope is active: an unbalanced `end_scope` would let `clear`
|
||||
/// run while an enclosing scope still references arena memory, which is
|
||||
/// exactly the use-after-free this bookkeeping exists to prevent, so failing
|
||||
/// loudly here is preferable.
|
||||
pub fn end_scope(&mut self) {
|
||||
self.scope_depth = self
|
||||
.scope_depth
|
||||
.checked_sub(1)
|
||||
.expect("Arena::end_scope called without a matching begin_scope");
|
||||
}
|
||||
|
||||
/// Drops all allocations and resets the arena, unless a scope is still active.
|
||||
///
|
||||
/// When a draw triggers a nested draw (e.g. re-entrant window procedure
|
||||
/// invocations on Windows, or opening a window from within a draw), the nested
|
||||
/// draw's clear must not free memory the outer draw still references, so it is
|
||||
/// deferred: the outer draw's own clear will drop both draws' allocations.
|
||||
pub fn clear(&mut self) {
|
||||
if self.scope_depth == 0 {
|
||||
self.force_clear();
|
||||
} else {
|
||||
log::debug!(
|
||||
"deferring arena clear; {} enclosing scope(s) still active",
|
||||
self.scope_depth
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn force_clear(&mut self) {
|
||||
self.valid.set(false);
|
||||
self.valid = Rc::new(Cell::new(true));
|
||||
self.elements.clear();
|
||||
for chunk_index in 0..=self.current_chunk_index {
|
||||
self.chunks[chunk_index].reset();
|
||||
}
|
||||
self.current_chunk_index = 0;
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn alloc<T>(&mut self, f: impl FnOnce() -> T) -> ArenaBox<T> {
|
||||
#[inline(always)]
|
||||
unsafe fn inner_writer<T, F>(ptr: *mut T, f: F)
|
||||
where
|
||||
F: FnOnce() -> T,
|
||||
{
|
||||
unsafe { ptr::write(ptr, f()) };
|
||||
}
|
||||
|
||||
unsafe fn drop<T>(ptr: *mut u8) {
|
||||
unsafe { std::ptr::drop_in_place(ptr.cast::<T>()) };
|
||||
}
|
||||
|
||||
let layout = alloc::Layout::new::<T>();
|
||||
let current_chunk = &mut self.chunks[self.current_chunk_index];
|
||||
let ptr = if let Some(ptr) = current_chunk.allocate(layout) {
|
||||
ptr.as_ptr()
|
||||
} else {
|
||||
self.allocate_next_chunk(layout)
|
||||
};
|
||||
|
||||
unsafe { inner_writer(ptr.cast(), f) };
|
||||
self.elements.push(ArenaElement {
|
||||
value: ptr,
|
||||
drop: drop::<T>,
|
||||
});
|
||||
|
||||
ArenaBox {
|
||||
ptr: ptr.cast(),
|
||||
valid: self.valid.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
fn allocate_next_chunk(&mut self, layout: alloc::Layout) -> *mut u8 {
|
||||
self.current_chunk_index += 1;
|
||||
if self.current_chunk_index >= self.chunks.len() {
|
||||
self.chunks.push(Chunk::new(self.chunk_size));
|
||||
assert_eq!(self.current_chunk_index, self.chunks.len() - 1);
|
||||
log::trace!(
|
||||
"increased element arena capacity to {}kb",
|
||||
self.capacity() / 1024,
|
||||
);
|
||||
}
|
||||
let current_chunk = &mut self.chunks[self.current_chunk_index];
|
||||
if let Some(pointer) = current_chunk.allocate(layout) {
|
||||
pointer.as_ptr()
|
||||
} else {
|
||||
panic!(
|
||||
"Arena chunk_size of {} is too small to allocate {} bytes",
|
||||
self.chunk_size,
|
||||
layout.size()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ArenaBox<T: ?Sized> {
|
||||
ptr: *mut T,
|
||||
valid: Rc<Cell<bool>>,
|
||||
}
|
||||
|
||||
impl<T: ?Sized> ArenaBox<T> {
|
||||
#[inline(always)]
|
||||
pub fn map<U: ?Sized>(mut self, f: impl FnOnce(&mut T) -> &mut U) -> ArenaBox<U> {
|
||||
ArenaBox {
|
||||
ptr: f(&mut self),
|
||||
valid: self.valid,
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn validate(&self) {
|
||||
assert!(
|
||||
self.valid.get(),
|
||||
"attempted to dereference an ArenaRef after its Arena was cleared"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> Deref for ArenaBox<T> {
|
||||
type Target = T;
|
||||
|
||||
#[inline(always)]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.validate();
|
||||
unsafe { &*self.ptr }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> DerefMut for ArenaBox<T> {
|
||||
#[inline(always)]
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.validate();
|
||||
unsafe { &mut *self.ptr }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_arena() {
|
||||
let mut arena = Arena::new(1024);
|
||||
let a = arena.alloc(|| 1u64);
|
||||
let b = arena.alloc(|| 2u32);
|
||||
let c = arena.alloc(|| 3u16);
|
||||
let d = arena.alloc(|| 4u8);
|
||||
assert_eq!(*a, 1);
|
||||
assert_eq!(*b, 2);
|
||||
assert_eq!(*c, 3);
|
||||
assert_eq!(*d, 4);
|
||||
|
||||
arena.clear();
|
||||
let a = arena.alloc(|| 5u64);
|
||||
let b = arena.alloc(|| 6u32);
|
||||
let c = arena.alloc(|| 7u16);
|
||||
let d = arena.alloc(|| 8u8);
|
||||
assert_eq!(*a, 5);
|
||||
assert_eq!(*b, 6);
|
||||
assert_eq!(*c, 7);
|
||||
assert_eq!(*d, 8);
|
||||
|
||||
// Ensure drop gets called.
|
||||
let dropped = Rc::new(Cell::new(false));
|
||||
struct DropGuard(Rc<Cell<bool>>);
|
||||
impl Drop for DropGuard {
|
||||
fn drop(&mut self) {
|
||||
self.0.set(true);
|
||||
}
|
||||
}
|
||||
arena.alloc(|| DropGuard(dropped.clone()));
|
||||
arena.clear();
|
||||
assert!(dropped.get());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arena_grow() {
|
||||
let mut arena = Arena::new(8);
|
||||
arena.alloc(|| 1u64);
|
||||
arena.alloc(|| 2u64);
|
||||
|
||||
assert_eq!(arena.capacity(), 16);
|
||||
|
||||
arena.alloc(|| 3u32);
|
||||
arena.alloc(|| 4u32);
|
||||
|
||||
assert_eq!(arena.capacity(), 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arena_alignment() {
|
||||
let mut arena = Arena::new(256);
|
||||
let x1 = arena.alloc(|| 1u8);
|
||||
let x2 = arena.alloc(|| 2u16);
|
||||
let x3 = arena.alloc(|| 3u32);
|
||||
let x4 = arena.alloc(|| 4u64);
|
||||
let x5 = arena.alloc(|| 5u64);
|
||||
|
||||
assert_eq!(*x1, 1);
|
||||
assert_eq!(*x2, 2);
|
||||
assert_eq!(*x3, 3);
|
||||
assert_eq!(*x4, 4);
|
||||
assert_eq!(*x5, 5);
|
||||
|
||||
assert_eq!(x1.ptr.align_offset(std::mem::align_of_val(&*x1)), 0);
|
||||
assert_eq!(x2.ptr.align_offset(std::mem::align_of_val(&*x2)), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "attempted to dereference an ArenaRef after its Arena was cleared")]
|
||||
fn test_arena_use_after_clear() {
|
||||
let mut arena = Arena::new(16);
|
||||
let value = arena.alloc(|| 1u64);
|
||||
|
||||
arena.clear();
|
||||
let _read_value = *value;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clear_deferred_while_scope_active() {
|
||||
struct DropCounter(Rc<Cell<usize>>);
|
||||
impl Drop for DropCounter {
|
||||
fn drop(&mut self) {
|
||||
self.0.set(self.0.get() + 1);
|
||||
}
|
||||
}
|
||||
|
||||
let drops = Rc::new(Cell::new(0));
|
||||
let mut arena = Arena::new(1024);
|
||||
|
||||
// Outer draw starts and allocates.
|
||||
arena.begin_scope();
|
||||
let outer = arena.alloc(|| 42u64);
|
||||
arena.alloc({
|
||||
let drops = drops.clone();
|
||||
|| DropCounter(drops)
|
||||
});
|
||||
|
||||
// Nested draw runs to completion and requests a clear.
|
||||
arena.begin_scope();
|
||||
let inner = arena.alloc(|| 7u64);
|
||||
arena.alloc({
|
||||
let drops = drops.clone();
|
||||
|| DropCounter(drops)
|
||||
});
|
||||
arena.end_scope();
|
||||
arena.clear();
|
||||
|
||||
// The clear must be deferred: the outer draw's allocations are still live.
|
||||
assert_eq!(*outer, 42);
|
||||
assert_eq!(*inner, 7);
|
||||
assert_eq!(drops.get(), 0);
|
||||
|
||||
// Once the outer draw finishes, its clear drops both draws' allocations.
|
||||
arena.end_scope();
|
||||
arena.clear();
|
||||
assert_eq!(drops.get(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clear_without_scope_is_immediate() {
|
||||
let mut arena = Arena::new(1024);
|
||||
let value = arena.alloc(|| 1u64);
|
||||
assert_eq!(*value, 1);
|
||||
arena.clear();
|
||||
assert!(!value.valid.get());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Arena::end_scope called without a matching begin_scope")]
|
||||
fn test_unbalanced_end_scope_panics() {
|
||||
let mut arena = Arena::new(1024);
|
||||
arena.begin_scope();
|
||||
arena.end_scope();
|
||||
arena.end_scope();
|
||||
}
|
||||
}
|
||||
Vendored
+358
@@ -0,0 +1,358 @@
|
||||
use crate::{App, EntityId, SharedString, SharedUri, Task};
|
||||
use collections::FxHashSet;
|
||||
use futures::{Future, TryFutureExt};
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::fmt::Debug;
|
||||
use std::hash::{BuildHasher, Hash};
|
||||
use std::marker::PhantomData;
|
||||
use std::mem;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(crate) struct CachedLoad<T> {
|
||||
state: Rc<RefCell<CachedLoadState<T>>>,
|
||||
// Keep the owner outside the state so publishing a result cannot cancel its own task.
|
||||
_task: Task<()>,
|
||||
}
|
||||
|
||||
enum CachedLoadState<T> {
|
||||
Loading(FxHashSet<EntityId>),
|
||||
Loaded(T),
|
||||
}
|
||||
|
||||
impl<T: Clone + Send + 'static> CachedLoad<T> {
|
||||
pub(crate) fn new(future: impl Future<Output = T> + Send + 'static, cx: &App) -> Self {
|
||||
let state = Rc::new(RefCell::new(CachedLoadState::Loading(FxHashSet::default())));
|
||||
let task = cx.background_executor().spawn(future);
|
||||
let task = cx.spawn({
|
||||
let state = Rc::downgrade(&state);
|
||||
async move |cx| {
|
||||
let result = task.await;
|
||||
let Some(state) = state.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let previous =
|
||||
mem::replace(&mut *state.borrow_mut(), CachedLoadState::Loaded(result));
|
||||
let CachedLoadState::Loading(views) = previous else {
|
||||
unreachable!("a cached load completes only once");
|
||||
};
|
||||
cx.update(|cx| {
|
||||
for view in views {
|
||||
cx.notify(view);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
Self { state, _task: task }
|
||||
}
|
||||
|
||||
pub(crate) fn get(&self) -> Option<T> {
|
||||
match &*self.state.borrow() {
|
||||
CachedLoadState::Loading(_) => None,
|
||||
CachedLoadState::Loaded(result) => Some(result.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn use_by(&self, view: EntityId) -> Option<T> {
|
||||
match &mut *self.state.borrow_mut() {
|
||||
CachedLoadState::Loading(views) => {
|
||||
views.insert(view);
|
||||
None
|
||||
}
|
||||
CachedLoadState::Loaded(result) => Some(result.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An enum representing
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
|
||||
pub enum Resource {
|
||||
/// This resource is at a given URI
|
||||
Uri(SharedUri),
|
||||
/// This resource is at a given path in the file system
|
||||
Path(Arc<Path>),
|
||||
/// This resource is embedded in the application binary
|
||||
Embedded(SharedString),
|
||||
}
|
||||
|
||||
impl From<SharedUri> for Resource {
|
||||
fn from(value: SharedUri) -> Self {
|
||||
Self::Uri(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PathBuf> for Resource {
|
||||
fn from(value: PathBuf) -> Self {
|
||||
Self::Path(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Arc<Path>> for Resource {
|
||||
fn from(value: Arc<Path>) -> Self {
|
||||
Self::Path(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for asynchronous asset loading.
|
||||
pub trait Asset: 'static {
|
||||
/// The source of the asset.
|
||||
type Source: Clone + Hash + Send;
|
||||
|
||||
/// The loaded asset
|
||||
type Output: Clone + Send;
|
||||
|
||||
/// Load the asset asynchronously
|
||||
fn load(
|
||||
source: Self::Source,
|
||||
cx: &mut App,
|
||||
) -> impl Future<Output = Self::Output> + Send + 'static;
|
||||
}
|
||||
|
||||
/// An asset Loader which logs the [`Err`] variant of a [`Result`] during loading
|
||||
pub enum AssetLogger<T> {
|
||||
#[doc(hidden)]
|
||||
_Phantom(PhantomData<T>, &'static dyn crate::seal::Sealed),
|
||||
}
|
||||
|
||||
impl<T, R, E> Asset for AssetLogger<T>
|
||||
where
|
||||
T: Asset<Output = Result<R, E>>,
|
||||
R: Clone + Send,
|
||||
E: Clone + Send + Debug,
|
||||
{
|
||||
type Source = T::Source;
|
||||
|
||||
type Output = T::Output;
|
||||
|
||||
fn load(
|
||||
source: Self::Source,
|
||||
cx: &mut App,
|
||||
) -> impl Future<Output = Self::Output> + Send + 'static {
|
||||
let load = T::load(source, cx);
|
||||
load.inspect_err(|e| log::error!("Failed to load asset: {:?}", e))
|
||||
}
|
||||
}
|
||||
|
||||
/// Use a quick, non-cryptographically secure hash function to get an identifier from data
|
||||
pub fn hash<T: Hash>(data: &T) -> u64 {
|
||||
collections::FxBuildHasher.hash_one(data)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{AppContext, TestAppContext};
|
||||
use futures::channel::oneshot;
|
||||
use std::{
|
||||
cell::Cell,
|
||||
collections::VecDeque,
|
||||
hash::{Hash, Hasher},
|
||||
rc::Rc,
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
fn cached_load_caches_success_and_error(cx: &mut TestAppContext) {
|
||||
let successful_load =
|
||||
cx.update(|cx| CachedLoad::new(async { Ok::<_, &'static str>(42) }, cx));
|
||||
let failed_load =
|
||||
cx.update(|cx| CachedLoad::new(async { Err::<i32, _>("load failed") }, cx));
|
||||
|
||||
assert_eq!(successful_load.get(), None);
|
||||
assert_eq!(failed_load.get(), None);
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
assert_eq!(successful_load.get(), Some(Ok(42)));
|
||||
assert_eq!(successful_load.get(), Some(Ok(42)));
|
||||
assert_eq!(failed_load.get(), Some(Err("load failed")));
|
||||
assert_eq!(failed_load.get(), Some(Err("load failed")));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn completed_load_without_subscribers_is_cached_and_deduplicated(cx: &mut TestAppContext) {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
let source = TestAssetSource::new(1, [receiver]);
|
||||
|
||||
assert_eq!(cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)), None);
|
||||
assert_eq!(cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)), None);
|
||||
assert_eq!(source.load_count(), 1);
|
||||
|
||||
assert!(sender.send(Ok(42)).is_ok());
|
||||
cx.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)),
|
||||
Some(Ok(42))
|
||||
);
|
||||
assert_eq!(
|
||||
cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)),
|
||||
Some(Ok(42))
|
||||
);
|
||||
assert_eq!(source.load_count(), 1);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn load_completion_notifies_each_observing_entity_once(cx: &mut TestAppContext) {
|
||||
let (sender, receiver) = oneshot::channel::<i32>();
|
||||
let load = cx.update(|cx| {
|
||||
CachedLoad::new(
|
||||
async move { receiver.await.expect("test sends a result") },
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let first_notification_count = Rc::new(Cell::new(0));
|
||||
let second_notification_count = Rc::new(Cell::new(0));
|
||||
let (first_entity, second_entity) = cx.update(|cx| {
|
||||
let first_entity = cx.new(|_| ());
|
||||
let second_entity = cx.new(|_| ());
|
||||
cx.observe(&first_entity, {
|
||||
let first_notification_count = first_notification_count.clone();
|
||||
move |_, _| first_notification_count.set(first_notification_count.get() + 1)
|
||||
})
|
||||
.detach();
|
||||
cx.observe(&second_entity, {
|
||||
let second_notification_count = second_notification_count.clone();
|
||||
move |_, _| second_notification_count.set(second_notification_count.get() + 1)
|
||||
})
|
||||
.detach();
|
||||
(first_entity, second_entity)
|
||||
});
|
||||
|
||||
assert_eq!(load.use_by(first_entity.entity_id()), None);
|
||||
assert_eq!(load.use_by(first_entity.entity_id()), None);
|
||||
assert_eq!(load.use_by(second_entity.entity_id()), None);
|
||||
|
||||
assert!(sender.send(42).is_ok());
|
||||
cx.run_until_parked();
|
||||
|
||||
assert_eq!(first_notification_count.get(), 1);
|
||||
assert_eq!(second_notification_count.get(), 1);
|
||||
assert_eq!(load.use_by(first_entity.entity_id()), Some(42));
|
||||
assert_eq!(load.use_by(second_entity.entity_id()), Some(42));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn removing_pending_asset_cancels_it_and_replacement_ignores_stale_completion(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
let (stale_sender, stale_receiver) = oneshot::channel();
|
||||
let (replacement_sender, replacement_receiver) = oneshot::channel();
|
||||
let source = TestAssetSource::new(2, [stale_receiver, replacement_receiver]);
|
||||
|
||||
assert_eq!(cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)), None);
|
||||
assert_eq!(source.load_count(), 1);
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.update(|cx| cx.remove_asset::<TestAsset>(&source));
|
||||
assert!(!cx.update(|cx| cx.has_asset::<TestAsset>(&source)));
|
||||
cx.run_until_parked();
|
||||
assert!(stale_sender.send(Ok(1)).is_err());
|
||||
|
||||
assert_eq!(cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)), None);
|
||||
assert_eq!(source.load_count(), 2);
|
||||
|
||||
assert!(replacement_sender.send(Err("replacement failed")).is_ok());
|
||||
cx.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)),
|
||||
Some(Err("replacement failed"))
|
||||
);
|
||||
assert_eq!(source.load_count(), 2);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn eviction_after_worker_completion_does_not_publish_into_replacement(cx: &mut TestAppContext) {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
let (replacement_sender, replacement_receiver) = oneshot::channel();
|
||||
let source = TestAssetSource::new(3, [receiver, replacement_receiver]);
|
||||
|
||||
assert_eq!(cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)), None);
|
||||
assert!(sender.send(Ok(1)).is_ok());
|
||||
while source.completion_count.load(Ordering::SeqCst) == 0 {
|
||||
assert!(cx.background_executor.tick());
|
||||
}
|
||||
assert_eq!(cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)), None);
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.remove_asset::<TestAsset>(&source);
|
||||
assert_eq!(cx.fetch_asset::<TestAsset>(&source), None);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)), None);
|
||||
|
||||
assert!(replacement_sender.send(Ok(2)).is_ok());
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)),
|
||||
Some(Ok(2))
|
||||
);
|
||||
assert_eq!(source.load_count(), 2);
|
||||
}
|
||||
|
||||
struct TestAsset;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TestAssetSource {
|
||||
id: usize,
|
||||
load_count: Arc<AtomicUsize>,
|
||||
completion_count: Arc<AtomicUsize>,
|
||||
receivers: Arc<Mutex<VecDeque<oneshot::Receiver<Result<i32, &'static str>>>>>,
|
||||
}
|
||||
|
||||
impl TestAssetSource {
|
||||
fn new(
|
||||
id: usize,
|
||||
receivers: impl IntoIterator<Item = oneshot::Receiver<Result<i32, &'static str>>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
load_count: Arc::new(AtomicUsize::new(0)),
|
||||
completion_count: Arc::new(AtomicUsize::new(0)),
|
||||
receivers: Arc::new(Mutex::new(receivers.into_iter().collect())),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_count(&self) -> usize {
|
||||
self.load_count.load(Ordering::SeqCst)
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for TestAssetSource {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.id.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Asset for TestAsset {
|
||||
type Source = TestAssetSource;
|
||||
type Output = Result<i32, &'static str>;
|
||||
|
||||
fn load(
|
||||
source: Self::Source,
|
||||
_cx: &mut App,
|
||||
) -> impl Future<Output = Self::Output> + Send + 'static {
|
||||
source.load_count.fetch_add(1, Ordering::SeqCst);
|
||||
let receiver = source
|
||||
.receivers
|
||||
.lock()
|
||||
.expect("test receiver mutex should not be poisoned")
|
||||
.pop_front()
|
||||
.expect("each test load should have a receiver");
|
||||
async move {
|
||||
let result = match receiver.await {
|
||||
Ok(result) => result,
|
||||
Err(_) => Err("cancelled"),
|
||||
};
|
||||
source.completion_count.fetch_add(1, Ordering::SeqCst);
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+133
@@ -0,0 +1,133 @@
|
||||
use crate::{DevicePixels, Pixels, Result, SharedString, Size, size};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use image::{Delay, Frame};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt,
|
||||
hash::Hash,
|
||||
sync::atomic::{AtomicUsize, Ordering::SeqCst},
|
||||
};
|
||||
|
||||
/// A source of assets for this app to use.
|
||||
pub trait AssetSource: 'static + Send + Sync {
|
||||
/// Load the given asset from the source path.
|
||||
fn load(&self, path: &str) -> Result<Option<Cow<'static, [u8]>>>;
|
||||
|
||||
/// List the assets at the given path.
|
||||
fn list(&self, path: &str) -> Result<Vec<SharedString>>;
|
||||
}
|
||||
|
||||
impl AssetSource for () {
|
||||
fn load(&self, _path: &str) -> Result<Option<Cow<'static, [u8]>>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn list(&self, _path: &str) -> Result<Vec<SharedString>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
/// A unique identifier for the image cache
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
pub struct ImageId(pub usize);
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone)]
|
||||
#[expect(missing_docs)]
|
||||
pub struct RenderImageParams {
|
||||
pub image_id: ImageId,
|
||||
pub frame_index: usize,
|
||||
}
|
||||
|
||||
/// A cached and processed image, in BGRA format
|
||||
pub struct RenderImage {
|
||||
/// The ID associated with this image
|
||||
pub id: ImageId,
|
||||
/// The scale factor of this image on render.
|
||||
pub(crate) scale_factor: f32,
|
||||
data: SmallVec<[Frame; 1]>,
|
||||
}
|
||||
|
||||
impl PartialEq for RenderImage {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for RenderImage {}
|
||||
|
||||
impl RenderImage {
|
||||
/// Create a new image from the given data.
|
||||
pub fn new(data: impl Into<SmallVec<[Frame; 1]>>) -> Self {
|
||||
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
Self {
|
||||
id: ImageId(NEXT_ID.fetch_add(1, SeqCst)),
|
||||
scale_factor: 1.0,
|
||||
data: data.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert this image into a byte slice.
|
||||
pub fn as_bytes(&self, frame_index: usize) -> Option<&[u8]> {
|
||||
self.data
|
||||
.get(frame_index)
|
||||
.map(|frame| frame.buffer().as_raw().as_slice())
|
||||
}
|
||||
|
||||
/// Get the size of this image, in pixels.
|
||||
pub fn size(&self, frame_index: usize) -> Size<DevicePixels> {
|
||||
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.
|
||||
pub(crate) fn render_size(&self, frame_index: usize) -> Size<Pixels> {
|
||||
self.size(frame_index)
|
||||
.map(|v| (v.0 as f32 / self.scale_factor).into())
|
||||
}
|
||||
|
||||
/// Get the delay of this frame from the previous
|
||||
pub fn delay(&self, frame_index: usize) -> Delay {
|
||||
self.data
|
||||
.get(frame_index)
|
||||
.map(|frame| frame.delay())
|
||||
.unwrap_or(Delay::from_numer_denom_ms(100, 1))
|
||||
}
|
||||
|
||||
/// Get the number of frames for this image.
|
||||
pub fn frame_count(&self) -> usize {
|
||||
self.data.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for RenderImage {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("ImageData")
|
||||
.field("id", &self.id)
|
||||
.field("size", &self.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:?}");
|
||||
}
|
||||
}
|
||||
Vendored
+472
@@ -0,0 +1,472 @@
|
||||
use crate::{Bounds, Half};
|
||||
use std::{
|
||||
cmp,
|
||||
fmt::Debug,
|
||||
ops::{Add, Sub},
|
||||
ptr::NonNull,
|
||||
};
|
||||
|
||||
/// Maximum children per internal node (R-tree style branching factor).
|
||||
/// Higher values = shorter tree = fewer cache misses, but more work per node.
|
||||
const MAX_CHILDREN: usize = 12;
|
||||
|
||||
/// A spatial tree optimized for finding maximum ordering among intersecting bounds.
|
||||
///
|
||||
/// This is an R-tree variant specifically designed for the use case of assigning
|
||||
/// z-order to overlapping UI elements. Key optimizations:
|
||||
/// - Tracks the leaf with global max ordering for O(1) fast-path queries
|
||||
/// - Uses higher branching factor (4) for lower tree height
|
||||
/// - Aggressive pruning during search based on max_order metadata
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct BoundsTree<U>
|
||||
where
|
||||
U: Clone + Debug + Default + PartialEq,
|
||||
{
|
||||
/// All nodes stored contiguously for cache efficiency.
|
||||
nodes: Vec<Node<U>>,
|
||||
/// Index of the root node, if any.
|
||||
root: Option<usize>,
|
||||
/// Index of the leaf with the highest ordering (for fast-path lookups).
|
||||
max_leaf: Option<usize>,
|
||||
/// Reusable stack for tree traversal during insertion.
|
||||
insert_path: Vec<usize>,
|
||||
/// Reusable stack for search operations.
|
||||
search_stack: Vec<NonNull<Node<U>>>,
|
||||
}
|
||||
|
||||
/// A node in the bounds tree.
|
||||
#[derive(Debug, Clone)]
|
||||
struct Node<U>
|
||||
where
|
||||
U: Clone + Debug + Default + PartialEq,
|
||||
{
|
||||
/// Bounding box containing this node and all descendants.
|
||||
bounds: Bounds<U>,
|
||||
/// Maximum ordering value in this subtree.
|
||||
max_order: u32,
|
||||
/// Node-specific data.
|
||||
kind: NodeKind,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum NodeKind {
|
||||
/// Leaf node containing actual bounds data.
|
||||
Leaf {
|
||||
/// The ordering assigned to this bounds.
|
||||
order: u32,
|
||||
},
|
||||
/// Internal node with children.
|
||||
Internal {
|
||||
/// Indices of child nodes (2 to MAX_CHILDREN).
|
||||
children: NodeChildren,
|
||||
},
|
||||
}
|
||||
|
||||
/// Fixed-size array for child indices, avoiding heap allocation.
|
||||
#[derive(Debug, Clone)]
|
||||
struct NodeChildren {
|
||||
// Keeps an invariant where the max order child is always at the end
|
||||
indices: [usize; MAX_CHILDREN],
|
||||
len: u8,
|
||||
}
|
||||
|
||||
impl NodeChildren {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
indices: [0; MAX_CHILDREN],
|
||||
len: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, index: usize) {
|
||||
debug_assert!((self.len as usize) < MAX_CHILDREN);
|
||||
self.indices[self.len as usize] = index;
|
||||
self.len += 1;
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.len as usize
|
||||
}
|
||||
|
||||
fn as_slice(&self) -> &[usize] {
|
||||
&self.indices[..self.len as usize]
|
||||
}
|
||||
}
|
||||
|
||||
impl<U> BoundsTree<U>
|
||||
where
|
||||
U: Clone
|
||||
+ Debug
|
||||
+ PartialEq
|
||||
+ PartialOrd
|
||||
+ Add<U, Output = U>
|
||||
+ Sub<Output = U>
|
||||
+ Half
|
||||
+ Default,
|
||||
{
|
||||
/// Clears all nodes from the tree.
|
||||
pub fn clear(&mut self) {
|
||||
self.nodes.clear();
|
||||
self.root = None;
|
||||
self.max_leaf = None;
|
||||
self.insert_path.clear();
|
||||
self.search_stack.clear();
|
||||
}
|
||||
|
||||
/// Inserts bounds into the tree and returns its assigned ordering.
|
||||
///
|
||||
/// The ordering is one greater than the maximum ordering of any
|
||||
/// existing bounds that intersect with the new bounds.
|
||||
pub fn insert(&mut self, new_bounds: Bounds<U>) -> u32 {
|
||||
// Find maximum ordering among intersecting bounds
|
||||
let max_intersecting = self.find_max_ordering(&new_bounds);
|
||||
let ordering = max_intersecting + 1;
|
||||
|
||||
// Insert the new leaf
|
||||
let new_leaf_idx = self.insert_leaf(new_bounds, ordering);
|
||||
|
||||
// Update max_leaf tracking
|
||||
self.max_leaf = match self.max_leaf {
|
||||
None => Some(new_leaf_idx),
|
||||
Some(old_idx) if self.nodes[old_idx].max_order < ordering => Some(new_leaf_idx),
|
||||
some => some,
|
||||
};
|
||||
|
||||
ordering
|
||||
}
|
||||
|
||||
/// Finds the maximum ordering among all bounds that intersect with the query.
|
||||
fn find_max_ordering(&mut self, query: &Bounds<U>) -> u32 {
|
||||
let Some(root_idx) = self.root else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Fast path: check if the max-ordering leaf intersects
|
||||
if let Some(max_idx) = self.max_leaf {
|
||||
let max_node = &self.nodes[max_idx];
|
||||
if query.intersects(&max_node.bounds) {
|
||||
return max_node.max_order;
|
||||
}
|
||||
}
|
||||
|
||||
// Slow path: search the tree
|
||||
self.search_stack.clear();
|
||||
self.search_stack.push(NonNull::from(&self.nodes[root_idx]));
|
||||
|
||||
let mut max_found = 0u32;
|
||||
|
||||
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 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Spatial pruning: skip if bounds don't intersect
|
||||
if !query.intersects(&node.bounds) {
|
||||
continue;
|
||||
}
|
||||
|
||||
match &node.kind {
|
||||
NodeKind::Leaf { order } => {
|
||||
max_found = cmp::max(max_found, *order);
|
||||
}
|
||||
NodeKind::Internal { children } => {
|
||||
// Children are maintained with highest max_order at the end.
|
||||
// Push in forward order to highest (last) is popped first.
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
max_found
|
||||
}
|
||||
|
||||
/// Inserts a leaf node with the given bounds and ordering.
|
||||
/// Returns the index of the new leaf.
|
||||
fn insert_leaf(&mut self, bounds: Bounds<U>, order: u32) -> usize {
|
||||
let new_leaf_idx = self.nodes.len();
|
||||
self.nodes.push(Node {
|
||||
bounds: bounds.clone(),
|
||||
max_order: order,
|
||||
kind: NodeKind::Leaf { order },
|
||||
});
|
||||
|
||||
let Some(root_idx) = self.root else {
|
||||
// Tree is empty, new leaf becomes root
|
||||
self.root = Some(new_leaf_idx);
|
||||
return new_leaf_idx;
|
||||
};
|
||||
|
||||
// If root is a leaf, create internal node with both
|
||||
if matches!(self.nodes[root_idx].kind, NodeKind::Leaf { .. }) {
|
||||
let root_bounds = self.nodes[root_idx].bounds.clone();
|
||||
let root_order = self.nodes[root_idx].max_order;
|
||||
|
||||
let mut children = NodeChildren::new();
|
||||
// Max end invariant
|
||||
if order > root_order {
|
||||
children.push(root_idx);
|
||||
children.push(new_leaf_idx);
|
||||
} else {
|
||||
children.push(new_leaf_idx);
|
||||
children.push(root_idx);
|
||||
}
|
||||
|
||||
let new_root_idx = self.nodes.len();
|
||||
self.nodes.push(Node {
|
||||
bounds: root_bounds.union(&bounds),
|
||||
max_order: cmp::max(root_order, order),
|
||||
kind: NodeKind::Internal { children },
|
||||
});
|
||||
self.root = Some(new_root_idx);
|
||||
return new_leaf_idx;
|
||||
}
|
||||
|
||||
// Descend to find the best internal node to insert into
|
||||
self.insert_path.clear();
|
||||
let mut current_idx = root_idx;
|
||||
|
||||
loop {
|
||||
let current = &self.nodes[current_idx];
|
||||
let NodeKind::Internal { children } = ¤t.kind else {
|
||||
unreachable!("Should only traverse internal nodes");
|
||||
};
|
||||
|
||||
self.insert_path.push(current_idx);
|
||||
|
||||
// Find the best child to descend into
|
||||
let mut best_child_idx = children.as_slice()[0];
|
||||
let mut best_child_pos = 0;
|
||||
let mut best_cost = bounds
|
||||
.union(&self.nodes[best_child_idx].bounds)
|
||||
.half_perimeter();
|
||||
|
||||
for (pos, &child_idx) in children.as_slice().iter().enumerate().skip(1) {
|
||||
let cost = bounds.union(&self.nodes[child_idx].bounds).half_perimeter();
|
||||
if cost < best_cost {
|
||||
best_cost = cost;
|
||||
best_child_idx = child_idx;
|
||||
best_child_pos = pos;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if best child is a leaf or internal
|
||||
if matches!(self.nodes[best_child_idx].kind, NodeKind::Leaf { .. }) {
|
||||
// Best child is a leaf. Check if current node has room for another child.
|
||||
if children.len() < MAX_CHILDREN {
|
||||
// Add new leaf directly to this node
|
||||
let node = &mut self.nodes[current_idx];
|
||||
|
||||
if let NodeKind::Internal { children } = &mut node.kind {
|
||||
children.push(new_leaf_idx);
|
||||
// Swap new leaf only if it has the highest max_order
|
||||
if order <= node.max_order {
|
||||
let last = children.len() - 1;
|
||||
children.indices.swap(last - 1, last);
|
||||
}
|
||||
}
|
||||
|
||||
node.bounds = node.bounds.union(&bounds);
|
||||
node.max_order = cmp::max(node.max_order, order);
|
||||
break;
|
||||
} else {
|
||||
// Node is full, create new internal with [best_leaf, new_leaf]
|
||||
let sibling_bounds = self.nodes[best_child_idx].bounds.clone();
|
||||
let sibling_order = self.nodes[best_child_idx].max_order;
|
||||
|
||||
let mut new_children = NodeChildren::new();
|
||||
// Max end invariant
|
||||
if order > sibling_order {
|
||||
new_children.push(best_child_idx);
|
||||
new_children.push(new_leaf_idx);
|
||||
} else {
|
||||
new_children.push(new_leaf_idx);
|
||||
new_children.push(best_child_idx);
|
||||
}
|
||||
|
||||
let new_internal_idx = self.nodes.len();
|
||||
let new_internal_max = cmp::max(sibling_order, order);
|
||||
self.nodes.push(Node {
|
||||
bounds: sibling_bounds.union(&bounds),
|
||||
max_order: new_internal_max,
|
||||
kind: NodeKind::Internal {
|
||||
children: new_children,
|
||||
},
|
||||
});
|
||||
|
||||
// Replace the leaf with the new internal in parent
|
||||
let parent = &mut self.nodes[current_idx];
|
||||
if let NodeKind::Internal { children } = &mut parent.kind {
|
||||
let children_len = children.len();
|
||||
|
||||
children.indices[best_child_pos] = new_internal_idx;
|
||||
|
||||
// If new internal has highest max_order, swap it to the end
|
||||
// to maintain sorting invariant
|
||||
if new_internal_max > parent.max_order {
|
||||
children.indices.swap(best_child_pos, children_len - 1);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Best child is internal, continue descent
|
||||
current_idx = best_child_idx;
|
||||
}
|
||||
}
|
||||
|
||||
// Propagate bounds and max_order updates up the tree
|
||||
let mut updated_child_idx = None;
|
||||
for &node_idx in self.insert_path.iter().rev() {
|
||||
let node = &mut self.nodes[node_idx];
|
||||
node.bounds = node.bounds.union(&bounds);
|
||||
|
||||
if node.max_order < order {
|
||||
node.max_order = order;
|
||||
|
||||
// Swap updated child to end (skip first iteration since the invariant is already handled by previous cases)
|
||||
if let Some(child_idx) = updated_child_idx {
|
||||
if let NodeKind::Internal { children } = &mut node.kind {
|
||||
if let Some(pos) = children.as_slice().iter().position(|&c| c == child_idx)
|
||||
{
|
||||
let last = children.len() - 1;
|
||||
if pos != last {
|
||||
children.indices.swap(pos, last);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updated_child_idx = Some(node_idx);
|
||||
}
|
||||
|
||||
new_leaf_idx
|
||||
}
|
||||
}
|
||||
|
||||
impl<U> Default for BoundsTree<U>
|
||||
where
|
||||
U: Clone + Debug + Default + PartialEq,
|
||||
{
|
||||
fn default() -> Self {
|
||||
BoundsTree {
|
||||
nodes: Vec::new(),
|
||||
root: None,
|
||||
max_leaf: None,
|
||||
insert_path: Vec::new(),
|
||||
search_stack: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{Bounds, Point, Size};
|
||||
use rand::{Rng, SeedableRng};
|
||||
|
||||
#[test]
|
||||
fn test_insert() {
|
||||
let mut tree = BoundsTree::<f32>::default();
|
||||
let bounds1 = Bounds {
|
||||
origin: Point { x: 0.0, y: 0.0 },
|
||||
size: Size {
|
||||
width: 10.0,
|
||||
height: 10.0,
|
||||
},
|
||||
};
|
||||
let bounds2 = Bounds {
|
||||
origin: Point { x: 5.0, y: 5.0 },
|
||||
size: Size {
|
||||
width: 10.0,
|
||||
height: 10.0,
|
||||
},
|
||||
};
|
||||
let bounds3 = Bounds {
|
||||
origin: Point { x: 10.0, y: 10.0 },
|
||||
size: Size {
|
||||
width: 10.0,
|
||||
height: 10.0,
|
||||
},
|
||||
};
|
||||
|
||||
// Insert the bounds into the tree and verify the order is correct
|
||||
assert_eq!(tree.insert(bounds1), 1);
|
||||
assert_eq!(tree.insert(bounds2), 2);
|
||||
assert_eq!(tree.insert(bounds3), 3);
|
||||
|
||||
// Insert non-overlapping bounds and verify they can reuse orders
|
||||
let bounds4 = Bounds {
|
||||
origin: Point { x: 20.0, y: 20.0 },
|
||||
size: Size {
|
||||
width: 10.0,
|
||||
height: 10.0,
|
||||
},
|
||||
};
|
||||
let bounds5 = Bounds {
|
||||
origin: Point { x: 40.0, y: 40.0 },
|
||||
size: Size {
|
||||
width: 10.0,
|
||||
height: 10.0,
|
||||
},
|
||||
};
|
||||
let bounds6 = Bounds {
|
||||
origin: Point { x: 25.0, y: 25.0 },
|
||||
size: Size {
|
||||
width: 10.0,
|
||||
height: 10.0,
|
||||
},
|
||||
};
|
||||
assert_eq!(tree.insert(bounds4), 1); // bounds4 does not overlap with bounds1, bounds2, or bounds3
|
||||
assert_eq!(tree.insert(bounds5), 1); // bounds5 does not overlap with any other bounds
|
||||
assert_eq!(tree.insert(bounds6), 2); // bounds6 overlaps with bounds4, so it should have a different order
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_random_iterations() {
|
||||
let max_bounds = 100;
|
||||
for seed in 1..=1000 {
|
||||
// let seed = 44;
|
||||
let mut tree = BoundsTree::default();
|
||||
let mut rng = rand::rngs::StdRng::seed_from_u64(seed as u64);
|
||||
let mut expected_quads: Vec<(Bounds<f32>, u32)> = Vec::new();
|
||||
|
||||
// Insert a random number of random AABBs into the tree.
|
||||
let num_bounds = rng.random_range(1..=max_bounds);
|
||||
for _ in 0..num_bounds {
|
||||
let min_x: f32 = rng.random_range(-100.0..100.0);
|
||||
let min_y: f32 = rng.random_range(-100.0..100.0);
|
||||
let width: f32 = rng.random_range(0.0..50.0);
|
||||
let height: f32 = rng.random_range(0.0..50.0);
|
||||
let bounds = Bounds {
|
||||
origin: Point { x: min_x, y: min_y },
|
||||
size: Size { width, height },
|
||||
};
|
||||
|
||||
let expected_ordering = expected_quads
|
||||
.iter()
|
||||
.filter_map(|quad| quad.0.intersects(&bounds).then_some(quad.1))
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
+ 1;
|
||||
expected_quads.push((bounds, expected_ordering));
|
||||
|
||||
// Insert the AABB into the tree and collect intersections.
|
||||
let actual_ordering = tree.insert(bounds);
|
||||
assert_eq!(actual_ordering, expected_ordering);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+1070
File diff suppressed because it is too large.
Load diff
Vendored
+122
@@ -0,0 +1,122 @@
|
||||
use crate::{App, Global, Rgba, Window, WindowAppearance, rgb};
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// The default set of colors for gpui.
|
||||
///
|
||||
/// These are used for styling base components, examples and more.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Colors {
|
||||
/// Text color
|
||||
pub text: Rgba,
|
||||
/// Selected text color
|
||||
pub selected_text: Rgba,
|
||||
/// Background color
|
||||
pub background: Rgba,
|
||||
/// Disabled color
|
||||
pub disabled: Rgba,
|
||||
/// Selected color
|
||||
pub selected: Rgba,
|
||||
/// Border color
|
||||
pub border: Rgba,
|
||||
/// Separator color
|
||||
pub separator: Rgba,
|
||||
/// Container color
|
||||
pub container: Rgba,
|
||||
}
|
||||
|
||||
impl Default for Colors {
|
||||
fn default() -> Self {
|
||||
Self::light()
|
||||
}
|
||||
}
|
||||
|
||||
impl Colors {
|
||||
/// Returns the default colors for the given window appearance.
|
||||
pub fn for_appearance(window: &Window) -> Self {
|
||||
match window.appearance() {
|
||||
WindowAppearance::Light | WindowAppearance::VibrantLight => Self::light(),
|
||||
WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::dark(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the default dark colors.
|
||||
pub fn dark() -> Self {
|
||||
Self {
|
||||
text: rgb(0xffffff),
|
||||
selected_text: rgb(0xffffff),
|
||||
disabled: rgb(0x565656),
|
||||
selected: rgb(0x2457ca),
|
||||
background: rgb(0x222222),
|
||||
border: rgb(0x000000),
|
||||
separator: rgb(0xd9d9d9),
|
||||
container: rgb(0x262626),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the default light colors.
|
||||
pub fn light() -> Self {
|
||||
Self {
|
||||
text: rgb(0x252525),
|
||||
selected_text: rgb(0xffffff),
|
||||
background: rgb(0xffffff),
|
||||
disabled: rgb(0xb0b0b0),
|
||||
selected: rgb(0x2a63d9),
|
||||
border: rgb(0xd9d9d9),
|
||||
separator: rgb(0xe6e6e6),
|
||||
container: rgb(0xf4f5f5),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get [Colors] from the global state
|
||||
pub fn get_global(cx: &App) -> &Arc<Colors> {
|
||||
&cx.global::<GlobalColors>().0
|
||||
}
|
||||
}
|
||||
|
||||
/// Get [Colors] from the global state
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GlobalColors(pub Arc<Colors>);
|
||||
|
||||
impl Deref for GlobalColors {
|
||||
type Target = Arc<Colors>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Global for GlobalColors {}
|
||||
|
||||
/// Implement this trait to allow global [Colors] access via `cx.default_colors()`.
|
||||
pub trait DefaultColors {
|
||||
/// Returns the default [`Colors`]
|
||||
fn default_colors(&self) -> &Arc<Colors>;
|
||||
}
|
||||
|
||||
impl DefaultColors for App {
|
||||
fn default_colors(&self) -> &Arc<Colors> {
|
||||
&self.global::<GlobalColors>().0
|
||||
}
|
||||
}
|
||||
|
||||
/// The appearance of the base GPUI colors, used to style GPUI elements
|
||||
///
|
||||
/// Varies based on the system's current [`WindowAppearance`].
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DefaultAppearance {
|
||||
/// Use the set of colors for light appearances.
|
||||
#[default]
|
||||
Light,
|
||||
/// Use the set of colors for dark appearances.
|
||||
Dark,
|
||||
}
|
||||
|
||||
impl From<WindowAppearance> for DefaultAppearance {
|
||||
fn from(appearance: WindowAppearance) -> Self {
|
||||
match appearance {
|
||||
WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light,
|
||||
WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark,
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+428
@@ -0,0 +1,428 @@
|
||||
//! A developer overlay that paints frame-time statistics directly into the
|
||||
//! scene, bypassing layout, text, and view invalidation entirely (to avoid
|
||||
//! infinitely triggering new frames).
|
||||
|
||||
use crate::{
|
||||
BorderStyle, Bounds, ContentMask, Corners, Edges, Hsla, Pixels, Quad, ScaledPixels, Scene,
|
||||
Size, point, rgba, size, transparent_black,
|
||||
};
|
||||
use std::{collections::VecDeque, time::Duration};
|
||||
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub enum DebugFrameOverlayMode {
|
||||
#[default]
|
||||
Hidden,
|
||||
Minimal,
|
||||
Full,
|
||||
}
|
||||
|
||||
impl DebugFrameOverlayMode {
|
||||
/// Returns the next mode in the Hidden → FrameTime → Detailed cycle.
|
||||
pub fn next(self) -> Self {
|
||||
match self {
|
||||
Self::Hidden => Self::Minimal,
|
||||
Self::Minimal => Self::Full,
|
||||
Self::Full => Self::Hidden,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The number of most recent draw durations retained for percentile statistics.
|
||||
const MAX_SAMPLES: usize = 1000;
|
||||
|
||||
const GLYPH_WIDTH: usize = 5;
|
||||
const GLYPH_HEIGHT: usize = 7;
|
||||
/// Glyph advance and line advance, in font cells.
|
||||
const CHAR_ADVANCE: f32 = (GLYPH_WIDTH + 1) as f32;
|
||||
const LINE_ADVANCE: f32 = (GLYPH_HEIGHT + 2) as f32;
|
||||
/// Padding between the panel edge and the text, in font cells.
|
||||
const PANEL_PADDING: f32 = 2.0;
|
||||
/// Margin between the panel and the window corner, in font cells.
|
||||
const PANEL_MARGIN: f32 = 4.0;
|
||||
/// Side of one square font cell, in logical pixels.
|
||||
const CELL_SIZE: f32 = 2.0;
|
||||
|
||||
fn text_color() -> Hsla {
|
||||
rgba(0x33ff33ff).into()
|
||||
}
|
||||
|
||||
fn panel_color() -> Hsla {
|
||||
rgba(0x000000aa).into()
|
||||
}
|
||||
|
||||
pub(crate) struct DebugFrameOverlay {
|
||||
mode: DebugFrameOverlayMode,
|
||||
draw_durations: VecDeque<Duration>,
|
||||
total_frame_count: u64,
|
||||
}
|
||||
|
||||
impl DebugFrameOverlay {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
mode: DebugFrameOverlayMode::default(),
|
||||
draw_durations: VecDeque::new(),
|
||||
total_frame_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn mode(&self) -> DebugFrameOverlayMode {
|
||||
self.mode
|
||||
}
|
||||
|
||||
pub(crate) fn set_mode(&mut self, mode: DebugFrameOverlayMode) {
|
||||
self.mode = mode;
|
||||
}
|
||||
|
||||
/// Clears the draw-duration samples, restarting the percentile window.
|
||||
/// The total frame count is left untouched.
|
||||
pub(crate) fn reset_stats(&mut self) {
|
||||
self.draw_durations.clear();
|
||||
}
|
||||
|
||||
pub(crate) fn is_enabled(&self) -> bool {
|
||||
self.mode != DebugFrameOverlayMode::Hidden
|
||||
}
|
||||
|
||||
pub(crate) fn record_frame(&mut self, draw_duration: Duration) {
|
||||
self.total_frame_count += 1;
|
||||
if self.draw_durations.len() >= MAX_SAMPLES {
|
||||
self.draw_durations.pop_front();
|
||||
}
|
||||
self.draw_durations.push_back(draw_duration);
|
||||
}
|
||||
|
||||
pub(crate) fn paint(&self, scene: &mut Scene, viewport_size: Size<Pixels>, scale_factor: f32) {
|
||||
if !self.is_enabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
let lines = self.lines();
|
||||
let max_line_chars = lines.iter().map(|line| line.len()).max().unwrap_or(0);
|
||||
// Ensure at least one physical pixel per cell so the text stays legible
|
||||
// at fractional downscale factors.
|
||||
let cell = (CELL_SIZE * scale_factor).max(1.0);
|
||||
|
||||
let panel_width = cell * (max_line_chars as f32 * CHAR_ADVANCE + 2.0 * PANEL_PADDING);
|
||||
let panel_height = cell * (lines.len() as f32 * LINE_ADVANCE + 2.0 * PANEL_PADDING);
|
||||
let viewport = viewport_size.scale(scale_factor);
|
||||
let panel_left = viewport.width.0 - panel_width - cell * PANEL_MARGIN;
|
||||
let panel_top = cell * PANEL_MARGIN;
|
||||
|
||||
let content_mask = ContentMask {
|
||||
bounds: Bounds {
|
||||
origin: point(ScaledPixels(0.), ScaledPixels(0.)),
|
||||
size: viewport,
|
||||
},
|
||||
};
|
||||
|
||||
scene.insert_primitive(solid_quad(
|
||||
scaled_bounds(panel_left, panel_top, panel_width, panel_height),
|
||||
&content_mask,
|
||||
panel_color(),
|
||||
));
|
||||
|
||||
let text_color = text_color();
|
||||
for (line_index, line) in lines.iter().enumerate() {
|
||||
let line_top = panel_top + cell * (PANEL_PADDING + line_index as f32 * LINE_ADVANCE);
|
||||
for (char_index, character) in line.chars().enumerate() {
|
||||
let Some(rows) = glyph(character) else {
|
||||
continue;
|
||||
};
|
||||
let glyph_left =
|
||||
panel_left + cell * (PANEL_PADDING + char_index as f32 * CHAR_ADVANCE);
|
||||
for (row_index, row) in rows.iter().enumerate() {
|
||||
let row_top = line_top + cell * row_index as f32;
|
||||
// Merge horizontal runs of lit cells into single quads.
|
||||
let mut column = 0;
|
||||
while column < GLYPH_WIDTH {
|
||||
if row & (1 << (GLYPH_WIDTH - 1 - column)) == 0 {
|
||||
column += 1;
|
||||
continue;
|
||||
}
|
||||
let run_start = column;
|
||||
while column < GLYPH_WIDTH && row & (1 << (GLYPH_WIDTH - 1 - column)) != 0 {
|
||||
column += 1;
|
||||
}
|
||||
scene.insert_primitive(solid_quad(
|
||||
scaled_bounds(
|
||||
glyph_left + cell * run_start as f32,
|
||||
row_top,
|
||||
cell * (column - run_start) as f32,
|
||||
cell,
|
||||
),
|
||||
&content_mask,
|
||||
text_color,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn lines(&self) -> Vec<String> {
|
||||
let current = self.draw_durations.back().copied();
|
||||
match self.mode {
|
||||
DebugFrameOverlayMode::Hidden => Vec::new(),
|
||||
DebugFrameOverlayMode::Minimal => vec![format_ms(current)],
|
||||
DebugFrameOverlayMode::Full => {
|
||||
let mut sorted: Vec<Duration> = self.draw_durations.iter().copied().collect();
|
||||
sorted.sort_unstable();
|
||||
let percentile = |numerator: usize| {
|
||||
(!sorted.is_empty()).then(|| sorted[(sorted.len() - 1) * numerator / 100])
|
||||
};
|
||||
// Past five digits the count would break the column
|
||||
// alignment, so it saturates instead.
|
||||
let frame_count = if self.total_frame_count > 99_999 {
|
||||
"LOTS".to_string()
|
||||
} else {
|
||||
self.total_frame_count.to_string()
|
||||
};
|
||||
// Labels are padded to a uniform width so the fixed-width
|
||||
// durations start in the same column on every line.
|
||||
vec![
|
||||
format!("CUR {}", format_ms(current)),
|
||||
format!("1% {}", format_ms(percentile(99))),
|
||||
format!("10% {}", format_ms(percentile(90))),
|
||||
format!("MAX {}", format_ms(sorted.last().copied())),
|
||||
format!("FRAMES {frame_count:>5}"),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats as `abc.d MS`, right-aligned in room for three integer digits and
|
||||
/// one decimal (padded with spaces, not zeroes), so stacked readouts align.
|
||||
fn format_ms(duration: Option<Duration>) -> String {
|
||||
match duration {
|
||||
Some(duration) => {
|
||||
let ms = duration.as_secs_f32() * 1000.0;
|
||||
format!("{ms:>5.1} MS")
|
||||
}
|
||||
None => " -- MS".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn scaled_bounds(left: f32, top: f32, width: f32, height: f32) -> Bounds<ScaledPixels> {
|
||||
Bounds {
|
||||
origin: point(ScaledPixels(left), ScaledPixels(top)),
|
||||
size: size(ScaledPixels(width), ScaledPixels(height)),
|
||||
}
|
||||
}
|
||||
|
||||
fn solid_quad(
|
||||
bounds: Bounds<ScaledPixels>,
|
||||
content_mask: &ContentMask<ScaledPixels>,
|
||||
color: Hsla,
|
||||
) -> Quad {
|
||||
Quad {
|
||||
order: 0,
|
||||
border_style: BorderStyle::Solid,
|
||||
bounds,
|
||||
content_mask: *content_mask,
|
||||
background: color.into(),
|
||||
border_color: transparent_black(),
|
||||
corner_radii: Corners::default(),
|
||||
border_widths: Edges::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the 5x7 bitmap for the given character, one `u8` of column bits per
|
||||
/// row with the most significant of the 5 bits leftmost. Only the characters
|
||||
/// used by the overlay's readouts are defined.
|
||||
fn glyph(character: char) -> Option<[u8; GLYPH_HEIGHT]> {
|
||||
Some(match character {
|
||||
'0' => [
|
||||
0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110,
|
||||
],
|
||||
'1' => [
|
||||
0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110,
|
||||
],
|
||||
'2' => [
|
||||
0b01110, 0b10001, 0b00001, 0b00010, 0b00100, 0b01000, 0b11111,
|
||||
],
|
||||
'3' => [
|
||||
0b11111, 0b00010, 0b00100, 0b00010, 0b00001, 0b10001, 0b01110,
|
||||
],
|
||||
'4' => [
|
||||
0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010,
|
||||
],
|
||||
'5' => [
|
||||
0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110,
|
||||
],
|
||||
'6' => [
|
||||
0b00110, 0b01000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110,
|
||||
],
|
||||
'7' => [
|
||||
0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000,
|
||||
],
|
||||
'8' => [
|
||||
0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110,
|
||||
],
|
||||
'9' => [
|
||||
0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00010, 0b01100,
|
||||
],
|
||||
'.' => [
|
||||
0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b01100, 0b01100,
|
||||
],
|
||||
'-' => [
|
||||
0b00000, 0b00000, 0b00000, 0b11111, 0b00000, 0b00000, 0b00000,
|
||||
],
|
||||
'%' => [
|
||||
0b11001, 0b11001, 0b00010, 0b00100, 0b01000, 0b10011, 0b10011,
|
||||
],
|
||||
'A' => [
|
||||
0b01110, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001,
|
||||
],
|
||||
'C' => [
|
||||
0b01110, 0b10001, 0b10000, 0b10000, 0b10000, 0b10001, 0b01110,
|
||||
],
|
||||
'E' => [
|
||||
0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b11111,
|
||||
],
|
||||
'F' => [
|
||||
0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b10000,
|
||||
],
|
||||
'L' => [
|
||||
0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b11111,
|
||||
],
|
||||
'M' => [
|
||||
0b10001, 0b11011, 0b10101, 0b10101, 0b10001, 0b10001, 0b10001,
|
||||
],
|
||||
'N' => [
|
||||
0b10001, 0b11001, 0b10101, 0b10011, 0b10001, 0b10001, 0b10001,
|
||||
],
|
||||
'O' => [
|
||||
0b01110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110,
|
||||
],
|
||||
'R' => [
|
||||
0b11110, 0b10001, 0b10001, 0b11110, 0b10100, 0b10010, 0b10001,
|
||||
],
|
||||
'S' => [
|
||||
0b01111, 0b10000, 0b10000, 0b01110, 0b00001, 0b00001, 0b11110,
|
||||
],
|
||||
'T' => [
|
||||
0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100,
|
||||
],
|
||||
'U' => [
|
||||
0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110,
|
||||
],
|
||||
'X' => [
|
||||
0b10001, 0b10001, 0b01010, 0b00100, 0b01010, 0b10001, 0b10001,
|
||||
],
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Every character the readouts can produce must have a glyph, or it
|
||||
/// would silently render as blank space.
|
||||
#[test]
|
||||
fn all_rendered_characters_have_glyphs() {
|
||||
let mut overlay = DebugFrameOverlay::new();
|
||||
overlay.set_mode(DebugFrameOverlayMode::Full);
|
||||
|
||||
let mut lines = Vec::new();
|
||||
for duration in [
|
||||
Duration::ZERO,
|
||||
Duration::from_micros(1),
|
||||
Duration::from_micros(8_333),
|
||||
Duration::from_millis(123),
|
||||
Duration::from_secs(2),
|
||||
] {
|
||||
overlay.record_frame(duration);
|
||||
lines.extend(overlay.lines());
|
||||
}
|
||||
// Counts beyond five digits render as "LOTS".
|
||||
overlay.total_frame_count = 100_000;
|
||||
lines.extend(overlay.lines());
|
||||
|
||||
// An enabled overlay with no samples yet renders placeholders.
|
||||
let mut empty = DebugFrameOverlay::new();
|
||||
empty.set_mode(DebugFrameOverlayMode::Full);
|
||||
lines.extend(empty.lines());
|
||||
|
||||
for line in lines {
|
||||
for character in line.chars() {
|
||||
assert!(
|
||||
character == ' ' || glyph(character).is_some(),
|
||||
"no glyph for {character:?} in line {line:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percentile_lows_are_reported_as_times() {
|
||||
let mut overlay = DebugFrameOverlay::new();
|
||||
overlay.set_mode(DebugFrameOverlayMode::Full);
|
||||
for milliseconds in 1..=100 {
|
||||
overlay.record_frame(Duration::from_millis(milliseconds));
|
||||
}
|
||||
let lines = overlay.lines();
|
||||
assert_eq!(lines[0], "CUR 100.0 MS");
|
||||
assert_eq!(lines[1], "1% 99.0 MS");
|
||||
assert_eq!(lines[2], "10% 90.0 MS");
|
||||
assert_eq!(lines[3], "MAX 100.0 MS");
|
||||
assert_eq!(lines[4], "FRAMES 100");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_durations_but_keeps_frame_count() {
|
||||
let mut overlay = DebugFrameOverlay::new();
|
||||
overlay.set_mode(DebugFrameOverlayMode::Full);
|
||||
for _ in 0..10 {
|
||||
overlay.record_frame(Duration::from_millis(10));
|
||||
}
|
||||
overlay.reset_stats();
|
||||
let lines = overlay.lines();
|
||||
assert_eq!(lines[0], "CUR -- MS");
|
||||
assert_eq!(lines[3], "MAX -- MS");
|
||||
assert_eq!(lines[4], "FRAMES 10");
|
||||
overlay.record_frame(Duration::from_millis(20));
|
||||
let lines = overlay.lines();
|
||||
assert_eq!(lines[0], "CUR 20.0 MS");
|
||||
assert_eq!(lines[4], "FRAMES 11");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_count_accumulates_across_mode_changes() {
|
||||
let mut overlay = DebugFrameOverlay::new();
|
||||
for _ in 0..3 {
|
||||
overlay.record_frame(Duration::from_millis(10));
|
||||
}
|
||||
overlay.set_mode(DebugFrameOverlayMode::Minimal);
|
||||
assert_eq!(overlay.lines(), vec![" 10.0 MS".to_string()]);
|
||||
overlay.set_mode(DebugFrameOverlayMode::Full);
|
||||
overlay.record_frame(Duration::from_millis(10));
|
||||
assert_eq!(overlay.lines()[4], "FRAMES 4");
|
||||
overlay.set_mode(DebugFrameOverlayMode::Hidden);
|
||||
overlay.record_frame(Duration::from_millis(10));
|
||||
overlay.set_mode(DebugFrameOverlayMode::Full);
|
||||
assert_eq!(overlay.lines()[4], "FRAMES 5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_count_is_right_aligned_and_saturates() {
|
||||
let mut overlay = DebugFrameOverlay::new();
|
||||
overlay.set_mode(DebugFrameOverlayMode::Full);
|
||||
overlay.record_frame(Duration::from_millis(10));
|
||||
assert_eq!(overlay.lines()[4], "FRAMES 1");
|
||||
overlay.total_frame_count = 99_999;
|
||||
assert_eq!(overlay.lines()[4], "FRAMES 99999");
|
||||
overlay.total_frame_count = 100_000;
|
||||
assert_eq!(overlay.lines()[4], "FRAMES LOTS");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toggling_on_shows_previous_frame_immediately() {
|
||||
let mut overlay = DebugFrameOverlay::new();
|
||||
overlay.record_frame(Duration::from_millis(10));
|
||||
overlay.set_mode(DebugFrameOverlayMode::Minimal);
|
||||
assert_eq!(overlay.lines(), vec![" 10.0 MS".to_string()]);
|
||||
}
|
||||
}
|
||||
Vendored
+813
@@ -0,0 +1,813 @@
|
||||
//! Elements are the workhorses of GPUI. They are responsible for laying out and painting all of
|
||||
//! the contents of a window. Elements form a tree and are laid out according to the web layout
|
||||
//! standards as implemented by [taffy](https://github.com/DioxusLabs/taffy). Most of the time,
|
||||
//! you won't need to interact with this module or these APIs directly. Elements provide their
|
||||
//! own APIs and GPUI, or other element implementation, uses the APIs in this module to convert
|
||||
//! that element tree into the pixels you see on the screen.
|
||||
//!
|
||||
//! # Element Basics
|
||||
//!
|
||||
//! Elements are constructed by calling [`Render::render()`] on the root view of the window,
|
||||
//! which recursively constructs the element tree from the current state of the application,.
|
||||
//! These elements are then laid out by Taffy, and painted to the screen according to their own
|
||||
//! implementation of [`Element::paint()`]. Before the start of the next frame, the entire element
|
||||
//! tree and any callbacks they have registered with GPUI are dropped and the process repeats.
|
||||
//!
|
||||
//! But some state is too simple and voluminous to store in every view that needs it, e.g.
|
||||
//! whether a hover has been started or not. For this, GPUI provides the [`Element::PrepaintState`], associated type.
|
||||
//!
|
||||
//! # Implementing your own elements
|
||||
//!
|
||||
//! Elements are intended to be the low level, imperative API to GPUI. They are responsible for upholding,
|
||||
//! or breaking, GPUI's features as they deem necessary. As an example, most GPUI elements are expected
|
||||
//! to stay in the bounds that their parent element gives them. But with [`Window::with_content_mask`],
|
||||
//! you can ignore this restriction and paint anywhere inside of the window's bounds. This is useful for overlays
|
||||
//! and popups and anything else that shows up 'on top' of other elements.
|
||||
//! With great power, comes great responsibility.
|
||||
//!
|
||||
//! However, most of the time, you won't need to implement your own elements. GPUI provides a number of
|
||||
//! elements that should cover most common use cases out of the box and it's recommended that you use those
|
||||
//! to construct `components`, using the [`RenderOnce`] trait and the `#[derive(IntoElement)]` macro. Only implement
|
||||
//! elements when you need to take manual control of the layout and painting process, such as when using
|
||||
//! your own custom layout algorithm or rendering a code editor.
|
||||
|
||||
#[cfg(any(feature = "inspector", debug_assertions))]
|
||||
use crate::InspectorElementPath;
|
||||
use crate::{
|
||||
A11ySubtreeBuilder, App, ArenaBox, AvailableSpace, Bounds, Context, DispatchNodeId, ElementId,
|
||||
FocusHandle, InspectorElementId, LayoutId, Pixels, Point, Size, Style, Window,
|
||||
util::FluentBuilder, window::with_element_arena,
|
||||
};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use std::{
|
||||
any::Any,
|
||||
fmt::{self, Debug, Display},
|
||||
mem, panic,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
/// Implemented by types that participate in laying out and painting the contents of a window.
|
||||
/// Elements form a tree and are laid out according to web-based layout rules, as implemented by Taffy.
|
||||
/// You can create custom elements by implementing this trait, see the module-level documentation
|
||||
/// for more details.
|
||||
pub trait Element: 'static + IntoElement {
|
||||
/// The type of state returned from [`Element::request_layout`]. A mutable reference to this state is subsequently
|
||||
/// provided to [`Element::prepaint`] and [`Element::paint`].
|
||||
type RequestLayoutState: 'static;
|
||||
|
||||
/// The type of state returned from [`Element::prepaint`]. A mutable reference to this state is subsequently
|
||||
/// provided to [`Element::paint`].
|
||||
type PrepaintState: 'static;
|
||||
|
||||
/// If this element has a unique identifier, return it here. This is used to track elements across frames, and
|
||||
/// will cause a GlobalElementId to be passed to the request_layout, prepaint, and paint methods.
|
||||
///
|
||||
/// The global id can in turn be used to access state that's connected to an element with the same id across
|
||||
/// frames. This id must be unique among children of the first containing element with an id.
|
||||
fn id(&self) -> Option<ElementId>;
|
||||
|
||||
/// Source location where this element was constructed, used to disambiguate elements in the
|
||||
/// inspector and navigate to their source code.
|
||||
fn source_location(&self) -> Option<&'static panic::Location<'static>>;
|
||||
|
||||
/// Before an element can be painted, we need to know where it's going to be and how big it is.
|
||||
/// Use this method to request a layout from Taffy and initialize the element's state.
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState);
|
||||
|
||||
/// After laying out an element, we need to commit its bounds to the current frame for hitbox
|
||||
/// purposes. The state argument is the same state that was returned from [`Element::request_layout()`].
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self::PrepaintState;
|
||||
|
||||
/// Once layout has been completed, this method will be called to paint the element to the screen.
|
||||
/// The state argument is the same state that was returned from [`Element::request_layout()`].
|
||||
fn paint(
|
||||
&mut self,
|
||||
id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
prepaint: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
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) {}
|
||||
|
||||
/// Add synthetic child nodes to an [`Element`] that has an
|
||||
/// [`.id()`][Element::id] and a [`.role()`][Element::a11y_role].
|
||||
///
|
||||
/// Some elements may want to inject accessibility nodes that do not
|
||||
/// correspond to any GPUI element. For example, a custom text field element
|
||||
/// may want to inject synthetic child nodes for the text content.
|
||||
///
|
||||
/// See [Synthetic children](crate::_accessibility#synthetic-children) in
|
||||
/// the accessibility guide for more detail.
|
||||
fn a11y_synthetic_children(
|
||||
&mut self,
|
||||
_prepaint: &mut Self::PrepaintState,
|
||||
_builder: &mut A11ySubtreeBuilder,
|
||||
) {
|
||||
}
|
||||
|
||||
/// Convert this element into a dynamically-typed [`AnyElement`].
|
||||
fn into_any(self) -> AnyElement {
|
||||
AnyElement::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Implemented by any type that can be converted into an element.
|
||||
pub trait IntoElement: Sized {
|
||||
/// The specific type of element into which the implementing type is converted.
|
||||
/// Useful for converting other types into elements automatically, like Strings
|
||||
type Element: Element;
|
||||
|
||||
/// Convert self into a type that implements [`Element`].
|
||||
fn into_element(self) -> Self::Element;
|
||||
|
||||
/// Convert self into a dynamically-typed [`AnyElement`].
|
||||
fn into_any_element(self) -> AnyElement {
|
||||
self.into_element().into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoElement> FluentBuilder for T {}
|
||||
|
||||
/// An object that can be drawn to the screen. This is the trait that distinguishes "views" from
|
||||
/// other entities. Views are `Entity`'s which `impl Render` and drawn to the screen.
|
||||
pub trait Render: 'static + Sized {
|
||||
/// Render this view into an element tree.
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement;
|
||||
}
|
||||
|
||||
impl Render for Empty {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
Empty
|
||||
}
|
||||
}
|
||||
|
||||
/// You can derive [`IntoElement`] on any type that implements this trait.
|
||||
/// It is used to construct reusable `components` out of plain data. Think of
|
||||
/// components as a recipe for a certain pattern of elements. RenderOnce allows
|
||||
/// you to invoke this pattern, without breaking the fluent builder pattern of
|
||||
/// the element APIs.
|
||||
pub trait RenderOnce: 'static {
|
||||
/// Render this component into an element tree. Note that this method
|
||||
/// takes ownership of self, as compared to [`Render::render()`] method
|
||||
/// which takes a mutable reference.
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement;
|
||||
}
|
||||
|
||||
/// This is a helper trait to provide a uniform interface for constructing elements that
|
||||
/// can accept any number of any kind of child elements
|
||||
pub trait ParentElement {
|
||||
/// Extend this element's children with the given child elements.
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>);
|
||||
|
||||
/// Add a single child element to this element.
|
||||
fn child(mut self, child: impl IntoElement) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.extend(std::iter::once(child.into_any_element()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add multiple child elements to this element.
|
||||
fn children(mut self, children: impl IntoIterator<Item = impl IntoElement>) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.extend(children.into_iter().map(|child| child.into_any_element()));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A globally unique identifier for an element, used to track state across frames.
|
||||
#[derive(Deref, DerefMut, Clone, Default, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct GlobalElementId(pub(crate) Arc<[ElementId]>);
|
||||
|
||||
impl Display for GlobalElementId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for (i, element_id) in self.0.iter().enumerate() {
|
||||
if i > 0 {
|
||||
write!(f, ".")?;
|
||||
}
|
||||
write!(f, "{}", element_id)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
fn request_layout(&mut self, window: &mut Window, cx: &mut App) -> LayoutId;
|
||||
|
||||
fn prepaint(&mut self, window: &mut Window, cx: &mut App);
|
||||
|
||||
fn paint(&mut self, window: &mut Window, cx: &mut App);
|
||||
|
||||
fn layout_as_root(
|
||||
&mut self,
|
||||
available_space: Size<AvailableSpace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Size<Pixels>;
|
||||
}
|
||||
|
||||
/// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window.
|
||||
pub struct Drawable<E: Element> {
|
||||
/// The drawn element.
|
||||
pub element: E,
|
||||
phase: ElementDrawPhase<E::RequestLayoutState, E::PrepaintState>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
enum ElementDrawPhase<RequestLayoutState, PrepaintState> {
|
||||
#[default]
|
||||
Start,
|
||||
RequestLayout {
|
||||
layout_id: LayoutId,
|
||||
global_id: Option<GlobalElementId>,
|
||||
inspector_id: Option<InspectorElementId>,
|
||||
request_layout: RequestLayoutState,
|
||||
},
|
||||
LayoutComputed {
|
||||
layout_id: LayoutId,
|
||||
global_id: Option<GlobalElementId>,
|
||||
inspector_id: Option<InspectorElementId>,
|
||||
available_space: Size<AvailableSpace>,
|
||||
request_layout: RequestLayoutState,
|
||||
},
|
||||
Prepaint {
|
||||
node_id: DispatchNodeId,
|
||||
global_id: Option<GlobalElementId>,
|
||||
inspector_id: Option<InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
request_layout: RequestLayoutState,
|
||||
prepaint: PrepaintState,
|
||||
},
|
||||
Painted,
|
||||
}
|
||||
|
||||
/// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window.
|
||||
impl<E: Element> Drawable<E> {
|
||||
pub(crate) fn new(element: E) -> Self {
|
||||
Drawable {
|
||||
element,
|
||||
phase: ElementDrawPhase::Start,
|
||||
}
|
||||
}
|
||||
|
||||
fn request_layout(&mut self, window: &mut Window, cx: &mut App) -> LayoutId {
|
||||
match mem::take(&mut self.phase) {
|
||||
ElementDrawPhase::Start => {
|
||||
let global_id = self
|
||||
.element
|
||||
.id()
|
||||
.map(|element_id| prepare_element_id(element_id, window));
|
||||
|
||||
let inspector_id;
|
||||
#[cfg(any(feature = "inspector", debug_assertions))]
|
||||
{
|
||||
inspector_id = if window.inspector_enabled() {
|
||||
self.element
|
||||
.source_location()
|
||||
.map(|source| prepare_inspector_id(source, window))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
#[cfg(not(any(feature = "inspector", debug_assertions)))]
|
||||
{
|
||||
inspector_id = None;
|
||||
}
|
||||
|
||||
let (layout_id, request_layout) = self.element.request_layout(
|
||||
global_id.as_ref(),
|
||||
inspector_id.as_ref(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
if global_id.is_some() {
|
||||
window.element_id_stack.pop();
|
||||
}
|
||||
|
||||
self.phase = ElementDrawPhase::RequestLayout {
|
||||
layout_id,
|
||||
global_id,
|
||||
inspector_id,
|
||||
request_layout,
|
||||
};
|
||||
layout_id
|
||||
}
|
||||
_ => panic!("must call request_layout only once"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn prepaint(&mut self, window: &mut Window, cx: &mut App) {
|
||||
match mem::take(&mut self.phase) {
|
||||
ElementDrawPhase::RequestLayout {
|
||||
layout_id,
|
||||
global_id,
|
||||
inspector_id,
|
||||
mut request_layout,
|
||||
}
|
||||
| ElementDrawPhase::LayoutComputed {
|
||||
layout_id,
|
||||
global_id,
|
||||
inspector_id,
|
||||
mut request_layout,
|
||||
..
|
||||
} => {
|
||||
if let Some(element_id) = self.element.id() {
|
||||
window.element_id_stack.push(element_id);
|
||||
debug_assert_eq!(&*global_id.as_ref().unwrap().0, &*window.element_id_stack);
|
||||
}
|
||||
|
||||
let bounds = window.layout_bounds(layout_id);
|
||||
let 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);
|
||||
#[cfg(debug_assertions)]
|
||||
if pushed_a11y_node {
|
||||
let view = window
|
||||
.a11y
|
||||
.view_type_names
|
||||
.get(&window.current_view())
|
||||
.copied();
|
||||
let source_location = self.element.source_location();
|
||||
window.a11y.nodes.record_node_info(
|
||||
node_id,
|
||||
crate::window::a11y::debug::NodeDebugInfo {
|
||||
synthetic: false,
|
||||
view,
|
||||
element_id: global_id.0.last().map(|id| format!("{id:?}")),
|
||||
source_location,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let node_id = window.next_frame.dispatch_tree.push_node();
|
||||
let mut prepaint = self.element.prepaint(
|
||||
global_id.as_ref(),
|
||||
inspector_id.as_ref(),
|
||||
bounds,
|
||||
&mut request_layout,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
window.next_frame.dispatch_tree.pop_node();
|
||||
|
||||
if pushed_a11y_node {
|
||||
if let Some(global_id) = global_id.as_ref() {
|
||||
#[cfg(debug_assertions)]
|
||||
let creator = crate::window::a11y::debug::NodeCreator {
|
||||
view: window
|
||||
.a11y
|
||||
.view_type_names
|
||||
.get(&window.current_view())
|
||||
.copied(),
|
||||
element_id: global_id.0.last().map(|id| format!("{id:?}")),
|
||||
source_location: self.element.source_location(),
|
||||
};
|
||||
let mut builder = A11ySubtreeBuilder::new(
|
||||
global_id.accesskit_node_id(),
|
||||
&mut window.a11y.nodes,
|
||||
);
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
builder = builder.with_creator(creator);
|
||||
}
|
||||
self.element
|
||||
.a11y_synthetic_children(&mut prepaint, &mut builder);
|
||||
}
|
||||
window.a11y.nodes.pop();
|
||||
}
|
||||
|
||||
if global_id.is_some() {
|
||||
window.element_id_stack.pop();
|
||||
}
|
||||
|
||||
self.phase = ElementDrawPhase::Prepaint {
|
||||
node_id,
|
||||
global_id,
|
||||
inspector_id,
|
||||
bounds,
|
||||
request_layout,
|
||||
prepaint,
|
||||
};
|
||||
}
|
||||
_ => panic!("must call request_layout before prepaint"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn paint(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (E::RequestLayoutState, E::PrepaintState) {
|
||||
match mem::take(&mut self.phase) {
|
||||
ElementDrawPhase::Prepaint {
|
||||
node_id,
|
||||
global_id,
|
||||
inspector_id,
|
||||
bounds,
|
||||
mut request_layout,
|
||||
mut prepaint,
|
||||
..
|
||||
} => {
|
||||
if let Some(element_id) = self.element.id() {
|
||||
window.element_id_stack.push(element_id);
|
||||
debug_assert_eq!(&*global_id.as_ref().unwrap().0, &*window.element_id_stack);
|
||||
}
|
||||
|
||||
window.next_frame.dispatch_tree.set_active_node(node_id);
|
||||
self.element.paint(
|
||||
global_id.as_ref(),
|
||||
inspector_id.as_ref(),
|
||||
bounds,
|
||||
&mut request_layout,
|
||||
&mut prepaint,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
if global_id.is_some() {
|
||||
window.element_id_stack.pop();
|
||||
}
|
||||
|
||||
self.phase = ElementDrawPhase::Painted;
|
||||
(request_layout, prepaint)
|
||||
}
|
||||
_ => panic!("must call prepaint before paint"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn layout_as_root(
|
||||
&mut self,
|
||||
available_space: Size<AvailableSpace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Size<Pixels> {
|
||||
if matches!(&self.phase, ElementDrawPhase::Start) {
|
||||
self.request_layout(window, cx);
|
||||
}
|
||||
|
||||
let layout_id = match mem::take(&mut self.phase) {
|
||||
ElementDrawPhase::RequestLayout {
|
||||
layout_id,
|
||||
global_id,
|
||||
inspector_id,
|
||||
request_layout,
|
||||
} => {
|
||||
window.compute_layout(layout_id, available_space, cx);
|
||||
self.phase = ElementDrawPhase::LayoutComputed {
|
||||
layout_id,
|
||||
global_id,
|
||||
inspector_id,
|
||||
available_space,
|
||||
request_layout,
|
||||
};
|
||||
layout_id
|
||||
}
|
||||
ElementDrawPhase::LayoutComputed {
|
||||
layout_id,
|
||||
global_id,
|
||||
inspector_id,
|
||||
available_space: prev_available_space,
|
||||
request_layout,
|
||||
} => {
|
||||
if available_space != prev_available_space {
|
||||
window.compute_layout(layout_id, available_space, cx);
|
||||
}
|
||||
self.phase = ElementDrawPhase::LayoutComputed {
|
||||
layout_id,
|
||||
global_id,
|
||||
inspector_id,
|
||||
available_space,
|
||||
request_layout,
|
||||
};
|
||||
layout_id
|
||||
}
|
||||
_ => panic!("cannot measure after painting"),
|
||||
};
|
||||
|
||||
window.layout_bounds(layout_id).size
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> ElementObject for Drawable<E>
|
||||
where
|
||||
E: Element,
|
||||
E::RequestLayoutState: 'static,
|
||||
{
|
||||
fn inner_element(&mut self) -> &mut dyn Any {
|
||||
&mut self.element
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn request_layout(&mut self, window: &mut Window, cx: &mut App) -> LayoutId {
|
||||
Drawable::request_layout(self, window, cx)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn prepaint(&mut self, window: &mut Window, cx: &mut App) {
|
||||
Drawable::prepaint(self, window, cx);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn paint(&mut self, window: &mut Window, cx: &mut App) {
|
||||
Drawable::paint(self, window, cx);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn layout_as_root(
|
||||
&mut self,
|
||||
available_space: Size<AvailableSpace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Size<Pixels> {
|
||||
Drawable::layout_as_root(self, available_space, window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
/// A dynamically typed element that can be used to store any element type.
|
||||
pub struct AnyElement(ArenaBox<dyn ElementObject>);
|
||||
|
||||
impl AnyElement {
|
||||
pub(crate) fn new<E>(element: E) -> Self
|
||||
where
|
||||
E: 'static + Element,
|
||||
E::RequestLayoutState: Any,
|
||||
{
|
||||
let element = with_element_arena(|arena| arena.alloc(|| Drawable::new(element)))
|
||||
.map(|element| element as &mut dyn ElementObject);
|
||||
AnyElement(element)
|
||||
}
|
||||
|
||||
/// Attempt to downcast a reference to the boxed element to a specific type.
|
||||
pub fn downcast_mut<T: 'static>(&mut self) -> Option<&mut T> {
|
||||
self.0.inner_element().downcast_mut::<T>()
|
||||
}
|
||||
|
||||
/// Request the layout ID of the element stored in this `AnyElement`.
|
||||
/// Used for laying out child elements in a parent element.
|
||||
pub fn request_layout(&mut self, window: &mut Window, cx: &mut App) -> LayoutId {
|
||||
self.0.request_layout(window, cx)
|
||||
}
|
||||
|
||||
/// Prepares the element to be painted by storing its bounds, giving it a chance to draw hitboxes and
|
||||
/// request autoscroll before the final paint pass is confirmed.
|
||||
pub fn prepaint(&mut self, window: &mut Window, cx: &mut App) -> Option<FocusHandle> {
|
||||
let focus_assigned = window.next_frame.focus.is_some();
|
||||
|
||||
self.0.prepaint(window, cx);
|
||||
|
||||
if !focus_assigned && let Some(focus_id) = window.next_frame.focus {
|
||||
return FocusHandle::for_id(focus_id, &cx.focus_handles);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Paints the element stored in this `AnyElement`.
|
||||
pub fn paint(&mut self, window: &mut Window, cx: &mut App) {
|
||||
self.0.paint(window, cx);
|
||||
}
|
||||
|
||||
/// Performs layout for this element within the given available space and returns its size.
|
||||
pub fn layout_as_root(
|
||||
&mut self,
|
||||
available_space: Size<AvailableSpace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Size<Pixels> {
|
||||
self.0.layout_as_root(available_space, window, cx)
|
||||
}
|
||||
|
||||
/// Prepaints this element at the given absolute origin.
|
||||
/// If any element in the subtree beneath this element is focused, its FocusHandle is returned.
|
||||
pub fn prepaint_at(
|
||||
&mut self,
|
||||
origin: Point<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<FocusHandle> {
|
||||
window.with_absolute_element_offset(origin, |window| self.prepaint(window, cx))
|
||||
}
|
||||
|
||||
/// Performs layout on this element in the available space, then prepaints it at the given absolute origin.
|
||||
/// If any element in the subtree beneath this element is focused, its FocusHandle is returned.
|
||||
pub fn prepaint_as_root(
|
||||
&mut self,
|
||||
origin: Point<Pixels>,
|
||||
available_space: Size<AvailableSpace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<FocusHandle> {
|
||||
self.layout_as_root(available_space, window, cx);
|
||||
window.with_absolute_element_offset(origin, |window| self.prepaint(window, cx))
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for AnyElement {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = ();
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let layout_id = self.request_layout(window, cx);
|
||||
(layout_id, ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
_: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
self.prepaint(window, cx);
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
_: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
_: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
self.paint(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for AnyElement {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
|
||||
fn into_any_element(self) -> AnyElement {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// The empty element, which renders nothing.
|
||||
pub struct Empty;
|
||||
|
||||
impl IntoElement for Empty {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for Empty {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = ();
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
(
|
||||
window.request_layout(
|
||||
Style {
|
||||
display: crate::Display::None,
|
||||
..Default::default()
|
||||
},
|
||||
None,
|
||||
cx,
|
||||
),
|
||||
(),
|
||||
)
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_state: &mut Self::RequestLayoutState,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) {
|
||||
}
|
||||
|
||||
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,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
fn prepare_element_id(element_id: ElementId, window: &mut Window) -> GlobalElementId {
|
||||
window.element_id_stack.push(element_id);
|
||||
GlobalElementId(Arc::from(&*window.element_id_stack))
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "inspector", debug_assertions))]
|
||||
#[inline(never)]
|
||||
fn prepare_inspector_id(
|
||||
source: &'static panic::Location<'static>,
|
||||
window: &mut Window,
|
||||
) -> InspectorElementId {
|
||||
let path = InspectorElementPath {
|
||||
global_id: GlobalElementId(Arc::from(&*window.element_id_stack)),
|
||||
source_location: source,
|
||||
};
|
||||
window.build_inspector_element_id(path)
|
||||
}
|
||||
+398
@@ -0,0 +1,398 @@
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{
|
||||
Anchor, AnyElement, App, Axis, Bounds, Display, Edges, Element, GlobalElementId,
|
||||
InspectorElementId, IntoElement, LayoutId, ParentElement, Pixels, Point, Position, Size, Style,
|
||||
Window, point, px,
|
||||
};
|
||||
|
||||
/// The state that the anchored element element uses to track its children.
|
||||
pub struct AnchoredState {
|
||||
child_layout_ids: SmallVec<[LayoutId; 4]>,
|
||||
}
|
||||
|
||||
/// An anchored element that can be used to display UI that
|
||||
/// will avoid overflowing the window bounds.
|
||||
pub struct Anchored {
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
anchor: Anchor,
|
||||
fit_mode: AnchoredFitMode,
|
||||
anchor_position: Option<Point<Pixels>>,
|
||||
position_mode: AnchoredPositionMode,
|
||||
offset: Option<Point<Pixels>>,
|
||||
}
|
||||
|
||||
/// anchored gives you an element that will avoid overflowing the window bounds.
|
||||
/// Its children should have no margin to avoid measurement issues.
|
||||
pub fn anchored() -> Anchored {
|
||||
Anchored {
|
||||
children: SmallVec::new(),
|
||||
anchor: Anchor::TopLeft,
|
||||
fit_mode: AnchoredFitMode::SwitchAnchor,
|
||||
anchor_position: None,
|
||||
position_mode: AnchoredPositionMode::Window,
|
||||
offset: None,
|
||||
}
|
||||
}
|
||||
|
||||
impl Anchored {
|
||||
/// Sets which corner of the anchored element should be anchored to the current position.
|
||||
pub fn anchor(mut self, anchor: Anchor) -> Self {
|
||||
self.anchor = anchor;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the position in window coordinates
|
||||
/// (otherwise the location the anchored element is rendered is used)
|
||||
pub fn position(mut self, anchor: Point<Pixels>) -> Self {
|
||||
self.anchor_position = Some(anchor);
|
||||
self
|
||||
}
|
||||
|
||||
/// Offset the final position by this amount.
|
||||
/// Useful when you want to anchor to an element but offset from it, such as in PopoverMenu.
|
||||
pub fn offset(mut self, offset: Point<Pixels>) -> Self {
|
||||
self.offset = Some(offset);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the position mode for this anchored element. Local will have this
|
||||
/// interpret its [`Anchored::position`] as relative to the parent element.
|
||||
/// While Window will have it interpret the position as relative to the window.
|
||||
pub fn position_mode(mut self, mode: AnchoredPositionMode) -> Self {
|
||||
self.position_mode = mode;
|
||||
self
|
||||
}
|
||||
|
||||
/// Snap to window edge instead of switching anchor corner when an overflow would occur.
|
||||
pub fn snap_to_window(mut self) -> Self {
|
||||
self.fit_mode = AnchoredFitMode::SnapToWindow;
|
||||
self
|
||||
}
|
||||
|
||||
/// Snap to window edge and leave some margins.
|
||||
pub fn snap_to_window_with_margin(mut self, edges: impl Into<Edges<Pixels>>) -> Self {
|
||||
self.fit_mode = AnchoredFitMode::SnapToWindowWithMargin(edges.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for Anchored {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for Anchored {
|
||||
type RequestLayoutState = AnchoredState;
|
||||
type PrepaintState = ();
|
||||
|
||||
fn id(&self) -> Option<crate::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,
|
||||
) -> (crate::LayoutId, Self::RequestLayoutState) {
|
||||
let child_layout_ids = self
|
||||
.children
|
||||
.iter_mut()
|
||||
.map(|child| child.request_layout(window, cx))
|
||||
.collect::<SmallVec<_>>();
|
||||
|
||||
let anchored_style = Style {
|
||||
position: Position::Absolute,
|
||||
display: Display::Flex,
|
||||
..Style::default()
|
||||
};
|
||||
|
||||
let layout_id = window.request_layout(anchored_style, child_layout_ids.iter().copied(), cx);
|
||||
|
||||
(layout_id, AnchoredState { child_layout_ids })
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
if request_layout.child_layout_ids.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
children_bounds.size,
|
||||
bounds,
|
||||
self.offset,
|
||||
);
|
||||
|
||||
let limits = Bounds {
|
||||
origin: Point::default(),
|
||||
size: window.viewport_size(),
|
||||
};
|
||||
|
||||
if self.fit_mode == AnchoredFitMode::SwitchAnchor {
|
||||
let mut anchor = self.anchor;
|
||||
|
||||
if desired.left() < limits.left() || desired.right() > limits.right() {
|
||||
let switched = Bounds::from_anchor_and_size(
|
||||
anchor.other_side_along(Axis::Horizontal),
|
||||
origin,
|
||||
children_bounds.size,
|
||||
);
|
||||
if !(switched.left() < limits.left() || switched.right() > limits.right()) {
|
||||
anchor = anchor.other_side_along(Axis::Horizontal);
|
||||
desired = switched
|
||||
}
|
||||
}
|
||||
|
||||
if desired.top() < limits.top() || desired.bottom() > limits.bottom() {
|
||||
let switched = Bounds::from_anchor_and_size(
|
||||
anchor.other_side_along(Axis::Vertical),
|
||||
origin,
|
||||
children_bounds.size,
|
||||
);
|
||||
if !(switched.top() < limits.top() || switched.bottom() > limits.bottom()) {
|
||||
desired = switched;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let client_inset = window.client_inset.unwrap_or(px(0.));
|
||||
let edges = match self.fit_mode {
|
||||
AnchoredFitMode::SnapToWindowWithMargin(edges) => edges,
|
||||
_ => Edges::default(),
|
||||
}
|
||||
.map(|edge| *edge + client_inset);
|
||||
|
||||
// Snap the horizontal edges of the anchored element to the horizontal edges of the window if
|
||||
// its horizontal bounds overflow, aligning to the left if it is wider than the limits.
|
||||
if desired.right() > limits.right() {
|
||||
desired.origin.x -= desired.right() - limits.right() + edges.right;
|
||||
}
|
||||
if desired.left() < limits.left() {
|
||||
desired.origin.x = limits.origin.x + edges.left;
|
||||
}
|
||||
|
||||
// Snap the vertical edges of the anchored element to the vertical edges of the window if
|
||||
// its vertical bounds overflow, aligning to the top if it is taller than the limits.
|
||||
if desired.bottom() > limits.bottom() {
|
||||
desired.origin.y -= desired.bottom() - limits.bottom() + edges.bottom;
|
||||
}
|
||||
if desired.top() < limits.top() {
|
||||
desired.origin.y = limits.origin.y + edges.top;
|
||||
}
|
||||
|
||||
let offset = desired.origin - bounds.origin;
|
||||
let offset = point(offset.x.round(), offset.y.round());
|
||||
|
||||
window.with_element_offset(offset, |window| {
|
||||
for child in &mut self.children {
|
||||
child.prepaint(window, cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
_bounds: crate::Bounds<crate::Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
_prepaint: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
for child in &mut self.children {
|
||||
child.paint(window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for Anchored {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Which algorithm to use when fitting the anchored element to be inside the window.
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
pub enum AnchoredFitMode {
|
||||
/// Snap the anchored element to the window edge.
|
||||
SnapToWindow,
|
||||
/// Snap to window edge and leave some margins.
|
||||
SnapToWindowWithMargin(Edges<Pixels>),
|
||||
/// Switch which corner anchor this anchored element is attached to.
|
||||
SwitchAnchor,
|
||||
}
|
||||
|
||||
/// Which algorithm to use when positioning the anchored element.
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
pub enum AnchoredPositionMode {
|
||||
/// Position the anchored element relative to the window.
|
||||
Window,
|
||||
/// Position the anchored element relative to its parent.
|
||||
Local,
|
||||
}
|
||||
|
||||
impl AnchoredPositionMode {
|
||||
fn get_position_and_bounds(
|
||||
&self,
|
||||
anchor_position: Option<Point<Pixels>>,
|
||||
anchor: Anchor,
|
||||
size: Size<Pixels>,
|
||||
bounds: Bounds<Pixels>,
|
||||
offset: Option<Point<Pixels>>,
|
||||
) -> (Point<Pixels>, Bounds<Pixels>) {
|
||||
let offset = offset.unwrap_or_default();
|
||||
|
||||
match self {
|
||||
AnchoredPositionMode::Window => {
|
||||
let anchor_position = anchor_position.unwrap_or(bounds.origin);
|
||||
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_anchor_and_size(
|
||||
anchor,
|
||||
bounds.origin + anchor_position + offset,
|
||||
size,
|
||||
);
|
||||
(anchor_position, bounds)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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.)));
|
||||
}
|
||||
}
|
||||
+1028
File diff suppressed because it is too large.
Load diff
+95
@@ -0,0 +1,95 @@
|
||||
use refineable::Refineable as _;
|
||||
|
||||
use crate::{
|
||||
App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, Pixels,
|
||||
Style, StyleRefinement, Styled, Window,
|
||||
};
|
||||
|
||||
/// Construct a canvas element with the given paint callback.
|
||||
/// Useful for adding short term custom drawing to a view.
|
||||
pub fn canvas<T>(
|
||||
prepaint: impl 'static + FnOnce(Bounds<Pixels>, &mut Window, &mut App) -> T,
|
||||
paint: impl 'static + FnOnce(Bounds<Pixels>, T, &mut Window, &mut App),
|
||||
) -> Canvas<T> {
|
||||
Canvas {
|
||||
prepaint: Some(Box::new(prepaint)),
|
||||
paint: Some(Box::new(paint)),
|
||||
style: StyleRefinement::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A canvas element, meant for accessing the low level paint API without defining a whole
|
||||
/// custom element
|
||||
pub struct Canvas<T> {
|
||||
prepaint: Option<Box<dyn FnOnce(Bounds<Pixels>, &mut Window, &mut App) -> T>>,
|
||||
paint: Option<Box<dyn FnOnce(Bounds<Pixels>, T, &mut Window, &mut App)>>,
|
||||
style: StyleRefinement,
|
||||
}
|
||||
|
||||
impl<T: 'static> IntoElement for Canvas<T> {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> Element for Canvas<T> {
|
||||
type RequestLayoutState = Style;
|
||||
type PrepaintState = Option<T>;
|
||||
|
||||
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,
|
||||
) -> (crate::LayoutId, Self::RequestLayoutState) {
|
||||
let mut style = Style::default();
|
||||
style.refine(&self.style);
|
||||
let layout_id = window.request_layout(style.clone(), [], cx);
|
||||
(layout_id, style)
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Style,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<T> {
|
||||
Some(self.prepaint.take().unwrap()(bounds, window, cx))
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
style: &mut Style,
|
||||
prepaint: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let prepaint = prepaint.take().unwrap();
|
||||
style.paint(bounds, window, cx, |window, cx| {
|
||||
(self.paint.take().unwrap())(bounds, prepaint, window, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Styled for Canvas<T> {
|
||||
fn style(&mut self) -> &mut crate::StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
//! A container query element, in the spirit of CSS container queries.
|
||||
//! The element's own size is determined solely by its style and the space
|
||||
//! offered by its parent.
|
||||
|
||||
use refineable::Refineable as _;
|
||||
|
||||
use crate::{
|
||||
AnyElement, App, AvailableSpace, Bounds, Element, ElementId, GlobalElementId,
|
||||
InspectorElementId, IntoElement, LayoutId, Pixels, Size, Style, StyleRefinement, Styled,
|
||||
Window, relative,
|
||||
};
|
||||
|
||||
/// Construct a container query element with the given render callback.
|
||||
/// The callback receives the size the element was assigned during layout and
|
||||
/// returns the contents to display within it.
|
||||
///
|
||||
/// By default the element fills its parent (equivalent to `.size_full()`);
|
||||
/// use the [`Styled`] methods to size it differently. Because the contents
|
||||
/// don't exist until after layout, they cannot influence the element's size.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use gpui::{container_query, div, px, IntoElement, ParentElement};
|
||||
/// container_query(|size, _window, _cx| {
|
||||
/// if size.width < px(240.) {
|
||||
/// div().child("Narrow layout")
|
||||
/// } else {
|
||||
/// div().child("Wide layout")
|
||||
/// }
|
||||
/// });
|
||||
/// ```
|
||||
pub fn container_query<E>(
|
||||
render: impl 'static + FnOnce(Size<Pixels>, &mut Window, &mut App) -> E,
|
||||
) -> ContainerQuery
|
||||
where
|
||||
E: IntoElement,
|
||||
{
|
||||
let mut base_style = StyleRefinement::default();
|
||||
base_style.size.width = Some(relative(1.).into());
|
||||
base_style.size.height = Some(relative(1.).into());
|
||||
|
||||
ContainerQuery {
|
||||
render: Some(Box::new(|size, window, cx| {
|
||||
render(size, window, cx).into_any_element()
|
||||
})),
|
||||
style: base_style,
|
||||
}
|
||||
}
|
||||
|
||||
/// A container query element, created with [`container_query`].
|
||||
pub struct ContainerQuery {
|
||||
render: Option<Box<dyn FnOnce(Size<Pixels>, &mut Window, &mut App) -> AnyElement>>,
|
||||
style: StyleRefinement,
|
||||
}
|
||||
|
||||
impl Element for ContainerQuery {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = Option<AnyElement>;
|
||||
|
||||
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) {
|
||||
let mut style = Style::default();
|
||||
style.refine(&self.style);
|
||||
let layout_id = window.request_layout(style, [], cx);
|
||||
(layout_id, ())
|
||||
}
|
||||
|
||||
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,
|
||||
) -> Option<AnyElement> {
|
||||
let render = self.render.take()?;
|
||||
let mut child = render(bounds.size, window, cx);
|
||||
child.layout_as_root(bounds.size.map(AvailableSpace::Definite), window, cx);
|
||||
child.prepaint_at(bounds.origin, window, cx);
|
||||
Some(child)
|
||||
}
|
||||
|
||||
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,
|
||||
) {
|
||||
if let Some(child) = prepaint {
|
||||
child.paint(window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for ContainerQuery {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for ContainerQuery {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
use crate::{
|
||||
AnyElement, App, Bounds, Element, GlobalElementId, InspectorElementId, IntoElement, LayoutId,
|
||||
Pixels, Window,
|
||||
};
|
||||
|
||||
/// Builds a `Deferred` element, which delays the layout and paint of its child.
|
||||
pub fn deferred(child: impl IntoElement) -> Deferred {
|
||||
Deferred {
|
||||
child: Some(child.into_any_element()),
|
||||
priority: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// An element which delays the painting of its child until after all of
|
||||
/// its ancestors, while keeping its layout as part of the current element tree.
|
||||
pub struct Deferred {
|
||||
child: Option<AnyElement>,
|
||||
priority: usize,
|
||||
}
|
||||
|
||||
impl Deferred {
|
||||
/// Sets the `priority` value of the `deferred` element, which
|
||||
/// determines the drawing order relative to other deferred elements,
|
||||
/// with higher values being drawn on top.
|
||||
pub fn with_priority(mut self, priority: usize) -> Self {
|
||||
self.priority = priority;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for Deferred {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = ();
|
||||
|
||||
fn id(&self) -> Option<crate::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, ()) {
|
||||
let layout_id = self.child.as_mut().unwrap().request_layout(window, cx);
|
||||
(layout_id, ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) {
|
||||
let child = self.child.take().unwrap();
|
||||
let element_offset = window.element_offset();
|
||||
window.defer_draw(child, element_offset, self.priority, None)
|
||||
}
|
||||
|
||||
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,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for Deferred {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Deferred {
|
||||
/// Sets a priority for the element. A higher priority conceptually means painting the element
|
||||
/// on top of deferred draws with a lower priority (i.e. closer to the viewer).
|
||||
pub fn priority(mut self, priority: usize) -> Self {
|
||||
self.priority = priority;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
Context, Entity, StyleRefinement, TestAppContext, Window, anchored, deferred, div, point,
|
||||
prelude::*, px, size,
|
||||
};
|
||||
|
||||
/// A stand-in for a dock panel hosting a popover (deferred draw) whose
|
||||
/// content opens another popover (a deferred draw created while
|
||||
/// prepainting the first one's content).
|
||||
struct PanelView;
|
||||
|
||||
impl Render for PanelView {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().key_context("Panel").size_full().child(
|
||||
deferred(
|
||||
anchored().position(point(px(10.), px(10.))).child(
|
||||
div().key_context("Popover").w(px(200.)).h(px(200.)).child(
|
||||
deferred(
|
||||
anchored().position(point(px(30.), px(30.))).child(
|
||||
div()
|
||||
.key_context("NestedMenu")
|
||||
.debug_selector(|| "NESTED_MENU".into())
|
||||
.w(px(50.))
|
||||
.h(px(50.)),
|
||||
),
|
||||
)
|
||||
.with_priority(2),
|
||||
),
|
||||
),
|
||||
)
|
||||
.with_priority(1),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct RootView {
|
||||
panel: Entity<PanelView>,
|
||||
}
|
||||
|
||||
impl Render for RootView {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().key_context("Root").size_full().child(
|
||||
self.panel
|
||||
.clone()
|
||||
.cached(StyleRefinement::default().size_full()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Regression test for a crash with nested deferred draws (e.g. a popover
|
||||
/// menu inside a popover hosted by a cached dock panel). Prepaint indices
|
||||
/// recorded during the deferred draw rounds must index the same
|
||||
/// `deferred_draws` vector that `reuse_prepaint` slices on the next frame;
|
||||
/// previously they were measured against a transient per-round vector, so
|
||||
/// reusing the panel's subtree grafted the wrong deferred draws and
|
||||
/// panicked in the dispatch tree.
|
||||
#[gpui::test]
|
||||
fn test_nested_deferred_draws_with_reused_views(cx: &mut TestAppContext) {
|
||||
let window = cx.open_window(size(px(800.), px(600.)), |_, cx| {
|
||||
let panel = cx.new(|_| PanelView);
|
||||
RootView { panel }
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
let menu_bounds = window
|
||||
.update(cx, |_, window, _| {
|
||||
window
|
||||
.rendered_frame
|
||||
.debug_bounds
|
||||
.get("NESTED_MENU")
|
||||
.copied()
|
||||
})
|
||||
.unwrap()
|
||||
.expect("NESTED_MENU debug bounds not found");
|
||||
assert_eq!(menu_bounds.size, size(px(50.), px(50.)));
|
||||
|
||||
// Re-render only the root view; the panel is cached, so its subtree -
|
||||
// including both deferred draw records - is reused from the previous
|
||||
// frame.
|
||||
window.update(cx, |_, _, cx| cx.notify()).unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Reuse the subtree a second time, exercising ranges that were
|
||||
// themselves recorded during a reused frame.
|
||||
window.update(cx, |_, _, cx| cx.notify()).unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Re-render the panel itself again to prove the popovers still draw.
|
||||
window
|
||||
.update(cx, |root, _, cx| {
|
||||
root.panel.update(cx, |_, cx| cx.notify());
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
window
|
||||
.update(cx, |_, window, _| {
|
||||
assert_eq!(window.rendered_frame.deferred_draws.len(), 2);
|
||||
assert!(
|
||||
window
|
||||
.rendered_frame
|
||||
.debug_bounds
|
||||
.contains_key("NESTED_MENU")
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
Vendored
+5640
File diff suppressed because it is too large.
Load diff
+330
@@ -0,0 +1,330 @@
|
||||
use crate::{
|
||||
AnyElement, AnyEntity, App, AppContext, Asset, AssetLogger, Bounds, Element, ElementId, Entity,
|
||||
GlobalElementId, ImageAssetLoader, ImageCacheError, InspectorElementId, IntoElement, LayoutId,
|
||||
ParentElement, Pixels, RenderImage, Resource, Style, StyleRefinement, Styled, Window, hash,
|
||||
};
|
||||
|
||||
use crate::asset_cache::CachedLoad;
|
||||
use refineable::Refineable;
|
||||
use smallvec::SmallVec;
|
||||
use std::{collections::HashMap, fmt, sync::Arc};
|
||||
|
||||
/// An image cache element, all its child img elements will use the cache specified by this element.
|
||||
/// Note that this could as simple as passing an `Entity<T: ImageCache>`
|
||||
pub fn image_cache(image_cache_provider: impl ImageCacheProvider) -> ImageCacheElement {
|
||||
ImageCacheElement {
|
||||
image_cache_provider: Box::new(image_cache_provider),
|
||||
style: StyleRefinement::default(),
|
||||
children: SmallVec::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A dynamically typed image cache, which can be used to store any image cache
|
||||
#[derive(Clone)]
|
||||
pub struct AnyImageCache {
|
||||
image_cache: AnyEntity,
|
||||
load_fn: fn(
|
||||
image_cache: &AnyEntity,
|
||||
resource: &Resource,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<Result<Arc<RenderImage>, ImageCacheError>>,
|
||||
}
|
||||
|
||||
impl<I: ImageCache> From<Entity<I>> for AnyImageCache {
|
||||
fn from(image_cache: Entity<I>) -> Self {
|
||||
Self {
|
||||
image_cache: image_cache.into_any(),
|
||||
load_fn: any_image_cache::load::<I>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AnyImageCache {
|
||||
/// Load an image given a resource
|
||||
/// returns the result of loading the image if it has finished loading, or None if it is still loading
|
||||
pub fn load(
|
||||
&self,
|
||||
resource: &Resource,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
|
||||
(self.load_fn)(&self.image_cache, resource, window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
mod any_image_cache {
|
||||
use super::*;
|
||||
|
||||
pub(crate) fn load<I: 'static + ImageCache>(
|
||||
image_cache: &AnyEntity,
|
||||
resource: &Resource,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
|
||||
let image_cache = image_cache.clone().downcast::<I>().unwrap();
|
||||
image_cache.update(cx, |image_cache, cx| image_cache.load(resource, window, cx))
|
||||
}
|
||||
}
|
||||
|
||||
/// An image cache element.
|
||||
pub struct ImageCacheElement {
|
||||
image_cache_provider: Box<dyn ImageCacheProvider>,
|
||||
style: StyleRefinement,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl ParentElement for ImageCacheElement {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for ImageCacheElement {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for ImageCacheElement {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for ImageCacheElement {
|
||||
type RequestLayoutState = SmallVec<[LayoutId; 4]>;
|
||||
type PrepaintState = ();
|
||||
|
||||
fn id(&self) -> Option<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) {
|
||||
let image_cache = self.image_cache_provider.provide(window, cx);
|
||||
window.with_image_cache(Some(image_cache), |window| {
|
||||
let child_layout_ids = self
|
||||
.children
|
||||
.iter_mut()
|
||||
.map(|child| child.request_layout(window, cx))
|
||||
.collect::<SmallVec<_>>();
|
||||
let mut style = Style::default();
|
||||
style.refine(&self.style);
|
||||
let layout_id = window.request_layout(style, child_layout_ids.iter().copied(), cx);
|
||||
(layout_id, child_layout_ids)
|
||||
})
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self::PrepaintState {
|
||||
for child in &mut self.children {
|
||||
child.prepaint(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
_prepaint: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let image_cache = self.image_cache_provider.provide(window, cx);
|
||||
window.with_image_cache(Some(image_cache), |window| {
|
||||
for child in &mut self.children {
|
||||
child.paint(window, cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Owns an image load and its cached result.
|
||||
///
|
||||
/// Dropping the entry cancels a pending load. Image caches must remove loaded
|
||||
/// images from windows before discarding the entry.
|
||||
pub struct ImageCacheItem(CachedLoad<Result<Arc<RenderImage>, ImageCacheError>>);
|
||||
|
||||
impl std::fmt::Debug for ImageCacheItem {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("ImageCacheItem")
|
||||
.field("result", &self.get())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl ImageCacheItem {
|
||||
/// Starts loading an image into a new cache entry.
|
||||
pub fn new(source: &Resource, cx: &mut App) -> Self {
|
||||
let future = AssetLogger::<ImageAssetLoader>::load(source.clone(), cx);
|
||||
Self(CachedLoad::new(future, cx))
|
||||
}
|
||||
|
||||
/// Returns the cached result without subscribing to completion notifications.
|
||||
pub fn get(&self) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
|
||||
self.0.get()
|
||||
}
|
||||
|
||||
/// Returns the cached result or subscribes the current view to completion.
|
||||
pub fn use_image(&self, window: &Window) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
|
||||
self.0.use_by(window.current_view())
|
||||
}
|
||||
}
|
||||
|
||||
/// An object that can handle the caching and unloading of images.
|
||||
/// Implementations of this trait should ensure that images are removed from all windows when they are no longer needed.
|
||||
pub trait ImageCache: 'static {
|
||||
/// Load an image given a resource
|
||||
/// returns the result of loading the image if it has finished loading, or None if it is still loading
|
||||
fn load(
|
||||
&mut self,
|
||||
resource: &Resource,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<Result<Arc<RenderImage>, ImageCacheError>>;
|
||||
}
|
||||
|
||||
/// An object that can create an ImageCache during the render phase.
|
||||
/// See the ImageCache trait for more information.
|
||||
pub trait ImageCacheProvider: 'static {
|
||||
/// Called during the request_layout phase to create an ImageCache.
|
||||
fn provide(&mut self, _window: &mut Window, _cx: &mut App) -> AnyImageCache;
|
||||
}
|
||||
|
||||
impl<T: ImageCache> ImageCacheProvider for Entity<T> {
|
||||
fn provide(&mut self, _window: &mut Window, _cx: &mut App) -> AnyImageCache {
|
||||
self.clone().into()
|
||||
}
|
||||
}
|
||||
|
||||
/// An implementation of ImageCache, that uses an LRU caching strategy to unload images when the cache is full
|
||||
pub struct RetainAllImageCache(HashMap<u64, ImageCacheItem>);
|
||||
|
||||
impl fmt::Debug for RetainAllImageCache {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("HashMapImageCache")
|
||||
.field("num_images", &self.0.len())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl RetainAllImageCache {
|
||||
/// Create a new image cache.
|
||||
#[inline]
|
||||
pub fn new(cx: &mut App) -> Entity<Self> {
|
||||
let e = cx.new(|_cx| RetainAllImageCache(HashMap::new()));
|
||||
cx.observe_release(&e, |image_cache, cx| {
|
||||
for (_, item) in std::mem::replace(&mut image_cache.0, HashMap::new()) {
|
||||
if let Some(Ok(image)) = item.get() {
|
||||
cx.drop_image(image, None);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
e
|
||||
}
|
||||
|
||||
/// Load an image from the given source.
|
||||
///
|
||||
/// Returns `None` if the image is loading.
|
||||
pub fn load(
|
||||
&mut self,
|
||||
source: &Resource,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
|
||||
let hash = hash(source);
|
||||
|
||||
self.0
|
||||
.entry(hash)
|
||||
.or_insert_with(|| ImageCacheItem::new(source, cx))
|
||||
.use_image(window)
|
||||
}
|
||||
|
||||
/// Clear the image cache.
|
||||
pub fn clear(&mut self, window: &mut Window, cx: &mut App) {
|
||||
for (_, item) in std::mem::replace(&mut self.0, HashMap::new()) {
|
||||
if let Some(Ok(image)) = item.get() {
|
||||
cx.drop_image(image, Some(window));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the image from the cache by the given source.
|
||||
pub fn remove(&mut self, source: &Resource, window: &mut Window, cx: &mut App) {
|
||||
let hash = hash(source);
|
||||
if let Some(item) = self.0.remove(&hash)
|
||||
&& let Some(Ok(image)) = item.get()
|
||||
{
|
||||
cx.drop_image(image, Some(window));
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of images in the cache.
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
/// Returns true if the cache is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl ImageCache for RetainAllImageCache {
|
||||
fn load(
|
||||
&mut self,
|
||||
resource: &Resource,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
|
||||
RetainAllImageCache::load(self, resource, window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a retain-all image cache that uses the element state associated with the given ID.
|
||||
pub fn retain_all(id: impl Into<ElementId>) -> RetainAllImageCacheProvider {
|
||||
RetainAllImageCacheProvider { id: id.into() }
|
||||
}
|
||||
|
||||
/// A provider struct for creating a retain-all image cache inline
|
||||
pub struct RetainAllImageCacheProvider {
|
||||
id: ElementId,
|
||||
}
|
||||
|
||||
impl ImageCacheProvider for RetainAllImageCacheProvider {
|
||||
fn provide(&mut self, window: &mut Window, cx: &mut App) -> AnyImageCache {
|
||||
window
|
||||
.with_global_id(self.id.clone(), |global_id, window| {
|
||||
window.with_element_state::<Entity<RetainAllImageCache>, _>(
|
||||
global_id,
|
||||
|cache, _window| {
|
||||
let mut cache = cache.unwrap_or_else(|| RetainAllImageCache::new(cx));
|
||||
(cache.clone(), cache)
|
||||
},
|
||||
)
|
||||
})
|
||||
.into()
|
||||
}
|
||||
}
|
||||
Vendored
+983
@@ -0,0 +1,983 @@
|
||||
use crate::{
|
||||
AnyElement, AnyImageCache, App, Asset, AssetLogger, Bounds, DefiniteLength, Element, ElementId,
|
||||
Entity, GlobalElementId, Hitbox, Image, ImageCache, InspectorElementId, InteractiveElement,
|
||||
Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource,
|
||||
SharedString, SharedUri, StyleRefinement, Styled, Task, Window, decode_static_image,
|
||||
decode_static_image_from_decoder, px,
|
||||
};
|
||||
use anyhow::Result;
|
||||
|
||||
use futures::Future;
|
||||
use gpui_util::ResultExt;
|
||||
use image::{
|
||||
AnimationDecoder, ImageError, ImageFormat, Rgba,
|
||||
codecs::{gif::GifDecoder, webp::WebPDecoder},
|
||||
};
|
||||
use scheduler::Instant;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
fs,
|
||||
io::{self, Cursor},
|
||||
ops::{Deref, DerefMut},
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
use super::{Stateful, StatefulInteractiveElement};
|
||||
|
||||
/// The delay before showing the loading state.
|
||||
pub const LOADING_DELAY: Duration = Duration::from_millis(200);
|
||||
|
||||
/// A type alias to the resource loader that the `img()` element uses.
|
||||
///
|
||||
/// Note: that this is only for Resources, like URLs or file paths.
|
||||
/// Custom loaders, or external images will not use this asset loader
|
||||
pub type ImgResourceLoader = AssetLogger<ImageAssetLoader>;
|
||||
|
||||
/// A source of image content.
|
||||
#[derive(Clone)]
|
||||
pub enum ImageSource {
|
||||
/// The image content will be loaded from some resource location
|
||||
Resource(Resource),
|
||||
/// Cached image data
|
||||
Render(Arc<RenderImage>),
|
||||
/// Cached image data
|
||||
Image(Arc<Image>),
|
||||
/// A custom loading function to use
|
||||
Custom(Arc<dyn Fn(&mut Window, &mut App) -> Option<Result<Arc<RenderImage>, ImageCacheError>>>),
|
||||
}
|
||||
|
||||
fn is_uri(uri: &str) -> bool {
|
||||
url::Url::from_str(uri).is_ok()
|
||||
}
|
||||
|
||||
impl From<SharedUri> for ImageSource {
|
||||
fn from(value: SharedUri) -> Self {
|
||||
Self::Resource(Resource::Uri(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for ImageSource {
|
||||
fn from(s: &'a str) -> Self {
|
||||
if is_uri(s) {
|
||||
Self::Resource(Resource::Uri(s.to_string().into()))
|
||||
} else {
|
||||
Self::Resource(Resource::Embedded(s.to_string().into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ImageSource {
|
||||
fn from(s: String) -> Self {
|
||||
if is_uri(&s) {
|
||||
Self::Resource(Resource::Uri(s.into()))
|
||||
} else {
|
||||
Self::Resource(Resource::Embedded(s.into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SharedString> for ImageSource {
|
||||
fn from(s: SharedString) -> Self {
|
||||
s.as_ref().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Path> for ImageSource {
|
||||
fn from(value: &Path) -> Self {
|
||||
Self::Resource(value.to_path_buf().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Arc<Path>> for ImageSource {
|
||||
fn from(value: Arc<Path>) -> Self {
|
||||
Self::Resource(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PathBuf> for ImageSource {
|
||||
fn from(value: PathBuf) -> Self {
|
||||
Self::Resource(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Arc<RenderImage>> for ImageSource {
|
||||
fn from(value: Arc<RenderImage>) -> Self {
|
||||
Self::Render(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Arc<Image>> for ImageSource {
|
||||
fn from(value: Arc<Image>) -> Self {
|
||||
Self::Image(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> From<F> for ImageSource
|
||||
where
|
||||
F: Fn(&mut Window, &mut App) -> Option<Result<Arc<RenderImage>, ImageCacheError>> + 'static,
|
||||
{
|
||||
fn from(value: F) -> Self {
|
||||
Self::Custom(Arc::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
/// The style of an image element.
|
||||
pub struct ImageStyle {
|
||||
grayscale: bool,
|
||||
object_fit: ObjectFit,
|
||||
loading: Option<Box<dyn Fn() -> AnyElement>>,
|
||||
fallback: Option<Box<dyn Fn() -> AnyElement>>,
|
||||
}
|
||||
|
||||
impl Default for ImageStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
grayscale: false,
|
||||
object_fit: ObjectFit::Contain,
|
||||
loading: None,
|
||||
fallback: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Style an image element.
|
||||
pub trait StyledImage: Sized {
|
||||
/// Get a mutable [ImageStyle] from the element.
|
||||
fn image_style(&mut self) -> &mut ImageStyle;
|
||||
|
||||
/// Set the image to be displayed in grayscale.
|
||||
fn grayscale(mut self, grayscale: bool) -> Self {
|
||||
self.image_style().grayscale = grayscale;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the object fit for the image.
|
||||
fn object_fit(mut self, object_fit: ObjectFit) -> Self {
|
||||
self.image_style().object_fit = object_fit;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a fallback function that will be invoked to render an error view should
|
||||
/// the image fail to load.
|
||||
fn with_fallback(mut self, fallback: impl Fn() -> AnyElement + 'static) -> Self {
|
||||
self.image_style().fallback = Some(Box::new(fallback));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a fallback function that will be invoked to render a view while the image
|
||||
/// is still being loaded.
|
||||
fn with_loading(mut self, loading: impl Fn() -> AnyElement + 'static) -> Self {
|
||||
self.image_style().loading = Some(Box::new(loading));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl StyledImage for Img {
|
||||
fn image_style(&mut self) -> &mut ImageStyle {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl StyledImage for Stateful<Img> {
|
||||
fn image_style(&mut self) -> &mut ImageStyle {
|
||||
&mut self.element.style
|
||||
}
|
||||
}
|
||||
|
||||
/// An image element.
|
||||
pub struct Img {
|
||||
interactivity: Interactivity,
|
||||
source: ImageSource,
|
||||
style: ImageStyle,
|
||||
image_cache: Option<AnyImageCache>,
|
||||
}
|
||||
|
||||
/// Create a new image element.
|
||||
#[track_caller]
|
||||
pub fn img(source: impl Into<ImageSource>) -> Img {
|
||||
Img {
|
||||
interactivity: Interactivity::new(),
|
||||
source: source.into(),
|
||||
style: ImageStyle::default(),
|
||||
image_cache: None,
|
||||
}
|
||||
}
|
||||
|
||||
impl Img {
|
||||
/// A list of all format extensions currently supported by this img element
|
||||
pub fn extensions() -> &'static [&'static str] {
|
||||
// This is the list in [image::ImageFormat::from_extension] + `svg`
|
||||
&[
|
||||
"avif", "jpg", "jpeg", "png", "gif", "webp", "tif", "tiff", "tga", "dds", "bmp", "ico",
|
||||
"hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg",
|
||||
]
|
||||
}
|
||||
|
||||
/// Sets the image cache for the current node.
|
||||
///
|
||||
/// If the `image_cache` is not explicitly provided, the function will determine the image cache by:
|
||||
///
|
||||
/// 1. Checking if any ancestor node of the current node contains an `ImageCacheElement`, If such a node exists, the image cache specified by that ancestor will be used.
|
||||
/// 2. If no ancestor node contains an `ImageCacheElement`, the global image cache will be used as a fallback.
|
||||
///
|
||||
/// This mechanism provides a flexible way to manage image caching, allowing precise control when needed,
|
||||
/// while ensuring a default behavior when no cache is explicitly specified.
|
||||
#[inline]
|
||||
pub fn image_cache<I: ImageCache>(self, image_cache: &Entity<I>) -> Self {
|
||||
Self {
|
||||
image_cache: Some(image_cache.clone().into()),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Stateful<Img> {
|
||||
type Target = Img;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.element
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Stateful<Img> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.element
|
||||
}
|
||||
}
|
||||
|
||||
/// The image state between frames
|
||||
struct ImgState {
|
||||
frame_index: usize,
|
||||
last_frame_time: Option<Instant>,
|
||||
started_loading: Option<(Instant, Task<()>)>,
|
||||
}
|
||||
|
||||
/// The image layout state between frames
|
||||
pub struct ImgLayoutState {
|
||||
frame_index: usize,
|
||||
replacement: Option<AnyElement>,
|
||||
}
|
||||
|
||||
impl Element for Img {
|
||||
type RequestLayoutState = ImgLayoutState;
|
||||
type PrepaintState = Option<Hitbox>;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
self.interactivity.element_id.clone()
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
|
||||
self.interactivity.source_location()
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let mut layout_state = ImgLayoutState {
|
||||
frame_index: 0,
|
||||
replacement: None,
|
||||
};
|
||||
|
||||
window.with_optional_element_state(global_id, |state, window| {
|
||||
let mut state = state.map(|state| {
|
||||
state.unwrap_or(ImgState {
|
||||
frame_index: 0,
|
||||
last_frame_time: None,
|
||||
started_loading: None,
|
||||
})
|
||||
});
|
||||
|
||||
let mut frame_index = state.as_ref().map(|state| state.frame_index).unwrap_or(0);
|
||||
|
||||
let layout_id = self.interactivity.request_layout(
|
||||
global_id,
|
||||
inspector_id,
|
||||
window,
|
||||
cx,
|
||||
|mut style, window, cx| {
|
||||
let mut replacement_id = None;
|
||||
|
||||
match self.source.use_data(
|
||||
self.image_cache
|
||||
.clone()
|
||||
.or_else(|| window.image_cache_stack.last().cloned()),
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
Some(Ok(data)) => {
|
||||
let frame_count = data.frame_count();
|
||||
let max_frame_index = frame_count.saturating_sub(1);
|
||||
|
||||
if let Some(state) = &mut state {
|
||||
state.frame_index = state.frame_index.min(max_frame_index);
|
||||
if frame_count > 1 && !cx.reduce_motion() {
|
||||
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 = None;
|
||||
}
|
||||
} else {
|
||||
state.last_frame_time = None;
|
||||
}
|
||||
state.started_loading = None;
|
||||
frame_index = state.frame_index;
|
||||
}
|
||||
|
||||
let image_size = data.render_size(frame_index);
|
||||
|
||||
if style.aspect_ratio.is_none() {
|
||||
style.aspect_ratio = Some(image_size.width / image_size.height);
|
||||
}
|
||||
|
||||
if let Length::Auto = style.size.width {
|
||||
style.size.width = match style.size.height {
|
||||
Length::Definite(DefiniteLength::Absolute(abs_length)) => {
|
||||
let height_px = abs_length.to_pixels(window.rem_size());
|
||||
Length::Definite(
|
||||
px(image_size.width.0 * height_px.0
|
||||
/ image_size.height.0)
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
_ => Length::Definite(image_size.width.into()),
|
||||
};
|
||||
}
|
||||
|
||||
if let Length::Auto = style.size.height {
|
||||
style.size.height = match style.size.width {
|
||||
Length::Definite(DefiniteLength::Absolute(abs_length)) => {
|
||||
let width_px = abs_length.to_pixels(window.rem_size());
|
||||
Length::Definite(
|
||||
px(image_size.height.0 * width_px.0
|
||||
/ image_size.width.0)
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
_ => Length::Definite(image_size.height.into()),
|
||||
};
|
||||
}
|
||||
|
||||
if global_id.is_some()
|
||||
&& data.frame_count() > 1
|
||||
&& window.is_window_active()
|
||||
&& !cx.reduce_motion()
|
||||
{
|
||||
window.request_animation_frame();
|
||||
}
|
||||
}
|
||||
Some(_err) => {
|
||||
if let Some(fallback) = self.style.fallback.as_ref() {
|
||||
let mut element = fallback();
|
||||
replacement_id = Some(element.request_layout(window, cx));
|
||||
layout_state.replacement = Some(element);
|
||||
}
|
||||
if let Some(state) = &mut state {
|
||||
state.started_loading = None;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if let Some(state) = &mut state {
|
||||
if let Some((started_loading, _)) = state.started_loading {
|
||||
if started_loading.elapsed() > LOADING_DELAY
|
||||
&& let Some(loading) = self.style.loading.as_ref()
|
||||
{
|
||||
let mut element = loading();
|
||||
replacement_id = Some(element.request_layout(window, cx));
|
||||
layout_state.replacement = Some(element);
|
||||
}
|
||||
} else {
|
||||
let current_view = window.current_view();
|
||||
let task = window.spawn(cx, async move |cx| {
|
||||
cx.background_executor().timer(LOADING_DELAY).await;
|
||||
cx.update(move |_, cx| {
|
||||
cx.notify(current_view);
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
state.started_loading = Some((Instant::now(), task));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.request_layout(style, replacement_id, cx)
|
||||
},
|
||||
);
|
||||
|
||||
layout_state.frame_index = frame_index;
|
||||
|
||||
((layout_id, layout_state), state)
|
||||
})
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self::PrepaintState {
|
||||
self.interactivity.prepaint(
|
||||
global_id,
|
||||
inspector_id,
|
||||
bounds,
|
||||
bounds.size,
|
||||
window,
|
||||
cx,
|
||||
|_, _, hitbox, window, cx| {
|
||||
if let Some(replacement) = &mut request_layout.replacement {
|
||||
replacement.prepaint(window, cx);
|
||||
}
|
||||
|
||||
hitbox
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
layout_state: &mut Self::RequestLayoutState,
|
||||
hitbox: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let source = self.source.clone();
|
||||
self.interactivity.paint(
|
||||
global_id,
|
||||
inspector_id,
|
||||
bounds,
|
||||
hitbox.as_ref(),
|
||||
window,
|
||||
cx,
|
||||
|style, window, cx| {
|
||||
if let Some(Ok(data)) = source.use_data(
|
||||
self.image_cache
|
||||
.clone()
|
||||
.or_else(|| window.image_cache_stack.last().cloned()),
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
if data.frame_count() == 0 {
|
||||
return;
|
||||
}
|
||||
let new_bounds = self
|
||||
.style
|
||||
.object_fit
|
||||
.get_bounds(bounds, data.size(layout_state.frame_index));
|
||||
let corner_radii = style.corner_radii.to_pixels(window.rem_size());
|
||||
window
|
||||
.paint_image(
|
||||
bounds,
|
||||
new_bounds,
|
||||
corner_radii,
|
||||
data,
|
||||
layout_state.frame_index,
|
||||
self.style.grayscale,
|
||||
)
|
||||
.log_err();
|
||||
} else if let Some(replacement) = &mut layout_state.replacement {
|
||||
replacement.paint(window, cx);
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Img {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.interactivity.base_style
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveElement for Img {
|
||||
fn interactivity(&mut self) -> &mut Interactivity {
|
||||
&mut self.interactivity
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for Img {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl StatefulInteractiveElement for Img {}
|
||||
|
||||
impl ImageSource {
|
||||
pub(crate) fn use_data(
|
||||
&self,
|
||||
cache: Option<AnyImageCache>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
|
||||
match self {
|
||||
ImageSource::Resource(resource) => {
|
||||
if let Some(cache) = cache {
|
||||
cache.load(resource, window, cx)
|
||||
} else {
|
||||
window.use_asset::<ImgResourceLoader>(resource, cx)
|
||||
}
|
||||
}
|
||||
ImageSource::Custom(loading_fn) => loading_fn(window, cx),
|
||||
ImageSource::Render(data) => Some(Ok(data.to_owned())),
|
||||
ImageSource::Image(data) => window.use_asset::<AssetLogger<ImageDecoder>>(data, cx),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_data(
|
||||
&self,
|
||||
cache: Option<AnyImageCache>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
|
||||
match self {
|
||||
ImageSource::Resource(resource) => {
|
||||
if let Some(cache) = cache {
|
||||
cache.load(resource, window, cx)
|
||||
} else {
|
||||
window.get_asset::<ImgResourceLoader>(resource, cx)
|
||||
}
|
||||
}
|
||||
ImageSource::Custom(loading_fn) => loading_fn(window, cx),
|
||||
ImageSource::Render(data) => Some(Ok(data.to_owned())),
|
||||
ImageSource::Image(data) => window.get_asset::<AssetLogger<ImageDecoder>>(data, cx),
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove this image source from the asset system
|
||||
pub fn remove_asset(&self, cx: &mut App) {
|
||||
match self {
|
||||
ImageSource::Resource(resource) => {
|
||||
cx.remove_asset::<ImgResourceLoader>(resource);
|
||||
}
|
||||
ImageSource::Custom(_) | ImageSource::Render(_) => {}
|
||||
ImageSource::Image(data) => cx.remove_asset::<AssetLogger<ImageDecoder>>(data),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether this image source is present in the asset system (loading
|
||||
/// or loaded), without fetching it.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn is_asset_cached(&self, cx: &App) -> bool {
|
||||
match self {
|
||||
ImageSource::Resource(resource) => cx.has_asset::<ImgResourceLoader>(resource),
|
||||
ImageSource::Custom(_) | ImageSource::Render(_) => false,
|
||||
ImageSource::Image(data) => cx.has_asset::<AssetLogger<ImageDecoder>>(data),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum ImageDecoder {}
|
||||
|
||||
impl Asset for ImageDecoder {
|
||||
type Source = Arc<Image>;
|
||||
type Output = Result<Arc<RenderImage>, ImageCacheError>;
|
||||
|
||||
fn load(
|
||||
source: Self::Source,
|
||||
cx: &mut App,
|
||||
) -> impl Future<Output = Self::Output> + Send + 'static {
|
||||
let renderer = cx.svg_renderer();
|
||||
async move { source.to_image_data(renderer).map_err(Into::into) }
|
||||
}
|
||||
}
|
||||
|
||||
/// An image loader for the GPUI asset system
|
||||
#[derive(Clone)]
|
||||
pub enum ImageAssetLoader {}
|
||||
|
||||
impl Asset for ImageAssetLoader {
|
||||
type Source = Resource;
|
||||
type Output = Result<Arc<RenderImage>, ImageCacheError>;
|
||||
|
||||
fn load(
|
||||
source: Self::Source,
|
||||
cx: &mut App,
|
||||
) -> impl Future<Output = Self::Output> + Send + 'static {
|
||||
let client = cx.http_client();
|
||||
// TODO: Can we make SVGs always rescale?
|
||||
// let scale_factor = cx.scale_factor();
|
||||
let svg_renderer = cx.svg_renderer();
|
||||
let asset_source = cx.asset_source().clone();
|
||||
async move {
|
||||
let bytes = match source.clone() {
|
||||
Resource::Path(uri) => fs::read(uri.as_ref())?,
|
||||
Resource::Uri(uri) => {
|
||||
use anyhow::Context as _;
|
||||
use futures::AsyncReadExt as _;
|
||||
|
||||
let mut response = client
|
||||
.get(uri.as_ref(), ().into(), true)
|
||||
.await
|
||||
.with_context(|| format!("loading image asset from {uri:?}"))?;
|
||||
let mut body = Vec::new();
|
||||
response.body_mut().read_to_end(&mut body).await?;
|
||||
if !response.status().is_success() {
|
||||
let mut body = String::from_utf8_lossy(&body).into_owned();
|
||||
let first_line = body.lines().next().unwrap_or("").trim_end();
|
||||
body.truncate(first_line.len());
|
||||
return Err(ImageCacheError::BadStatus {
|
||||
uri,
|
||||
status: response.status(),
|
||||
body,
|
||||
});
|
||||
}
|
||||
body
|
||||
}
|
||||
Resource::Embedded(path) => {
|
||||
let data = asset_source.load(&path).ok().flatten();
|
||||
if let Some(data) = data {
|
||||
data.to_vec()
|
||||
} else {
|
||||
return Err(ImageCacheError::Asset(
|
||||
format!("Embedded resource not found: {}", path).into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Ok(format) = image::guess_format(&bytes) {
|
||||
let data = match format {
|
||||
ImageFormat::Gif => {
|
||||
let decoder = GifDecoder::new(Cursor::new(&bytes))?;
|
||||
let mut frames = SmallVec::new();
|
||||
|
||||
for frame in decoder.into_frames() {
|
||||
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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if frames.is_empty() {
|
||||
return Err(ImageCacheError::Other(Arc::new(anyhow::anyhow!(
|
||||
"GIF could not be decoded: all frames failed ({source:?})"
|
||||
))));
|
||||
}
|
||||
|
||||
frames
|
||||
}
|
||||
ImageFormat::WebP => {
|
||||
let mut decoder = WebPDecoder::new(Cursor::new(&bytes))?;
|
||||
|
||||
if decoder.has_animation() {
|
||||
let _ = decoder.set_background_color(Rgba([0, 0, 0, 0]));
|
||||
let mut frames = SmallVec::new();
|
||||
|
||||
for frame in decoder.into_frames() {
|
||||
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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if frames.is_empty() {
|
||||
return Err(ImageCacheError::Other(Arc::new(anyhow::anyhow!(
|
||||
"WebP could not be decoded: all frames failed ({source:?})"
|
||||
))));
|
||||
}
|
||||
|
||||
frames
|
||||
} else {
|
||||
decode_static_image_from_decoder(decoder)?
|
||||
}
|
||||
}
|
||||
_ => decode_static_image(&bytes, format)?,
|
||||
};
|
||||
|
||||
Ok(Arc::new(RenderImage::new(data)))
|
||||
} else {
|
||||
svg_renderer
|
||||
.render_single_frame(&bytes, 1.0)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An error that can occur when interacting with the image cache.
|
||||
#[derive(Debug, Error, Clone)]
|
||||
pub enum ImageCacheError {
|
||||
/// Some other kind of error occurred
|
||||
#[error("error: {0}")]
|
||||
Other(#[from] Arc<anyhow::Error>),
|
||||
/// An error that occurred while reading the image from disk.
|
||||
#[error("IO error: {0}")]
|
||||
Io(Arc<std::io::Error>),
|
||||
/// An error that occurred while processing an image.
|
||||
#[error("unexpected http status for {uri}: {status}, body: {body}")]
|
||||
BadStatus {
|
||||
/// The URI of the image.
|
||||
uri: SharedUri,
|
||||
/// The HTTP status code.
|
||||
status: http_client::StatusCode,
|
||||
/// The HTTP response body.
|
||||
body: String,
|
||||
},
|
||||
/// An error that occurred while processing an asset.
|
||||
#[error("asset error: {0}")]
|
||||
Asset(SharedString),
|
||||
/// An error that occurred while processing an image.
|
||||
#[error("image error: {0}")]
|
||||
Image(Arc<ImageError>),
|
||||
/// An error that occurred while processing an SVG.
|
||||
#[error("svg error: {0}")]
|
||||
Usvg(Arc<usvg::Error>),
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for ImageCacheError {
|
||||
fn from(value: anyhow::Error) -> Self {
|
||||
Self::Other(Arc::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for ImageCacheError {
|
||||
fn from(value: io::Error) -> Self {
|
||||
Self::Io(Arc::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<usvg::Error> for ImageCacheError {
|
||||
fn from(value: usvg::Error) -> Self {
|
||||
Self::Usvg(Arc::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<image::ImageError> for ImageCacheError {
|
||||
fn from(value: image::ImageError) -> Self {
|
||||
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()),
|
||||
)))
|
||||
}
|
||||
|
||||
fn test_image_with_size(width: u32, height: u32) -> Arc<RenderImage> {
|
||||
let frame = Frame::new(ImageBuffer::from_pixel(width, height, Rgba([0, 0, 0, 0])));
|
||||
Arc::new(RenderImage::new(SmallVec::from_elem(frame, 1)))
|
||||
}
|
||||
|
||||
/// 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 image_object_fit_cover_crops_to_element_bounds(cx: &mut TestAppContext) {
|
||||
let window = cx.add_empty_window();
|
||||
let image = test_image_with_size(200, 100);
|
||||
window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
|
||||
img(ImageSource::Render(image.clone()))
|
||||
.size_full()
|
||||
.object_fit(ObjectFit::Fill)
|
||||
.into_any_element()
|
||||
});
|
||||
let full_tile_bounds = window.update(|window, _| {
|
||||
window
|
||||
.rendered_frame
|
||||
.scene
|
||||
.polychrome_sprites
|
||||
.last()
|
||||
.expect("fill image should paint a sprite")
|
||||
.tile
|
||||
.bounds
|
||||
});
|
||||
|
||||
window.draw(point(px(10.), px(20.)), size(px(100.), px(100.)), |_, _| {
|
||||
img(ImageSource::Render(image))
|
||||
.size_full()
|
||||
.object_fit(ObjectFit::Cover)
|
||||
.into_any_element()
|
||||
});
|
||||
|
||||
let (rendered_bounds, rendered_tile_bounds, scale_factor) = window.update(|window, _| {
|
||||
let sprite = window
|
||||
.rendered_frame
|
||||
.scene
|
||||
.polychrome_sprites
|
||||
.last()
|
||||
.expect("cover image should paint a sprite");
|
||||
(sprite.bounds, sprite.tile.bounds, window.scale_factor())
|
||||
});
|
||||
assert_eq!(
|
||||
rendered_bounds,
|
||||
Bounds {
|
||||
origin: point(px(10.).scale(scale_factor), px(20.).scale(scale_factor)),
|
||||
size: size(px(100.).scale(scale_factor), px(100.).scale(scale_factor)),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
(
|
||||
rendered_tile_bounds.origin.x.0 - full_tile_bounds.origin.x.0,
|
||||
rendered_tile_bounds.origin.y.0 - full_tile_bounds.origin.y.0,
|
||||
rendered_tile_bounds.size.width.0,
|
||||
rendered_tile_bounds.size.height.0,
|
||||
),
|
||||
(50, 0, 100, 100),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn explicit_aspect_ratio_is_not_overridden_by_intrinsic_ratio(cx: &mut TestAppContext) {
|
||||
let window = cx.add_empty_window();
|
||||
|
||||
// A portrait image in a square container
|
||||
window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
|
||||
div()
|
||||
.size(px(100.))
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
img(ImageSource::Render(test_image_with_size(100, 200)))
|
||||
.size_full()
|
||||
.aspect_square()
|
||||
.object_fit(ObjectFit::Contain),
|
||||
)
|
||||
.into_any_element()
|
||||
});
|
||||
|
||||
let (rendered_bounds, scale_factor) = window.update(|window, _| {
|
||||
let sprite = window
|
||||
.rendered_frame
|
||||
.scene
|
||||
.polychrome_sprites
|
||||
.last()
|
||||
.expect("contained image should paint a sprite");
|
||||
(sprite.bounds, window.scale_factor())
|
||||
});
|
||||
|
||||
// The element stays 100x100, so the image is letterboxed horizontally
|
||||
assert_eq!(
|
||||
rendered_bounds,
|
||||
Bounds {
|
||||
origin: point(px(25.).scale(scale_factor), px(0.).scale(scale_factor)),
|
||||
size: size(px(50.).scale(scale_factor), px(100.).scale(scale_factor)),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn image_object_fit_cover_clamps_corner_radii_to_visible_bounds(cx: &mut TestAppContext) {
|
||||
let window = cx.add_empty_window();
|
||||
window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
|
||||
img(ImageSource::Render(test_image_with_size(200, 100)))
|
||||
.size_full()
|
||||
.rounded(px(100.))
|
||||
.object_fit(ObjectFit::Cover)
|
||||
.into_any_element()
|
||||
});
|
||||
|
||||
let (corner_radius, expected_corner_radius) = window.update(|window, _| {
|
||||
(
|
||||
window
|
||||
.rendered_frame
|
||||
.scene
|
||||
.polychrome_sprites
|
||||
.last()
|
||||
.map(|sprite| sprite.corner_radii.top_left),
|
||||
px(50.).scale(window.scale_factor()),
|
||||
)
|
||||
});
|
||||
assert_eq!(corner_radius, Some(expected_corner_radius));
|
||||
}
|
||||
|
||||
#[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()
|
||||
});
|
||||
}
|
||||
}
|
||||
Vendored
+2973
File diff suppressed because it is too large.
Load diff
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
mod anchored;
|
||||
mod animation;
|
||||
mod canvas;
|
||||
mod container_query;
|
||||
mod deferred;
|
||||
mod div;
|
||||
mod image_cache;
|
||||
mod img;
|
||||
mod list;
|
||||
mod surface;
|
||||
mod svg;
|
||||
mod text;
|
||||
mod uniform_list;
|
||||
|
||||
pub use anchored::*;
|
||||
pub use animation::*;
|
||||
pub use canvas::*;
|
||||
pub use container_query::*;
|
||||
pub use deferred::*;
|
||||
pub use div::*;
|
||||
pub use image_cache::*;
|
||||
pub use img::*;
|
||||
pub use list::*;
|
||||
pub use surface::*;
|
||||
pub use svg::*;
|
||||
pub use text::*;
|
||||
pub use uniform_list::*;
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
use crate::{
|
||||
App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId,
|
||||
ObjectFit, Pixels, Style, StyleRefinement, Styled, Window,
|
||||
};
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
use core_video::pixel_buffer::CVPixelBuffer;
|
||||
use refineable::Refineable;
|
||||
|
||||
/// A source of a surface's content.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum SurfaceSource {
|
||||
/// A CoreVideo image buffer
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
Surface(CVPixelBuffer),
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
impl From<CVPixelBuffer> for SurfaceSource {
|
||||
fn from(value: CVPixelBuffer) -> Self {
|
||||
SurfaceSource::Surface(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// A surface element.
|
||||
pub struct Surface {
|
||||
source: SurfaceSource,
|
||||
object_fit: ObjectFit,
|
||||
style: StyleRefinement,
|
||||
}
|
||||
|
||||
/// Create a new surface element.
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
pub fn surface(source: impl Into<SurfaceSource>) -> Surface {
|
||||
Surface {
|
||||
source: source.into(),
|
||||
object_fit: ObjectFit::Contain,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
impl Surface {
|
||||
/// Set the object fit for the image.
|
||||
pub fn object_fit(mut self, object_fit: ObjectFit) -> Self {
|
||||
self.object_fit = object_fit;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for Surface {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = ();
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_global_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let mut style = Style::default();
|
||||
style.refine(&self.style);
|
||||
let layout_id = window.request_layout(style, [], cx);
|
||||
(layout_id, ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_global_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Self::PrepaintState {
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_global_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
#[cfg_attr(
|
||||
not(any(target_os = "macos", target_os = "ios")),
|
||||
allow(unused_variables)
|
||||
)]
|
||||
bounds: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
_: &mut Self::PrepaintState,
|
||||
#[cfg_attr(
|
||||
not(any(target_os = "macos", target_os = "ios")),
|
||||
allow(unused_variables)
|
||||
)]
|
||||
window: &mut Window,
|
||||
_: &mut App,
|
||||
) {
|
||||
match &self.source {
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
SurfaceSource::Surface(surface) => {
|
||||
let size = crate::size(surface.get_width().into(), surface.get_height().into());
|
||||
let new_bounds = self.object_fit.get_bounds(bounds, size);
|
||||
// TODO: Add support for corner_radii
|
||||
window.paint_surface(new_bounds, surface.clone());
|
||||
}
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for Surface {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Surface {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
Vendored
+303
@@ -0,0 +1,303 @@
|
||||
use std::{
|
||||
fs,
|
||||
hash::{Hash, Hasher},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
App, Asset, Bounds, Element, GlobalElementId, Hitbox, InspectorElementId, InteractiveElement,
|
||||
Interactivity, IntoElement, LayoutId, Pixels, Point, Radians, SharedString, Size,
|
||||
StyleRefinement, Styled, TransformationMatrix, Window, point, px, radians, size,
|
||||
};
|
||||
use gpui_util::ResultExt;
|
||||
|
||||
/// An SVG element.
|
||||
pub struct Svg {
|
||||
interactivity: Interactivity,
|
||||
transformation: Option<Transformation>,
|
||||
path: Option<SharedString>,
|
||||
external_path: Option<SharedString>,
|
||||
data: Option<Arc<[u8]>>,
|
||||
data_path: Option<SharedString>,
|
||||
}
|
||||
|
||||
/// Create a new SVG element.
|
||||
#[track_caller]
|
||||
pub fn svg() -> Svg {
|
||||
Svg {
|
||||
interactivity: Interactivity::new(),
|
||||
transformation: None,
|
||||
path: None,
|
||||
external_path: None,
|
||||
data: None,
|
||||
data_path: None,
|
||||
}
|
||||
}
|
||||
|
||||
impl Svg {
|
||||
/// Set the path to the SVG file for this element.
|
||||
pub fn path(mut self, path: impl Into<SharedString>) -> Self {
|
||||
self.path = Some(path.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the path to the SVG file for this element.
|
||||
pub fn external_path(mut self, path: impl Into<SharedString>) -> Self {
|
||||
self.external_path = Some(path.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the raw SVG data for this element.
|
||||
/// The SVG will be rendered directly from the provided bytes.
|
||||
pub fn data(mut self, data: &[u8]) -> Self {
|
||||
// Generate a unique deterministic path based on the data hash for caching
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
data.hash(&mut hasher);
|
||||
let hash = hasher.finish();
|
||||
let path = SharedString::from(format!("__binary_svg__{}", hash));
|
||||
self.data = Some(Arc::from(data));
|
||||
self.data_path = Some(path);
|
||||
self
|
||||
}
|
||||
|
||||
/// Transform the SVG element with the given transformation.
|
||||
/// Note that this won't effect the hitbox or layout of the element, only the rendering.
|
||||
pub fn with_transformation(mut self, transformation: Transformation) -> Self {
|
||||
self.transformation = Some(transformation);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for Svg {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = Option<Hitbox>;
|
||||
|
||||
fn id(&self) -> Option<crate::ElementId> {
|
||||
self.interactivity.element_id.clone()
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
||||
self.interactivity.source_location()
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let layout_id = self.interactivity.request_layout(
|
||||
global_id,
|
||||
inspector_id,
|
||||
window,
|
||||
cx,
|
||||
|style, window, cx| window.request_layout(style, None, cx),
|
||||
);
|
||||
(layout_id, ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<Hitbox> {
|
||||
self.interactivity.prepaint(
|
||||
global_id,
|
||||
inspector_id,
|
||||
bounds,
|
||||
bounds.size,
|
||||
window,
|
||||
cx,
|
||||
|_, _, hitbox, _, _| hitbox,
|
||||
)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
hitbox: &mut Option<Hitbox>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
self.interactivity.paint(
|
||||
global_id,
|
||||
inspector_id,
|
||||
bounds,
|
||||
hitbox.as_ref(),
|
||||
window,
|
||||
cx,
|
||||
|style, window, cx| {
|
||||
let transformation = self
|
||||
.transformation
|
||||
.as_ref()
|
||||
.map(|transformation| {
|
||||
transformation.into_matrix(bounds.center(), window.scale_factor())
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some((data, path)) = self.data.as_ref().zip(self.data_path.as_ref()) {
|
||||
if let Some(color) = style.text.color {
|
||||
window
|
||||
.paint_svg(
|
||||
bounds,
|
||||
path.clone(),
|
||||
Some(&**data),
|
||||
transformation,
|
||||
color,
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
}
|
||||
} else if let Some((path, color)) =
|
||||
self.external_path.as_ref().zip(style.text.color)
|
||||
{
|
||||
let Some(bytes) = window
|
||||
.use_asset::<SvgAsset>(path, cx)
|
||||
.and_then(|asset| asset.log_err())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
window
|
||||
.paint_svg(
|
||||
bounds,
|
||||
path.clone(),
|
||||
Some(&bytes),
|
||||
transformation,
|
||||
color,
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
} else if let Some((path, color)) = self.path.as_ref().zip(style.text.color) {
|
||||
window
|
||||
.paint_svg(bounds, path.clone(), None, transformation, color, cx)
|
||||
.log_err();
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for Svg {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Svg {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.interactivity.base_style
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveElement for Svg {
|
||||
fn interactivity(&mut self) -> &mut Interactivity {
|
||||
&mut self.interactivity
|
||||
}
|
||||
}
|
||||
|
||||
/// A transformation to apply to an SVG element.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Transformation {
|
||||
scale: Size<f32>,
|
||||
translate: Point<Pixels>,
|
||||
rotate: Radians,
|
||||
}
|
||||
|
||||
impl Default for Transformation {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
scale: size(1.0, 1.0),
|
||||
translate: point(px(0.0), px(0.0)),
|
||||
rotate: radians(0.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Transformation {
|
||||
/// Create a new Transformation with the specified scale along each axis.
|
||||
pub fn scale(scale: Size<f32>) -> Self {
|
||||
Self {
|
||||
scale,
|
||||
translate: point(px(0.0), px(0.0)),
|
||||
rotate: radians(0.0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new Transformation with the specified translation.
|
||||
pub fn translate(translate: Point<Pixels>) -> Self {
|
||||
Self {
|
||||
scale: size(1.0, 1.0),
|
||||
translate,
|
||||
rotate: radians(0.0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new Transformation with the specified rotation in radians.
|
||||
pub fn rotate(rotate: impl Into<Radians>) -> Self {
|
||||
let rotate = rotate.into();
|
||||
Self {
|
||||
scale: size(1.0, 1.0),
|
||||
translate: point(px(0.0), px(0.0)),
|
||||
rotate,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the scaling factor of this transformation.
|
||||
pub fn with_scaling(mut self, scale: Size<f32>) -> Self {
|
||||
self.scale = scale;
|
||||
self
|
||||
}
|
||||
|
||||
/// Update the translation value of this transformation.
|
||||
pub fn with_translation(mut self, translate: Point<Pixels>) -> Self {
|
||||
self.translate = translate;
|
||||
self
|
||||
}
|
||||
|
||||
/// Update the rotation angle of this transformation.
|
||||
pub fn with_rotation(mut self, rotate: impl Into<Radians>) -> Self {
|
||||
self.rotate = rotate.into();
|
||||
self
|
||||
}
|
||||
|
||||
fn into_matrix(self, center: Point<Pixels>, scale_factor: f32) -> TransformationMatrix {
|
||||
//Note: if you read this as a sequence of matrix multiplications, start from the bottom
|
||||
TransformationMatrix::unit()
|
||||
.translate(center.scale(scale_factor) + self.translate.scale(scale_factor))
|
||||
.rotate(self.rotate)
|
||||
.scale(self.scale)
|
||||
.translate(center.scale(-scale_factor))
|
||||
}
|
||||
}
|
||||
|
||||
enum SvgAsset {}
|
||||
|
||||
impl Asset for SvgAsset {
|
||||
type Source = SharedString;
|
||||
type Output = Result<Arc<[u8]>, Arc<std::io::Error>>;
|
||||
|
||||
fn load(
|
||||
source: Self::Source,
|
||||
_cx: &mut App,
|
||||
) -> impl Future<Output = Self::Output> + Send + 'static {
|
||||
async move {
|
||||
let bytes = fs::read(Path::new(source.as_ref())).map_err(|e| Arc::new(e))?;
|
||||
let bytes = Arc::from(bytes);
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loaded 100 of 162 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user