Re-re-fork (#23)
This commit is contained in:
@@ -15,14 +15,4 @@ rustflags = ["-D", "warnings"]
|
||||
[profile.dev]
|
||||
debug = "limited"
|
||||
|
||||
# Use Mold on Linux, because it's faster than GNU ld and LLD.
|
||||
#
|
||||
# We no longer set this in the default `config.toml` so that developers can opt in to Wild, which
|
||||
# is faster than Mold, in their own ~/.cargo/config.toml.
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
@@ -16,5 +16,9 @@ rustflags = [
|
||||
"target-feature=+crt-static", # This fixes the linking issue when compiling livekit on Windows
|
||||
]
|
||||
|
||||
# We need lld to link libwebrtc.a successfully on aarch64-linux
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
|
||||
|
||||
[env]
|
||||
MACOSX_DEPLOYMENT_TARGET = "10.15.7"
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# Nextest configuration for GPUI
|
||||
# https://nexte.st/book/configuration.html
|
||||
|
||||
[profile.default]
|
||||
# Default test settings
|
||||
@@ -1,2 +1,3 @@
|
||||
# Prevent GitHub from displaying comments within JSON files as errors.
|
||||
*.json linguist-language=JSON-with-Comments
|
||||
|
||||
|
||||
@@ -31,3 +31,4 @@ xcuserdata/
|
||||
# Misc
|
||||
**/*.db
|
||||
.build
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"languages": {
|
||||
"Markdown": {
|
||||
"tab_size": 2,
|
||||
},
|
||||
"TOML": {
|
||||
"format_on_save": "off",
|
||||
},
|
||||
"YAML": {
|
||||
"tab_size": 2,
|
||||
},
|
||||
"JSON": {
|
||||
"tab_size": 2,
|
||||
"preferred_line_length": 120,
|
||||
},
|
||||
"JSONC": {
|
||||
"tab_size": 2,
|
||||
"preferred_line_length": 120,
|
||||
},
|
||||
},
|
||||
"hard_tabs": false,
|
||||
"formatter": "auto",
|
||||
"remove_trailing_whitespace_on_save": true,
|
||||
"ensure_final_newline_on_save": true,
|
||||
"file_scan_exclusions": [
|
||||
"**/.git",
|
||||
"**/.svn",
|
||||
"**/.hg",
|
||||
"**/.jj",
|
||||
"**/CVS",
|
||||
"**/.DS_Store",
|
||||
"**/Thumbs.db",
|
||||
"**/target",
|
||||
],
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
[
|
||||
{
|
||||
"label": "clippy",
|
||||
"command": "./script/clippy",
|
||||
"args": [],
|
||||
"allow_concurrent_runs": true,
|
||||
"use_new_terminal": false,
|
||||
},
|
||||
{
|
||||
"label": "cargo check gpui",
|
||||
"command": "cargo",
|
||||
"args": ["check", "--package", "gpui"],
|
||||
"allow_concurrent_runs": true,
|
||||
"use_new_terminal": false,
|
||||
},
|
||||
{
|
||||
"label": "cargo test gpui",
|
||||
"command": "cargo",
|
||||
"args": ["test", "--package", "gpui"],
|
||||
"allow_concurrent_runs": false,
|
||||
"use_new_terminal": false,
|
||||
},
|
||||
]
|
||||
@@ -1,143 +0,0 @@
|
||||
# Rust coding guidelines
|
||||
|
||||
* Prioritize code correctness and clarity. Speed and efficiency are secondary priorities unless otherwise specified.
|
||||
* Do not write organizational or comments that summarize the code. Comments should only be written in order to explain "why" the code is written in some way in the case there is a reason that is tricky / non-obvious.
|
||||
* Prefer implementing functionality in existing files unless it is a new logical component. Avoid creating many small files.
|
||||
* Avoid using functions that panic like `unwrap()`, instead use mechanisms like `?` to propagate errors.
|
||||
* Be careful with operations like indexing which may panic if the indexes are out of bounds.
|
||||
* Never silently discard errors with `let _ =` on fallible operations. Always handle errors appropriately:
|
||||
- Propagate errors with `?` when the calling function should handle them
|
||||
- Use `.log_err()` or similar when you need to ignore errors but want visibility
|
||||
- Use explicit error handling with `match` or `if let Err(...)` when you need custom logic
|
||||
- Example: avoid `let _ = client.request(...).await?;` - use `client.request(...).await?;` instead
|
||||
* When implementing async operations that may fail, ensure errors propagate to the UI layer so users get meaningful feedback.
|
||||
* Never create files with `mod.rs` paths - prefer `src/some_module.rs` instead of `src/some_module/mod.rs`.
|
||||
* When creating new crates, prefer specifying the library root path in `Cargo.toml` using `[lib] path = "...rs"` instead of the default `lib.rs`, to maintain consistent and descriptive naming (e.g., `gpui.rs` or `main.rs`).
|
||||
* Avoid creative additions unless explicitly requested
|
||||
* Use full words for variable names (no abbreviations like "q" for "queue")
|
||||
* Use variable shadowing to scope clones in async contexts for clarity, minimizing the lifetime of borrowed references.
|
||||
Example:
|
||||
```rust
|
||||
executor.spawn({
|
||||
let task_ran = task_ran.clone();
|
||||
async move {
|
||||
*task_ran.borrow_mut() = true;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
# GPUI
|
||||
|
||||
GPUI is a UI framework which also provides primitives for state and concurrency management.
|
||||
|
||||
## Context
|
||||
|
||||
Context types allow interaction with global state, windows, entities, and system services. They are typically passed to functions as the argument named `cx`. When a function takes callbacks they come after the `cx` parameter.
|
||||
|
||||
* `App` is the root context type, providing access to global state and read and update of entities.
|
||||
* `Context<T>` is provided when updating an `Entity<T>`. This context dereferences into `App`, so functions which take `&App` can also take `&Context<T>`.
|
||||
* `AsyncApp` and `AsyncWindowContext` are provided by `cx.spawn` and `cx.spawn_in`. These can be held across await points.
|
||||
|
||||
## `Window`
|
||||
|
||||
`Window` provides access to the state of an application window. It is passed to functions as an argument named `window` and comes before `cx` when present. It is used for managing focus, dispatching actions, directly drawing, getting user input state, etc.
|
||||
|
||||
## Entities
|
||||
|
||||
An `Entity<T>` is a handle to state of type `T`. With `thing: Entity<T>`:
|
||||
|
||||
* `thing.entity_id()` returns `EntityId`
|
||||
* `thing.downgrade()` returns `WeakEntity<T>`
|
||||
* `thing.read(cx: &App)` returns `&T`.
|
||||
* `thing.read_with(cx, |thing: &T, cx: &App| ...)` returns the closure's return value.
|
||||
* `thing.update(cx, |thing: &mut T, cx: &mut Context<T>| ...)` allows the closure to mutate the state, and provides a `Context<T>` for interacting with the entity. It returns the closure's return value.
|
||||
* `thing.update_in(cx, |thing: &mut T, window: &mut Window, cx: &mut Context<T>| ...)` takes a `AsyncWindowContext` or `VisualTestContext`. It's the same as `update` while also providing the `Window`.
|
||||
|
||||
Within the closures, the inner `cx` provided to the closure must be used instead of the outer `cx` to avoid issues with multiple borrows.
|
||||
|
||||
Trying to update an entity while it's already being updated must be avoided as this will cause a panic.
|
||||
|
||||
When `read_with`, `update`, or `update_in` are used with an async context, the closure's return value is wrapped in an `anyhow::Result`.
|
||||
|
||||
`WeakEntity<T>` is a weak handle. It has `read_with`, `update`, and `update_in` methods that work the same, but always return an `anyhow::Result` so that they can fail if the entity no longer exists. This can be useful to avoid memory leaks - if entities have mutually recursive handles to each other they will never be dropped.
|
||||
|
||||
## Concurrency
|
||||
|
||||
All use of entities and UI rendering occurs on a single foreground thread.
|
||||
|
||||
`cx.spawn(async move |cx| ...)` runs an async closure on the foreground thread. Within the closure, `cx` is an async context like `AsyncApp` or `AsyncWindowContext`.
|
||||
|
||||
When the outer cx is a `Context<T>`, the use of `spawn` instead looks like `cx.spawn(async move |handle, cx| ...)`, where `handle: WeakEntity<T>`.
|
||||
|
||||
To do work on other threads, `cx.background_spawn(async move { ... })` is used. Often this background task is awaited on by a foreground task which uses the results to update state.
|
||||
|
||||
Both `cx.spawn` and `cx.background_spawn` return a `Task<R>`, which is a future that can be awaited upon. If this task is dropped, then its work is cancelled. To prevent this one of the following must be done:
|
||||
|
||||
* Awaiting the task in some other async context.
|
||||
* Detaching the task via `task.detach()` or `task.detach_and_log_err(cx)`, allowing it to run indefinitely.
|
||||
* Storing the task in a field, if the work should be halted when the struct is dropped.
|
||||
|
||||
A task which doesn't do anything but provide a value can be created with `Task::ready(value)`.
|
||||
|
||||
## Elements
|
||||
|
||||
The `Render` trait is used to render some state into an element tree that is laid out using flexbox layout. An `Entity<T>` where `T` implements `Render` is sometimes called a "view".
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
struct TextWithBorder(SharedString);
|
||||
|
||||
impl Render for TextWithBorder {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().border_1().child(self.0.clone())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Since `impl IntoElement for SharedString` exists, it can be used as an argument to `child`. `SharedString` is used to avoid copying strings, and is either an `&'static str` or `Arc<str>`.
|
||||
|
||||
UI components that are constructed just to be turned into elements can instead implement the `RenderOnce` trait, which is similar to `Render`, but its `render` method takes ownership of `self`. Types that implement this trait can use `#[derive(IntoElement)]` to use them directly as children.
|
||||
|
||||
The style methods on elements are similar to those used by Tailwind CSS.
|
||||
|
||||
If some attributes or children of an element tree are conditional, `.when(condition, |this| ...)` can be used to run the closure only when `condition` is true. Similarly, `.when_some(option, |this, value| ...)` runs the closure when the `Option` has a value.
|
||||
|
||||
## Input events
|
||||
|
||||
Input event handlers can be registered on an element via methods like `.on_click(|event, window, cx: &mut App| ...)`.
|
||||
|
||||
Often event handlers will want to update the entity that's in the current `Context<T>`. The `cx.listener` method provides this - its use looks like `.on_click(cx.listener(|this: &mut T, event, window, cx: &mut Context<T>| ...)`.
|
||||
|
||||
## Actions
|
||||
|
||||
Actions are dispatched via user keyboard interaction or in code via `window.dispatch_action(SomeAction.boxed_clone(), cx)` or `focus_handle.dispatch_action(&SomeAction, window, cx)`.
|
||||
|
||||
Actions with no data defined with the `actions!(some_namespace, [SomeAction, AnotherAction])` macro call. Otherwise the `Action` derive macro is used. Doc comments on actions are displayed to the user.
|
||||
|
||||
Action handlers can be registered on an element via the event handler `.on_action(|action, window, cx| ...)`. Like other event handlers, this is often used with `cx.listener`.
|
||||
|
||||
## Notify
|
||||
|
||||
When a view's state has changed in a way that may affect its rendering, it should call `cx.notify()`. This will cause the view to be rerendered. It will also cause any observe callbacks registered for the entity with `cx.observe` to be called.
|
||||
|
||||
## Entity events
|
||||
|
||||
While updating an entity (`cx: Context<T>`), it can emit an event using `cx.emit(event)`. Entities register which events they can emit by declaring `impl EventEmittor<EventType> for EntityType {}`.
|
||||
|
||||
Other entities can then register a callback to handle these events by doing `cx.subscribe(other_entity, |this, other_entity, event, cx| ...)`. This will return a `Subscription` which deregisters the callback when dropped. Typically `cx.subscribe` happens when creating a new entity and the subscriptions are stored in a `_subscriptions: Vec<Subscription>` field.
|
||||
|
||||
## Recent API changes
|
||||
|
||||
GPUI has had some changes to its APIs. Always write code using the new APIs:
|
||||
|
||||
* `spawn` methods now take async closures (`AsyncFn`), and so should be called like `cx.spawn(async move |cx| ...)`.
|
||||
* Use `Entity<T>`. This replaces `Model<T>` and `View<T>` which no longer exist and should NEVER be used.
|
||||
* Use `App` references. This replaces `AppContext` which no longer exists and should NEVER be used.
|
||||
* Use `Context<T>` references. This replaces `ModelContext<T>` which no longer exists and should NEVER be used.
|
||||
* `Window` is now passed around explicitly. The new interface adds a `Window` reference parameter to some methods, and adds some new "*_in" methods for plumbing `Window`. The old types `WindowContext` and `ViewContext<T>` should NEVER be used.
|
||||
|
||||
|
||||
## General guidelines
|
||||
|
||||
- Use `./script/clippy` instead of `cargo clippy`
|
||||
Generated
+1145
-1041
File diff suppressed because it is too large
Load Diff
+35
-49
@@ -1,3 +1,13 @@
|
||||
[workspace]
|
||||
members = [
|
||||
".",
|
||||
"tooling/macros",
|
||||
]
|
||||
default-members = [
|
||||
".",
|
||||
]
|
||||
resolver = "3"
|
||||
|
||||
[package]
|
||||
name = "gpui-ce"
|
||||
version = "0.3.3"
|
||||
@@ -17,31 +27,19 @@ autoexamples = false
|
||||
default = ["font-kit", "wayland", "x11", "windows-manifest"]
|
||||
test-support = [
|
||||
"leak-detection",
|
||||
"backtrace",
|
||||
"collections/test-support",
|
||||
"util/test-support",
|
||||
"http_client/test-support",
|
||||
"wayland",
|
||||
"x11",
|
||||
]
|
||||
inspector = ["gpui_macros/inspector"]
|
||||
inspector = ["gpui-ce-macros/inspector"]
|
||||
leak-detection = ["backtrace"]
|
||||
runtime_shaders = []
|
||||
macos-blade = [
|
||||
"blade-graphics",
|
||||
"blade-macros",
|
||||
"blade-util",
|
||||
"bytemuck",
|
||||
"objc2",
|
||||
"objc2-metal",
|
||||
]
|
||||
wayland = [
|
||||
"bitflags",
|
||||
"blade-graphics",
|
||||
"blade-macros",
|
||||
"blade-util",
|
||||
"bytemuck",
|
||||
"ashpd/wayland",
|
||||
"cosmic-text",
|
||||
"font-kit",
|
||||
"calloop-wayland-source",
|
||||
"wayland-backend",
|
||||
@@ -55,12 +53,7 @@ wayland = [
|
||||
"open",
|
||||
]
|
||||
x11 = [
|
||||
"blade-graphics",
|
||||
"blade-macros",
|
||||
"blade-util",
|
||||
"bytemuck",
|
||||
"ashpd",
|
||||
"cosmic-text",
|
||||
"font-kit",
|
||||
"as-raw-xcb-connection",
|
||||
"x11rb",
|
||||
@@ -69,10 +62,6 @@ x11 = [
|
||||
"x11-clipboard",
|
||||
"filedescriptor",
|
||||
"open",
|
||||
"scap?/x11",
|
||||
]
|
||||
screen-capture = [
|
||||
"scap",
|
||||
]
|
||||
windows-manifest = []
|
||||
|
||||
@@ -85,18 +74,14 @@ doctest = false
|
||||
anyhow = "1.0.86"
|
||||
async-task = "4.7"
|
||||
backtrace = { version = "0.3", optional = true }
|
||||
bitflags = { version = "2.6.0", optional = true }
|
||||
blade-graphics = { version = "0.7.0", optional = true }
|
||||
blade-macros = { version = "0.3.0", optional = true }
|
||||
blade-util = { version = "0.3.0", optional = true }
|
||||
bytemuck = { version = "1", optional = true }
|
||||
circular-buffer = "1.0"
|
||||
collections = { package = "gpui_collections", version = "0.2.2" }
|
||||
ctor = "0.4.0"
|
||||
derive_more = "0.99.17"
|
||||
etagere = "0.2"
|
||||
futures = "0.3"
|
||||
gpui_macros = { package = "gpui-macros", version = "0.2.2" }
|
||||
futures-concurrency = "7"
|
||||
gpui-ce-macros = { path = "./tooling/macros", features = ["inspector"] }
|
||||
http_client = { package = "gpui_http_client", version = "0.2.2" }
|
||||
image = "0.25.1"
|
||||
inventory = "0.3.19"
|
||||
@@ -110,6 +95,7 @@ parking_lot = "0.12.1"
|
||||
pin-project = "1.1.10"
|
||||
postage = { version = "0.5", features = ["futures-traits"] }
|
||||
profiling = "1"
|
||||
proptest = "1"
|
||||
rand = "0.9"
|
||||
raw-window-handle = "0.6"
|
||||
refineable = { package = "gpui_refineable", version = "0.2.2" }
|
||||
@@ -135,8 +121,13 @@ thiserror = "2.0.12"
|
||||
util = { package = "gpui_util", version = "0.2.2" }
|
||||
util_macros = { package = "gpui_util_macros", version = "0.2.2" }
|
||||
usvg = { version = "0.45.0", default-features = false }
|
||||
url = "2"
|
||||
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
|
||||
waker-fn = "1.2.0"
|
||||
web-time = "1"
|
||||
async-channel = "2"
|
||||
pollster = "0.4"
|
||||
chrono = "0.4"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
block = "0.1"
|
||||
@@ -147,11 +138,11 @@ core-foundation-sys = "0.8.6"
|
||||
core-graphics = "0.24"
|
||||
core-text = "21"
|
||||
core-video = { version = "0.4.3", features = ["metal"] }
|
||||
dispatch2 = "0.3.1"
|
||||
flume = "0.11"
|
||||
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "110523127440aefb11ce0cf280ae7c5071337ec5", package = "zed-font-kit", version = "0.14.1-zed", optional = true }
|
||||
foreign-types = "0.5"
|
||||
mach2 = "0.5"
|
||||
media = { package = "gpui_media", version = "0.2.2" }
|
||||
metal = "0.29"
|
||||
objc = "0.2"
|
||||
objc2 = { version = "0.6", optional = true }
|
||||
@@ -160,22 +151,18 @@ objc2-metal = { version = "0.3", optional = true }
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))'.dependencies]
|
||||
pathfinder_geometry = "0.5"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))'.dependencies]
|
||||
scap = { git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197176fb19cd0f9b180dd36eaac", default-features = false, package = "zed-scap", version = "0.0.8-zed", optional = true }
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
|
||||
flume = "0.11"
|
||||
oo7 = { version = "0.5.0", default-features = false, features = [
|
||||
oo7 = { version = "0.6", default-features = false, features = [
|
||||
"async-std",
|
||||
"native_crypto",
|
||||
] }
|
||||
wgpu = "24"
|
||||
cosmic-text = "0.17.0"
|
||||
swash = "0.2.6"
|
||||
bytemuck = "1"
|
||||
|
||||
ashpd = { version = "0.11", default-features = false, features = ["async-std"], optional = true }
|
||||
blade-graphics = { version = "0.7.0", optional = true }
|
||||
blade-macros = { version = "0.3.0", optional = true }
|
||||
blade-util = { version = "0.3.0", optional = true }
|
||||
bytemuck = { version = "1", optional = true }
|
||||
cosmic-text = { version = "0.14.0", optional = true }
|
||||
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "110523127440aefb11ce0cf280ae7c5071337ec5", package = "zed-font-kit", version = "0.14.1-zed", features = [
|
||||
"source-fontconfig-dlopen",
|
||||
], optional = true }
|
||||
@@ -189,14 +176,14 @@ wayland-backend = { version = "0.3.3", features = [
|
||||
"client_system",
|
||||
"dlopen",
|
||||
], optional = true }
|
||||
wayland-client = { version = "0.31.2", optional = true }
|
||||
wayland-cursor = { version = "0.31.1", optional = true }
|
||||
wayland-protocols = { version = "0.31.2", features = [
|
||||
wayland-client = { version = "0.31.11", optional = true }
|
||||
wayland-cursor = { version = "0.31.11", optional = true }
|
||||
wayland-protocols = { version = "0.32.9", features = [
|
||||
"client",
|
||||
"staging",
|
||||
"unstable",
|
||||
], optional = true }
|
||||
wayland-protocols-plasma = { version = "0.2.0", features = [
|
||||
wayland-protocols-plasma = { version = "0.3.9", features = [
|
||||
"client",
|
||||
], optional = true }
|
||||
wayland-protocols-wlr = { version = "0.3.9", features = [
|
||||
@@ -212,6 +199,7 @@ x11rb = { version = "0.13.1", features = [
|
||||
"cursor",
|
||||
"resource_manager",
|
||||
"sync",
|
||||
"dri3",
|
||||
], optional = true }
|
||||
xkbcommon = { version = "0.8.0", features = [
|
||||
"wayland",
|
||||
@@ -222,6 +210,7 @@ xim = { git = "https://github.com/zed-industries/xim-rs.git", rev = "16f35a2c881
|
||||
"x11rb-client",
|
||||
], package = "zed-xim", version = "0.4.0-zed", optional = true }
|
||||
x11-clipboard = { version = "0.9.3", optional = true }
|
||||
bitflags = { version = "2.6.0", optional = true }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
flume = "0.11"
|
||||
@@ -285,6 +274,7 @@ env_logger = "0.11"
|
||||
http_client = { package = "gpui_http_client", version = "0.2.2", features = ["test-support"] }
|
||||
lyon = { version = "1.0", features = ["extra"] }
|
||||
pretty_assertions = { version = "1.3.0", features = ["unstable"] }
|
||||
proptest = "1"
|
||||
rand = "0.9"
|
||||
unicode-segmentation = "1.10"
|
||||
util = { package = "gpui_util", version = "0.2.2", features = ["test-support"] }
|
||||
@@ -294,15 +284,10 @@ embed-resource = "3.0"
|
||||
windows-registry = "0.5"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.build-dependencies]
|
||||
bindgen = "0.71"
|
||||
cbindgen = { version = "0.28.0", default-features = false }
|
||||
naga = { version = "25.0", features = ["wgsl-in"] }
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.build-dependencies]
|
||||
naga = { version = "25.0", features = ["wgsl-in"] }
|
||||
|
||||
[patch.crates-io]
|
||||
calloop = { git = "https://github.com/zed-industries/calloop" }
|
||||
naga = { version = "29.0", features = ["wgsl-in"] }
|
||||
|
||||
# ============================================================================
|
||||
# Learn Examples - Educational examples for learning GPUI
|
||||
@@ -474,3 +459,4 @@ single_range_in_vec_init = "allow"
|
||||
too_many_arguments = "allow"
|
||||
large_enum_variant = "allow"
|
||||
nonminimal_bool = "allow"
|
||||
|
||||
|
||||
-222
@@ -1,222 +0,0 @@
|
||||
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
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
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_
|
||||
@@ -75,3 +75,4 @@ In addition to the systems above, GPUI provides a range of smaller services that
|
||||
- The `[gpui::test]` macro provides a convenient way to write tests for your GPUI applications. Tests also have their own kind of context, a `TestAppContext` which provides ways of simulating common platform input. See `app::test_context` and `test` modules for more details.
|
||||
|
||||
Currently, the best way to learn about these APIs is to read the Zed source code or drop a question in the [Zed Discord](https://zed.dev/community-links). We're working on improving the documentation, creating more examples, and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog).
|
||||
|
||||
|
||||
@@ -1,59 +1,25 @@
|
||||
#![allow(clippy::disallowed_methods, reason = "build scripts are exempt")]
|
||||
#![cfg_attr(any(not(target_os = "macos"), feature = "macos-blade"), allow(unused))]
|
||||
|
||||
//TODO: consider generating shader code for WGSL
|
||||
//TODO: deprecate "runtime-shaders" and "macos-blade"
|
||||
|
||||
use std::env;
|
||||
|
||||
fn main() {
|
||||
let target = env::var("CARGO_CFG_TARGET_OS");
|
||||
println!("cargo::rustc-check-cfg=cfg(gles)");
|
||||
|
||||
#[cfg(any(
|
||||
not(any(target_os = "macos", target_os = "windows")),
|
||||
all(target_os = "macos", feature = "macos-blade")
|
||||
))]
|
||||
check_wgsl_shaders();
|
||||
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||
|
||||
match target.as_deref() {
|
||||
Ok("macos") => {
|
||||
match target_os.as_str() {
|
||||
"macos" => {
|
||||
#[cfg(target_os = "macos")]
|
||||
macos::build();
|
||||
}
|
||||
Ok("windows") => {
|
||||
"windows" => {
|
||||
#[cfg(target_os = "windows")]
|
||||
windows::build();
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(any(
|
||||
not(any(target_os = "macos", target_os = "windows")),
|
||||
all(target_os = "macos", feature = "macos-blade")
|
||||
))]
|
||||
fn check_wgsl_shaders() {
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
use std::str::FromStr;
|
||||
|
||||
let shader_source_path = "./src/platform/blade/shaders.wgsl";
|
||||
let shader_path = PathBuf::from_str(shader_source_path).unwrap();
|
||||
println!("cargo:rerun-if-changed={}", &shader_path.display());
|
||||
|
||||
let shader_source = std::fs::read_to_string(&shader_path).unwrap();
|
||||
|
||||
match naga::front::wgsl::parse_str(&shader_source) {
|
||||
Ok(_) => {
|
||||
// All clear
|
||||
}
|
||||
Err(e) => {
|
||||
println!("cargo::error=WGSL shader compilation failed:\n{}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos {
|
||||
use std::{
|
||||
@@ -63,55 +29,19 @@ mod macos {
|
||||
|
||||
use cbindgen::Config;
|
||||
|
||||
pub(super) fn build() {
|
||||
generate_dispatch_bindings();
|
||||
#[cfg(not(feature = "macos-blade"))]
|
||||
{
|
||||
let header_path = generate_shader_bindings();
|
||||
pub fn build() {
|
||||
let header_path = generate_shader_bindings();
|
||||
|
||||
#[cfg(feature = "runtime_shaders")]
|
||||
emit_stitched_shaders(&header_path);
|
||||
#[cfg(not(feature = "runtime_shaders"))]
|
||||
compile_metal_shaders(&header_path);
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_dispatch_bindings() {
|
||||
println!("cargo:rustc-link-lib=framework=System");
|
||||
|
||||
let bindings = bindgen::Builder::default()
|
||||
.header("src/platform/mac/dispatch.h")
|
||||
.allowlist_var("_dispatch_main_q")
|
||||
.allowlist_var("_dispatch_source_type_data_add")
|
||||
.allowlist_var("DISPATCH_QUEUE_PRIORITY_HIGH")
|
||||
.allowlist_var("DISPATCH_QUEUE_PRIORITY_DEFAULT")
|
||||
.allowlist_var("DISPATCH_QUEUE_PRIORITY_LOW")
|
||||
.allowlist_var("DISPATCH_TIME_NOW")
|
||||
.allowlist_function("dispatch_get_global_queue")
|
||||
.allowlist_function("dispatch_async_f")
|
||||
.allowlist_function("dispatch_after_f")
|
||||
.allowlist_function("dispatch_time")
|
||||
.allowlist_function("dispatch_source_merge_data")
|
||||
.allowlist_function("dispatch_source_create")
|
||||
.allowlist_function("dispatch_source_set_event_handler_f")
|
||||
.allowlist_function("dispatch_resume")
|
||||
.allowlist_function("dispatch_suspend")
|
||||
.allowlist_function("dispatch_source_cancel")
|
||||
.allowlist_function("dispatch_set_context")
|
||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
|
||||
.layout_tests(false)
|
||||
.generate()
|
||||
.expect("unable to generate bindings");
|
||||
|
||||
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
bindings
|
||||
.write_to_file(out_path.join("dispatch_sys.rs"))
|
||||
.expect("couldn't write dispatch bindings");
|
||||
#[cfg(feature = "runtime_shaders")]
|
||||
emit_stitched_shaders(&header_path);
|
||||
#[cfg(not(feature = "runtime_shaders"))]
|
||||
compile_metal_shaders(&header_path);
|
||||
}
|
||||
|
||||
fn generate_shader_bindings() -> PathBuf {
|
||||
let output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("scene.h");
|
||||
let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
|
||||
let mut config = Config {
|
||||
include_guard: Some("SCENE_H".into()),
|
||||
language: cbindgen::Language::C,
|
||||
@@ -152,6 +82,7 @@ mod macos {
|
||||
|
||||
let mut builder = cbindgen::Builder::new();
|
||||
|
||||
// Source files that define types used in shaders
|
||||
let src_paths = [
|
||||
crate_dir.join("src/scene.rs"),
|
||||
crate_dir.join("src/geometry.rs"),
|
||||
@@ -160,7 +91,8 @@ mod macos {
|
||||
crate_dir.join("src/platform.rs"),
|
||||
crate_dir.join("src/platform/mac/metal_renderer.rs"),
|
||||
];
|
||||
for src_path in src_paths {
|
||||
|
||||
for src_path in &src_paths {
|
||||
println!("cargo:rerun-if-changed={}", src_path.display());
|
||||
builder = builder.with_src(src_path);
|
||||
}
|
||||
@@ -178,7 +110,6 @@ mod macos {
|
||||
/// so that it is self-contained.
|
||||
#[cfg(feature = "runtime_shaders")]
|
||||
fn emit_stitched_shaders(header_path: &Path) {
|
||||
use std::str::FromStr;
|
||||
fn stitch_header(header: &Path, shader_path: &Path) -> std::io::Result<PathBuf> {
|
||||
let header_contents = std::fs::read_to_string(header)?;
|
||||
let shader_contents = std::fs::read_to_string(shader_path)?;
|
||||
@@ -189,7 +120,7 @@ mod macos {
|
||||
Ok(out_path)
|
||||
}
|
||||
let shader_source_path = "./src/platform/mac/shaders.metal";
|
||||
let shader_path = PathBuf::from_str(shader_source_path).unwrap();
|
||||
let shader_path = PathBuf::from(shader_source_path);
|
||||
stitch_header(header_path, &shader_path).unwrap();
|
||||
println!("cargo:rerun-if-changed={}", &shader_source_path);
|
||||
}
|
||||
@@ -249,20 +180,10 @@ mod macos {
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows {
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
fs,
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
process::{self, Command},
|
||||
};
|
||||
|
||||
pub(super) fn build() {
|
||||
// Compile HLSL shaders
|
||||
pub fn build() {
|
||||
#[cfg(not(debug_assertions))]
|
||||
compile_shaders();
|
||||
shader_compilation::compile_shaders();
|
||||
|
||||
// Embed the Windows manifest and resource file
|
||||
#[cfg(feature = "windows-manifest")]
|
||||
embed_resource();
|
||||
}
|
||||
@@ -278,222 +199,223 @@ mod windows {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// You can set the `GPUI_FXC_PATH` environment variable to specify the path to the fxc.exe compiler.
|
||||
fn compile_shaders() {
|
||||
let shader_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
|
||||
.join("src/platform/windows/shaders.hlsl");
|
||||
let out_dir = std::env::var("OUT_DIR").unwrap();
|
||||
#[cfg(not(debug_assertions))]
|
||||
mod shader_compilation {
|
||||
use std::{
|
||||
fs,
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
process::{self, Command},
|
||||
};
|
||||
|
||||
println!("cargo:rerun-if-changed={}", shader_path.display());
|
||||
|
||||
// Check if fxc.exe is available
|
||||
let fxc_path = find_fxc_compiler();
|
||||
|
||||
// Define all modules
|
||||
let modules = [
|
||||
"quad",
|
||||
"shadow",
|
||||
"path_rasterization",
|
||||
"path_sprite",
|
||||
"underline",
|
||||
"monochrome_sprite",
|
||||
"polychrome_sprite",
|
||||
];
|
||||
|
||||
let rust_binding_path = format!("{}/shaders_bytes.rs", out_dir);
|
||||
if Path::new(&rust_binding_path).exists() {
|
||||
fs::remove_file(&rust_binding_path)
|
||||
.expect("Failed to remove existing Rust binding file");
|
||||
}
|
||||
for module in modules {
|
||||
compile_shader_for_module(
|
||||
module,
|
||||
&out_dir,
|
||||
&fxc_path,
|
||||
shader_path.to_str().unwrap(),
|
||||
&rust_binding_path,
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
pub fn compile_shaders() {
|
||||
let shader_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
|
||||
.join("src/platform/windows/color_text_raster.hlsl");
|
||||
compile_shader_for_module(
|
||||
"emoji_rasterization",
|
||||
&out_dir,
|
||||
&fxc_path,
|
||||
shader_path.to_str().unwrap(),
|
||||
&rust_binding_path,
|
||||
);
|
||||
}
|
||||
}
|
||||
.join("src/platform/windows/shaders.hlsl");
|
||||
let out_dir = std::env::var("OUT_DIR").unwrap();
|
||||
|
||||
/// Locate `binary` in the newest installed Windows SDK.
|
||||
pub fn find_latest_windows_sdk_binary(
|
||||
binary: &str,
|
||||
) -> Result<Option<PathBuf>, Box<dyn std::error::Error>> {
|
||||
let key = windows_registry::LOCAL_MACHINE
|
||||
.open("SOFTWARE\\WOW6432Node\\Microsoft\\Microsoft SDKs\\Windows\\v10.0")?;
|
||||
println!("cargo:rerun-if-changed={}", shader_path.display());
|
||||
|
||||
let install_folder: String = key.get_string("InstallationFolder")?; // "C:\Program Files (x86)\Windows Kits\10\"
|
||||
let install_folder_bin = Path::new(&install_folder).join("bin");
|
||||
let fxc_path = find_fxc_compiler();
|
||||
|
||||
let mut versions: Vec<_> = std::fs::read_dir(&install_folder_bin)?
|
||||
.flatten()
|
||||
.filter(|entry| entry.path().is_dir())
|
||||
.filter_map(|entry| entry.file_name().into_string().ok())
|
||||
.collect();
|
||||
let modules = [
|
||||
"quad",
|
||||
"shadow",
|
||||
"path_rasterization",
|
||||
"path_sprite",
|
||||
"underline",
|
||||
"monochrome_sprite",
|
||||
"subpixel_sprite",
|
||||
"polychrome_sprite",
|
||||
];
|
||||
|
||||
versions.sort_by_key(|s| {
|
||||
s.split('.')
|
||||
.filter_map(|p| p.parse().ok())
|
||||
.collect::<Vec<u32>>()
|
||||
});
|
||||
|
||||
let arch = match std::env::consts::ARCH {
|
||||
"x86_64" => "x64",
|
||||
"aarch64" => "arm64",
|
||||
_ => Err(format!(
|
||||
"Unsupported architecture: {}",
|
||||
std::env::consts::ARCH
|
||||
))?,
|
||||
};
|
||||
|
||||
if let Some(highest_version) = versions.last() {
|
||||
return Ok(Some(
|
||||
install_folder_bin
|
||||
.join(highest_version)
|
||||
.join(arch)
|
||||
.join(binary),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// You can set the `GPUI_FXC_PATH` environment variable to specify the path to the fxc.exe compiler.
|
||||
fn find_fxc_compiler() -> String {
|
||||
// Check environment variable
|
||||
if let Ok(path) = std::env::var("GPUI_FXC_PATH")
|
||||
&& Path::new(&path).exists()
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
// Try to find in PATH
|
||||
// NOTE: This has to be `where.exe` on Windows, not `where`, it must be ended with `.exe`
|
||||
if let Ok(output) = std::process::Command::new("where.exe")
|
||||
.arg("fxc.exe")
|
||||
.output()
|
||||
&& output.status.success()
|
||||
{
|
||||
let path = String::from_utf8_lossy(&output.stdout);
|
||||
return path.trim().to_string();
|
||||
}
|
||||
|
||||
if let Ok(Some(path)) = find_latest_windows_sdk_binary("fxc.exe") {
|
||||
return path.to_string_lossy().into_owned();
|
||||
}
|
||||
|
||||
panic!("Failed to find fxc.exe");
|
||||
}
|
||||
|
||||
fn compile_shader_for_module(
|
||||
module: &str,
|
||||
out_dir: &str,
|
||||
fxc_path: &str,
|
||||
shader_path: &str,
|
||||
rust_binding_path: &str,
|
||||
) {
|
||||
// Compile vertex shader
|
||||
let output_file = format!("{}/{}_vs.h", out_dir, module);
|
||||
let const_name = format!("{}_VERTEX_BYTES", module.to_uppercase());
|
||||
compile_shader_impl(
|
||||
fxc_path,
|
||||
&format!("{module}_vertex"),
|
||||
&output_file,
|
||||
&const_name,
|
||||
shader_path,
|
||||
"vs_4_1",
|
||||
);
|
||||
generate_rust_binding(&const_name, &output_file, rust_binding_path);
|
||||
|
||||
// Compile fragment shader
|
||||
let output_file = format!("{}/{}_ps.h", out_dir, module);
|
||||
let const_name = format!("{}_FRAGMENT_BYTES", module.to_uppercase());
|
||||
compile_shader_impl(
|
||||
fxc_path,
|
||||
&format!("{module}_fragment"),
|
||||
&output_file,
|
||||
&const_name,
|
||||
shader_path,
|
||||
"ps_4_1",
|
||||
);
|
||||
generate_rust_binding(&const_name, &output_file, rust_binding_path);
|
||||
}
|
||||
|
||||
fn compile_shader_impl(
|
||||
fxc_path: &str,
|
||||
entry_point: &str,
|
||||
output_path: &str,
|
||||
var_name: &str,
|
||||
shader_path: &str,
|
||||
target: &str,
|
||||
) {
|
||||
let output = Command::new(fxc_path)
|
||||
.args([
|
||||
"/T",
|
||||
target,
|
||||
"/E",
|
||||
entry_point,
|
||||
"/Fh",
|
||||
output_path,
|
||||
"/Vn",
|
||||
var_name,
|
||||
"/O3",
|
||||
shader_path,
|
||||
])
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(result) => {
|
||||
if result.status.success() {
|
||||
return;
|
||||
}
|
||||
println!(
|
||||
"cargo::error=Shader compilation failed for {}:\n{}",
|
||||
entry_point,
|
||||
String::from_utf8_lossy(&result.stderr)
|
||||
let rust_binding_path = format!("{}/shaders_bytes.rs", out_dir);
|
||||
if Path::new(&rust_binding_path).exists() {
|
||||
fs::remove_file(&rust_binding_path)
|
||||
.expect("Failed to remove existing Rust binding file");
|
||||
}
|
||||
for module in modules {
|
||||
compile_shader_for_module(
|
||||
module,
|
||||
&out_dir,
|
||||
&fxc_path,
|
||||
shader_path.to_str().unwrap(),
|
||||
&rust_binding_path,
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("cargo::error=Failed to run fxc for {}: {}", entry_point, e);
|
||||
process::exit(1);
|
||||
|
||||
{
|
||||
let shader_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
|
||||
.join("src/platform/windows/color_text_raster.hlsl");
|
||||
compile_shader_for_module(
|
||||
"emoji_rasterization",
|
||||
&out_dir,
|
||||
&fxc_path,
|
||||
shader_path.to_str().unwrap(),
|
||||
&rust_binding_path,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_rust_binding(const_name: &str, head_file: &str, output_path: &str) {
|
||||
let header_content = fs::read_to_string(head_file).expect("Failed to read header file");
|
||||
let const_definition = {
|
||||
let global_var_start = header_content.find("const BYTE").unwrap();
|
||||
let global_var = &header_content[global_var_start..];
|
||||
let equal = global_var.find('=').unwrap();
|
||||
global_var[equal + 1..].trim()
|
||||
};
|
||||
let rust_binding = format!(
|
||||
"const {}: &[u8] = &{}\n",
|
||||
const_name,
|
||||
const_definition.replace('{', "[").replace('}', "]")
|
||||
);
|
||||
let mut options = fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(output_path)
|
||||
.expect("Failed to open Rust binding file");
|
||||
options
|
||||
.write_all(rust_binding.as_bytes())
|
||||
.expect("Failed to write Rust binding file");
|
||||
fn find_latest_windows_sdk_binary(
|
||||
binary: &str,
|
||||
) -> Result<Option<PathBuf>, Box<dyn std::error::Error>> {
|
||||
let key = windows_registry::LOCAL_MACHINE
|
||||
.open("SOFTWARE\\WOW6432Node\\Microsoft\\Microsoft SDKs\\Windows\\v10.0")?;
|
||||
|
||||
let install_folder: String = key.get_string("InstallationFolder")?;
|
||||
let install_folder_bin = Path::new(&install_folder).join("bin");
|
||||
|
||||
let mut versions: Vec<_> = std::fs::read_dir(&install_folder_bin)?
|
||||
.flatten()
|
||||
.filter(|entry| entry.path().is_dir())
|
||||
.filter_map(|entry| entry.file_name().into_string().ok())
|
||||
.collect();
|
||||
|
||||
versions.sort_by_key(|s| {
|
||||
s.split('.')
|
||||
.filter_map(|p| p.parse().ok())
|
||||
.collect::<Vec<u32>>()
|
||||
});
|
||||
|
||||
let arch = match std::env::consts::ARCH {
|
||||
"x86_64" => "x64",
|
||||
"aarch64" => "arm64",
|
||||
_ => Err(format!(
|
||||
"Unsupported architecture: {}",
|
||||
std::env::consts::ARCH
|
||||
))?,
|
||||
};
|
||||
|
||||
if let Some(highest_version) = versions.last() {
|
||||
return Ok(Some(
|
||||
install_folder_bin
|
||||
.join(highest_version)
|
||||
.join(arch)
|
||||
.join(binary),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn find_fxc_compiler() -> String {
|
||||
if let Ok(path) = std::env::var("GPUI_FXC_PATH")
|
||||
&& Path::new(&path).exists()
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
if let Ok(output) = std::process::Command::new("where.exe")
|
||||
.arg("fxc.exe")
|
||||
.output()
|
||||
&& output.status.success()
|
||||
{
|
||||
let path = String::from_utf8_lossy(&output.stdout);
|
||||
return path.trim().to_string();
|
||||
}
|
||||
|
||||
if let Ok(Some(path)) = find_latest_windows_sdk_binary("fxc.exe") {
|
||||
return path.to_string_lossy().into_owned();
|
||||
}
|
||||
|
||||
panic!("Failed to find fxc.exe");
|
||||
}
|
||||
|
||||
fn compile_shader_for_module(
|
||||
module: &str,
|
||||
out_dir: &str,
|
||||
fxc_path: &str,
|
||||
shader_path: &str,
|
||||
rust_binding_path: &str,
|
||||
) {
|
||||
let output_file = format!("{}/{}_vs.h", out_dir, module);
|
||||
let const_name = format!("{}_VERTEX_BYTES", module.to_uppercase());
|
||||
compile_shader_impl(
|
||||
fxc_path,
|
||||
&format!("{module}_vertex"),
|
||||
&output_file,
|
||||
&const_name,
|
||||
shader_path,
|
||||
"vs_4_1",
|
||||
);
|
||||
generate_rust_binding(&const_name, &output_file, rust_binding_path);
|
||||
|
||||
let output_file = format!("{}/{}_ps.h", out_dir, module);
|
||||
let const_name = format!("{}_FRAGMENT_BYTES", module.to_uppercase());
|
||||
compile_shader_impl(
|
||||
fxc_path,
|
||||
&format!("{module}_fragment"),
|
||||
&output_file,
|
||||
&const_name,
|
||||
shader_path,
|
||||
"ps_4_1",
|
||||
);
|
||||
generate_rust_binding(&const_name, &output_file, rust_binding_path);
|
||||
}
|
||||
|
||||
fn compile_shader_impl(
|
||||
fxc_path: &str,
|
||||
entry_point: &str,
|
||||
output_path: &str,
|
||||
var_name: &str,
|
||||
shader_path: &str,
|
||||
target: &str,
|
||||
) {
|
||||
let output = Command::new(fxc_path)
|
||||
.args([
|
||||
"/T",
|
||||
target,
|
||||
"/E",
|
||||
entry_point,
|
||||
"/Fh",
|
||||
output_path,
|
||||
"/Vn",
|
||||
var_name,
|
||||
"/O3",
|
||||
shader_path,
|
||||
])
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(result) => {
|
||||
if result.status.success() {
|
||||
return;
|
||||
}
|
||||
println!(
|
||||
"cargo::error=Shader compilation failed for {}:\n{}",
|
||||
entry_point,
|
||||
String::from_utf8_lossy(&result.stderr)
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("cargo::error=Failed to run fxc for {}: {}", entry_point, e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_rust_binding(const_name: &str, head_file: &str, output_path: &str) {
|
||||
let header_content = fs::read_to_string(head_file).expect("Failed to read header file");
|
||||
let const_definition = {
|
||||
let global_var_start = header_content.find("const BYTE").unwrap();
|
||||
let global_var = &header_content[global_var_start..];
|
||||
let equal = global_var.find('=').unwrap();
|
||||
global_var[equal + 1..].trim()
|
||||
};
|
||||
let rust_binding = format!(
|
||||
"const {}: &[u8] = &{}\n",
|
||||
const_name,
|
||||
const_definition.replace('{', "[").replace('}', "]")
|
||||
);
|
||||
let mut options = fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(output_path)
|
||||
.expect("Failed to open Rust binding file");
|
||||
options
|
||||
.write_all(rust_binding.as_bytes())
|
||||
.expect("Failed to write Rust binding file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,3 +21,4 @@ disallowed-types = [
|
||||
# { path = "indexmap::IndexSet", replacement = "collections::IndexSet" },
|
||||
# { path = "indexmap::IndexMap", replacement = "collections::IndexMap" },
|
||||
]
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{
|
||||
App, Application, Bounds, BoxShadow, Context, Div, SharedString, Window, WindowBounds,
|
||||
WindowOptions, div, hsla, point, prelude::*, px, relative, rgb, size,
|
||||
WindowOptions, current_platform, div, hsla, point, prelude::*, px, relative, rgb, size,
|
||||
};
|
||||
|
||||
struct Shadow {}
|
||||
@@ -569,7 +569,7 @@ impl Render for Shadow {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
Application::with_platform(current_platform(false)).run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(1000.0), px(800.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
|
||||
+13
-12
@@ -13,10 +13,11 @@ mod example_prelude;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::colors::Colors;
|
||||
use gpui::{
|
||||
Animation, AnimationExt as _, App, Application, AssetSource, Bounds, Colors, Context, Hsla,
|
||||
Animation, AnimationExt as _, App, Application, AssetSource, Bounds, Context, Hsla,
|
||||
SharedString, Transformation, Window, WindowBounds, WindowOptions, bounce, div, ease_in_out,
|
||||
linear, percentage, prelude::*, px, size as gpui_size, svg,
|
||||
linear, percentage, prelude::*, px, rgb, size as gpui_size, svg,
|
||||
};
|
||||
|
||||
struct Assets {}
|
||||
@@ -78,7 +79,7 @@ impl Render for AnimationExample {
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
.child("Animations, easing, and transformations in GPUI"),
|
||||
),
|
||||
)
|
||||
@@ -99,8 +100,8 @@ impl Render for AnimationExample {
|
||||
}
|
||||
|
||||
fn rotation_example(colors: &Colors) -> impl IntoElement {
|
||||
let text_muted = colors.text_muted;
|
||||
let accent = colors.accent;
|
||||
let text_muted = colors.disabled;
|
||||
let accent = colors.selected;
|
||||
|
||||
div()
|
||||
.flex()
|
||||
@@ -133,8 +134,8 @@ fn rotation_example(colors: &Colors) -> impl IntoElement {
|
||||
}
|
||||
|
||||
fn bounce_example(colors: &Colors) -> impl IntoElement {
|
||||
let text_muted = colors.text_muted;
|
||||
let success = colors.success;
|
||||
let text_muted = colors.disabled;
|
||||
let success = rgb(0x388e3c);
|
||||
|
||||
div()
|
||||
.flex()
|
||||
@@ -167,8 +168,8 @@ fn bounce_example(colors: &Colors) -> impl IntoElement {
|
||||
}
|
||||
|
||||
fn scale_example(colors: &Colors) -> impl IntoElement {
|
||||
let text_muted = colors.text_muted;
|
||||
let warning = colors.warning;
|
||||
let text_muted = colors.disabled;
|
||||
let warning = rgb(0xf9a825);
|
||||
|
||||
div()
|
||||
.flex()
|
||||
@@ -202,8 +203,8 @@ fn scale_example(colors: &Colors) -> impl IntoElement {
|
||||
}
|
||||
|
||||
fn combined_example(colors: &Colors) -> impl IntoElement {
|
||||
let text_muted = colors.text_muted;
|
||||
let error = colors.error;
|
||||
let text_muted = colors.disabled;
|
||||
let error = rgb(0xd32f2f);
|
||||
|
||||
div()
|
||||
.flex()
|
||||
@@ -240,7 +241,7 @@ fn combined_example(colors: &Colors) -> impl IntoElement {
|
||||
}
|
||||
|
||||
fn section(colors: &Colors, title: &'static str, content: impl IntoElement) -> impl IntoElement {
|
||||
let surface: Hsla = colors.surface.into();
|
||||
let surface: Hsla = colors.container.into();
|
||||
|
||||
div()
|
||||
.flex()
|
||||
|
||||
@@ -12,9 +12,10 @@ mod example_prelude;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::colors::Colors;
|
||||
use gpui::{
|
||||
App, Application, Bounds, Colors, Context, Entity, Render, Task, Window, WindowBounds,
|
||||
WindowOptions, div, prelude::*, px, size,
|
||||
App, Application, Bounds, Context, Entity, Render, Task, Window, WindowBounds, WindowOptions,
|
||||
div, prelude::*, px, rgb, size,
|
||||
};
|
||||
|
||||
// Example 1: Simple Foreground Task
|
||||
@@ -272,7 +273,7 @@ impl Render for AsyncTasksExample {
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
.child("Spawning, background work, and task management"),
|
||||
),
|
||||
)
|
||||
@@ -348,9 +349,9 @@ impl Render for AsyncTasksExample {
|
||||
.child({
|
||||
let is_running = cancellable.is_running();
|
||||
let (bg, bg_hover) = if is_running {
|
||||
(colors.error, colors.error_hover)
|
||||
(rgb(0xd32f2f), rgb(0xe04545))
|
||||
} else {
|
||||
(colors.success, colors.success_hover)
|
||||
(rgb(0x388e3c), rgb(0x43a047))
|
||||
};
|
||||
div()
|
||||
.id("cancel-btn")
|
||||
@@ -381,7 +382,7 @@ impl Render for AsyncTasksExample {
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
.child(format!("Numbers: {:?}", return_demo.numbers)),
|
||||
)
|
||||
.child(
|
||||
@@ -446,7 +447,7 @@ fn demo_section(
|
||||
.gap_3()
|
||||
.p_4()
|
||||
.rounded_lg()
|
||||
.bg(colors.surface)
|
||||
.bg(colors.container)
|
||||
.border_1()
|
||||
.border_color(colors.border)
|
||||
.child(
|
||||
@@ -464,7 +465,7 @@ fn demo_section(
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
.child(description),
|
||||
),
|
||||
)
|
||||
@@ -477,10 +478,10 @@ fn button(
|
||||
label: &'static str,
|
||||
disabled: bool,
|
||||
) -> gpui::Stateful<gpui::Div> {
|
||||
let disabled_bg = colors.surface_hover;
|
||||
let bg = colors.accent;
|
||||
let bg_hover = colors.accent_hover;
|
||||
let bg_active = colors.accent_active;
|
||||
let disabled_bg = colors.selected;
|
||||
let bg = colors.selected;
|
||||
let bg_hover = colors.selected;
|
||||
let bg_active = colors.selected;
|
||||
let text = colors.selected_text;
|
||||
|
||||
div()
|
||||
@@ -507,7 +508,7 @@ fn secondary_button(
|
||||
id: impl Into<gpui::ElementId>,
|
||||
label: &'static str,
|
||||
) -> gpui::Stateful<gpui::Div> {
|
||||
let bg = colors.surface_hover;
|
||||
let bg = colors.selected;
|
||||
let bg_hover = colors.border;
|
||||
let text = colors.text;
|
||||
|
||||
@@ -526,8 +527,8 @@ fn secondary_button(
|
||||
|
||||
fn progress_bar(colors: &Colors, progress: u32) -> impl IntoElement {
|
||||
let clamped = progress.min(100);
|
||||
let bar_bg = colors.surface_hover;
|
||||
let bar_fill = colors.success;
|
||||
let bar_bg = colors.selected;
|
||||
let bar_fill = rgb(0x388e3c);
|
||||
|
||||
div()
|
||||
.h_2()
|
||||
|
||||
@@ -11,9 +11,10 @@
|
||||
mod example_prelude;
|
||||
|
||||
use example_prelude::init_example;
|
||||
use gpui::colors::Colors;
|
||||
use gpui::{
|
||||
App, Application, Bounds, Colors, Context, Entity, IntoElement, Render, RenderOnce, Window,
|
||||
WindowBounds, WindowOptions, div, prelude::*, px, size,
|
||||
App, Application, Bounds, Context, Entity, IntoElement, Render, RenderOnce, Window,
|
||||
WindowBounds, WindowOptions, div, prelude::*, px, rgb, size,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -43,10 +44,10 @@ fn use_state_counter(colors: &Colors, window: &mut Window, cx: &mut App) -> impl
|
||||
|
||||
let count = state.read(cx).count;
|
||||
|
||||
let error = colors.error;
|
||||
let error_hover = colors.error_hover;
|
||||
let success = colors.success;
|
||||
let success_hover = colors.success_hover;
|
||||
let error = rgb(0xd32f2f);
|
||||
let error_hover = rgb(0xe04545);
|
||||
let success = rgb(0x388e3c);
|
||||
let success_hover = rgb(0x43a047);
|
||||
|
||||
div()
|
||||
.id("use-state-counter")
|
||||
@@ -55,11 +56,11 @@ fn use_state_counter(colors: &Colors, window: &mut Window, cx: &mut App) -> impl
|
||||
.gap_2()
|
||||
.p_4()
|
||||
.rounded_lg()
|
||||
.bg(colors.surface)
|
||||
.bg(colors.container)
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
.child("use_state Counter"),
|
||||
)
|
||||
.child(
|
||||
@@ -164,10 +165,10 @@ impl RenderOnceCounter {
|
||||
impl RenderOnce for RenderOnceCounter {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let colors = self.colors;
|
||||
let error = colors.error;
|
||||
let error_hover = colors.error_hover;
|
||||
let success = colors.success;
|
||||
let success_hover = colors.success_hover;
|
||||
let error = rgb(0xd32f2f);
|
||||
let error_hover = rgb(0xe04545);
|
||||
let success = rgb(0x388e3c);
|
||||
let success_hover = rgb(0x43a047);
|
||||
|
||||
div()
|
||||
.id("render-once-counter")
|
||||
@@ -176,11 +177,11 @@ impl RenderOnce for RenderOnceCounter {
|
||||
.gap_2()
|
||||
.p_4()
|
||||
.rounded_lg()
|
||||
.bg(colors.surface)
|
||||
.bg(colors.container)
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
.child("RenderOnce Counter"),
|
||||
)
|
||||
.child(
|
||||
@@ -269,10 +270,10 @@ impl RenderCounter {
|
||||
impl Render for RenderCounter {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let colors = Colors::for_appearance(window);
|
||||
let error = colors.error;
|
||||
let error_hover = colors.error_hover;
|
||||
let success = colors.success;
|
||||
let success_hover = colors.success_hover;
|
||||
let error = rgb(0xd32f2f);
|
||||
let error_hover = rgb(0xe04545);
|
||||
let success = rgb(0x388e3c);
|
||||
let success_hover = rgb(0x43a047);
|
||||
|
||||
div()
|
||||
.id("render-counter")
|
||||
@@ -281,11 +282,11 @@ impl Render for RenderCounter {
|
||||
.gap_2()
|
||||
.p_4()
|
||||
.rounded_lg()
|
||||
.bg(colors.surface)
|
||||
.bg(colors.container)
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
.child("Render Counter"),
|
||||
)
|
||||
.child(
|
||||
@@ -380,7 +381,7 @@ impl Render for CreatingComponentsExample {
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
.child("Three approaches to stateful components in GPUI"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
//! 3. `window.paint_*` methods - Drawing quads, paths, and more
|
||||
//! 4. Interactive drawing - Responding to mouse events
|
||||
|
||||
use gpui::colors::Colors;
|
||||
use gpui::{
|
||||
App, Application, Bounds, Colors, Context, Hsla, MouseButton, MouseDownEvent, MouseMoveEvent,
|
||||
App, Application, Bounds, Context, Hsla, MouseButton, MouseDownEvent, MouseMoveEvent,
|
||||
MouseUpEvent, Path, PathBuilder, Pixels, Point, Render, Rgba, Window, WindowBounds,
|
||||
WindowOptions, canvas, div, fill, point, prelude::*, px, rgb, size,
|
||||
};
|
||||
@@ -23,9 +24,9 @@ mod example_prelude;
|
||||
// - paint: Called during paint to actually draw
|
||||
|
||||
fn basic_shapes_canvas(colors: &Colors) -> impl IntoElement {
|
||||
let error = colors.error;
|
||||
let success = colors.success;
|
||||
let accent = colors.accent;
|
||||
let error = rgb(0xd32f2f);
|
||||
let success = rgb(0x388e3c);
|
||||
let accent = colors.selected;
|
||||
|
||||
canvas(
|
||||
move |_bounds, _window, _cx| {},
|
||||
@@ -106,8 +107,8 @@ fn create_triangle(p1: Point<Pixels>, p2: Point<Pixels>, p3: Point<Pixels>) -> P
|
||||
}
|
||||
|
||||
fn custom_paths_canvas(colors: &Colors) -> impl IntoElement {
|
||||
let warning = colors.warning;
|
||||
let accent = colors.accent;
|
||||
let warning = rgb(0xf9a825);
|
||||
let accent = colors.selected;
|
||||
|
||||
canvas(
|
||||
move |_bounds, _window, _cx| {},
|
||||
@@ -169,10 +170,10 @@ impl DrawingCanvas {
|
||||
|
||||
fn get_colors(colors: &Colors) -> Vec<Rgba> {
|
||||
vec![
|
||||
colors.error,
|
||||
colors.success,
|
||||
colors.accent,
|
||||
colors.warning,
|
||||
rgb(0xd32f2f),
|
||||
rgb(0x388e3c),
|
||||
colors.selected,
|
||||
rgb(0xf9a825),
|
||||
rgb(0x8b5cf6), // Purple
|
||||
rgb(0x06b6d4), // Cyan
|
||||
]
|
||||
@@ -277,12 +278,12 @@ impl Render for DrawingCanvas {
|
||||
let current_color = self.current_color(&colors);
|
||||
let palette = Self::get_colors(&colors);
|
||||
|
||||
let surface = colors.surface;
|
||||
let surface = colors.container;
|
||||
let border = colors.border;
|
||||
let error = colors.error;
|
||||
let error_hover = colors.error_hover;
|
||||
let error = rgb(0xd32f2f);
|
||||
let error_hover = rgb(0xe04545);
|
||||
let text = colors.selected_text;
|
||||
let text_muted = colors.text_muted;
|
||||
let text_muted = colors.disabled;
|
||||
|
||||
div()
|
||||
.flex()
|
||||
@@ -393,7 +394,7 @@ impl Render for CustomDrawingExample {
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
.child("Canvas element, paths, and interactive painting"),
|
||||
),
|
||||
)
|
||||
@@ -429,7 +430,7 @@ fn section(
|
||||
content: impl IntoElement,
|
||||
height: Pixels,
|
||||
) -> impl IntoElement {
|
||||
let surface: Hsla = colors.surface.into();
|
||||
let surface: Hsla = colors.container.into();
|
||||
|
||||
div()
|
||||
.flex()
|
||||
@@ -455,7 +456,7 @@ fn section(
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
.child(description),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -11,10 +11,11 @@
|
||||
mod example_prelude;
|
||||
|
||||
use example_prelude::init_example;
|
||||
use gpui::colors::Colors;
|
||||
use gpui::{
|
||||
App, Application, Bounds, ClickEvent, Colors, Context, Entity, Half, Hsla, IntoElement,
|
||||
MouseButton, MouseMoveEvent, Pixels, Point, Render, Window, WindowBounds, WindowOptions, div,
|
||||
prelude::*, px, size,
|
||||
App, Application, Bounds, ClickEvent, Context, Entity, Half, Hsla, IntoElement, MouseButton,
|
||||
MouseMoveEvent, Pixels, Point, Render, Window, WindowBounds, WindowOptions, div, prelude::*,
|
||||
px, rgb, size,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -50,7 +51,7 @@ impl Render for ClickDemo {
|
||||
.gap_3()
|
||||
.p_4()
|
||||
.rounded_lg()
|
||||
.bg(colors.surface)
|
||||
.bg(colors.container)
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
@@ -61,7 +62,7 @@ impl Render for ClickDemo {
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
.child("Single click, double click, or triple click the button"),
|
||||
)
|
||||
.child(
|
||||
@@ -70,12 +71,12 @@ impl Render for ClickDemo {
|
||||
.px_4()
|
||||
.py_2()
|
||||
.rounded_md()
|
||||
.bg(colors.accent)
|
||||
.bg(colors.selected)
|
||||
.text_color(colors.selected_text)
|
||||
.text_sm()
|
||||
.cursor_pointer()
|
||||
.hover(|style| style.bg(colors.accent_hover))
|
||||
.active(|style| style.bg(colors.accent_active))
|
||||
.hover(|style| style.bg(colors.selected))
|
||||
.active(|style| style.bg(colors.selected))
|
||||
.child("Click Me!")
|
||||
// on_click receives a ClickEvent with click_count() method
|
||||
.on_click(cx.listener(|this, event: &ClickEvent, _window, cx| {
|
||||
@@ -98,13 +99,13 @@ impl Render for ClickDemo {
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
.child(format!("Total clicks: {}", self.click_count)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
.child(format!("Last: {}", self.last_click_type)),
|
||||
),
|
||||
)
|
||||
@@ -144,7 +145,7 @@ impl Render for HoverDemo {
|
||||
.gap_3()
|
||||
.p_4()
|
||||
.rounded_lg()
|
||||
.bg(colors.surface)
|
||||
.bg(colors.container)
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
@@ -155,7 +156,7 @@ impl Render for HoverDemo {
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
.child("Move your mouse in and out of the target"),
|
||||
)
|
||||
.child(
|
||||
@@ -166,14 +167,14 @@ impl Render for HoverDemo {
|
||||
.rounded_md()
|
||||
.border_2()
|
||||
.border_color(if is_hovered {
|
||||
colors.accent
|
||||
colors.selected
|
||||
} else {
|
||||
colors.border
|
||||
})
|
||||
.bg(if is_hovered {
|
||||
colors.accent_hover
|
||||
colors.selected
|
||||
} else {
|
||||
colors.surface_hover
|
||||
colors.selected
|
||||
})
|
||||
.text_color(if is_hovered {
|
||||
colors.selected_text
|
||||
@@ -199,7 +200,7 @@ impl Render for HoverDemo {
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
.mt_2()
|
||||
.child(format!("Times hovered: {}", self.hover_count)),
|
||||
)
|
||||
@@ -253,7 +254,7 @@ impl Render for MouseEventsDemo {
|
||||
.gap_3()
|
||||
.p_4()
|
||||
.rounded_lg()
|
||||
.bg(colors.surface)
|
||||
.bg(colors.container)
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
@@ -264,7 +265,7 @@ impl Render for MouseEventsDemo {
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
.child("Move and click within the target area"),
|
||||
)
|
||||
.child(
|
||||
@@ -274,14 +275,14 @@ impl Render for MouseEventsDemo {
|
||||
.rounded_md()
|
||||
.border_2()
|
||||
.border_color(if is_pressed {
|
||||
colors.accent
|
||||
colors.selected
|
||||
} else {
|
||||
colors.border
|
||||
})
|
||||
.bg(if is_pressed {
|
||||
colors.accent_hover
|
||||
colors.selected
|
||||
} else {
|
||||
colors.surface_hover
|
||||
colors.selected
|
||||
})
|
||||
.flex()
|
||||
.items_center()
|
||||
@@ -317,7 +318,7 @@ impl Render for MouseEventsDemo {
|
||||
.gap_0p5()
|
||||
.mt_2()
|
||||
.text_xs()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
.children(
|
||||
self.event_log
|
||||
.iter()
|
||||
@@ -396,7 +397,7 @@ impl DragDropDemo {
|
||||
impl Render for DragDropDemo {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let colors = Colors::for_appearance(window);
|
||||
let item_colors = [colors.error, colors.success, colors.warning];
|
||||
let item_colors = [rgb(0xd32f2f), rgb(0x388e3c), rgb(0xf9a825)];
|
||||
|
||||
div()
|
||||
.flex()
|
||||
@@ -404,7 +405,7 @@ impl Render for DragDropDemo {
|
||||
.gap_3()
|
||||
.p_4()
|
||||
.rounded_lg()
|
||||
.bg(colors.surface)
|
||||
.bg(colors.container)
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
@@ -415,7 +416,7 @@ impl Render for DragDropDemo {
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
.child("Drag items to the drop zone below"),
|
||||
)
|
||||
.child(
|
||||
@@ -464,7 +465,7 @@ impl Render for DragDropDemo {
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_xs()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
// on_drop receives the drag data when an item is dropped
|
||||
.on_drop(cx.listener(|this, data: &DragData, _window, cx| {
|
||||
this.dropped_item = Some(*data);
|
||||
@@ -532,7 +533,7 @@ impl Render for InteractiveElementsExample {
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
.child("Click, hover, mouse events, and drag-and-drop in GPUI"),
|
||||
),
|
||||
)
|
||||
|
||||
+19
-18
@@ -10,8 +10,9 @@
|
||||
mod example_prelude;
|
||||
|
||||
use example_prelude::init_example;
|
||||
use gpui::colors::Colors;
|
||||
use gpui::{
|
||||
App, Application, Bounds, Colors, Context, Div, Hsla, Render, Rgba, Window, WindowBounds,
|
||||
App, Application, Bounds, Context, Div, Hsla, Render, Rgba, Window, WindowBounds,
|
||||
WindowOptions, div, prelude::*, px, size,
|
||||
};
|
||||
|
||||
@@ -34,7 +35,7 @@ fn block(label: &'static str, color: Hsla, text_color: Rgba) -> Div {
|
||||
// Flexbox Examples
|
||||
|
||||
fn flexbox_row_example(colors: &Colors) -> impl IntoElement {
|
||||
let text_muted = colors.text_muted;
|
||||
let text_muted = colors.disabled;
|
||||
let text = colors.selected_text;
|
||||
|
||||
div()
|
||||
@@ -59,7 +60,7 @@ fn flexbox_row_example(colors: &Colors) -> impl IntoElement {
|
||||
}
|
||||
|
||||
fn flexbox_column_example(colors: &Colors) -> impl IntoElement {
|
||||
let text_muted = colors.text_muted;
|
||||
let text_muted = colors.disabled;
|
||||
let text = colors.selected_text;
|
||||
|
||||
div()
|
||||
@@ -85,9 +86,9 @@ fn flexbox_column_example(colors: &Colors) -> impl IntoElement {
|
||||
}
|
||||
|
||||
fn flexbox_justify_example(colors: &Colors) -> impl IntoElement {
|
||||
let text_muted = colors.text_muted;
|
||||
let text_muted = colors.disabled;
|
||||
let text = colors.selected_text;
|
||||
let surface = colors.surface;
|
||||
let surface = colors.container;
|
||||
|
||||
div()
|
||||
.flex()
|
||||
@@ -136,7 +137,7 @@ fn flexbox_justify_example(colors: &Colors) -> impl IntoElement {
|
||||
}
|
||||
|
||||
fn flexbox_grow_example(colors: &Colors) -> impl IntoElement {
|
||||
let text_muted = colors.text_muted;
|
||||
let text_muted = colors.disabled;
|
||||
let text = colors.selected_text;
|
||||
|
||||
div()
|
||||
@@ -162,7 +163,7 @@ fn flexbox_grow_example(colors: &Colors) -> impl IntoElement {
|
||||
// Grid Examples
|
||||
|
||||
fn grid_basic_example(colors: &Colors) -> impl IntoElement {
|
||||
let text_muted = colors.text_muted;
|
||||
let text_muted = colors.disabled;
|
||||
let text = colors.selected_text;
|
||||
|
||||
div()
|
||||
@@ -190,7 +191,7 @@ fn grid_basic_example(colors: &Colors) -> impl IntoElement {
|
||||
}
|
||||
|
||||
fn grid_span_example(colors: &Colors) -> impl IntoElement {
|
||||
let text_muted = colors.text_muted;
|
||||
let text_muted = colors.disabled;
|
||||
let text = colors.selected_text;
|
||||
|
||||
div()
|
||||
@@ -232,10 +233,10 @@ fn grid_span_example(colors: &Colors) -> impl IntoElement {
|
||||
// Common Layout Patterns
|
||||
|
||||
fn app_shell_pattern(colors: &Colors) -> impl IntoElement {
|
||||
let text_muted = colors.text_muted;
|
||||
let text_muted = colors.disabled;
|
||||
let text = colors.text;
|
||||
let surface = colors.surface;
|
||||
let surface_hover = colors.surface_hover;
|
||||
let surface = colors.container;
|
||||
let surface_hover = colors.selected;
|
||||
let background = colors.background;
|
||||
let border = colors.border;
|
||||
|
||||
@@ -300,10 +301,10 @@ fn app_shell_pattern(colors: &Colors) -> impl IntoElement {
|
||||
}
|
||||
|
||||
fn centered_pattern(colors: &Colors) -> impl IntoElement {
|
||||
let text_muted = colors.text_muted;
|
||||
let text_muted = colors.disabled;
|
||||
let text = colors.selected_text;
|
||||
let surface = colors.surface;
|
||||
let accent = colors.accent;
|
||||
let surface = colors.container;
|
||||
let accent = colors.selected;
|
||||
|
||||
div()
|
||||
.flex()
|
||||
@@ -337,8 +338,8 @@ fn centered_pattern(colors: &Colors) -> impl IntoElement {
|
||||
}
|
||||
|
||||
fn stack_pattern(colors: &Colors) -> impl IntoElement {
|
||||
let text_muted = colors.text_muted;
|
||||
let surface = colors.surface;
|
||||
let text_muted = colors.disabled;
|
||||
let surface = colors.container;
|
||||
|
||||
div()
|
||||
.flex()
|
||||
@@ -421,7 +422,7 @@ impl Render for LayoutExample {
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
.child("Flexbox, Grid, and common layout patterns in GPUI"),
|
||||
),
|
||||
)
|
||||
@@ -463,7 +464,7 @@ impl Render for LayoutExample {
|
||||
}
|
||||
|
||||
fn section(colors: &Colors, title: &'static str, content: impl IntoElement) -> impl IntoElement {
|
||||
let surface: Hsla = colors.surface.into();
|
||||
let surface: Hsla = colors.container.into();
|
||||
|
||||
div()
|
||||
.flex()
|
||||
|
||||
+40
-38
@@ -6,9 +6,10 @@
|
||||
//! 2. Conditional styling - when, when_some, map
|
||||
//! 3. Theming patterns - using Colors for consistent styling
|
||||
|
||||
use gpui::colors::Colors;
|
||||
use gpui::{
|
||||
App, Application, Bounds, Colors, Context, FocusHandle, Hsla, KeyBinding, Menu, MenuItem,
|
||||
Render, Rgba, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, rgb, size,
|
||||
App, Application, Bounds, Context, FocusHandle, Hsla, KeyBinding, Menu, MenuItem, Render, Rgba,
|
||||
Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, rgb, size,
|
||||
};
|
||||
|
||||
actions!(styling_example, [Quit, Tab, TabPrev]);
|
||||
@@ -20,9 +21,9 @@ fn interactive_button(
|
||||
label: &'static str,
|
||||
colors: &Colors,
|
||||
) -> impl IntoElement {
|
||||
let accent = colors.accent;
|
||||
let accent_hover = colors.accent_hover;
|
||||
let accent_active = colors.accent_active;
|
||||
let accent = colors.selected;
|
||||
let accent_hover = colors.selected;
|
||||
let accent_active = colors.selected;
|
||||
let text = colors.selected_text;
|
||||
|
||||
div()
|
||||
@@ -45,10 +46,10 @@ fn focus_button(
|
||||
focus_handle: &FocusHandle,
|
||||
colors: &Colors,
|
||||
) -> impl IntoElement {
|
||||
let surface = colors.surface;
|
||||
let surface_hover = colors.surface_hover;
|
||||
let surface = colors.container;
|
||||
let surface_hover = colors.selected;
|
||||
let text = colors.text;
|
||||
let accent = colors.accent;
|
||||
let accent = colors.selected;
|
||||
let focus_ring: Rgba = rgb(0x60a5fa);
|
||||
|
||||
div()
|
||||
@@ -77,7 +78,7 @@ fn interactive_states_section(colors: &Colors) -> impl IntoElement {
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
.child("hover() / active() - Mouse interaction states"),
|
||||
)
|
||||
.child(
|
||||
@@ -93,10 +94,10 @@ fn interactive_states_section(colors: &Colors) -> impl IntoElement {
|
||||
|
||||
fn status_badge(status: &'static str, variant: StatusVariant, colors: &Colors) -> impl IntoElement {
|
||||
let (bg, text): (Rgba, Rgba) = match variant {
|
||||
StatusVariant::Success => (colors.success, colors.selected_text),
|
||||
StatusVariant::Warning => (colors.warning, rgb(0x000000)),
|
||||
StatusVariant::Error => (colors.error, colors.selected_text),
|
||||
StatusVariant::Neutral => (colors.surface, colors.text),
|
||||
StatusVariant::Success => (rgb(0x388e3c), colors.selected_text),
|
||||
StatusVariant::Warning => (rgb(0xf9a825), rgb(0x000000)),
|
||||
StatusVariant::Error => (rgb(0xd32f2f), colors.selected_text),
|
||||
StatusVariant::Neutral => (colors.container, colors.text),
|
||||
};
|
||||
|
||||
div()
|
||||
@@ -124,11 +125,11 @@ fn list_item(
|
||||
is_disabled: bool,
|
||||
colors: &Colors,
|
||||
) -> impl IntoElement {
|
||||
let surface = colors.surface;
|
||||
let surface_hover = colors.surface_hover;
|
||||
let surface = colors.container;
|
||||
let surface_hover = colors.selected;
|
||||
let text = colors.text;
|
||||
let text_muted = colors.text_muted;
|
||||
let accent = colors.accent;
|
||||
let text_muted = colors.disabled;
|
||||
let accent = colors.selected;
|
||||
|
||||
div()
|
||||
.id(id)
|
||||
@@ -167,7 +168,7 @@ fn conditional_section(colors: &Colors) -> impl IntoElement {
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
.child("when() - Apply styles conditionally"),
|
||||
)
|
||||
.child(
|
||||
@@ -182,7 +183,7 @@ fn conditional_section(colors: &Colors) -> impl IntoElement {
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
.mt_2()
|
||||
.child("Status badges with variant-based styling"),
|
||||
)
|
||||
@@ -205,11 +206,11 @@ fn card_with_group_hover(
|
||||
description: &'static str,
|
||||
colors: &Colors,
|
||||
) -> impl IntoElement {
|
||||
let surface = colors.surface;
|
||||
let surface = colors.container;
|
||||
let border = colors.border;
|
||||
let accent = colors.accent;
|
||||
let accent = colors.selected;
|
||||
let text = colors.text;
|
||||
let text_muted = colors.text_muted;
|
||||
let text_muted = colors.disabled;
|
||||
|
||||
div()
|
||||
.id(id)
|
||||
@@ -259,7 +260,7 @@ fn group_hover_section(colors: &Colors) -> impl IntoElement {
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
.child("group() / group_hover() - Parent hover affects children"),
|
||||
)
|
||||
.child(
|
||||
@@ -292,7 +293,7 @@ struct StylingExample {
|
||||
impl StylingExample {
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
window.focus(&focus_handle);
|
||||
window.focus(&focus_handle, cx);
|
||||
|
||||
let buttons = vec![
|
||||
cx.focus_handle().tab_index(1).tab_stop(true),
|
||||
@@ -306,12 +307,12 @@ impl StylingExample {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tab(&mut self, _: &Tab, window: &mut Window, _: &mut Context<Self>) {
|
||||
window.focus_next();
|
||||
fn on_tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
|
||||
window.focus_next(cx);
|
||||
}
|
||||
|
||||
fn on_tab_prev(&mut self, _: &TabPrev, window: &mut Window, _: &mut Context<Self>) {
|
||||
window.focus_prev();
|
||||
fn on_tab_prev(&mut self, _: &TabPrev, window: &mut Window, cx: &mut Context<Self>) {
|
||||
window.focus_prev(cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,7 +350,7 @@ impl Render for StylingExample {
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
.child("Interactive states, conditional styling, and theming"),
|
||||
),
|
||||
)
|
||||
@@ -368,7 +369,7 @@ impl Render for StylingExample {
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
.child("focus() / focus_visible() - Keyboard navigation"),
|
||||
)
|
||||
.child(
|
||||
@@ -415,7 +416,7 @@ impl Render for StylingExample {
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.disabled)
|
||||
.child("Using Colors::for_appearance() for consistent theming"),
|
||||
)
|
||||
.child(
|
||||
@@ -424,11 +425,11 @@ impl Render for StylingExample {
|
||||
.flex_wrap()
|
||||
.gap_2()
|
||||
.child(color_swatch(&colors, "background", colors.background))
|
||||
.child(color_swatch(&colors, "surface", colors.surface))
|
||||
.child(color_swatch(&colors, "accent", colors.accent))
|
||||
.child(color_swatch(&colors, "success", colors.success))
|
||||
.child(color_swatch(&colors, "warning", colors.warning))
|
||||
.child(color_swatch(&colors, "error", colors.error))
|
||||
.child(color_swatch(&colors, "container", colors.container))
|
||||
.child(color_swatch(&colors, "selected", colors.selected))
|
||||
.child(color_swatch(&colors, "success", rgb(0x388e3c)))
|
||||
.child(color_swatch(&colors, "warning", rgb(0xf9a825)))
|
||||
.child(color_swatch(&colors, "error", rgb(0xd32f2f)))
|
||||
.child(color_swatch(&colors, "border", colors.border)),
|
||||
),
|
||||
)),
|
||||
@@ -437,7 +438,7 @@ impl Render for StylingExample {
|
||||
}
|
||||
|
||||
fn section(colors: &Colors, title: &'static str, content: impl IntoElement) -> impl IntoElement {
|
||||
let surface: Hsla = colors.surface.into();
|
||||
let surface: Hsla = colors.container.into();
|
||||
let border: Hsla = colors.border.into();
|
||||
|
||||
div()
|
||||
@@ -460,7 +461,7 @@ fn section(colors: &Colors, title: &'static str, content: impl IntoElement) -> i
|
||||
}
|
||||
|
||||
fn color_swatch(colors: &Colors, name: &'static str, color: Rgba) -> impl IntoElement {
|
||||
let text_muted = colors.text_muted;
|
||||
let text_muted = colors.disabled;
|
||||
|
||||
div()
|
||||
.flex()
|
||||
@@ -490,6 +491,7 @@ fn main() {
|
||||
cx.set_menus(vec![Menu {
|
||||
name: "Styling".into(),
|
||||
items: vec![MenuItem::action("Quit", Quit)],
|
||||
disabled: false,
|
||||
}]);
|
||||
cx.on_window_closed(|cx| {
|
||||
if cx.windows().is_empty() {
|
||||
|
||||
+20
-19
@@ -14,15 +14,16 @@ mod example_prelude;
|
||||
|
||||
use example_prelude::init_example;
|
||||
use gpui::{
|
||||
App, Application, Bounds, Colors, Context, FontStyle, FontWeight, Hsla, Render, StyledText,
|
||||
TextOverflow, Window, WindowBounds, WindowOptions, div, prelude::*, px, relative, size,
|
||||
App, Application, Bounds, Context, FontStyle, FontWeight, Hsla, Render, StyledText,
|
||||
TextOverflow, Window, WindowBounds, WindowOptions, colors::Colors, current_platform, div,
|
||||
prelude::*, px, relative, rgb, size,
|
||||
};
|
||||
|
||||
// Text Styling Examples
|
||||
|
||||
fn text_sizes_example(colors: &Colors) -> impl IntoElement {
|
||||
let text = colors.text;
|
||||
let text_muted = colors.text_muted;
|
||||
let text_muted = colors.disabled;
|
||||
|
||||
div()
|
||||
.flex()
|
||||
@@ -50,7 +51,7 @@ fn text_sizes_example(colors: &Colors) -> impl IntoElement {
|
||||
|
||||
fn text_weights_example(colors: &Colors) -> impl IntoElement {
|
||||
let text = colors.text;
|
||||
let text_muted = colors.text_muted;
|
||||
let text_muted = colors.disabled;
|
||||
|
||||
div()
|
||||
.flex()
|
||||
@@ -116,8 +117,8 @@ fn text_weights_example(colors: &Colors) -> impl IntoElement {
|
||||
|
||||
fn text_alignment_example(colors: &Colors) -> impl IntoElement {
|
||||
let text = colors.text;
|
||||
let text_muted = colors.text_muted;
|
||||
let surface = colors.surface;
|
||||
let text_muted = colors.disabled;
|
||||
let surface = colors.container;
|
||||
|
||||
div()
|
||||
.flex()
|
||||
@@ -167,9 +168,9 @@ fn text_alignment_example(colors: &Colors) -> impl IntoElement {
|
||||
|
||||
fn text_decoration_example(colors: &Colors) -> impl IntoElement {
|
||||
let text = colors.text;
|
||||
let text_muted = colors.text_muted;
|
||||
let accent = colors.accent;
|
||||
let error = colors.error;
|
||||
let text_muted = colors.disabled;
|
||||
let accent = colors.selected;
|
||||
let error = rgb(0xd32f2f);
|
||||
|
||||
div()
|
||||
.flex()
|
||||
@@ -208,8 +209,8 @@ fn text_decoration_example(colors: &Colors) -> impl IntoElement {
|
||||
|
||||
fn text_overflow_example(colors: &Colors) -> impl IntoElement {
|
||||
let text = colors.text;
|
||||
let text_muted = colors.text_muted;
|
||||
let surface = colors.surface;
|
||||
let text_muted = colors.disabled;
|
||||
let surface = colors.container;
|
||||
let border = colors.border;
|
||||
|
||||
let long_text = "The quick brown fox jumps over the lazy dog. This is a long sentence that will overflow its container.";
|
||||
@@ -333,7 +334,7 @@ fn text_overflow_example(colors: &Colors) -> impl IntoElement {
|
||||
|
||||
fn styled_text_example(colors: &Colors) -> impl IntoElement {
|
||||
let text = colors.text;
|
||||
let text_muted = colors.text_muted;
|
||||
let text_muted = colors.disabled;
|
||||
|
||||
div()
|
||||
.flex()
|
||||
@@ -358,8 +359,8 @@ fn styled_text_example(colors: &Colors) -> impl IntoElement {
|
||||
|
||||
fn character_grid_example(colors: &Colors) -> impl IntoElement {
|
||||
let text = colors.text;
|
||||
let text_muted = colors.text_muted;
|
||||
let surface = colors.surface;
|
||||
let text_muted = colors.disabled;
|
||||
let surface = colors.container;
|
||||
let border = colors.border;
|
||||
|
||||
let characters = [
|
||||
@@ -414,8 +415,8 @@ fn character_grid_example(colors: &Colors) -> impl IntoElement {
|
||||
|
||||
fn line_height_example(colors: &Colors) -> impl IntoElement {
|
||||
let text = colors.text;
|
||||
let text_muted = colors.text_muted;
|
||||
let surface = colors.surface;
|
||||
let text_muted = colors.disabled;
|
||||
let surface = colors.container;
|
||||
|
||||
div()
|
||||
.flex()
|
||||
@@ -500,7 +501,7 @@ impl Render for TextExample {
|
||||
.child("Text & Typography"),
|
||||
)
|
||||
.child(
|
||||
div().text_sm().text_color(colors.text_muted).child(
|
||||
div().text_sm().text_color(colors.disabled).child(
|
||||
"Font styling, alignment, overflow, and unicode support",
|
||||
),
|
||||
),
|
||||
@@ -546,7 +547,7 @@ impl Render for TextExample {
|
||||
}
|
||||
|
||||
fn section(colors: &Colors, title: &'static str, content: impl IntoElement) -> impl IntoElement {
|
||||
let surface: Hsla = colors.surface.into();
|
||||
let surface: Hsla = colors.container.into();
|
||||
|
||||
div()
|
||||
.flex()
|
||||
@@ -566,7 +567,7 @@ fn section(colors: &Colors, title: &'static str, content: impl IntoElement) -> i
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
Application::with_platform(current_platform(false)).run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(650.), px(900.)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
|
||||
@@ -29,7 +29,7 @@ impl Example {
|
||||
];
|
||||
|
||||
let focus_handle = cx.focus_handle();
|
||||
window.focus(&focus_handle);
|
||||
window.focus(&focus_handle, cx);
|
||||
|
||||
Self {
|
||||
focus_handle,
|
||||
@@ -40,13 +40,13 @@ impl Example {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tab(&mut self, _: &Tab, window: &mut Window, _: &mut Context<Self>) {
|
||||
window.focus_next();
|
||||
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, _: &mut Context<Self>) {
|
||||
window.focus_prev();
|
||||
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!");
|
||||
}
|
||||
|
||||
@@ -546,8 +546,15 @@ impl Element for TextElement {
|
||||
window.paint_quad(selection)
|
||||
}
|
||||
let line = prepaint.line.take().unwrap();
|
||||
line.paint(bounds.origin, window.line_height(), window, cx)
|
||||
.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()
|
||||
@@ -736,7 +743,7 @@ fn main() {
|
||||
|
||||
window
|
||||
.update(cx, |view, window, cx| {
|
||||
window.focus(&view.text_input.focus_handle(cx));
|
||||
window.focus(&view.text_input.focus_handle(cx), cx);
|
||||
cx.activate(true);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
@@ -55,7 +55,7 @@ fn main() {
|
||||
cx.activate(false);
|
||||
cx.new(|cx| {
|
||||
let focus_handle = cx.focus_handle();
|
||||
focus_handle.focus(window);
|
||||
focus_handle.focus(window, cx);
|
||||
ExampleWindow { focus_handle }
|
||||
})
|
||||
},
|
||||
@@ -72,7 +72,7 @@ fn main() {
|
||||
|window, cx| {
|
||||
cx.new(|cx| {
|
||||
let focus_handle = cx.focus_handle();
|
||||
focus_handle.focus(window);
|
||||
focus_handle.focus(window, cx);
|
||||
ExampleWindow { focus_handle }
|
||||
})
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::path::PathBuf;
|
||||
use anyhow::Result;
|
||||
use gpui::{
|
||||
App, Application, AssetSource, Bounds, Context, SharedString, Window, WindowBounds,
|
||||
WindowOptions, div, prelude::*, px, rgb, size, svg,
|
||||
WindowOptions, current_platform, div, prelude::*, px, rgb, size, svg,
|
||||
};
|
||||
|
||||
struct Assets {
|
||||
@@ -68,7 +68,7 @@ impl Render for SvgExample {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new()
|
||||
Application::with_platform(current_platform(false))
|
||||
.with_assets(Assets {
|
||||
base: PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples"),
|
||||
})
|
||||
|
||||
@@ -22,7 +22,7 @@ impl Example {
|
||||
];
|
||||
|
||||
let focus_handle = cx.focus_handle();
|
||||
window.focus(&focus_handle);
|
||||
window.focus(&focus_handle, cx);
|
||||
|
||||
Self {
|
||||
focus_handle,
|
||||
@@ -31,13 +31,13 @@ impl Example {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tab(&mut self, _: &Tab, window: &mut Window, _: &mut Context<Self>) {
|
||||
window.focus_next();
|
||||
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, _: &mut Context<Self>) {
|
||||
window.focus_prev();
|
||||
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`.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{
|
||||
App, Application, Bounds, Context, KeyBinding, PromptButton, PromptLevel, Timer, Window,
|
||||
WindowBounds, WindowKind, WindowOptions, actions, div, prelude::*, px, rgb, size,
|
||||
App, Application, Bounds, Context, KeyBinding, PromptButton, PromptLevel, Window, WindowBounds,
|
||||
WindowKind, WindowOptions, actions, div, prelude::*, px, rgb, size,
|
||||
};
|
||||
|
||||
struct SubWindow {
|
||||
@@ -188,7 +188,7 @@ impl Render for WindowDemo {
|
||||
// Restore the application after 3 seconds
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
Timer::after(std::time::Duration::from_secs(3)).await;
|
||||
smol::Timer::after(std::time::Duration::from_secs(3)).await;
|
||||
cx.update(|_, cx| {
|
||||
cx.activate(false);
|
||||
})
|
||||
|
||||
@@ -29,6 +29,7 @@ pub fn init_example(cx: &mut App, name: impl Into<SharedString>) {
|
||||
cx.set_menus(vec![Menu {
|
||||
name: name.into(),
|
||||
items: vec![MenuItem::action("Quit", Quit)],
|
||||
disabled: false,
|
||||
}]);
|
||||
|
||||
// Quit the app when all windows are closed
|
||||
|
||||
Generated
+116
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"nodes": {
|
||||
"crane": {
|
||||
"locked": {
|
||||
"lastModified": 1773857772,
|
||||
"narHash": "sha256-5xsK26KRHf0WytBtsBnQYC/lTWDhQuT57HJ7SzuqZcM=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "b556d7bbae5ff86e378451511873dfd07e4504cd",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1774076307,
|
||||
"narHash": "sha256-v8axK9HGgVERw9oG3SKdsuE+ri0GPUZDyRBN4GLqQ1c=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "556198cc6c69c0a13228a15e33b2360f333b0092",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1773840656,
|
||||
"narHash": "sha256-9tpvMGFteZnd3gRQZFlRCohVpqooygFuy9yjuyRL2C0=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "9cf7092bdd603554bd8b63c216e8943cf9b12512",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"crane": "crane",
|
||||
"fenix": "fenix",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1774036669,
|
||||
"narHash": "sha256-EWhsBSh/h1VcyLKXuTEyH8lNVB2ktuKVkqx8dkQ6hxk=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "0cf3e8a07f0e29825f5db78840e646a4bb519742",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "rust-lang",
|
||||
"ref": "nightly",
|
||||
"repo": "rust-analyzer",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
description = "Standalone GPUI build";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
fenix = {
|
||||
url = "github:nix-community/fenix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
crane.url = "github:ipetkov/crane";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
fenix,
|
||||
crane,
|
||||
flake-utils,
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
inherit (pkgs) lib;
|
||||
|
||||
toolchain = fenix.packages.${system}.latest.withComponents [
|
||||
"cargo"
|
||||
"rustc"
|
||||
"rust-src"
|
||||
"rustfmt"
|
||||
"clippy"
|
||||
];
|
||||
|
||||
craneLib = (crane.mkLib pkgs).overrideToolchain toolchain;
|
||||
|
||||
src = lib.cleanSourceWith {
|
||||
src = craneLib.path ./.;
|
||||
filter =
|
||||
path: type:
|
||||
(craneLib.filterCargoSources path type)
|
||||
|| (lib.hasSuffix ".metal" path)
|
||||
|| (lib.hasSuffix ".wgsl" path)
|
||||
|| (lib.hasSuffix ".hlsl" path)
|
||||
|| (lib.hasSuffix ".glsl" path);
|
||||
};
|
||||
|
||||
linuxLibs = with pkgs; [
|
||||
alsa-lib
|
||||
libdrm
|
||||
mesa # provides libgbm
|
||||
libxkbcommon
|
||||
libva
|
||||
vulkan-loader
|
||||
wayland
|
||||
xorg.libX11
|
||||
xorg.libxcb
|
||||
];
|
||||
|
||||
commonArgs = {
|
||||
pname = "gpui-ce";
|
||||
version = "0.3.3";
|
||||
|
||||
inherit src;
|
||||
strictDeps = true;
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
cmake
|
||||
pkg-config
|
||||
rustPlatform.bindgenHook
|
||||
];
|
||||
|
||||
buildInputs =
|
||||
with pkgs;
|
||||
[
|
||||
fontconfig
|
||||
freetype
|
||||
openssl
|
||||
zlib
|
||||
]
|
||||
++ lib.optionals stdenv.isLinux linuxLibs
|
||||
++ lib.optionals stdenv.isDarwin [
|
||||
apple-sdk_15
|
||||
(darwinMinVersionHook "11.0")
|
||||
];
|
||||
|
||||
env = lib.optionalAttrs pkgs.stdenv.isLinux {
|
||||
LD_LIBRARY_PATH = lib.makeLibraryPath linuxLibs;
|
||||
};
|
||||
|
||||
cargoExtraArgs = "--features runtime_shaders";
|
||||
};
|
||||
|
||||
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
||||
|
||||
gpui = craneLib.buildPackage (
|
||||
commonArgs
|
||||
// {
|
||||
inherit cargoArtifacts;
|
||||
}
|
||||
);
|
||||
in
|
||||
{
|
||||
packages.default = gpui;
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
inputsFrom = [ gpui ];
|
||||
packages = [ toolchain ];
|
||||
|
||||
shellHook = ''
|
||||
export RUST_BACKTRACE=1
|
||||
export RUST_SRC_PATH="${toolchain}/lib/rustlib/src/rust/library"
|
||||
${lib.optionalString pkgs.stdenv.isLinux ''
|
||||
export LD_LIBRARY_PATH="${lib.makeLibraryPath linuxLibs}:$LD_LIBRARY_PATH"
|
||||
''}
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
set -x
|
||||
"${CARGO:-cargo}" clippy "$@" --release --all-targets --all-features -- --deny warnings
|
||||
|
||||
# If local, run other checks if we have the tools installed.
|
||||
if [[ -z "${GITHUB_ACTIONS+x}" ]]; then
|
||||
which cargo-machete >/dev/null 2>&1 || exit 0
|
||||
cargo machete
|
||||
|
||||
which typos >/dev/null 2>&1 || exit 0
|
||||
typos --config typos.toml
|
||||
fi
|
||||
@@ -1,32 +0,0 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "Your PATH entries:"
|
||||
$env:Path -split ";" | ForEach-Object { Write-Host " $_" }
|
||||
|
||||
$needAddWorkspace = $false
|
||||
if ($args -notcontains "-p" -and $args -notcontains "--package")
|
||||
{
|
||||
$needAddWorkspace = $true
|
||||
}
|
||||
|
||||
# https://stackoverflow.com/questions/41324882/how-to-run-a-powershell-script-with-verbose-output/70020655#70020655
|
||||
# Set-PSDebug -Trace 2
|
||||
|
||||
if ($env:CARGO)
|
||||
{
|
||||
$Cargo = $env:CARGO
|
||||
} elseif (Get-Command "cargo" -ErrorAction SilentlyContinue)
|
||||
{
|
||||
$Cargo = "cargo"
|
||||
} else
|
||||
{
|
||||
Write-Error "Could not find cargo in path." -ErrorAction Stop
|
||||
}
|
||||
|
||||
if ($needAddWorkspace)
|
||||
{
|
||||
& $Cargo clippy @args --workspace --release --all-targets --all-features -- --deny warnings
|
||||
} else
|
||||
{
|
||||
& $Cargo clippy @args --release --all-targets --all-features -- --deny warnings
|
||||
}
|
||||
-246
@@ -1,246 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -xeuo pipefail
|
||||
|
||||
# if root or if sudo/unavailable, define an empty variable
|
||||
if [ "$(id -u)" -eq 0 ]
|
||||
then maysudo=''
|
||||
else maysudo="$(command -v sudo || command -v doas || true)"
|
||||
fi
|
||||
|
||||
function finalize {
|
||||
# after packages install (curl, etc), get the rust toolchain
|
||||
which rustup > /dev/null 2>&1 || curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
cat <<EOF
|
||||
Note: It's recommended to install and configure Wild or Mold for faster builds.
|
||||
Run script/install-wild or script/install-mold.
|
||||
|
||||
Finished installing Linux dependencies with script/linux
|
||||
EOF
|
||||
}
|
||||
|
||||
# Ubuntu, Debian, Mint, Kali, Pop!_OS, Raspbian, etc.
|
||||
apt=$(command -v apt-get || true)
|
||||
if [[ -n $apt ]]; then
|
||||
deps=(
|
||||
gcc
|
||||
g++
|
||||
libasound2-dev
|
||||
libfontconfig-dev
|
||||
libwayland-dev
|
||||
libx11-xcb-dev
|
||||
libxkbcommon-x11-dev
|
||||
libssl-dev
|
||||
libzstd-dev
|
||||
libvulkan1
|
||||
libgit2-dev
|
||||
make
|
||||
cmake
|
||||
clang
|
||||
jq
|
||||
git
|
||||
curl
|
||||
gettext-base
|
||||
elfutils
|
||||
libsqlite3-dev
|
||||
musl-tools
|
||||
musl-dev
|
||||
build-essential
|
||||
)
|
||||
if (grep -qP 'PRETTY_NAME="(Debian|Raspbian).+13' /etc/os-release); then
|
||||
# libstdc++-14-dev is in build-essential
|
||||
deps+=( mold )
|
||||
elif (grep -qP 'PRETTY_NAME="(Linux Mint 22|.+24\.(04|10))' /etc/os-release); then
|
||||
deps+=( mold libstdc++-14-dev )
|
||||
elif (grep -qP 'PRETTY_NAME="((Debian|Raspbian).+12|Linux Mint 21|.+22\.04)' /etc/os-release); then
|
||||
deps+=( mold libstdc++-12-dev )
|
||||
elif (grep -qP 'PRETTY_NAME="((Debian|Raspbian).+11|Linux Mint 20|.+20\.04)' /etc/os-release); then
|
||||
deps+=( libstdc++-10-dev )
|
||||
fi
|
||||
|
||||
$maysudo "$apt" update
|
||||
$maysudo "$apt" install -y "${deps[@]}"
|
||||
finalize
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Fedora, CentOS, RHEL, Alma, Amazon 2023, Oracle, etc.
|
||||
dnf=$(command -v dnf || true)
|
||||
# Old Redhat (yum only): Amazon Linux 2, Oracle Linux 7, etc.
|
||||
yum=$(command -v yum || true)
|
||||
|
||||
if [[ -n $dnf ]] || [[ -n $yum ]]; then
|
||||
pkg_cmd="${dnf:-${yum}}"
|
||||
deps=(
|
||||
musl-gcc
|
||||
gcc
|
||||
clang
|
||||
cmake
|
||||
alsa-lib-devel
|
||||
fontconfig-devel
|
||||
wayland-devel
|
||||
libxcb-devel
|
||||
libxkbcommon-x11-devel
|
||||
openssl-devel
|
||||
libzstd-devel
|
||||
vulkan-loader
|
||||
sqlite-devel
|
||||
jq
|
||||
git
|
||||
tar
|
||||
)
|
||||
# perl used for building openssl-sys crate. See: https://docs.rs/openssl/latest/openssl/
|
||||
if grep -qP '^ID="?(fedora)' /etc/os-release; then
|
||||
deps+=(
|
||||
perl-FindBin
|
||||
perl-IPC-Cmd
|
||||
perl-File-Compare
|
||||
perl-File-Copy
|
||||
mold
|
||||
)
|
||||
elif grep -qP '^ID="?(rhel|rocky|alma|centos|ol)' /etc/os-release; then
|
||||
deps+=( perl-interpreter )
|
||||
fi
|
||||
|
||||
# gcc-c++ is g++ on RHEL8 and 8.x clones
|
||||
if grep -qP '^ID="?(rhel|rocky|alma|centos|ol)' /etc/os-release \
|
||||
&& grep -qP '^VERSION_ID="?(8)' /etc/os-release; then
|
||||
deps+=( gcc-c++ )
|
||||
else
|
||||
deps+=( g++ )
|
||||
fi
|
||||
|
||||
# libxkbcommon-x11-devel is in a non-default repo on RHEL 8.x/9.x (except on AmazonLinux)
|
||||
if grep -qP '^VERSION_ID="?(8|9)' /etc/os-release && grep -qP '^ID="?(rhel|rocky|centos|alma|ol)' /etc/os-release; then
|
||||
$maysudo dnf install -y 'dnf-command(config-manager)'
|
||||
if grep -qP '^PRETTY_NAME="(AlmaLinux 8|Rocky Linux 8)' /etc/os-release; then
|
||||
$maysudo dnf config-manager --set-enabled powertools
|
||||
elif grep -qP '^PRETTY_NAME="((AlmaLinux|Rocky|CentOS Stream) 9|Red Hat.+(8|9))' /etc/os-release; then
|
||||
$maysudo dnf config-manager --set-enabled crb
|
||||
elif grep -qP '^PRETTY_NAME="Oracle Linux Server 8' /etc/os-release; then
|
||||
$maysudo dnf config-manager --set-enabled ol8_codeready_builder
|
||||
elif grep -qP '^PRETTY_NAME="Oracle Linux Server 9' /etc/os-release; then
|
||||
$maysudo dnf config-manager --set-enabled ol9_codeready_builder
|
||||
else
|
||||
echo "Unexpected distro" && grep 'PRETTY_NAME' /etc/os-release && exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
$maysudo "$pkg_cmd" install -y "${deps[@]}"
|
||||
finalize
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# openSUSE
|
||||
# https://software.opensuse.org/
|
||||
zyp=$(command -v zypper || true)
|
||||
if [[ -n $zyp ]]; then
|
||||
deps=(
|
||||
alsa-devel
|
||||
clang
|
||||
cmake
|
||||
fontconfig-devel
|
||||
gcc
|
||||
gcc-c++
|
||||
git
|
||||
gzip
|
||||
jq
|
||||
libvulkan1
|
||||
libx11-devel
|
||||
libxcb-devel
|
||||
libxkbcommon-devel
|
||||
libxkbcommon-x11-devel
|
||||
libzstd-devel
|
||||
make
|
||||
mold
|
||||
openssl-devel
|
||||
sqlite3-devel
|
||||
tar
|
||||
wayland-devel
|
||||
xcb-util-devel
|
||||
)
|
||||
$maysudo "$zyp" install -y "${deps[@]}"
|
||||
finalize
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Arch, Manjaro, etc.
|
||||
# https://archlinux.org/packages
|
||||
pacman=$(command -v pacman || true)
|
||||
if [[ -n $pacman ]]; then
|
||||
deps=(
|
||||
gcc
|
||||
clang
|
||||
musl
|
||||
cmake
|
||||
alsa-lib
|
||||
fontconfig
|
||||
wayland
|
||||
libgit2
|
||||
libxcb
|
||||
libxkbcommon-x11
|
||||
openssl
|
||||
zstd
|
||||
pkgconf
|
||||
mold
|
||||
sqlite
|
||||
jq
|
||||
git
|
||||
)
|
||||
$maysudo "$pacman" -Syu --needed --noconfirm "${deps[@]}"
|
||||
finalize
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Void
|
||||
# https://voidlinux.org/packages/
|
||||
xbps=$(command -v xbps-install || true)
|
||||
if [[ -n $xbps ]]; then
|
||||
deps=(
|
||||
gettext-devel
|
||||
clang
|
||||
cmake
|
||||
jq
|
||||
elfutils-devel
|
||||
gcc
|
||||
alsa-lib-devel
|
||||
fontconfig-devel
|
||||
libxcb-devel
|
||||
libxkbcommon-devel
|
||||
libzstd-devel
|
||||
openssl-devel
|
||||
wayland-devel
|
||||
vulkan-loader
|
||||
mold
|
||||
sqlite-devel
|
||||
)
|
||||
$maysudo "$xbps" -Syu "${deps[@]}"
|
||||
finalize
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Gentoo
|
||||
# https://packages.gentoo.org/
|
||||
emerge=$(command -v emerge || true)
|
||||
if [[ -n $emerge ]]; then
|
||||
deps=(
|
||||
app-arch/zstd
|
||||
app-misc/jq
|
||||
dev-libs/openssl
|
||||
dev-libs/wayland
|
||||
dev-util/cmake
|
||||
media-libs/alsa-lib
|
||||
media-libs/fontconfig
|
||||
media-libs/vulkan-loader
|
||||
x11-libs/libxcb
|
||||
x11-libs/libxkbcommon
|
||||
sys-devel/mold
|
||||
dev-db/sqlite
|
||||
)
|
||||
$maysudo "$emerge" -u "${deps[@]}"
|
||||
finalize
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Unsupported Linux distribution in script/linux"
|
||||
exit 1
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export GPUTOOLS_LOAD_GTMTLCAPTURE=1
|
||||
export DYLD_LIBRARY_PATH="/usr/lib/system/introspection"
|
||||
export METAL_LOAD_INTERPOSER=1
|
||||
export DYLD_INSERT_LIBRARIES="/usr/lib/libMTLCapture.dylib"
|
||||
export DYMTL_TOOLS_DYLIB_PATH="/usr/lib/libMTLCapture.dylib"
|
||||
export METAL_DEVICE_WRAPPER_TYPE=1
|
||||
export GPUProfilerEnabled="YES"
|
||||
export METAL_DEBUG_ERROR_MODE=0
|
||||
export LD_LIBRARY_PATH="/Applications/Xcode.app/Contents/Developer/../SharedFrameworks/"
|
||||
|
||||
cargo run "$@"
|
||||
@@ -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);
|
||||
//! });
|
||||
//! ```
|
||||
+29
-16
@@ -1,7 +1,7 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
pub use gpui_macros::Action;
|
||||
pub use no_action::{NoAction, is_no_action};
|
||||
pub use no_action::{NoAction, Unbind, is_no_action, is_unbind};
|
||||
use serde_json::json;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
@@ -290,19 +290,6 @@ impl ActionRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn load_action<A: Action>(&mut self) {
|
||||
self.insert_action(MacroActionData {
|
||||
name: A::name_for_type(),
|
||||
type_id: TypeId::of::<A>(),
|
||||
build: A::build,
|
||||
json_schema: A::action_json_schema,
|
||||
deprecated_aliases: A::deprecated_aliases(),
|
||||
deprecation_message: A::deprecation_message(),
|
||||
documentation: A::documentation(),
|
||||
});
|
||||
}
|
||||
|
||||
fn insert_action(&mut self, action: MacroActionData) {
|
||||
let name = action.name;
|
||||
if self.by_name.contains_key(name) {
|
||||
@@ -397,6 +384,16 @@ impl ActionRegistry {
|
||||
.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
|
||||
}
|
||||
@@ -422,7 +419,8 @@ pub fn generate_list_of_all_registered_actions() -> impl Iterator<Item = MacroAc
|
||||
|
||||
mod no_action {
|
||||
use crate as gpui;
|
||||
use std::any::Any as _;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
actions!(
|
||||
zed,
|
||||
@@ -433,8 +431,23 @@ mod no_action {
|
||||
]
|
||||
);
|
||||
|
||||
/// 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().type_id() == (NoAction {}).type_id()
|
||||
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>()
|
||||
}
|
||||
}
|
||||
|
||||
+263
-112
@@ -1,13 +1,14 @@
|
||||
use crate::scheduler::Instant;
|
||||
use std::{
|
||||
any::{TypeId, type_name},
|
||||
cell::{BorrowMutError, Ref, RefCell, RefMut},
|
||||
cell::{BorrowMutError, Cell, Ref, RefCell, RefMut},
|
||||
marker::PhantomData,
|
||||
mem,
|
||||
ops::{Deref, DerefMut},
|
||||
path::{Path, PathBuf},
|
||||
rc::{Rc, Weak},
|
||||
sync::{Arc, atomic::Ordering::SeqCst},
|
||||
time::{Duration, Instant},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
@@ -25,25 +26,31 @@ pub use async_context::*;
|
||||
use collections::{FxHashMap, FxHashSet, HashMap, VecDeque};
|
||||
pub use context::*;
|
||||
pub use entity_map::*;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use headless_app_context::*;
|
||||
use http_client::{HttpClient, Url};
|
||||
use smallvec::SmallVec;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use test_app::*;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use test_context::*;
|
||||
use util::{ResultExt, debug_panic};
|
||||
#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
|
||||
pub use visual_test_context::*;
|
||||
|
||||
#[cfg(any(feature = "inspector", debug_assertions))]
|
||||
use crate::InspectorElementRegistry;
|
||||
use crate::{
|
||||
Action, ActionBuildError, ActionRegistry, Any, AnyView, AnyWindowHandle, AppContext, Asset,
|
||||
AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
|
||||
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
|
||||
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
|
||||
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, Priority,
|
||||
PromptBuilder, PromptButton, PromptHandle, PromptLevel, Render, RenderImage,
|
||||
RenderablePromptHandle, Reservation, ScreenCaptureSource, SharedString, SubscriberSet,
|
||||
Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance, WindowHandle, WindowId,
|
||||
WindowInvalidator, current_platform,
|
||||
default_colors::{Colors, GlobalColors},
|
||||
Action, ActionBuildError, ActionRegistry, Any, AnyView, AnyWindowHandle, AppContext, Arena,
|
||||
ArenaBox, Asset, AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle,
|
||||
DispatchPhase, DisplayId, EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global,
|
||||
KeyBinding, KeyContext, Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu,
|
||||
PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout,
|
||||
PlatformKeyboardMapper, Point, Priority, PromptBuilder, PromptButton, PromptHandle,
|
||||
PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource,
|
||||
SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextRenderingMode, TextSystem,
|
||||
ThermalState, Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
|
||||
colors::{Colors, GlobalColors},
|
||||
hash, init_app_menus,
|
||||
};
|
||||
|
||||
@@ -51,7 +58,13 @@ mod async_context;
|
||||
mod context;
|
||||
mod entity_map;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
mod headless_app_context;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
mod test_app;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
mod test_context;
|
||||
#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
|
||||
mod visual_test_context;
|
||||
|
||||
/// The duration for which futures returned from [Context::on_app_quit] can run before the application fully quits.
|
||||
pub const SHUTDOWN_TIMEOUT: Duration = Duration::from_millis(100);
|
||||
@@ -128,31 +141,21 @@ pub struct Application(Rc<AppCell>);
|
||||
/// Represents an application before it is fully launched. Once your app is
|
||||
/// configured, you'll start the app with `App::run`.
|
||||
impl Application {
|
||||
/// Builds an app with the given asset source.
|
||||
#[allow(clippy::new_without_default)]
|
||||
/// Builds an app with the default platform for the current OS.
|
||||
pub fn new() -> Self {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
log::info!("GPUI was compiled in test mode");
|
||||
Self::with_platform(crate::current_platform(false))
|
||||
}
|
||||
|
||||
/// Builds an app with a caller-provided platform implementation.
|
||||
pub fn with_platform(platform: Rc<dyn Platform>) -> Self {
|
||||
Self(App::new_app(
|
||||
current_platform(false),
|
||||
platform,
|
||||
Arc::new(()),
|
||||
Arc::new(NullHttpClient),
|
||||
))
|
||||
}
|
||||
|
||||
/// Build an app in headless mode. This prevents opening windows,
|
||||
/// but makes it possible to run an application in an context like
|
||||
/// SSH, where GUI applications are not allowed.
|
||||
pub fn headless() -> Self {
|
||||
Self(App::new_app(
|
||||
current_platform(true),
|
||||
Arc::new(()),
|
||||
Arc::new(NullHttpClient),
|
||||
))
|
||||
}
|
||||
|
||||
/// Assign
|
||||
/// Assigns the source of assets for the application.
|
||||
pub fn with_assets(self, asset_source: impl AssetSource) -> Self {
|
||||
let mut context_lock = self.0.borrow_mut();
|
||||
let asset_source = Arc::new(asset_source);
|
||||
@@ -316,6 +319,7 @@ impl SystemWindowTabController {
|
||||
.find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group));
|
||||
|
||||
let current_group = current_group?;
|
||||
// TODO: `.keys()` returns arbitrary order, what does "next" mean?
|
||||
let mut group_ids: Vec<_> = controller.tab_groups.keys().collect();
|
||||
let idx = group_ids.iter().position(|g| *g == current_group)?;
|
||||
let next_idx = (idx + 1) % group_ids.len();
|
||||
@@ -340,6 +344,7 @@ impl SystemWindowTabController {
|
||||
.find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group));
|
||||
|
||||
let current_group = current_group?;
|
||||
// TODO: `.keys()` returns arbitrary order, what does "previous" mean?
|
||||
let mut group_ids: Vec<_> = controller.tab_groups.keys().collect();
|
||||
let idx = group_ids.iter().position(|g| *g == current_group)?;
|
||||
let prev_idx = if idx == 0 {
|
||||
@@ -361,12 +366,9 @@ impl SystemWindowTabController {
|
||||
|
||||
/// Get all tabs in the same window.
|
||||
pub fn tabs(&self, id: WindowId) -> Option<&Vec<SystemWindowTab>> {
|
||||
let tab_group = self
|
||||
.tab_groups
|
||||
.iter()
|
||||
.find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| *group))?;
|
||||
|
||||
self.tab_groups.get(&tab_group)
|
||||
self.tab_groups
|
||||
.values()
|
||||
.find(|tabs| tabs.iter().any(|tab| tab.id == id))
|
||||
}
|
||||
|
||||
/// Initialize the visibility of the system window tab controller.
|
||||
@@ -441,7 +443,7 @@ impl SystemWindowTabController {
|
||||
/// Insert a tab into a tab group.
|
||||
pub fn add_tab(cx: &mut App, id: WindowId, tabs: Vec<SystemWindowTab>) {
|
||||
let mut controller = cx.global_mut::<SystemWindowTabController>();
|
||||
let Some(tab) = tabs.clone().into_iter().find(|tab| tab.id == id) else {
|
||||
let Some(tab) = tabs.iter().find(|tab| tab.id == id).cloned() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -504,16 +506,14 @@ impl SystemWindowTabController {
|
||||
return;
|
||||
};
|
||||
|
||||
let initial_tabs_len = initial_tabs.len();
|
||||
let mut all_tabs = initial_tabs.clone();
|
||||
for tabs in controller.tab_groups.values() {
|
||||
all_tabs.extend(
|
||||
tabs.iter()
|
||||
.filter(|tab| !initial_tabs.contains(tab))
|
||||
.cloned(),
|
||||
);
|
||||
|
||||
for (_, mut tabs) in controller.tab_groups.drain() {
|
||||
tabs.retain(|tab| !all_tabs[..initial_tabs_len].contains(tab));
|
||||
all_tabs.extend(tabs);
|
||||
}
|
||||
|
||||
controller.tab_groups.clear();
|
||||
controller.tab_groups.insert(0, all_tabs);
|
||||
}
|
||||
|
||||
@@ -584,21 +584,13 @@ impl GpuiMode {
|
||||
pub struct App {
|
||||
pub(crate) this: Weak<AppCell>,
|
||||
pub(crate) platform: Rc<dyn Platform>,
|
||||
pub(crate) mode: GpuiMode,
|
||||
text_system: Arc<TextSystem>,
|
||||
flushing_effects: bool,
|
||||
pending_updates: usize,
|
||||
|
||||
pub(crate) actions: Rc<ActionRegistry>,
|
||||
pub(crate) active_drag: Option<AnyDrag>,
|
||||
pub(crate) background_executor: BackgroundExecutor,
|
||||
pub(crate) foreground_executor: ForegroundExecutor,
|
||||
pub(crate) loading_assets: FxHashMap<(TypeId, u64), Box<dyn Any>>,
|
||||
asset_source: Arc<dyn AssetSource>,
|
||||
pub(crate) svg_renderer: SvgRenderer,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
pub(crate) globals_by_type: FxHashMap<TypeId, Box<dyn Any>>,
|
||||
pub(crate) entities: EntityMap,
|
||||
pub(crate) window_update_stack: Vec<WindowId>,
|
||||
pub(crate) new_entity_observers: SubscriberSet<TypeId, NewEntityListener>,
|
||||
pub(crate) windows: SlotMap<WindowId, Option<Box<Window>>>,
|
||||
pub(crate) window_handles: FxHashMap<WindowId, AnyWindowHandle>,
|
||||
@@ -609,20 +601,41 @@ pub struct App {
|
||||
pub(crate) global_action_listeners:
|
||||
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
|
||||
pending_effects: VecDeque<Effect>,
|
||||
pub(crate) pending_notifications: FxHashSet<EntityId>,
|
||||
pub(crate) pending_global_notifications: FxHashSet<TypeId>,
|
||||
|
||||
pub(crate) observers: SubscriberSet<EntityId, Handler>,
|
||||
// TypeId is the type of the event that the listener callback expects
|
||||
pub(crate) event_listeners: SubscriberSet<EntityId, (TypeId, Listener)>,
|
||||
pub(crate) keystroke_observers: SubscriberSet<(), KeystrokeObserver>,
|
||||
pub(crate) keystroke_interceptors: SubscriberSet<(), KeystrokeObserver>,
|
||||
pub(crate) keyboard_layout_observers: SubscriberSet<(), Handler>,
|
||||
pub(crate) thermal_state_observers: SubscriberSet<(), Handler>,
|
||||
pub(crate) release_listeners: SubscriberSet<EntityId, ReleaseListener>,
|
||||
pub(crate) global_observers: SubscriberSet<TypeId, Handler>,
|
||||
pub(crate) quit_observers: SubscriberSet<(), QuitHandler>,
|
||||
pub(crate) restart_observers: SubscriberSet<(), Handler>,
|
||||
pub(crate) restart_path: Option<PathBuf>,
|
||||
pub(crate) window_closed_observers: SubscriberSet<(), WindowClosedHandler>,
|
||||
|
||||
/// Per-App element arena. This isolates element allocations between different
|
||||
/// App instances (important for tests where multiple Apps run concurrently).
|
||||
pub(crate) element_arena: RefCell<Arena>,
|
||||
/// Per-App event arena.
|
||||
pub(crate) event_arena: Arena,
|
||||
|
||||
// Drop globals last. We need to ensure all tasks owned by entities and
|
||||
// callbacks are marked cancelled at this point as this will also shutdown
|
||||
// the tokio runtime. As any task attempting to spawn a blocking tokio task,
|
||||
// might panic.
|
||||
pub(crate) globals_by_type: FxHashMap<TypeId, Box<dyn Any>>,
|
||||
|
||||
// assets
|
||||
pub(crate) loading_assets: FxHashMap<(TypeId, u64), Box<dyn Any>>,
|
||||
asset_source: Arc<dyn AssetSource>,
|
||||
pub(crate) svg_renderer: SvgRenderer,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
|
||||
// below is plain data, the drop order is insignificant here
|
||||
pub(crate) pending_notifications: FxHashSet<EntityId>,
|
||||
pub(crate) pending_global_notifications: FxHashSet<TypeId>,
|
||||
pub(crate) restart_path: Option<PathBuf>,
|
||||
pub(crate) layout_id_buffer: Vec<LayoutId>, // We recycle this memory across layout requests.
|
||||
pub(crate) propagate_event: bool,
|
||||
pub(crate) prompt_builder: Option<PromptBuilder>,
|
||||
@@ -635,8 +648,19 @@ pub struct App {
|
||||
pub(crate) inspector_element_registry: InspectorElementRegistry,
|
||||
#[cfg(any(test, feature = "test-support", debug_assertions))]
|
||||
pub(crate) name: Option<&'static str>,
|
||||
pub(crate) text_rendering_mode: Rc<Cell<TextRenderingMode>>,
|
||||
|
||||
pub(crate) window_update_stack: Vec<WindowId>,
|
||||
pub(crate) mode: GpuiMode,
|
||||
flushing_effects: bool,
|
||||
pending_updates: usize,
|
||||
quit_mode: QuitMode,
|
||||
quitting: bool,
|
||||
|
||||
// We need to ensure the leak detector drops last, after all tasks, callbacks and things have been dropped.
|
||||
// Otherwise it may report false positives.
|
||||
#[cfg(any(test, feature = "leak-detection"))]
|
||||
_ref_counts: Arc<RwLock<EntityRefCounts>>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -646,10 +670,10 @@ impl App {
|
||||
asset_source: Arc<dyn AssetSource>,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
) -> Rc<AppCell> {
|
||||
let executor = platform.background_executor();
|
||||
let background_executor = platform.background_executor();
|
||||
let foreground_executor = platform.foreground_executor();
|
||||
assert!(
|
||||
executor.is_main_thread(),
|
||||
background_executor.is_main_thread(),
|
||||
"must construct App on main thread"
|
||||
);
|
||||
|
||||
@@ -658,17 +682,21 @@ impl App {
|
||||
let keyboard_layout = platform.keyboard_layout();
|
||||
let keyboard_mapper = platform.keyboard_mapper();
|
||||
|
||||
#[cfg(any(test, feature = "leak-detection"))]
|
||||
let _ref_counts = entities.ref_counts_drop_handle();
|
||||
|
||||
let app = Rc::new_cyclic(|this| AppCell {
|
||||
app: RefCell::new(App {
|
||||
this: this.clone(),
|
||||
platform: platform.clone(),
|
||||
text_system,
|
||||
text_rendering_mode: Rc::new(Cell::new(TextRenderingMode::default())),
|
||||
mode: GpuiMode::Production,
|
||||
actions: Rc::new(ActionRegistry::default()),
|
||||
flushing_effects: false,
|
||||
pending_updates: 0,
|
||||
active_drag: None,
|
||||
background_executor: executor,
|
||||
background_executor,
|
||||
foreground_executor,
|
||||
svg_renderer: SvgRenderer::new(asset_source.clone()),
|
||||
loading_assets: Default::default(),
|
||||
@@ -696,6 +724,7 @@ impl App {
|
||||
keystroke_observers: SubscriberSet::new(),
|
||||
keystroke_interceptors: SubscriberSet::new(),
|
||||
keyboard_layout_observers: SubscriberSet::new(),
|
||||
thermal_state_observers: SubscriberSet::new(),
|
||||
global_observers: SubscriberSet::new(),
|
||||
quit_observers: SubscriberSet::new(),
|
||||
restart_observers: SubscriberSet::new(),
|
||||
@@ -713,6 +742,11 @@ impl App {
|
||||
|
||||
#[cfg(any(test, feature = "test-support", debug_assertions))]
|
||||
name: None,
|
||||
element_arena: RefCell::new(Arena::new(1024 * 1024)),
|
||||
event_arena: Arena::new(1024 * 1024),
|
||||
|
||||
#[cfg(any(test, feature = "leak-detection"))]
|
||||
_ref_counts,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -733,16 +767,61 @@ impl App {
|
||||
}
|
||||
}));
|
||||
|
||||
platform.on_quit(Box::new({
|
||||
let cx = app.clone();
|
||||
platform.on_thermal_state_change(Box::new({
|
||||
let app = Rc::downgrade(&app);
|
||||
move || {
|
||||
cx.borrow_mut().shutdown();
|
||||
if let Some(app) = app.upgrade() {
|
||||
let cx = &mut app.borrow_mut();
|
||||
cx.thermal_state_observers
|
||||
.clone()
|
||||
.retain(&(), move |callback| (callback)(cx));
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
platform.on_quit(Box::new({
|
||||
let cx = Rc::downgrade(&app);
|
||||
move || {
|
||||
if let Some(cx) = cx.upgrade() {
|
||||
cx.borrow_mut().shutdown();
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
app
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn ref_counts_drop_handle(&self) -> impl Sized + use<> {
|
||||
self.entities.ref_counts_drop_handle()
|
||||
}
|
||||
|
||||
/// Captures a snapshot of all entities that currently have alive handles.
|
||||
///
|
||||
/// The returned [`LeakDetectorSnapshot`] can later be passed to
|
||||
/// [`assert_no_new_leaks`](Self::assert_no_new_leaks) to verify that no
|
||||
/// entities created after the snapshot are still alive.
|
||||
#[cfg(any(test, feature = "leak-detection"))]
|
||||
pub fn leak_detector_snapshot(&self) -> LeakDetectorSnapshot {
|
||||
self.entities.leak_detector_snapshot()
|
||||
}
|
||||
|
||||
/// Asserts that no entities created after `snapshot` still have alive handles.
|
||||
///
|
||||
/// Entities that were already tracked at the time of the snapshot are ignored,
|
||||
/// even if they still have handles. Only *new* entities (those whose
|
||||
/// `EntityId` was not present in the snapshot) are considered leaks.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if any new entity handles exist. The panic message lists every
|
||||
/// leaked entity with its type name, and includes allocation-site backtraces
|
||||
/// when `LEAK_BACKTRACE` is set.
|
||||
#[cfg(any(test, feature = "leak-detection"))]
|
||||
pub fn assert_no_new_leaks(&self, snapshot: &LeakDetectorSnapshot) {
|
||||
self.entities.assert_no_new_leaks(snapshot)
|
||||
}
|
||||
|
||||
/// Quit the application gracefully. Handlers registered with [`Context::on_app_quit`]
|
||||
/// will be given 100ms to complete before exiting.
|
||||
pub fn shutdown(&mut self) {
|
||||
@@ -759,7 +838,7 @@ impl App {
|
||||
|
||||
let futures = futures::future::join_all(futures);
|
||||
if self
|
||||
.background_executor
|
||||
.foreground_executor
|
||||
.block_with_timeout(SHUTDOWN_TIMEOUT, futures)
|
||||
.is_err()
|
||||
{
|
||||
@@ -845,10 +924,12 @@ impl App {
|
||||
&mut self,
|
||||
callback: impl FnOnce(&mut App) -> R,
|
||||
) -> (R, FxHashSet<EntityId>) {
|
||||
let accessed_entities_start = self.entities.accessed_entities.borrow().clone();
|
||||
let accessed_entities_start = self.entities.accessed_entities.get_mut().clone();
|
||||
let result = callback(self);
|
||||
let accessed_entities_end = self.entities.accessed_entities.borrow().clone();
|
||||
let entities_accessed_in_callback = accessed_entities_end
|
||||
let entities_accessed_in_callback = self
|
||||
.entities
|
||||
.accessed_entities
|
||||
.get_mut()
|
||||
.difference(&accessed_entities_start)
|
||||
.copied()
|
||||
.collect::<FxHashSet<EntityId>>();
|
||||
@@ -1075,16 +1156,45 @@ impl App {
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// Returns the current thermal state of the system.
|
||||
pub fn thermal_state(&self) -> ThermalState {
|
||||
self.platform.thermal_state()
|
||||
}
|
||||
|
||||
/// Invokes a handler when the thermal state changes
|
||||
pub fn on_thermal_state_change<F>(&self, mut callback: F) -> Subscription
|
||||
where
|
||||
F: 'static + FnMut(&mut App),
|
||||
{
|
||||
let (subscription, activate) = self.thermal_state_observers.insert(
|
||||
(),
|
||||
Box::new(move |cx| {
|
||||
callback(cx);
|
||||
true
|
||||
}),
|
||||
);
|
||||
activate();
|
||||
subscription
|
||||
}
|
||||
|
||||
/// Returns the appearance of the application's windows.
|
||||
pub fn window_appearance(&self) -> WindowAppearance {
|
||||
self.platform.window_appearance()
|
||||
}
|
||||
|
||||
/// Writes data to the primary selection buffer.
|
||||
/// Only available on Linux.
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
pub fn write_to_primary(&self, item: ClipboardItem) {
|
||||
self.platform.write_to_primary(item)
|
||||
/// Reads data from the platform clipboard.
|
||||
pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
self.platform.read_from_clipboard()
|
||||
}
|
||||
|
||||
/// Sets the text rendering mode for the application.
|
||||
pub fn set_text_rendering_mode(&mut self, mode: TextRenderingMode) {
|
||||
self.text_rendering_mode.set(mode);
|
||||
}
|
||||
|
||||
/// Returns the current text rendering mode for the application.
|
||||
pub fn text_rendering_mode(&self) -> TextRenderingMode {
|
||||
self.text_rendering_mode.get()
|
||||
}
|
||||
|
||||
/// Writes data to the platform clipboard.
|
||||
@@ -1099,9 +1209,31 @@ impl App {
|
||||
self.platform.read_from_primary()
|
||||
}
|
||||
|
||||
/// Reads data from the platform clipboard.
|
||||
pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
self.platform.read_from_clipboard()
|
||||
/// Writes data to the primary selection buffer.
|
||||
/// Only available on Linux.
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
pub fn write_to_primary(&self, item: ClipboardItem) {
|
||||
self.platform.write_to_primary(item)
|
||||
}
|
||||
|
||||
/// Reads data from macOS's "Find" pasteboard.
|
||||
///
|
||||
/// Used to share the current search string between apps.
|
||||
///
|
||||
/// https://developer.apple.com/documentation/appkit/nspasteboard/name-swift.struct/find
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn read_from_find_pasteboard(&self) -> Option<ClipboardItem> {
|
||||
self.platform.read_from_find_pasteboard()
|
||||
}
|
||||
|
||||
/// Writes data to macOS's "Find" pasteboard.
|
||||
///
|
||||
/// Used to share the current search string between apps.
|
||||
///
|
||||
/// https://developer.apple.com/documentation/appkit/nspasteboard/name-swift.struct/find
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn write_to_find_pasteboard(&self, item: ClipboardItem) {
|
||||
self.platform.write_to_find_pasteboard(item)
|
||||
}
|
||||
|
||||
/// Writes credentials to the platform keychain.
|
||||
@@ -1268,7 +1400,7 @@ impl App {
|
||||
emitter,
|
||||
event_type,
|
||||
event,
|
||||
} => self.apply_emit_effect(emitter, event_type, event),
|
||||
} => self.apply_emit_effect(emitter, event_type, &*event),
|
||||
|
||||
Effect::RefreshWindows => {
|
||||
self.apply_refresh_effect();
|
||||
@@ -1305,6 +1437,7 @@ impl App {
|
||||
}
|
||||
|
||||
if self.pending_effects.is_empty() {
|
||||
self.event_arena.clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1362,12 +1495,12 @@ impl App {
|
||||
.retain(&emitter, |handler| handler(self));
|
||||
}
|
||||
|
||||
fn apply_emit_effect(&mut self, emitter: EntityId, event_type: TypeId, event: Box<dyn Any>) {
|
||||
fn apply_emit_effect(&mut self, emitter: EntityId, event_type: TypeId, event: &dyn Any) {
|
||||
self.event_listeners
|
||||
.clone()
|
||||
.retain(&emitter, |(stored_type, handler)| {
|
||||
if *stored_type == event_type {
|
||||
handler(event.as_ref(), self)
|
||||
handler(event, self)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
@@ -1425,29 +1558,33 @@ impl App {
|
||||
|
||||
cx.window_update_stack.push(window.handle.id);
|
||||
let result = update(root_view, &mut window, cx);
|
||||
cx.window_update_stack.pop();
|
||||
fn trail(id: WindowId, window: Box<Window>, cx: &mut App) -> Option<()> {
|
||||
cx.window_update_stack.pop();
|
||||
|
||||
if window.removed {
|
||||
cx.window_handles.remove(&id);
|
||||
cx.windows.remove(id);
|
||||
if window.removed {
|
||||
cx.window_handles.remove(&id);
|
||||
cx.windows.remove(id);
|
||||
|
||||
cx.window_closed_observers.clone().retain(&(), |callback| {
|
||||
callback(cx);
|
||||
true
|
||||
});
|
||||
cx.window_closed_observers.clone().retain(&(), |callback| {
|
||||
callback(cx);
|
||||
true
|
||||
});
|
||||
|
||||
let quit_on_empty = match cx.quit_mode {
|
||||
QuitMode::Explicit => false,
|
||||
QuitMode::LastWindowClosed => true,
|
||||
QuitMode::Default => cfg!(not(target_os = "macos")),
|
||||
};
|
||||
let quit_on_empty = match cx.quit_mode {
|
||||
QuitMode::Explicit => false,
|
||||
QuitMode::LastWindowClosed => true,
|
||||
QuitMode::Default => cfg!(not(target_os = "macos")),
|
||||
};
|
||||
|
||||
if quit_on_empty && cx.windows.is_empty() {
|
||||
cx.quit();
|
||||
if quit_on_empty && cx.windows.is_empty() {
|
||||
cx.quit();
|
||||
}
|
||||
} else {
|
||||
cx.windows.get_mut(id)?.replace(window);
|
||||
}
|
||||
} else {
|
||||
cx.windows.get_mut(id)?.replace(window);
|
||||
Some(())
|
||||
}
|
||||
trail(id, window, cx)?;
|
||||
|
||||
Some(result)
|
||||
})
|
||||
@@ -1492,7 +1629,7 @@ impl App {
|
||||
let mut cx = self.to_async();
|
||||
|
||||
self.foreground_executor
|
||||
.spawn(async move { f(&mut cx).await })
|
||||
.spawn(async move { f(&mut cx).await }.boxed_local())
|
||||
}
|
||||
|
||||
/// Spawns the future returned by the given function on the main thread with
|
||||
@@ -1510,7 +1647,7 @@ impl App {
|
||||
let mut cx = self.to_async();
|
||||
|
||||
self.foreground_executor
|
||||
.spawn_with_priority(priority, async move { f(&mut cx).await })
|
||||
.spawn_with_priority(priority, async move { f(&mut cx).await }.boxed_local())
|
||||
}
|
||||
|
||||
/// Schedules the given function to be run at the end of the current effect cycle, allowing entities
|
||||
@@ -1777,7 +1914,10 @@ impl App {
|
||||
/// Register a global handler for actions invoked via the keyboard. These handlers are run at
|
||||
/// the end of the bubble phase for actions, and so will only be invoked if there are no other
|
||||
/// handlers or if they called `cx.propagate()`.
|
||||
pub fn on_action<A: Action>(&mut self, listener: impl Fn(&A, &mut Self) + 'static) {
|
||||
pub fn on_action<A: Action>(
|
||||
&mut self,
|
||||
listener: impl Fn(&A, &mut Self) + 'static,
|
||||
) -> &mut Self {
|
||||
self.global_action_listeners
|
||||
.entry(TypeId::of::<A>())
|
||||
.or_default()
|
||||
@@ -1787,6 +1927,7 @@ impl App {
|
||||
listener(action, cx)
|
||||
}
|
||||
}));
|
||||
self
|
||||
}
|
||||
|
||||
/// Event handlers propagate events by default. Call this method to stop dispatching to
|
||||
@@ -1835,6 +1976,18 @@ impl App {
|
||||
self.actions.action_schemas(generator)
|
||||
}
|
||||
|
||||
/// Get the schema for a specific action by name.
|
||||
/// Returns `None` if the action is not found.
|
||||
/// Returns `Some(None)` if the action exists but has no schema.
|
||||
/// Returns `Some(Some(schema))` if the action exists and has a schema.
|
||||
pub fn action_schema_by_name(
|
||||
&self,
|
||||
name: &str,
|
||||
generator: &mut schemars::SchemaGenerator,
|
||||
) -> Option<Option<schemars::Schema>> {
|
||||
self.actions.action_schema_by_name(name, generator)
|
||||
}
|
||||
|
||||
/// Get a map from a deprecated action name to the canonical name.
|
||||
pub fn deprecated_actions_to_preferred_actions(&self) -> &HashMap<&'static str, &'static str> {
|
||||
self.actions.deprecated_aliases()
|
||||
@@ -1896,8 +2049,11 @@ impl App {
|
||||
pub(crate) fn clear_pending_keystrokes(&mut self) {
|
||||
for window in self.windows() {
|
||||
window
|
||||
.update(self, |_, window, _| {
|
||||
window.clear_pending_keystrokes();
|
||||
.update(self, |_, window, cx| {
|
||||
if window.pending_input_keystrokes().is_some() {
|
||||
window.clear_pending_keystrokes();
|
||||
window.pending_input_changed(cx);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -1921,7 +2077,8 @@ impl App {
|
||||
}
|
||||
|
||||
/// Sets the menu bar for this application. This will replace any existing menu bar.
|
||||
pub fn set_menus(&self, menus: Vec<Menu>) {
|
||||
pub fn set_menus(&self, menus: impl IntoIterator<Item = Menu>) {
|
||||
let menus: Vec<Menu> = menus.into_iter().collect();
|
||||
self.platform.set_menus(menus, &self.keymap.borrow());
|
||||
}
|
||||
|
||||
@@ -1954,7 +2111,7 @@ impl App {
|
||||
&self,
|
||||
menus: Vec<MenuItem>,
|
||||
entries: Vec<SmallVec<[PathBuf; 2]>>,
|
||||
) -> Vec<SmallVec<[PathBuf; 2]>> {
|
||||
) -> Task<Vec<SmallVec<[PathBuf; 2]>>> {
|
||||
self.platform.update_jump_list(menus, entries)
|
||||
}
|
||||
|
||||
@@ -2188,8 +2345,6 @@ impl App {
|
||||
}
|
||||
|
||||
impl AppContext for App {
|
||||
type Result<T> = T;
|
||||
|
||||
/// Builds an entity that is owned by the application.
|
||||
///
|
||||
/// The given function will be invoked with a [`Context`] and must return an object representing the entity. An
|
||||
@@ -2211,7 +2366,7 @@ impl AppContext for App {
|
||||
})
|
||||
}
|
||||
|
||||
fn reserve_entity<T: 'static>(&mut self) -> Self::Result<Reservation<T>> {
|
||||
fn reserve_entity<T: 'static>(&mut self) -> Reservation<T> {
|
||||
Reservation(self.entities.reserve())
|
||||
}
|
||||
|
||||
@@ -2219,7 +2374,7 @@ impl AppContext for App {
|
||||
&mut self,
|
||||
reservation: Reservation<T>,
|
||||
build_entity: impl FnOnce(&mut Context<T>) -> T,
|
||||
) -> Self::Result<Entity<T>> {
|
||||
) -> Entity<T> {
|
||||
self.update(|cx| {
|
||||
let slot = reservation.0;
|
||||
let entity = build_entity(&mut Context::new_context(cx, slot.downgrade()));
|
||||
@@ -2252,11 +2407,7 @@ impl AppContext for App {
|
||||
GpuiBorrow::new(handle.clone(), self)
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &Entity<T>,
|
||||
read: impl FnOnce(&T, &App) -> R,
|
||||
) -> Self::Result<R>
|
||||
fn read_entity<T, R>(&self, handle: &Entity<T>, read: impl FnOnce(&T, &App) -> R) -> R
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
@@ -2301,7 +2452,7 @@ impl AppContext for App {
|
||||
self.background_executor.spawn(future)
|
||||
}
|
||||
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> Self::Result<R>
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
|
||||
where
|
||||
G: Global,
|
||||
{
|
||||
@@ -2318,7 +2469,7 @@ pub(crate) enum Effect {
|
||||
Emit {
|
||||
emitter: EntityId,
|
||||
event_type: TypeId,
|
||||
event: Box<dyn Any>,
|
||||
event: ArenaBox<dyn Any>,
|
||||
},
|
||||
RefreshWindows,
|
||||
NotifyGlobalObservers {
|
||||
|
||||
+96
-104
@@ -1,18 +1,23 @@
|
||||
use crate::{
|
||||
AnyView, AnyWindowHandle, App, AppCell, AppContext, BackgroundExecutor, BorrowAppContext,
|
||||
Entity, EventEmitter, Focusable, ForegroundExecutor, Global, PromptButton, PromptLevel, Render,
|
||||
Reservation, Result, Subscription, Task, VisualContext, Window, WindowHandle,
|
||||
Entity, EventEmitter, Focusable, ForegroundExecutor, Global, GpuiBorrow, PromptButton,
|
||||
PromptLevel, Render, Reservation, Result, Subscription, Task, VisualContext, Window,
|
||||
WindowHandle,
|
||||
};
|
||||
use anyhow::{Context as _, anyhow};
|
||||
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`, so its methods are fallible to protect against cases where the [App] is dropped.
|
||||
///
|
||||
/// 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>,
|
||||
@@ -20,64 +25,61 @@ pub struct AsyncApp {
|
||||
pub(crate) foreground_executor: ForegroundExecutor,
|
||||
}
|
||||
|
||||
impl AppContext for AsyncApp {
|
||||
type Result<T> = Result<T>;
|
||||
impl AsyncApp {
|
||||
fn app(&self) -> std::rc::Rc<AppCell> {
|
||||
self.app
|
||||
.upgrade()
|
||||
.expect("app was released before async operation completed")
|
||||
}
|
||||
}
|
||||
|
||||
fn new<T: 'static>(
|
||||
&mut self,
|
||||
build_entity: impl FnOnce(&mut Context<T>) -> T,
|
||||
) -> Self::Result<Entity<T>> {
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
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();
|
||||
Ok(app.new(build_entity))
|
||||
app.new(build_entity)
|
||||
}
|
||||
|
||||
fn reserve_entity<T: 'static>(&mut self) -> Result<Reservation<T>> {
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
fn reserve_entity<T: 'static>(&mut self) -> Reservation<T> {
|
||||
let app = self.app();
|
||||
let mut app = app.borrow_mut();
|
||||
Ok(app.reserve_entity())
|
||||
app.reserve_entity()
|
||||
}
|
||||
|
||||
fn insert_entity<T: 'static>(
|
||||
&mut self,
|
||||
reservation: Reservation<T>,
|
||||
build_entity: impl FnOnce(&mut Context<T>) -> T,
|
||||
) -> Result<Entity<T>> {
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
) -> Entity<T> {
|
||||
let app = self.app();
|
||||
let mut app = app.borrow_mut();
|
||||
Ok(app.insert_entity(reservation, build_entity))
|
||||
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,
|
||||
) -> Self::Result<R> {
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
) -> R {
|
||||
let app = self.app();
|
||||
let mut app = app.borrow_mut();
|
||||
Ok(app.update_entity(handle, update))
|
||||
app.update_entity(handle, update)
|
||||
}
|
||||
|
||||
fn as_mut<'a, T>(&'a mut self, _handle: &Entity<T>) -> Self::Result<super::GpuiBorrow<'a, T>>
|
||||
fn as_mut<'a, T>(&'a mut self, _handle: &Entity<T>) -> GpuiBorrow<'a, T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
Err(anyhow!(
|
||||
"Cannot as_mut with an async context. Try calling update() first"
|
||||
))
|
||||
panic!("Cannot as_mut with an async context. Try calling update() first")
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &Entity<T>,
|
||||
callback: impl FnOnce(&T, &App) -> R,
|
||||
) -> Self::Result<R>
|
||||
fn read_entity<T, R>(&self, handle: &Entity<T>, callback: impl FnOnce(&T, &App) -> R) -> R
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
let app = self.app();
|
||||
let lock = app.borrow();
|
||||
Ok(lock.read_entity(handle, callback))
|
||||
lock.read_entity(handle, callback)
|
||||
}
|
||||
|
||||
fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
|
||||
@@ -86,6 +88,9 @@ impl AppContext for AsyncApp {
|
||||
{
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -99,9 +104,13 @@ impl AppContext for AsyncApp {
|
||||
{
|
||||
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,
|
||||
@@ -109,23 +118,22 @@ impl AppContext for AsyncApp {
|
||||
self.background_executor.spawn(future)
|
||||
}
|
||||
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> Self::Result<R>
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
|
||||
where
|
||||
G: Global,
|
||||
{
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
let app = self.app();
|
||||
let mut lock = app.borrow_mut();
|
||||
Ok(lock.update(|this| this.read_global(callback)))
|
||||
lock.update(|this| this.read_global(callback))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncApp {
|
||||
/// Schedules all windows in the application to be redrawn.
|
||||
pub fn refresh(&self) -> Result<()> {
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
pub fn refresh(&self) {
|
||||
let app = self.app();
|
||||
let mut lock = app.borrow_mut();
|
||||
lock.refresh_windows();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get an executor which can be used to spawn futures in the background.
|
||||
@@ -139,10 +147,10 @@ impl AsyncApp {
|
||||
}
|
||||
|
||||
/// Invoke the given function in the context of the app, then flush any effects produced during its invocation.
|
||||
pub fn update<R>(&self, f: impl FnOnce(&mut App) -> R) -> Result<R> {
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
pub fn update<R>(&self, f: impl FnOnce(&mut App) -> R) -> R {
|
||||
let app = self.app();
|
||||
let mut lock = app.borrow_mut();
|
||||
Ok(lock.update(f))
|
||||
lock.update(f)
|
||||
}
|
||||
|
||||
/// Arrange for the given callback to be invoked whenever the given entity emits an event of a given type.
|
||||
@@ -150,16 +158,15 @@ impl AsyncApp {
|
||||
pub fn subscribe<T, Event>(
|
||||
&mut self,
|
||||
entity: &Entity<T>,
|
||||
mut on_event: impl FnMut(Entity<T>, &Event, &mut App) + 'static,
|
||||
) -> Result<Subscription>
|
||||
on_event: impl FnMut(Entity<T>, &Event, &mut App) + 'static,
|
||||
) -> Subscription
|
||||
where
|
||||
T: 'static + EventEmitter<Event>,
|
||||
Event: 'static,
|
||||
{
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
let app = self.app();
|
||||
let mut lock = app.borrow_mut();
|
||||
let subscription = lock.subscribe(entity, on_event);
|
||||
Ok(subscription)
|
||||
lock.subscribe(entity, on_event)
|
||||
}
|
||||
|
||||
/// Open a window with the given options based on the root view returned by the given function.
|
||||
@@ -171,8 +178,11 @@ impl AsyncApp {
|
||||
where
|
||||
V: 'static + Render,
|
||||
{
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
let app = self.app();
|
||||
let mut lock = app.borrow_mut();
|
||||
if lock.quitting {
|
||||
bail!("app is quitting");
|
||||
}
|
||||
lock.open_window(options, build_root_view)
|
||||
}
|
||||
|
||||
@@ -185,65 +195,57 @@ impl AsyncApp {
|
||||
{
|
||||
let mut cx = self.clone();
|
||||
self.foreground_executor
|
||||
.spawn(async move { f(&mut cx).await })
|
||||
.spawn(async move { f(&mut cx).await }.boxed_local())
|
||||
}
|
||||
|
||||
/// Determine whether global state of the specified type has been assigned.
|
||||
/// Returns an error if the `App` has been dropped.
|
||||
pub fn has_global<G: Global>(&self) -> Result<bool> {
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
pub fn has_global<G: Global>(&self) -> bool {
|
||||
let app = self.app();
|
||||
let app = app.borrow_mut();
|
||||
Ok(app.has_global::<G>())
|
||||
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.
|
||||
/// Returns an error if the `App` has been dropped.
|
||||
pub fn read_global<G: Global, R>(&self, read: impl FnOnce(&G, &App) -> R) -> Result<R> {
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
pub fn read_global<G: Global, R>(&self, read: impl FnOnce(&G, &App) -> R) -> R {
|
||||
let app = self.app();
|
||||
let app = app.borrow_mut();
|
||||
Ok(read(app.global(), &app))
|
||||
read(app.global(), &app)
|
||||
}
|
||||
|
||||
/// Reads the global state of the specified type, passing it to the given callback.
|
||||
///
|
||||
/// Similar to [`AsyncApp::read_global`], but returns an error instead of panicking
|
||||
/// if no state of the specified type has been assigned.
|
||||
///
|
||||
/// Returns an error if no state of the specified type has been assigned the `App` has been dropped.
|
||||
pub fn try_read_global<G: Global, R>(&self, read: impl FnOnce(&G, &App) -> R) -> Option<R> {
|
||||
let app = self.app.upgrade()?;
|
||||
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.
|
||||
///
|
||||
/// # Errors
|
||||
/// If the app has ben dropped this returns an error.
|
||||
pub fn try_read_default_global<G: Global + Default, R>(
|
||||
pub fn read_default_global<G: Global + Default, R>(
|
||||
&self,
|
||||
read: impl FnOnce(&G, &App) -> R,
|
||||
) -> Result<R> {
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
) -> R {
|
||||
let app = self.app();
|
||||
let mut app = app.borrow_mut();
|
||||
app.update(|cx| {
|
||||
cx.default_global::<G>();
|
||||
});
|
||||
Ok(read(app.try_global().context("app was released")?, &app))
|
||||
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,
|
||||
) -> Result<R> {
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
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();
|
||||
Ok(app.update(|cx| cx.update_global(update)))
|
||||
app.update(|cx| cx.update_global(update))
|
||||
}
|
||||
|
||||
/// Run something using this entity and cx, when the returned struct is dropped
|
||||
@@ -334,7 +336,7 @@ impl AsyncWindowContext {
|
||||
{
|
||||
let mut cx = self.clone();
|
||||
self.foreground_executor
|
||||
.spawn(async move { f(&mut cx).await })
|
||||
.spawn(async move { f(&mut cx).await }.boxed_local())
|
||||
}
|
||||
|
||||
/// Present a platform dialog.
|
||||
@@ -359,54 +361,41 @@ impl AsyncWindowContext {
|
||||
}
|
||||
|
||||
impl AppContext for AsyncWindowContext {
|
||||
type Result<T> = Result<T>;
|
||||
|
||||
fn new<T>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Result<Entity<T>>
|
||||
fn new<T>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.app
|
||||
.update_window(self.window, |_, _, cx| cx.new(build_entity))
|
||||
self.app.new(build_entity)
|
||||
}
|
||||
|
||||
fn reserve_entity<T: 'static>(&mut self) -> Result<Reservation<T>> {
|
||||
self.app
|
||||
.update_window(self.window, |_, _, cx| cx.reserve_entity())
|
||||
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,
|
||||
) -> Self::Result<Entity<T>> {
|
||||
self.app.update_window(self.window, |_, _, cx| {
|
||||
cx.insert_entity(reservation, build_entity)
|
||||
})
|
||||
) -> Entity<T> {
|
||||
self.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,
|
||||
) -> Result<R> {
|
||||
self.app
|
||||
.update_window(self.window, |_, _, cx| cx.update_entity(handle, update))
|
||||
) -> R {
|
||||
self.app.update_entity(handle, update)
|
||||
}
|
||||
|
||||
fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> Self::Result<super::GpuiBorrow<'a, T>>
|
||||
fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> GpuiBorrow<'a, T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
Err(anyhow!(
|
||||
"Cannot use as_mut() from an async context, call `update`"
|
||||
))
|
||||
panic!("Cannot use as_mut() from an async context, call `update`")
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &Entity<T>,
|
||||
read: impl FnOnce(&T, &App) -> R,
|
||||
) -> Self::Result<R>
|
||||
fn read_entity<T, R>(&self, handle: &Entity<T>, read: impl FnOnce(&T, &App) -> R) -> R
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
@@ -431,6 +420,7 @@ impl AppContext for AsyncWindowContext {
|
||||
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,
|
||||
@@ -438,7 +428,7 @@ impl AppContext for AsyncWindowContext {
|
||||
self.app.background_executor.spawn(future)
|
||||
}
|
||||
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> Result<R>
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
|
||||
where
|
||||
G: Global,
|
||||
{
|
||||
@@ -447,6 +437,8 @@ impl AppContext for AsyncWindowContext {
|
||||
}
|
||||
|
||||
impl VisualContext for AsyncWindowContext {
|
||||
type Result<T> = Result<T>;
|
||||
|
||||
fn window_handle(&self) -> AnyWindowHandle {
|
||||
self.window
|
||||
}
|
||||
@@ -454,7 +446,7 @@ impl VisualContext for AsyncWindowContext {
|
||||
fn new_window_entity<T: 'static>(
|
||||
&mut self,
|
||||
build_entity: impl FnOnce(&mut Window, &mut Context<T>) -> T,
|
||||
) -> Self::Result<Entity<T>> {
|
||||
) -> Result<Entity<T>> {
|
||||
self.app.update_window(self.window, |_, window, cx| {
|
||||
cx.new(|cx| build_entity(window, cx))
|
||||
})
|
||||
@@ -464,7 +456,7 @@ impl VisualContext for AsyncWindowContext {
|
||||
&mut self,
|
||||
view: &Entity<T>,
|
||||
update: impl FnOnce(&mut T, &mut Window, &mut Context<T>) -> R,
|
||||
) -> Self::Result<R> {
|
||||
) -> Result<R> {
|
||||
self.app.update_window(self.window, |_, window, cx| {
|
||||
view.update(cx, |entity, cx| update(entity, window, cx))
|
||||
})
|
||||
@@ -473,7 +465,7 @@ impl VisualContext for AsyncWindowContext {
|
||||
fn replace_root_view<V>(
|
||||
&mut self,
|
||||
build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,
|
||||
) -> Self::Result<Entity<V>>
|
||||
) -> Result<Entity<V>>
|
||||
where
|
||||
V: 'static + Render,
|
||||
{
|
||||
@@ -482,12 +474,12 @@ impl VisualContext for AsyncWindowContext {
|
||||
})
|
||||
}
|
||||
|
||||
fn focus<V>(&mut self, view: &Entity<V>) -> Self::Result<()>
|
||||
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);
|
||||
view.read(cx).focus_handle(cx).focus(window, cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+24
-18
@@ -285,7 +285,7 @@ impl<'a, T: 'static> Context<'a, T> {
|
||||
|
||||
/// 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));
|
||||
window.focus(&view.focus_handle(self), self);
|
||||
}
|
||||
|
||||
/// Sets a given callback to be run on the next frame.
|
||||
@@ -697,11 +697,19 @@ impl<'a, T: 'static> Context<'a, T> {
|
||||
let (subscription, activate) = self.global_observers.insert(
|
||||
TypeId::of::<G>(),
|
||||
Box::new(move |cx| {
|
||||
window_handle
|
||||
.update(cx, |_, window, cx| {
|
||||
view.update(cx, |view, cx| f(view, window, cx)).is_ok()
|
||||
})
|
||||
.unwrap_or(false)
|
||||
// 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());
|
||||
@@ -732,7 +740,7 @@ impl<'a, T: 'static> Context<'a, T> {
|
||||
{
|
||||
let view = self.entity();
|
||||
window.defer(self, move |window, cx| {
|
||||
view.read(cx).focus_handle(cx).focus(window)
|
||||
view.read(cx).focus_handle(cx).focus(window, cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -744,17 +752,19 @@ impl<T> Context<'_, T> {
|
||||
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: Box::new(event),
|
||||
event,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AppContext for Context<'_, T> {
|
||||
type Result<U> = U;
|
||||
|
||||
#[inline]
|
||||
fn new<U: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<U>) -> U) -> Entity<U> {
|
||||
self.app.new(build_entity)
|
||||
@@ -770,7 +780,7 @@ impl<T> AppContext for Context<'_, T> {
|
||||
&mut self,
|
||||
reservation: Reservation<U>,
|
||||
build_entity: impl FnOnce(&mut Context<U>) -> U,
|
||||
) -> Self::Result<Entity<U>> {
|
||||
) -> Entity<U> {
|
||||
self.app.insert_entity(reservation, build_entity)
|
||||
}
|
||||
|
||||
@@ -784,7 +794,7 @@ impl<T> AppContext for Context<'_, T> {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn as_mut<'a, E>(&'a mut self, handle: &Entity<E>) -> Self::Result<super::GpuiBorrow<'a, E>>
|
||||
fn as_mut<'a, E>(&'a mut self, handle: &Entity<E>) -> super::GpuiBorrow<'a, E>
|
||||
where
|
||||
E: 'static,
|
||||
{
|
||||
@@ -792,11 +802,7 @@ impl<T> AppContext for Context<'_, T> {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read_entity<U, R>(
|
||||
&self,
|
||||
handle: &Entity<U>,
|
||||
read: impl FnOnce(&U, &App) -> R,
|
||||
) -> Self::Result<R>
|
||||
fn read_entity<U, R>(&self, handle: &Entity<U>, read: impl FnOnce(&U, &App) -> R) -> R
|
||||
where
|
||||
U: 'static,
|
||||
{
|
||||
@@ -832,7 +838,7 @@ impl<T> AppContext for Context<'_, T> {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> Self::Result<R>
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
|
||||
where
|
||||
G: Global,
|
||||
{
|
||||
|
||||
+234
-52
@@ -11,7 +11,6 @@ use std::{
|
||||
fmt::{self, Display},
|
||||
hash::{Hash, Hasher},
|
||||
marker::PhantomData,
|
||||
mem,
|
||||
num::NonZeroU64,
|
||||
sync::{
|
||||
Arc, Weak,
|
||||
@@ -21,7 +20,7 @@ use std::{
|
||||
};
|
||||
|
||||
use super::Context;
|
||||
use crate::util::atomic_incr_if_not_zero;
|
||||
use crate::local_util::atomic_incr_if_not_zero;
|
||||
#[cfg(any(test, feature = "leak-detection"))]
|
||||
use collections::HashMap;
|
||||
|
||||
@@ -60,7 +59,8 @@ pub(crate) struct EntityMap {
|
||||
ref_counts: Arc<RwLock<EntityRefCounts>>,
|
||||
}
|
||||
|
||||
struct EntityRefCounts {
|
||||
#[doc(hidden)]
|
||||
pub(crate) struct EntityRefCounts {
|
||||
counts: SlotMap<EntityId, AtomicUsize>,
|
||||
dropped_entity_ids: Vec<EntityId>,
|
||||
#[cfg(any(test, feature = "leak-detection"))]
|
||||
@@ -84,6 +84,32 @@ impl EntityMap {
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn ref_counts_drop_handle(&self) -> Arc<RwLock<EntityRefCounts>> {
|
||||
self.ref_counts.clone()
|
||||
}
|
||||
|
||||
/// Captures a snapshot of all entities that currently have alive handles.
|
||||
///
|
||||
/// The returned [`LeakDetectorSnapshot`] can later be passed to
|
||||
/// [`assert_no_new_leaks`](Self::assert_no_new_leaks) to verify that no
|
||||
/// entities created after the snapshot are still alive.
|
||||
#[cfg(any(test, feature = "leak-detection"))]
|
||||
pub fn leak_detector_snapshot(&self) -> LeakDetectorSnapshot {
|
||||
self.ref_counts.read().leak_detector.snapshot()
|
||||
}
|
||||
|
||||
/// Asserts that no entities created after `snapshot` still have alive handles.
|
||||
///
|
||||
/// See [`LeakDetector::assert_no_new_leaks`] for details.
|
||||
#[cfg(any(test, feature = "leak-detection"))]
|
||||
pub fn assert_no_new_leaks(&self, snapshot: &LeakDetectorSnapshot) {
|
||||
self.ref_counts
|
||||
.read()
|
||||
.leak_detector
|
||||
.assert_no_new_leaks(snapshot)
|
||||
}
|
||||
|
||||
/// Reserve a slot for an entity, which you can subsequently use with `insert`.
|
||||
pub fn reserve<T: 'static>(&self) -> Slot<T> {
|
||||
let id = self.ref_counts.write().counts.insert(1.into());
|
||||
@@ -95,7 +121,7 @@ impl EntityMap {
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
let mut accessed_entities = self.accessed_entities.borrow_mut();
|
||||
let mut accessed_entities = self.accessed_entities.get_mut();
|
||||
accessed_entities.insert(slot.entity_id);
|
||||
|
||||
let handle = slot.0;
|
||||
@@ -107,7 +133,7 @@ impl EntityMap {
|
||||
#[track_caller]
|
||||
pub fn lease<T>(&mut self, pointer: &Entity<T>) -> Lease<T> {
|
||||
self.assert_valid_context(pointer);
|
||||
let mut accessed_entities = self.accessed_entities.borrow_mut();
|
||||
let mut accessed_entities = self.accessed_entities.get_mut();
|
||||
accessed_entities.insert(pointer.entity_id);
|
||||
|
||||
let entity = Some(
|
||||
@@ -147,21 +173,20 @@ impl EntityMap {
|
||||
|
||||
pub fn extend_accessed(&mut self, entities: &FxHashSet<EntityId>) {
|
||||
self.accessed_entities
|
||||
.borrow_mut()
|
||||
.get_mut()
|
||||
.extend(entities.iter().copied());
|
||||
}
|
||||
|
||||
pub fn clear_accessed(&mut self) {
|
||||
self.accessed_entities.borrow_mut().clear();
|
||||
self.accessed_entities.get_mut().clear();
|
||||
}
|
||||
|
||||
pub fn take_dropped(&mut self) -> Vec<(EntityId, Box<dyn Any>)> {
|
||||
let mut ref_counts = self.ref_counts.write();
|
||||
let dropped_entity_ids = mem::take(&mut ref_counts.dropped_entity_ids);
|
||||
let mut accessed_entities = self.accessed_entities.borrow_mut();
|
||||
let mut ref_counts = &mut *self.ref_counts.write();
|
||||
let dropped_entity_ids = ref_counts.dropped_entity_ids.drain(..);
|
||||
let mut accessed_entities = self.accessed_entities.get_mut();
|
||||
|
||||
dropped_entity_ids
|
||||
.into_iter()
|
||||
.filter_map(|entity_id| {
|
||||
let count = ref_counts.counts.remove(entity_id).unwrap();
|
||||
debug_assert_eq!(
|
||||
@@ -227,7 +252,12 @@ pub struct AnyEntity {
|
||||
}
|
||||
|
||||
impl AnyEntity {
|
||||
fn new(id: EntityId, entity_type: TypeId, entity_map: Weak<RwLock<EntityRefCounts>>) -> Self {
|
||||
fn new(
|
||||
id: EntityId,
|
||||
entity_type: TypeId,
|
||||
entity_map: Weak<RwLock<EntityRefCounts>>,
|
||||
#[cfg(any(test, feature = "leak-detection"))] type_name: &'static str,
|
||||
) -> Self {
|
||||
Self {
|
||||
entity_id: id,
|
||||
entity_type,
|
||||
@@ -238,7 +268,7 @@ impl AnyEntity {
|
||||
.unwrap()
|
||||
.write()
|
||||
.leak_detector
|
||||
.handle_created(id),
|
||||
.handle_created(id, Some(type_name)),
|
||||
entity_map,
|
||||
}
|
||||
}
|
||||
@@ -301,7 +331,7 @@ impl Clone for AnyEntity {
|
||||
.unwrap()
|
||||
.write()
|
||||
.leak_detector
|
||||
.handle_created(self.entity_id),
|
||||
.handle_created(self.entity_id, None),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -397,7 +427,13 @@ impl<T: 'static> Entity<T> {
|
||||
T: 'static,
|
||||
{
|
||||
Self {
|
||||
any_entity: AnyEntity::new(id, TypeId::of::<T>(), entity_map),
|
||||
any_entity: AnyEntity::new(
|
||||
id,
|
||||
TypeId::of::<T>(),
|
||||
entity_map,
|
||||
#[cfg(any(test, feature = "leak-detection"))]
|
||||
std::any::type_name::<T>(),
|
||||
),
|
||||
entity_type: PhantomData,
|
||||
}
|
||||
}
|
||||
@@ -431,11 +467,7 @@ impl<T: 'static> Entity<T> {
|
||||
|
||||
/// Read the entity referenced by this handle with the given function.
|
||||
#[inline]
|
||||
pub fn read_with<R, C: AppContext>(
|
||||
&self,
|
||||
cx: &C,
|
||||
f: impl FnOnce(&T, &App) -> R,
|
||||
) -> C::Result<R> {
|
||||
pub fn read_with<R, C: AppContext>(&self, cx: &C, f: impl FnOnce(&T, &App) -> R) -> R {
|
||||
cx.read_entity(self, f)
|
||||
}
|
||||
|
||||
@@ -445,18 +477,18 @@ impl<T: 'static> Entity<T> {
|
||||
&self,
|
||||
cx: &mut C,
|
||||
update: impl FnOnce(&mut T, &mut Context<T>) -> R,
|
||||
) -> C::Result<R> {
|
||||
) -> R {
|
||||
cx.update_entity(self, update)
|
||||
}
|
||||
|
||||
/// Updates the entity referenced by this handle with the given function.
|
||||
#[inline]
|
||||
pub fn as_mut<'a, C: AppContext>(&self, cx: &'a mut C) -> C::Result<GpuiBorrow<'a, T>> {
|
||||
pub fn as_mut<'a, C: AppContext>(&self, cx: &'a mut C) -> GpuiBorrow<'a, T> {
|
||||
cx.as_mut(self)
|
||||
}
|
||||
|
||||
/// Updates the entity referenced by this handle with the given function.
|
||||
pub fn write<C: AppContext>(&self, cx: &mut C, value: T) -> C::Result<()> {
|
||||
pub fn write<C: AppContext>(&self, cx: &mut C, value: T) {
|
||||
self.update(cx, |entity, cx| {
|
||||
*entity = value;
|
||||
cx.notify();
|
||||
@@ -465,7 +497,7 @@ impl<T: 'static> Entity<T> {
|
||||
|
||||
/// Updates the entity referenced by this handle with the given function if
|
||||
/// the referenced entity still exists, within a visual context that has a window.
|
||||
/// Returns an error if the entity has been released.
|
||||
/// Returns an error if the window has been closed.
|
||||
#[inline]
|
||||
pub fn update_in<R, C: VisualContext>(
|
||||
&self,
|
||||
@@ -580,7 +612,7 @@ impl AnyWeakEntity {
|
||||
.unwrap()
|
||||
.write()
|
||||
.leak_detector
|
||||
.handle_created(self.entity_id),
|
||||
.handle_created(self.entity_id, None),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -749,13 +781,9 @@ impl<T: 'static> WeakEntity<T> {
|
||||
) -> Result<R>
|
||||
where
|
||||
C: AppContext,
|
||||
Result<C::Result<R>>: crate::Flatten<R>,
|
||||
{
|
||||
crate::Flatten::flatten(
|
||||
self.upgrade()
|
||||
.context("entity released")
|
||||
.map(|this| cx.update_entity(&this, update)),
|
||||
)
|
||||
let entity = self.upgrade().context("entity released")?;
|
||||
Ok(cx.update_entity(&entity, update))
|
||||
}
|
||||
|
||||
/// Updates the entity referenced by this handle with the given function if
|
||||
@@ -768,14 +796,13 @@ impl<T: 'static> WeakEntity<T> {
|
||||
) -> Result<R>
|
||||
where
|
||||
C: VisualContext,
|
||||
Result<C::Result<R>>: crate::Flatten<R>,
|
||||
{
|
||||
let window = cx.window_handle();
|
||||
let this = self.upgrade().context("entity released")?;
|
||||
let entity = self.upgrade().context("entity released")?;
|
||||
|
||||
crate::Flatten::flatten(window.update(cx, |_, window, cx| {
|
||||
this.update(cx, |entity, cx| update(entity, window, cx))
|
||||
}))
|
||||
window.update(cx, |_, window, cx| {
|
||||
entity.update(cx, |entity, cx| update(entity, window, cx))
|
||||
})
|
||||
}
|
||||
|
||||
/// Reads the entity referenced by this handle with the given function if
|
||||
@@ -784,13 +811,9 @@ impl<T: 'static> WeakEntity<T> {
|
||||
pub fn read_with<C, R>(&self, cx: &C, read: impl FnOnce(&T, &App) -> R) -> Result<R>
|
||||
where
|
||||
C: AppContext,
|
||||
Result<C::Result<R>>: crate::Flatten<R>,
|
||||
{
|
||||
crate::Flatten::flatten(
|
||||
self.upgrade()
|
||||
.context("entity released")
|
||||
.map(|this| cx.read_entity(&this, read)),
|
||||
)
|
||||
let entity = self.upgrade().context("entity released")?;
|
||||
Ok(cx.read_entity(&entity, read))
|
||||
}
|
||||
|
||||
/// Create a new weak entity that can never be upgraded.
|
||||
@@ -907,7 +930,23 @@ pub(crate) struct HandleId {
|
||||
#[cfg(any(test, feature = "leak-detection"))]
|
||||
pub(crate) struct LeakDetector {
|
||||
next_handle_id: u64,
|
||||
entity_handles: HashMap<EntityId, HashMap<HandleId, Option<backtrace::Backtrace>>>,
|
||||
entity_handles: HashMap<EntityId, EntityLeakData>,
|
||||
}
|
||||
|
||||
/// A snapshot of the set of alive entities at a point in time.
|
||||
///
|
||||
/// Created by [`LeakDetector::snapshot`]. Can later be passed to
|
||||
/// [`LeakDetector::assert_no_new_leaks`] to verify that no new entity
|
||||
/// handles remain between the snapshot and the current state.
|
||||
#[cfg(any(test, feature = "leak-detection"))]
|
||||
pub struct LeakDetectorSnapshot {
|
||||
entity_ids: collections::HashSet<EntityId>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "leak-detection"))]
|
||||
struct EntityLeakData {
|
||||
handles: HashMap<HandleId, Option<backtrace::Backtrace>>,
|
||||
type_name: &'static str,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "leak-detection"))]
|
||||
@@ -918,11 +957,21 @@ impl LeakDetector {
|
||||
/// the handle is dropped. If `LEAK_BACKTRACE` is set, captures a backtrace
|
||||
/// at the allocation site.
|
||||
#[track_caller]
|
||||
pub fn handle_created(&mut self, entity_id: EntityId) -> HandleId {
|
||||
pub fn handle_created(
|
||||
&mut self,
|
||||
entity_id: EntityId,
|
||||
type_name: Option<&'static str>,
|
||||
) -> HandleId {
|
||||
let id = util::post_inc(&mut self.next_handle_id);
|
||||
let handle_id = HandleId { id };
|
||||
let handles = self.entity_handles.entry(entity_id).or_default();
|
||||
handles.insert(
|
||||
let handles = self
|
||||
.entity_handles
|
||||
.entry(entity_id)
|
||||
.or_insert_with(|| EntityLeakData {
|
||||
handles: HashMap::default(),
|
||||
type_name: type_name.unwrap_or("<unknown>"),
|
||||
});
|
||||
handles.handles.insert(
|
||||
handle_id,
|
||||
LEAK_BACKTRACE.then(backtrace::Backtrace::new_unresolved),
|
||||
);
|
||||
@@ -934,8 +983,14 @@ impl LeakDetector {
|
||||
/// This removes the handle from tracking. The `handle_id` should be the same
|
||||
/// one returned by `handle_created` when the handle was allocated.
|
||||
pub fn handle_released(&mut self, entity_id: EntityId, handle_id: HandleId) {
|
||||
let handles = self.entity_handles.entry(entity_id).or_default();
|
||||
handles.remove(&handle_id);
|
||||
if let std::collections::hash_map::Entry::Occupied(mut data) =
|
||||
self.entity_handles.entry(entity_id)
|
||||
{
|
||||
data.get_mut().handles.remove(&handle_id);
|
||||
if data.get().handles.is_empty() {
|
||||
data.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Asserts that all handles to the given entity have been released.
|
||||
@@ -947,11 +1002,10 @@ impl LeakDetector {
|
||||
/// otherwise it suggests setting the environment variable to get more info.
|
||||
pub fn assert_released(&mut self, entity_id: EntityId) {
|
||||
use std::fmt::Write as _;
|
||||
let handles = self.entity_handles.entry(entity_id).or_default();
|
||||
if !handles.is_empty() {
|
||||
if let Some(data) = self.entity_handles.remove(&entity_id) {
|
||||
let mut out = String::new();
|
||||
for backtrace in handles.values_mut() {
|
||||
if let Some(mut backtrace) = backtrace.take() {
|
||||
for (_, backtrace) in data.handles {
|
||||
if let Some(mut backtrace) = backtrace {
|
||||
backtrace.resolve();
|
||||
writeln!(out, "Leaked handle:\n{:?}", backtrace).unwrap();
|
||||
} else {
|
||||
@@ -965,6 +1019,96 @@ impl LeakDetector {
|
||||
panic!("{out}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Captures a snapshot of all entity IDs that currently have alive handles.
|
||||
///
|
||||
/// The returned [`LeakDetectorSnapshot`] can later be passed to
|
||||
/// [`assert_no_new_leaks`](Self::assert_no_new_leaks) to verify that no
|
||||
/// entities created after the snapshot are still alive.
|
||||
pub fn snapshot(&self) -> LeakDetectorSnapshot {
|
||||
LeakDetectorSnapshot {
|
||||
entity_ids: self.entity_handles.keys().copied().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Asserts that no entities created after `snapshot` still have alive handles.
|
||||
///
|
||||
/// Entities that were already tracked at the time of the snapshot are ignored,
|
||||
/// even if they still have handles. Only *new* entities (those whose
|
||||
/// `EntityId` was not present in the snapshot) are considered leaks.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if any new entity handles exist. The panic message lists every
|
||||
/// leaked entity with its type name, and includes allocation-site backtraces
|
||||
/// when `LEAK_BACKTRACE` is set.
|
||||
pub fn assert_no_new_leaks(&self, snapshot: &LeakDetectorSnapshot) {
|
||||
use std::fmt::Write as _;
|
||||
|
||||
let mut out = String::new();
|
||||
for (entity_id, data) in &self.entity_handles {
|
||||
if snapshot.entity_ids.contains(entity_id) {
|
||||
continue;
|
||||
}
|
||||
for (_, backtrace) in &data.handles {
|
||||
if let Some(backtrace) = backtrace {
|
||||
let mut backtrace = backtrace.clone();
|
||||
backtrace.resolve();
|
||||
writeln!(
|
||||
out,
|
||||
"Leaked handle for entity {} ({entity_id:?}):\n{:?}",
|
||||
data.type_name, backtrace
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(
|
||||
out,
|
||||
"Leaked handle for entity {} ({entity_id:?}): (export LEAK_BACKTRACE to find allocation site)",
|
||||
data.type_name
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !out.is_empty() {
|
||||
panic!("New entity leaks detected since snapshot:\n{out}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "leak-detection"))]
|
||||
impl Drop for LeakDetector {
|
||||
fn drop(&mut self) {
|
||||
use std::fmt::Write;
|
||||
|
||||
if self.entity_handles.is_empty() || std::thread::panicking() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut out = String::new();
|
||||
for (entity_id, data) in self.entity_handles.drain() {
|
||||
for (_handle, backtrace) in data.handles {
|
||||
if let Some(mut backtrace) = backtrace {
|
||||
backtrace.resolve();
|
||||
writeln!(
|
||||
out,
|
||||
"Leaked handle for entity {} ({entity_id:?}):\n{:?}",
|
||||
data.type_name, backtrace
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(
|
||||
out,
|
||||
"Leaked handle for entity {} ({entity_id:?}): (export LEAK_BACKTRACE to find allocation site)",
|
||||
data.type_name
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
panic!("Exited with leaked handles:\n{out}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1022,4 +1166,42 @@ mod test {
|
||||
vec![1],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_leak_detector_snapshot_no_leaks() {
|
||||
let mut entity_map = EntityMap::new();
|
||||
|
||||
let slot = entity_map.reserve::<TestEntity>();
|
||||
let pre_existing = entity_map.insert(slot, TestEntity { i: 1 });
|
||||
|
||||
let snapshot = entity_map.leak_detector_snapshot();
|
||||
|
||||
let slot = entity_map.reserve::<TestEntity>();
|
||||
let temporary = entity_map.insert(slot, TestEntity { i: 2 });
|
||||
drop(temporary);
|
||||
|
||||
entity_map.assert_no_new_leaks(&snapshot);
|
||||
|
||||
drop(pre_existing);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "New entity leaks detected since snapshot")]
|
||||
fn test_leak_detector_snapshot_detects_new_leak() {
|
||||
let mut entity_map = EntityMap::new();
|
||||
|
||||
let slot = entity_map.reserve::<TestEntity>();
|
||||
let pre_existing = entity_map.insert(slot, TestEntity { i: 1 });
|
||||
|
||||
let snapshot = entity_map.leak_detector_snapshot();
|
||||
|
||||
let slot = entity_map.reserve::<TestEntity>();
|
||||
let leaked = entity_map.insert(slot, TestEntity { i: 2 });
|
||||
|
||||
// `leaked` is still alive, so this should panic.
|
||||
entity_map.assert_no_new_leaks(&snapshot);
|
||||
|
||||
drop(pre_existing);
|
||||
drop(leaked);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
//! 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, 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(()), || 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, || 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() -> 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() -> 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 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,607 @@
|
||||
//! 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();
|
||||
}
|
||||
|
||||
/// 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();
|
||||
})
|
||||
.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_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");
|
||||
});
|
||||
}
|
||||
}
|
||||
+100
-94
@@ -5,11 +5,11 @@ use crate::{
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
|
||||
Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform,
|
||||
TestScreenCaptureSource, TestWindow, TextSystem, VisualContext, Window, WindowBounds,
|
||||
WindowHandle, WindowOptions, app::GpuiMode,
|
||||
WindowHandle, WindowOptions, app::GpuiMode, window::ElementArenaScope,
|
||||
};
|
||||
use anyhow::{anyhow, bail};
|
||||
use futures::{Stream, StreamExt, channel::oneshot};
|
||||
use rand::{SeedableRng, rngs::StdRng};
|
||||
|
||||
use std::{
|
||||
cell::RefCell, future::Future, ops::Deref, path::PathBuf, rc::Rc, sync::Arc, time::Duration,
|
||||
};
|
||||
@@ -18,8 +18,6 @@ use std::{
|
||||
/// an implementation of `Context` with additional methods that are useful in tests.
|
||||
#[derive(Clone)]
|
||||
pub struct TestAppContext {
|
||||
#[doc(hidden)]
|
||||
pub app: Rc<AppCell>,
|
||||
#[doc(hidden)]
|
||||
pub background_executor: BackgroundExecutor,
|
||||
#[doc(hidden)]
|
||||
@@ -30,20 +28,17 @@ pub struct TestAppContext {
|
||||
text_system: Arc<TextSystem>,
|
||||
fn_name: Option<&'static str>,
|
||||
on_quit: Rc<RefCell<Vec<Box<dyn FnOnce() + 'static>>>>,
|
||||
#[doc(hidden)]
|
||||
pub app: Rc<AppCell>,
|
||||
}
|
||||
|
||||
impl AppContext for TestAppContext {
|
||||
type Result<T> = T;
|
||||
|
||||
fn new<T: 'static>(
|
||||
&mut self,
|
||||
build_entity: impl FnOnce(&mut Context<T>) -> T,
|
||||
) -> Self::Result<Entity<T>> {
|
||||
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) -> Self::Result<crate::Reservation<T>> {
|
||||
fn reserve_entity<T: 'static>(&mut self) -> crate::Reservation<T> {
|
||||
let mut app = self.app.borrow_mut();
|
||||
app.reserve_entity()
|
||||
}
|
||||
@@ -52,7 +47,7 @@ impl AppContext for TestAppContext {
|
||||
&mut self,
|
||||
reservation: crate::Reservation<T>,
|
||||
build_entity: impl FnOnce(&mut Context<T>) -> T,
|
||||
) -> Self::Result<Entity<T>> {
|
||||
) -> Entity<T> {
|
||||
let mut app = self.app.borrow_mut();
|
||||
app.insert_entity(reservation, build_entity)
|
||||
}
|
||||
@@ -61,23 +56,19 @@ impl AppContext for TestAppContext {
|
||||
&mut self,
|
||||
handle: &Entity<T>,
|
||||
update: impl FnOnce(&mut T, &mut Context<T>) -> R,
|
||||
) -> Self::Result<R> {
|
||||
) -> R {
|
||||
let mut app = self.app.borrow_mut();
|
||||
app.update_entity(handle, update)
|
||||
}
|
||||
|
||||
fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> Self::Result<super::GpuiBorrow<'a, T>>
|
||||
fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> super::GpuiBorrow<'a, T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
panic!("Cannot use as_mut with a test app context. Try calling update() first")
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &Entity<T>,
|
||||
read: impl FnOnce(&T, &App) -> R,
|
||||
) -> Self::Result<R>
|
||||
fn read_entity<T, R>(&self, handle: &Entity<T>, read: impl FnOnce(&T, &App) -> R) -> R
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
@@ -112,7 +103,7 @@ impl AppContext for TestAppContext {
|
||||
self.background_executor.spawn(future)
|
||||
}
|
||||
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> Self::Result<R>
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
|
||||
where
|
||||
G: Global,
|
||||
{
|
||||
@@ -132,7 +123,7 @@ impl TestAppContext {
|
||||
let http_client = http_client::FakeHttpClient::with_404_response();
|
||||
let text_system = Arc::new(TextSystem::new(platform.text_system()));
|
||||
|
||||
let mut app = App::new_app(platform.clone(), asset_source, http_client);
|
||||
let app = App::new_app(platform.clone(), asset_source, http_client);
|
||||
app.borrow_mut().mode = GpuiMode::test();
|
||||
|
||||
Self {
|
||||
@@ -154,7 +145,7 @@ impl TestAppContext {
|
||||
|
||||
/// Create a single TestAppContext, for non-multi-client tests
|
||||
pub fn single() -> Self {
|
||||
let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0));
|
||||
let dispatcher = TestDispatcher::new(0);
|
||||
Self::build(dispatcher, None)
|
||||
}
|
||||
|
||||
@@ -241,6 +232,33 @@ impl TestAppContext {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Opens a new window with a specific size.
|
||||
///
|
||||
/// Unlike `add_window` which uses maximized bounds, this allows controlling
|
||||
/// the window dimensions, which is important for layout-sensitive tests.
|
||||
pub fn open_window<F, V>(
|
||||
&mut self,
|
||||
window_size: Size<Pixels>,
|
||||
build_window: F,
|
||||
) -> WindowHandle<V>
|
||||
where
|
||||
F: FnOnce(&mut Window, &mut Context<V>) -> V,
|
||||
V: 'static + Render,
|
||||
{
|
||||
let mut cx = self.app.borrow_mut();
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(Bounds {
|
||||
origin: Point::default(),
|
||||
size: window_size,
|
||||
})),
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| cx.new(|cx| build_window(window, cx)),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Adds a new window with no content.
|
||||
pub fn add_empty_window(&mut self) -> &mut VisualTestContext {
|
||||
let mut cx = self.app.borrow_mut();
|
||||
@@ -411,8 +429,8 @@ impl TestAppContext {
|
||||
}
|
||||
|
||||
/// Wait until there are no more pending tasks.
|
||||
pub fn run_until_parked(&mut self) {
|
||||
self.background_executor.run_until_parked()
|
||||
pub fn run_until_parked(&self) {
|
||||
self.dispatcher.run_until_parked();
|
||||
}
|
||||
|
||||
/// Simulate dispatching an action to the currently focused node in the window.
|
||||
@@ -530,22 +548,25 @@ impl TestAppContext {
|
||||
let mut notifications = self.notifications(entity);
|
||||
|
||||
use futures::FutureExt as _;
|
||||
use smol::future::FutureExt as _;
|
||||
use futures_concurrency::future::Race as _;
|
||||
|
||||
async {
|
||||
loop {
|
||||
if entity.update(self, &mut predicate) {
|
||||
return Ok(());
|
||||
}
|
||||
(
|
||||
async {
|
||||
loop {
|
||||
if entity.update(self, &mut predicate) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if notifications.next().await.is_none() {
|
||||
bail!("entity dropped")
|
||||
if notifications.next().await.is_none() {
|
||||
bail!("entity dropped")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.race(timer.map(|_| Err(anyhow!("condition timed out"))))
|
||||
.await
|
||||
.unwrap();
|
||||
},
|
||||
timer.map(|_| Err(anyhow!("condition timed out"))),
|
||||
)
|
||||
.race()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Set a name for this App.
|
||||
@@ -594,20 +615,13 @@ impl<V: 'static> Entity<V> {
|
||||
tx.try_send(()).ok();
|
||||
});
|
||||
|
||||
let duration = if std::env::var("CI").is_ok() {
|
||||
Duration::from_secs(5)
|
||||
} else {
|
||||
Duration::from_secs(1)
|
||||
};
|
||||
|
||||
cx.executor().advance_clock(advance_clock_by);
|
||||
|
||||
async move {
|
||||
let notification = crate::util::smol_timeout(duration, rx.recv())
|
||||
rx.recv()
|
||||
.await
|
||||
.expect("next notification timed out");
|
||||
.expect("entity dropped while test was waiting for its next notification");
|
||||
drop(subscription);
|
||||
notification.expect("entity dropped while test was waiting for its next notification")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -647,31 +661,25 @@ impl<V> Entity<V> {
|
||||
let handle = self.downgrade();
|
||||
|
||||
async move {
|
||||
crate::util::smol_timeout(Duration::from_secs(1), async move {
|
||||
loop {
|
||||
{
|
||||
let cx = cx.borrow();
|
||||
let cx = &*cx;
|
||||
if predicate(
|
||||
handle
|
||||
.upgrade()
|
||||
.expect("view dropped with pending condition")
|
||||
.read(cx),
|
||||
cx,
|
||||
) {
|
||||
break;
|
||||
}
|
||||
loop {
|
||||
{
|
||||
let cx = cx.borrow();
|
||||
let cx = &*cx;
|
||||
if predicate(
|
||||
handle
|
||||
.upgrade()
|
||||
.expect("view dropped with pending condition")
|
||||
.read(cx),
|
||||
cx,
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
cx.borrow().background_executor().start_waiting();
|
||||
rx.recv()
|
||||
.await
|
||||
.expect("view dropped with pending condition");
|
||||
cx.borrow().background_executor().finish_waiting();
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("condition timed out");
|
||||
|
||||
rx.recv()
|
||||
.await
|
||||
.expect("view dropped with pending condition");
|
||||
}
|
||||
drop(subscriptions);
|
||||
}
|
||||
}
|
||||
@@ -838,6 +846,8 @@ impl VisualTestContext {
|
||||
E: Element,
|
||||
{
|
||||
self.update(|window, cx| {
|
||||
let _arena_scope = ElementArenaScope::enter(&cx.element_arena);
|
||||
|
||||
window.invalidator.set_phase(DrawPhase::Prepaint);
|
||||
let mut element = Drawable::new(f(window, cx));
|
||||
element.layout_as_root(space.into(), window, cx);
|
||||
@@ -849,6 +859,9 @@ impl VisualTestContext {
|
||||
window.invalidator.set_phase(DrawPhase::None);
|
||||
window.refresh();
|
||||
|
||||
drop(element);
|
||||
cx.element_arena.borrow_mut().clear();
|
||||
|
||||
(request_layout_state, prepaint_state)
|
||||
})
|
||||
}
|
||||
@@ -916,16 +929,11 @@ impl VisualTestContext {
|
||||
}
|
||||
|
||||
impl AppContext for VisualTestContext {
|
||||
type Result<T> = <TestAppContext as AppContext>::Result<T>;
|
||||
|
||||
fn new<T: 'static>(
|
||||
&mut self,
|
||||
build_entity: impl FnOnce(&mut Context<T>) -> T,
|
||||
) -> Self::Result<Entity<T>> {
|
||||
fn new<T: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T> {
|
||||
self.cx.new(build_entity)
|
||||
}
|
||||
|
||||
fn reserve_entity<T: 'static>(&mut self) -> Self::Result<crate::Reservation<T>> {
|
||||
fn reserve_entity<T: 'static>(&mut self) -> crate::Reservation<T> {
|
||||
self.cx.reserve_entity()
|
||||
}
|
||||
|
||||
@@ -933,7 +941,7 @@ impl AppContext for VisualTestContext {
|
||||
&mut self,
|
||||
reservation: crate::Reservation<T>,
|
||||
build_entity: impl FnOnce(&mut Context<T>) -> T,
|
||||
) -> Self::Result<Entity<T>> {
|
||||
) -> Entity<T> {
|
||||
self.cx.insert_entity(reservation, build_entity)
|
||||
}
|
||||
|
||||
@@ -941,25 +949,21 @@ impl AppContext for VisualTestContext {
|
||||
&mut self,
|
||||
handle: &Entity<T>,
|
||||
update: impl FnOnce(&mut T, &mut Context<T>) -> R,
|
||||
) -> Self::Result<R>
|
||||
) -> R
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.cx.update_entity(handle, update)
|
||||
}
|
||||
|
||||
fn as_mut<'a, T>(&'a mut self, handle: &Entity<T>) -> Self::Result<super::GpuiBorrow<'a, T>>
|
||||
fn as_mut<'a, T>(&'a mut self, handle: &Entity<T>) -> super::GpuiBorrow<'a, T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.cx.as_mut(handle)
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &Entity<T>,
|
||||
read: impl FnOnce(&T, &App) -> R,
|
||||
) -> Self::Result<R>
|
||||
fn read_entity<T, R>(&self, handle: &Entity<T>, read: impl FnOnce(&T, &App) -> R) -> R
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
@@ -991,7 +995,7 @@ impl AppContext for VisualTestContext {
|
||||
self.cx.background_spawn(future)
|
||||
}
|
||||
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> Self::Result<R>
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
|
||||
where
|
||||
G: Global,
|
||||
{
|
||||
@@ -1000,6 +1004,8 @@ impl AppContext for VisualTestContext {
|
||||
}
|
||||
|
||||
impl VisualContext for VisualTestContext {
|
||||
type Result<T> = T;
|
||||
|
||||
/// Get the underlying window handle underlying this context.
|
||||
fn window_handle(&self) -> AnyWindowHandle {
|
||||
self.window
|
||||
@@ -1008,30 +1014,30 @@ impl VisualContext for VisualTestContext {
|
||||
fn new_window_entity<T: 'static>(
|
||||
&mut self,
|
||||
build_entity: impl FnOnce(&mut Window, &mut Context<T>) -> T,
|
||||
) -> Self::Result<Entity<T>> {
|
||||
) -> Entity<T> {
|
||||
self.window
|
||||
.update(&mut self.cx, |_, window, cx| {
|
||||
cx.new(|cx| build_entity(window, cx))
|
||||
})
|
||||
.unwrap()
|
||||
.expect("window was unexpectedly closed")
|
||||
}
|
||||
|
||||
fn update_window_entity<V: 'static, R>(
|
||||
&mut self,
|
||||
view: &Entity<V>,
|
||||
update: impl FnOnce(&mut V, &mut Window, &mut Context<V>) -> R,
|
||||
) -> Self::Result<R> {
|
||||
) -> R {
|
||||
self.window
|
||||
.update(&mut self.cx, |_, window, cx| {
|
||||
view.update(cx, |v, cx| update(v, window, cx))
|
||||
})
|
||||
.unwrap()
|
||||
.expect("window was unexpectedly closed")
|
||||
}
|
||||
|
||||
fn replace_root_view<V>(
|
||||
&mut self,
|
||||
build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,
|
||||
) -> Self::Result<Entity<V>>
|
||||
) -> Entity<V>
|
||||
where
|
||||
V: 'static + Render,
|
||||
{
|
||||
@@ -1039,15 +1045,15 @@ impl VisualContext for VisualTestContext {
|
||||
.update(&mut self.cx, |_, window, cx| {
|
||||
window.replace_root(cx, build_view)
|
||||
})
|
||||
.unwrap()
|
||||
.expect("window was unexpectedly closed")
|
||||
}
|
||||
|
||||
fn focus<V: crate::Focusable>(&mut self, view: &Entity<V>) -> Self::Result<()> {
|
||||
fn focus<V: crate::Focusable>(&mut self, view: &Entity<V>) {
|
||||
self.window
|
||||
.update(&mut self.cx, |_, window, cx| {
|
||||
view.read(cx).focus_handle(cx).focus(window)
|
||||
view.read(cx).focus_handle(cx).focus(window, cx)
|
||||
})
|
||||
.unwrap()
|
||||
.expect("window was unexpectedly closed")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,475 @@
|
||||
use crate::{
|
||||
Action, AnyView, AnyWindowHandle, App, AppCell, AppContext, AssetSource, BackgroundExecutor,
|
||||
Bounds, ClipboardItem, Context, Entity, ForegroundExecutor, Global, InputEvent, Keystroke,
|
||||
Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Platform, Point,
|
||||
Render, Result, Size, Task, TestDispatcher, TextSystem, VisualTestPlatform, Window,
|
||||
WindowBounds, WindowHandle, WindowOptions, app::GpuiMode,
|
||||
};
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
+4
-3
@@ -33,9 +33,10 @@ impl AssetSource for () {
|
||||
pub struct ImageId(pub usize);
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone)]
|
||||
pub(crate) struct RenderImageParams {
|
||||
pub(crate) image_id: ImageId,
|
||||
pub(crate) frame_index: usize,
|
||||
#[expect(missing_docs)]
|
||||
pub struct RenderImageParams {
|
||||
pub image_id: ImageId,
|
||||
pub frame_index: usize,
|
||||
}
|
||||
|
||||
/// A cached and processed image, in BGRA format
|
||||
|
||||
+309
-179
@@ -5,14 +5,91 @@ use std::{
|
||||
ops::{Add, Sub},
|
||||
};
|
||||
|
||||
/// 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,
|
||||
{
|
||||
root: Option<usize>,
|
||||
/// All nodes stored contiguously for cache efficiency.
|
||||
nodes: Vec<Node<U>>,
|
||||
stack: Vec<usize>,
|
||||
/// 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<usize>,
|
||||
}
|
||||
|
||||
/// 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>
|
||||
@@ -26,158 +103,250 @@ where
|
||||
+ Half
|
||||
+ Default,
|
||||
{
|
||||
/// Clears all nodes from the tree.
|
||||
pub fn clear(&mut self) {
|
||||
self.root = None;
|
||||
self.nodes.clear();
|
||||
self.stack.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 {
|
||||
// If the tree is empty, make the root the new leaf.
|
||||
let Some(mut index) = self.root else {
|
||||
let new_node = self.push_leaf(new_bounds, 1);
|
||||
self.root = Some(new_node);
|
||||
return 1;
|
||||
// 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,
|
||||
};
|
||||
|
||||
// Search for the best place to add the new leaf based on heuristics.
|
||||
let mut max_intersecting_ordering = 0;
|
||||
while let Node::Internal {
|
||||
left,
|
||||
right,
|
||||
bounds: node_bounds,
|
||||
..
|
||||
} = &mut self.nodes[index]
|
||||
{
|
||||
let left = *left;
|
||||
let right = *right;
|
||||
*node_bounds = node_bounds.union(&new_bounds);
|
||||
self.stack.push(index);
|
||||
|
||||
// Descend to the best-fit child, based on which one would increase
|
||||
// the surface area the least. This attempts to keep the tree balanced
|
||||
// in terms of surface area. If there is an intersection with the other child,
|
||||
// add its keys to the intersections vector.
|
||||
let left_cost = new_bounds.union(self.nodes[left].bounds()).half_perimeter();
|
||||
let right_cost = new_bounds
|
||||
.union(self.nodes[right].bounds())
|
||||
.half_perimeter();
|
||||
if left_cost < right_cost {
|
||||
max_intersecting_ordering =
|
||||
self.find_max_ordering(right, &new_bounds, max_intersecting_ordering);
|
||||
index = left;
|
||||
} else {
|
||||
max_intersecting_ordering =
|
||||
self.find_max_ordering(left, &new_bounds, max_intersecting_ordering);
|
||||
index = right;
|
||||
}
|
||||
}
|
||||
|
||||
// We've found a leaf ('index' now refers to a leaf node).
|
||||
// We'll insert a new parent node above the leaf and attach our new leaf to it.
|
||||
let sibling = index;
|
||||
|
||||
// Check for collision with the located leaf node
|
||||
let Node::Leaf {
|
||||
bounds: sibling_bounds,
|
||||
order: sibling_ordering,
|
||||
..
|
||||
} = &self.nodes[index]
|
||||
else {
|
||||
unreachable!();
|
||||
};
|
||||
if sibling_bounds.intersects(&new_bounds) {
|
||||
max_intersecting_ordering = cmp::max(max_intersecting_ordering, *sibling_ordering);
|
||||
}
|
||||
|
||||
let ordering = max_intersecting_ordering + 1;
|
||||
let new_node = self.push_leaf(new_bounds, ordering);
|
||||
let new_parent = self.push_internal(sibling, new_node);
|
||||
|
||||
// If there was an old parent, we need to update its children indices.
|
||||
if let Some(old_parent) = self.stack.last().copied() {
|
||||
let Node::Internal { left, right, .. } = &mut self.nodes[old_parent] else {
|
||||
unreachable!();
|
||||
};
|
||||
|
||||
if *left == sibling {
|
||||
*left = new_parent;
|
||||
} else {
|
||||
*right = new_parent;
|
||||
}
|
||||
} else {
|
||||
// If the old parent was the root, the new parent is the new root.
|
||||
self.root = Some(new_parent);
|
||||
}
|
||||
|
||||
for node_index in self.stack.drain(..).rev() {
|
||||
let Node::Internal {
|
||||
max_order: max_ordering,
|
||||
..
|
||||
} = &mut self.nodes[node_index]
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
if *max_ordering >= ordering {
|
||||
break;
|
||||
}
|
||||
*max_ordering = ordering;
|
||||
}
|
||||
|
||||
ordering
|
||||
}
|
||||
|
||||
fn find_max_ordering(&self, index: usize, bounds: &Bounds<U>, mut max_ordering: u32) -> u32 {
|
||||
match &self.nodes[index] {
|
||||
Node::Leaf {
|
||||
bounds: node_bounds,
|
||||
order: ordering,
|
||||
..
|
||||
} => {
|
||||
if bounds.intersects(node_bounds) {
|
||||
max_ordering = cmp::max(*ordering, max_ordering);
|
||||
}
|
||||
/// 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;
|
||||
}
|
||||
Node::Internal {
|
||||
left,
|
||||
right,
|
||||
bounds: node_bounds,
|
||||
max_order: node_max_ordering,
|
||||
..
|
||||
} => {
|
||||
if bounds.intersects(node_bounds) && max_ordering < *node_max_ordering {
|
||||
let left_max_ordering = self.nodes[*left].max_ordering();
|
||||
let right_max_ordering = self.nodes[*right].max_ordering();
|
||||
if left_max_ordering > right_max_ordering {
|
||||
max_ordering = self.find_max_ordering(*left, bounds, max_ordering);
|
||||
max_ordering = self.find_max_ordering(*right, bounds, max_ordering);
|
||||
} else {
|
||||
max_ordering = self.find_max_ordering(*right, bounds, max_ordering);
|
||||
max_ordering = self.find_max_ordering(*left, bounds, max_ordering);
|
||||
}
|
||||
|
||||
// Slow path: search the tree
|
||||
self.search_stack.clear();
|
||||
self.search_stack.push(root_idx);
|
||||
|
||||
let mut max_found = 0u32;
|
||||
|
||||
while let Some(node_idx) = self.search_stack.pop() {
|
||||
let node = &self.nodes[node_idx];
|
||||
|
||||
// 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.
|
||||
for &child_idx in children.as_slice() {
|
||||
if self.nodes[child_idx].max_order > max_found {
|
||||
self.search_stack.push(child_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
max_ordering
|
||||
|
||||
max_found
|
||||
}
|
||||
|
||||
fn push_leaf(&mut self, bounds: Bounds<U>, order: u32) -> usize {
|
||||
self.nodes.push(Node::Leaf { bounds, order });
|
||||
self.nodes.len() - 1
|
||||
}
|
||||
|
||||
fn push_internal(&mut self, left: usize, right: usize) -> usize {
|
||||
let left_node = &self.nodes[left];
|
||||
let right_node = &self.nodes[right];
|
||||
let new_bounds = left_node.bounds().union(right_node.bounds());
|
||||
let max_ordering = cmp::max(left_node.max_ordering(), right_node.max_ordering());
|
||||
self.nodes.push(Node::Internal {
|
||||
bounds: new_bounds,
|
||||
left,
|
||||
right,
|
||||
max_order: max_ordering,
|
||||
/// 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 },
|
||||
});
|
||||
self.nodes.len() - 1
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,50 +356,11 @@ where
|
||||
{
|
||||
fn default() -> Self {
|
||||
BoundsTree {
|
||||
root: None,
|
||||
nodes: Vec::new(),
|
||||
stack: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Node<U>
|
||||
where
|
||||
U: Clone + Debug + Default + PartialEq,
|
||||
{
|
||||
Leaf {
|
||||
bounds: Bounds<U>,
|
||||
order: u32,
|
||||
},
|
||||
Internal {
|
||||
left: usize,
|
||||
right: usize,
|
||||
bounds: Bounds<U>,
|
||||
max_order: u32,
|
||||
},
|
||||
}
|
||||
|
||||
impl<U> Node<U>
|
||||
where
|
||||
U: Clone + Debug + Default + PartialEq,
|
||||
{
|
||||
fn bounds(&self) -> &Bounds<U> {
|
||||
match self {
|
||||
Node::Leaf { bounds, .. } => bounds,
|
||||
Node::Internal { bounds, .. } => bounds,
|
||||
}
|
||||
}
|
||||
|
||||
fn max_ordering(&self) -> u32 {
|
||||
match self {
|
||||
Node::Leaf {
|
||||
order: ordering, ..
|
||||
} => *ordering,
|
||||
Node::Internal {
|
||||
max_order: max_ordering,
|
||||
..
|
||||
} => *max_ordering,
|
||||
root: None,
|
||||
max_leaf: None,
|
||||
insert_path: Vec::new(),
|
||||
search_stack: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+39
-17
@@ -23,7 +23,7 @@ pub fn rgba(hex: u32) -> Rgba {
|
||||
}
|
||||
|
||||
/// Swap from RGBA with premultiplied alpha to BGRA
|
||||
pub(crate) fn swap_rgba_pa_to_bgra(color: &mut [u8]) {
|
||||
pub fn swap_rgba_pa_to_bgra(color: &mut [u8]) {
|
||||
color.swap(0, 2);
|
||||
if color[3] > 0 {
|
||||
let a = color[3] as f32 / 255.;
|
||||
@@ -658,6 +658,7 @@ pub(crate) enum BackgroundTag {
|
||||
Solid = 0,
|
||||
LinearGradient = 1,
|
||||
PatternSlash = 2,
|
||||
Checkerboard = 3,
|
||||
}
|
||||
|
||||
/// A color space for color interpolation.
|
||||
@@ -701,20 +702,21 @@ impl std::fmt::Debug for Background {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self.tag {
|
||||
BackgroundTag::Solid => write!(f, "Solid({:?})", self.solid),
|
||||
BackgroundTag::LinearGradient => {
|
||||
write!(
|
||||
f,
|
||||
"LinearGradient({}, {:?}, {:?})",
|
||||
self.gradient_angle_or_pattern_height, self.colors[0], self.colors[1]
|
||||
)
|
||||
}
|
||||
BackgroundTag::PatternSlash => {
|
||||
write!(
|
||||
f,
|
||||
"PatternSlash({:?}, {})",
|
||||
self.solid, self.gradient_angle_or_pattern_height
|
||||
)
|
||||
}
|
||||
BackgroundTag::LinearGradient => write!(
|
||||
f,
|
||||
"LinearGradient({}, {:?}, {:?})",
|
||||
self.gradient_angle_or_pattern_height, self.colors[0], self.colors[1]
|
||||
),
|
||||
BackgroundTag::PatternSlash => write!(
|
||||
f,
|
||||
"PatternSlash({:?}, {})",
|
||||
self.solid, self.gradient_angle_or_pattern_height
|
||||
),
|
||||
BackgroundTag::Checkerboard => write!(
|
||||
f,
|
||||
"Checkerboard({:?}, {})",
|
||||
self.solid, self.gradient_angle_or_pattern_height
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -734,19 +736,29 @@ impl Default for Background {
|
||||
}
|
||||
|
||||
/// Creates a hash pattern background
|
||||
pub fn pattern_slash(color: Hsla, width: f32, interval: f32) -> Background {
|
||||
pub fn pattern_slash(color: impl Into<Hsla>, width: f32, interval: f32) -> Background {
|
||||
let width_scaled = (width * 255.0) as u32;
|
||||
let interval_scaled = (interval * 255.0) as u32;
|
||||
let height = ((width_scaled * 0xFFFF) + interval_scaled) as f32;
|
||||
|
||||
Background {
|
||||
tag: BackgroundTag::PatternSlash,
|
||||
solid: color,
|
||||
solid: color.into(),
|
||||
gradient_angle_or_pattern_height: height,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a checkerboard pattern background
|
||||
pub fn checkerboard(color: impl Into<Hsla>, size: f32) -> Background {
|
||||
Background {
|
||||
tag: BackgroundTag::Checkerboard,
|
||||
solid: color.into(),
|
||||
gradient_angle_or_pattern_height: size,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a solid background color.
|
||||
pub fn solid_background(color: impl Into<Hsla>) -> Background {
|
||||
Background {
|
||||
@@ -808,6 +820,15 @@ impl LinearColorStop {
|
||||
}
|
||||
|
||||
impl Background {
|
||||
/// Returns the solid color if this is a solid background, None otherwise.
|
||||
pub fn as_solid(&self) -> Option<Hsla> {
|
||||
if self.tag == BackgroundTag::Solid {
|
||||
Some(self.solid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Use specified color space for color interpolation.
|
||||
///
|
||||
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method>
|
||||
@@ -833,6 +854,7 @@ impl Background {
|
||||
BackgroundTag::Solid => self.solid.is_transparent(),
|
||||
BackgroundTag::LinearGradient => self.colors.iter().all(|c| c.color.is_transparent()),
|
||||
BackgroundTag::PatternSlash => self.solid.is_transparent(),
|
||||
BackgroundTag::Checkerboard => self.solid.is_transparent(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,18 +7,12 @@ use std::sync::Arc;
|
||||
/// These are used for styling base components, examples and more.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Colors {
|
||||
/// Primary text color
|
||||
/// Text color
|
||||
pub text: Rgba,
|
||||
/// Muted/secondary text color
|
||||
pub text_muted: Rgba,
|
||||
/// Selected text color
|
||||
pub selected_text: Rgba,
|
||||
/// Background color (root level)
|
||||
/// Background color
|
||||
pub background: Rgba,
|
||||
/// Surface color (cards, panels, elevated containers)
|
||||
pub surface: Rgba,
|
||||
/// Surface color on hover
|
||||
pub surface_hover: Rgba,
|
||||
/// Disabled color
|
||||
pub disabled: Rgba,
|
||||
/// Selected color
|
||||
@@ -29,24 +23,6 @@ pub struct Colors {
|
||||
pub separator: Rgba,
|
||||
/// Container color
|
||||
pub container: Rgba,
|
||||
/// Accent/primary action color (macOS blue)
|
||||
pub accent: Rgba,
|
||||
/// Accent color on hover
|
||||
pub accent_hover: Rgba,
|
||||
/// Accent color when active/pressed
|
||||
pub accent_active: Rgba,
|
||||
/// Success/positive color
|
||||
pub success: Rgba,
|
||||
/// Success color on hover
|
||||
pub success_hover: Rgba,
|
||||
/// Warning/caution color
|
||||
pub warning: Rgba,
|
||||
/// Warning color on hover
|
||||
pub warning_hover: Rgba,
|
||||
/// Error/destructive color
|
||||
pub error: Rgba,
|
||||
/// Error color on hover
|
||||
pub error_hover: Rgba,
|
||||
}
|
||||
|
||||
impl Default for Colors {
|
||||
@@ -64,85 +40,31 @@ impl Colors {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the default dark colors
|
||||
/// Returns the default dark colors.
|
||||
pub fn dark() -> Self {
|
||||
Self {
|
||||
// Text
|
||||
text: rgb(0xffffff),
|
||||
text_muted: rgb(0x98989d),
|
||||
selected_text: rgb(0xffffff),
|
||||
disabled: rgb(0x565656),
|
||||
|
||||
// Backgrounds
|
||||
background: rgb(0x1e1e1e),
|
||||
surface: rgb(0x2d2d2d),
|
||||
surface_hover: rgb(0x3d3d3d),
|
||||
selected: rgb(0x2457ca),
|
||||
background: rgb(0x222222),
|
||||
border: rgb(0x000000),
|
||||
separator: rgb(0xd9d9d9),
|
||||
container: rgb(0x262626),
|
||||
|
||||
// Borders
|
||||
border: rgb(0x3d3d3d),
|
||||
separator: rgb(0x3d3d3d),
|
||||
|
||||
// Selection
|
||||
selected: rgb(0x0058d0),
|
||||
|
||||
// Accent (macOS blue)
|
||||
accent: rgb(0x0a84ff),
|
||||
accent_hover: rgb(0x409cff),
|
||||
accent_active: rgb(0x0071e3),
|
||||
|
||||
// Success (green)
|
||||
success: rgb(0x30d158),
|
||||
success_hover: rgb(0x28cd52),
|
||||
|
||||
// Warning (yellow/orange)
|
||||
warning: rgb(0xffd60a),
|
||||
warning_hover: rgb(0xffcc00),
|
||||
|
||||
// Error (red)
|
||||
error: rgb(0xff453a),
|
||||
error_hover: rgb(0xff6961),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the default light colors
|
||||
/// Returns the default light colors.
|
||||
pub fn light() -> Self {
|
||||
Self {
|
||||
// Text
|
||||
text: rgb(0x1d1d1f),
|
||||
text_muted: rgb(0x86868b),
|
||||
text: rgb(0x252525),
|
||||
selected_text: rgb(0xffffff),
|
||||
disabled: rgb(0xb0b0b0),
|
||||
|
||||
// Backgrounds
|
||||
background: rgb(0xffffff),
|
||||
surface: rgb(0xf5f5f7),
|
||||
surface_hover: rgb(0xe8e8ed),
|
||||
container: rgb(0xf5f5f7),
|
||||
|
||||
// Borders
|
||||
border: rgb(0xd2d2d7),
|
||||
separator: rgb(0xd2d2d7),
|
||||
|
||||
// Selection
|
||||
selected: rgb(0x0066cc),
|
||||
|
||||
// Accent (macOS blue)
|
||||
accent: rgb(0x007aff),
|
||||
accent_hover: rgb(0x0071e3),
|
||||
accent_active: rgb(0x0058d0),
|
||||
|
||||
// Success (green)
|
||||
success: rgb(0x28cd41),
|
||||
success_hover: rgb(0x23b839),
|
||||
|
||||
// Warning (yellow/orange)
|
||||
warning: rgb(0xff9f0a),
|
||||
warning_hover: rgb(0xe68f09),
|
||||
|
||||
// Error (red)
|
||||
error: rgb(0xff3b30),
|
||||
error_hover: rgb(0xe6352b),
|
||||
disabled: rgb(0xb0b0b0),
|
||||
selected: rgb(0x2a63d9),
|
||||
border: rgb(0xd9d9d9),
|
||||
separator: rgb(0xe6e6e6),
|
||||
container: rgb(0xf4f5f5),
|
||||
}
|
||||
}
|
||||
|
||||
+34
-16
@@ -32,9 +32,9 @@
|
||||
//! your own custom layout algorithm or rendering a code editor.
|
||||
|
||||
use crate::{
|
||||
App, ArenaBox, AvailableSpace, Bounds, Context, DispatchNodeId, ELEMENT_ARENA, ElementId,
|
||||
FocusHandle, InspectorElementId, LayoutId, Pixels, Point, Size, Style, Window,
|
||||
util::FluentBuilder,
|
||||
App, ArenaBox, AvailableSpace, Bounds, Context, DispatchNodeId, ElementId, FocusHandle,
|
||||
InspectorElementId, LayoutId, Pixels, Point, SharedString, Size, Style, Window,
|
||||
local_util::FluentBuilder, window::with_element_arena,
|
||||
};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use std::{
|
||||
@@ -197,8 +197,27 @@ impl<C: RenderOnce> Component<C> {
|
||||
}
|
||||
}
|
||||
|
||||
fn prepaint_component(
|
||||
(element, name): &mut (AnyElement, &'static str),
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
window.with_id(ElementId::Name(SharedString::new_static(name)), |window| {
|
||||
element.prepaint(window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn paint_component(
|
||||
(element, name): &mut (AnyElement, &'static str),
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
window.with_id(ElementId::Name(SharedString::new_static(name)), |window| {
|
||||
element.paint(window, cx);
|
||||
})
|
||||
}
|
||||
impl<C: RenderOnce> Element for Component<C> {
|
||||
type RequestLayoutState = AnyElement;
|
||||
type RequestLayoutState = (AnyElement, &'static str);
|
||||
type PrepaintState = ();
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
@@ -220,7 +239,7 @@ impl<C: RenderOnce> Element for Component<C> {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
window.with_global_id(ElementId::Name(type_name::<C>().into()), |_, window| {
|
||||
window.with_id(ElementId::Name(type_name::<C>().into()), |window| {
|
||||
let mut element = self
|
||||
.component
|
||||
.take()
|
||||
@@ -229,7 +248,7 @@ impl<C: RenderOnce> Element for Component<C> {
|
||||
.into_any_element();
|
||||
|
||||
let layout_id = element.request_layout(window, cx);
|
||||
(layout_id, element)
|
||||
(layout_id, (element, type_name::<C>()))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -238,13 +257,11 @@ impl<C: RenderOnce> Element for Component<C> {
|
||||
_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
_: Bounds<Pixels>,
|
||||
element: &mut AnyElement,
|
||||
state: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
window.with_global_id(ElementId::Name(type_name::<C>().into()), |_, window| {
|
||||
element.prepaint(window, cx);
|
||||
})
|
||||
prepaint_component(state, window, cx);
|
||||
}
|
||||
|
||||
fn paint(
|
||||
@@ -252,14 +269,12 @@ impl<C: RenderOnce> Element for Component<C> {
|
||||
_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
_: Bounds<Pixels>,
|
||||
element: &mut Self::RequestLayoutState,
|
||||
state: &mut Self::RequestLayoutState,
|
||||
_: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
window.with_global_id(ElementId::Name(type_name::<C>().into()), |_, window| {
|
||||
element.paint(window, cx);
|
||||
})
|
||||
paint_component(state, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,18 +563,22 @@ where
|
||||
&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>,
|
||||
@@ -579,8 +598,7 @@ impl AnyElement {
|
||||
E: 'static + Element,
|
||||
E::RequestLayoutState: Any,
|
||||
{
|
||||
let element = ELEMENT_ARENA
|
||||
.with_borrow_mut(|arena| arena.alloc(|| Drawable::new(element)))
|
||||
let element = with_element_arena(|arena| arena.alloc(|| Drawable::new(element)))
|
||||
.map(|element| element as &mut dyn ElementObject);
|
||||
AnyElement(element)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use std::{
|
||||
rc::Rc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use crate::scheduler::Instant;
|
||||
use std::{rc::Rc, time::Duration};
|
||||
|
||||
use crate::{
|
||||
AnyElement, App, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, Window,
|
||||
|
||||
@@ -62,7 +62,7 @@ impl Element for Deferred {
|
||||
) {
|
||||
let child = self.child.take().unwrap();
|
||||
let element_offset = window.element_offset();
|
||||
window.defer_draw(child, element_offset, self.priority)
|
||||
window.defer_draw(child, element_offset, self.priority, None)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
|
||||
+426
-52
@@ -15,13 +15,15 @@
|
||||
//! and Tailwind-like styling that you can use to build your own custom elements. Div is
|
||||
//! constructed by combining these two systems into an all-in-one element.
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
use crate::PinchEvent;
|
||||
use crate::{
|
||||
AbsoluteLength, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, App, Bounds, ClickEvent,
|
||||
DispatchPhase, Display, Element, ElementId, Entity, FocusHandle, Global, GlobalElementId,
|
||||
Hitbox, HitboxBehavior, HitboxId, InspectorElementId, IntoElement, IsZero, KeyContext,
|
||||
KeyDownEvent, KeyUpEvent, KeyboardButton, KeyboardClickEvent, LayoutId, ModifiersChangedEvent,
|
||||
MouseButton, MouseClickEvent, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Overflow,
|
||||
ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style,
|
||||
MouseButton, MouseClickEvent, MouseDownEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent,
|
||||
Overflow, ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style,
|
||||
StyleRefinement, Styled, Task, TooltipId, Visibility, Window, WindowControlArea, point, px,
|
||||
size,
|
||||
};
|
||||
@@ -166,6 +168,38 @@ impl Interactivity {
|
||||
}));
|
||||
}
|
||||
|
||||
/// Bind the given callback to the mouse pressure event, during the bubble phase
|
||||
/// the imperative API equivalent to [`InteractiveElement::on_mouse_pressure`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
pub fn on_mouse_pressure(
|
||||
&mut self,
|
||||
listener: impl Fn(&MousePressureEvent, &mut Window, &mut App) + 'static,
|
||||
) {
|
||||
self.mouse_pressure_listeners
|
||||
.push(Box::new(move |event, phase, hitbox, window, cx| {
|
||||
if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
|
||||
(listener)(event, window, cx)
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/// Bind the given callback to the mouse pressure event, during the capture phase
|
||||
/// the imperative API equivalent to [`InteractiveElement::on_mouse_pressure`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
pub fn capture_mouse_pressure(
|
||||
&mut self,
|
||||
listener: impl Fn(&MousePressureEvent, &mut Window, &mut App) + 'static,
|
||||
) {
|
||||
self.mouse_pressure_listeners
|
||||
.push(Box::new(move |event, phase, hitbox, window, cx| {
|
||||
if phase == DispatchPhase::Capture && hitbox.is_hovered(window) {
|
||||
(listener)(event, window, cx)
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/// Bind the given callback to the mouse up event for the given button, during the bubble phase.
|
||||
/// The imperative API equivalent to [`InteractiveElement::on_mouse_up`].
|
||||
///
|
||||
@@ -321,6 +355,43 @@ impl Interactivity {
|
||||
}));
|
||||
}
|
||||
|
||||
/// Bind the given callback to pinch gesture events during the bubble phase.
|
||||
///
|
||||
/// Note: This event is only available on macOS and Wayland (Linux).
|
||||
/// On Windows, pinch gestures are simulated as scroll wheel events with Ctrl held.
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub fn on_pinch(&mut self, listener: impl Fn(&PinchEvent, &mut Window, &mut App) + 'static) {
|
||||
self.pinch_listeners
|
||||
.push(Box::new(move |event, phase, hitbox, window, cx| {
|
||||
if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
|
||||
(listener)(event, window, cx);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/// Bind the given callback to pinch gesture events during the capture phase.
|
||||
///
|
||||
/// Note: This event is only available on macOS and Wayland (Linux).
|
||||
/// On Windows, pinch gestures are simulated as scroll wheel events with Ctrl held.
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub fn capture_pinch(
|
||||
&mut self,
|
||||
listener: impl Fn(&PinchEvent, &mut Window, &mut App) + 'static,
|
||||
) {
|
||||
self.pinch_listeners
|
||||
.push(Box::new(move |event, phase, _hitbox, window, cx| {
|
||||
if phase == DispatchPhase::Capture {
|
||||
(listener)(event, window, cx);
|
||||
} else {
|
||||
cx.propagate();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/// Bind the given callback to an action dispatch during the capture phase.
|
||||
/// The imperative API equivalent to [`InteractiveElement::capture_action`].
|
||||
///
|
||||
@@ -490,6 +561,20 @@ impl Interactivity {
|
||||
}));
|
||||
}
|
||||
|
||||
/// Bind the given callback to non-primary click events of this element.
|
||||
/// The imperative API equivalent to [`StatefulInteractiveElement::on_aux_click`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
pub fn on_aux_click(&mut self, listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.aux_click_listeners
|
||||
.push(Rc::new(move |event, window, cx| {
|
||||
listener(event, window, cx)
|
||||
}));
|
||||
}
|
||||
|
||||
/// On drag initiation, this callback will be used to create a new view to render the dragged value for a
|
||||
/// drag and drop operation. This API should also be used as the equivalent of 'on drag start' with
|
||||
/// the [`Self::on_drag_move`] API.
|
||||
@@ -589,6 +674,16 @@ impl Interactivity {
|
||||
pub fn block_mouse_except_scroll(&mut self) {
|
||||
self.hitbox_behavior = HitboxBehavior::BlockMouseExceptScroll;
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
fn has_pinch_listeners(&self) -> bool {
|
||||
!self.pinch_listeners.is_empty()
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||
fn has_pinch_listeners(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for elements that want to use the standard GPUI event handlers that don't
|
||||
@@ -622,7 +717,7 @@ pub trait InteractiveElement: Sized {
|
||||
/// Set whether this element is a tab stop.
|
||||
///
|
||||
/// When false, the element remains in tab-index order but cannot be reached via keyboard navigation.
|
||||
/// Useful for container elements: focus the container, then call `window.focus_next()` to focus
|
||||
/// Useful for container elements: focus the container, then call `window.focus_next(cx)` to focus
|
||||
/// the first tab stop inside it while having the container element itself be unreachable via the keyboard.
|
||||
/// Should only be used with `tab_index`.
|
||||
fn tab_stop(mut self, tab_stop: bool) -> Self {
|
||||
@@ -769,6 +864,30 @@ pub trait InteractiveElement: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Bind the given callback to the mouse pressure event, during the bubble phase
|
||||
/// the fluent API equivalent to [`Interactivity::on_mouse_pressure`]
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
fn on_mouse_pressure(
|
||||
mut self,
|
||||
listener: impl Fn(&MousePressureEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.interactivity().on_mouse_pressure(listener);
|
||||
self
|
||||
}
|
||||
|
||||
/// Bind the given callback to the mouse pressure event, during the capture phase
|
||||
/// the fluent API equivalent to [`Interactivity::on_mouse_pressure`]
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
fn capture_mouse_pressure(
|
||||
mut self,
|
||||
listener: impl Fn(&MousePressureEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.interactivity().capture_mouse_pressure(listener);
|
||||
self
|
||||
}
|
||||
|
||||
/// Bind the given callback to the mouse down event, on any button, during the capture phase,
|
||||
/// when the mouse is outside of the bounds of this element.
|
||||
/// The fluent API equivalent to [`Interactivity::on_mouse_down_out`].
|
||||
@@ -835,6 +954,34 @@ pub trait InteractiveElement: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Bind the given callback to pinch gesture events during the bubble phase.
|
||||
/// The fluent API equivalent to [`Interactivity::on_pinch`].
|
||||
///
|
||||
/// Note: This event is only available on macOS and Wayland (Linux).
|
||||
/// On Windows, pinch gestures are simulated as scroll wheel events with Ctrl held.
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
fn on_pinch(mut self, listener: impl Fn(&PinchEvent, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.interactivity().on_pinch(listener);
|
||||
self
|
||||
}
|
||||
|
||||
/// Bind the given callback to pinch gesture events during the capture phase.
|
||||
/// The fluent API equivalent to [`Interactivity::capture_pinch`].
|
||||
///
|
||||
/// Note: This event is only available on macOS and Wayland (Linux).
|
||||
/// On Windows, pinch gestures are simulated as scroll wheel events with Ctrl held.
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
fn capture_pinch(
|
||||
mut self,
|
||||
listener: impl Fn(&PinchEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.interactivity().capture_pinch(listener);
|
||||
self
|
||||
}
|
||||
/// Capture the given action, before normal action dispatch can fire.
|
||||
/// The fluent API equivalent to [`Interactivity::capture_action`].
|
||||
///
|
||||
@@ -1134,6 +1281,21 @@ pub trait StatefulInteractiveElement: InteractiveElement {
|
||||
self
|
||||
}
|
||||
|
||||
/// Bind the given callback to non-primary click events of this element.
|
||||
/// The fluent API equivalent to [`Interactivity::on_aux_click`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
fn on_aux_click(
|
||||
mut self,
|
||||
listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.interactivity().on_aux_click(listener);
|
||||
self
|
||||
}
|
||||
|
||||
/// On drag initiation, this callback will be used to create a new view to render the dragged value for a
|
||||
/// drag and drop operation. This API should also be used as the equivalent of 'on drag start' with
|
||||
/// the [`InteractiveElement::on_drag_move`] API.
|
||||
@@ -1197,13 +1359,18 @@ pub(crate) type MouseDownListener =
|
||||
Box<dyn Fn(&MouseDownEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
|
||||
pub(crate) type MouseUpListener =
|
||||
Box<dyn Fn(&MouseUpEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
|
||||
|
||||
pub(crate) type MousePressureListener =
|
||||
Box<dyn Fn(&MousePressureEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
|
||||
pub(crate) type MouseMoveListener =
|
||||
Box<dyn Fn(&MouseMoveEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
|
||||
|
||||
pub(crate) type ScrollWheelListener =
|
||||
Box<dyn Fn(&ScrollWheelEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub(crate) type PinchListener =
|
||||
Box<dyn Fn(&PinchEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
|
||||
|
||||
pub(crate) type ClickListener = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
|
||||
|
||||
pub(crate) type DragListener =
|
||||
@@ -1238,6 +1405,7 @@ pub fn div() -> Div {
|
||||
children: SmallVec::default(),
|
||||
prepaint_listener: None,
|
||||
image_cache: None,
|
||||
prepaint_order_fn: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1247,6 +1415,7 @@ pub struct Div {
|
||||
children: SmallVec<[StackSafe<AnyElement>; 2]>,
|
||||
prepaint_listener: Option<Box<dyn Fn(Vec<Bounds<Pixels>>, &mut Window, &mut App) + 'static>>,
|
||||
image_cache: Option<Box<dyn ImageCacheProvider>>,
|
||||
prepaint_order_fn: Option<Box<dyn Fn(&mut Window, &mut App) -> SmallVec<[usize; 8]>>>,
|
||||
}
|
||||
|
||||
impl Div {
|
||||
@@ -1265,6 +1434,22 @@ impl Div {
|
||||
self.image_cache = Some(Box::new(cache));
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify a function that determines the order in which children are prepainted.
|
||||
///
|
||||
/// The function is called at prepaint time and should return a vector of child indices
|
||||
/// in the desired prepaint order. Each index should appear exactly once.
|
||||
///
|
||||
/// This is useful when the prepaint of one child affects state that another child reads.
|
||||
/// For example, in split editor views, the editor with an autoscroll request should
|
||||
/// be prepainted first so its scroll position update is visible to the other editor.
|
||||
pub fn with_dynamic_prepaint_order(
|
||||
mut self,
|
||||
order_fn: impl Fn(&mut Window, &mut App) -> SmallVec<[usize; 8]> + 'static,
|
||||
) -> Self {
|
||||
self.prepaint_order_fn = Some(Box::new(order_fn));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A frame state for a `Div` element, which contains layout IDs for its children.
|
||||
@@ -1368,6 +1553,11 @@ impl Element for Div {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<Hitbox> {
|
||||
let image_cache = self
|
||||
.image_cache
|
||||
.as_mut()
|
||||
.map(|provider| provider.provide(window, cx));
|
||||
|
||||
let has_prepaint_listener = self.prepaint_listener.is_some();
|
||||
let mut children_bounds = Vec::with_capacity(if has_prepaint_listener {
|
||||
request_layout.child_layout_ids.len()
|
||||
@@ -1422,16 +1612,27 @@ impl Element for Div {
|
||||
return hitbox;
|
||||
}
|
||||
|
||||
window.with_element_offset(scroll_offset, |window| {
|
||||
for child in &mut self.children {
|
||||
child.prepaint(window, cx);
|
||||
window.with_image_cache(image_cache, |window| {
|
||||
window.with_element_offset(scroll_offset, |window| {
|
||||
if let Some(order_fn) = &self.prepaint_order_fn {
|
||||
let order = order_fn(window, cx);
|
||||
for idx in order {
|
||||
if let Some(child) = self.children.get_mut(idx) {
|
||||
child.prepaint(window, cx);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for child in &mut self.children {
|
||||
child.prepaint(window, cx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(listener) = self.prepaint_listener.as_ref() {
|
||||
listener(children_bounds, window, cx);
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(listener) = self.prepaint_listener.as_ref() {
|
||||
listener(children_bounds, window, cx);
|
||||
}
|
||||
|
||||
hitbox
|
||||
},
|
||||
)
|
||||
@@ -1521,8 +1722,11 @@ pub struct Interactivity {
|
||||
pub(crate) group_drag_over_styles: Vec<(TypeId, GroupStyle)>,
|
||||
pub(crate) mouse_down_listeners: Vec<MouseDownListener>,
|
||||
pub(crate) mouse_up_listeners: Vec<MouseUpListener>,
|
||||
pub(crate) mouse_pressure_listeners: Vec<MousePressureListener>,
|
||||
pub(crate) mouse_move_listeners: Vec<MouseMoveListener>,
|
||||
pub(crate) scroll_wheel_listeners: Vec<ScrollWheelListener>,
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub(crate) pinch_listeners: Vec<PinchListener>,
|
||||
pub(crate) key_down_listeners: Vec<KeyDownListener>,
|
||||
pub(crate) key_up_listeners: Vec<KeyUpListener>,
|
||||
pub(crate) modifiers_changed_listeners: Vec<ModifiersChangedListener>,
|
||||
@@ -1530,6 +1734,7 @@ pub struct Interactivity {
|
||||
pub(crate) drop_listeners: Vec<(TypeId, DropListener)>,
|
||||
pub(crate) can_drop_predicate: Option<CanDropPredicate>,
|
||||
pub(crate) click_listeners: Vec<ClickListener>,
|
||||
pub(crate) aux_click_listeners: Vec<ClickListener>,
|
||||
pub(crate) drag_listener: Option<(Arc<dyn Any>, DragListener)>,
|
||||
pub(crate) hover_listener: Option<Box<dyn Fn(&bool, &mut Window, &mut App)>>,
|
||||
pub(crate) tooltip_builder: Option<TooltipBuilder>,
|
||||
@@ -1672,6 +1877,11 @@ impl Interactivity {
|
||||
let clicked_state = clicked_state.borrow();
|
||||
self.active = Some(clicked_state.element);
|
||||
}
|
||||
if self.hover_style.is_some() || self.group_hover_style.is_some() {
|
||||
element_state
|
||||
.hover_state
|
||||
.get_or_insert_with(Default::default);
|
||||
}
|
||||
if let Some(active_tooltip) = element_state.active_tooltip.as_ref() {
|
||||
if self.tooltip_builder.is_some() {
|
||||
self.tooltip_id = set_tooltip_on_window(active_tooltip, window);
|
||||
@@ -1714,10 +1924,13 @@ impl Interactivity {
|
||||
|| self.group_hover_style.is_some()
|
||||
|| self.hover_listener.is_some()
|
||||
|| !self.mouse_up_listeners.is_empty()
|
||||
|| !self.mouse_pressure_listeners.is_empty()
|
||||
|| !self.mouse_down_listeners.is_empty()
|
||||
|| !self.mouse_move_listeners.is_empty()
|
||||
|| !self.click_listeners.is_empty()
|
||||
|| !self.aux_click_listeners.is_empty()
|
||||
|| !self.scroll_wheel_listeners.is_empty()
|
||||
|| self.has_pinch_listeners()
|
||||
|| self.drag_listener.is_some()
|
||||
|| !self.drop_listeners.is_empty()
|
||||
|| self.tooltip_builder.is_some()
|
||||
@@ -1757,18 +1970,18 @@ impl Interactivity {
|
||||
// high for the maximum scroll, we round the scroll max to 2 decimal
|
||||
// places here.
|
||||
let padded_content_size = self.content_size + padding_size;
|
||||
let scroll_max = (padded_content_size - bounds.size)
|
||||
let scroll_max = Point::from(padded_content_size - bounds.size)
|
||||
.map(round_to_two_decimals)
|
||||
.max(&Default::default());
|
||||
// Clamp scroll offset in case scroll max is smaller now (e.g., if children
|
||||
// were removed or the bounds became larger).
|
||||
let mut scroll_offset = scroll_offset.borrow_mut();
|
||||
|
||||
scroll_offset.x = scroll_offset.x.clamp(-scroll_max.width, px(0.));
|
||||
scroll_offset.x = scroll_offset.x.clamp(-scroll_max.x, px(0.));
|
||||
if scroll_to_bottom {
|
||||
scroll_offset.y = -scroll_max.height;
|
||||
scroll_offset.y = -scroll_max.y;
|
||||
} else {
|
||||
scroll_offset.y = scroll_offset.y.clamp(-scroll_max.height, px(0.));
|
||||
scroll_offset.y = scroll_offset.y.clamp(-scroll_max.y, px(0.));
|
||||
}
|
||||
|
||||
if let Some(mut scroll_handle_state) = tracked_scroll_handle {
|
||||
@@ -1912,12 +2125,12 @@ impl Interactivity {
|
||||
) {
|
||||
use crate::{BorderStyle, TextAlign};
|
||||
|
||||
if global_id.is_some()
|
||||
if let Some(global_id) = global_id
|
||||
&& (style.debug || style.debug_below || cx.has_global::<crate::DebugBelow>())
|
||||
&& hitbox.is_hovered(window)
|
||||
{
|
||||
const FONT_SIZE: crate::Pixels = crate::Pixels(10.);
|
||||
let element_id = format!("{:?}", global_id.unwrap());
|
||||
let element_id = format!("{global_id:?}");
|
||||
let str_len = element_id.len();
|
||||
|
||||
let render_debug_text = |window: &mut Window| {
|
||||
@@ -1940,7 +2153,7 @@ impl Interactivity {
|
||||
origin: hitbox.origin,
|
||||
size: text.size(FONT_SIZE),
|
||||
};
|
||||
if self.source_location.is_some()
|
||||
if let Some(source_location) = self.source_location
|
||||
&& text_bounds.contains(&window.mouse_position())
|
||||
&& window.modifiers().secondary()
|
||||
{
|
||||
@@ -1971,7 +2184,6 @@ impl Interactivity {
|
||||
|
||||
window.on_mouse_event({
|
||||
let hitbox = hitbox.clone();
|
||||
let location = self.source_location.unwrap();
|
||||
move |e: &crate::MouseDownEvent, phase, window, cx| {
|
||||
if text_bounds.contains(&e.position)
|
||||
&& phase.capture()
|
||||
@@ -1984,9 +2196,9 @@ impl Interactivity {
|
||||
|
||||
eprintln!(
|
||||
"This element was created at:\n{}:{}:{}",
|
||||
dir.join(location.file()).to_string_lossy(),
|
||||
location.line(),
|
||||
location.column()
|
||||
dir.join(source_location.file()).to_string_lossy(),
|
||||
source_location.line(),
|
||||
source_location.column()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2037,12 +2249,12 @@ impl Interactivity {
|
||||
// This behavior can be suppressed by using `cx.prevent_default()`.
|
||||
if let Some(focus_handle) = self.tracked_focus_handle.clone() {
|
||||
let hitbox = hitbox.clone();
|
||||
window.on_mouse_event(move |_: &MouseDownEvent, phase, window, _| {
|
||||
window.on_mouse_event(move |_: &MouseDownEvent, phase, window, cx| {
|
||||
if phase == DispatchPhase::Bubble
|
||||
&& hitbox.is_hovered(window)
|
||||
&& !window.default_prevented()
|
||||
{
|
||||
window.focus(&focus_handle);
|
||||
window.focus(&focus_handle, cx);
|
||||
// If there is a parent that is also focusable, prevent it
|
||||
// from transferring focus because we already did so.
|
||||
window.prevent_default();
|
||||
@@ -2064,6 +2276,13 @@ impl Interactivity {
|
||||
})
|
||||
}
|
||||
|
||||
for listener in self.mouse_pressure_listeners.drain(..) {
|
||||
let hitbox = hitbox.clone();
|
||||
window.on_mouse_event(move |event: &MousePressureEvent, phase, window, cx| {
|
||||
listener(event, phase, &hitbox, window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
for listener in self.mouse_move_listeners.drain(..) {
|
||||
let hitbox = hitbox.clone();
|
||||
window.on_mouse_event(move |event: &MouseMoveEvent, phase, window, cx| {
|
||||
@@ -2078,25 +2297,70 @@ impl Interactivity {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
for listener in self.pinch_listeners.drain(..) {
|
||||
let hitbox = hitbox.clone();
|
||||
window.on_mouse_event(move |event: &PinchEvent, phase, window, cx| {
|
||||
listener(event, phase, &hitbox, window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
if self.hover_style.is_some()
|
||||
|| self.base_style.mouse_cursor.is_some()
|
||||
|| cx.active_drag.is_some() && !self.drag_over_styles.is_empty()
|
||||
{
|
||||
let hitbox = hitbox.clone();
|
||||
let was_hovered = hitbox.is_hovered(window);
|
||||
let hover_state = self.hover_style.as_ref().and_then(|_| {
|
||||
element_state
|
||||
.as_ref()
|
||||
.and_then(|state| state.hover_state.as_ref())
|
||||
.cloned()
|
||||
});
|
||||
let current_view = window.current_view();
|
||||
|
||||
window.on_mouse_event(move |_: &MouseMoveEvent, phase, window, cx| {
|
||||
let hovered = hitbox.is_hovered(window);
|
||||
let was_hovered = hover_state
|
||||
.as_ref()
|
||||
.is_some_and(|state| state.borrow().element);
|
||||
if phase == DispatchPhase::Capture && hovered != was_hovered {
|
||||
cx.notify(current_view);
|
||||
if let Some(hover_state) = &hover_state {
|
||||
hover_state.borrow_mut().element = hovered;
|
||||
cx.notify(current_view);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(group_hover) = self.group_hover_style.as_ref() {
|
||||
if let Some(group_hitbox_id) = GroupHitboxes::get(&group_hover.group, cx) {
|
||||
let hover_state = element_state
|
||||
.as_ref()
|
||||
.and_then(|element| element.hover_state.as_ref())
|
||||
.cloned();
|
||||
let current_view = window.current_view();
|
||||
|
||||
window.on_mouse_event(move |_: &MouseMoveEvent, phase, window, cx| {
|
||||
let group_hovered = group_hitbox_id.is_hovered(window);
|
||||
let was_group_hovered = hover_state
|
||||
.as_ref()
|
||||
.is_some_and(|state| state.borrow().group);
|
||||
if phase == DispatchPhase::Capture && group_hovered != was_group_hovered {
|
||||
if let Some(hover_state) = &hover_state {
|
||||
hover_state.borrow_mut().group = group_hovered;
|
||||
}
|
||||
cx.notify(current_view);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let drag_cursor_style = self.base_style.as_ref().mouse_cursor;
|
||||
|
||||
let mut drag_listener = mem::take(&mut self.drag_listener);
|
||||
let drop_listeners = mem::take(&mut self.drop_listeners);
|
||||
let click_listeners = mem::take(&mut self.click_listeners);
|
||||
let aux_click_listeners = mem::take(&mut self.aux_click_listeners);
|
||||
let can_drop_predicate = mem::take(&mut self.can_drop_predicate);
|
||||
|
||||
if !drop_listeners.is_empty() {
|
||||
@@ -2133,7 +2397,10 @@ impl Interactivity {
|
||||
}
|
||||
|
||||
if let Some(element_state) = element_state {
|
||||
if !click_listeners.is_empty() || drag_listener.is_some() {
|
||||
if !click_listeners.is_empty()
|
||||
|| !aux_click_listeners.is_empty()
|
||||
|| drag_listener.is_some()
|
||||
{
|
||||
let pending_mouse_down = element_state
|
||||
.pending_mouse_down
|
||||
.get_or_insert_with(Default::default)
|
||||
@@ -2147,9 +2414,10 @@ impl Interactivity {
|
||||
window.on_mouse_event({
|
||||
let pending_mouse_down = pending_mouse_down.clone();
|
||||
let hitbox = hitbox.clone();
|
||||
let has_aux_click_listeners = !aux_click_listeners.is_empty();
|
||||
move |event: &MouseDownEvent, phase, window, _cx| {
|
||||
if phase == DispatchPhase::Bubble
|
||||
&& event.button == MouseButton::Left
|
||||
&& (event.button == MouseButton::Left || has_aux_click_listeners)
|
||||
&& hitbox.is_hovered(window)
|
||||
{
|
||||
*pending_mouse_down.borrow_mut() = Some(event.clone());
|
||||
@@ -2171,6 +2439,7 @@ impl Interactivity {
|
||||
&& !cx.has_active_drag()
|
||||
&& (event.position - mouse_down.position).magnitude() > DRAG_THRESHOLD
|
||||
&& let Some((drag_value, drag_listener)) = drag_listener.take()
|
||||
&& mouse_down.button == MouseButton::Left
|
||||
{
|
||||
*clicked_state.borrow_mut() = ElementClickedState::default();
|
||||
let cursor_offset = event.position - hitbox.origin;
|
||||
@@ -2247,12 +2516,24 @@ impl Interactivity {
|
||||
// Fire click handlers during the bubble phase.
|
||||
DispatchPhase::Bubble => {
|
||||
if let Some(mouse_down) = captured_mouse_down.take() {
|
||||
let btn = mouse_down.button;
|
||||
|
||||
let mouse_click = ClickEvent::Mouse(MouseClickEvent {
|
||||
down: mouse_down,
|
||||
up: event.clone(),
|
||||
});
|
||||
for listener in &click_listeners {
|
||||
listener(&mouse_click, window, cx);
|
||||
|
||||
match btn {
|
||||
MouseButton::Left => {
|
||||
for listener in &click_listeners {
|
||||
listener(&mouse_click, window, cx);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
for listener in &aux_click_listeners {
|
||||
listener(&mouse_click, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2263,7 +2544,7 @@ impl Interactivity {
|
||||
if let Some(hover_listener) = self.hover_listener.take() {
|
||||
let hitbox = hitbox.clone();
|
||||
let was_hovered = element_state
|
||||
.hover_state
|
||||
.hover_listener_state
|
||||
.get_or_insert_with(Default::default)
|
||||
.clone();
|
||||
let has_mouse_down = element_state
|
||||
@@ -2308,7 +2589,8 @@ impl Interactivity {
|
||||
let pending_mouse_down = pending_mouse_down.clone();
|
||||
let source_bounds = hitbox.bounds;
|
||||
move |window: &Window| {
|
||||
pending_mouse_down.borrow().is_none()
|
||||
!window.last_input_was_keyboard()
|
||||
&& pending_mouse_down.borrow().is_none()
|
||||
&& source_bounds.contains(&window.mouse_position())
|
||||
}
|
||||
});
|
||||
@@ -2328,18 +2610,24 @@ impl Interactivity {
|
||||
);
|
||||
}
|
||||
|
||||
// We unconditionally bind both the mouse up and mouse down active state handlers
|
||||
// Because we might not get a chance to render a frame before the mouse up event arrives.
|
||||
let active_state = element_state
|
||||
.clicked_state
|
||||
.get_or_insert_with(Default::default)
|
||||
.clone();
|
||||
if active_state.borrow().is_clicked() {
|
||||
|
||||
{
|
||||
let active_state = active_state.clone();
|
||||
window.on_mouse_event(move |_: &MouseUpEvent, phase, window, _cx| {
|
||||
if phase == DispatchPhase::Capture {
|
||||
if phase == DispatchPhase::Capture && active_state.borrow().is_clicked() {
|
||||
*active_state.borrow_mut() = ElementClickedState::default();
|
||||
window.refresh();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
}
|
||||
|
||||
{
|
||||
let active_group_hitbox = self
|
||||
.group_active_style
|
||||
.as_ref()
|
||||
@@ -2514,22 +2802,46 @@ impl Interactivity {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(hitbox) = hitbox {
|
||||
if !cx.has_active_drag() {
|
||||
if let Some(group_hover) = self.group_hover_style.as_ref()
|
||||
&& let Some(group_hitbox_id) = GroupHitboxes::get(&group_hover.group, cx)
|
||||
&& group_hitbox_id.is_hovered(window)
|
||||
{
|
||||
style.refine(&group_hover.style);
|
||||
}
|
||||
if !cx.has_active_drag() {
|
||||
if let Some(group_hover) = self.group_hover_style.as_ref() {
|
||||
let is_group_hovered =
|
||||
if let Some(group_hitbox_id) = GroupHitboxes::get(&group_hover.group, cx) {
|
||||
group_hitbox_id.is_hovered(window)
|
||||
} else if let Some(element_state) = element_state.as_ref() {
|
||||
element_state
|
||||
.hover_state
|
||||
.as_ref()
|
||||
.map(|state| state.borrow().group)
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if let Some(hover_style) = self.hover_style.as_ref()
|
||||
&& hitbox.is_hovered(window)
|
||||
{
|
||||
style.refine(hover_style);
|
||||
if is_group_hovered {
|
||||
style.refine(&group_hover.style);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(hover_style) = self.hover_style.as_ref() {
|
||||
let is_hovered = if let Some(hitbox) = hitbox {
|
||||
hitbox.is_hovered(window)
|
||||
} else if let Some(element_state) = element_state.as_ref() {
|
||||
element_state
|
||||
.hover_state
|
||||
.as_ref()
|
||||
.map(|state| state.borrow().element)
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if is_hovered {
|
||||
style.refine(hover_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(hitbox) = hitbox {
|
||||
if let Some(drag) = cx.active_drag.take() {
|
||||
let mut can_drop = true;
|
||||
if let Some(can_drop_predicate) = &self.can_drop_predicate {
|
||||
@@ -2588,7 +2900,8 @@ impl Interactivity {
|
||||
pub struct InteractiveElementState {
|
||||
pub(crate) focus_handle: Option<FocusHandle>,
|
||||
pub(crate) clicked_state: Option<Rc<RefCell<ElementClickedState>>>,
|
||||
pub(crate) hover_state: Option<Rc<RefCell<bool>>>,
|
||||
pub(crate) hover_state: Option<Rc<RefCell<ElementHoverState>>>,
|
||||
pub(crate) hover_listener_state: Option<Rc<RefCell<bool>>>,
|
||||
pub(crate) pending_mouse_down: Option<Rc<RefCell<Option<MouseDownEvent>>>>,
|
||||
pub(crate) scroll_offset: Option<Rc<RefCell<Point<Pixels>>>>,
|
||||
pub(crate) active_tooltip: Option<Rc<RefCell<Option<ActiveTooltip>>>>,
|
||||
@@ -2610,6 +2923,16 @@ impl ElementClickedState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether or not the element or a group that contains it is hovered.
|
||||
#[derive(Copy, Clone, Default, Eq, PartialEq)]
|
||||
pub struct ElementHoverState {
|
||||
/// True if this element's group is hovered, false otherwise
|
||||
pub group: bool,
|
||||
|
||||
/// True if this element is hovered, false otherwise
|
||||
pub element: bool,
|
||||
}
|
||||
|
||||
pub(crate) enum ActiveTooltip {
|
||||
/// Currently delaying before showing the tooltip.
|
||||
WaitingForShow { _task: Task<()> },
|
||||
@@ -3061,7 +3384,7 @@ impl ScrollAnchor {
|
||||
struct ScrollHandleState {
|
||||
offset: Rc<RefCell<Point<Pixels>>>,
|
||||
bounds: Bounds<Pixels>,
|
||||
max_offset: Size<Pixels>,
|
||||
max_offset: Point<Pixels>,
|
||||
child_bounds: Vec<Bounds<Pixels>>,
|
||||
scroll_to_bottom: bool,
|
||||
overflow: Point<Overflow>,
|
||||
@@ -3105,7 +3428,7 @@ impl ScrollHandle {
|
||||
}
|
||||
|
||||
/// Get the maximum scroll offset.
|
||||
pub fn max_offset(&self) -> Size<Pixels> {
|
||||
pub fn max_offset(&self) -> Point<Pixels> {
|
||||
self.0.borrow().max_offset
|
||||
}
|
||||
|
||||
@@ -3193,7 +3516,11 @@ impl ScrollHandle {
|
||||
match active_item.strategy {
|
||||
ScrollStrategy::FirstVisible => {
|
||||
if state.overflow.y == Overflow::Scroll {
|
||||
if bounds.top() + scroll_offset.y < state.bounds.top() {
|
||||
let child_height = bounds.size.height;
|
||||
let viewport_height = state.bounds.size.height;
|
||||
if child_height > viewport_height {
|
||||
scroll_offset.y = state.bounds.top() - bounds.top();
|
||||
} else if bounds.top() + scroll_offset.y < state.bounds.top() {
|
||||
scroll_offset.y = state.bounds.top() - bounds.top();
|
||||
} else if bounds.bottom() + scroll_offset.y > state.bounds.bottom() {
|
||||
scroll_offset.y = state.bounds.bottom() - bounds.bottom();
|
||||
@@ -3206,7 +3533,11 @@ impl ScrollHandle {
|
||||
}
|
||||
|
||||
if state.overflow.x == Overflow::Scroll {
|
||||
if bounds.left() + scroll_offset.x < state.bounds.left() {
|
||||
let child_width = bounds.size.width;
|
||||
let viewport_width = state.bounds.size.width;
|
||||
if child_width > viewport_width {
|
||||
scroll_offset.x = state.bounds.left() - bounds.left();
|
||||
} else if bounds.left() + scroll_offset.x < state.bounds.left() {
|
||||
scroll_offset.x = state.bounds.left() - bounds.left();
|
||||
} else if bounds.right() + scroll_offset.x > state.bounds.right() {
|
||||
scroll_offset.x = state.bounds.right() - bounds.right();
|
||||
@@ -3268,3 +3599,46 @@ impl ScrollHandle {
|
||||
self.0.borrow().child_bounds.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn scroll_handle_aligns_wide_children_to_left_edge() {
|
||||
let handle = ScrollHandle::new();
|
||||
{
|
||||
let mut state = handle.0.borrow_mut();
|
||||
state.bounds = Bounds::new(point(px(0.), px(0.)), size(px(80.), px(20.)));
|
||||
state.child_bounds = vec![Bounds::new(point(px(25.), px(0.)), size(px(200.), px(20.)))];
|
||||
state.overflow.x = Overflow::Scroll;
|
||||
state.active_item = Some(ScrollActiveItem {
|
||||
index: 0,
|
||||
strategy: ScrollStrategy::default(),
|
||||
});
|
||||
}
|
||||
|
||||
handle.scroll_to_active_item();
|
||||
|
||||
assert_eq!(handle.offset().x, px(-25.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scroll_handle_aligns_tall_children_to_top_edge() {
|
||||
let handle = ScrollHandle::new();
|
||||
{
|
||||
let mut state = handle.0.borrow_mut();
|
||||
state.bounds = Bounds::new(point(px(0.), px(0.)), size(px(20.), px(80.)));
|
||||
state.child_bounds = vec![Bounds::new(point(px(0.), px(25.)), size(px(20.), px(200.)))];
|
||||
state.overflow.y = Overflow::Scroll;
|
||||
state.active_item = Some(ScrollActiveItem {
|
||||
index: 0,
|
||||
strategy: ScrollStrategy::default(),
|
||||
});
|
||||
}
|
||||
|
||||
handle.scroll_to_active_item();
|
||||
|
||||
assert_eq!(handle.offset().y, px(-25.));
|
||||
}
|
||||
}
|
||||
|
||||
+8
-4
@@ -4,9 +4,10 @@ use crate::{
|
||||
Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource,
|
||||
SharedString, SharedUri, StyleRefinement, Styled, Task, Window, px,
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::Result;
|
||||
|
||||
use futures::{AsyncReadExt, Future};
|
||||
use crate::scheduler::Instant;
|
||||
use futures::Future;
|
||||
use image::{
|
||||
AnimationDecoder, DynamicImage, Frame, ImageError, ImageFormat, Rgba,
|
||||
codecs::{gif::GifDecoder, webp::WebPDecoder},
|
||||
@@ -19,7 +20,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
time::Duration,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use util::ResultExt;
|
||||
@@ -49,7 +50,7 @@ pub enum ImageSource {
|
||||
}
|
||||
|
||||
fn is_uri(uri: &str) -> bool {
|
||||
http_client::Uri::from_str(uri).is_ok()
|
||||
url::Url::from_str(uri).is_ok()
|
||||
}
|
||||
|
||||
impl From<SharedUri> for ImageSource {
|
||||
@@ -602,6 +603,9 @@ impl Asset for ImageAssetLoader {
|
||||
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
|
||||
|
||||
+185
-20
@@ -71,6 +71,16 @@ struct StateInner {
|
||||
scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut Window, &mut App)>>,
|
||||
scrollbar_drag_start_height: Option<Pixels>,
|
||||
measuring_behavior: ListMeasuringBehavior,
|
||||
pending_scroll: Option<PendingScrollFraction>,
|
||||
}
|
||||
|
||||
/// Keeps track of a fractional scroll position within an item for restoration
|
||||
/// after remeasurement.
|
||||
struct PendingScrollFraction {
|
||||
/// The index of the item to scroll within.
|
||||
item_ix: usize,
|
||||
/// Fractional offset (0.0 to 1.0) within the item's height.
|
||||
fraction: f32,
|
||||
}
|
||||
|
||||
/// Whether the list is scrolling from top to bottom or bottom to top.
|
||||
@@ -225,6 +235,7 @@ impl ListState {
|
||||
reset: false,
|
||||
scrollbar_drag_start_height: None,
|
||||
measuring_behavior: ListMeasuringBehavior::default(),
|
||||
pending_scroll: None,
|
||||
})));
|
||||
this.splice(0..0, item_count);
|
||||
this
|
||||
@@ -254,6 +265,45 @@ impl ListState {
|
||||
self.splice(0..old_count, element_count);
|
||||
}
|
||||
|
||||
/// Remeasure all items while preserving proportional scroll position.
|
||||
///
|
||||
/// Use this when item heights may have changed (e.g., font size changes)
|
||||
/// but the number and identity of items remains the same.
|
||||
pub fn remeasure(&self) {
|
||||
let state = &mut *self.0.borrow_mut();
|
||||
|
||||
let new_items = state.items.iter().map(|item| ListItem::Unmeasured {
|
||||
focus_handle: item.focus_handle(),
|
||||
});
|
||||
|
||||
// If there's a `logical_scroll_top`, we need to keep track of it as a
|
||||
// `PendingScrollFraction`, so we can later preserve that scroll
|
||||
// position proportionally to the item, in case the item's height
|
||||
// changes.
|
||||
if let Some(scroll_top) = state.logical_scroll_top {
|
||||
let mut cursor = state.items.cursor::<Count>(());
|
||||
cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
|
||||
|
||||
if let Some(item) = cursor.item() {
|
||||
if let Some(size) = item.size() {
|
||||
let fraction = if size.height.0 > 0.0 {
|
||||
(scroll_top.offset_in_item.0 / size.height.0).clamp(0.0, 1.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
state.pending_scroll = Some(PendingScrollFraction {
|
||||
item_ix: scroll_top.item_ix,
|
||||
fraction,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.items = SumTree::from_iter(new_items, ());
|
||||
state.measuring_behavior.reset();
|
||||
}
|
||||
|
||||
/// The number of items in this list.
|
||||
pub fn item_count(&self) -> usize {
|
||||
self.0.borrow().items.summary().count
|
||||
@@ -441,7 +491,7 @@ impl ListState {
|
||||
|
||||
/// Returns the maximum scroll offset according to the items we have measured.
|
||||
/// This value remains constant while dragging to prevent the scrollbar from moving away unexpectedly.
|
||||
pub fn max_offset_for_scrollbar(&self) -> Size<Pixels> {
|
||||
pub fn max_offset_for_scrollbar(&self) -> Point<Pixels> {
|
||||
let state = self.0.borrow();
|
||||
let bounds = state.last_layout_bounds.unwrap_or_default();
|
||||
|
||||
@@ -449,7 +499,7 @@ impl ListState {
|
||||
.scrollbar_drag_start_height
|
||||
.unwrap_or_else(|| state.items.summary().height);
|
||||
|
||||
Size::new(Pixels::ZERO, Pixels::ZERO.max(height - bounds.size.height))
|
||||
point(Pixels::ZERO, Pixels::ZERO.max(height - bounds.size.height))
|
||||
}
|
||||
|
||||
/// Returns the current scroll offset adjusted for the scrollbar
|
||||
@@ -476,8 +526,12 @@ impl ListState {
|
||||
}
|
||||
|
||||
impl StateInner {
|
||||
fn visible_range(&self, height: Pixels, scroll_top: &ListOffset) -> Range<usize> {
|
||||
let mut cursor = self.items.cursor::<ListItemSummary>(());
|
||||
fn visible_range(
|
||||
items: &SumTree<ListItem>,
|
||||
height: Pixels,
|
||||
scroll_top: &ListOffset,
|
||||
) -> Range<usize> {
|
||||
let mut cursor = items.cursor::<ListItemSummary>(());
|
||||
cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
|
||||
let start_y = cursor.start().height + scroll_top.offset_in_item;
|
||||
cursor.seek_forward(&Height(start_y + height), Bias::Left);
|
||||
@@ -520,9 +574,9 @@ impl StateInner {
|
||||
});
|
||||
}
|
||||
|
||||
if self.scroll_handler.is_some() {
|
||||
let visible_range = self.visible_range(height, scroll_top);
|
||||
self.scroll_handler.as_mut().unwrap()(
|
||||
if let Some(handler) = self.scroll_handler.as_mut() {
|
||||
let visible_range = Self::visible_range(&self.items, height, scroll_top);
|
||||
handler(
|
||||
&ListScrollEvent {
|
||||
visible_range,
|
||||
count: self.items.summary().count,
|
||||
@@ -644,6 +698,20 @@ impl StateInner {
|
||||
let mut element = render_item(item_index, window, cx);
|
||||
let element_size = element.layout_as_root(available_item_space, window, cx);
|
||||
size = Some(element_size);
|
||||
|
||||
// If there's a pending scroll adjustment for the scroll-top
|
||||
// item, apply it, ensuring proportional scroll position is
|
||||
// maintained after re-measuring.
|
||||
if ix == 0 {
|
||||
if let Some(pending_scroll) = self.pending_scroll.take() {
|
||||
if pending_scroll.item_ix == scroll_top.item_ix {
|
||||
scroll_top.offset_in_item =
|
||||
Pixels(pending_scroll.fraction * element_size.height.0);
|
||||
self.logical_scroll_top = Some(scroll_top);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if visible_height < available_height {
|
||||
item_layouts.push_back(ItemLayout {
|
||||
index: item_index,
|
||||
@@ -1035,6 +1103,7 @@ impl Element for List {
|
||||
);
|
||||
|
||||
state.items = new_items;
|
||||
state.measuring_behavior.reset();
|
||||
}
|
||||
|
||||
let padding = style
|
||||
@@ -1184,16 +1253,16 @@ impl sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Height {
|
||||
mod test {
|
||||
|
||||
use gpui::{ScrollDelta, ScrollWheelEvent};
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{self as gpui, TestAppContext};
|
||||
use crate::{
|
||||
self as gpui, AppContext, Context, Element, IntoElement, ListState, Render, Styled,
|
||||
TestAppContext, Window, div, list, point, px, size,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
fn test_reset_after_paint_before_scroll(cx: &mut TestAppContext) {
|
||||
use crate::{
|
||||
AppContext, Context, Element, IntoElement, ListState, Render, Styled, Window, div,
|
||||
list, point, px, size,
|
||||
};
|
||||
|
||||
let cx = cx.add_empty_window();
|
||||
|
||||
let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
|
||||
@@ -1217,7 +1286,7 @@ mod test {
|
||||
|
||||
// Paint
|
||||
cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
|
||||
cx.new(|_| TestView(state.clone()))
|
||||
cx.new(|_| TestView(state.clone())).into_any_element()
|
||||
});
|
||||
|
||||
// Reset
|
||||
@@ -1237,11 +1306,6 @@ mod test {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_scroll_by_positive_and_negative_distance(cx: &mut TestAppContext) {
|
||||
use crate::{
|
||||
AppContext, Context, Element, IntoElement, ListState, Render, Styled, Window, div,
|
||||
list, point, px, size,
|
||||
};
|
||||
|
||||
let cx = cx.add_empty_window();
|
||||
|
||||
let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
|
||||
@@ -1259,7 +1323,7 @@ mod test {
|
||||
|
||||
// Paint
|
||||
cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| {
|
||||
cx.new(|_| TestView(state.clone()))
|
||||
cx.new(|_| TestView(state.clone())).into_any_element()
|
||||
});
|
||||
|
||||
// Test positive distance: start at item 1, move down 30px
|
||||
@@ -1284,4 +1348,105 @@ mod test {
|
||||
assert_eq!(offset.item_ix, 0);
|
||||
assert_eq!(offset.offset_in_item, px(0.));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_measure_all_after_width_change(cx: &mut TestAppContext) {
|
||||
let cx = cx.add_empty_window();
|
||||
|
||||
let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
|
||||
|
||||
struct TestView(ListState);
|
||||
impl Render for TestView {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
list(self.0.clone(), |_, _, _| {
|
||||
div().h(px(50.)).w_full().into_any()
|
||||
})
|
||||
.w_full()
|
||||
.h_full()
|
||||
}
|
||||
}
|
||||
|
||||
let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
|
||||
|
||||
// First draw at width 100: all 10 items measured (total 500px).
|
||||
// Viewport is 200px, so max scroll offset should be 300px.
|
||||
cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
|
||||
view.clone().into_any_element()
|
||||
});
|
||||
assert_eq!(state.max_offset_for_scrollbar().y, px(300.));
|
||||
|
||||
// Second draw at a different width: items get invalidated.
|
||||
// Without the fix, max_offset would drop because unmeasured items
|
||||
// contribute 0 height.
|
||||
cx.draw(point(px(0.), px(0.)), size(px(200.), px(200.)), |_, _| {
|
||||
view.into_any_element()
|
||||
});
|
||||
assert_eq!(state.max_offset_for_scrollbar().y, px(300.));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_remeasure(cx: &mut TestAppContext) {
|
||||
let cx = cx.add_empty_window();
|
||||
|
||||
// Create a list with 10 items, each 100px tall. We'll keep a reference
|
||||
// to the item height so we can later change the height and assert how
|
||||
// `ListState` handles it.
|
||||
let item_height = Rc::new(Cell::new(100usize));
|
||||
let state = ListState::new(10, crate::ListAlignment::Top, px(10.));
|
||||
|
||||
struct TestView {
|
||||
state: ListState,
|
||||
item_height: Rc<Cell<usize>>,
|
||||
}
|
||||
|
||||
impl Render for TestView {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
let height = self.item_height.get();
|
||||
list(self.state.clone(), move |_, _, _| {
|
||||
div().h(px(height as f32)).w_full().into_any()
|
||||
})
|
||||
.w_full()
|
||||
.h_full()
|
||||
}
|
||||
}
|
||||
|
||||
let state_clone = state.clone();
|
||||
let item_height_clone = item_height.clone();
|
||||
let view = cx.update(|_, cx| {
|
||||
cx.new(|_| TestView {
|
||||
state: state_clone,
|
||||
item_height: item_height_clone,
|
||||
})
|
||||
});
|
||||
|
||||
// Simulate scrolling 40px inside the element with index 2. Since the
|
||||
// original item height is 100px, this equates to 40% inside the item.
|
||||
state.scroll_to(gpui::ListOffset {
|
||||
item_ix: 2,
|
||||
offset_in_item: px(40.),
|
||||
});
|
||||
|
||||
cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
|
||||
view.clone().into_any_element()
|
||||
});
|
||||
|
||||
let offset = state.logical_scroll_top();
|
||||
assert_eq!(offset.item_ix, 2);
|
||||
assert_eq!(offset.offset_in_item, px(40.));
|
||||
|
||||
// Update the `item_height` to be 50px instead of 100px so we can assert
|
||||
// that the scroll position is proportionally preserved, that is,
|
||||
// instead of 40px from the top of item 2, it should be 20px, since the
|
||||
// item's height has been halved.
|
||||
item_height.set(50);
|
||||
state.remeasure();
|
||||
|
||||
cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
|
||||
view.into_any_element()
|
||||
});
|
||||
|
||||
let offset = state.logical_scroll_top();
|
||||
assert_eq!(offset.item_ix, 2);
|
||||
assert_eq!(offset.offset_in_item, px(20.));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ pub struct Surface {
|
||||
}
|
||||
|
||||
/// Create a new surface element.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn surface(source: impl Into<SurfaceSource>) -> Surface {
|
||||
Surface {
|
||||
source: source.into(),
|
||||
|
||||
+2
-3
@@ -3,8 +3,7 @@ use std::{fs, 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, geometry::Negate as _, point, px,
|
||||
radians, size,
|
||||
StyleRefinement, Styled, TransformationMatrix, Window, point, px, radians, size,
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
@@ -254,7 +253,7 @@ impl Transformation {
|
||||
.translate(center.scale(scale_factor) + self.translate.scale(scale_factor))
|
||||
.rotate(self.rotate)
|
||||
.scale(self.scale)
|
||||
.translate(center.scale(scale_factor).negate())
|
||||
.translate(center.scale(-scale_factor))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+55
-16
@@ -2,10 +2,11 @@ use crate::{
|
||||
ActiveTooltip, AnyView, App, Bounds, DispatchPhase, Element, ElementId, GlobalElementId,
|
||||
HighlightStyle, Hitbox, HitboxBehavior, InspectorElementId, IntoElement, LayoutId,
|
||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextOverflow,
|
||||
TextRun, TextStyle, TooltipId, WhiteSpace, Window, WrappedLine, WrappedLineLayout,
|
||||
register_tooltip_mouse_handlers, set_tooltip_on_window,
|
||||
TextRun, TextStyle, TooltipId, TruncateFrom, WhiteSpace, Window, WrappedLine,
|
||||
WrappedLineLayout, register_tooltip_mouse_handlers, set_tooltip_on_window,
|
||||
};
|
||||
use anyhow::Context as _;
|
||||
use itertools::Itertools;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
@@ -83,6 +84,14 @@ impl IntoElement for String {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for Cow<'static, str> {
|
||||
type Element = SharedString;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for SharedString {
|
||||
type RequestLayoutState = TextLayout;
|
||||
type PrepaintState = ();
|
||||
@@ -237,7 +246,12 @@ impl StyledText {
|
||||
pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
|
||||
let mut text = &**self.text;
|
||||
for run in &runs {
|
||||
text = text.get(run.len..).expect("invalid text run");
|
||||
text = text.get(run.len..).unwrap_or_else(|| {
|
||||
#[cfg(debug_assertions)]
|
||||
panic!("invalid text run. Text: '{text}', run: {run:?}");
|
||||
#[cfg(not(debug_assertions))]
|
||||
panic!("invalid text run");
|
||||
});
|
||||
}
|
||||
assert!(text.is_empty(), "invalid text run");
|
||||
self.runs = Some(runs);
|
||||
@@ -353,7 +367,7 @@ impl TextLayout {
|
||||
None
|
||||
};
|
||||
|
||||
let (truncate_width, truncation_suffix) =
|
||||
let (truncate_width, truncation_affix, truncate_from) =
|
||||
if let Some(text_overflow) = text_style.text_overflow.clone() {
|
||||
let width = known_dimensions.width.or(match available_space.width {
|
||||
crate::AvailableSpace::Definite(x) => match text_style.line_clamp {
|
||||
@@ -364,17 +378,24 @@ impl TextLayout {
|
||||
});
|
||||
|
||||
match text_overflow {
|
||||
TextOverflow::Truncate(s) => (width, s),
|
||||
TextOverflow::Truncate(s) => (width, s, TruncateFrom::End),
|
||||
TextOverflow::TruncateStart(s) => (width, s, TruncateFrom::Start),
|
||||
}
|
||||
} else {
|
||||
(None, "".into())
|
||||
(None, "".into(), TruncateFrom::End)
|
||||
};
|
||||
|
||||
// Only use cached layout if:
|
||||
// 1. We have a cached size
|
||||
// 2. wrap_width matches (or both are None)
|
||||
// 3. truncate_width is None (if truncate_width is Some, we need to re-layout
|
||||
// because the previous layout may have been computed without truncation)
|
||||
if let Some(text_layout) = element_state.0.borrow().as_ref()
|
||||
&& text_layout.size.is_some()
|
||||
&& let Some(size) = text_layout.size
|
||||
&& (wrap_width.is_none() || wrap_width == text_layout.wrap_width)
|
||||
&& truncate_width.is_none()
|
||||
{
|
||||
return text_layout.size.unwrap();
|
||||
return size;
|
||||
}
|
||||
|
||||
let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size);
|
||||
@@ -382,8 +403,9 @@ impl TextLayout {
|
||||
line_wrapper.truncate_line(
|
||||
text.clone(),
|
||||
truncate_width,
|
||||
&truncation_suffix,
|
||||
&truncation_affix,
|
||||
&runs,
|
||||
truncate_from,
|
||||
)
|
||||
} else {
|
||||
(text.clone(), Cow::Borrowed(&*runs))
|
||||
@@ -597,14 +619,14 @@ impl TextLayout {
|
||||
.unwrap()
|
||||
.lines
|
||||
.iter()
|
||||
.map(|s| s.text.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.map(|s| &s.text)
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
/// The text for this layout (with soft-wraps as newlines)
|
||||
pub fn wrapped_text(&self) -> String {
|
||||
let mut lines = Vec::new();
|
||||
let mut accumulator = String::new();
|
||||
|
||||
for wrapped in self.0.borrow().as_ref().unwrap().lines.iter() {
|
||||
let mut seen = 0;
|
||||
for boundary in wrapped.layout.wrap_boundaries.iter() {
|
||||
@@ -612,13 +634,16 @@ impl TextLayout {
|
||||
[boundary.glyph_ix]
|
||||
.index;
|
||||
|
||||
lines.push(wrapped.text[seen..index].to_string());
|
||||
accumulator.push_str(&wrapped.text[seen..index]);
|
||||
accumulator.push('\n');
|
||||
seen = index;
|
||||
}
|
||||
lines.push(wrapped.text[seen..].to_string());
|
||||
accumulator.push_str(&wrapped.text[seen..]);
|
||||
accumulator.push('\n');
|
||||
}
|
||||
|
||||
lines.join("\n")
|
||||
// Remove trailing newline
|
||||
accumulator.pop();
|
||||
accumulator
|
||||
}
|
||||
}
|
||||
|
||||
@@ -912,3 +937,17 @@ impl IntoElement for InteractiveText {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_into_element_for() {
|
||||
use crate::{ParentElement as _, SharedString, div};
|
||||
use std::borrow::Cow;
|
||||
|
||||
let _ = div().child("static str");
|
||||
let _ = div().child("String".to_string());
|
||||
let _ = div().child(Cow::Borrowed("Cow"));
|
||||
let _ = div().child(SharedString::from("SharedString"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,8 +712,8 @@ mod test {
|
||||
#[gpui::test]
|
||||
fn test_scroll_strategy_nearest(cx: &mut TestAppContext) {
|
||||
use crate::{
|
||||
Context, FocusHandle, ScrollStrategy, UniformListScrollHandle, Window, actions, div,
|
||||
prelude::*, px, uniform_list,
|
||||
Context, FocusHandle, ScrollStrategy, UniformListScrollHandle, Window, div, prelude::*,
|
||||
px, uniform_list,
|
||||
};
|
||||
use std::ops::Range;
|
||||
|
||||
@@ -788,7 +788,7 @@ mod test {
|
||||
|
||||
let (view, cx) = cx.add_window_view(|window, cx| {
|
||||
let focus_handle = cx.focus_handle();
|
||||
window.focus(&focus_handle);
|
||||
window.focus(&focus_handle, cx);
|
||||
TestView {
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
index: 0,
|
||||
|
||||
+282
-526
@@ -1,98 +1,35 @@
|
||||
use crate::{App, PlatformDispatcher, RunnableMeta, RunnableVariant, TaskTiming, profiler};
|
||||
use async_task::Runnable;
|
||||
use crate::scheduler::Instant;
|
||||
use crate::scheduler::Scheduler;
|
||||
use crate::{App, PlatformDispatcher, PlatformScheduler};
|
||||
use futures::channel::mpsc;
|
||||
use parking_lot::{Condvar, Mutex};
|
||||
use smol::prelude::*;
|
||||
use futures::prelude::*;
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
marker::PhantomData,
|
||||
mem::{self, ManuallyDrop},
|
||||
num::NonZeroUsize,
|
||||
panic::Location,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
},
|
||||
task::{Context, Poll},
|
||||
thread::{self, ThreadId},
|
||||
time::{Duration, Instant},
|
||||
fmt::Debug, future::Future, marker::PhantomData, mem, pin::Pin, rc::Rc, sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use util::TryFutureExt;
|
||||
use waker_fn::waker_fn;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use rand::rngs::StdRng;
|
||||
pub use crate::scheduler::{
|
||||
FallibleTask, ForegroundExecutor as SchedulerForegroundExecutor, Priority,
|
||||
};
|
||||
|
||||
/// A pointer to the executor that is currently running,
|
||||
/// for spawning background tasks.
|
||||
#[derive(Clone)]
|
||||
pub struct BackgroundExecutor {
|
||||
#[doc(hidden)]
|
||||
pub dispatcher: Arc<dyn PlatformDispatcher>,
|
||||
inner: crate::scheduler::BackgroundExecutor,
|
||||
dispatcher: Arc<dyn PlatformDispatcher>,
|
||||
}
|
||||
|
||||
/// A pointer to the executor that is currently running,
|
||||
/// for spawning tasks on the main thread.
|
||||
///
|
||||
/// This is intentionally `!Send` via the `not_send` marker field. This is because
|
||||
/// `ForegroundExecutor::spawn` does not require `Send` but checks at runtime that the future is
|
||||
/// only polled from the same thread it was spawned from. These checks would fail when spawning
|
||||
/// foreground tasks from background threads.
|
||||
#[derive(Clone)]
|
||||
pub struct ForegroundExecutor {
|
||||
#[doc(hidden)]
|
||||
pub dispatcher: Arc<dyn PlatformDispatcher>,
|
||||
inner: crate::scheduler::ForegroundExecutor,
|
||||
dispatcher: Arc<dyn PlatformDispatcher>,
|
||||
not_send: PhantomData<Rc<()>>,
|
||||
}
|
||||
|
||||
/// Realtime task priority
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum RealtimePriority {
|
||||
/// Audio task
|
||||
Audio,
|
||||
/// Other realtime task
|
||||
#[default]
|
||||
Other,
|
||||
}
|
||||
|
||||
/// Task priority
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum Priority {
|
||||
/// Realtime priority
|
||||
///
|
||||
/// Spawning a task with this priority will spin it off on a separate thread dedicated just to that task.
|
||||
Realtime(RealtimePriority),
|
||||
/// High priority
|
||||
///
|
||||
/// Only use for tasks that are critical to the user experience / responsiveness of the editor.
|
||||
High,
|
||||
/// Medium priority, probably suits most of your use cases.
|
||||
#[default]
|
||||
Medium,
|
||||
/// Low priority
|
||||
///
|
||||
/// Prioritize this for background work that can come in large quantities
|
||||
/// to not starve the executor of resources for high priority tasks
|
||||
Low,
|
||||
}
|
||||
|
||||
impl Priority {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) const fn probability(&self) -> u32 {
|
||||
match self {
|
||||
// realtime priorities are not considered for probability scheduling
|
||||
Priority::Realtime(_) => 0,
|
||||
Priority::High => 60,
|
||||
Priority::Medium => 30,
|
||||
Priority::Low => 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Task is a primitive that allows work to happen in the background.
|
||||
///
|
||||
/// It implements [`Future`] so you can `.await` on it.
|
||||
@@ -101,39 +38,57 @@ impl Priority {
|
||||
/// the task to continue running, but with no way to return a value.
|
||||
#[must_use]
|
||||
#[derive(Debug)]
|
||||
pub struct Task<T>(TaskState<T>);
|
||||
|
||||
#[derive(Debug)]
|
||||
enum TaskState<T> {
|
||||
/// A task that is ready to return a value
|
||||
Ready(Option<T>),
|
||||
|
||||
/// A task that is currently running.
|
||||
Spawned(async_task::Task<T, RunnableMeta>),
|
||||
}
|
||||
pub struct Task<T>(crate::scheduler::Task<T>);
|
||||
|
||||
impl<T> Task<T> {
|
||||
/// Creates a new task that will resolve with the value
|
||||
/// Creates a new task that will resolve with the value.
|
||||
pub fn ready(val: T) -> Self {
|
||||
Task(TaskState::Ready(Some(val)))
|
||||
Task(crate::scheduler::Task::ready(val))
|
||||
}
|
||||
|
||||
/// Detaching a task runs it to completion in the background
|
||||
/// Returns true if the task has completed or was created with `Task::ready`.
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.0.is_ready()
|
||||
}
|
||||
|
||||
/// Detaching a task runs it to completion in the background.
|
||||
pub fn detach(self) {
|
||||
match self {
|
||||
Task(TaskState::Ready(_)) => {}
|
||||
Task(TaskState::Spawned(task)) => task.detach(),
|
||||
}
|
||||
self.0.detach()
|
||||
}
|
||||
|
||||
/// Wraps a scheduler Task.
|
||||
pub fn from_scheduler(task: crate::scheduler::Task<T>) -> Self {
|
||||
Task(task)
|
||||
}
|
||||
|
||||
/// Converts this task into a fallible task that returns `Option<T>`.
|
||||
///
|
||||
/// Unlike the standard `Task<T>`, a [`FallibleTask`] will return `None`
|
||||
/// if the task was cancelled.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// // Background task that gracefully handles cancellation:
|
||||
/// cx.background_spawn(async move {
|
||||
/// let result = foreground_task.fallible().await;
|
||||
/// if let Some(value) = result {
|
||||
/// // Process the value
|
||||
/// }
|
||||
/// // If None, task was cancelled - just exit gracefully
|
||||
/// }).detach();
|
||||
/// ```
|
||||
pub fn fallible(self) -> FallibleTask<T> {
|
||||
self.0.fallible()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, T> Task<Result<T, E>>
|
||||
impl<T, E> Task<Result<T, E>>
|
||||
where
|
||||
T: 'static,
|
||||
E: 'static + Debug,
|
||||
{
|
||||
/// Run the task to completion in the background and log any
|
||||
/// errors that occur.
|
||||
/// Run the task to completion in the background and log any errors that occur.
|
||||
#[track_caller]
|
||||
pub fn detach_and_log_err(self, cx: &App) {
|
||||
let location = core::panic::Location::caller();
|
||||
@@ -143,53 +98,44 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Future for Task<T> {
|
||||
impl<T> std::future::Future for Task<T> {
|
||||
type Output = T;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||
match unsafe { self.get_unchecked_mut() } {
|
||||
Task(TaskState::Ready(val)) => Poll::Ready(val.take().unwrap()),
|
||||
Task(TaskState::Spawned(task)) => task.poll(cx),
|
||||
fn poll(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Self::Output> {
|
||||
// SAFETY: Task is a repr(transparent) wrapper around the scheduler Task,
|
||||
// and we're just projecting the pin through to the inner task.
|
||||
let inner = unsafe { self.map_unchecked_mut(|t| &mut t.0) };
|
||||
inner.poll(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl BackgroundExecutor {
|
||||
/// Creates a new BackgroundExecutor from the given PlatformDispatcher.
|
||||
pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
let scheduler: Arc<dyn Scheduler> = if let Some(test_dispatcher) = dispatcher.as_test() {
|
||||
test_dispatcher.scheduler().clone()
|
||||
} else {
|
||||
Arc::new(PlatformScheduler::new(dispatcher.clone()))
|
||||
};
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
let scheduler: Arc<dyn Scheduler> = Arc::new(PlatformScheduler::new(dispatcher.clone()));
|
||||
|
||||
Self {
|
||||
inner: crate::scheduler::BackgroundExecutor::new(scheduler),
|
||||
dispatcher,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A task label is an opaque identifier that you can use to
|
||||
/// refer to a task in tests.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
pub struct TaskLabel(NonZeroUsize);
|
||||
|
||||
impl Default for TaskLabel {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl TaskLabel {
|
||||
/// Construct a new task label.
|
||||
pub fn new() -> Self {
|
||||
static NEXT_TASK_LABEL: AtomicUsize = AtomicUsize::new(1);
|
||||
Self(
|
||||
NEXT_TASK_LABEL
|
||||
.fetch_add(1, Ordering::SeqCst)
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type AnyLocalFuture<R> = Pin<Box<dyn 'static + Future<Output = R>>>;
|
||||
|
||||
type AnyFuture<R> = Pin<Box<dyn 'static + Send + Future<Output = R>>>;
|
||||
|
||||
/// BackgroundExecutor lets you run things on background threads.
|
||||
/// In production this is a thread pool with no ordering guarantees.
|
||||
/// In tests this is simulated by running tasks one by one in a deterministic
|
||||
/// (but arbitrary) order controlled by the `SEED` environment variable.
|
||||
impl BackgroundExecutor {
|
||||
#[doc(hidden)]
|
||||
pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
|
||||
Self { dispatcher }
|
||||
/// Returns the underlying crate::scheduler::BackgroundExecutor.
|
||||
///
|
||||
/// This is used by Ex to pass the executor to thread/worktree code.
|
||||
pub fn scheduler_executor(&self) -> crate::scheduler::BackgroundExecutor {
|
||||
self.inner.clone()
|
||||
}
|
||||
|
||||
/// Enqueues the given future to be run to completion on a background thread.
|
||||
@@ -198,10 +144,13 @@ impl BackgroundExecutor {
|
||||
where
|
||||
R: Send + 'static,
|
||||
{
|
||||
self.spawn_with_priority(Priority::default(), future)
|
||||
self.spawn_with_priority(Priority::default(), future.boxed())
|
||||
}
|
||||
|
||||
/// Enqueues the given future to be run to completion on a background thread.
|
||||
/// Enqueues the given future to be run to completion on a background thread with the given priority.
|
||||
///
|
||||
/// When `Priority::RealtimeAudio` is used, the task runs on a dedicated thread with
|
||||
/// realtime scheduling priority, suitable for audio processing.
|
||||
#[track_caller]
|
||||
pub fn spawn_with_priority<R>(
|
||||
&self,
|
||||
@@ -211,7 +160,11 @@ impl BackgroundExecutor {
|
||||
where
|
||||
R: Send + 'static,
|
||||
{
|
||||
self.spawn_internal::<R>(Box::pin(future), None, priority)
|
||||
if priority == Priority::RealtimeAudio {
|
||||
Task::from_scheduler(self.inner.spawn_realtime(future))
|
||||
} else {
|
||||
Task::from_scheduler(self.inner.spawn_with_priority(priority, future))
|
||||
}
|
||||
}
|
||||
|
||||
/// Enqueues the given future to be run to completion on a background thread and blocking the current task on it.
|
||||
@@ -222,8 +175,9 @@ impl BackgroundExecutor {
|
||||
where
|
||||
R: Send,
|
||||
{
|
||||
// We need to ensure that cancellation of the parent task does not drop the environment
|
||||
// before the our own task has completed or got cancelled.
|
||||
use crate::RunnableMeta;
|
||||
use parking_lot::{Condvar, Mutex};
|
||||
|
||||
struct NotifyOnDrop<'a>(&'a (Condvar, Mutex<bool>));
|
||||
|
||||
impl Drop for NotifyOnDrop<'_> {
|
||||
@@ -259,11 +213,7 @@ impl BackgroundExecutor {
|
||||
future.await
|
||||
},
|
||||
move |runnable| {
|
||||
dispatcher.dispatch(
|
||||
RunnableVariant::Meta(runnable),
|
||||
None,
|
||||
Priority::default(),
|
||||
)
|
||||
dispatcher.dispatch(runnable, Priority::default());
|
||||
},
|
||||
)
|
||||
};
|
||||
@@ -271,240 +221,6 @@ impl BackgroundExecutor {
|
||||
task.await
|
||||
}
|
||||
|
||||
/// Enqueues the given future to be run to completion on a background thread.
|
||||
/// The given label can be used to control the priority of the task in tests.
|
||||
#[track_caller]
|
||||
pub fn spawn_labeled<R>(
|
||||
&self,
|
||||
label: TaskLabel,
|
||||
future: impl Future<Output = R> + Send + 'static,
|
||||
) -> Task<R>
|
||||
where
|
||||
R: Send + 'static,
|
||||
{
|
||||
self.spawn_internal::<R>(Box::pin(future), Some(label), Priority::default())
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn spawn_internal<R: Send + 'static>(
|
||||
&self,
|
||||
future: AnyFuture<R>,
|
||||
label: Option<TaskLabel>,
|
||||
priority: Priority,
|
||||
) -> Task<R> {
|
||||
let dispatcher = self.dispatcher.clone();
|
||||
let (runnable, task) = if let Priority::Realtime(realtime) = priority {
|
||||
let location = core::panic::Location::caller();
|
||||
let (mut tx, rx) = flume::bounded::<Runnable<RunnableMeta>>(1);
|
||||
|
||||
dispatcher.spawn_realtime(
|
||||
realtime,
|
||||
Box::new(move || {
|
||||
while let Ok(runnable) = rx.recv() {
|
||||
let start = Instant::now();
|
||||
let location = runnable.metadata().location;
|
||||
let mut timing = TaskTiming {
|
||||
location,
|
||||
start,
|
||||
end: None,
|
||||
};
|
||||
profiler::add_task_timing(timing);
|
||||
|
||||
runnable.run();
|
||||
|
||||
let end = Instant::now();
|
||||
timing.end = Some(end);
|
||||
profiler::add_task_timing(timing);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
async_task::Builder::new()
|
||||
.metadata(RunnableMeta { location })
|
||||
.spawn(
|
||||
move |_| future,
|
||||
move |runnable| {
|
||||
let _ = tx.send(runnable);
|
||||
},
|
||||
)
|
||||
} else {
|
||||
let location = core::panic::Location::caller();
|
||||
async_task::Builder::new()
|
||||
.metadata(RunnableMeta { location })
|
||||
.spawn(
|
||||
move |_| future,
|
||||
move |runnable| {
|
||||
dispatcher.dispatch(RunnableVariant::Meta(runnable), label, priority)
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
runnable.schedule();
|
||||
Task(TaskState::Spawned(task))
|
||||
}
|
||||
|
||||
/// Used by the test harness to run an async test in a synchronous fashion.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[track_caller]
|
||||
pub fn block_test<R>(&self, future: impl Future<Output = R>) -> R {
|
||||
if let Ok(value) = self.block_internal(false, future, None) {
|
||||
value
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
/// Block the current thread until the given future resolves.
|
||||
/// Consider using `block_with_timeout` instead.
|
||||
pub fn block<R>(&self, future: impl Future<Output = R>) -> R {
|
||||
if let Ok(value) = self.block_internal(true, future, None) {
|
||||
value
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
pub(crate) fn block_internal<Fut: Future>(
|
||||
&self,
|
||||
_background_only: bool,
|
||||
future: Fut,
|
||||
timeout: Option<Duration>,
|
||||
) -> Result<Fut::Output, impl Future<Output = Fut::Output> + use<Fut>> {
|
||||
use std::time::Instant;
|
||||
|
||||
let mut future = Box::pin(future);
|
||||
if timeout == Some(Duration::ZERO) {
|
||||
return Err(future);
|
||||
}
|
||||
let deadline = timeout.map(|timeout| Instant::now() + timeout);
|
||||
|
||||
let parker = parking::Parker::new();
|
||||
let unparker = parker.unparker();
|
||||
let waker = waker_fn(move || {
|
||||
unparker.unpark();
|
||||
});
|
||||
let mut cx = std::task::Context::from_waker(&waker);
|
||||
|
||||
loop {
|
||||
match future.as_mut().poll(&mut cx) {
|
||||
Poll::Ready(result) => return Ok(result),
|
||||
Poll::Pending => {
|
||||
let timeout =
|
||||
deadline.map(|deadline| deadline.saturating_duration_since(Instant::now()));
|
||||
if let Some(timeout) = timeout {
|
||||
if !parker.park_timeout(timeout)
|
||||
&& deadline.is_some_and(|deadline| deadline < Instant::now())
|
||||
{
|
||||
return Err(future);
|
||||
}
|
||||
} else {
|
||||
parker.park();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[track_caller]
|
||||
pub(crate) fn block_internal<Fut: Future>(
|
||||
&self,
|
||||
background_only: bool,
|
||||
future: Fut,
|
||||
timeout: Option<Duration>,
|
||||
) -> Result<Fut::Output, impl Future<Output = Fut::Output> + use<Fut>> {
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use parking::Parker;
|
||||
|
||||
let mut future = Box::pin(future);
|
||||
if timeout == Some(Duration::ZERO) {
|
||||
return Err(future);
|
||||
}
|
||||
let Some(dispatcher) = self.dispatcher.as_test() else {
|
||||
return Err(future);
|
||||
};
|
||||
|
||||
let mut max_ticks = if timeout.is_some() {
|
||||
dispatcher.gen_block_on_ticks()
|
||||
} else {
|
||||
usize::MAX
|
||||
};
|
||||
|
||||
let parker = Parker::new();
|
||||
let unparker = parker.unparker();
|
||||
|
||||
let awoken = Arc::new(AtomicBool::new(false));
|
||||
let waker = waker_fn({
|
||||
let awoken = awoken.clone();
|
||||
let unparker = unparker.clone();
|
||||
move || {
|
||||
awoken.store(true, Ordering::SeqCst);
|
||||
unparker.unpark();
|
||||
}
|
||||
});
|
||||
let mut cx = std::task::Context::from_waker(&waker);
|
||||
|
||||
let duration = Duration::from_secs(
|
||||
option_env!("GPUI_TEST_TIMEOUT")
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.unwrap_or(180),
|
||||
);
|
||||
let mut test_should_end_by = Instant::now() + duration;
|
||||
|
||||
loop {
|
||||
match future.as_mut().poll(&mut cx) {
|
||||
Poll::Ready(result) => return Ok(result),
|
||||
Poll::Pending => {
|
||||
if max_ticks == 0 {
|
||||
return Err(future);
|
||||
}
|
||||
max_ticks -= 1;
|
||||
|
||||
if !dispatcher.tick(background_only) {
|
||||
if awoken.swap(false, Ordering::SeqCst) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !dispatcher.parking_allowed() {
|
||||
if dispatcher.advance_clock_to_next_delayed() {
|
||||
continue;
|
||||
}
|
||||
let mut backtrace_message = String::new();
|
||||
let mut waiting_message = String::new();
|
||||
if let Some(backtrace) = dispatcher.waiting_backtrace() {
|
||||
backtrace_message =
|
||||
format!("\nbacktrace of waiting future:\n{:?}", backtrace);
|
||||
}
|
||||
if let Some(waiting_hint) = dispatcher.waiting_hint() {
|
||||
waiting_message = format!("\n waiting on: {}\n", waiting_hint);
|
||||
}
|
||||
panic!(
|
||||
"parked with nothing left to run{waiting_message}{backtrace_message}",
|
||||
)
|
||||
}
|
||||
dispatcher.push_unparker(unparker.clone());
|
||||
parker.park_timeout(Duration::from_millis(1));
|
||||
if Instant::now() > test_should_end_by {
|
||||
panic!("test timed out after {duration:?} with allow_parking")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Block the current thread until the given future resolves
|
||||
/// or `duration` has elapsed.
|
||||
pub fn block_with_timeout<Fut: Future>(
|
||||
&self,
|
||||
duration: Duration,
|
||||
future: Fut,
|
||||
) -> Result<Fut::Output, impl Future<Output = Fut::Output> + use<Fut>> {
|
||||
self.block_internal(true, future, Some(duration))
|
||||
}
|
||||
|
||||
/// Scoped lets you start a number of tasks and waits
|
||||
/// for all of them to complete before returning.
|
||||
pub async fn scoped<'scope, F>(&self, scheduler: F)
|
||||
@@ -544,103 +260,107 @@ impl BackgroundExecutor {
|
||||
/// Calling this instead of `std::time::Instant::now` allows the use
|
||||
/// of fake timers in tests.
|
||||
pub fn now(&self) -> Instant {
|
||||
self.dispatcher.now()
|
||||
self.inner.scheduler().clock().now()
|
||||
}
|
||||
|
||||
/// Returns a task that will complete after the given duration.
|
||||
/// Depending on other concurrent tasks the elapsed duration may be longer
|
||||
/// than requested.
|
||||
#[track_caller]
|
||||
pub fn timer(&self, duration: Duration) -> Task<()> {
|
||||
if duration.is_zero() {
|
||||
return Task::ready(());
|
||||
}
|
||||
let location = core::panic::Location::caller();
|
||||
let (runnable, task) = async_task::Builder::new()
|
||||
.metadata(RunnableMeta { location })
|
||||
.spawn(move |_| async move {}, {
|
||||
let dispatcher = self.dispatcher.clone();
|
||||
move |runnable| dispatcher.dispatch_after(duration, RunnableVariant::Meta(runnable))
|
||||
});
|
||||
runnable.schedule();
|
||||
Task(TaskState::Spawned(task))
|
||||
self.spawn(self.inner.scheduler().timer(duration))
|
||||
}
|
||||
|
||||
/// in tests, start_waiting lets you indicate which task is waiting (for debugging only)
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn start_waiting(&self) {
|
||||
self.dispatcher.as_test().unwrap().start_waiting();
|
||||
}
|
||||
|
||||
/// in tests, removes the debugging data added by start_waiting
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn finish_waiting(&self) {
|
||||
self.dispatcher.as_test().unwrap().finish_waiting();
|
||||
}
|
||||
|
||||
/// in tests, run an arbitrary number of tasks (determined by the SEED environment variable)
|
||||
/// In tests, run an arbitrary number of tasks (determined by the SEED environment variable)
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn simulate_random_delay(&self) -> impl Future<Output = ()> + use<> {
|
||||
self.dispatcher.as_test().unwrap().simulate_random_delay()
|
||||
}
|
||||
|
||||
/// in tests, indicate that a given task from `spawn_labeled` should run after everything else
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn deprioritize(&self, task_label: TaskLabel) {
|
||||
self.dispatcher.as_test().unwrap().deprioritize(task_label)
|
||||
}
|
||||
|
||||
/// in tests, move time forward. This does not run any tasks, but does make `timer`s ready.
|
||||
/// In tests, move time forward. This does not run any tasks, but does make `timer`s ready.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn advance_clock(&self, duration: Duration) {
|
||||
self.dispatcher.as_test().unwrap().advance_clock(duration)
|
||||
}
|
||||
|
||||
/// in tests, run one task.
|
||||
/// In tests, run one task.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn tick(&self) -> bool {
|
||||
self.dispatcher.as_test().unwrap().tick(false)
|
||||
self.dispatcher.as_test().unwrap().scheduler().tick()
|
||||
}
|
||||
|
||||
/// in tests, run all tasks that are ready to run. If after doing so
|
||||
/// the test still has outstanding tasks, this will panic. (See also [`Self::allow_parking`])
|
||||
/// In tests, run tasks until the scheduler would park.
|
||||
///
|
||||
/// Under the scheduler-backed test dispatcher, `tick()` will not advance the clock, so a pending
|
||||
/// timer can keep `has_pending_tasks()` true even after all currently-runnable tasks have been
|
||||
/// drained. To preserve the historical semantics that tests relied on (drain all work that can
|
||||
/// make progress), we advance the clock to the next timer when no runnable tasks remain.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn run_until_parked(&self) {
|
||||
self.dispatcher.as_test().unwrap().run_until_parked()
|
||||
let scheduler = self.dispatcher.as_test().unwrap().scheduler();
|
||||
scheduler.run();
|
||||
}
|
||||
|
||||
/// in tests, prevents `run_until_parked` from panicking if there are outstanding tasks.
|
||||
/// This is useful when you are integrating other (non-GPUI) futures, like disk access, that
|
||||
/// do take real async time to run.
|
||||
/// In tests, prevents `run_until_parked` from panicking if there are outstanding tasks.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn allow_parking(&self) {
|
||||
self.dispatcher.as_test().unwrap().allow_parking();
|
||||
self.dispatcher
|
||||
.as_test()
|
||||
.unwrap()
|
||||
.scheduler()
|
||||
.allow_parking();
|
||||
|
||||
if std::env::var("GPUI_RUN_UNTIL_PARKED_LOG").ok().as_deref() == Some("1") {
|
||||
log::warn!("[gpui::executor] allow_parking: enabled");
|
||||
}
|
||||
}
|
||||
|
||||
/// undoes the effect of [`Self::allow_parking`].
|
||||
/// Sets the range of ticks to run before timing out in block_on.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive<usize>) {
|
||||
self.dispatcher
|
||||
.as_test()
|
||||
.unwrap()
|
||||
.scheduler()
|
||||
.set_timeout_ticks(range);
|
||||
}
|
||||
|
||||
/// Undoes the effect of [`Self::allow_parking`].
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn forbid_parking(&self) {
|
||||
self.dispatcher.as_test().unwrap().forbid_parking();
|
||||
self.dispatcher
|
||||
.as_test()
|
||||
.unwrap()
|
||||
.scheduler()
|
||||
.forbid_parking();
|
||||
}
|
||||
|
||||
/// adds detail to the "parked with nothing let to run" message.
|
||||
/// In tests, returns the rng used by the dispatcher.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn set_waiting_hint(&self, msg: Option<String>) {
|
||||
self.dispatcher.as_test().unwrap().set_waiting_hint(msg);
|
||||
}
|
||||
|
||||
/// in tests, returns the rng used by the dispatcher and seeded by the `SEED` environment variable
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn rng(&self) -> StdRng {
|
||||
self.dispatcher.as_test().unwrap().rng()
|
||||
pub fn rng(&self) -> crate::scheduler::SharedRng {
|
||||
self.dispatcher.as_test().unwrap().scheduler().rng()
|
||||
}
|
||||
|
||||
/// How many CPUs are available to the dispatcher.
|
||||
pub fn num_cpus(&self) -> usize {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
return 4;
|
||||
if let Some(test) = self.dispatcher.as_test() {
|
||||
return test.num_cpus_override().unwrap_or(4);
|
||||
}
|
||||
num_cpus::get()
|
||||
}
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
return num_cpus::get();
|
||||
/// Override the number of CPUs reported by this executor in tests.
|
||||
/// Panics if not called on a test executor.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn set_num_cpus(&self, count: usize) {
|
||||
self.dispatcher
|
||||
.as_test()
|
||||
.expect("set_num_cpus can only be called on a test executor")
|
||||
.set_num_cpus(count);
|
||||
}
|
||||
|
||||
/// Whether we're on the main thread.
|
||||
@@ -648,132 +368,112 @@ impl BackgroundExecutor {
|
||||
self.dispatcher.is_main_thread()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
/// in tests, control the number of ticks that `block_with_timeout` will run before timing out.
|
||||
pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive<usize>) {
|
||||
self.dispatcher.as_test().unwrap().set_block_on_ticks(range);
|
||||
#[doc(hidden)]
|
||||
pub fn dispatcher(&self) -> &Arc<dyn PlatformDispatcher> {
|
||||
&self.dispatcher
|
||||
}
|
||||
}
|
||||
|
||||
/// ForegroundExecutor runs things on the main thread.
|
||||
impl ForegroundExecutor {
|
||||
/// Creates a new ForegroundExecutor from the given PlatformDispatcher.
|
||||
pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
let (scheduler, session_id): (Arc<dyn Scheduler>, _) =
|
||||
if let Some(test_dispatcher) = dispatcher.as_test() {
|
||||
(
|
||||
test_dispatcher.scheduler().clone(),
|
||||
test_dispatcher.session_id(),
|
||||
)
|
||||
} else {
|
||||
let platform_scheduler = Arc::new(PlatformScheduler::new(dispatcher.clone()));
|
||||
let session_id = platform_scheduler.allocate_session_id();
|
||||
(platform_scheduler, session_id)
|
||||
};
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
let (scheduler, session_id): (Arc<dyn Scheduler>, _) = {
|
||||
let platform_scheduler = Arc::new(PlatformScheduler::new(dispatcher.clone()));
|
||||
let session_id = platform_scheduler.allocate_session_id();
|
||||
(platform_scheduler, session_id)
|
||||
};
|
||||
|
||||
let inner = crate::scheduler::ForegroundExecutor::new(session_id, scheduler);
|
||||
|
||||
Self {
|
||||
inner,
|
||||
dispatcher,
|
||||
not_send: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Enqueues the given Task to run on the main thread at some point in the future.
|
||||
/// Enqueues the given Task to run on the main thread.
|
||||
#[track_caller]
|
||||
pub fn spawn<R>(&self, future: impl Future<Output = R> + 'static) -> Task<R>
|
||||
where
|
||||
R: 'static,
|
||||
{
|
||||
self.spawn_with_priority(Priority::default(), future)
|
||||
Task::from_scheduler(self.inner.spawn(future.boxed_local()))
|
||||
}
|
||||
|
||||
/// Enqueues the given Task to run on the main thread at some point in the future.
|
||||
/// Enqueues the given Task to run on the main thread with the given priority.
|
||||
#[track_caller]
|
||||
pub fn spawn_with_priority<R>(
|
||||
&self,
|
||||
priority: Priority,
|
||||
_priority: Priority,
|
||||
future: impl Future<Output = R> + 'static,
|
||||
) -> Task<R>
|
||||
where
|
||||
R: 'static,
|
||||
{
|
||||
let dispatcher = self.dispatcher.clone();
|
||||
let location = core::panic::Location::caller();
|
||||
|
||||
#[track_caller]
|
||||
fn inner<R: 'static>(
|
||||
dispatcher: Arc<dyn PlatformDispatcher>,
|
||||
future: AnyLocalFuture<R>,
|
||||
location: &'static core::panic::Location<'static>,
|
||||
priority: Priority,
|
||||
) -> Task<R> {
|
||||
let (runnable, task) = spawn_local_with_source_location(
|
||||
future,
|
||||
move |runnable| {
|
||||
dispatcher.dispatch_on_main_thread(RunnableVariant::Meta(runnable), priority)
|
||||
},
|
||||
RunnableMeta { location },
|
||||
);
|
||||
runnable.schedule();
|
||||
Task(TaskState::Spawned(task))
|
||||
}
|
||||
inner::<R>(dispatcher, Box::pin(future), location, priority)
|
||||
}
|
||||
}
|
||||
|
||||
/// Variant of `async_task::spawn_local` that includes the source location of the spawn in panics.
|
||||
///
|
||||
/// Copy-modified from:
|
||||
/// <https://github.com/smol-rs/async-task/blob/ca9dbe1db9c422fd765847fa91306e30a6bb58a9/src/runnable.rs#L405>
|
||||
#[track_caller]
|
||||
fn spawn_local_with_source_location<Fut, S, M>(
|
||||
future: Fut,
|
||||
schedule: S,
|
||||
metadata: M,
|
||||
) -> (Runnable<M>, async_task::Task<Fut::Output, M>)
|
||||
where
|
||||
Fut: Future + 'static,
|
||||
Fut::Output: 'static,
|
||||
S: async_task::Schedule<M> + Send + Sync + 'static,
|
||||
M: 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn thread_id() -> ThreadId {
|
||||
std::thread_local! {
|
||||
static ID: ThreadId = thread::current().id();
|
||||
}
|
||||
ID.try_with(|id| *id)
|
||||
.unwrap_or_else(|_| thread::current().id())
|
||||
// Priority is ignored for foreground tasks - they run in order on the main thread
|
||||
Task::from_scheduler(self.inner.spawn(future))
|
||||
}
|
||||
|
||||
struct Checked<F> {
|
||||
id: ThreadId,
|
||||
inner: ManuallyDrop<F>,
|
||||
location: &'static Location<'static>,
|
||||
/// Used by the test harness to run an async test in a synchronous fashion.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[track_caller]
|
||||
pub fn block_test<R>(&self, future: impl Future<Output = R>) -> R {
|
||||
use std::cell::Cell;
|
||||
|
||||
let scheduler = self.inner.scheduler();
|
||||
|
||||
let output = Cell::new(None);
|
||||
let future = async {
|
||||
output.set(Some(future.await));
|
||||
};
|
||||
let mut future = std::pin::pin!(future);
|
||||
|
||||
// In async GPUI tests, we must allow foreground tasks scheduled by the test itself
|
||||
// (which are associated with the test session) to make progress while we block.
|
||||
// Otherwise, awaiting futures that depend on same-session foreground work can deadlock.
|
||||
scheduler.block(None, future.as_mut(), None);
|
||||
|
||||
output.take().expect("block_test future did not complete")
|
||||
}
|
||||
|
||||
impl<F> Drop for Checked<F> {
|
||||
fn drop(&mut self) {
|
||||
assert!(
|
||||
self.id == thread_id(),
|
||||
"local task dropped by a thread that didn't spawn it. Task spawned at {}",
|
||||
self.location
|
||||
);
|
||||
unsafe { ManuallyDrop::drop(&mut self.inner) };
|
||||
}
|
||||
/// Block the current thread until the given future resolves.
|
||||
/// Consider using `block_with_timeout` instead.
|
||||
pub fn block_on<R>(&self, future: impl Future<Output = R>) -> R {
|
||||
self.inner.block_on(future)
|
||||
}
|
||||
|
||||
impl<F: Future> Future for Checked<F> {
|
||||
type Output = F::Output;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
assert!(
|
||||
self.id == thread_id(),
|
||||
"local task polled by a thread that didn't spawn it. Task spawned at {}",
|
||||
self.location
|
||||
);
|
||||
unsafe { self.map_unchecked_mut(|c| &mut *c.inner).poll(cx) }
|
||||
}
|
||||
/// Block the current thread until the given future resolves or the timeout elapses.
|
||||
pub fn block_with_timeout<R, Fut: Future<Output = R>>(
|
||||
&self,
|
||||
duration: Duration,
|
||||
future: Fut,
|
||||
) -> Result<R, impl Future<Output = R> + use<R, Fut>> {
|
||||
self.inner.block_with_timeout(duration, future)
|
||||
}
|
||||
|
||||
// Wrap the future into one that checks which thread it's on.
|
||||
let future = Checked {
|
||||
id: thread_id(),
|
||||
inner: ManuallyDrop::new(future),
|
||||
location: Location::caller(),
|
||||
};
|
||||
#[doc(hidden)]
|
||||
pub fn dispatcher(&self) -> &Arc<dyn PlatformDispatcher> {
|
||||
&self.dispatcher
|
||||
}
|
||||
|
||||
unsafe {
|
||||
async_task::Builder::new()
|
||||
.metadata(metadata)
|
||||
.spawn_unchecked(move |_| future, schedule)
|
||||
#[doc(hidden)]
|
||||
pub fn scheduler_executor(&self) -> SchedulerForegroundExecutor {
|
||||
self.inner.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -834,6 +534,62 @@ impl Drop for Scope<'_> {
|
||||
|
||||
// Wait until the channel is closed, which means that all of the spawned
|
||||
// futures have resolved.
|
||||
self.executor.block(self.rx.next());
|
||||
let future = async {
|
||||
self.rx.next().await;
|
||||
};
|
||||
let mut future = std::pin::pin!(future);
|
||||
self.executor
|
||||
.inner
|
||||
.scheduler()
|
||||
.block(None, future.as_mut(), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::{App, TestDispatcher, TestPlatform};
|
||||
use std::cell::RefCell;
|
||||
|
||||
/// Helper to create test infrastructure.
|
||||
/// Returns (dispatcher, background_executor, app).
|
||||
fn create_test_app() -> (TestDispatcher, BackgroundExecutor, Rc<crate::AppCell>) {
|
||||
let dispatcher = TestDispatcher::new(0);
|
||||
let arc_dispatcher = Arc::new(dispatcher.clone());
|
||||
let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
|
||||
let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
|
||||
|
||||
let platform = TestPlatform::new(background_executor.clone(), foreground_executor);
|
||||
let asset_source = Arc::new(());
|
||||
let http_client = http_client::FakeHttpClient::with_404_response();
|
||||
|
||||
let app = App::new_app(platform, asset_source, http_client);
|
||||
(dispatcher, background_executor, app)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanity_test_tasks_run() {
|
||||
let (dispatcher, _background_executor, app) = create_test_app();
|
||||
let foreground_executor = app.borrow().foreground_executor.clone();
|
||||
|
||||
let task_ran = Rc::new(RefCell::new(false));
|
||||
|
||||
foreground_executor
|
||||
.spawn({
|
||||
let task_ran = Rc::clone(&task_ran);
|
||||
async move {
|
||||
*task_ran.borrow_mut() = true;
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
// Run dispatcher while app is still alive
|
||||
dispatcher.run_until_parked();
|
||||
|
||||
// Task should have run
|
||||
assert!(
|
||||
*task_ran.borrow(),
|
||||
"Task should run normally when app is alive"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+42
-81
@@ -78,6 +78,7 @@ pub trait Along {
|
||||
Deserialize,
|
||||
JsonSchema,
|
||||
Hash,
|
||||
Neg,
|
||||
)]
|
||||
#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[repr(C)]
|
||||
@@ -182,12 +183,6 @@ impl<T: Clone + Debug + Default + PartialEq> Along for Point<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + Debug + Default + PartialEq + Negate> Negate for Point<T> {
|
||||
fn negate(self) -> Self {
|
||||
self.map(Negate::negate)
|
||||
}
|
||||
}
|
||||
|
||||
impl Point<Pixels> {
|
||||
/// Scales the point by a given factor, which is typically derived from the resolution
|
||||
/// of a target display to ensure proper sizing of UI elements.
|
||||
@@ -393,7 +388,9 @@ impl<T: Clone + Debug + Default + PartialEq + Display> Display for Point<T> {
|
||||
///
|
||||
/// This struct is generic over the type `T`, which can be any type that implements `Clone`, `Default`, and `Debug`.
|
||||
/// It is commonly used to specify dimensions for elements in a UI, such as a window or element.
|
||||
#[derive(Refineable, Default, Clone, Copy, PartialEq, Div, Hash, Serialize, Deserialize)]
|
||||
#[derive(
|
||||
Add, Clone, Copy, Default, Deserialize, Div, Hash, Neg, PartialEq, Refineable, Serialize, Sub,
|
||||
)]
|
||||
#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[repr(C)]
|
||||
pub struct Size<T: Clone + Debug + Default + PartialEq> {
|
||||
@@ -598,34 +595,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Sub for Size<T>
|
||||
where
|
||||
T: Sub<Output = T> + Clone + Debug + Default + PartialEq,
|
||||
{
|
||||
type Output = Size<T>;
|
||||
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
Size {
|
||||
width: self.width - rhs.width,
|
||||
height: self.height - rhs.height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Add for Size<T>
|
||||
where
|
||||
T: Add<Output = T> + Clone + Debug + Default + PartialEq,
|
||||
{
|
||||
type Output = Size<T>;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Size {
|
||||
width: self.width + rhs.width,
|
||||
height: self.height + rhs.height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Rhs> Mul<Rhs> for Size<T>
|
||||
where
|
||||
T: Mul<Rhs, Output = Rhs> + Clone + Debug + Default + PartialEq,
|
||||
@@ -1112,7 +1081,10 @@ impl<T: PartialOrd + Add<T, Output = T> + Sub<Output = T> + Clone + Debug + Defa
|
||||
/// ```
|
||||
pub fn intersect(&self, other: &Self) -> Self {
|
||||
let upper_left = self.origin.max(&other.origin);
|
||||
let bottom_right = self.bottom_right().min(&other.bottom_right());
|
||||
let bottom_right = self
|
||||
.bottom_right()
|
||||
.min(&other.bottom_right())
|
||||
.max(&upper_left);
|
||||
Self::from_corners(upper_left, bottom_right)
|
||||
}
|
||||
|
||||
@@ -1242,6 +1214,15 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + Debug + Default + PartialEq> From<Size<T>> for Point<T> {
|
||||
fn from(size: Size<T>) -> Self {
|
||||
Self {
|
||||
x: size.width,
|
||||
y: size.height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Bounds<T>
|
||||
where
|
||||
T: Add<T, Output = T> + Clone + Debug + Default + PartialEq,
|
||||
@@ -1589,7 +1570,7 @@ impl<T: Clone + Debug + Default + PartialEq + Display + Add<T, Output = T>> Disp
|
||||
|
||||
impl Size<DevicePixels> {
|
||||
/// Converts the size from physical to logical pixels.
|
||||
pub(crate) fn to_pixels(self, scale_factor: f32) -> Size<Pixels> {
|
||||
pub fn to_pixels(self, scale_factor: f32) -> Size<Pixels> {
|
||||
size(
|
||||
px(self.width.0 as f32 / scale_factor),
|
||||
px(self.height.0 as f32 / scale_factor),
|
||||
@@ -1599,7 +1580,7 @@ impl Size<DevicePixels> {
|
||||
|
||||
impl Size<Pixels> {
|
||||
/// Converts the size from logical to physical pixels.
|
||||
pub(crate) fn to_device_pixels(self, scale_factor: f32) -> Size<DevicePixels> {
|
||||
pub fn to_device_pixels(self, scale_factor: f32) -> Size<DevicePixels> {
|
||||
size(
|
||||
DevicePixels((self.width.0 * scale_factor).round() as i32),
|
||||
DevicePixels((self.height.0 * scale_factor).round() as i32),
|
||||
@@ -2648,6 +2629,18 @@ impl Debug for Pixels {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::Sum for Pixels {
|
||||
fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
|
||||
iter.fold(Self::ZERO, |a, b| a + b)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> std::iter::Sum<&'a Pixels> for Pixels {
|
||||
fn sum<I: Iterator<Item = &'a Self>>(iter: I) -> Self {
|
||||
iter.fold(Self::ZERO, |a, b| a + *b)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&'_ str> for Pixels {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
@@ -2668,6 +2661,11 @@ impl Pixels {
|
||||
/// The minimum value that can be represented by `Pixels`.
|
||||
pub const MIN: Pixels = Pixels(f32::MIN);
|
||||
|
||||
/// Returns the raw `f32` value of this `Pixels`.
|
||||
pub fn as_f32(self) -> f32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Floors the `Pixels` value to the nearest whole number.
|
||||
///
|
||||
/// # Returns
|
||||
@@ -2949,9 +2947,14 @@ impl From<usize> for DevicePixels {
|
||||
/// display resolutions.
|
||||
#[derive(Clone, Copy, Default, Add, AddAssign, Sub, SubAssign, Div, DivAssign, PartialEq)]
|
||||
#[repr(transparent)]
|
||||
pub struct ScaledPixels(pub(crate) f32);
|
||||
pub struct ScaledPixels(pub f32);
|
||||
|
||||
impl ScaledPixels {
|
||||
/// Returns the raw `f32` value of this `ScaledPixels`.
|
||||
pub fn as_f32(self) -> f32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Floors the `ScaledPixels` value to the nearest whole number.
|
||||
///
|
||||
/// # Returns
|
||||
@@ -3729,48 +3732,6 @@ impl Half for Rems {
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides a trait for types that can negate their values.
|
||||
pub trait Negate {
|
||||
/// Returns the negation of the given value
|
||||
fn negate(self) -> Self;
|
||||
}
|
||||
|
||||
impl Negate for i32 {
|
||||
fn negate(self) -> Self {
|
||||
-self
|
||||
}
|
||||
}
|
||||
|
||||
impl Negate for f32 {
|
||||
fn negate(self) -> Self {
|
||||
-self
|
||||
}
|
||||
}
|
||||
|
||||
impl Negate for DevicePixels {
|
||||
fn negate(self) -> Self {
|
||||
Self(-self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Negate for ScaledPixels {
|
||||
fn negate(self) -> Self {
|
||||
Self(-self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Negate for Pixels {
|
||||
fn negate(self) -> Self {
|
||||
Self(-self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Negate for Rems {
|
||||
fn negate(self) -> Self {
|
||||
Self(-self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for checking if a value is zero.
|
||||
///
|
||||
/// This trait provides a method to determine if a value is considered to be zero.
|
||||
|
||||
+34
-50
@@ -1,10 +1,11 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![deny(missing_docs)]
|
||||
#![warn(missing_docs)]
|
||||
#![allow(clippy::type_complexity)] // Not useful, GPUI makes heavy use of callbacks
|
||||
#![allow(clippy::collapsible_else_if)] // False positives in platform specific code
|
||||
#![allow(unused_mut)] // False positives in platform specific code
|
||||
|
||||
extern crate self as gpui;
|
||||
extern crate gpui_ce_macros as gpui_macros;
|
||||
|
||||
#[macro_use]
|
||||
mod action;
|
||||
@@ -16,10 +17,12 @@ mod assets;
|
||||
mod bounds_tree;
|
||||
mod color;
|
||||
/// The default colors used by GPUI.
|
||||
pub mod default_colors;
|
||||
pub mod colors;
|
||||
mod element;
|
||||
mod elements;
|
||||
mod executor;
|
||||
mod platform_scheduler;
|
||||
pub(crate) use platform_scheduler::PlatformScheduler;
|
||||
mod geometry;
|
||||
mod global;
|
||||
mod input;
|
||||
@@ -27,13 +30,18 @@ mod inspector;
|
||||
mod interactive;
|
||||
mod key_dispatch;
|
||||
mod keymap;
|
||||
mod local_util;
|
||||
mod path_builder;
|
||||
mod platform;
|
||||
pub mod prelude;
|
||||
mod profiler;
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
mod queue;
|
||||
/// Profiling utilities for task timing and thread performance tracking.
|
||||
pub mod profiler;
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_family = "wasm"))]
|
||||
#[expect(missing_docs)]
|
||||
pub mod queue;
|
||||
mod scene;
|
||||
/// The scheduler module provides task scheduling, execution, and timing primitives.
|
||||
pub mod scheduler;
|
||||
mod shared_string;
|
||||
mod shared_uri;
|
||||
mod style;
|
||||
@@ -45,10 +53,12 @@ mod taffy;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
mod text_system;
|
||||
mod util;
|
||||
mod view;
|
||||
mod window;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use proptest;
|
||||
|
||||
#[cfg(doc)]
|
||||
pub mod _ownership_and_data_flow;
|
||||
|
||||
@@ -76,29 +86,31 @@ pub use asset_cache::*;
|
||||
pub use assets::*;
|
||||
pub use color::*;
|
||||
pub use ctor::ctor;
|
||||
pub use default_colors::*;
|
||||
pub use element::*;
|
||||
pub use elements::*;
|
||||
pub use executor::*;
|
||||
pub use geometry::*;
|
||||
pub use global::*;
|
||||
pub use gpui_macros::{AppContext, IntoElement, Render, VisualContext, register_action, test};
|
||||
pub use gpui_macros::{
|
||||
AppContext, IntoElement, Render, VisualContext, derive_inspector_reflection, register_action,
|
||||
test,
|
||||
};
|
||||
pub use http_client;
|
||||
pub use input::*;
|
||||
pub use inspector::*;
|
||||
pub use interactive::*;
|
||||
use key_dispatch::*;
|
||||
pub use keymap::*;
|
||||
pub use local_util::{FutureExt, Timeout, command};
|
||||
pub use path_builder::*;
|
||||
pub use platform::*;
|
||||
pub use profiler::*;
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
pub(crate) use queue::{PriorityQueueReceiver, PriorityQueueSender};
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_family = "wasm"))]
|
||||
pub use queue::{PriorityQueueReceiver, PriorityQueueSender};
|
||||
pub use refineable::*;
|
||||
pub use scene::*;
|
||||
pub use shared_string::*;
|
||||
pub use shared_uri::*;
|
||||
pub use smol::Timer;
|
||||
use std::{any::Any, future::Future};
|
||||
pub use style::*;
|
||||
pub use styled::*;
|
||||
@@ -110,32 +122,23 @@ pub use taffy::{AvailableSpace, LayoutId};
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use test::*;
|
||||
pub use text_system::*;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use util::smol_timeout;
|
||||
pub use util::{FutureExt, Timeout, arc_cow::ArcCow};
|
||||
pub use util::arc_cow::ArcCow;
|
||||
pub use view::*;
|
||||
pub use window::*;
|
||||
|
||||
/// The context trait, allows the different contexts in GPUI to be used
|
||||
/// interchangeably for certain operations.
|
||||
pub trait AppContext {
|
||||
/// The result type for this context, used for async contexts that
|
||||
/// can't hold a direct reference to the application context.
|
||||
type Result<T>;
|
||||
|
||||
/// Create a new entity in the app context.
|
||||
#[expect(
|
||||
clippy::wrong_self_convention,
|
||||
reason = "`App::new` is an ubiquitous function for creating entities"
|
||||
)]
|
||||
fn new<T: 'static>(
|
||||
&mut self,
|
||||
build_entity: impl FnOnce(&mut Context<T>) -> T,
|
||||
) -> Self::Result<Entity<T>>;
|
||||
fn new<T: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T>;
|
||||
|
||||
/// Reserve a slot for a entity to be inserted later.
|
||||
/// The returned [Reservation] allows you to obtain the [EntityId] for the future entity.
|
||||
fn reserve_entity<T: 'static>(&mut self) -> Self::Result<Reservation<T>>;
|
||||
fn reserve_entity<T: 'static>(&mut self) -> Reservation<T>;
|
||||
|
||||
/// Insert a new entity in the app context based on a [Reservation] previously obtained from [`reserve_entity`].
|
||||
///
|
||||
@@ -144,28 +147,24 @@ pub trait AppContext {
|
||||
&mut self,
|
||||
reservation: Reservation<T>,
|
||||
build_entity: impl FnOnce(&mut Context<T>) -> T,
|
||||
) -> Self::Result<Entity<T>>;
|
||||
) -> Entity<T>;
|
||||
|
||||
/// Update a entity in the app context.
|
||||
fn update_entity<T, R>(
|
||||
&mut self,
|
||||
handle: &Entity<T>,
|
||||
update: impl FnOnce(&mut T, &mut Context<T>) -> R,
|
||||
) -> Self::Result<R>
|
||||
) -> R
|
||||
where
|
||||
T: 'static;
|
||||
|
||||
/// Update a entity in the app context.
|
||||
fn as_mut<'a, T>(&'a mut self, handle: &Entity<T>) -> Self::Result<GpuiBorrow<'a, T>>
|
||||
fn as_mut<'a, T>(&'a mut self, handle: &Entity<T>) -> GpuiBorrow<'a, T>
|
||||
where
|
||||
T: 'static;
|
||||
|
||||
/// Read a entity from the app context.
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &Entity<T>,
|
||||
read: impl FnOnce(&T, &App) -> R,
|
||||
) -> Self::Result<R>
|
||||
fn read_entity<T, R>(&self, handle: &Entity<T>, read: impl FnOnce(&T, &App) -> R) -> R
|
||||
where
|
||||
T: 'static;
|
||||
|
||||
@@ -189,7 +188,7 @@ pub trait AppContext {
|
||||
R: Send + 'static;
|
||||
|
||||
/// Read a global from this app context
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> Self::Result<R>
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
|
||||
where
|
||||
G: Global;
|
||||
}
|
||||
@@ -208,6 +207,9 @@ impl<T: 'static> Reservation<T> {
|
||||
/// This trait is used for the different visual contexts in GPUI that
|
||||
/// require a window to be present.
|
||||
pub trait VisualContext: AppContext {
|
||||
/// The result type for window operations.
|
||||
type Result<T>;
|
||||
|
||||
/// Returns the handle of the window associated with this context.
|
||||
fn window_handle(&self) -> AnyWindowHandle;
|
||||
|
||||
@@ -285,24 +287,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// A flatten equivalent for anyhow `Result`s.
|
||||
pub trait Flatten<T> {
|
||||
/// Convert this type into a simple `Result<T>`.
|
||||
fn flatten(self) -> Result<T>;
|
||||
}
|
||||
|
||||
impl<T> Flatten<T> for Result<Result<T>> {
|
||||
fn flatten(self) -> Result<T> {
|
||||
self?
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Flatten<T> for Result<T> {
|
||||
fn flatten(self) -> Result<T> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about the GPU GPUI is running on.
|
||||
#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone)]
|
||||
pub struct GpuSpecs {
|
||||
|
||||
+109
-3
@@ -17,6 +17,9 @@ pub trait KeyEvent: InputEvent {}
|
||||
/// A mouse event from the platform.
|
||||
pub trait MouseEvent: InputEvent {}
|
||||
|
||||
/// A gesture event from the platform.
|
||||
pub trait GestureEvent: InputEvent {}
|
||||
|
||||
/// The key down event equivalent for the platform.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct KeyDownEvent {
|
||||
@@ -174,6 +177,40 @@ pub struct MouseClickEvent {
|
||||
pub up: MouseUpEvent,
|
||||
}
|
||||
|
||||
/// The stage of a pressure click event.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub enum PressureStage {
|
||||
/// No pressure.
|
||||
#[default]
|
||||
Zero,
|
||||
/// Normal click pressure.
|
||||
Normal,
|
||||
/// High pressure, enough to trigger a force click.
|
||||
Force,
|
||||
}
|
||||
|
||||
/// A mouse pressure event from the platform. Generated when a force-sensitive trackpad is pressed hard.
|
||||
/// Currently only implemented for macOS trackpads.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MousePressureEvent {
|
||||
/// Pressure of the current stage as a float between 0 and 1
|
||||
pub pressure: f32,
|
||||
/// The pressure stage of the event.
|
||||
pub stage: PressureStage,
|
||||
/// The position of the mouse on the window.
|
||||
pub position: Point<Pixels>,
|
||||
/// The modifiers that were held down when the mouse pressure changed.
|
||||
pub modifiers: Modifiers,
|
||||
}
|
||||
|
||||
impl Sealed for MousePressureEvent {}
|
||||
impl InputEvent for MousePressureEvent {
|
||||
fn to_platform_input(self) -> PlatformInput {
|
||||
PlatformInput::MousePressure(self)
|
||||
}
|
||||
}
|
||||
impl MouseEvent for MousePressureEvent {}
|
||||
|
||||
/// A click event that was generated by a keyboard button being pressed and released.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct KeyboardClickEvent {
|
||||
@@ -250,6 +287,19 @@ impl ClickEvent {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns if this was a middle click
|
||||
///
|
||||
/// `Keyboard`: false
|
||||
/// `Mouse`: Whether the middle button was pressed and released
|
||||
pub fn is_middle_click(&self) -> bool {
|
||||
match self {
|
||||
ClickEvent::Keyboard(_) => false,
|
||||
ClickEvent::Mouse(event) => {
|
||||
event.down.button == MouseButton::Middle && event.up.button == MouseButton::Middle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the click was a standard click
|
||||
///
|
||||
/// `Keyboard`: Always true
|
||||
@@ -420,6 +470,51 @@ impl Default for ScrollDelta {
|
||||
}
|
||||
}
|
||||
|
||||
/// A pinch gesture event from the platform, generated when the user performs
|
||||
/// a pinch-to-zoom gesture (typically on a trackpad).
|
||||
///
|
||||
/// Note: This event is only available on macOS and Wayland (Linux).
|
||||
/// On Windows, pinch gestures are simulated as scroll wheel events with Ctrl held.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub struct PinchEvent {
|
||||
/// The position of the pinch center on the window.
|
||||
pub position: Point<Pixels>,
|
||||
|
||||
/// The zoom delta for this event.
|
||||
/// Positive values indicate zooming in, negative values indicate zooming out.
|
||||
/// For example, 0.1 represents a 10% zoom increase.
|
||||
pub delta: f32,
|
||||
|
||||
/// The modifiers that were held down during the pinch gesture.
|
||||
pub modifiers: Modifiers,
|
||||
|
||||
/// The phase of the pinch gesture.
|
||||
pub phase: TouchPhase,
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
impl Sealed for PinchEvent {}
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
impl InputEvent for PinchEvent {
|
||||
fn to_platform_input(self) -> PlatformInput {
|
||||
PlatformInput::Pinch(self)
|
||||
}
|
||||
}
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
impl GestureEvent for PinchEvent {}
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
impl MouseEvent for PinchEvent {}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
impl Deref for PinchEvent {
|
||||
type Target = Modifiers;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.modifiers
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollDelta {
|
||||
/// Returns true if this is a precise scroll delta in pixels.
|
||||
pub fn precise(&self) -> bool {
|
||||
@@ -510,7 +605,7 @@ impl Deref for MouseExitEvent {
|
||||
|
||||
/// A collection of paths from the platform, such as from a file drop.
|
||||
#[derive(Debug, Clone, Default, Eq, PartialEq)]
|
||||
pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>);
|
||||
pub struct ExternalPaths(pub SmallVec<[PathBuf; 2]>);
|
||||
|
||||
impl ExternalPaths {
|
||||
/// Convert this collection of paths into a slice.
|
||||
@@ -571,12 +666,17 @@ pub enum PlatformInput {
|
||||
MouseDown(MouseDownEvent),
|
||||
/// The mouse was released.
|
||||
MouseUp(MouseUpEvent),
|
||||
/// Mouse pressure.
|
||||
MousePressure(MousePressureEvent),
|
||||
/// The mouse was moved.
|
||||
MouseMove(MouseMoveEvent),
|
||||
/// The mouse exited the window.
|
||||
MouseExited(MouseExitEvent),
|
||||
/// The scroll wheel was used.
|
||||
ScrollWheel(ScrollWheelEvent),
|
||||
/// A pinch gesture was performed.
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
Pinch(PinchEvent),
|
||||
/// Files were dragged and dropped onto the window.
|
||||
FileDrop(FileDropEvent),
|
||||
}
|
||||
@@ -590,8 +690,11 @@ impl PlatformInput {
|
||||
PlatformInput::MouseDown(event) => Some(event),
|
||||
PlatformInput::MouseUp(event) => Some(event),
|
||||
PlatformInput::MouseMove(event) => Some(event),
|
||||
PlatformInput::MousePressure(event) => Some(event),
|
||||
PlatformInput::MouseExited(event) => Some(event),
|
||||
PlatformInput::ScrollWheel(event) => Some(event),
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
PlatformInput::Pinch(event) => Some(event),
|
||||
PlatformInput::FileDrop(event) => Some(event),
|
||||
}
|
||||
}
|
||||
@@ -604,8 +707,11 @@ impl PlatformInput {
|
||||
PlatformInput::MouseDown(_) => None,
|
||||
PlatformInput::MouseUp(_) => None,
|
||||
PlatformInput::MouseMove(_) => None,
|
||||
PlatformInput::MousePressure(_) => None,
|
||||
PlatformInput::MouseExited(_) => None,
|
||||
PlatformInput::ScrollWheel(_) => None,
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
PlatformInput::Pinch(_) => None,
|
||||
PlatformInput::FileDrop(_) => None,
|
||||
}
|
||||
}
|
||||
@@ -667,8 +773,8 @@ mod test {
|
||||
});
|
||||
|
||||
window
|
||||
.update(cx, |test_view, window, _cx| {
|
||||
window.focus(&test_view.focus_handle)
|
||||
.update(cx, |test_view, window, cx| {
|
||||
window.focus(&test_view.focus_handle, cx)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
|
||||
+297
-48
@@ -199,8 +199,8 @@ impl DispatchTree {
|
||||
if let Some(context) = node.context.clone() {
|
||||
self.context_stack.push(context);
|
||||
}
|
||||
if node.view_id.is_some() {
|
||||
self.view_stack.push(node.view_id.unwrap());
|
||||
if let Some(view_id) = node.view_id {
|
||||
self.view_stack.push(view_id);
|
||||
}
|
||||
self.node_stack.push(node_id);
|
||||
current_node_id = node.parent;
|
||||
@@ -462,6 +462,17 @@ impl DispatchTree {
|
||||
(bindings, partial, context_stack)
|
||||
}
|
||||
|
||||
/// Find the bindings that can follow the current input sequence.
|
||||
pub fn possible_next_bindings_for_input(
|
||||
&self,
|
||||
input: &[Keystroke],
|
||||
context_stack: &[KeyContext],
|
||||
) -> Vec<KeyBinding> {
|
||||
self.keymap
|
||||
.borrow()
|
||||
.possible_next_bindings_for_input(input, context_stack)
|
||||
}
|
||||
|
||||
/// dispatch_key processes the keystroke
|
||||
/// input should be set to the value of `pending` from the previous call to dispatch_key.
|
||||
/// This returns three instructions to the input handler:
|
||||
@@ -610,66 +621,38 @@ impl DispatchTree {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
self as gpui, DispatchResult, Element, ElementId, GlobalElementId, InspectorElementId,
|
||||
Keystroke, LayoutId, Style,
|
||||
self as gpui, AppContext, DispatchResult, Element, ElementId, GlobalElementId,
|
||||
InspectorElementId, Keystroke, LayoutId, Style,
|
||||
};
|
||||
use core::panic;
|
||||
use smallvec::SmallVec;
|
||||
use std::{cell::RefCell, ops::Range, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
Action, ActionRegistry, App, Bounds, Context, DispatchTree, FocusHandle, InputHandler,
|
||||
IntoElement, KeyBinding, KeyContext, Keymap, Pixels, Point, Render, TestAppContext,
|
||||
UTF16Selection, Window,
|
||||
ActionRegistry, App, Bounds, Context, DispatchTree, FocusHandle, InputHandler, IntoElement,
|
||||
KeyBinding, KeyContext, Keymap, Pixels, Point, Render, Subscription, TestAppContext,
|
||||
UTF16Selection, Unbind, Window,
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
struct TestAction;
|
||||
actions!(dispatch_test, [TestAction, SecondaryTestAction]);
|
||||
|
||||
impl Action for TestAction {
|
||||
fn name(&self) -> &'static str {
|
||||
"test::TestAction"
|
||||
}
|
||||
fn test_dispatch_tree(bindings: Vec<KeyBinding>) -> DispatchTree {
|
||||
let registry = ActionRegistry::default();
|
||||
|
||||
fn name_for_type() -> &'static str
|
||||
where
|
||||
Self: ::std::marker::Sized,
|
||||
{
|
||||
"test::TestAction"
|
||||
}
|
||||
|
||||
fn partial_eq(&self, action: &dyn Action) -> bool {
|
||||
action.as_any().downcast_ref::<Self>() == Some(self)
|
||||
}
|
||||
|
||||
fn boxed_clone(&self) -> std::boxed::Box<dyn Action> {
|
||||
Box::new(TestAction)
|
||||
}
|
||||
|
||||
fn build(_value: serde_json::Value) -> anyhow::Result<Box<dyn Action>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Ok(Box::new(TestAction))
|
||||
}
|
||||
DispatchTree::new(
|
||||
Rc::new(RefCell::new(Keymap::new(bindings))),
|
||||
Rc::new(registry),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keybinding_for_action_bounds() {
|
||||
let keymap = Keymap::new(vec![KeyBinding::new(
|
||||
let tree = test_dispatch_tree(vec![KeyBinding::new(
|
||||
"cmd-n",
|
||||
TestAction,
|
||||
Some("ProjectPanel"),
|
||||
)]);
|
||||
|
||||
let mut registry = ActionRegistry::default();
|
||||
|
||||
registry.load_action::<TestAction>();
|
||||
|
||||
let keymap = Rc::new(RefCell::new(keymap));
|
||||
|
||||
let tree = DispatchTree::new(keymap, Rc::new(registry));
|
||||
|
||||
let contexts = vec![
|
||||
KeyContext::parse("Workspace").unwrap(),
|
||||
KeyContext::parse("ProjectPanel").unwrap(),
|
||||
@@ -680,6 +663,67 @@ mod tests {
|
||||
assert!(keybinding[0].action.partial_eq(&TestAction))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bindings_for_action_hides_targeted_unbind_in_active_context() {
|
||||
let tree = test_dispatch_tree(vec![
|
||||
KeyBinding::new("tab", TestAction, Some("Editor")),
|
||||
KeyBinding::new(
|
||||
"tab",
|
||||
Unbind("dispatch_test::TestAction".into()),
|
||||
Some("Editor && edit_prediction"),
|
||||
),
|
||||
KeyBinding::new(
|
||||
"tab",
|
||||
SecondaryTestAction,
|
||||
Some("Editor && showing_completions"),
|
||||
),
|
||||
]);
|
||||
|
||||
let contexts = vec![
|
||||
KeyContext::parse("Workspace").unwrap(),
|
||||
KeyContext::parse("Editor showing_completions edit_prediction").unwrap(),
|
||||
];
|
||||
|
||||
let bindings = tree.bindings_for_action(&TestAction, &contexts);
|
||||
assert!(bindings.is_empty());
|
||||
|
||||
let highest = tree.highest_precedence_binding_for_action(&TestAction, &contexts);
|
||||
assert!(highest.is_none());
|
||||
|
||||
let fallback_bindings = tree.bindings_for_action(&SecondaryTestAction, &contexts);
|
||||
assert_eq!(fallback_bindings.len(), 1);
|
||||
assert!(fallback_bindings[0].action.partial_eq(&SecondaryTestAction));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bindings_for_action_keeps_targeted_binding_outside_unbind_context() {
|
||||
let tree = test_dispatch_tree(vec![
|
||||
KeyBinding::new("tab", TestAction, Some("Editor")),
|
||||
KeyBinding::new(
|
||||
"tab",
|
||||
Unbind("dispatch_test::TestAction".into()),
|
||||
Some("Editor && edit_prediction"),
|
||||
),
|
||||
KeyBinding::new(
|
||||
"tab",
|
||||
SecondaryTestAction,
|
||||
Some("Editor && showing_completions"),
|
||||
),
|
||||
]);
|
||||
|
||||
let contexts = vec![
|
||||
KeyContext::parse("Workspace").unwrap(),
|
||||
KeyContext::parse("Editor").unwrap(),
|
||||
];
|
||||
|
||||
let bindings = tree.bindings_for_action(&TestAction, &contexts);
|
||||
assert_eq!(bindings.len(), 1);
|
||||
assert!(bindings[0].action.partial_eq(&TestAction));
|
||||
|
||||
let highest = tree.highest_precedence_binding_for_action(&TestAction, &contexts);
|
||||
assert!(highest.is_some_and(|binding| binding.action.partial_eq(&TestAction)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pending_has_binding_state() {
|
||||
let bindings = vec![
|
||||
@@ -687,10 +731,7 @@ mod tests {
|
||||
KeyBinding::new("space", TestAction, Some("ContextA")),
|
||||
KeyBinding::new("space f g", TestAction, Some("ContextB")),
|
||||
];
|
||||
let keymap = Rc::new(RefCell::new(Keymap::new(bindings)));
|
||||
let mut registry = ActionRegistry::default();
|
||||
registry.load_action::<TestAction>();
|
||||
let mut tree = DispatchTree::new(keymap, Rc::new(registry));
|
||||
let mut tree = test_dispatch_tree(bindings);
|
||||
|
||||
type DispatchPath = SmallVec<[super::DispatchNodeId; 32]>;
|
||||
fn dispatch(
|
||||
@@ -723,6 +764,213 @@ mod tests {
|
||||
assert!(!result.pending_has_binding);
|
||||
}
|
||||
|
||||
#[crate::test]
|
||||
fn test_pending_input_observers_notified_on_focus_change(cx: &mut TestAppContext) {
|
||||
#[derive(Clone)]
|
||||
struct CustomElement {
|
||||
focus_handle: FocusHandle,
|
||||
text: Rc<RefCell<String>>,
|
||||
}
|
||||
|
||||
impl CustomElement {
|
||||
fn new(cx: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
text: Rc::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for CustomElement {
|
||||
type RequestLayoutState = ();
|
||||
|
||||
type PrepaintState = ();
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
Some("custom".into())
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_: Option<&GlobalElementId>,
|
||||
_: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
(window.request_layout(Style::default(), [], cx), ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_: Option<&GlobalElementId>,
|
||||
_: Option<&InspectorElementId>,
|
||||
_: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self::PrepaintState {
|
||||
window.set_focus_handle(&self.focus_handle, cx);
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_: Option<&GlobalElementId>,
|
||||
_: Option<&InspectorElementId>,
|
||||
_: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
_: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let mut key_context = KeyContext::default();
|
||||
key_context.add("Terminal");
|
||||
window.set_key_context(key_context);
|
||||
window.handle_input(&self.focus_handle, self.clone(), cx);
|
||||
window.on_action(std::any::TypeId::of::<TestAction>(), |_, _, _, _| {});
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for CustomElement {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl InputHandler for CustomElement {
|
||||
fn selected_text_range(
|
||||
&mut self,
|
||||
_: bool,
|
||||
_: &mut Window,
|
||||
_: &mut App,
|
||||
) -> Option<UTF16Selection> {
|
||||
None
|
||||
}
|
||||
|
||||
fn marked_text_range(&mut self, _: &mut Window, _: &mut App) -> Option<Range<usize>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn text_for_range(
|
||||
&mut self,
|
||||
_: Range<usize>,
|
||||
_: &mut Option<Range<usize>>,
|
||||
_: &mut Window,
|
||||
_: &mut App,
|
||||
) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn replace_text_in_range(
|
||||
&mut self,
|
||||
replacement_range: Option<Range<usize>>,
|
||||
text: &str,
|
||||
_: &mut Window,
|
||||
_: &mut App,
|
||||
) {
|
||||
if replacement_range.is_some() {
|
||||
unimplemented!()
|
||||
}
|
||||
self.text.borrow_mut().push_str(text)
|
||||
}
|
||||
|
||||
fn replace_and_mark_text_in_range(
|
||||
&mut self,
|
||||
replacement_range: Option<Range<usize>>,
|
||||
new_text: &str,
|
||||
_: Option<Range<usize>>,
|
||||
_: &mut Window,
|
||||
_: &mut App,
|
||||
) {
|
||||
if replacement_range.is_some() {
|
||||
unimplemented!()
|
||||
}
|
||||
self.text.borrow_mut().push_str(new_text)
|
||||
}
|
||||
|
||||
fn unmark_text(&mut self, _: &mut Window, _: &mut App) {}
|
||||
|
||||
fn bounds_for_range(
|
||||
&mut self,
|
||||
_: Range<usize>,
|
||||
_: &mut Window,
|
||||
_: &mut App,
|
||||
) -> Option<Bounds<Pixels>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn character_index_for_point(
|
||||
&mut self,
|
||||
_: Point<Pixels>,
|
||||
_: &mut Window,
|
||||
_: &mut App,
|
||||
) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for CustomElement {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.bind_keys([KeyBinding::new("ctrl-b", TestAction, Some("Terminal"))]);
|
||||
cx.bind_keys([KeyBinding::new("ctrl-b h", TestAction, Some("Terminal"))]);
|
||||
});
|
||||
|
||||
let (test, cx) = cx.add_window_view(|_, cx| CustomElement::new(cx));
|
||||
let focus_handle = test.update(cx, |test, _| test.focus_handle.clone());
|
||||
|
||||
let pending_input_changed_count = Rc::new(RefCell::new(0usize));
|
||||
let pending_input_changed_count_for_observer = pending_input_changed_count.clone();
|
||||
|
||||
struct PendingInputObserver {
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
let _observer = cx.update(|window, cx| {
|
||||
cx.new(|cx| PendingInputObserver {
|
||||
_subscription: cx.observe_pending_input(window, move |_, _, _| {
|
||||
*pending_input_changed_count_for_observer.borrow_mut() += 1;
|
||||
}),
|
||||
})
|
||||
});
|
||||
|
||||
cx.update(|window, cx| {
|
||||
window.focus(&focus_handle, cx);
|
||||
window.activate_window();
|
||||
});
|
||||
|
||||
cx.simulate_keystrokes("ctrl-b");
|
||||
|
||||
let count_after_pending = Rc::new(RefCell::new(0usize));
|
||||
let count_after_pending_for_assertion = count_after_pending.clone();
|
||||
|
||||
cx.update(|window, cx| {
|
||||
assert!(window.has_pending_keystrokes());
|
||||
*count_after_pending.borrow_mut() = *pending_input_changed_count.borrow();
|
||||
assert!(*count_after_pending.borrow() > 0);
|
||||
|
||||
window.focus(&cx.focus_handle(), cx);
|
||||
|
||||
assert!(!window.has_pending_keystrokes());
|
||||
});
|
||||
|
||||
// Focus-triggered pending-input notifications are deferred to the end of the current
|
||||
// effect cycle, so the observer callback should run after the focus update completes.
|
||||
cx.update(|_, _| {
|
||||
let count_after_focus_change = *pending_input_changed_count.borrow();
|
||||
assert!(count_after_focus_change > *count_after_pending_for_assertion.borrow());
|
||||
});
|
||||
}
|
||||
|
||||
#[crate::test]
|
||||
fn test_input_handler_pending(cx: &mut TestAppContext) {
|
||||
#[derive(Clone)]
|
||||
@@ -876,8 +1124,9 @@ mod tests {
|
||||
cx.bind_keys([KeyBinding::new("ctrl-b h", TestAction, Some("Terminal"))]);
|
||||
});
|
||||
let (test, cx) = cx.add_window_view(|_, cx| CustomElement::new(cx));
|
||||
let focus_handle = test.update(cx, |test, _| test.focus_handle.clone());
|
||||
cx.update(|window, cx| {
|
||||
window.focus(&test.read(cx).focus_handle);
|
||||
window.focus(&focus_handle, cx);
|
||||
window.activate_window();
|
||||
});
|
||||
cx.simulate_keystrokes("ctrl-b [");
|
||||
|
||||
+163
-20
@@ -4,7 +4,7 @@ mod context;
|
||||
pub use binding::*;
|
||||
pub use context::*;
|
||||
|
||||
use crate::{Action, AsKeystroke, Keystroke, is_no_action};
|
||||
use crate::{Action, AsKeystroke, Keystroke, Unbind, is_no_action, is_unbind};
|
||||
use collections::{HashMap, HashSet};
|
||||
use smallvec::SmallVec;
|
||||
use std::any::TypeId;
|
||||
@@ -19,7 +19,7 @@ pub struct KeymapVersion(usize);
|
||||
pub struct Keymap {
|
||||
bindings: Vec<KeyBinding>,
|
||||
binding_indices_by_action_id: HashMap<TypeId, SmallVec<[usize; 3]>>,
|
||||
no_action_binding_indices: Vec<usize>,
|
||||
disabled_binding_indices: Vec<usize>,
|
||||
version: KeymapVersion,
|
||||
}
|
||||
|
||||
@@ -27,6 +27,26 @@ pub struct Keymap {
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub struct BindingIndex(usize);
|
||||
|
||||
fn disabled_binding_matches_context(disabled_binding: &KeyBinding, binding: &KeyBinding) -> bool {
|
||||
match (
|
||||
&disabled_binding.context_predicate,
|
||||
&binding.context_predicate,
|
||||
) {
|
||||
(None, _) => true,
|
||||
(Some(_), None) => false,
|
||||
(Some(disabled_predicate), Some(predicate)) => disabled_predicate.is_superset(predicate),
|
||||
}
|
||||
}
|
||||
|
||||
fn binding_is_unbound(disabled_binding: &KeyBinding, binding: &KeyBinding) -> bool {
|
||||
disabled_binding.keystrokes == binding.keystrokes
|
||||
&& disabled_binding
|
||||
.action()
|
||||
.as_any()
|
||||
.downcast_ref::<Unbind>()
|
||||
.is_some_and(|unbind| unbind.0.as_ref() == binding.action.name())
|
||||
}
|
||||
|
||||
impl Keymap {
|
||||
/// Create a new keymap with the given bindings.
|
||||
pub fn new(bindings: Vec<KeyBinding>) -> Self {
|
||||
@@ -44,8 +64,8 @@ impl Keymap {
|
||||
pub fn add_bindings<T: IntoIterator<Item = KeyBinding>>(&mut self, bindings: T) {
|
||||
for binding in bindings {
|
||||
let action_id = binding.action().as_any().type_id();
|
||||
if is_no_action(&*binding.action) {
|
||||
self.no_action_binding_indices.push(self.bindings.len());
|
||||
if is_no_action(&*binding.action) || is_unbind(&*binding.action) {
|
||||
self.disabled_binding_indices.push(self.bindings.len());
|
||||
} else {
|
||||
self.binding_indices_by_action_id
|
||||
.entry(action_id)
|
||||
@@ -62,7 +82,7 @@ impl Keymap {
|
||||
pub fn clear(&mut self) {
|
||||
self.bindings.clear();
|
||||
self.binding_indices_by_action_id.clear();
|
||||
self.no_action_binding_indices.clear();
|
||||
self.disabled_binding_indices.clear();
|
||||
self.version.0 += 1;
|
||||
}
|
||||
|
||||
@@ -90,21 +110,22 @@ impl Keymap {
|
||||
return None;
|
||||
}
|
||||
|
||||
for null_ix in &self.no_action_binding_indices {
|
||||
if null_ix > ix {
|
||||
let null_binding = &self.bindings[*null_ix];
|
||||
if null_binding.keystrokes == binding.keystrokes {
|
||||
let null_binding_matches =
|
||||
match (&null_binding.context_predicate, &binding.context_predicate) {
|
||||
(None, _) => true,
|
||||
(Some(_), None) => false,
|
||||
(Some(null_predicate), Some(predicate)) => {
|
||||
null_predicate.is_superset(predicate)
|
||||
}
|
||||
};
|
||||
if null_binding_matches {
|
||||
for disabled_ix in &self.disabled_binding_indices {
|
||||
if disabled_ix > ix {
|
||||
let disabled_binding = &self.bindings[*disabled_ix];
|
||||
if disabled_binding.keystrokes != binding.keystrokes {
|
||||
continue;
|
||||
}
|
||||
|
||||
if is_no_action(&*disabled_binding.action) {
|
||||
if disabled_binding_matches_context(disabled_binding, binding) {
|
||||
return None;
|
||||
}
|
||||
} else if is_unbind(&*disabled_binding.action)
|
||||
&& disabled_binding_matches_context(disabled_binding, binding)
|
||||
&& binding_is_unbound(disabled_binding, binding)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,6 +191,7 @@ impl Keymap {
|
||||
|
||||
let mut bindings: SmallVec<[_; 1]> = SmallVec::new();
|
||||
let mut first_binding_index = None;
|
||||
let mut unbound_bindings: Vec<&KeyBinding> = Vec::new();
|
||||
|
||||
for (_, ix, binding) in matched_bindings {
|
||||
if is_no_action(&*binding.action) {
|
||||
@@ -186,6 +208,19 @@ impl Keymap {
|
||||
// For non-user NoAction bindings, continue searching for user overrides
|
||||
continue;
|
||||
}
|
||||
|
||||
if is_unbind(&*binding.action) {
|
||||
unbound_bindings.push(binding);
|
||||
continue;
|
||||
}
|
||||
|
||||
if unbound_bindings
|
||||
.iter()
|
||||
.any(|disabled_binding| binding_is_unbound(disabled_binding, binding))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bindings.push(binding.clone());
|
||||
first_binding_index.get_or_insert(ix);
|
||||
}
|
||||
@@ -197,7 +232,7 @@ impl Keymap {
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if is_no_action(&*binding.action) {
|
||||
if is_no_action(&*binding.action) || is_unbind(&*binding.action) {
|
||||
pending.remove(&&binding.keystrokes);
|
||||
continue;
|
||||
}
|
||||
@@ -215,13 +250,51 @@ impl Keymap {
|
||||
Some(contexts.len())
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the bindings that can follow the current input sequence.
|
||||
pub fn possible_next_bindings_for_input(
|
||||
&self,
|
||||
input: &[Keystroke],
|
||||
context_stack: &[KeyContext],
|
||||
) -> Vec<KeyBinding> {
|
||||
let mut bindings = self
|
||||
.bindings()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.filter_map(|(ix, binding)| {
|
||||
let depth = self.binding_enabled(binding, context_stack)?;
|
||||
let pending = binding.match_keystrokes(input);
|
||||
match pending {
|
||||
None => None,
|
||||
Some(is_pending) => {
|
||||
if !is_pending
|
||||
|| is_no_action(&*binding.action)
|
||||
|| is_unbind(&*binding.action)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some((depth, BindingIndex(ix), binding))
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
bindings.sort_by(|(depth_a, ix_a, _), (depth_b, ix_b, _)| {
|
||||
depth_b.cmp(depth_a).then(ix_b.cmp(ix_a))
|
||||
});
|
||||
|
||||
bindings
|
||||
.into_iter()
|
||||
.map(|(_, _, binding)| binding.clone())
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate as gpui;
|
||||
use gpui::NoAction;
|
||||
use gpui::{NoAction, Unbind};
|
||||
|
||||
actions!(
|
||||
test_only,
|
||||
@@ -685,6 +758,76 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_targeted_unbind_ignores_target_context() {
|
||||
let bindings = [
|
||||
KeyBinding::new("tab", ActionAlpha {}, Some("Editor")),
|
||||
KeyBinding::new("tab", ActionBeta {}, Some("Editor && showing_completions")),
|
||||
KeyBinding::new(
|
||||
"tab",
|
||||
Unbind("test_only::ActionAlpha".into()),
|
||||
Some("Editor && edit_prediction"),
|
||||
),
|
||||
];
|
||||
|
||||
let mut keymap = Keymap::default();
|
||||
keymap.add_bindings(bindings);
|
||||
|
||||
let (result, pending) = keymap.bindings_for_input(
|
||||
&[Keystroke::parse("tab").unwrap()],
|
||||
&[KeyContext::parse("Editor showing_completions edit_prediction").unwrap()],
|
||||
);
|
||||
|
||||
assert!(!pending);
|
||||
assert_eq!(result.len(), 1);
|
||||
assert!(result[0].action.partial_eq(&ActionBeta {}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bindings_for_action_keeps_binding_for_narrower_targeted_unbind() {
|
||||
let bindings = [
|
||||
KeyBinding::new("tab", ActionAlpha {}, Some("Editor")),
|
||||
KeyBinding::new(
|
||||
"tab",
|
||||
Unbind("test_only::ActionAlpha".into()),
|
||||
Some("Editor && edit_prediction"),
|
||||
),
|
||||
KeyBinding::new("tab", ActionBeta {}, Some("Editor && showing_completions")),
|
||||
];
|
||||
|
||||
let mut keymap = Keymap::default();
|
||||
keymap.add_bindings(bindings);
|
||||
|
||||
assert_bindings(&keymap, &ActionAlpha {}, &["tab"]);
|
||||
assert_bindings(&keymap, &ActionBeta {}, &["tab"]);
|
||||
|
||||
#[track_caller]
|
||||
fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) {
|
||||
let actual = keymap
|
||||
.bindings_for_action(action)
|
||||
.map(|binding| binding.keystrokes[0].inner().unparse())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(actual, expected, "{:?}", action);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bindings_for_action_removes_binding_for_broader_targeted_unbind() {
|
||||
let bindings = [
|
||||
KeyBinding::new("tab", ActionAlpha {}, Some("Editor && edit_prediction")),
|
||||
KeyBinding::new(
|
||||
"tab",
|
||||
Unbind("test_only::ActionAlpha".into()),
|
||||
Some("Editor"),
|
||||
),
|
||||
];
|
||||
|
||||
let mut keymap = Keymap::default();
|
||||
keymap.add_bindings(bindings);
|
||||
|
||||
assert!(keymap.bindings_for_action(&ActionAlpha {}).next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_source_precedence_sorting() {
|
||||
// KeybindSource precedence: User (0) > Vim (1) > Base (2) > Default (3)
|
||||
|
||||
+139
-8
@@ -199,13 +199,20 @@ pub enum KeyBindingContextPredicate {
|
||||
impl fmt::Display for KeyBindingContextPredicate {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Identifier(name) => write!(f, "{}", name),
|
||||
Self::Equal(left, right) => write!(f, "{} == {}", left, right),
|
||||
Self::NotEqual(left, right) => write!(f, "{} != {}", left, right),
|
||||
Self::Not(pred) => write!(f, "!{}", pred),
|
||||
Self::Descendant(parent, child) => write!(f, "{} > {}", parent, child),
|
||||
Self::And(left, right) => write!(f, "({} && {})", left, right),
|
||||
Self::Or(left, right) => write!(f, "({} || {})", left, right),
|
||||
Self::Identifier(name) => write!(f, "{name}"),
|
||||
Self::Equal(left, right) => write!(f, "{left} == {right}"),
|
||||
Self::NotEqual(left, right) => write!(f, "{left} != {right}"),
|
||||
Self::Descendant(parent, child) => write!(f, "{parent} > {child}"),
|
||||
Self::Not(pred) => match pred.as_ref() {
|
||||
Self::Identifier(name) => write!(f, "!{name}"),
|
||||
_ => write!(f, "!({pred})"),
|
||||
},
|
||||
Self::And(..) => self.fmt_joined(f, " && ", LogicalOperator::And, |node| {
|
||||
matches!(node, Self::Or(..))
|
||||
}),
|
||||
Self::Or(..) => self.fmt_joined(f, " || ", LogicalOperator::Or, |node| {
|
||||
matches!(node, Self::And(..))
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -262,7 +269,7 @@ impl KeyBindingContextPredicate {
|
||||
|
||||
/// Eval a predicate against a set of contexts, arranged from lowest to highest.
|
||||
#[allow(unused)]
|
||||
pub(crate) fn eval(&self, contexts: &[KeyContext]) -> bool {
|
||||
pub fn eval(&self, contexts: &[KeyContext]) -> bool {
|
||||
self.eval_inner(contexts, contexts)
|
||||
}
|
||||
|
||||
@@ -436,6 +443,52 @@ impl KeyBindingContextPredicate {
|
||||
anyhow::bail!("operands of != must be identifiers");
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_joined(
|
||||
&self,
|
||||
f: &mut fmt::Formatter<'_>,
|
||||
separator: &str,
|
||||
operator: LogicalOperator,
|
||||
needs_parens: impl Fn(&Self) -> bool + Copy,
|
||||
) -> fmt::Result {
|
||||
let mut first = true;
|
||||
self.fmt_joined_inner(f, separator, operator, needs_parens, &mut first)
|
||||
}
|
||||
|
||||
fn fmt_joined_inner(
|
||||
&self,
|
||||
f: &mut fmt::Formatter<'_>,
|
||||
separator: &str,
|
||||
operator: LogicalOperator,
|
||||
needs_parens: impl Fn(&Self) -> bool + Copy,
|
||||
first: &mut bool,
|
||||
) -> fmt::Result {
|
||||
match (operator, self) {
|
||||
(LogicalOperator::And, Self::And(left, right))
|
||||
| (LogicalOperator::Or, Self::Or(left, right)) => {
|
||||
left.fmt_joined_inner(f, separator, operator, needs_parens, first)?;
|
||||
right.fmt_joined_inner(f, separator, operator, needs_parens, first)
|
||||
}
|
||||
(_, node) => {
|
||||
if !*first {
|
||||
f.write_str(separator)?;
|
||||
}
|
||||
*first = false;
|
||||
|
||||
if needs_parens(node) {
|
||||
write!(f, "({node})")
|
||||
} else {
|
||||
write!(f, "{node}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum LogicalOperator {
|
||||
And,
|
||||
Or,
|
||||
}
|
||||
|
||||
const PRECEDENCE_CHILD: u32 = 1;
|
||||
@@ -757,4 +810,82 @@ mod tests {
|
||||
assert!(not_workspace.eval(slice::from_ref(&editor_context)));
|
||||
assert!(!not_workspace.eval(&workspace_pane_editor));
|
||||
}
|
||||
|
||||
// MARK: - Display
|
||||
|
||||
#[test]
|
||||
fn test_context_display() {
|
||||
fn ident(s: &str) -> Box<KeyBindingContextPredicate> {
|
||||
Box::new(Identifier(SharedString::new(s)))
|
||||
}
|
||||
fn eq(a: &str, b: &str) -> Box<KeyBindingContextPredicate> {
|
||||
Box::new(Equal(SharedString::new(a), SharedString::new(b)))
|
||||
}
|
||||
fn not_eq(a: &str, b: &str) -> Box<KeyBindingContextPredicate> {
|
||||
Box::new(NotEqual(SharedString::new(a), SharedString::new(b)))
|
||||
}
|
||||
fn and(
|
||||
a: Box<KeyBindingContextPredicate>,
|
||||
b: Box<KeyBindingContextPredicate>,
|
||||
) -> Box<KeyBindingContextPredicate> {
|
||||
Box::new(And(a, b))
|
||||
}
|
||||
fn or(
|
||||
a: Box<KeyBindingContextPredicate>,
|
||||
b: Box<KeyBindingContextPredicate>,
|
||||
) -> Box<KeyBindingContextPredicate> {
|
||||
Box::new(Or(a, b))
|
||||
}
|
||||
fn descendant(
|
||||
a: Box<KeyBindingContextPredicate>,
|
||||
b: Box<KeyBindingContextPredicate>,
|
||||
) -> Box<KeyBindingContextPredicate> {
|
||||
Box::new(Descendant(a, b))
|
||||
}
|
||||
fn not(a: Box<KeyBindingContextPredicate>) -> Box<KeyBindingContextPredicate> {
|
||||
Box::new(Not(a))
|
||||
}
|
||||
|
||||
let test_cases = [
|
||||
(ident("a"), "a"),
|
||||
(eq("a", "b"), "a == b"),
|
||||
(not_eq("a", "b"), "a != b"),
|
||||
(descendant(ident("a"), ident("b")), "a > b"),
|
||||
(not(ident("a")), "!a"),
|
||||
(not_eq("a", "b"), "a != b"),
|
||||
(descendant(ident("a"), ident("b")), "a > b"),
|
||||
(not(and(ident("a"), ident("b"))), "!(a && b)"),
|
||||
(not(or(ident("a"), ident("b"))), "!(a || b)"),
|
||||
(and(ident("a"), ident("b")), "a && b"),
|
||||
(and(and(ident("a"), ident("b")), ident("c")), "a && b && c"),
|
||||
(or(ident("a"), ident("b")), "a || b"),
|
||||
(or(or(ident("a"), ident("b")), ident("c")), "a || b || c"),
|
||||
(or(ident("a"), and(ident("b"), ident("c"))), "a || (b && c)"),
|
||||
(
|
||||
and(
|
||||
and(
|
||||
and(ident("a"), eq("b", "c")),
|
||||
not(descendant(ident("d"), ident("e"))),
|
||||
),
|
||||
eq("f", "g"),
|
||||
),
|
||||
"a && b == c && !(d > e) && f == g",
|
||||
),
|
||||
(
|
||||
and(and(ident("a"), or(ident("b"), ident("c"))), ident("d")),
|
||||
"a && (b || c) && d",
|
||||
),
|
||||
(
|
||||
or(or(ident("a"), and(ident("b"), ident("c"))), ident("d")),
|
||||
"a || (b && c) || d",
|
||||
),
|
||||
];
|
||||
|
||||
for (predicate, expected) in test_cases {
|
||||
let actual = predicate.to_string();
|
||||
assert_eq!(actual, expected);
|
||||
let parsed = KeyBindingContextPredicate::parse(&actual).unwrap();
|
||||
assert_eq!(parsed, *predicate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
pub use util::*;
|
||||
|
||||
/// A helper trait for building complex objects with imperative conditionals in a fluent style.
|
||||
pub trait FluentBuilder {
|
||||
/// Imperatively modify self with the given closure.
|
||||
@@ -112,19 +110,37 @@ impl<T: Future> Future for WithTimeout<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
/// Uses smol executor to run a given future no longer than the timeout specified.
|
||||
/// Note that this won't "rewind" on `cx.executor().advance_clock` call, truly waiting for the timeout to elapse.
|
||||
pub async fn smol_timeout<F, T>(timeout: Duration, f: F) -> Result<T, ()>
|
||||
where
|
||||
F: Future<Output = T>,
|
||||
{
|
||||
let timer = async {
|
||||
smol::Timer::after(timeout).await;
|
||||
Err(())
|
||||
};
|
||||
let future = async move { Ok(f.await) };
|
||||
smol::future::FutureExt::race(timer, future).await
|
||||
/// Utilities for creating and managing shell commands.
|
||||
pub mod command {
|
||||
use smol::process::Command as SmolCommand;
|
||||
use std::ffi::OsStr;
|
||||
use std::process::Command as StdCommand;
|
||||
|
||||
/// Creates a new `smol::process::Command` with platform-specific defaults.
|
||||
pub fn new_command(program: impl AsRef<OsStr>) -> SmolCommand {
|
||||
let mut cmd = SmolCommand::new(program);
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use smol::process::windows::CommandExt;
|
||||
cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
|
||||
}
|
||||
cmd
|
||||
}
|
||||
|
||||
/// Creates a new `std::process::Command` with platform-specific defaults.
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn new_std_command(program: impl AsRef<OsStr>) -> StdCommand {
|
||||
StdCommand::new(program)
|
||||
}
|
||||
|
||||
/// Creates a new `std::process::Command` with platform-specific defaults.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn new_std_command(program: impl AsRef<OsStr>) -> StdCommand {
|
||||
use std::os::windows::process::CommandExt;
|
||||
let mut cmd = StdCommand::new(program);
|
||||
cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
|
||||
cmd
|
||||
}
|
||||
}
|
||||
|
||||
/// Increment the given atomic counter if it is not zero.
|
||||
+305
-141
@@ -2,51 +2,62 @@ mod app_menu;
|
||||
mod keyboard;
|
||||
mod keystroke;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
mod linux;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod mac;
|
||||
pub(crate) mod mac;
|
||||
|
||||
#[cfg(any(
|
||||
all(
|
||||
any(target_os = "linux", target_os = "freebsd"),
|
||||
any(feature = "x11", feature = "wayland")
|
||||
),
|
||||
all(target_os = "macos", feature = "macos-blade")
|
||||
))]
|
||||
mod blade;
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
pub(crate) mod linux;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) mod windows;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd", target_family = "wasm"))]
|
||||
pub(crate) mod wgpu;
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub(crate) mod web;
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "wayland"))]
|
||||
#[expect(missing_docs)]
|
||||
pub mod layer_shell;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
mod test;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows;
|
||||
#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
|
||||
mod visual_test;
|
||||
|
||||
#[cfg(all(
|
||||
feature = "screen-capture",
|
||||
any(
|
||||
target_os = "windows",
|
||||
all(
|
||||
any(target_os = "linux", target_os = "freebsd"),
|
||||
any(feature = "wayland", feature = "x11"),
|
||||
)
|
||||
)
|
||||
any(target_os = "windows", target_os = "linux", target_os = "freebsd",)
|
||||
))]
|
||||
pub(crate) mod scap_screen_capture;
|
||||
pub mod scap_screen_capture;
|
||||
|
||||
#[cfg(all(
|
||||
any(target_os = "windows", target_os = "linux"),
|
||||
feature = "screen-capture"
|
||||
))]
|
||||
pub(crate) type PlatformScreenCaptureFrame = scap::frame::Frame;
|
||||
#[cfg(not(feature = "screen-capture"))]
|
||||
pub(crate) type PlatformScreenCaptureFrame = ();
|
||||
#[cfg(all(target_os = "macos", feature = "screen-capture"))]
|
||||
pub(crate) type PlatformScreenCaptureFrame = core_video::image_buffer::CVImageBuffer;
|
||||
|
||||
use crate::scheduler::Instant;
|
||||
pub use crate::scheduler::RunnableMeta;
|
||||
use crate::{
|
||||
Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds,
|
||||
DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun,
|
||||
ForegroundExecutor, GlyphId, GpuSpecs, ImageSource, Keymap, LineLayout, Pixels, PlatformInput,
|
||||
Point, Priority, RealtimePriority, RenderGlyphParams, RenderImage, RenderImageParams,
|
||||
RenderSvgParams, Scene, ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer,
|
||||
SystemWindowTab, Task, TaskLabel, TaskTiming, ThreadTaskTimings, Window, WindowControlArea,
|
||||
hash, point, px, size,
|
||||
Point, Priority, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Scene,
|
||||
ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer, SystemWindowTab, Task,
|
||||
ThreadTaskTimings, Window, WindowControlArea, hash, point, px, size,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use async_task::Runnable;
|
||||
use futures::channel::oneshot;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use image::RgbaImage;
|
||||
use image::codecs::gif::GifDecoder;
|
||||
use image::{AnimationDecoder as _, Frame};
|
||||
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
|
||||
@@ -58,7 +69,7 @@ use std::borrow::Cow;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::io::Cursor;
|
||||
use std::ops;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
fmt::{self, Debug},
|
||||
ops::Range,
|
||||
@@ -73,67 +84,93 @@ pub use app_menu::*;
|
||||
pub use keyboard::*;
|
||||
pub use keystroke::*;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
pub(crate) use linux::*;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) use mac::*;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) use test::*;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) use windows::*;
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "wayland"))]
|
||||
pub use linux::layer_shell;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream};
|
||||
|
||||
#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
|
||||
pub use visual_test::VisualTestPlatform;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use mac::MacPlatform;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use windows::WindowsPlatform;
|
||||
|
||||
/// Returns the default [`Platform`] for the current OS.
|
||||
pub fn current_platform(headless: bool) -> Rc<dyn Platform> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Rc::new(mac::MacPlatform::new(headless))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
Rc::new(
|
||||
windows::WindowsPlatform::new(headless).expect("failed to initialize Windows platform"),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
{
|
||||
linux::current_platform(headless)
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
{
|
||||
let _ = headless;
|
||||
Rc::new(web::WebPlatform::new(true))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a background executor for the current platform.
|
||||
pub fn background_executor() -> BackgroundExecutor {
|
||||
pub fn background_executor() -> crate::BackgroundExecutor {
|
||||
current_platform(true).background_executor()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) fn current_platform(headless: bool) -> Rc<dyn Platform> {
|
||||
Rc::new(MacPlatform::new(headless))
|
||||
/// Creates a new application with the current platform.
|
||||
pub fn application() -> crate::Application {
|
||||
crate::Application::with_platform(current_platform(false))
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
pub(crate) fn current_platform(headless: bool) -> Rc<dyn Platform> {
|
||||
#[cfg(feature = "x11")]
|
||||
use anyhow::Context as _;
|
||||
/// Creates a new headless application.
|
||||
pub fn headless() -> crate::Application {
|
||||
crate::Application::with_platform(current_platform(true))
|
||||
}
|
||||
|
||||
if headless {
|
||||
return Rc::new(HeadlessClient::new());
|
||||
/// Unlike `application`, this function returns a single-threaded web application.
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub fn single_threaded_web() -> crate::Application {
|
||||
crate::Application::with_platform(Rc::new(web::WebPlatform::new(false)))
|
||||
}
|
||||
|
||||
/// Initializes panic hooks and logging for the web platform.
|
||||
/// Call this before running the application in a wasm_bindgen entrypoint.
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub fn web_init() {
|
||||
console_error_panic_hook::set_once();
|
||||
web::init_logging();
|
||||
}
|
||||
|
||||
/// Returns a new headless renderer for the current platform, if available.
|
||||
#[cfg(feature = "test-support")]
|
||||
pub fn current_headless_renderer() -> Option<Box<dyn crate::PlatformHeadlessRenderer>> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Some(Box::new(mac::metal_renderer::MetalHeadlessRenderer::new()))
|
||||
}
|
||||
|
||||
match guess_compositor() {
|
||||
#[cfg(feature = "wayland")]
|
||||
"Wayland" => Rc::new(WaylandClient::new()),
|
||||
|
||||
#[cfg(feature = "x11")]
|
||||
"X11" => Rc::new(
|
||||
X11Client::new()
|
||||
.context("Failed to initialize X11 client.")
|
||||
.unwrap(),
|
||||
),
|
||||
|
||||
"Headless" => Rc::new(HeadlessClient::new()),
|
||||
_ => unreachable!(),
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn current_platform(_headless: bool) -> Rc<dyn Platform> {
|
||||
Rc::new(
|
||||
WindowsPlatform::new()
|
||||
.inspect_err(|err| show_error("Failed to launch", err.to_string()))
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
// TODO(jk): return an enum instead of a string
|
||||
/// Return which compositor we're guessing we'll use.
|
||||
/// Does not attempt to connect to the given compositor
|
||||
/// Does not attempt to connect to the given compositor.
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
#[inline]
|
||||
pub fn guess_compositor() -> &'static str {
|
||||
@@ -163,7 +200,8 @@ pub fn guess_compositor() -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait Platform: 'static {
|
||||
#[expect(missing_docs)]
|
||||
pub trait Platform: 'static {
|
||||
fn background_executor(&self) -> BackgroundExecutor;
|
||||
fn foreground_executor(&self) -> ForegroundExecutor;
|
||||
fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
|
||||
@@ -183,16 +221,10 @@ pub(crate) trait Platform: 'static {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "screen-capture")]
|
||||
fn is_screen_capture_supported(&self) -> bool;
|
||||
#[cfg(not(feature = "screen-capture"))]
|
||||
fn is_screen_capture_supported(&self) -> bool {
|
||||
false
|
||||
}
|
||||
#[cfg(feature = "screen-capture")]
|
||||
fn screen_capture_sources(&self)
|
||||
-> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>>;
|
||||
#[cfg(not(feature = "screen-capture"))]
|
||||
|
||||
fn screen_capture_sources(
|
||||
&self,
|
||||
) -> oneshot::Receiver<anyhow::Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
||||
@@ -246,13 +278,16 @@ pub(crate) trait Platform: 'static {
|
||||
&self,
|
||||
_menus: Vec<MenuItem>,
|
||||
_entries: Vec<SmallVec<[PathBuf; 2]>>,
|
||||
) -> Vec<SmallVec<[PathBuf; 2]>> {
|
||||
Vec::new()
|
||||
) -> Task<Vec<SmallVec<[PathBuf; 2]>>> {
|
||||
Task::ready(Vec::new())
|
||||
}
|
||||
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
||||
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
|
||||
|
||||
fn thermal_state(&self) -> ThermalState;
|
||||
fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>);
|
||||
|
||||
fn compositor_name(&self) -> &'static str {
|
||||
""
|
||||
}
|
||||
@@ -262,12 +297,18 @@ pub(crate) trait Platform: 'static {
|
||||
fn set_cursor_style(&self, style: CursorStyle);
|
||||
fn should_auto_hide_scrollbars(&self) -> bool;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
fn write_to_primary(&self, item: ClipboardItem);
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
|
||||
fn write_to_clipboard(&self, item: ClipboardItem);
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
fn read_from_primary(&self) -> Option<ClipboardItem>;
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
fn write_to_primary(&self, item: ClipboardItem);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn read_from_find_pasteboard(&self) -> Option<ClipboardItem>;
|
||||
#[cfg(target_os = "macos")]
|
||||
fn write_to_find_pasteboard(&self, item: ClipboardItem);
|
||||
|
||||
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
|
||||
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
|
||||
@@ -279,7 +320,7 @@ pub(crate) trait Platform: 'static {
|
||||
}
|
||||
|
||||
/// A handle to a platform's display, e.g. a monitor or laptop screen.
|
||||
pub trait PlatformDisplay: Send + Sync + Debug {
|
||||
pub trait PlatformDisplay: Debug {
|
||||
/// Get the ID for this display
|
||||
fn id(&self) -> DisplayId;
|
||||
|
||||
@@ -309,6 +350,19 @@ pub trait PlatformDisplay: Send + Sync + Debug {
|
||||
}
|
||||
}
|
||||
|
||||
/// Thermal state of the system
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ThermalState {
|
||||
/// System has no thermal constraints
|
||||
Nominal,
|
||||
/// System is slightly constrained, reduce discretionary work
|
||||
Fair,
|
||||
/// System is moderately constrained, reduce CPU/GPU intensive work
|
||||
Serious,
|
||||
/// System is critically constrained, minimize all resource usage
|
||||
Critical,
|
||||
}
|
||||
|
||||
/// Metadata for a given [ScreenCaptureSource]
|
||||
#[derive(Clone)]
|
||||
pub struct SourceMetadata {
|
||||
@@ -349,6 +403,19 @@ pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
|
||||
#[derive(PartialEq, Eq, Hash, Copy, Clone)]
|
||||
pub struct DisplayId(pub(crate) u32);
|
||||
|
||||
impl DisplayId {
|
||||
/// Create a new `DisplayId` from a raw platform display identifier.
|
||||
pub fn new(id: u32) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for DisplayId {
|
||||
fn from(id: u32) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DisplayId> for u32 {
|
||||
fn from(id: DisplayId) -> Self {
|
||||
id.0
|
||||
@@ -461,13 +528,16 @@ impl Tiling {
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
|
||||
pub(crate) struct RequestFrameOptions {
|
||||
pub(crate) require_presentation: bool,
|
||||
/// Force refresh of all rendering states when true
|
||||
pub(crate) force_render: bool,
|
||||
#[expect(missing_docs)]
|
||||
pub struct RequestFrameOptions {
|
||||
/// Whether a presentation is required.
|
||||
pub require_presentation: bool,
|
||||
/// Force refresh of all rendering states when true.
|
||||
pub force_render: bool,
|
||||
}
|
||||
|
||||
pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
#[expect(missing_docs)]
|
||||
pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
fn bounds(&self) -> Bounds<Pixels>;
|
||||
fn is_maximized(&self) -> bool;
|
||||
fn window_bounds(&self) -> WindowBounds;
|
||||
@@ -491,6 +561,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
fn activate(&self);
|
||||
fn is_active(&self) -> bool;
|
||||
fn is_hovered(&self) -> bool;
|
||||
fn background_appearance(&self) -> WindowBackgroundAppearance;
|
||||
fn set_title(&mut self, title: &str);
|
||||
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
|
||||
fn minimize(&self);
|
||||
@@ -510,6 +581,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
fn draw(&self, scene: &Scene);
|
||||
fn completed_frame(&self) {}
|
||||
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
|
||||
fn is_subpixel_rendering_supported(&self) -> bool;
|
||||
|
||||
// macOS specific methods
|
||||
fn get_title(&self) -> String {
|
||||
@@ -535,7 +607,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
fn set_tabbing_identifier(&self, _identifier: Option<String>) {}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_raw_handle(&self) -> windows::HWND;
|
||||
fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND;
|
||||
|
||||
// Linux specific methods
|
||||
fn inner_window_bounds(&self) -> WindowBounds {
|
||||
@@ -564,64 +636,99 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
fn as_test(&mut self) -> Option<&mut TestWindow> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Renders the given scene to a texture and returns the pixel data as an RGBA image.
|
||||
/// This does not present the frame to screen - useful for visual testing where we want
|
||||
/// to capture what would be rendered without displaying it or requiring the window to be visible.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
fn render_to_image(&self, _scene: &Scene) -> Result<RgbaImage> {
|
||||
anyhow::bail!("render_to_image not implemented for this platform")
|
||||
}
|
||||
}
|
||||
|
||||
/// This type is public so that our test macro can generate and use it, but it should not
|
||||
/// be considered part of our public API.
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug)]
|
||||
pub struct RunnableMeta {
|
||||
/// Location of the runnable
|
||||
pub location: &'static core::panic::Location<'static>,
|
||||
/// A renderer for headless windows that can produce real rendered output.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub trait PlatformHeadlessRenderer {
|
||||
/// Render a scene and return the result as an RGBA image.
|
||||
fn render_scene_to_image(
|
||||
&mut self,
|
||||
scene: &Scene,
|
||||
size: Size<DevicePixels>,
|
||||
) -> Result<RgbaImage>;
|
||||
|
||||
/// Returns the sprite atlas used by this renderer.
|
||||
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
|
||||
}
|
||||
|
||||
/// Type alias for runnables with metadata.
|
||||
/// Previously an enum with a single variant, now simplified to a direct type alias.
|
||||
#[doc(hidden)]
|
||||
pub enum RunnableVariant {
|
||||
Meta(Runnable<RunnableMeta>),
|
||||
Compat(Runnable),
|
||||
}
|
||||
pub type RunnableVariant = Runnable<RunnableMeta>;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub type TimerResolutionGuard = util::Deferred<Box<dyn FnOnce() + Send>>;
|
||||
|
||||
/// This type is public so that our test macro can generate and use it, but it should not
|
||||
/// be considered part of our public API.
|
||||
#[doc(hidden)]
|
||||
pub trait PlatformDispatcher: Send + Sync {
|
||||
fn get_all_timings(&self) -> Vec<ThreadTaskTimings>;
|
||||
fn get_current_thread_timings(&self) -> Vec<TaskTiming>;
|
||||
fn get_current_thread_timings(&self) -> ThreadTaskTimings;
|
||||
fn is_main_thread(&self) -> bool;
|
||||
fn dispatch(&self, runnable: RunnableVariant, label: Option<TaskLabel>, priority: Priority);
|
||||
fn dispatch(&self, runnable: RunnableVariant, priority: Priority);
|
||||
fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority);
|
||||
fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant);
|
||||
fn spawn_realtime(&self, priority: RealtimePriority, f: Box<dyn FnOnce() + Send>);
|
||||
|
||||
fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
|
||||
|
||||
fn now(&self) -> Instant {
|
||||
Instant::now()
|
||||
}
|
||||
|
||||
fn increase_timer_resolution(&self) -> TimerResolutionGuard {
|
||||
util::defer(Box::new(|| {}))
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
fn as_test(&self) -> Option<&TestDispatcher> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait PlatformTextSystem: Send + Sync {
|
||||
#[expect(missing_docs)]
|
||||
pub trait PlatformTextSystem: Send + Sync {
|
||||
fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
|
||||
/// Get all available font names.
|
||||
fn all_font_names(&self) -> Vec<String>;
|
||||
/// Get the font ID for a font descriptor.
|
||||
fn font_id(&self, descriptor: &Font) -> Result<FontId>;
|
||||
/// Get metrics for a font.
|
||||
fn font_metrics(&self, font_id: FontId) -> FontMetrics;
|
||||
/// Get typographic bounds for a glyph.
|
||||
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
|
||||
/// Get the advance width for a glyph.
|
||||
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
|
||||
/// Get the glyph ID for a character.
|
||||
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
|
||||
/// Get raster bounds for a glyph.
|
||||
fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
|
||||
/// Rasterize a glyph.
|
||||
fn rasterize_glyph(
|
||||
&self,
|
||||
params: &RenderGlyphParams,
|
||||
raster_bounds: Bounds<DevicePixels>,
|
||||
) -> Result<(Size<DevicePixels>, Vec<u8>)>;
|
||||
/// Layout a line of text with the given font runs.
|
||||
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
|
||||
/// Returns the recommended text rendering mode for the given font and size.
|
||||
fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
|
||||
-> TextRenderingMode;
|
||||
}
|
||||
|
||||
pub(crate) struct NoopTextSystem;
|
||||
#[expect(missing_docs)]
|
||||
pub struct NoopTextSystem;
|
||||
|
||||
#[expect(missing_docs)]
|
||||
impl NoopTextSystem {
|
||||
#[allow(dead_code)]
|
||||
pub fn new() -> Self {
|
||||
@@ -738,13 +845,22 @@ impl PlatformTextSystem for NoopTextSystem {
|
||||
len: text.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn recommended_rendering_mode(
|
||||
&self,
|
||||
_font_id: FontId,
|
||||
_font_size: Pixels,
|
||||
) -> TextRenderingMode {
|
||||
TextRenderingMode::Grayscale
|
||||
}
|
||||
}
|
||||
|
||||
// Adapted from https://github.com/microsoft/terminal/blob/1283c0f5b99a2961673249fa77c6b986efb5086c/src/renderer/atlas/dwrite.cpp
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
/// Compute gamma correction ratios for subpixel text rendering.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] {
|
||||
pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] {
|
||||
const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [
|
||||
[0.0000 / 4.0, 0.0000 / 4.0, 0.0000 / 4.0, 0.0000 / 4.0], // gamma = 1.0
|
||||
[0.0166 / 4.0, -0.0807 / 4.0, 0.2227 / 4.0, -0.0751 / 4.0], // gamma = 1.1
|
||||
@@ -776,7 +892,8 @@ pub(crate) fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] {
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone)]
|
||||
pub(crate) enum AtlasKey {
|
||||
#[expect(missing_docs)]
|
||||
pub enum AtlasKey {
|
||||
Glyph(RenderGlyphParams),
|
||||
Svg(RenderSvgParams),
|
||||
Image(RenderImageParams),
|
||||
@@ -790,11 +907,14 @@ impl AtlasKey {
|
||||
),
|
||||
allow(dead_code)
|
||||
)]
|
||||
pub(crate) fn texture_kind(&self) -> AtlasTextureKind {
|
||||
/// Returns the texture kind for this atlas key.
|
||||
pub fn texture_kind(&self) -> AtlasTextureKind {
|
||||
match self {
|
||||
AtlasKey::Glyph(params) => {
|
||||
if params.is_emoji {
|
||||
AtlasTextureKind::Polychrome
|
||||
} else if params.subpixel_rendering {
|
||||
AtlasTextureKind::Subpixel
|
||||
} else {
|
||||
AtlasTextureKind::Monochrome
|
||||
}
|
||||
@@ -823,7 +943,8 @@ impl From<RenderImageParams> for AtlasKey {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait PlatformAtlas: Send + Sync {
|
||||
#[expect(missing_docs)]
|
||||
pub trait PlatformAtlas {
|
||||
fn get_or_insert_with<'a>(
|
||||
&self,
|
||||
key: &AtlasKey,
|
||||
@@ -832,9 +953,10 @@ pub(crate) trait PlatformAtlas: Send + Sync {
|
||||
fn remove(&self, key: &AtlasKey);
|
||||
}
|
||||
|
||||
struct AtlasTextureList<T> {
|
||||
textures: Vec<Option<T>>,
|
||||
free_list: Vec<usize>,
|
||||
#[doc(hidden)]
|
||||
pub struct AtlasTextureList<T> {
|
||||
pub textures: Vec<Option<T>>,
|
||||
pub free_list: Vec<usize>,
|
||||
}
|
||||
|
||||
impl<T> Default for AtlasTextureList<T> {
|
||||
@@ -856,32 +978,40 @@ impl<T> ops::Index<usize> for AtlasTextureList<T> {
|
||||
|
||||
impl<T> AtlasTextureList<T> {
|
||||
#[allow(unused)]
|
||||
fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
|
||||
pub fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
|
||||
self.free_list.clear();
|
||||
self.textures.drain(..)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
|
||||
pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
|
||||
self.textures.iter_mut().flatten()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub(crate) struct AtlasTile {
|
||||
pub(crate) texture_id: AtlasTextureId,
|
||||
pub(crate) tile_id: TileId,
|
||||
pub(crate) padding: u32,
|
||||
pub(crate) bounds: Bounds<DevicePixels>,
|
||||
#[expect(missing_docs)]
|
||||
pub struct AtlasTile {
|
||||
/// The texture this tile belongs to.
|
||||
pub texture_id: AtlasTextureId,
|
||||
/// The unique ID of this tile within its texture.
|
||||
pub tile_id: TileId,
|
||||
/// Padding around the tile content in pixels.
|
||||
pub padding: u32,
|
||||
/// The bounds of this tile within the texture.
|
||||
pub bounds: Bounds<DevicePixels>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[repr(C)]
|
||||
pub(crate) struct AtlasTextureId {
|
||||
#[expect(missing_docs)]
|
||||
pub struct AtlasTextureId {
|
||||
// We use u32 instead of usize for Metal Shader Language compatibility
|
||||
pub(crate) index: u32,
|
||||
pub(crate) kind: AtlasTextureKind,
|
||||
/// The index of this texture in the atlas.
|
||||
pub index: u32,
|
||||
/// The kind of content stored in this texture.
|
||||
pub kind: AtlasTextureKind,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
@@ -893,14 +1023,17 @@ pub(crate) struct AtlasTextureId {
|
||||
),
|
||||
allow(dead_code)
|
||||
)]
|
||||
pub(crate) enum AtlasTextureKind {
|
||||
#[expect(missing_docs)]
|
||||
pub enum AtlasTextureKind {
|
||||
Monochrome = 0,
|
||||
Polychrome = 1,
|
||||
Subpixel = 2,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[repr(C)]
|
||||
pub(crate) struct TileId(pub(crate) u32);
|
||||
#[expect(missing_docs)]
|
||||
pub struct TileId(pub u32);
|
||||
|
||||
impl From<etagere::AllocId> for TileId {
|
||||
fn from(id: etagere::AllocId) -> Self {
|
||||
@@ -914,11 +1047,13 @@ impl From<TileId> for etagere::AllocId {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PlatformInputHandler {
|
||||
#[expect(missing_docs)]
|
||||
pub struct PlatformInputHandler {
|
||||
cx: AsyncWindowContext,
|
||||
handler: Box<dyn InputHandler>,
|
||||
}
|
||||
|
||||
#[expect(missing_docs)]
|
||||
#[cfg_attr(
|
||||
all(
|
||||
any(target_os = "linux", target_os = "freebsd"),
|
||||
@@ -931,7 +1066,7 @@ impl PlatformInputHandler {
|
||||
Self { cx, handler }
|
||||
}
|
||||
|
||||
fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
|
||||
pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
|
||||
self.cx
|
||||
.update(|window, cx| {
|
||||
self.handler
|
||||
@@ -942,7 +1077,7 @@ impl PlatformInputHandler {
|
||||
}
|
||||
|
||||
#[cfg_attr(target_os = "windows", allow(dead_code))]
|
||||
fn marked_text_range(&mut self) -> Option<Range<usize>> {
|
||||
pub fn marked_text_range(&mut self) -> Option<Range<usize>> {
|
||||
self.cx
|
||||
.update(|window, cx| self.handler.marked_text_range(window, cx))
|
||||
.ok()
|
||||
@@ -953,7 +1088,7 @@ impl PlatformInputHandler {
|
||||
any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
|
||||
allow(dead_code)
|
||||
)]
|
||||
fn text_for_range(
|
||||
pub fn text_for_range(
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
adjusted: &mut Option<Range<usize>>,
|
||||
@@ -967,7 +1102,7 @@ impl PlatformInputHandler {
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
|
||||
pub fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
|
||||
self.cx
|
||||
.update(|window, cx| {
|
||||
self.handler
|
||||
@@ -996,13 +1131,13 @@ impl PlatformInputHandler {
|
||||
}
|
||||
|
||||
#[cfg_attr(target_os = "windows", allow(dead_code))]
|
||||
fn unmark_text(&mut self) {
|
||||
pub fn unmark_text(&mut self) {
|
||||
self.cx
|
||||
.update(|window, cx| self.handler.unmark_text(window, cx))
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
|
||||
pub fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
|
||||
self.cx
|
||||
.update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
|
||||
.ok()
|
||||
@@ -1010,11 +1145,11 @@ impl PlatformInputHandler {
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn apple_press_and_hold_enabled(&mut self) -> bool {
|
||||
pub fn apple_press_and_hold_enabled(&mut self) -> bool {
|
||||
self.handler.apple_press_and_hold_enabled()
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
|
||||
pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
|
||||
self.handler.replace_text_in_range(None, input, window, cx);
|
||||
}
|
||||
|
||||
@@ -1040,9 +1175,16 @@ impl PlatformInputHandler {
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
|
||||
pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
|
||||
self.handler.accepts_text_input(window, cx)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn query_accepts_text_input(&mut self) -> bool {
|
||||
self.cx
|
||||
.update(|window, cx| self.handler.accepts_text_input(window, cx))
|
||||
.unwrap_or(true)
|
||||
}
|
||||
}
|
||||
|
||||
/// A struct representing a selection in a text buffer, in UTF16 characters.
|
||||
@@ -1217,7 +1359,8 @@ pub struct WindowOptions {
|
||||
),
|
||||
allow(dead_code)
|
||||
)]
|
||||
pub(crate) struct WindowParams {
|
||||
#[allow(missing_docs)]
|
||||
pub struct WindowParams {
|
||||
pub bounds: Bounds<Pixels>,
|
||||
|
||||
/// The titlebar configuration of the window
|
||||
@@ -1348,6 +1491,10 @@ pub enum WindowKind {
|
||||
/// docks, notifications or wallpapers.
|
||||
#[cfg(all(target_os = "linux", feature = "wayland"))]
|
||||
LayerShell(layer_shell::LayerShellOptions),
|
||||
|
||||
/// A window that appears on top of its parent window and blocks interaction with it
|
||||
/// until the modal window is closed
|
||||
Dialog,
|
||||
}
|
||||
|
||||
/// The appearance of the window, as defined by the operating system.
|
||||
@@ -1403,6 +1550,18 @@ pub enum WindowBackgroundAppearance {
|
||||
MicaAltBackdrop,
|
||||
}
|
||||
|
||||
/// The text rendering mode to use for drawing glyphs.
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub enum TextRenderingMode {
|
||||
/// Use the platform's default text rendering mode.
|
||||
#[default]
|
||||
PlatformDefault,
|
||||
/// Use subpixel (ClearType-style) text rendering.
|
||||
Subpixel,
|
||||
/// Use grayscale text rendering.
|
||||
Grayscale,
|
||||
}
|
||||
|
||||
/// The options that can be configured for a file dialog prompt
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PathPromptOptions {
|
||||
@@ -1456,8 +1615,9 @@ impl PromptButton {
|
||||
PromptButton::Cancel(label.into())
|
||||
}
|
||||
|
||||
/// Returns true if this button is a cancel button.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn is_cancel(&self) -> bool {
|
||||
pub fn is_cancel(&self) -> bool {
|
||||
matches!(self, PromptButton::Cancel(_))
|
||||
}
|
||||
|
||||
@@ -1575,7 +1735,8 @@ pub enum CursorStyle {
|
||||
/// A clipboard item that should be copied to the clipboard
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ClipboardItem {
|
||||
entries: Vec<ClipboardEntry>,
|
||||
/// The entries in this clipboard item.
|
||||
pub entries: Vec<ClipboardEntry>,
|
||||
}
|
||||
|
||||
/// Either a ClipboardString or a ClipboardImage
|
||||
@@ -1775,7 +1936,7 @@ pub struct Image {
|
||||
/// The raw image bytes
|
||||
pub bytes: Vec<u8>,
|
||||
/// The unique ID for the image
|
||||
id: u64,
|
||||
pub id: u64,
|
||||
}
|
||||
|
||||
impl Hash for Image {
|
||||
@@ -1893,8 +2054,10 @@ impl Image {
|
||||
/// A clipboard item that should be copied to the clipboard
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ClipboardString {
|
||||
pub(crate) text: String,
|
||||
pub(crate) metadata: Option<String>,
|
||||
/// The text content.
|
||||
pub text: String,
|
||||
/// Optional metadata associated with this clipboard string.
|
||||
pub metadata: Option<String>,
|
||||
}
|
||||
|
||||
impl ClipboardString {
|
||||
@@ -1934,7 +2097,8 @@ impl ClipboardString {
|
||||
}
|
||||
|
||||
#[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
|
||||
pub(crate) fn text_hash(text: &str) -> u64 {
|
||||
/// Compute a hash of the given text for clipboard change detection.
|
||||
pub fn text_hash(text: &str) -> u64 {
|
||||
let mut hasher = SeaHasher::new();
|
||||
text.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
|
||||
+158
-16
@@ -1,5 +1,4 @@
|
||||
use crate::{Action, App, Platform, SharedString};
|
||||
use util::ResultExt;
|
||||
|
||||
/// A menu of the application, either a main menu or a submenu
|
||||
pub struct Menu {
|
||||
@@ -8,14 +7,39 @@ pub struct Menu {
|
||||
|
||||
/// The items in the menu
|
||||
pub items: Vec<MenuItem>,
|
||||
|
||||
/// Whether this menu is disabled
|
||||
pub disabled: bool,
|
||||
}
|
||||
|
||||
impl Menu {
|
||||
/// Create a new Menu with the given name
|
||||
pub fn new(name: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
items: vec![],
|
||||
disabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set items to be in this menu
|
||||
pub fn items(mut self, items: impl IntoIterator<Item = MenuItem>) -> Self {
|
||||
self.items = items.into_iter().collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether this menu is disabled
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Create an OwnedMenu from this Menu
|
||||
pub fn owned(self) -> OwnedMenu {
|
||||
OwnedMenu {
|
||||
name: self.name.to_string().into(),
|
||||
items: self.items.into_iter().map(|item| item.owned()).collect(),
|
||||
disabled: self.disabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,6 +97,9 @@ pub enum MenuItem {
|
||||
|
||||
/// Whether this action is checked
|
||||
checked: bool,
|
||||
|
||||
/// Whether this action is disabled
|
||||
disabled: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -102,6 +129,7 @@ impl MenuItem {
|
||||
action: Box::new(action),
|
||||
os_action: None,
|
||||
checked: false,
|
||||
disabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +144,7 @@ impl MenuItem {
|
||||
action: Box::new(action),
|
||||
os_action: Some(os_action),
|
||||
checked: false,
|
||||
disabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,11 +158,13 @@ impl MenuItem {
|
||||
action,
|
||||
os_action,
|
||||
checked,
|
||||
disabled,
|
||||
} => OwnedMenuItem::Action {
|
||||
name: name.into(),
|
||||
action,
|
||||
os_action,
|
||||
checked,
|
||||
disabled,
|
||||
},
|
||||
MenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.owned()),
|
||||
}
|
||||
@@ -143,19 +174,49 @@ impl MenuItem {
|
||||
///
|
||||
/// Only for [`MenuItem::Action`], otherwise, will be ignored
|
||||
pub fn checked(mut self, checked: bool) -> Self {
|
||||
match &mut self {
|
||||
MenuItem::Action { checked: old, .. } => {
|
||||
*old = checked;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns whether this menu item is checked
|
||||
///
|
||||
/// Only for [`MenuItem::Action`], otherwise, returns false
|
||||
#[inline]
|
||||
pub fn is_checked(&self) -> bool {
|
||||
match self {
|
||||
MenuItem::Action {
|
||||
action,
|
||||
os_action,
|
||||
name,
|
||||
..
|
||||
} => MenuItem::Action {
|
||||
name,
|
||||
action,
|
||||
os_action,
|
||||
checked,
|
||||
},
|
||||
_ => self,
|
||||
MenuItem::Action { checked, .. } => *checked,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set whether this menu item is disabled
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
match &mut self {
|
||||
MenuItem::Action { disabled: old, .. } => {
|
||||
*old = disabled;
|
||||
}
|
||||
MenuItem::Submenu(submenu) => {
|
||||
submenu.disabled = disabled;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns whether this menu item is disabled
|
||||
///
|
||||
/// Only for [`MenuItem::Action`] and [`MenuItem::Submenu`], otherwise, returns false
|
||||
#[inline]
|
||||
pub fn is_disabled(&self) -> bool {
|
||||
match self {
|
||||
MenuItem::Action { disabled, .. } => *disabled,
|
||||
MenuItem::Submenu(submenu) => submenu.disabled,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,6 +241,9 @@ pub struct OwnedMenu {
|
||||
|
||||
/// The items in the menu
|
||||
pub items: Vec<OwnedMenuItem>,
|
||||
|
||||
/// Whether this menu is disabled
|
||||
pub disabled: bool,
|
||||
}
|
||||
|
||||
/// The different kinds of items that can be in a menu
|
||||
@@ -207,6 +271,9 @@ pub enum OwnedMenuItem {
|
||||
|
||||
/// Whether this action is checked
|
||||
checked: bool,
|
||||
|
||||
/// Whether this action is disabled
|
||||
disabled: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -220,11 +287,13 @@ impl Clone for OwnedMenuItem {
|
||||
action,
|
||||
os_action,
|
||||
checked,
|
||||
disabled,
|
||||
} => OwnedMenuItem::Action {
|
||||
name: name.clone(),
|
||||
action: action.boxed_clone(),
|
||||
os_action: *os_action,
|
||||
checked: *checked,
|
||||
disabled: *disabled,
|
||||
},
|
||||
OwnedMenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.clone()),
|
||||
}
|
||||
@@ -263,14 +332,18 @@ pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &App) {
|
||||
platform.on_will_open_app_menu(Box::new({
|
||||
let cx = cx.to_async();
|
||||
move || {
|
||||
cx.update(|cx| cx.clear_pending_keystrokes()).ok();
|
||||
if let Some(app) = cx.app.upgrade() {
|
||||
app.borrow_mut().update(|cx| cx.clear_pending_keystrokes());
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
platform.on_validate_app_menu_command(Box::new({
|
||||
let cx = cx.to_async();
|
||||
move |action| {
|
||||
cx.update(|cx| cx.is_action_available(action))
|
||||
cx.app
|
||||
.upgrade()
|
||||
.map(|app| app.borrow_mut().update(|cx| cx.is_action_available(action)))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}));
|
||||
@@ -278,7 +351,76 @@ pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &App) {
|
||||
platform.on_app_menu_action(Box::new({
|
||||
let cx = cx.to_async();
|
||||
move |action| {
|
||||
cx.update(|cx| cx.dispatch_action(action)).log_err();
|
||||
if let Some(app) = cx.app.upgrade() {
|
||||
app.borrow_mut().update(|cx| cx.dispatch_action(action));
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Menu;
|
||||
|
||||
#[test]
|
||||
fn test_menu() {
|
||||
let menu = Menu::new("App")
|
||||
.items(vec![
|
||||
crate::MenuItem::action("Action 1", gpui::NoAction),
|
||||
crate::MenuItem::separator(),
|
||||
])
|
||||
.disabled(true);
|
||||
|
||||
assert_eq!(menu.name.as_ref(), "App");
|
||||
assert_eq!(menu.items.len(), 2);
|
||||
assert!(menu.disabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_menu_item_builder() {
|
||||
use super::MenuItem;
|
||||
|
||||
let item = MenuItem::action("Test Action", gpui::NoAction);
|
||||
assert_eq!(
|
||||
match &item {
|
||||
MenuItem::Action { name, .. } => name.as_ref(),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
"Test Action"
|
||||
);
|
||||
assert!(matches!(
|
||||
item,
|
||||
MenuItem::Action {
|
||||
checked: false,
|
||||
disabled: false,
|
||||
..
|
||||
}
|
||||
));
|
||||
|
||||
assert!(
|
||||
MenuItem::action("Test Action", gpui::NoAction)
|
||||
.checked(true)
|
||||
.is_checked()
|
||||
);
|
||||
assert!(
|
||||
MenuItem::action("Test Action", gpui::NoAction)
|
||||
.disabled(true)
|
||||
.is_disabled()
|
||||
);
|
||||
|
||||
let submenu = MenuItem::submenu(super::Menu {
|
||||
name: "Submenu".into(),
|
||||
items: vec![],
|
||||
disabled: true,
|
||||
});
|
||||
assert_eq!(
|
||||
match &submenu {
|
||||
MenuItem::Submenu(menu) => menu.name.as_ref(),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
"Submenu"
|
||||
);
|
||||
assert!(!submenu.is_checked());
|
||||
assert!(submenu.is_disabled());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
mod apple_compat;
|
||||
mod blade_atlas;
|
||||
mod blade_context;
|
||||
mod blade_renderer;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) use apple_compat::*;
|
||||
pub(crate) use blade_atlas::*;
|
||||
pub(crate) use blade_context::*;
|
||||
pub(crate) use blade_renderer::*;
|
||||
@@ -1,60 +0,0 @@
|
||||
use super::{BladeContext, BladeRenderer, BladeSurfaceConfig};
|
||||
use blade_graphics as gpu;
|
||||
use std::{ffi::c_void, ptr::NonNull};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Context {
|
||||
inner: BladeContext,
|
||||
}
|
||||
impl Default for Context {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inner: BladeContext::new().unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type Renderer = BladeRenderer;
|
||||
|
||||
pub unsafe fn new_renderer(
|
||||
context: Context,
|
||||
_native_window: *mut c_void,
|
||||
native_view: *mut c_void,
|
||||
bounds: crate::Size<f32>,
|
||||
transparent: bool,
|
||||
) -> Renderer {
|
||||
use raw_window_handle as rwh;
|
||||
struct RawWindow {
|
||||
view: *mut c_void,
|
||||
}
|
||||
|
||||
impl rwh::HasWindowHandle for RawWindow {
|
||||
fn window_handle(&self) -> Result<rwh::WindowHandle<'_>, rwh::HandleError> {
|
||||
let view = NonNull::new(self.view).unwrap();
|
||||
let handle = rwh::AppKitWindowHandle::new(view);
|
||||
Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) })
|
||||
}
|
||||
}
|
||||
impl rwh::HasDisplayHandle for RawWindow {
|
||||
fn display_handle(&self) -> Result<rwh::DisplayHandle<'_>, rwh::HandleError> {
|
||||
let handle = rwh::AppKitDisplayHandle::new();
|
||||
Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) })
|
||||
}
|
||||
}
|
||||
|
||||
BladeRenderer::new(
|
||||
&context.inner,
|
||||
&RawWindow {
|
||||
view: native_view as *mut _,
|
||||
},
|
||||
BladeSurfaceConfig {
|
||||
size: gpu::Extent {
|
||||
width: bounds.width as u32,
|
||||
height: bounds.height as u32,
|
||||
depth: 1,
|
||||
},
|
||||
transparent,
|
||||
},
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
@@ -1,384 +0,0 @@
|
||||
use crate::{
|
||||
AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, DevicePixels, PlatformAtlas,
|
||||
Point, Size, platform::AtlasTextureList,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use blade_graphics as gpu;
|
||||
use blade_util::{BufferBelt, BufferBeltDescriptor};
|
||||
use collections::FxHashMap;
|
||||
use etagere::BucketedAtlasAllocator;
|
||||
use parking_lot::Mutex;
|
||||
use std::{borrow::Cow, ops, sync::Arc};
|
||||
|
||||
pub(crate) struct BladeAtlas(Mutex<BladeAtlasState>);
|
||||
|
||||
struct PendingUpload {
|
||||
id: AtlasTextureId,
|
||||
bounds: Bounds<DevicePixels>,
|
||||
data: gpu::BufferPiece,
|
||||
}
|
||||
|
||||
struct BladeAtlasState {
|
||||
gpu: Arc<gpu::Context>,
|
||||
upload_belt: BufferBelt,
|
||||
storage: BladeAtlasStorage,
|
||||
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
|
||||
initializations: Vec<AtlasTextureId>,
|
||||
uploads: Vec<PendingUpload>,
|
||||
}
|
||||
|
||||
#[cfg(gles)]
|
||||
unsafe impl Send for BladeAtlasState {}
|
||||
|
||||
impl BladeAtlasState {
|
||||
fn destroy(&mut self) {
|
||||
self.storage.destroy(&self.gpu);
|
||||
self.upload_belt.destroy(&self.gpu);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BladeTextureInfo {
|
||||
pub raw_view: gpu::TextureView,
|
||||
}
|
||||
|
||||
impl BladeAtlas {
|
||||
pub(crate) fn new(gpu: &Arc<gpu::Context>) -> Self {
|
||||
BladeAtlas(Mutex::new(BladeAtlasState {
|
||||
gpu: Arc::clone(gpu),
|
||||
upload_belt: BufferBelt::new(BufferBeltDescriptor {
|
||||
memory: gpu::Memory::Upload,
|
||||
min_chunk_size: 0x10000,
|
||||
alignment: 64, // Vulkan `optimalBufferCopyOffsetAlignment` on Intel XE
|
||||
}),
|
||||
storage: BladeAtlasStorage::default(),
|
||||
tiles_by_key: Default::default(),
|
||||
initializations: Vec::new(),
|
||||
uploads: Vec::new(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn destroy(&self) {
|
||||
self.0.lock().destroy();
|
||||
}
|
||||
|
||||
pub fn before_frame(&self, gpu_encoder: &mut gpu::CommandEncoder) {
|
||||
let mut lock = self.0.lock();
|
||||
lock.flush(gpu_encoder);
|
||||
}
|
||||
|
||||
pub fn after_frame(&self, sync_point: &gpu::SyncPoint) {
|
||||
let mut lock = self.0.lock();
|
||||
lock.upload_belt.flush(sync_point);
|
||||
}
|
||||
|
||||
pub fn get_texture_info(&self, id: AtlasTextureId) -> BladeTextureInfo {
|
||||
let lock = self.0.lock();
|
||||
let texture = &lock.storage[id];
|
||||
BladeTextureInfo {
|
||||
raw_view: texture.raw_view,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformAtlas for BladeAtlas {
|
||||
fn get_or_insert_with<'a>(
|
||||
&self,
|
||||
key: &AtlasKey,
|
||||
build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
|
||||
) -> Result<Option<AtlasTile>> {
|
||||
let mut lock = self.0.lock();
|
||||
if let Some(tile) = lock.tiles_by_key.get(key) {
|
||||
Ok(Some(tile.clone()))
|
||||
} else {
|
||||
profiling::scope!("new tile");
|
||||
let Some((size, bytes)) = build()? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let tile = lock.allocate(size, key.texture_kind());
|
||||
lock.upload_texture(tile.texture_id, tile.bounds, &bytes);
|
||||
lock.tiles_by_key.insert(key.clone(), tile.clone());
|
||||
Ok(Some(tile))
|
||||
}
|
||||
}
|
||||
|
||||
fn remove(&self, key: &AtlasKey) {
|
||||
let mut lock = self.0.lock();
|
||||
|
||||
let Some(id) = lock.tiles_by_key.remove(key).map(|tile| tile.texture_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(texture_slot) = lock.storage[id.kind].textures.get_mut(id.index as usize) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(mut texture) = texture_slot.take() {
|
||||
texture.decrement_ref_count();
|
||||
if texture.is_unreferenced() {
|
||||
lock.storage[id.kind]
|
||||
.free_list
|
||||
.push(texture.id.index as usize);
|
||||
texture.destroy(&lock.gpu);
|
||||
} else {
|
||||
*texture_slot = Some(texture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BladeAtlasState {
|
||||
fn allocate(&mut self, size: Size<DevicePixels>, texture_kind: AtlasTextureKind) -> AtlasTile {
|
||||
{
|
||||
let textures = &mut self.storage[texture_kind];
|
||||
|
||||
if let Some(tile) = textures
|
||||
.iter_mut()
|
||||
.rev()
|
||||
.find_map(|texture| texture.allocate(size))
|
||||
{
|
||||
return tile;
|
||||
}
|
||||
}
|
||||
|
||||
let texture = self.push_texture(size, texture_kind);
|
||||
texture.allocate(size).unwrap()
|
||||
}
|
||||
|
||||
fn push_texture(
|
||||
&mut self,
|
||||
min_size: Size<DevicePixels>,
|
||||
kind: AtlasTextureKind,
|
||||
) -> &mut BladeAtlasTexture {
|
||||
const DEFAULT_ATLAS_SIZE: Size<DevicePixels> = Size {
|
||||
width: DevicePixels(1024),
|
||||
height: DevicePixels(1024),
|
||||
};
|
||||
|
||||
let size = min_size.max(&DEFAULT_ATLAS_SIZE);
|
||||
let format;
|
||||
let usage;
|
||||
match kind {
|
||||
AtlasTextureKind::Monochrome => {
|
||||
format = gpu::TextureFormat::R8Unorm;
|
||||
usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE;
|
||||
}
|
||||
AtlasTextureKind::Polychrome => {
|
||||
format = gpu::TextureFormat::Bgra8Unorm;
|
||||
usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE;
|
||||
}
|
||||
}
|
||||
|
||||
let raw = self.gpu.create_texture(gpu::TextureDesc {
|
||||
name: "atlas",
|
||||
format,
|
||||
size: gpu::Extent {
|
||||
width: size.width.into(),
|
||||
height: size.height.into(),
|
||||
depth: 1,
|
||||
},
|
||||
array_layer_count: 1,
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: gpu::TextureDimension::D2,
|
||||
usage,
|
||||
external: None,
|
||||
});
|
||||
let raw_view = self.gpu.create_texture_view(
|
||||
raw,
|
||||
gpu::TextureViewDesc {
|
||||
name: "",
|
||||
format,
|
||||
dimension: gpu::ViewDimension::D2,
|
||||
subresources: &Default::default(),
|
||||
},
|
||||
);
|
||||
|
||||
let texture_list = &mut self.storage[kind];
|
||||
let index = texture_list.free_list.pop();
|
||||
|
||||
let atlas_texture = BladeAtlasTexture {
|
||||
id: AtlasTextureId {
|
||||
index: index.unwrap_or(texture_list.textures.len()) as u32,
|
||||
kind,
|
||||
},
|
||||
allocator: etagere::BucketedAtlasAllocator::new(size.into()),
|
||||
format,
|
||||
raw,
|
||||
raw_view,
|
||||
live_atlas_keys: 0,
|
||||
};
|
||||
|
||||
self.initializations.push(atlas_texture.id);
|
||||
|
||||
if let Some(ix) = index {
|
||||
texture_list.textures[ix] = Some(atlas_texture);
|
||||
texture_list.textures.get_mut(ix).unwrap().as_mut().unwrap()
|
||||
} else {
|
||||
texture_list.textures.push(Some(atlas_texture));
|
||||
texture_list.textures.last_mut().unwrap().as_mut().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn upload_texture(&mut self, id: AtlasTextureId, bounds: Bounds<DevicePixels>, bytes: &[u8]) {
|
||||
let data = self.upload_belt.alloc_bytes(bytes, &self.gpu);
|
||||
self.uploads.push(PendingUpload { id, bounds, data });
|
||||
}
|
||||
|
||||
fn flush_initializations(&mut self, encoder: &mut gpu::CommandEncoder) {
|
||||
for id in self.initializations.drain(..) {
|
||||
let texture = &self.storage[id];
|
||||
encoder.init_texture(texture.raw);
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self, encoder: &mut gpu::CommandEncoder) {
|
||||
self.flush_initializations(encoder);
|
||||
|
||||
let mut transfers = encoder.transfer("atlas");
|
||||
for upload in self.uploads.drain(..) {
|
||||
let texture = &self.storage[upload.id];
|
||||
transfers.copy_buffer_to_texture(
|
||||
upload.data,
|
||||
upload.bounds.size.width.to_bytes(texture.bytes_per_pixel()),
|
||||
gpu::TexturePiece {
|
||||
texture: texture.raw,
|
||||
mip_level: 0,
|
||||
array_layer: 0,
|
||||
origin: [
|
||||
upload.bounds.origin.x.into(),
|
||||
upload.bounds.origin.y.into(),
|
||||
0,
|
||||
],
|
||||
},
|
||||
gpu::Extent {
|
||||
width: upload.bounds.size.width.into(),
|
||||
height: upload.bounds.size.height.into(),
|
||||
depth: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct BladeAtlasStorage {
|
||||
monochrome_textures: AtlasTextureList<BladeAtlasTexture>,
|
||||
polychrome_textures: AtlasTextureList<BladeAtlasTexture>,
|
||||
}
|
||||
|
||||
impl ops::Index<AtlasTextureKind> for BladeAtlasStorage {
|
||||
type Output = AtlasTextureList<BladeAtlasTexture>;
|
||||
fn index(&self, kind: AtlasTextureKind) -> &Self::Output {
|
||||
match kind {
|
||||
crate::AtlasTextureKind::Monochrome => &self.monochrome_textures,
|
||||
crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::IndexMut<AtlasTextureKind> for BladeAtlasStorage {
|
||||
fn index_mut(&mut self, kind: AtlasTextureKind) -> &mut Self::Output {
|
||||
match kind {
|
||||
crate::AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
|
||||
crate::AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Index<AtlasTextureId> for BladeAtlasStorage {
|
||||
type Output = BladeAtlasTexture;
|
||||
fn index(&self, id: AtlasTextureId) -> &Self::Output {
|
||||
let textures = match id.kind {
|
||||
crate::AtlasTextureKind::Monochrome => &self.monochrome_textures,
|
||||
crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
|
||||
};
|
||||
textures[id.index as usize].as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl BladeAtlasStorage {
|
||||
fn destroy(&mut self, gpu: &gpu::Context) {
|
||||
for mut texture in self.monochrome_textures.drain().flatten() {
|
||||
texture.destroy(gpu);
|
||||
}
|
||||
for mut texture in self.polychrome_textures.drain().flatten() {
|
||||
texture.destroy(gpu);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BladeAtlasTexture {
|
||||
id: AtlasTextureId,
|
||||
allocator: BucketedAtlasAllocator,
|
||||
raw: gpu::Texture,
|
||||
raw_view: gpu::TextureView,
|
||||
format: gpu::TextureFormat,
|
||||
live_atlas_keys: u32,
|
||||
}
|
||||
|
||||
impl BladeAtlasTexture {
|
||||
fn allocate(&mut self, size: Size<DevicePixels>) -> Option<AtlasTile> {
|
||||
let allocation = self.allocator.allocate(size.into())?;
|
||||
let tile = AtlasTile {
|
||||
texture_id: self.id,
|
||||
tile_id: allocation.id.into(),
|
||||
padding: 0,
|
||||
bounds: Bounds {
|
||||
origin: allocation.rectangle.min.into(),
|
||||
size,
|
||||
},
|
||||
};
|
||||
self.live_atlas_keys += 1;
|
||||
Some(tile)
|
||||
}
|
||||
|
||||
fn destroy(&mut self, gpu: &gpu::Context) {
|
||||
gpu.destroy_texture(self.raw);
|
||||
gpu.destroy_texture_view(self.raw_view);
|
||||
}
|
||||
|
||||
fn bytes_per_pixel(&self) -> u8 {
|
||||
self.format.block_info().size
|
||||
}
|
||||
|
||||
fn decrement_ref_count(&mut self) {
|
||||
self.live_atlas_keys -= 1;
|
||||
}
|
||||
|
||||
fn is_unreferenced(&mut self) -> bool {
|
||||
self.live_atlas_keys == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Size<DevicePixels>> for etagere::Size {
|
||||
fn from(size: Size<DevicePixels>) -> Self {
|
||||
etagere::Size::new(size.width.into(), size.height.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<etagere::Point> for Point<DevicePixels> {
|
||||
fn from(value: etagere::Point) -> Self {
|
||||
Point {
|
||||
x: DevicePixels::from(value.x),
|
||||
y: DevicePixels::from(value.y),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<etagere::Size> for Size<DevicePixels> {
|
||||
fn from(size: etagere::Size) -> Self {
|
||||
Size {
|
||||
width: DevicePixels::from(size.width),
|
||||
height: DevicePixels::from(size.height),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<etagere::Rectangle> for Bounds<DevicePixels> {
|
||||
fn from(rectangle: etagere::Rectangle) -> Self {
|
||||
Bounds {
|
||||
origin: rectangle.min.into(),
|
||||
size: rectangle.size().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
use anyhow::Context as _;
|
||||
use blade_graphics as gpu;
|
||||
use std::sync::Arc;
|
||||
use util::ResultExt;
|
||||
|
||||
#[cfg_attr(target_os = "macos", derive(Clone))]
|
||||
pub struct BladeContext {
|
||||
pub(super) gpu: Arc<gpu::Context>,
|
||||
}
|
||||
|
||||
impl BladeContext {
|
||||
pub fn new() -> anyhow::Result<Self> {
|
||||
let device_id_forced = match std::env::var("ZED_DEVICE_ID") {
|
||||
Ok(val) => parse_pci_id(&val)
|
||||
.context("Failed to parse device ID from `ZED_DEVICE_ID` environment variable")
|
||||
.log_err(),
|
||||
Err(std::env::VarError::NotPresent) => None,
|
||||
err => {
|
||||
err.context("Failed to read value of `ZED_DEVICE_ID` environment variable")
|
||||
.log_err();
|
||||
None
|
||||
}
|
||||
};
|
||||
let gpu = Arc::new(
|
||||
unsafe {
|
||||
gpu::Context::init(gpu::ContextDesc {
|
||||
presentation: true,
|
||||
validation: false,
|
||||
device_id: device_id_forced.unwrap_or(0),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
.map_err(|e| anyhow::anyhow!("{e:?}"))?,
|
||||
);
|
||||
Ok(Self { gpu })
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_pci_id(id: &str) -> anyhow::Result<u32> {
|
||||
let mut id = id.trim();
|
||||
|
||||
if id.starts_with("0x") || id.starts_with("0X") {
|
||||
id = &id[2..];
|
||||
}
|
||||
let is_hex_string = id.chars().all(|c| c.is_ascii_hexdigit());
|
||||
let is_4_chars = id.len() == 4;
|
||||
anyhow::ensure!(
|
||||
is_4_chars && is_hex_string,
|
||||
"Expected a 4 digit PCI ID in hexadecimal format"
|
||||
);
|
||||
|
||||
u32::from_str_radix(id, 16).context("parsing PCI ID as hex")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse_pci_id;
|
||||
|
||||
#[test]
|
||||
fn test_parse_device_id() {
|
||||
assert!(parse_pci_id("0xABCD").is_ok());
|
||||
assert!(parse_pci_id("ABCD").is_ok());
|
||||
assert!(parse_pci_id("abcd").is_ok());
|
||||
assert!(parse_pci_id("1234").is_ok());
|
||||
assert!(parse_pci_id("123").is_err());
|
||||
assert_eq!(
|
||||
parse_pci_id(&format!("{:x}", 0x1234)).unwrap(),
|
||||
parse_pci_id(&format!("{:X}", 0x1234)).unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_pci_id(&format!("{:#x}", 0x1234)).unwrap(),
|
||||
parse_pci_id(&format!("{:#X}", 0x1234)).unwrap(),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_pci_id(&format!("{:#x}", 0x1234)).unwrap(),
|
||||
parse_pci_id(&format!("{:#X}", 0x1234)).unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -265,7 +265,8 @@ impl Keystroke {
|
||||
|
||||
impl KeybindingKeystroke {
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn new(inner: Keystroke, display_modifiers: Modifiers, display_key: String) -> Self {
|
||||
#[expect(missing_docs)]
|
||||
pub fn new(inner: Keystroke, display_modifiers: Modifiers, display_key: String) -> Self {
|
||||
KeybindingKeystroke {
|
||||
inner,
|
||||
display_modifiers,
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
use bitflags::bitflags;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::Pixels;
|
||||
|
||||
/// The layer the surface is rendered on. Multiple surfaces can share a layer, and ordering within
|
||||
/// a single layer is undefined.
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Layer {
|
||||
/// The background layer, typically used for wallpapers.
|
||||
Background,
|
||||
|
||||
/// The bottom layer.
|
||||
Bottom,
|
||||
|
||||
/// The top layer, typically used for fullscreen windows.
|
||||
Top,
|
||||
|
||||
/// The overlay layer, used for surfaces that should always be on top.
|
||||
#[default]
|
||||
Overlay,
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Screen anchor point for layer_shell surfaces. These can be used in any combination, e.g.
|
||||
/// specifying `Anchor::LEFT | Anchor::RIGHT` will stretch the surface across the width of the
|
||||
/// screen.
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Anchor: u32 {
|
||||
/// Anchor to the top edge of the screen.
|
||||
const TOP = 1;
|
||||
/// Anchor to the bottom edge of the screen.
|
||||
const BOTTOM = 2;
|
||||
/// Anchor to the left edge of the screen.
|
||||
const LEFT = 4;
|
||||
/// Anchor to the right edge of the screen.
|
||||
const RIGHT = 8;
|
||||
}
|
||||
}
|
||||
|
||||
/// Keyboard interactivity mode for the layer_shell surfaces.
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub enum KeyboardInteractivity {
|
||||
/// No keyboard inputs will be delivered to the surface and it won't be able to receive
|
||||
/// keyboard focus.
|
||||
None,
|
||||
|
||||
/// The surface will receive exclusive keyboard focus as long as it is above the shell surface
|
||||
/// layer, and no other layer_shell surfaces are above it.
|
||||
Exclusive,
|
||||
|
||||
/// The surface can be focused similarly to a normal window.
|
||||
#[default]
|
||||
OnDemand,
|
||||
}
|
||||
|
||||
/// Options for creating a layer_shell window.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct LayerShellOptions {
|
||||
/// The namespace for the surface, mostly used by compositors to apply rules, can not be
|
||||
/// changed after the surface is created.
|
||||
pub namespace: String,
|
||||
/// The layer the surface is rendered on.
|
||||
pub layer: Layer,
|
||||
/// The anchor point of the surface.
|
||||
pub anchor: Anchor,
|
||||
/// Requests that the compositor avoids occluding an area with other surfaces.
|
||||
pub exclusive_zone: Option<Pixels>,
|
||||
/// The anchor point of the exclusive zone, will be determined using the anchor if left
|
||||
/// unspecified.
|
||||
pub exclusive_edge: Option<Anchor>,
|
||||
/// Margins between the surface and its anchor point(s).
|
||||
/// Specified in CSS order: top, right, bottom, left.
|
||||
pub margin: Option<(Pixels, Pixels, Pixels, Pixels)>,
|
||||
/// How keyboard events should be delivered to the surface.
|
||||
pub keyboard_interactivity: KeyboardInteractivity,
|
||||
}
|
||||
|
||||
/// An error indicating that an action failed because the compositor doesn't support the required
|
||||
/// layer_shell protocol.
|
||||
#[derive(Debug, Error)]
|
||||
#[error("Compositor doesn't support zwlr_layer_shell_v1")]
|
||||
pub struct LayerShellNotSupportedError;
|
||||
+32
-7
@@ -12,7 +12,7 @@ mod x11;
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
mod xdg_desktop_portal;
|
||||
|
||||
pub(crate) use dispatcher::*;
|
||||
pub use dispatcher::*;
|
||||
pub(crate) use headless::*;
|
||||
pub(crate) use keyboard::*;
|
||||
pub(crate) use platform::*;
|
||||
@@ -23,10 +23,35 @@ pub(crate) use wayland::*;
|
||||
#[cfg(feature = "x11")]
|
||||
pub(crate) use x11::*;
|
||||
|
||||
#[cfg(all(feature = "screen-capture", any(feature = "wayland", feature = "x11")))]
|
||||
pub(crate) type PlatformScreenCaptureFrame = scap::frame::Frame;
|
||||
#[cfg(not(all(feature = "screen-capture", any(feature = "wayland", feature = "x11"))))]
|
||||
pub(crate) type PlatformScreenCaptureFrame = ();
|
||||
use std::rc::Rc;
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
pub use wayland::layer_shell;
|
||||
/// Returns the default platform implementation for the current OS.
|
||||
pub fn current_platform(headless: bool) -> Rc<dyn gpui::Platform> {
|
||||
#[cfg(feature = "x11")]
|
||||
use anyhow::Context as _;
|
||||
|
||||
if headless {
|
||||
return Rc::new(LinuxPlatform {
|
||||
inner: HeadlessClient::new(),
|
||||
});
|
||||
}
|
||||
|
||||
match gpui::guess_compositor() {
|
||||
#[cfg(feature = "wayland")]
|
||||
"Wayland" => Rc::new(LinuxPlatform {
|
||||
inner: WaylandClient::new(),
|
||||
}),
|
||||
|
||||
#[cfg(feature = "x11")]
|
||||
"X11" => Rc::new(LinuxPlatform {
|
||||
inner: X11Client::new()
|
||||
.context("Failed to initialize X11 client.")
|
||||
.unwrap(),
|
||||
}),
|
||||
|
||||
"Headless" => Rc::new(LinuxPlatform {
|
||||
inner: HeadlessClient::new(),
|
||||
}),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
use crate::{
|
||||
GLOBAL_THREAD_TIMINGS, PlatformDispatcher, Priority, PriorityQueueReceiver,
|
||||
PriorityQueueSender, RealtimePriority, RunnableVariant, THREAD_TIMINGS, TaskLabel, TaskTiming,
|
||||
ThreadTaskTimings, profiler,
|
||||
};
|
||||
use calloop::{
|
||||
EventLoop, PostAction,
|
||||
channel::{self, Sender},
|
||||
timer::TimeoutAction,
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
use std::{
|
||||
mem::MaybeUninit,
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
use gpui::{
|
||||
GLOBAL_THREAD_TIMINGS, PlatformDispatcher, Priority, PriorityQueueReceiver,
|
||||
PriorityQueueSender, RunnableVariant, THREAD_TIMINGS, TaskTiming, ThreadTaskTimings, profiler,
|
||||
};
|
||||
|
||||
struct TimerAfter {
|
||||
duration: Duration,
|
||||
@@ -35,47 +37,28 @@ impl LinuxDispatcher {
|
||||
let thread_count =
|
||||
std::thread::available_parallelism().map_or(MIN_THREADS, |i| i.get().max(MIN_THREADS));
|
||||
|
||||
// These thread should really be lower prio then the foreground
|
||||
// executor
|
||||
let mut background_threads = (0..thread_count)
|
||||
.map(|i| {
|
||||
let mut receiver = background_receiver.clone();
|
||||
let receiver: PriorityQueueReceiver<RunnableVariant> = background_receiver.clone();
|
||||
std::thread::Builder::new()
|
||||
.name(format!("Worker-{i}"))
|
||||
.spawn(move || {
|
||||
for runnable in receiver.iter() {
|
||||
let start = Instant::now();
|
||||
|
||||
let mut location = match runnable {
|
||||
RunnableVariant::Meta(runnable) => {
|
||||
let location = runnable.metadata().location;
|
||||
let timing = TaskTiming {
|
||||
location,
|
||||
start,
|
||||
end: None,
|
||||
};
|
||||
profiler::add_task_timing(timing);
|
||||
|
||||
runnable.run();
|
||||
timing
|
||||
}
|
||||
RunnableVariant::Compat(runnable) => {
|
||||
let location = core::panic::Location::caller();
|
||||
let timing = TaskTiming {
|
||||
location,
|
||||
start,
|
||||
end: None,
|
||||
};
|
||||
profiler::add_task_timing(timing);
|
||||
|
||||
runnable.run();
|
||||
timing
|
||||
}
|
||||
let location = runnable.metadata().location;
|
||||
let mut timing = TaskTiming {
|
||||
location,
|
||||
start,
|
||||
end: None,
|
||||
};
|
||||
profiler::add_task_timing(timing);
|
||||
|
||||
runnable.run();
|
||||
|
||||
let end = Instant::now();
|
||||
location.end = Some(end);
|
||||
profiler::add_task_timing(location);
|
||||
timing.end = Some(end);
|
||||
profiler::add_task_timing(timing);
|
||||
|
||||
log::trace!(
|
||||
"background thread {}: ran runnable. took: {:?}",
|
||||
@@ -91,7 +74,7 @@ impl LinuxDispatcher {
|
||||
let (timer_sender, timer_channel) = calloop::channel::channel::<TimerAfter>();
|
||||
let timer_thread = std::thread::Builder::new()
|
||||
.name("Timer".to_owned())
|
||||
.spawn(|| {
|
||||
.spawn(move || {
|
||||
let mut event_loop: EventLoop<()> =
|
||||
EventLoop::try_new().expect("Failed to initialize timer loop!");
|
||||
|
||||
@@ -100,7 +83,6 @@ impl LinuxDispatcher {
|
||||
handle
|
||||
.insert_source(timer_channel, move |e, _, _| {
|
||||
if let channel::Event::Msg(timer) = e {
|
||||
// This has to be in an option to satisfy the borrow checker. The callback below should only be scheduled once.
|
||||
let mut runnable = Some(timer.runnable);
|
||||
timer_handle
|
||||
.insert_source(
|
||||
@@ -108,31 +90,15 @@ impl LinuxDispatcher {
|
||||
move |_, _, _| {
|
||||
if let Some(runnable) = runnable.take() {
|
||||
let start = Instant::now();
|
||||
let mut timing = match runnable {
|
||||
RunnableVariant::Meta(runnable) => {
|
||||
let location = runnable.metadata().location;
|
||||
let timing = TaskTiming {
|
||||
location,
|
||||
start,
|
||||
end: None,
|
||||
};
|
||||
profiler::add_task_timing(timing);
|
||||
|
||||
runnable.run();
|
||||
timing
|
||||
}
|
||||
RunnableVariant::Compat(runnable) => {
|
||||
let timing = TaskTiming {
|
||||
location: core::panic::Location::caller(),
|
||||
start,
|
||||
end: None,
|
||||
};
|
||||
profiler::add_task_timing(timing);
|
||||
|
||||
runnable.run();
|
||||
timing
|
||||
}
|
||||
let location = runnable.metadata().location;
|
||||
let mut timing = TaskTiming {
|
||||
location,
|
||||
start,
|
||||
end: None,
|
||||
};
|
||||
profiler::add_task_timing(timing);
|
||||
|
||||
runnable.run();
|
||||
let end = Instant::now();
|
||||
|
||||
timing.end = Some(end);
|
||||
@@ -163,14 +129,16 @@ impl LinuxDispatcher {
|
||||
}
|
||||
|
||||
impl PlatformDispatcher for LinuxDispatcher {
|
||||
fn get_all_timings(&self) -> Vec<crate::ThreadTaskTimings> {
|
||||
fn get_all_timings(&self) -> Vec<gpui::ThreadTaskTimings> {
|
||||
let global_timings = GLOBAL_THREAD_TIMINGS.lock();
|
||||
ThreadTaskTimings::convert(&global_timings)
|
||||
}
|
||||
|
||||
fn get_current_thread_timings(&self) -> Vec<crate::TaskTiming> {
|
||||
fn get_current_thread_timings(&self) -> gpui::ThreadTaskTimings {
|
||||
THREAD_TIMINGS.with(|timings| {
|
||||
let timings = timings.lock();
|
||||
let thread_name = timings.thread_name.clone();
|
||||
let total_pushed = timings.total_pushed;
|
||||
let timings = &timings.timings;
|
||||
|
||||
let mut vec = Vec::with_capacity(timings.len());
|
||||
@@ -178,7 +146,13 @@ impl PlatformDispatcher for LinuxDispatcher {
|
||||
let (s1, s2) = timings.as_slices();
|
||||
vec.extend_from_slice(s1);
|
||||
vec.extend_from_slice(s2);
|
||||
vec
|
||||
|
||||
gpui::ThreadTaskTimings {
|
||||
thread_name,
|
||||
thread_id: std::thread::current().id(),
|
||||
timings: vec,
|
||||
total_pushed,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -186,7 +160,7 @@ impl PlatformDispatcher for LinuxDispatcher {
|
||||
thread::current().id() == self.main_thread_id
|
||||
}
|
||||
|
||||
fn dispatch(&self, runnable: RunnableVariant, _: Option<TaskLabel>, priority: Priority) {
|
||||
fn dispatch(&self, runnable: RunnableVariant, priority: Priority) {
|
||||
self.background_sender
|
||||
.send(priority, runnable)
|
||||
.unwrap_or_else(|_| panic!("blocking sender returned without value"));
|
||||
@@ -214,25 +188,22 @@ impl PlatformDispatcher for LinuxDispatcher {
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn spawn_realtime(&self, priority: RealtimePriority, f: Box<dyn FnOnce() + Send>) {
|
||||
fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>) {
|
||||
std::thread::spawn(move || {
|
||||
// SAFETY: always safe to call
|
||||
let thread_id = unsafe { libc::pthread_self() };
|
||||
|
||||
let policy = match priority {
|
||||
RealtimePriority::Audio => libc::SCHED_FIFO,
|
||||
RealtimePriority::Other => libc::SCHED_RR,
|
||||
};
|
||||
let sched_priority = match priority {
|
||||
RealtimePriority::Audio => 65,
|
||||
RealtimePriority::Other => 45,
|
||||
};
|
||||
let policy = libc::SCHED_FIFO;
|
||||
let sched_priority = 65;
|
||||
|
||||
let sched_param = libc::sched_param { sched_priority };
|
||||
// SAFETY: all sched_param members are valid when initialized to zero.
|
||||
let mut sched_param =
|
||||
unsafe { MaybeUninit::<libc::sched_param>::zeroed().assume_init() };
|
||||
sched_param.sched_priority = sched_priority;
|
||||
// SAFETY: sched_param is a valid initialized structure
|
||||
let result = unsafe { libc::pthread_setschedparam(thread_id, policy, &sched_param) };
|
||||
if result != 0 {
|
||||
log::warn!("failed to set realtime thread priority to {:?}", priority);
|
||||
log::warn!("failed to set realtime thread priority");
|
||||
}
|
||||
|
||||
f();
|
||||
@@ -250,7 +221,7 @@ impl<T> PriorityQueueCalloopSender<T> {
|
||||
Self { sender: tx, ping }
|
||||
}
|
||||
|
||||
fn send(&self, priority: Priority, item: T) -> Result<(), crate::queue::SendError<T>> {
|
||||
fn send(&self, priority: Priority, item: T) -> Result<(), gpui::queue::SendError<T>> {
|
||||
let res = self.sender.send(priority, item);
|
||||
if res.is_ok() {
|
||||
self.ping.ping();
|
||||
@@ -330,7 +301,7 @@ impl<T> calloop::EventSource for PriorityQueueCalloopReceiver<T> {
|
||||
.process_events(readiness, token, |(), &mut ()| {
|
||||
let mut is_empty = true;
|
||||
|
||||
let mut receiver = self.receiver.clone();
|
||||
let receiver = self.receiver.clone();
|
||||
for runnable in receiver.try_iter() {
|
||||
match runnable {
|
||||
Ok(r) => {
|
||||
@@ -447,11 +418,11 @@ mod tests {
|
||||
}
|
||||
|
||||
// running 1 test
|
||||
// test platform::linux::dispatcher::tests::tomato ... FAILED
|
||||
// test linux::dispatcher::tests::tomato ... FAILED
|
||||
|
||||
// failures:
|
||||
|
||||
// ---- platform::linux::dispatcher::tests::tomato stdout ----
|
||||
// ---- linux::dispatcher::tests::tomato stdout ----
|
||||
// [crates/gpui/src/platform/linux/dispatcher.rs:262:9]
|
||||
// returning 1 tasks to process
|
||||
// [crates/gpui/src/platform/linux/dispatcher.rs:480:75] evt = Msg(
|
||||
@@ -459,6 +430,6 @@ mod tests {
|
||||
// )
|
||||
// returning 0 tasks to process
|
||||
|
||||
// thread 'platform::linux::dispatcher::tests::tomato' (478301) panicked at crates/gpui/src/platform/linux/dispatcher.rs:515:9:
|
||||
// thread 'linux::dispatcher::tests::tomato' (478301) panicked at crates/gpui/src/platform/linux/dispatcher.rs:515:9:
|
||||
// assertion failed: data.got_closed
|
||||
// note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
||||
|
||||
@@ -4,11 +4,10 @@ use std::rc::Rc;
|
||||
use calloop::{EventLoop, LoopHandle};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::platform::linux::LinuxClient;
|
||||
use crate::platform::{LinuxCommon, PlatformWindow};
|
||||
use crate::{
|
||||
AnyWindowHandle, CursorStyle, DisplayId, LinuxKeyboardLayout, PlatformDisplay,
|
||||
PlatformKeyboardLayout, WindowParams,
|
||||
use crate::platform::linux::{LinuxClient, LinuxCommon, LinuxKeyboardLayout};
|
||||
use gpui::{
|
||||
AnyWindowHandle, CursorStyle, DisplayId, PlatformDisplay, PlatformKeyboardLayout,
|
||||
PlatformWindow, WindowParams,
|
||||
};
|
||||
|
||||
pub struct HeadlessClientState {
|
||||
@@ -31,10 +30,7 @@ impl HeadlessClient {
|
||||
handle
|
||||
.insert_source(main_receiver, |event, _, _: &mut HeadlessClient| {
|
||||
if let calloop::channel::Event::Msg(runnable) = event {
|
||||
match runnable {
|
||||
crate::RunnableVariant::Meta(runnable) => runnable.run(),
|
||||
crate::RunnableVariant::Compat(runnable) => runnable.run(),
|
||||
};
|
||||
runnable.run();
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
@@ -68,17 +64,12 @@ impl LinuxClient for HeadlessClient {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "screen-capture")]
|
||||
fn is_screen_capture_supported(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(feature = "screen-capture")]
|
||||
fn screen_capture_sources(
|
||||
&self,
|
||||
) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>>
|
||||
) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Rc<dyn gpui::ScreenCaptureSource>>>>
|
||||
{
|
||||
let (mut tx, rx) = futures::channel::oneshot::channel();
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
tx.send(Err(anyhow::anyhow!(
|
||||
"Headless mode does not support screen capture."
|
||||
)))
|
||||
@@ -112,15 +103,15 @@ impl LinuxClient for HeadlessClient {
|
||||
|
||||
fn reveal_path(&self, _path: std::path::PathBuf) {}
|
||||
|
||||
fn write_to_primary(&self, _item: crate::ClipboardItem) {}
|
||||
fn write_to_primary(&self, _item: gpui::ClipboardItem) {}
|
||||
|
||||
fn write_to_clipboard(&self, _item: crate::ClipboardItem) {}
|
||||
fn write_to_clipboard(&self, _item: gpui::ClipboardItem) {}
|
||||
|
||||
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
|
||||
fn read_from_primary(&self) -> Option<gpui::ClipboardItem> {
|
||||
None
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
|
||||
fn read_from_clipboard(&self) -> Option<gpui::ClipboardItem> {
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{PlatformKeyboardLayout, SharedString};
|
||||
use gpui::{PlatformKeyboardLayout, SharedString};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct LinuxKeyboardLayout {
|
||||
|
||||
+380
-344
File diff suppressed because it is too large
Load Diff
@@ -1,581 +1 @@
|
||||
use crate::{
|
||||
Bounds, DevicePixels, Font, FontFeatures, FontId, FontMetrics, FontRun, FontStyle, FontWeight,
|
||||
GlyphId, LineLayout, Pixels, PlatformTextSystem, Point, RenderGlyphParams, SUBPIXEL_VARIANTS_X,
|
||||
SUBPIXEL_VARIANTS_Y, ShapedGlyph, ShapedRun, SharedString, Size, point, size,
|
||||
};
|
||||
use anyhow::{Context as _, Ok, Result};
|
||||
use collections::HashMap;
|
||||
use cosmic_text::{
|
||||
Attrs, AttrsList, CacheKey, Family, Font as CosmicTextFont, FontFeatures as CosmicFontFeatures,
|
||||
FontSystem, ShapeBuffer, ShapeLine, SwashCache,
|
||||
};
|
||||
|
||||
use itertools::Itertools;
|
||||
use parking_lot::RwLock;
|
||||
use pathfinder_geometry::{
|
||||
rect::{RectF, RectI},
|
||||
vector::{Vector2F, Vector2I},
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
pub(crate) struct CosmicTextSystem(RwLock<CosmicTextSystemState>);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
struct FontKey {
|
||||
family: SharedString,
|
||||
features: FontFeatures,
|
||||
}
|
||||
|
||||
impl FontKey {
|
||||
fn new(family: SharedString, features: FontFeatures) -> Self {
|
||||
Self { family, features }
|
||||
}
|
||||
}
|
||||
|
||||
struct CosmicTextSystemState {
|
||||
swash_cache: SwashCache,
|
||||
font_system: FontSystem,
|
||||
scratch: ShapeBuffer,
|
||||
/// Contains all already loaded fonts, including all faces. Indexed by `FontId`.
|
||||
loaded_fonts: Vec<LoadedFont>,
|
||||
/// Caches the `FontId`s associated with a specific family to avoid iterating the font database
|
||||
/// for every font face in a family.
|
||||
font_ids_by_family_cache: HashMap<FontKey, SmallVec<[FontId; 4]>>,
|
||||
}
|
||||
|
||||
struct LoadedFont {
|
||||
font: Arc<CosmicTextFont>,
|
||||
features: CosmicFontFeatures,
|
||||
is_known_emoji_font: bool,
|
||||
}
|
||||
|
||||
impl CosmicTextSystem {
|
||||
pub(crate) fn new() -> Self {
|
||||
// todo(linux) make font loading non-blocking
|
||||
let mut font_system = FontSystem::new();
|
||||
|
||||
Self(RwLock::new(CosmicTextSystemState {
|
||||
font_system,
|
||||
swash_cache: SwashCache::new(),
|
||||
scratch: ShapeBuffer::default(),
|
||||
loaded_fonts: Vec::new(),
|
||||
font_ids_by_family_cache: HashMap::default(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CosmicTextSystem {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformTextSystem for CosmicTextSystem {
|
||||
fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
|
||||
self.0.write().add_fonts(fonts)
|
||||
}
|
||||
|
||||
fn all_font_names(&self) -> Vec<String> {
|
||||
let mut result = self
|
||||
.0
|
||||
.read()
|
||||
.font_system
|
||||
.db()
|
||||
.faces()
|
||||
.filter_map(|face| face.families.first().map(|family| family.0.clone()))
|
||||
.collect_vec();
|
||||
result.sort();
|
||||
result.dedup();
|
||||
result
|
||||
}
|
||||
|
||||
fn font_id(&self, font: &Font) -> Result<FontId> {
|
||||
// todo(linux): Do we need to use CosmicText's Font APIs? Can we consolidate this to use font_kit?
|
||||
let mut state = self.0.write();
|
||||
let key = FontKey::new(font.family.clone(), font.features.clone());
|
||||
let candidates = if let Some(font_ids) = state.font_ids_by_family_cache.get(&key) {
|
||||
font_ids.as_slice()
|
||||
} else {
|
||||
let font_ids = state.load_family(&font.family, &font.features)?;
|
||||
state.font_ids_by_family_cache.insert(key.clone(), font_ids);
|
||||
state.font_ids_by_family_cache[&key].as_ref()
|
||||
};
|
||||
|
||||
// todo(linux) ideally we would make fontdb's `find_best_match` pub instead of using font-kit here
|
||||
let candidate_properties = candidates
|
||||
.iter()
|
||||
.map(|font_id| {
|
||||
let database_id = state.loaded_font(*font_id).font.id();
|
||||
let face_info = state.font_system.db().face(database_id).expect("");
|
||||
face_info_into_properties(face_info)
|
||||
})
|
||||
.collect::<SmallVec<[_; 4]>>();
|
||||
|
||||
let ix =
|
||||
font_kit::matching::find_best_match(&candidate_properties, &font_into_properties(font))
|
||||
.context("requested font family contains no font matching the other parameters")?;
|
||||
|
||||
Ok(candidates[ix])
|
||||
}
|
||||
|
||||
fn font_metrics(&self, font_id: FontId) -> FontMetrics {
|
||||
let metrics = self
|
||||
.0
|
||||
.read()
|
||||
.loaded_font(font_id)
|
||||
.font
|
||||
.as_swash()
|
||||
.metrics(&[]);
|
||||
|
||||
FontMetrics {
|
||||
units_per_em: metrics.units_per_em as u32,
|
||||
ascent: metrics.ascent,
|
||||
descent: -metrics.descent, // todo(linux) confirm this is correct
|
||||
line_gap: metrics.leading,
|
||||
underline_position: metrics.underline_offset,
|
||||
underline_thickness: metrics.stroke_size,
|
||||
cap_height: metrics.cap_height,
|
||||
x_height: metrics.x_height,
|
||||
// todo(linux): Compute this correctly
|
||||
bounding_box: Bounds {
|
||||
origin: point(0.0, 0.0),
|
||||
size: size(metrics.max_width, metrics.ascent + metrics.descent),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {
|
||||
let lock = self.0.read();
|
||||
let glyph_metrics = lock.loaded_font(font_id).font.as_swash().glyph_metrics(&[]);
|
||||
let glyph_id = glyph_id.0 as u16;
|
||||
// todo(linux): Compute this correctly
|
||||
// see https://github.com/servo/font-kit/blob/master/src/loaders/freetype.rs#L614-L620
|
||||
Ok(Bounds {
|
||||
origin: point(0.0, 0.0),
|
||||
size: size(
|
||||
glyph_metrics.advance_width(glyph_id),
|
||||
glyph_metrics.advance_height(glyph_id),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
|
||||
self.0.read().advance(font_id, glyph_id)
|
||||
}
|
||||
|
||||
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
|
||||
self.0.read().glyph_for_char(font_id, ch)
|
||||
}
|
||||
|
||||
fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
|
||||
self.0.write().raster_bounds(params)
|
||||
}
|
||||
|
||||
fn rasterize_glyph(
|
||||
&self,
|
||||
params: &RenderGlyphParams,
|
||||
raster_bounds: Bounds<DevicePixels>,
|
||||
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
|
||||
self.0.write().rasterize_glyph(params, raster_bounds)
|
||||
}
|
||||
|
||||
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout {
|
||||
self.0.write().layout_line(text, font_size, runs)
|
||||
}
|
||||
}
|
||||
|
||||
impl CosmicTextSystemState {
|
||||
fn loaded_font(&self, font_id: FontId) -> &LoadedFont {
|
||||
&self.loaded_fonts[font_id.0]
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn add_fonts(&mut self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
|
||||
let db = self.font_system.db_mut();
|
||||
for bytes in fonts {
|
||||
match bytes {
|
||||
Cow::Borrowed(embedded_font) => {
|
||||
db.load_font_data(embedded_font.to_vec());
|
||||
}
|
||||
Cow::Owned(bytes) => {
|
||||
db.load_font_data(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn load_family(
|
||||
&mut self,
|
||||
name: &str,
|
||||
features: &FontFeatures,
|
||||
) -> Result<SmallVec<[FontId; 4]>> {
|
||||
// TODO: Determine the proper system UI font.
|
||||
let name = crate::text_system::font_name_with_fallbacks(name, "IBM Plex Sans");
|
||||
|
||||
let families = self
|
||||
.font_system
|
||||
.db()
|
||||
.faces()
|
||||
.filter(|face| face.families.iter().any(|family| *name == family.0))
|
||||
.map(|face| (face.id, face.post_script_name.clone()))
|
||||
.collect::<SmallVec<[_; 4]>>();
|
||||
|
||||
let mut loaded_font_ids = SmallVec::new();
|
||||
for (font_id, postscript_name) in families {
|
||||
let font = self
|
||||
.font_system
|
||||
.get_font(font_id)
|
||||
.context("Could not load font")?;
|
||||
|
||||
// HACK: To let the storybook run and render Windows caption icons. We should actually do better font fallback.
|
||||
let allowed_bad_font_names = [
|
||||
"SegoeFluentIcons", // NOTE: Segoe fluent icons postscript name is inconsistent
|
||||
"Segoe Fluent Icons",
|
||||
];
|
||||
|
||||
if font.as_swash().charmap().map('m') == 0
|
||||
&& !allowed_bad_font_names.contains(&postscript_name.as_str())
|
||||
{
|
||||
self.font_system.db_mut().remove_face(font.id());
|
||||
continue;
|
||||
};
|
||||
|
||||
let font_id = FontId(self.loaded_fonts.len());
|
||||
loaded_font_ids.push(font_id);
|
||||
self.loaded_fonts.push(LoadedFont {
|
||||
font,
|
||||
features: features.try_into()?,
|
||||
is_known_emoji_font: check_is_known_emoji_font(&postscript_name),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(loaded_font_ids)
|
||||
}
|
||||
|
||||
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
|
||||
let glyph_metrics = self.loaded_font(font_id).font.as_swash().glyph_metrics(&[]);
|
||||
Ok(Size {
|
||||
width: glyph_metrics.advance_width(glyph_id.0 as u16),
|
||||
height: glyph_metrics.advance_height(glyph_id.0 as u16),
|
||||
})
|
||||
}
|
||||
|
||||
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
|
||||
let glyph_id = self.loaded_font(font_id).font.as_swash().charmap().map(ch);
|
||||
if glyph_id == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(GlyphId(glyph_id.into()))
|
||||
}
|
||||
}
|
||||
|
||||
fn raster_bounds(&mut self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
|
||||
let font = &self.loaded_fonts[params.font_id.0].font;
|
||||
let subpixel_shift = point(
|
||||
params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor,
|
||||
params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor,
|
||||
);
|
||||
let image = self
|
||||
.swash_cache
|
||||
.get_image(
|
||||
&mut self.font_system,
|
||||
CacheKey::new(
|
||||
font.id(),
|
||||
params.glyph_id.0 as u16,
|
||||
(params.font_size * params.scale_factor).into(),
|
||||
(subpixel_shift.x, subpixel_shift.y.trunc()),
|
||||
cosmic_text::CacheKeyFlags::empty(),
|
||||
)
|
||||
.0,
|
||||
)
|
||||
.clone()
|
||||
.with_context(|| format!("no image for {params:?} in font {font:?}"))?;
|
||||
Ok(Bounds {
|
||||
origin: point(image.placement.left.into(), (-image.placement.top).into()),
|
||||
size: size(image.placement.width.into(), image.placement.height.into()),
|
||||
})
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn rasterize_glyph(
|
||||
&mut self,
|
||||
params: &RenderGlyphParams,
|
||||
glyph_bounds: Bounds<DevicePixels>,
|
||||
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
|
||||
if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 {
|
||||
anyhow::bail!("glyph bounds are empty");
|
||||
} else {
|
||||
let bitmap_size = glyph_bounds.size;
|
||||
let font = &self.loaded_fonts[params.font_id.0].font;
|
||||
let subpixel_shift = point(
|
||||
params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor,
|
||||
params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor,
|
||||
);
|
||||
let mut image = self
|
||||
.swash_cache
|
||||
.get_image(
|
||||
&mut self.font_system,
|
||||
CacheKey::new(
|
||||
font.id(),
|
||||
params.glyph_id.0 as u16,
|
||||
(params.font_size * params.scale_factor).into(),
|
||||
(subpixel_shift.x, subpixel_shift.y.trunc()),
|
||||
cosmic_text::CacheKeyFlags::empty(),
|
||||
)
|
||||
.0,
|
||||
)
|
||||
.clone()
|
||||
.with_context(|| format!("no image for {params:?} in font {font:?}"))?;
|
||||
|
||||
if params.is_emoji {
|
||||
// Convert from RGBA to BGRA.
|
||||
for pixel in image.data.chunks_exact_mut(4) {
|
||||
pixel.swap(0, 2);
|
||||
}
|
||||
}
|
||||
|
||||
Ok((bitmap_size, image.data))
|
||||
}
|
||||
}
|
||||
|
||||
/// This is used when cosmic_text has chosen a fallback font instead of using the requested
|
||||
/// font, typically to handle some unicode characters. When this happens, `loaded_fonts` may not
|
||||
/// yet have an entry for this fallback font, and so one is added.
|
||||
///
|
||||
/// Note that callers shouldn't use this `FontId` somewhere that will retrieve the corresponding
|
||||
/// `LoadedFont.features`, as it will have an arbitrarily chosen or empty value. The only
|
||||
/// current use of this field is for the *input* of `layout_line`, and so it's fine to use
|
||||
/// `font_id_for_cosmic_id` when computing the *output* of `layout_line`.
|
||||
fn font_id_for_cosmic_id(&mut self, id: cosmic_text::fontdb::ID) -> FontId {
|
||||
if let Some(ix) = self
|
||||
.loaded_fonts
|
||||
.iter()
|
||||
.position(|loaded_font| loaded_font.font.id() == id)
|
||||
{
|
||||
FontId(ix)
|
||||
} else {
|
||||
let font = self.font_system.get_font(id).unwrap();
|
||||
let face = self.font_system.db().face(id).unwrap();
|
||||
|
||||
let font_id = FontId(self.loaded_fonts.len());
|
||||
self.loaded_fonts.push(LoadedFont {
|
||||
font,
|
||||
features: CosmicFontFeatures::new(),
|
||||
is_known_emoji_font: check_is_known_emoji_font(&face.post_script_name),
|
||||
});
|
||||
|
||||
font_id
|
||||
}
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
|
||||
let mut attrs_list = AttrsList::new(&Attrs::new());
|
||||
let mut offs = 0;
|
||||
for run in font_runs {
|
||||
let loaded_font = self.loaded_font(run.font_id);
|
||||
let font = self.font_system.db().face(loaded_font.font.id()).unwrap();
|
||||
|
||||
attrs_list.add_span(
|
||||
offs..(offs + run.len),
|
||||
&Attrs::new()
|
||||
.metadata(run.font_id.0)
|
||||
.family(Family::Name(&font.families.first().unwrap().0))
|
||||
.stretch(font.stretch)
|
||||
.style(font.style)
|
||||
.weight(font.weight)
|
||||
.font_features(loaded_font.features.clone()),
|
||||
);
|
||||
offs += run.len;
|
||||
}
|
||||
|
||||
let line = ShapeLine::new(
|
||||
&mut self.font_system,
|
||||
text,
|
||||
&attrs_list,
|
||||
cosmic_text::Shaping::Advanced,
|
||||
4,
|
||||
);
|
||||
let mut layout_lines = Vec::with_capacity(1);
|
||||
line.layout_to_buffer(
|
||||
&mut self.scratch,
|
||||
font_size.0,
|
||||
None, // We do our own wrapping
|
||||
cosmic_text::Wrap::None,
|
||||
None,
|
||||
&mut layout_lines,
|
||||
None,
|
||||
);
|
||||
let layout = layout_lines.first().unwrap();
|
||||
|
||||
let mut runs: Vec<ShapedRun> = Vec::new();
|
||||
for glyph in &layout.glyphs {
|
||||
let mut font_id = FontId(glyph.metadata);
|
||||
let mut loaded_font = self.loaded_font(font_id);
|
||||
if loaded_font.font.id() != glyph.font_id {
|
||||
font_id = self.font_id_for_cosmic_id(glyph.font_id);
|
||||
loaded_font = self.loaded_font(font_id);
|
||||
}
|
||||
let is_emoji = loaded_font.is_known_emoji_font;
|
||||
|
||||
// HACK: Prevent crash caused by variation selectors.
|
||||
if glyph.glyph_id == 3 && is_emoji {
|
||||
continue;
|
||||
}
|
||||
|
||||
let shaped_glyph = ShapedGlyph {
|
||||
id: GlyphId(glyph.glyph_id as u32),
|
||||
position: point(glyph.x.into(), glyph.y.into()),
|
||||
index: glyph.start,
|
||||
is_emoji,
|
||||
};
|
||||
|
||||
if let Some(last_run) = runs
|
||||
.last_mut()
|
||||
.filter(|last_run| last_run.font_id == font_id)
|
||||
{
|
||||
last_run.glyphs.push(shaped_glyph);
|
||||
} else {
|
||||
runs.push(ShapedRun {
|
||||
font_id,
|
||||
glyphs: vec![shaped_glyph],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
LineLayout {
|
||||
font_size,
|
||||
width: layout.w.into(),
|
||||
ascent: layout.max_ascent.into(),
|
||||
descent: layout.max_descent.into(),
|
||||
runs,
|
||||
len: text.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&FontFeatures> for CosmicFontFeatures {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(features: &FontFeatures) -> Result<Self> {
|
||||
let mut result = CosmicFontFeatures::new();
|
||||
for feature in features.0.iter() {
|
||||
let name_bytes: [u8; 4] = feature
|
||||
.0
|
||||
.as_bytes()
|
||||
.try_into()
|
||||
.context("Incorrect feature flag format")?;
|
||||
|
||||
let tag = cosmic_text::FeatureTag::new(&name_bytes);
|
||||
|
||||
result.set(tag, feature.1);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RectF> for Bounds<f32> {
|
||||
fn from(rect: RectF) -> Self {
|
||||
Bounds {
|
||||
origin: point(rect.origin_x(), rect.origin_y()),
|
||||
size: size(rect.width(), rect.height()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RectI> for Bounds<DevicePixels> {
|
||||
fn from(rect: RectI) -> Self {
|
||||
Bounds {
|
||||
origin: point(DevicePixels(rect.origin_x()), DevicePixels(rect.origin_y())),
|
||||
size: size(DevicePixels(rect.width()), DevicePixels(rect.height())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vector2I> for Size<DevicePixels> {
|
||||
fn from(value: Vector2I) -> Self {
|
||||
size(value.x().into(), value.y().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RectI> for Bounds<i32> {
|
||||
fn from(rect: RectI) -> Self {
|
||||
Bounds {
|
||||
origin: point(rect.origin_x(), rect.origin_y()),
|
||||
size: size(rect.width(), rect.height()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Point<u32>> for Vector2I {
|
||||
fn from(size: Point<u32>) -> Self {
|
||||
Vector2I::new(size.x as i32, size.y as i32)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vector2F> for Size<f32> {
|
||||
fn from(vec: Vector2F) -> Self {
|
||||
size(vec.x(), vec.y())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FontWeight> for cosmic_text::Weight {
|
||||
fn from(value: FontWeight) -> Self {
|
||||
cosmic_text::Weight(value.0 as u16)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FontStyle> for cosmic_text::Style {
|
||||
fn from(style: FontStyle) -> Self {
|
||||
match style {
|
||||
FontStyle::Normal => cosmic_text::Style::Normal,
|
||||
FontStyle::Italic => cosmic_text::Style::Italic,
|
||||
FontStyle::Oblique => cosmic_text::Style::Oblique,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn font_into_properties(font: &crate::Font) -> font_kit::properties::Properties {
|
||||
font_kit::properties::Properties {
|
||||
style: match font.style {
|
||||
crate::FontStyle::Normal => font_kit::properties::Style::Normal,
|
||||
crate::FontStyle::Italic => font_kit::properties::Style::Italic,
|
||||
crate::FontStyle::Oblique => font_kit::properties::Style::Oblique,
|
||||
},
|
||||
weight: font_kit::properties::Weight(font.weight.0),
|
||||
stretch: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn face_info_into_properties(
|
||||
face_info: &cosmic_text::fontdb::FaceInfo,
|
||||
) -> font_kit::properties::Properties {
|
||||
font_kit::properties::Properties {
|
||||
style: match face_info.style {
|
||||
cosmic_text::Style::Normal => font_kit::properties::Style::Normal,
|
||||
cosmic_text::Style::Italic => font_kit::properties::Style::Italic,
|
||||
cosmic_text::Style::Oblique => font_kit::properties::Style::Oblique,
|
||||
},
|
||||
// both libs use the same values for weight
|
||||
weight: font_kit::properties::Weight(face_info.weight.0.into()),
|
||||
stretch: match face_info.stretch {
|
||||
cosmic_text::Stretch::Condensed => font_kit::properties::Stretch::CONDENSED,
|
||||
cosmic_text::Stretch::Expanded => font_kit::properties::Stretch::EXPANDED,
|
||||
cosmic_text::Stretch::ExtraCondensed => font_kit::properties::Stretch::EXTRA_CONDENSED,
|
||||
cosmic_text::Stretch::ExtraExpanded => font_kit::properties::Stretch::EXTRA_EXPANDED,
|
||||
cosmic_text::Stretch::Normal => font_kit::properties::Stretch::NORMAL,
|
||||
cosmic_text::Stretch::SemiCondensed => font_kit::properties::Stretch::SEMI_CONDENSED,
|
||||
cosmic_text::Stretch::SemiExpanded => font_kit::properties::Stretch::SEMI_EXPANDED,
|
||||
cosmic_text::Stretch::UltraCondensed => font_kit::properties::Stretch::ULTRA_CONDENSED,
|
||||
cosmic_text::Stretch::UltraExpanded => font_kit::properties::Stretch::ULTRA_EXPANDED,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn check_is_known_emoji_font(postscript_name: &str) -> bool {
|
||||
// TODO: Include other common emoji fonts
|
||||
postscript_name == "NotoColorEmoji"
|
||||
}
|
||||
pub(crate) use crate::platform::wgpu::CosmicTextSystem;
|
||||
|
||||
@@ -12,38 +12,36 @@ pub(crate) use client::*;
|
||||
|
||||
use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape;
|
||||
|
||||
use crate::CursorStyle;
|
||||
use gpui::CursorStyle;
|
||||
|
||||
impl CursorStyle {
|
||||
pub(super) fn to_shape(self) -> Shape {
|
||||
match self {
|
||||
CursorStyle::Arrow => Shape::Default,
|
||||
CursorStyle::IBeam => Shape::Text,
|
||||
CursorStyle::Crosshair => Shape::Crosshair,
|
||||
CursorStyle::ClosedHand => Shape::Grabbing,
|
||||
CursorStyle::OpenHand => Shape::Grab,
|
||||
CursorStyle::PointingHand => Shape::Pointer,
|
||||
CursorStyle::ResizeLeft => Shape::WResize,
|
||||
CursorStyle::ResizeRight => Shape::EResize,
|
||||
CursorStyle::ResizeLeftRight => Shape::EwResize,
|
||||
CursorStyle::ResizeUp => Shape::NResize,
|
||||
CursorStyle::ResizeDown => Shape::SResize,
|
||||
CursorStyle::ResizeUpDown => Shape::NsResize,
|
||||
CursorStyle::ResizeUpLeftDownRight => Shape::NwseResize,
|
||||
CursorStyle::ResizeUpRightDownLeft => Shape::NeswResize,
|
||||
CursorStyle::ResizeColumn => Shape::ColResize,
|
||||
CursorStyle::ResizeRow => Shape::RowResize,
|
||||
CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText,
|
||||
CursorStyle::OperationNotAllowed => Shape::NotAllowed,
|
||||
CursorStyle::DragLink => Shape::Alias,
|
||||
CursorStyle::DragCopy => Shape::Copy,
|
||||
CursorStyle::ContextualMenu => Shape::ContextMenu,
|
||||
CursorStyle::None => {
|
||||
#[cfg(debug_assertions)]
|
||||
panic!("CursorStyle::None should be handled separately in the client");
|
||||
#[cfg(not(debug_assertions))]
|
||||
Shape::Default
|
||||
}
|
||||
pub(super) fn to_shape(style: CursorStyle) -> Shape {
|
||||
match style {
|
||||
CursorStyle::Arrow => Shape::Default,
|
||||
CursorStyle::IBeam => Shape::Text,
|
||||
CursorStyle::Crosshair => Shape::Crosshair,
|
||||
CursorStyle::ClosedHand => Shape::Grabbing,
|
||||
CursorStyle::OpenHand => Shape::Grab,
|
||||
CursorStyle::PointingHand => Shape::Pointer,
|
||||
CursorStyle::ResizeLeft => Shape::WResize,
|
||||
CursorStyle::ResizeRight => Shape::EResize,
|
||||
CursorStyle::ResizeLeftRight => Shape::EwResize,
|
||||
CursorStyle::ResizeUp => Shape::NResize,
|
||||
CursorStyle::ResizeDown => Shape::SResize,
|
||||
CursorStyle::ResizeUpDown => Shape::NsResize,
|
||||
CursorStyle::ResizeUpLeftDownRight => Shape::NwseResize,
|
||||
CursorStyle::ResizeUpRightDownLeft => Shape::NeswResize,
|
||||
CursorStyle::ResizeColumn => Shape::ColResize,
|
||||
CursorStyle::ResizeRow => Shape::RowResize,
|
||||
CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText,
|
||||
CursorStyle::OperationNotAllowed => Shape::NotAllowed,
|
||||
CursorStyle::DragLink => Shape::Alias,
|
||||
CursorStyle::DragCopy => Shape::Copy,
|
||||
CursorStyle::ContextualMenu => Shape::ContextMenu,
|
||||
CursorStyle::None => {
|
||||
#[cfg(debug_assertions)]
|
||||
panic!("CursorStyle::None should be handled separately in the client");
|
||||
#[cfg(not(debug_assertions))]
|
||||
Shape::Default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,11 +36,8 @@ use wayland_client::{
|
||||
wl_shm_pool, wl_surface,
|
||||
},
|
||||
};
|
||||
use wayland_protocols::wp::cursor_shape::v1::client::{
|
||||
wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1,
|
||||
};
|
||||
use wayland_protocols::wp::fractional_scale::v1::client::{
|
||||
wp_fractional_scale_manager_v1, wp_fractional_scale_v1,
|
||||
use wayland_protocols::wp::pointer_gestures::zv1::client::{
|
||||
zwp_pointer_gesture_pinch_v1, zwp_pointer_gestures_v1,
|
||||
};
|
||||
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::{
|
||||
self, ZwpPrimarySelectionOfferV1,
|
||||
@@ -61,6 +58,14 @@ use wayland_protocols::xdg::decoration::zv1::client::{
|
||||
zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1,
|
||||
};
|
||||
use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
|
||||
use wayland_protocols::{
|
||||
wp::cursor_shape::v1::client::{wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1},
|
||||
xdg::dialog::v1::client::xdg_wm_dialog_v1::{self, XdgWmDialogV1},
|
||||
};
|
||||
use wayland_protocols::{
|
||||
wp::fractional_scale::v1::client::{wp_fractional_scale_manager_v1, wp_fractional_scale_v1},
|
||||
xdg::dialog::v1::client::xdg_dialog_v1::XdgDialogV1,
|
||||
};
|
||||
use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager};
|
||||
use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1};
|
||||
use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
|
||||
@@ -71,31 +76,31 @@ use super::{
|
||||
window::{ImeInput, WaylandWindowStatePtr},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
AnyWindowHandle, Bounds, Capslock, CursorStyle, DOUBLE_CLICK_INTERVAL, DevicePixels, DisplayId,
|
||||
FileDropEvent, ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, LinuxCommon,
|
||||
LinuxKeyboardLayout, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent,
|
||||
MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, PlatformDisplay,
|
||||
PlatformInput, PlatformKeyboardLayout, Point, ResultExt as _, SCROLL_LINES, ScrollDelta,
|
||||
ScrollWheelEvent, Size, TouchPhase, WindowParams, point, profiler, px, size,
|
||||
};
|
||||
use crate::{
|
||||
RunnableVariant, TaskTiming,
|
||||
platform::{PlatformWindow, blade::BladeContext},
|
||||
};
|
||||
use crate::{
|
||||
SharedString,
|
||||
platform::linux::{
|
||||
LinuxClient, get_xkb_compose_state, is_within_click_distance, open_uri_internal, read_fd,
|
||||
reveal_path_internal,
|
||||
wayland::{
|
||||
clipboard::{Clipboard, DataOffer, FILE_LIST_MIME_TYPE, TEXT_MIME_TYPES},
|
||||
cursor::Cursor,
|
||||
serial::{SerialKind, SerialTracker},
|
||||
window::WaylandWindow,
|
||||
},
|
||||
xdg_desktop_portal::{Event as XDPEvent, XDPEventSource},
|
||||
use crate::platform::linux::{
|
||||
DOUBLE_CLICK_INTERVAL, LinuxClient, LinuxCommon, LinuxKeyboardLayout, SCROLL_LINES,
|
||||
capslock_from_xkb, cursor_style_to_icon_names, get_xkb_compose_state, is_within_click_distance,
|
||||
keystroke_from_xkb, keystroke_underlying_dead_key, modifiers_from_xkb, open_uri_internal,
|
||||
read_fd, reveal_path_internal,
|
||||
wayland::{
|
||||
clipboard::{Clipboard, DataOffer, FILE_LIST_MIME_TYPE, TEXT_MIME_TYPES},
|
||||
cursor::Cursor,
|
||||
serial::{SerialKind, SerialTracker},
|
||||
to_shape,
|
||||
window::WaylandWindow,
|
||||
},
|
||||
xdg_desktop_portal::{Event as XDPEvent, XDPEventSource},
|
||||
};
|
||||
use crate::platform::wgpu::{CompositorGpuHint, GpuContext};
|
||||
use gpui::{
|
||||
AnyWindowHandle, Bounds, Capslock, CursorStyle, DevicePixels, DisplayId, FileDropEvent,
|
||||
ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent,
|
||||
MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection,
|
||||
Pixels, PlatformDisplay, PlatformInput, PlatformKeyboardLayout, PlatformWindow, Point,
|
||||
ScrollDelta, ScrollWheelEvent, SharedString, Size, TaskTiming, TouchPhase, WindowParams, point,
|
||||
profiler, px, size,
|
||||
};
|
||||
use wayland_protocols::wp::linux_dmabuf::zv1::client::{
|
||||
zwp_linux_dmabuf_feedback_v1, zwp_linux_dmabuf_v1,
|
||||
};
|
||||
|
||||
/// Used to convert evdev scancode to xkb scancode
|
||||
@@ -122,6 +127,8 @@ pub struct Globals {
|
||||
pub layer_shell: Option<zwlr_layer_shell_v1::ZwlrLayerShellV1>,
|
||||
pub blur_manager: Option<org_kde_kwin_blur_manager::OrgKdeKwinBlurManager>,
|
||||
pub text_input_manager: Option<zwp_text_input_manager_v3::ZwpTextInputManagerV3>,
|
||||
pub gesture_manager: Option<zwp_pointer_gestures_v1::ZwpPointerGesturesV1>,
|
||||
pub dialog: Option<xdg_wm_dialog_v1::XdgWmDialogV1>,
|
||||
pub executor: ForegroundExecutor,
|
||||
}
|
||||
|
||||
@@ -132,6 +139,7 @@ impl Globals {
|
||||
qh: QueueHandle<WaylandClientStatePtr>,
|
||||
seat: wl_seat::WlSeat,
|
||||
) -> Self {
|
||||
let dialog_v = XdgWmDialogV1::interface().version;
|
||||
Globals {
|
||||
activation: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
compositor: globals
|
||||
@@ -153,13 +161,15 @@ impl Globals {
|
||||
primary_selection_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
shm: globals.bind(&qh, 1..=1, ()).unwrap(),
|
||||
seat,
|
||||
wm_base: globals.bind(&qh, 2..=5, ()).unwrap(),
|
||||
wm_base: globals.bind(&qh, 1..=5, ()).unwrap(),
|
||||
viewporter: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
fractional_scale_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
decoration_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
layer_shell: globals.bind(&qh, 1..=5, ()).ok(),
|
||||
blur_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
text_input_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
gesture_manager: globals.bind(&qh, 1..=3, ()).ok(),
|
||||
dialog: globals.bind(&qh, dialog_v..=dialog_v, ()).ok(),
|
||||
executor,
|
||||
qh,
|
||||
}
|
||||
@@ -199,9 +209,12 @@ pub struct Output {
|
||||
pub(crate) struct WaylandClientState {
|
||||
serial_tracker: SerialTracker,
|
||||
globals: Globals,
|
||||
gpu_context: BladeContext,
|
||||
pub gpu_context: GpuContext,
|
||||
pub compositor_gpu: Option<CompositorGpuHint>,
|
||||
wl_seat: wl_seat::WlSeat, // TODO: Multi seat support
|
||||
wl_pointer: Option<wl_pointer::WlPointer>,
|
||||
pinch_gesture: Option<zwp_pointer_gesture_pinch_v1::ZwpPointerGesturePinchV1>,
|
||||
pinch_scale: f32,
|
||||
wl_keyboard: Option<wl_keyboard::WlKeyboard>,
|
||||
cursor_shape_device: Option<wp_cursor_shape_device_v1::WpCursorShapeDeviceV1>,
|
||||
data_device: Option<wl_data_device::WlDataDevice>,
|
||||
@@ -215,6 +228,7 @@ pub(crate) struct WaylandClientState {
|
||||
// Output to scale mapping
|
||||
outputs: HashMap<ObjectId, Output>,
|
||||
in_progress_outputs: HashMap<ObjectId, InProgressOutput>,
|
||||
wl_outputs: HashMap<ObjectId, wl_output::WlOutput>,
|
||||
keyboard_layout: LinuxKeyboardLayout,
|
||||
keymap_state: Option<xkb::State>,
|
||||
compose_state: Option<xkb::compose::State>,
|
||||
@@ -242,7 +256,7 @@ pub(crate) struct WaylandClientState {
|
||||
cursor: Cursor,
|
||||
pending_activation: Option<PendingActivation>,
|
||||
event_loop: Option<EventLoop<'static, WaylandClientStatePtr>>,
|
||||
common: LinuxCommon,
|
||||
pub common: LinuxCommon,
|
||||
}
|
||||
|
||||
pub struct DragState {
|
||||
@@ -298,7 +312,7 @@ impl WaylandClientStatePtr {
|
||||
pub fn enable_ime(&self) {
|
||||
let client = self.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
let Some(mut text_input) = state.text_input.take() else {
|
||||
let Some(text_input) = state.text_input.take() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -308,10 +322,10 @@ impl WaylandClientStatePtr {
|
||||
drop(state);
|
||||
if let Some(area) = window.get_ime_area() {
|
||||
text_input.set_cursor_rectangle(
|
||||
area.origin.x.0 as i32,
|
||||
area.origin.y.0 as i32,
|
||||
area.size.width.0 as i32,
|
||||
area.size.height.0 as i32,
|
||||
f32::from(area.origin.x) as i32,
|
||||
f32::from(area.origin.y) as i32,
|
||||
f32::from(area.size.width) as i32,
|
||||
f32::from(area.size.height) as i32,
|
||||
);
|
||||
}
|
||||
state = client.borrow_mut();
|
||||
@@ -332,17 +346,17 @@ impl WaylandClientStatePtr {
|
||||
|
||||
pub fn update_ime_position(&self, bounds: Bounds<Pixels>) {
|
||||
let client = self.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
let state = client.borrow_mut();
|
||||
if state.composing || state.text_input.is_none() || state.pre_edit_text.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let text_input = state.text_input.as_ref().unwrap();
|
||||
text_input.set_cursor_rectangle(
|
||||
bounds.origin.x.0 as i32,
|
||||
bounds.origin.y.0 as i32,
|
||||
bounds.size.width.0 as i32,
|
||||
bounds.size.height.0 as i32,
|
||||
bounds.origin.x.as_f32() as i32,
|
||||
bounds.origin.y.as_f32() as i32,
|
||||
bounds.size.width.as_f32() as i32,
|
||||
bounds.size.height.as_f32() as i32,
|
||||
);
|
||||
text_input.commit();
|
||||
}
|
||||
@@ -377,7 +391,7 @@ impl WaylandClientStatePtr {
|
||||
}
|
||||
|
||||
pub fn drop_window(&self, surface_id: &ObjectId) {
|
||||
let mut client = self.get_client();
|
||||
let client = self.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
let closed_window = state.windows.remove(surface_id).unwrap();
|
||||
if let Some(window) = state.mouse_focused_window.take()
|
||||
@@ -451,13 +465,14 @@ impl WaylandClient {
|
||||
pub(crate) fn new() -> Self {
|
||||
let conn = Connection::connect_to_env().unwrap();
|
||||
|
||||
let (globals, mut event_queue) =
|
||||
registry_queue_init::<WaylandClientStatePtr>(&conn).unwrap();
|
||||
let (globals, event_queue) = registry_queue_init::<WaylandClientStatePtr>(&conn).unwrap();
|
||||
let qh = event_queue.handle();
|
||||
|
||||
let mut seat: Option<wl_seat::WlSeat> = None;
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
let mut in_progress_outputs = HashMap::default();
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
let mut wl_outputs: HashMap<ObjectId, wl_output::WlOutput> = HashMap::default();
|
||||
globals.contents().with_list(|list| {
|
||||
for global in list {
|
||||
match &global.interface[..] {
|
||||
@@ -477,6 +492,7 @@ impl WaylandClient {
|
||||
(),
|
||||
);
|
||||
in_progress_outputs.insert(output.id(), InProgressOutput::default());
|
||||
wl_outputs.insert(output.id(), output);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -495,32 +511,15 @@ impl WaylandClient {
|
||||
if let calloop::channel::Event::Msg(runnable) = event {
|
||||
handle.insert_idle(|_| {
|
||||
let start = Instant::now();
|
||||
let mut timing = match runnable {
|
||||
RunnableVariant::Meta(runnable) => {
|
||||
let location = runnable.metadata().location;
|
||||
let timing = TaskTiming {
|
||||
location,
|
||||
start,
|
||||
end: None,
|
||||
};
|
||||
profiler::add_task_timing(timing);
|
||||
|
||||
runnable.run();
|
||||
timing
|
||||
}
|
||||
RunnableVariant::Compat(runnable) => {
|
||||
let location = core::panic::Location::caller();
|
||||
let timing = TaskTiming {
|
||||
location,
|
||||
start,
|
||||
end: None,
|
||||
};
|
||||
profiler::add_task_timing(timing);
|
||||
|
||||
runnable.run();
|
||||
timing
|
||||
}
|
||||
let location = runnable.metadata().location;
|
||||
let mut timing = TaskTiming {
|
||||
location,
|
||||
start,
|
||||
end: None,
|
||||
};
|
||||
profiler::add_task_timing(timing);
|
||||
|
||||
runnable.run();
|
||||
|
||||
let end = Instant::now();
|
||||
timing.end = Some(end);
|
||||
@@ -531,8 +530,8 @@ impl WaylandClient {
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// This could be unified with the notification handling in zed/main:fail_to_open_window.
|
||||
let gpu_context = BladeContext::new().notify_err("Unable to init GPU context");
|
||||
let compositor_gpu = detect_compositor_gpu();
|
||||
let gpu_context = Rc::new(RefCell::new(None));
|
||||
|
||||
let seat = seat.unwrap();
|
||||
let globals = Globals::new(
|
||||
@@ -552,7 +551,7 @@ impl WaylandClient {
|
||||
.as_ref()
|
||||
.map(|primary_selection_manager| primary_selection_manager.get_device(&seat, &qh, ()));
|
||||
|
||||
let mut cursor = Cursor::new(&conn, &globals, 24);
|
||||
let cursor = Cursor::new(&conn, &globals, 24);
|
||||
|
||||
handle
|
||||
.insert_source(XDPEventSource::new(&common.background_executor), {
|
||||
@@ -584,13 +583,16 @@ impl WaylandClient {
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let mut state = Rc::new(RefCell::new(WaylandClientState {
|
||||
let state = Rc::new(RefCell::new(WaylandClientState {
|
||||
serial_tracker: SerialTracker::new(),
|
||||
globals,
|
||||
gpu_context,
|
||||
compositor_gpu,
|
||||
wl_seat: seat,
|
||||
wl_pointer: None,
|
||||
wl_keyboard: None,
|
||||
pinch_gesture: None,
|
||||
pinch_scale: 1.0,
|
||||
cursor_shape_device: None,
|
||||
data_device,
|
||||
primary_selection,
|
||||
@@ -600,6 +602,7 @@ impl WaylandClient {
|
||||
composing: false,
|
||||
outputs: HashMap::default(),
|
||||
in_progress_outputs,
|
||||
wl_outputs,
|
||||
windows: HashMap::default(),
|
||||
common,
|
||||
keyboard_layout: LinuxKeyboardLayout::new(UNKNOWN_KEYBOARD_LAYOUT_NAME),
|
||||
@@ -685,7 +688,7 @@ impl LinuxClient for WaylandClient {
|
||||
.outputs
|
||||
.iter()
|
||||
.find_map(|(object_id, output)| {
|
||||
(object_id.protocol_id() == id.0).then(|| {
|
||||
(object_id.protocol_id() == u32::from(id)).then(|| {
|
||||
Rc::new(WaylandDisplay {
|
||||
id: object_id.clone(),
|
||||
name: output.name.clone(),
|
||||
@@ -699,15 +702,10 @@ impl LinuxClient for WaylandClient {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "screen-capture")]
|
||||
fn is_screen_capture_supported(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(feature = "screen-capture")]
|
||||
fn screen_capture_sources(
|
||||
&self,
|
||||
) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>>
|
||||
) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Rc<dyn gpui::ScreenCaptureSource>>>>
|
||||
{
|
||||
// TODO: Get screen capture working on wayland. Be sure to try window resizing as that may
|
||||
// be tricky.
|
||||
@@ -729,19 +727,29 @@ impl LinuxClient for WaylandClient {
|
||||
) -> anyhow::Result<Box<dyn PlatformWindow>> {
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
let parent = state
|
||||
.keyboard_focused_window
|
||||
.as_ref()
|
||||
.and_then(|w| w.toplevel());
|
||||
let parent = state.keyboard_focused_window.clone();
|
||||
|
||||
let target_output = params.display_id.and_then(|display_id| {
|
||||
let target_protocol_id: u32 = display_id.into();
|
||||
state
|
||||
.wl_outputs
|
||||
.iter()
|
||||
.find(|(id, _)| id.protocol_id() == target_protocol_id)
|
||||
.map(|(_, output)| output.clone())
|
||||
});
|
||||
|
||||
let appearance = state.common.appearance;
|
||||
let compositor_gpu = state.compositor_gpu.take();
|
||||
let (window, surface_id) = WaylandWindow::new(
|
||||
handle,
|
||||
state.globals.clone(),
|
||||
&state.gpu_context,
|
||||
state.gpu_context.clone(),
|
||||
compositor_gpu,
|
||||
WaylandClientStatePtr(Rc::downgrade(&self.0)),
|
||||
params,
|
||||
state.common.appearance,
|
||||
appearance,
|
||||
parent,
|
||||
target_output,
|
||||
)?;
|
||||
state.windows.insert(surface_id, window.0.clone());
|
||||
|
||||
@@ -751,7 +759,12 @@ impl LinuxClient for WaylandClient {
|
||||
fn set_cursor_style(&self, style: CursorStyle) {
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
let need_update = state.cursor_style != Some(style);
|
||||
let need_update = state.cursor_style != Some(style)
|
||||
&& (state.mouse_focused_window.is_none()
|
||||
|| state
|
||||
.mouse_focused_window
|
||||
.as_ref()
|
||||
.is_some_and(|w| !w.is_blocked()));
|
||||
|
||||
if need_update {
|
||||
let serial = state.serial_tracker.get(SerialKind::MouseEnter);
|
||||
@@ -764,7 +777,7 @@ impl LinuxClient for WaylandClient {
|
||||
.expect("window is focused by pointer");
|
||||
wl_pointer.set_cursor(serial, None, 0, 0);
|
||||
} else if let Some(cursor_shape_device) = &state.cursor_shape_device {
|
||||
cursor_shape_device.set_shape(serial, style.to_shape());
|
||||
cursor_shape_device.set_shape(serial, to_shape(style));
|
||||
} else if let Some(focused_window) = &state.mouse_focused_window {
|
||||
// cursor-shape-v1 isn't supported, set the cursor using a surface.
|
||||
let wl_pointer = state
|
||||
@@ -772,9 +785,12 @@ impl LinuxClient for WaylandClient {
|
||||
.clone()
|
||||
.expect("window is focused by pointer");
|
||||
let scale = focused_window.primary_output_scale();
|
||||
state
|
||||
.cursor
|
||||
.set_icon(&wl_pointer, serial, style.to_icon_names(), scale);
|
||||
state.cursor.set_icon(
|
||||
&wl_pointer,
|
||||
serial,
|
||||
cursor_style_to_icon_names(style),
|
||||
scale,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -836,7 +852,7 @@ impl LinuxClient for WaylandClient {
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn write_to_primary(&self, item: crate::ClipboardItem) {
|
||||
fn write_to_primary(&self, item: gpui::ClipboardItem) {
|
||||
let mut state = self.0.borrow_mut();
|
||||
let (Some(primary_selection_manager), Some(primary_selection)) = (
|
||||
state.globals.primary_selection_manager.clone(),
|
||||
@@ -856,7 +872,7 @@ impl LinuxClient for WaylandClient {
|
||||
}
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: crate::ClipboardItem) {
|
||||
fn write_to_clipboard(&self, item: gpui::ClipboardItem) {
|
||||
let mut state = self.0.borrow_mut();
|
||||
let (Some(data_device_manager), Some(data_device)) = (
|
||||
state.globals.data_device_manager.clone(),
|
||||
@@ -876,11 +892,11 @@ impl LinuxClient for WaylandClient {
|
||||
}
|
||||
}
|
||||
|
||||
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
|
||||
fn read_from_primary(&self) -> Option<gpui::ClipboardItem> {
|
||||
self.0.borrow_mut().clipboard.read_primary()
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
|
||||
fn read_from_clipboard(&self) -> Option<gpui::ClipboardItem> {
|
||||
self.0.borrow_mut().clipboard.read()
|
||||
}
|
||||
|
||||
@@ -915,6 +931,70 @@ impl LinuxClient for WaylandClient {
|
||||
}
|
||||
}
|
||||
|
||||
struct DmabufProbeState {
|
||||
device: Option<u64>,
|
||||
}
|
||||
|
||||
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for DmabufProbeState {
|
||||
fn event(
|
||||
_: &mut Self,
|
||||
_: &wl_registry::WlRegistry,
|
||||
_: wl_registry::Event,
|
||||
_: &GlobalListContents,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, ()> for DmabufProbeState {
|
||||
fn event(
|
||||
_: &mut Self,
|
||||
_: &zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1,
|
||||
_: zwp_linux_dmabuf_v1::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1, ()> for DmabufProbeState {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_: &zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1,
|
||||
event: zwp_linux_dmabuf_feedback_v1::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
if let zwp_linux_dmabuf_feedback_v1::Event::MainDevice { device } = event {
|
||||
if let Ok(bytes) = <[u8; 8]>::try_from(device.as_slice()) {
|
||||
state.device = Some(u64::from_ne_bytes(bytes));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_compositor_gpu() -> Option<CompositorGpuHint> {
|
||||
let connection = Connection::connect_to_env().ok()?;
|
||||
let (globals, mut event_queue) = registry_queue_init::<DmabufProbeState>(&connection).ok()?;
|
||||
let queue_handle = event_queue.handle();
|
||||
|
||||
let dmabuf: zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1 =
|
||||
globals.bind(&queue_handle, 4..=4, ()).ok()?;
|
||||
let feedback = dmabuf.get_default_feedback(&queue_handle, ());
|
||||
|
||||
let mut state = DmabufProbeState { device: None };
|
||||
|
||||
event_queue.roundtrip(&mut state).ok()?;
|
||||
|
||||
feedback.destroy();
|
||||
dmabuf.destroy();
|
||||
|
||||
crate::platform::linux::compositor_gpu_hint_from_dev_t(state.device?)
|
||||
}
|
||||
|
||||
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClientStatePtr {
|
||||
fn event(
|
||||
this: &mut Self,
|
||||
@@ -924,7 +1004,7 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClientStat
|
||||
_: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
) {
|
||||
let mut client = this.get_client();
|
||||
let client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
|
||||
match event {
|
||||
@@ -959,6 +1039,7 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClientStat
|
||||
state
|
||||
.in_progress_outputs
|
||||
.insert(output.id(), InProgressOutput::default());
|
||||
state.wl_outputs.insert(output.id(), output);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
@@ -1011,8 +1092,8 @@ impl Dispatch<WlCallback, ObjectId> for WaylandClientStatePtr {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_window(
|
||||
mut state: &mut RefMut<WaylandClientState>,
|
||||
pub(crate) fn get_window(
|
||||
state: &mut RefMut<WaylandClientState>,
|
||||
surface_id: &ObjectId,
|
||||
) -> Option<WaylandWindowStatePtr> {
|
||||
state.windows.get(surface_id).cloned()
|
||||
@@ -1027,7 +1108,7 @@ impl Dispatch<wl_surface::WlSurface, ()> for WaylandClientStatePtr {
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
let mut client = this.get_client();
|
||||
let client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
|
||||
let Some(window) = get_window(&mut state, &surface.id()) else {
|
||||
@@ -1050,10 +1131,10 @@ impl Dispatch<wl_output::WlOutput, ()> for WaylandClientStatePtr {
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
let mut client = this.get_client();
|
||||
let client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
|
||||
let Some(mut in_progress_output) = state.in_progress_outputs.get_mut(&output.id()) else {
|
||||
let Some(in_progress_output) = state.in_progress_outputs.get_mut(&output.id()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1217,6 +1298,12 @@ impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientStatePtr {
|
||||
if capabilities.contains(wl_seat::Capability::Keyboard) {
|
||||
let keyboard = seat.get_keyboard(qh, ());
|
||||
|
||||
if let Some(text_input) = state.text_input.take() {
|
||||
text_input.destroy();
|
||||
state.ime_pre_edit = None;
|
||||
state.composing = false;
|
||||
}
|
||||
|
||||
state.text_input = state
|
||||
.globals
|
||||
.text_input_manager
|
||||
@@ -1231,12 +1318,23 @@ impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientStatePtr {
|
||||
}
|
||||
if capabilities.contains(wl_seat::Capability::Pointer) {
|
||||
let pointer = seat.get_pointer(qh, ());
|
||||
|
||||
if let Some(cursor_shape_device) = state.cursor_shape_device.take() {
|
||||
cursor_shape_device.destroy();
|
||||
}
|
||||
|
||||
state.cursor_shape_device = state
|
||||
.globals
|
||||
.cursor_shape_manager
|
||||
.as_ref()
|
||||
.map(|cursor_shape_manager| cursor_shape_manager.get_pointer(&pointer, qh, ()));
|
||||
|
||||
state.pinch_gesture = state.globals.gesture_manager.as_ref().map(
|
||||
|gesture_manager: &zwp_pointer_gestures_v1::ZwpPointerGesturesV1| {
|
||||
gesture_manager.get_pinch_gesture(&pointer, qh, ())
|
||||
},
|
||||
);
|
||||
|
||||
if let Some(wl_pointer) = &state.wl_pointer {
|
||||
wl_pointer.release();
|
||||
}
|
||||
@@ -1256,7 +1354,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
let mut client = this.get_client();
|
||||
let client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
match event {
|
||||
wl_keyboard::Event::RepeatInfo { rate, delay } => {
|
||||
@@ -1331,9 +1429,9 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
let old_layout =
|
||||
keymap_state.serialize_layout(xkbcommon::xkb::STATE_LAYOUT_EFFECTIVE);
|
||||
keymap_state.update_mask(mods_depressed, mods_latched, mods_locked, 0, 0, group);
|
||||
state.modifiers = Modifiers::from_xkb(keymap_state);
|
||||
state.modifiers = modifiers_from_xkb(keymap_state);
|
||||
let keymap_state = state.keymap_state.as_mut().unwrap();
|
||||
state.capslock = Capslock::from_xkb(keymap_state);
|
||||
state.capslock = capslock_from_xkb(keymap_state);
|
||||
|
||||
let input = PlatformInput::ModifiersChanged(ModifiersChangedEvent {
|
||||
modifiers: state.modifiers,
|
||||
@@ -1369,14 +1467,14 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
match key_state {
|
||||
wl_keyboard::KeyState::Pressed if !keysym.is_modifier_key() => {
|
||||
let mut keystroke =
|
||||
Keystroke::from_xkb(keymap_state, state.modifiers, keycode);
|
||||
keystroke_from_xkb(keymap_state, state.modifiers, keycode);
|
||||
if let Some(mut compose) = state.compose_state.take() {
|
||||
compose.feed(keysym);
|
||||
match compose.status() {
|
||||
xkb::Status::Composing => {
|
||||
keystroke.key_char = None;
|
||||
state.pre_edit_text =
|
||||
compose.utf8().or(Keystroke::underlying_dead_key(keysym));
|
||||
compose.utf8().or(keystroke_underlying_dead_key(keysym));
|
||||
let pre_edit =
|
||||
state.pre_edit_text.clone().unwrap_or(String::default());
|
||||
drop(state);
|
||||
@@ -1393,7 +1491,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
}
|
||||
xkb::Status::Cancelled => {
|
||||
let pre_edit = state.pre_edit_text.take();
|
||||
let new_pre_edit = Keystroke::underlying_dead_key(keysym);
|
||||
let new_pre_edit = keystroke_underlying_dead_key(keysym);
|
||||
state.pre_edit_text = new_pre_edit.clone();
|
||||
drop(state);
|
||||
if let Some(pre_edit) = pre_edit {
|
||||
@@ -1431,8 +1529,8 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
prefer_character_input: false,
|
||||
});
|
||||
move |event_timestamp, _metadata, this| {
|
||||
let mut client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
let client = this.get_client();
|
||||
let state = client.borrow();
|
||||
let is_repeating = id == state.repeat.current_id
|
||||
&& state.repeat.current_keycode.is_some()
|
||||
&& state.keyboard_focused_window.is_some();
|
||||
@@ -1458,7 +1556,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
}
|
||||
wl_keyboard::KeyState::Released if !keysym.is_modifier_key() => {
|
||||
let input = PlatformInput::KeyUp(KeyUpEvent {
|
||||
keystroke: Keystroke::from_xkb(keymap_state, state.modifiers, keycode),
|
||||
keystroke: keystroke_from_xkb(keymap_state, state.modifiers, keycode),
|
||||
});
|
||||
|
||||
if state.repeat.current_keycode == Some(keycode) {
|
||||
@@ -1537,10 +1635,10 @@ impl Dispatch<zwp_text_input_v3::ZwpTextInputV3, ()> for WaylandClientStatePtr {
|
||||
window.handle_ime(ImeInput::SetMarkedText(text));
|
||||
if let Some(area) = window.get_ime_area() {
|
||||
text_input.set_cursor_rectangle(
|
||||
area.origin.x.0 as i32,
|
||||
area.origin.y.0 as i32,
|
||||
area.size.width.0 as i32,
|
||||
area.size.height.0 as i32,
|
||||
f32::from(area.origin.x) as i32,
|
||||
f32::from(area.origin.y) as i32,
|
||||
f32::from(area.size.width) as i32,
|
||||
f32::from(area.size.height) as i32,
|
||||
);
|
||||
if last_serial == serial {
|
||||
text_input.commit();
|
||||
@@ -1586,7 +1684,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
let mut client = this.get_client();
|
||||
let client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
|
||||
match event {
|
||||
@@ -1615,12 +1713,15 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
.expect("window is focused by pointer");
|
||||
wl_pointer.set_cursor(serial, None, 0, 0);
|
||||
} else if let Some(cursor_shape_device) = &state.cursor_shape_device {
|
||||
cursor_shape_device.set_shape(serial, style.to_shape());
|
||||
cursor_shape_device.set_shape(serial, to_shape(style));
|
||||
} else {
|
||||
let scale = window.primary_output_scale();
|
||||
state
|
||||
.cursor
|
||||
.set_icon(wl_pointer, serial, style.to_icon_names(), scale);
|
||||
state.cursor.set_icon(
|
||||
wl_pointer,
|
||||
serial,
|
||||
cursor_style_to_icon_names(style),
|
||||
scale,
|
||||
);
|
||||
}
|
||||
}
|
||||
drop(state);
|
||||
@@ -1654,6 +1755,30 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32)));
|
||||
|
||||
if let Some(window) = state.mouse_focused_window.clone() {
|
||||
if window.is_blocked() {
|
||||
let default_style = CursorStyle::Arrow;
|
||||
if state.cursor_style != Some(default_style) {
|
||||
let serial = state.serial_tracker.get(SerialKind::MouseEnter);
|
||||
state.cursor_style = Some(default_style);
|
||||
|
||||
if let Some(cursor_shape_device) = &state.cursor_shape_device {
|
||||
cursor_shape_device.set_shape(serial, to_shape(default_style));
|
||||
} else {
|
||||
// cursor-shape-v1 isn't supported, set the cursor using a surface.
|
||||
let wl_pointer = state
|
||||
.wl_pointer
|
||||
.clone()
|
||||
.expect("window is focused by pointer");
|
||||
let scale = window.primary_output_scale();
|
||||
state.cursor.set_icon(
|
||||
&wl_pointer,
|
||||
serial,
|
||||
cursor_style_to_icon_names(default_style),
|
||||
scale,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if state
|
||||
.keyboard_focused_window
|
||||
.as_ref()
|
||||
@@ -1883,6 +2008,91 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<zwp_pointer_gestures_v1::ZwpPointerGesturesV1, ()> for WaylandClientStatePtr {
|
||||
fn event(
|
||||
_this: &mut Self,
|
||||
_: &zwp_pointer_gestures_v1::ZwpPointerGesturesV1,
|
||||
_: <zwp_pointer_gestures_v1::ZwpPointerGesturesV1 as Proxy>::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
// The gesture manager doesn't generate events
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<zwp_pointer_gesture_pinch_v1::ZwpPointerGesturePinchV1, ()>
|
||||
for WaylandClientStatePtr
|
||||
{
|
||||
fn event(
|
||||
this: &mut Self,
|
||||
_: &zwp_pointer_gesture_pinch_v1::ZwpPointerGesturePinchV1,
|
||||
event: <zwp_pointer_gesture_pinch_v1::ZwpPointerGesturePinchV1 as Proxy>::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
use gpui::PinchEvent;
|
||||
|
||||
let client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
|
||||
let Some(window) = state.mouse_focused_window.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
match event {
|
||||
zwp_pointer_gesture_pinch_v1::Event::Begin {
|
||||
serial: _,
|
||||
time: _,
|
||||
surface: _,
|
||||
fingers: _,
|
||||
} => {
|
||||
state.pinch_scale = 1.0;
|
||||
let input = PlatformInput::Pinch(PinchEvent {
|
||||
position: state.mouse_location.unwrap_or(point(px(0.0), px(0.0))),
|
||||
delta: 0.0,
|
||||
modifiers: state.modifiers,
|
||||
phase: TouchPhase::Started,
|
||||
});
|
||||
drop(state);
|
||||
window.handle_input(input);
|
||||
}
|
||||
zwp_pointer_gesture_pinch_v1::Event::Update { time: _, scale, .. } => {
|
||||
let new_absolute_scale = scale as f32;
|
||||
let previous_scale = state.pinch_scale;
|
||||
let zoom_delta = new_absolute_scale - previous_scale;
|
||||
state.pinch_scale = new_absolute_scale;
|
||||
|
||||
let input = PlatformInput::Pinch(PinchEvent {
|
||||
position: state.mouse_location.unwrap_or(point(px(0.0), px(0.0))),
|
||||
delta: zoom_delta,
|
||||
modifiers: state.modifiers,
|
||||
phase: TouchPhase::Moved,
|
||||
});
|
||||
drop(state);
|
||||
window.handle_input(input);
|
||||
}
|
||||
zwp_pointer_gesture_pinch_v1::Event::End {
|
||||
serial: _,
|
||||
time: _,
|
||||
cancelled: _,
|
||||
} => {
|
||||
state.pinch_scale = 1.0;
|
||||
let input = PlatformInput::Pinch(PinchEvent {
|
||||
position: state.mouse_location.unwrap_or(point(px(0.0), px(0.0))),
|
||||
delta: 0.0,
|
||||
modifiers: state.modifiers,
|
||||
phase: TouchPhase::Ended,
|
||||
});
|
||||
drop(state);
|
||||
window.handle_input(input);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<wp_fractional_scale_v1::WpFractionalScaleV1, ObjectId> for WaylandClientStatePtr {
|
||||
fn event(
|
||||
this: &mut Self,
|
||||
@@ -2018,7 +2228,7 @@ impl Dispatch<wl_data_device::WlDataDevice, ()> for WaylandClientStatePtr {
|
||||
|
||||
let input = PlatformInput::FileDrop(FileDropEvent::Entered {
|
||||
position,
|
||||
paths: crate::ExternalPaths(paths),
|
||||
paths: gpui::ExternalPaths(paths),
|
||||
});
|
||||
|
||||
let client = this.get_client();
|
||||
@@ -2126,7 +2336,7 @@ impl Dispatch<wl_data_source::WlDataSource, ()> for WaylandClientStatePtr {
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
let client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
let state = client.borrow_mut();
|
||||
|
||||
match event {
|
||||
wl_data_source::Event::Send { mime_type, fd } => {
|
||||
@@ -2212,7 +2422,7 @@ impl Dispatch<zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1, ()>
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
let client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
let state = client.borrow_mut();
|
||||
|
||||
match event {
|
||||
zwp_primary_selection_source_v1::Event::Send { mime_type, fd } => {
|
||||
@@ -2225,3 +2435,27 @@ impl Dispatch<zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1, ()>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<XdgWmDialogV1, ()> for WaylandClientStatePtr {
|
||||
fn event(
|
||||
_: &mut Self,
|
||||
_: &XdgWmDialogV1,
|
||||
_: <XdgWmDialogV1 as Proxy>::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<XdgDialogV1, ()> for WaylandClientStatePtr {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
_proxy: &XdgDialogV1,
|
||||
_event: <XdgDialogV1 as Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,8 @@ use strum::IntoEnumIterator;
|
||||
use wayland_client::{Connection, protocol::wl_data_offer::WlDataOffer};
|
||||
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1;
|
||||
|
||||
use crate::{
|
||||
ClipboardEntry, ClipboardItem, Image, ImageFormat, WaylandClientStatePtr, hash,
|
||||
platform::linux::platform::read_fd,
|
||||
};
|
||||
use crate::platform::linux::{WaylandClientStatePtr, platform::read_fd};
|
||||
use gpui::{ClipboardEntry, ClipboardItem, Image, ImageFormat, hash};
|
||||
|
||||
/// Text mime types that we'll offer to other programs.
|
||||
pub(crate) const TEXT_MIME_TYPES: [&str; 3] =
|
||||
@@ -241,7 +239,7 @@ impl Clipboard {
|
||||
calloop::Mode::Level,
|
||||
),
|
||||
move |_, file, _| {
|
||||
let mut file = unsafe { file.get_mut() };
|
||||
let file = unsafe { file.get_mut() };
|
||||
loop {
|
||||
match file.write(&bytes[written..]) {
|
||||
Ok(n) if written + n == bytes.len() => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::Globals;
|
||||
use crate::platform::linux::Globals;
|
||||
use crate::platform::linux::{DEFAULT_CURSOR_ICON_NAME, log_cursor_icon_warning};
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use util::ResultExt;
|
||||
@@ -95,7 +95,7 @@ impl Cursor {
|
||||
&mut self,
|
||||
wl_pointer: &WlPointer,
|
||||
serial_id: u32,
|
||||
mut cursor_icon_names: &[&str],
|
||||
cursor_icon_names: &[&str],
|
||||
scale: i32,
|
||||
) {
|
||||
self.set_scaled_size(self.size * scale as u32);
|
||||
@@ -104,9 +104,9 @@ impl Cursor {
|
||||
log::warn!("Wayland: Unable to load cursor themes");
|
||||
return;
|
||||
};
|
||||
let mut theme = &mut loaded_theme.theme;
|
||||
let theme = &mut loaded_theme.theme;
|
||||
|
||||
let mut buffer: &CursorImageBuffer;
|
||||
let buffer: &CursorImageBuffer;
|
||||
'outer: {
|
||||
for cursor_icon_name in cursor_icon_names {
|
||||
if let Some(cursor) = theme.get_cursor(cursor_icon_name) {
|
||||
|
||||
@@ -7,7 +7,7 @@ use anyhow::Context as _;
|
||||
use uuid::Uuid;
|
||||
use wayland_backend::client::ObjectId;
|
||||
|
||||
use crate::{Bounds, DisplayId, Pixels, PlatformDisplay};
|
||||
use gpui::{Bounds, DisplayId, Pixels, PlatformDisplay};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct WaylandDisplay {
|
||||
@@ -25,7 +25,7 @@ impl Hash for WaylandDisplay {
|
||||
|
||||
impl PlatformDisplay for WaylandDisplay {
|
||||
fn id(&self) -> DisplayId {
|
||||
DisplayId(self.id.protocol_id())
|
||||
DisplayId::new(self.id.protocol_id())
|
||||
}
|
||||
|
||||
fn uuid(&self) -> anyhow::Result<Uuid> {
|
||||
|
||||
@@ -1,111 +1,26 @@
|
||||
use bitflags::bitflags;
|
||||
use thiserror::Error;
|
||||
pub use gpui::layer_shell::*;
|
||||
|
||||
use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1};
|
||||
|
||||
use crate::Pixels;
|
||||
|
||||
/// The layer the surface is rendered on. Multiple surfaces can share a layer, and ordering within
|
||||
/// a single layer is undefined.
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Layer {
|
||||
/// The background layer, typically used for wallpapers.
|
||||
Background,
|
||||
|
||||
/// The bottom layer.
|
||||
Bottom,
|
||||
|
||||
/// The top layer, typically used for fullscreen windows.
|
||||
Top,
|
||||
|
||||
/// The overlay layer, used for surfaces that should always be on top.
|
||||
#[default]
|
||||
Overlay,
|
||||
}
|
||||
|
||||
impl From<Layer> for zwlr_layer_shell_v1::Layer {
|
||||
fn from(layer: Layer) -> Self {
|
||||
match layer {
|
||||
Layer::Background => Self::Background,
|
||||
Layer::Bottom => Self::Bottom,
|
||||
Layer::Top => Self::Top,
|
||||
Layer::Overlay => Self::Overlay,
|
||||
}
|
||||
pub(crate) fn wayland_layer(layer: Layer) -> zwlr_layer_shell_v1::Layer {
|
||||
match layer {
|
||||
Layer::Background => zwlr_layer_shell_v1::Layer::Background,
|
||||
Layer::Bottom => zwlr_layer_shell_v1::Layer::Bottom,
|
||||
Layer::Top => zwlr_layer_shell_v1::Layer::Top,
|
||||
Layer::Overlay => zwlr_layer_shell_v1::Layer::Overlay,
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Screen anchor point for layer_shell surfaces. These can be used in any combination, e.g.
|
||||
/// specifying `Anchor::LEFT | Anchor::RIGHT` will stretch the surface across the width of the
|
||||
/// screen.
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Anchor: u32 {
|
||||
/// Anchor to the top edge of the screen.
|
||||
const TOP = 1;
|
||||
/// Anchor to the bottom edge of the screen.
|
||||
const BOTTOM = 2;
|
||||
/// Anchor to the left edge of the screen.
|
||||
const LEFT = 4;
|
||||
/// Anchor to the right edge of the screen.
|
||||
const RIGHT = 8;
|
||||
pub(crate) fn wayland_anchor(anchor: Anchor) -> zwlr_layer_surface_v1::Anchor {
|
||||
zwlr_layer_surface_v1::Anchor::from_bits_truncate(anchor.bits())
|
||||
}
|
||||
|
||||
pub(crate) fn wayland_keyboard_interactivity(
|
||||
value: KeyboardInteractivity,
|
||||
) -> zwlr_layer_surface_v1::KeyboardInteractivity {
|
||||
match value {
|
||||
KeyboardInteractivity::None => zwlr_layer_surface_v1::KeyboardInteractivity::None,
|
||||
KeyboardInteractivity::Exclusive => zwlr_layer_surface_v1::KeyboardInteractivity::Exclusive,
|
||||
KeyboardInteractivity::OnDemand => zwlr_layer_surface_v1::KeyboardInteractivity::OnDemand,
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Anchor> for zwlr_layer_surface_v1::Anchor {
|
||||
fn from(anchor: Anchor) -> Self {
|
||||
Self::from_bits_truncate(anchor.bits())
|
||||
}
|
||||
}
|
||||
|
||||
/// Keyboard interactivity mode for the layer_shell surfaces.
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub enum KeyboardInteractivity {
|
||||
/// No keyboard inputs will be delivered to the surface and it won't be able to receive
|
||||
/// keyboard focus.
|
||||
None,
|
||||
|
||||
/// The surface will receive exclusive keyboard focus as long as it is above the shell surface
|
||||
/// layer, and no other layer_shell surfaces are above it.
|
||||
Exclusive,
|
||||
|
||||
/// The surface can be focused similarly to a normal window.
|
||||
#[default]
|
||||
OnDemand,
|
||||
}
|
||||
|
||||
impl From<KeyboardInteractivity> for zwlr_layer_surface_v1::KeyboardInteractivity {
|
||||
fn from(value: KeyboardInteractivity) -> Self {
|
||||
match value {
|
||||
KeyboardInteractivity::None => Self::None,
|
||||
KeyboardInteractivity::Exclusive => Self::Exclusive,
|
||||
KeyboardInteractivity::OnDemand => Self::OnDemand,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Options for creating a layer_shell window.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct LayerShellOptions {
|
||||
/// The namespace for the surface, mostly used by compositors to apply rules, can not be
|
||||
/// changed after the surface is created.
|
||||
pub namespace: String,
|
||||
/// The layer the surface is rendered on.
|
||||
pub layer: Layer,
|
||||
/// The anchor point of the surface.
|
||||
pub anchor: Anchor,
|
||||
/// Requests that the compositor avoids occluding an area with other surfaces.
|
||||
pub exclusive_zone: Option<Pixels>,
|
||||
/// The anchor point of the exclusive zone, will be determined using the anchor if left
|
||||
/// unspecified.
|
||||
pub exclusive_edge: Option<Anchor>,
|
||||
/// Margins between the surface and its anchor point(s).
|
||||
/// Specified in CSS order: top, right, bottom, left.
|
||||
pub margin: Option<(Pixels, Pixels, Pixels, Pixels)>,
|
||||
/// How keyboard events should be delivered to the surface.
|
||||
pub keyboard_interactivity: KeyboardInteractivity,
|
||||
}
|
||||
|
||||
/// An error indicating that an action failed because the compositor doesn't support the required
|
||||
/// layer_shell protocol.
|
||||
#[derive(Debug, Error)]
|
||||
#[error("Compositor doesn't support zwlr_layer_shell_v1")]
|
||||
pub struct LayerShellNotSupportedError;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user