You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/10/20 09:31:42 UTC
(maka) branch rust-preview updated (e1574d81dd -> f5598e34e8)
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a change to branch rust-preview
in repository https://gitbox.apache.org/repos/asf/maka.git
from e1574d81dd fix(transport): preserve complete application frames
new d553a8374c test(providers): specify Codex fixture language
new b4f6c339fe chore(gpui): sync Fast to the official 0.1.10 release
new e440d09284 test(gpui): observe referenced rounded-clip scenes
new 4b13afde3d fix(desktop): satisfy current GPUI strict lint checks
new cf4f147704 test(desktop): remove redundant GPUI test clones
new 0c2720ad07 chore(gpui): sync Kit to upstream 0.8.0
new d99e031c3a fix(gpui): share caret affinity across scroll projection
new 7b60f52777 test(gpui): use native Fast keystroke simulation
new fb00881401 test(gpui): deliver animation frames in bounce regressions
new c2ce2ceb0c test(gpui): separate independent mask wheel gestures
new f5598e34e8 chore(licenses): regenerate notices for GPUI upstream sync
The 11 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
Cargo.lock | 833 +++++-
Cargo.toml | 3 +
LICENSE | 8 +-
plugins/agent/providers/tests/codex/inventory.rs | 1 +
products/maka/cli/DEPENDENCIES.rust.tsv | 40 +-
products/maka/cli/THIRD_PARTY_NOTICES.txt | 2647 ++++++++++++++------
products/maka/cli/notices.json | 371 +--
third-party/gpui-fast/SOURCE.md | 12 +-
third-party/gpui-fast/SOURCE.zh-CN.md | 12 +-
third-party/gpui-fast/crates/gpui/Cargo.toml | 87 +-
third-party/gpui-fast/crates/gpui/build.rs | 10 +
third-party/gpui-fast/crates/gpui/src/app.rs | 148 +-
.../gpui-fast/crates/gpui/src/app/test_context.rs | 68 +
.../gpui-fast/crates/gpui/src/elements/div.rs | 98 +-
.../gpui-fast/crates/gpui/src/fast/composition.rs | 770 +++++-
.../crates/gpui/src/fast/layers/hover_freeze.rs | 43 +-
.../gpui-fast/crates/gpui/src/fast/layers/input.rs | 4 +
.../gpui-fast/crates/gpui/src/fast/layers/lists.rs | 16 +-
.../crates/gpui/src/fast/layers/overlay.rs | 12 +
.../gpui-fast/crates/gpui/src/fast/layers/paint.rs | 12 +-
.../crates/gpui/src/fast/layers/policy.rs | 2 +
.../gpui-fast/crates/gpui/src/fast/layers/reuse.rs | 23 +-
.../gpui-fast/crates/gpui/src/fast/layers/scene.rs | 3 +
.../gpui-fast/crates/gpui/src/fast/layers/tiles.rs | 10 +
.../crates/gpui/src/fast/layers/verify.rs | 3 +
.../gpui-fast/crates/gpui/src/fast/layout.rs | 115 +-
third-party/gpui-fast/crates/gpui/src/fast/mod.rs | 2 +
.../gpui-fast/crates/gpui/src/fast/retained.rs | 88 +-
.../gpui-fast/crates/gpui/src/fast/rounded_clip.rs | 506 ++++
.../crates/gpui/src/fast/rounded_clip_fixtures.rs | 556 ++++
.../crates/gpui/src/fast/rounded_clip_scene.rs | 156 ++
.../gpui-fast/crates/gpui/src/fast/scene.rs | 4 +
.../gpui-fast/crates/gpui/src/fast/stats.rs | 3 +-
.../gpui-fast/crates/gpui/src/fast/tests/layers.rs | 63 +
.../crates/gpui/src/fast/tests/layers_lists.rs | 254 +-
.../gpui-fast/crates/gpui/src/fast/tests/layout.rs | 73 +
.../crates/gpui/src/fast/tests/retained.rs | 81 +
.../crates/gpui/src/fast/tests/support.rs | 24 +-
.../crates/gpui/src/fast/tests/visibility.rs | 2 +-
third-party/gpui-fast/crates/gpui/src/geometry.rs | 2 +-
third-party/gpui-fast/crates/gpui/src/gestures.rs | 112 +-
third-party/gpui-fast/crates/gpui/src/gpui.rs | 13 +
.../gpui-fast/crates/gpui/src/hot_reload.rs | 248 ++
third-party/gpui-fast/crates/gpui/src/input.rs | 126 +
.../gpui-fast/crates/gpui/src/key_dispatch.rs | 20 +
third-party/gpui-fast/crates/gpui/src/platform.rs | 68 +-
.../crates/gpui/src/platform/test/platform.rs | 27 +-
.../gpui-fast/crates/gpui/src/profiler/journal.rs | 2 +-
third-party/gpui-fast/crates/gpui/src/scene.rs | 12 +-
third-party/gpui-fast/crates/gpui/src/style.rs | 2 +-
third-party/gpui-fast/crates/gpui/src/tab_stop.rs | 9 +-
third-party/gpui-fast/crates/gpui/src/view.rs | 26 +
third-party/gpui-fast/crates/gpui/src/window.rs | 250 +-
third-party/gpui-fast/crates/gpui_linux/Cargo.toml | 14 +-
.../crates/gpui_linux/src/fast/composition/mod.rs | 3 +
.../gpui_linux/src/fast/composition/occluders.rs | 265 ++
.../gpui_linux/src/fast/composition/wayland.rs | 138 +-
.../crates/gpui_linux/src/fast/composition/x11.rs | 335 +--
.../crates/gpui_linux/src/linux/wayland/client.rs | 13 +-
.../crates/gpui_linux/src/linux/x11/client.rs | 44 +-
third-party/gpui-fast/crates/gpui_macos/Cargo.toml | 16 +-
.../crates/gpui_macos/src/fast/composition.rs | 427 +++-
.../gpui-fast/crates/gpui_macos/src/window.rs | 12 +-
.../gpui-fast/crates/gpui_windows/Cargo.toml | 5 +-
third-party/gpui-fast/crates/gpui_windows/build.rs | 7 +-
.../crates/gpui_windows/src/directx_renderer.rs | 5 +-
.../gpui-fast/crates/gpui_windows/src/events.rs | 31 +-
.../crates/gpui_windows/src/fast/composition.rs | 13 -
.../crates/gpui_windows/src/fast/frame.rs | 67 +-
.../gpui_windows/src/fast/layers/composite.rs | 20 +-
.../crates/gpui_windows/src/fast/layers/raster.rs | 1 +
.../crates/gpui_windows/src/fast/layers/tests.rs | 36 +
.../gpui-fast/crates/gpui_windows/src/fast/mod.rs | 1 +
.../crates/gpui_windows/src/fast/rounded_clip.rs | 271 ++
.../src/fast/rounded_clip_shader_build.rs | 131 +
.../gpui-fast/crates/gpui_windows/src/platform.rs | 47 +-
.../gpui-fast/crates/gpui_windows/src/shaders.hlsl | 4 +-
third-party/gpui-fast/upstream.json | 151 +-
third-party/gpui-kit/README.md | 4 +
third-party/gpui-kit/SOURCE.md | 36 +-
third-party/gpui-kit/crates/base/Cargo.toml | 8 +-
.../gpui-kit/crates/base/src/a11y_disabled.rs | 304 +++
third-party/gpui-kit/crates/base/src/button.rs | 13 +-
third-party/gpui-kit/crates/base/src/checkbox.rs | 9 +-
.../gpui-kit/crates/base/src/date_picker.rs | 2 +
.../dock/fixtures/legacy_empty_stack_panel.json | 3 +
...l.json => placeholder_wrapped_empty_stack.json} | 7 +-
.../gpui-kit/crates/base/src/dock/state_convert.rs | 59 +-
third-party/gpui-kit/crates/base/src/focus_trap.rs | 51 +-
.../crates/base/src/input/base/blink_cursor.rs | 40 +-
.../gpui-kit/crates/base/src/input/base/element.rs | 456 ++--
.../gpui-kit/crates/base/src/input/base/state.rs | 714 +++++-
.../src/input/editor/display_map/display_map.rs | 7 +
.../base/src/input/editor/lsp/completions.rs | 176 +-
.../crates/base/src/input/editor/lsp/mod.rs | 41 +
.../crates/base/src/input/editor/search.rs | 16 +
third-party/gpui-kit/crates/base/src/input/mod.rs | 9 +-
third-party/gpui-kit/crates/base/src/lib.rs | 2 +
third-party/gpui-kit/crates/base/src/link.rs | 6 +-
third-party/gpui-kit/crates/base/src/motion.rs | 2 +-
.../gpui-kit/crates/base/src/motion/presence.rs | 5 +-
.../gpui-kit/crates/base/src/number_input.rs | 18 +-
third-party/gpui-kit/crates/base/src/overscroll.rs | 463 ++++
third-party/gpui-kit/crates/base/src/popover.rs | 13 +-
third-party/gpui-kit/crates/base/src/radio.rs | 3 +-
third-party/gpui-kit/crates/base/src/root.rs | 352 ++-
.../gpui-kit/crates/base/src/scroll_bounce.rs | 537 +++-
.../gpui-kit/crates/base/src/scrollable_mask.rs | 172 +-
third-party/gpui-kit/crates/base/src/scrollbar.rs | 218 +-
third-party/gpui-kit/crates/base/src/select.rs | 138 +-
third-party/gpui-kit/crates/base/src/slider.rs | 140 +-
third-party/gpui-kit/crates/base/src/switch.rs | 11 +-
third-party/gpui-kit/crates/base/src/tabs.rs | 3 +-
.../gpui-kit/crates/base/src/test_support.rs | 9 +
.../gpui-kit/crates/base/src/text/document.rs | 40 +-
.../gpui-kit/crates/base/src/text/editor_host.rs | 1324 ++++++++++
.../gpui-kit/crates/base/src/text/editor_seam.rs | 86 +
.../gpui-kit/crates/base/src/text/format/html.rs | 297 ++-
.../crates/base/src/text/format/html5minify/mod.rs | 34 +-
.../crates/base/src/text/format/markdown.rs | 1 +
.../gpui-kit/crates/base/src/text/inline.rs | 432 +++-
.../gpui-kit/crates/base/src/text/inline_flow.rs | 194 +-
.../gpui-kit/crates/base/src/text/inline_object.rs | 52 +-
third-party/gpui-kit/crates/base/src/text/link.rs | 407 ++-
third-party/gpui-kit/crates/base/src/text/mod.rs | 2 +
third-party/gpui-kit/crates/base/src/text/node.rs | 541 +++-
.../crates/base/src/text/selection_adapter.rs | 83 +-
third-party/gpui-kit/crates/base/src/text/state.rs | 29 +-
third-party/gpui-kit/crates/base/src/text/style.rs | 33 +-
.../gpui-kit/crates/base/src/text/text_view.rs | 472 +++-
.../gpui-kit/crates/base/src/text_selection.rs | 158 +-
third-party/gpui-kit/crates/base/src/time_field.rs | 3 +-
third-party/gpui-kit/crates/base/src/toggle.rs | 3 +-
third-party/gpui-kit/crates/base/src/tree.rs | 154 +-
.../gpui-kit/crates/base/src/window_border.rs | 17 +-
third-party/gpui-kit/crates/shell/Cargo.toml | 21 +-
third-party/gpui-kit/crates/shell/README.md | 107 +-
.../gpui-kit/crates/shell/component-inventory.json | 17 +
.../gpui-kit/crates/shell/src/capability.rs | 321 ++-
.../crates/shell/src/component_registry.rs | 147 +-
.../gpui-kit/crates/shell/src/components/mod.rs | 40 +-
.../crates/shell/src/components/shell/chat.rs | 187 +-
.../shell/src/components/shell/layout/textarea.rs | 1 +
.../crates/shell/src/components/shell/media/mod.rs | 4 +
.../shell/src/components/shell/media/webview.rs | 456 ++++
.../src/components/shell/retained_forms/mod.rs | 1 +
.../shell/src/components/shell/scroll/scroll.rs | 2 +
third-party/gpui-kit/crates/shell/src/default-env | 92 +
.../crates/shell/src/engine/quickjs/dock_api.rs | 39 +-
.../crates/shell/src/engine/quickjs/entity_api.rs | 190 +-
.../crates/shell/src/engine/quickjs/host.rs | 248 +-
.../shell/src/engine/quickjs/host_modules.rs | 34 +-
.../crates/shell/src/engine/quickjs/js_error.rs | 66 +
.../crates/shell/src/engine/quickjs/mod.rs | 955 +++++--
.../crates/shell/src/engine/quickjs/overlay.rs | 102 +-
.../crates/shell/src/engine/quickjs/sandbox.rs | 584 ++++-
.../crates/shell/src/engine/quickjs/scheduler.rs | 157 +-
.../shell/src/engine/quickjs/standard/encoding.rs | 307 +++
.../shell/src/engine/quickjs/standard/fetch.rs | 59 +-
.../crates/shell/src/engine/quickjs/standard/fs.rs | 8 +
.../shell/src/engine/quickjs/standard/mod.rs | 6 +
.../shell/src/engine/quickjs/standard/net.rs | 9 +-
.../crates/shell/src/engine/quickjs/standard/os.rs | 18 +-
.../shell/src/engine/quickjs/standard/process.rs | 2 +-
.../shell/src/engine/quickjs/standard/websocket.rs | 41 +-
.../crates/shell/src/engine/quickjs/template.rs | 65 +-
.../crates/shell/src/engine/quickjs/theme_api.rs | 22 +-
.../crates/shell/src/engine/quickjs/window_api.rs | 27 +-
third-party/gpui-kit/crates/shell/src/entities.rs | 2 +-
third-party/gpui-kit/crates/shell/src/host.rs | 148 +-
.../gpui-kit/crates/shell/src/host/agents.md | 64 +-
.../gpui-kit/crates/shell/src/host/installed.rs | 4 +-
.../gpui-kit/crates/shell/src/host/scaffold.rs | 4 +-
third-party/gpui-kit/crates/shell/src/lib.rs | 5 +-
.../gpui-kit/crates/shell/src/materialize.rs | 16 +
.../shell/src/materialize/components/list.rs | 32 +-
.../shell/src/materialize/components/tooltip.rs | 6 +-
.../src/materialize/components/virtual_list.rs | 60 +-
third-party/gpui-kit/crates/shell/src/path.rs | 32 +-
third-party/gpui-kit/crates/shell/src/plugin.rs | 232 +-
third-party/gpui-kit/crates/shell/src/policy.rs | 26 +-
third-party/gpui-kit/crates/shell/src/process.rs | 730 +++++-
third-party/gpui-kit/crates/shell/src/root.rs | 37 +-
third-party/gpui-kit/crates/shell/src/runtime.rs | 549 +++-
third-party/gpui-kit/crates/shell/src/spec.rs | 61 +-
third-party/gpui-kit/crates/shell/src/style.rs | 210 +-
third-party/gpui-kit/crates/shell/src/tests/fs.rs | 316 +++
.../gpui-kit/crates/shell/src/tests/network.rs | 123 +-
.../gpui-kit/crates/shell/src/tests/process.rs | 301 ++-
.../gpui-kit/crates/shell/src/tests/render.rs | 293 ++-
.../gpui-kit/crates/shell/src/tests/snapshot.rs | 101 +
.../crates/shell/src/tests/standard_runtime.rs | 144 +-
.../gpui-kit/crates/shell/src/tests/template.rs | 37 +
third-party/gpui-kit/crates/shell/src/typings.rs | 461 +++-
third-party/gpui-kit/crates/shell/src/value.rs | 120 +-
third-party/gpui-kit/crates/shell/src/view.rs | 32 +-
third-party/gpui-kit/crates/shell/src/watch.rs | 74 +-
third-party/gpui-kit/upstream.json | 230 +-
198 files changed, 23550 insertions(+), 3781 deletions(-)
create mode 100644 third-party/gpui-fast/crates/gpui/src/fast/rounded_clip.rs
create mode 100644 third-party/gpui-fast/crates/gpui/src/fast/rounded_clip_fixtures.rs
create mode 100644 third-party/gpui-fast/crates/gpui/src/fast/rounded_clip_scene.rs
create mode 100644 third-party/gpui-fast/crates/gpui/src/hot_reload.rs
create mode 100644 third-party/gpui-fast/crates/gpui_linux/src/fast/composition/occluders.rs
create mode 100644 third-party/gpui-fast/crates/gpui_windows/src/fast/rounded_clip.rs
create mode 100644 third-party/gpui-fast/crates/gpui_windows/src/fast/rounded_clip_shader_build.rs
create mode 100644 third-party/gpui-kit/crates/base/src/a11y_disabled.rs
create mode 100644 third-party/gpui-kit/crates/base/src/dock/fixtures/legacy_empty_stack_panel.json
copy third-party/gpui-kit/crates/base/src/dock/fixtures/{unregistered_panel.json => placeholder_wrapped_empty_stack.json} (61%)
create mode 100644 third-party/gpui-kit/crates/base/src/overscroll.rs
create mode 100644 third-party/gpui-kit/crates/base/src/text/editor_host.rs
create mode 100644 third-party/gpui-kit/crates/base/src/text/editor_seam.rs
create mode 100644 third-party/gpui-kit/crates/shell/src/components/shell/media/webview.rs
create mode 100644 third-party/gpui-kit/crates/shell/src/default-env
create mode 100644 third-party/gpui-kit/crates/shell/src/engine/quickjs/js_error.rs
create mode 100644 third-party/gpui-kit/crates/shell/src/engine/quickjs/standard/encoding.rs
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
(maka) 05/11: test(desktop): remove redundant GPUI test clones
Posted by wy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch rust-preview
in repository https://gitbox.apache.org/repos/asf/maka.git
commit cf4f147704f72434724dc6f381a91a66807c4658
Author: M4n5ter <m4...@gmail.com>
AuthorDate: Tue Oct 20 14:39:13 2026 +0800
test(desktop): remove redundant GPUI test clones
Move owned activation test captures directly and copy the background value without cloning it.
Generated-by: OpenAI Codex
---
third-party/gpui-fast/crates/gpui/src/elements/div.rs | 4 +---
third-party/gpui-fast/crates/gpui/src/fast/tests/visibility.rs | 2 +-
2 files changed, 2 insertions(+), 4 deletions(-)
diff --git a/third-party/gpui-fast/crates/gpui/src/elements/div.rs b/third-party/gpui-fast/crates/gpui/src/elements/div.rs
index a386e75055..154e06c9d7 100644
--- a/third-party/gpui-fast/crates/gpui/src/elements/div.rs
+++ b/third-party/gpui-fast/crates/gpui/src/elements/div.rs
@@ -6127,7 +6127,6 @@ mod tests {
.child(canvas(
|_, _, _| {},
move |_, _, window, _| {
- let nested_ax = nested_ax.clone();
window.on_mouse_event(move |_: &MouseUpEvent, phase, window, cx| {
if phase.capture() {
let request = nested_ax.borrow_mut().take();
@@ -6276,8 +6275,7 @@ mod tests {
focus_and_draw(&mut cx, window, &focus_a);
let request = a11y_click_request(&mut cx, window, "b");
cx.update_window(window, |root, _, cx| {
- root.clone()
- .downcast::<KeyboardActivationTest>()
+ root.downcast::<KeyboardActivationTest>()
.unwrap()
.update(cx, |view, _| {
*view.nested_ax.borrow_mut() = Some(request);
diff --git a/third-party/gpui-fast/crates/gpui/src/fast/tests/visibility.rs b/third-party/gpui-fast/crates/gpui/src/fast/tests/visibility.rs
index d3a728bae7..744dd49e9a 100644
--- a/third-party/gpui-fast/crates/gpui/src/fast/tests/visibility.rs
+++ b/third-party/gpui-fast/crates/gpui/src/fast/tests/visibility.rs
@@ -257,7 +257,7 @@ fn nested_deferred_priority_preserves_causal_paint_through_reuse_and_rebuild(
.filter_map(|operation| match operation {
crate::scene::PaintOperation::Primitive(
crate::scene::Primitive::Quad(quad),
- ) => Some(quad.background.clone()),
+ ) => Some(quad.background),
_ => None,
})
.filter(|background| expected.contains(background))
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
(maka) 03/11: test(gpui): observe referenced rounded-clip scenes
Posted by wy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch rust-preview
in repository https://gitbox.apache.org/repos/asf/maka.git
commit e440d092847375f491c3e0bc5b44e769cb3ddf99
Author: M4n5ter <m4...@gmail.com>
AuthorDate: Tue Oct 20 13:28:55 2026 +0800
test(gpui): observe referenced rounded-clip scenes
Follow the renderer clip-group graph in painted_quads instead of only reading the root scene. Preserve the original Kit table geometry assertions and cover nested nonzero coordinates plus unreferenced frames.
Generated-by: OpenAI Codex
---
third-party/gpui-fast/SOURCE.md | 2 ++
third-party/gpui-fast/SOURCE.zh-CN.md | 2 ++
.../gpui-fast/crates/gpui/src/fast/rounded_clip.rs | 34 ++++++++++++++++++++++
.../crates/gpui/src/fast/rounded_clip_scene.rs | 23 +++++++++++++++
third-party/gpui-fast/crates/gpui/src/window.rs | 3 +-
5 files changed, 63 insertions(+), 1 deletion(-)
diff --git a/third-party/gpui-fast/SOURCE.md b/third-party/gpui-fast/SOURCE.md
index 5b86dfd05f..7e20ff62ce 100644
--- a/third-party/gpui-fast/SOURCE.md
+++ b/third-party/gpui-fast/SOURCE.md
@@ -105,6 +105,8 @@ The five adapted crates now track `b836dd970b26830f2124b7cf91b0e19018fd12da` (`v
Three-way integration preserves Maka's physical input provenance, exact published IME-handler slot, selection context, scheduler cancellation and native cleanup receipts. Upstream field identity and handler restoration notifications use those same existing owners. The native timer patch retains its explicit async-io dependency. The profiler counter uses stable `AtomicUsize::try_update` in place of its deprecated alias, with the same orderings and checked subtraction.
+The test-support `Window::painted_quads` observation follows the final scene's referenced rounded-clip groups, as the renderers do. It preserves each quad's coordinates and geometry, without treating retained paint operations or unreferenced layer frames as presented output. Kit's table corner assertions remain unchanged.
+
## A wheel gesture scrolls one container (2026-10-20)
Maka's own change. A wheel or trackpad gesture, or a touch pan, over a scroll container inside another, such as a tool's output box in a conversation list, moved both, because every `Div` and `List` scroll listener under the pointer took every wheel event. `Window` now keeps a `ScrollLatch` (`crates/gpui/src/gestures.rs`). The innermost container under the pointer that moves along the gesture's axis (the axis of its larger delta) takes the gesture and keeps it until the gesture ends, at [...]
diff --git a/third-party/gpui-fast/SOURCE.zh-CN.md b/third-party/gpui-fast/SOURCE.zh-CN.md
index 362bd422e0..3ced51114b 100644
--- a/third-party/gpui-fast/SOURCE.zh-CN.md
+++ b/third-party/gpui-fast/SOURCE.zh-CN.md
@@ -80,6 +80,8 @@ Window 可根据原 ClickInputState interruption 创建不可 clone 的 InputGes
三方整合保留 Maka 原物理输入来源、实际发布的 IME-handler 槽位、选区上下文、定时器取消及原生清理回执。上游字段身份和 handler 恢复通知接入同一既有 owner。原生定时器补丁保留显式 async-io 依赖;profiler 计数器改用稳定的 `AtomicUsize::try_update` 替代弃用别名,保持原内存顺序和 checked subtraction。
+测试接口 `Window::painted_quads` 按渲染器实际引用路径读取最终 scene 的 rounded-clip 子组,保留 quad 原坐标与几何;保留的绘制操作和未引用的图层 frame 不作为已呈现输出。Kit 原表格四角断言不变。
+
## 一次滚轮手势只滚动一个容器(2026-10-20)
Maka 自己的改动。滚动容器嵌在另一个滚动容器里时(例如对话列表里的工具输出框),在它上面的一次滚轮或触控板手势(或一次触摸拖动)会让两者一起滚动,因为指针下每个 `Div` 和 `List` 的滚动监听器都会处理每个滚轮事件。现在 `Window` 持有一个 `ScrollLatch`(`crates/gpui/src/gestures.rs`):指针下沿手势方向(增量较大的那一轴)真正滚动了的最内层容器取得这次手势,直到手势结束,滚到尽头也不放手。它外层的容器(监听器在它之后运行)把这次手势的事件留给它,滚动中移到指针下的容器也不接手。滚不动的容器让事件继续向外传,所以框已在尽头时,下一次手势交给页面,与浏览器的滚动链一致;vendored GPUI Kit 的 `ScrollableMask` 在边缘把事件交给外层容器的行为也保持不变。手势在以下情况结束:收到 `TouchPhase::Started` 事件、滚轮事件位�
��改变、或取得手势的容器 300 毫秒内没有处理它的事件。以 `TouchPhase::Started` 开始的手势,要在收到 `TouchPh [...]
diff --git a/third-party/gpui-fast/crates/gpui/src/fast/rounded_clip.rs b/third-party/gpui-fast/crates/gpui/src/fast/rounded_clip.rs
index f8df69fa20..74233ec8ca 100644
--- a/third-party/gpui-fast/crates/gpui/src/fast/rounded_clip.rs
+++ b/third-party/gpui-fast/crates/gpui/src/fast/rounded_clip.rs
@@ -274,6 +274,40 @@ mod tests {
}
}
+ #[test]
+ fn painted_quads_follow_only_referenced_nested_clip_groups() {
+ let mut scene = Scene::default();
+ let mut outer = shape();
+ outer.bounds.origin = at(10., 20.);
+ let mut inner = outer;
+ inner.bounds.origin = at(17., 27.);
+ inner.bounds.size = size(ScaledPixels(48.), ScaledPixels(48.));
+ let bounds = Bounds::new(at(23., 31.), size(ScaledPixels(12.), ScaledPixels(15.)));
+ let target = Quad {
+ bounds,
+ content_mask: crate::ContentMask { bounds },
+ corner_radii: Corners::all(ScaledPixels(3.)),
+ ..Default::default()
+ };
+ record(&mut scene, ClipOperation::Begin(outer));
+ record(&mut scene, ClipOperation::Begin(inner));
+ scene.insert_primitive(target);
+ record(&mut scene, ClipOperation::End);
+ record(&mut scene, ClipOperation::End);
+ scene.finish();
+
+ let quads = crate::fast::rounded_clip_scene::painted_quads(&scene);
+ assert_eq!(quads.len(), 1);
+ assert_eq!(quads[0].bounds, bounds);
+ assert_eq!(quads[0].content_mask, target.content_mask);
+ assert_eq!(quads[0].corner_radii, Corners::all(ScaledPixels(3.)));
+
+ scene.polychrome_sprites.clear();
+ assert!(!scene.layers.frames.is_empty());
+ assert!(!scene.paint_operations.is_empty());
+ assert!(crate::fast::rounded_clip_scene::painted_quads(&scene).is_empty());
+ }
+
#[test]
fn rounded_clip_preserves_original_geometry_under_rectangular_cropping() {
let mut shape = shape();
diff --git a/third-party/gpui-fast/crates/gpui/src/fast/rounded_clip_scene.rs b/third-party/gpui-fast/crates/gpui/src/fast/rounded_clip_scene.rs
index c78beec0d4..06af930572 100644
--- a/third-party/gpui-fast/crates/gpui/src/fast/rounded_clip_scene.rs
+++ b/third-party/gpui-fast/crates/gpui/src/fast/rounded_clip_scene.rs
@@ -19,6 +19,29 @@ use crate::{
const CLIP_TEXTURE_BASE: u32 = 0xF100_0000;
const GEOMETRY_KEY: LayerKey = LayerKey(u32::MAX);
+/// Observe the final drawable graph using the renderers' clip-group references.
+#[cfg(any(test, feature = "test-support"))]
+pub(crate) fn painted_quads(scene: &Scene) -> Vec<crate::Quad> {
+ let mut quads = scene.quads.clone();
+ for sprite in &scene.polychrome_sprites {
+ let index = sprite.tile.texture_id.index;
+ if index < CLIP_TEXTURE_BASE {
+ continue;
+ }
+ let key = LayerKey(index - CLIP_TEXTURE_BASE + 0x0100_0000);
+ if let Some(child) = scene
+ .layers
+ .frames
+ .iter()
+ .find(|frame| frame.key == key && frame.tile_size == 0)
+ .and_then(|frame| frame.content.scene())
+ {
+ quads.extend(painted_quads(child));
+ }
+ }
+ quads
+}
+
/// Materialize private paint scopes as existing, renderer-readable layer
/// objects. Preserve the original operation stream for retained paint ranges.
pub(crate) fn compile(scene: &mut Scene) {
diff --git a/third-party/gpui-fast/crates/gpui/src/window.rs b/third-party/gpui-fast/crates/gpui/src/window.rs
index 610dd771ca..23c206e72b 100644
--- a/third-party/gpui-fast/crates/gpui/src/window.rs
+++ b/third-party/gpui-fast/crates/gpui/src/window.rs
@@ -3191,9 +3191,10 @@ impl Window {
/// painted output without rasterizing the frame. Quad bounds are in scaled pixels and are
/// not clipped; each quad carries the content mask it will be clipped to when drawn. Quads
/// whose bounds don't intersect their content mask are culled at paint time and won't appear.
+ /// Includes quads in rounded-clip groups referenced by the final scene's sprites.
#[cfg(any(test, feature = "test-support"))]
pub fn painted_quads(&self) -> Vec<Quad> {
- self.rendered_frame.scene.quads.clone()
+ crate::fast::rounded_clip_scene::painted_quads(&self.rendered_frame.scene)
}
/// Returns the underlines in the most recently rendered frame's scene.
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
(maka) 10/11: test(gpui): separate independent mask wheel gestures
Posted by wy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch rust-preview
in repository https://gitbox.apache.org/repos/asf/maka.git
commit c2ce2ceb0cafe716c18c5dd858d7e15b4a063ed8
Author: M4n5ter <m4...@gmail.com>
AuthorDate: Tue Oct 20 16:06:57 2026 +0800
test(gpui): separate independent mask wheel gestures
Use a fresh native window for each trackpad and horizontal-wheel case in
the imported mask test. Reusing the same pointer position and default
Moved phase shares the preceding native scroll latch. Keep all original
axis and offset assertions and the production mask and Fast behavior.
Update the Fast source notes in English and Chinese for Kit 0.8.0 at
77ff6f7. Record that Kit capture-phase areas coexist with the generic
native Div/List latch; the imported scenarios must own separate gestures.
Verified targeted rustfmt and diff checks. No Cargo, compilation or
fixture execution was performed; behavioral verification stays with the
parent task's final native test run.
Generated-by: OpenAI Codex
---
third-party/gpui-fast/SOURCE.md | 4 ++--
third-party/gpui-fast/SOURCE.zh-CN.md | 4 ++--
.../gpui-kit/crates/base/src/scrollable_mask.rs | 18 +++++++++++-------
3 files changed, 15 insertions(+), 11 deletions(-)
diff --git a/third-party/gpui-fast/SOURCE.md b/third-party/gpui-fast/SOURCE.md
index 7e20ff62ce..a9666b46f7 100644
--- a/third-party/gpui-fast/SOURCE.md
+++ b/third-party/gpui-fast/SOURCE.md
@@ -101,7 +101,7 @@ Programmatic list offsets still bump their original dependency version, includin
## Upstream synchronization (2026-10-20)
-The five adapted crates now track `b836dd970b26830f2124b7cf91b0e19018fd12da` (`v0.1.10`). Their manifests follow upstream `script/prepare-release.ts`; all nine Fast packages resolve to 0.1.10, support snapshots and the adapted scheduler remain at 0.3.9, and the published WGPU backend uses registry `wgpu =30.0.1` with upstream's release adaptations. This published WGPU version does not include upstream's unreleased Vulkan surface-drop fix. Kit remains at its existing revision, whose Fast [...]
+The five adapted crates now track `b836dd970b26830f2124b7cf91b0e19018fd12da` (`v0.1.10`). Their manifests follow upstream `script/prepare-release.ts`; all nine Fast packages resolve to 0.1.10, support snapshots and the adapted scheduler remain at 0.3.9, and the published WGPU backend uses registry `wgpu =30.0.1` with upstream's release adaptations. This published WGPU version does not include upstream's unreleased Vulkan surface-drop fix. Kit now tracks `77ff6f794dca3bcee2e90977cedf12a64 [...]
Three-way integration preserves Maka's physical input provenance, exact published IME-handler slot, selection context, scheduler cancellation and native cleanup receipts. Upstream field identity and handler restoration notifications use those same existing owners. The native timer patch retains its explicit async-io dependency. The profiler counter uses stable `AtomicUsize::try_update` in place of its deprecated alias, with the same orderings and checked subtraction.
@@ -109,4 +109,4 @@ The test-support `Window::painted_quads` observation follows the final scene's r
## A wheel gesture scrolls one container (2026-10-20)
-Maka's own change. A wheel or trackpad gesture, or a touch pan, over a scroll container inside another, such as a tool's output box in a conversation list, moved both, because every `Div` and `List` scroll listener under the pointer took every wheel event. `Window` now keeps a `ScrollLatch` (`crates/gpui/src/gestures.rs`). The innermost container under the pointer that moves along the gesture's axis (the axis of its larger delta) takes the gesture and keeps it until the gesture ends, at [...]
+Maka's own change. A wheel or trackpad gesture, or a touch pan, over a scroll container inside another, such as a tool's output box in a conversation list, moved both, because every `Div` and `List` scroll listener under the pointer took every wheel event. `Window` now keeps a `ScrollLatch` (`crates/gpui/src/gestures.rs`). The innermost container under the pointer that moves along the gesture's axis (the axis of its larger delta) takes the gesture and keeps it until the gesture ends, at [...]
diff --git a/third-party/gpui-fast/SOURCE.zh-CN.md b/third-party/gpui-fast/SOURCE.zh-CN.md
index 3ced51114b..f23fac70ee 100644
--- a/third-party/gpui-fast/SOURCE.zh-CN.md
+++ b/third-party/gpui-fast/SOURCE.zh-CN.md
@@ -76,7 +76,7 @@ Window 可根据原 ClickInputState interruption 创建不可 clone 的 InputGes
## 上游同步(2026-10-20)
-五个适配 crate 现固定到 `b836dd970b26830f2124b7cf91b0e19018fd12da`(`v0.1.10`)。清单采用上游 `script/prepare-release.ts` 的发布规范;九个 Fast 包统一解析为 0.1.10,支持快照及适配 scheduler 保持 0.3.9。发布的 WGPU 后端按上游受检适配使用 registry `wgpu =30.0.1`,其中不含上游尚未发布的 Vulkan surface-drop 修复。Kit 保持原固定版本,其 Fast 版本约束接受本次更新。新增 rounded-clip、composition 和 hot-reload 源码已纳入原始字节清单;无关示例及 iOS 实现不导入,由 registry 依赖提供。
+五个适配 crate 现固定到 `b836dd970b26830f2124b7cf91b0e19018fd12da`(`v0.1.10`)。清单采用上游 `script/prepare-release.ts` 的发布规范;九个 Fast 包统一解析为 0.1.10,支持快照及适配 scheduler 保持 0.3.9。发布的 WGPU 后端按上游受检适配使用 registry `wgpu =30.0.1`,其中不含上游尚未发布的 Vulkan surface-drop 修复。Kit 现固定到 `77ff6f794dca3bcee2e90977cedf12a64aed17e4`(0.8.0),其 Fast 约束仍为同一个精确的 0.1.10 发布版。新增 rounded-clip、composition 和 hot-reload 源码已纳入原始字节清单;无关示例及 iOS 实现不导入,由 registry 依赖提供。
三方整合保留 Maka 原物理输入来源、实际发布的 IME-handler 槽位、选区上下文、定时器取消及原生清理回执。上游字段身份和 handler 恢复通知接入同一既有 owner。原生定时器补丁保留显式 async-io 依赖;profiler 计数器改用稳定的 `AtomicUsize::try_update` 替代弃用别名,保持原内存顺序和 checked subtraction。
@@ -84,4 +84,4 @@ Window 可根据原 ClickInputState interruption 创建不可 clone 的 InputGes
## 一次滚轮手势只滚动一个容器(2026-10-20)
-Maka 自己的改动。滚动容器嵌在另一个滚动容器里时(例如对话列表里的工具输出框),在它上面的一次滚轮或触控板手势(或一次触摸拖动)会让两者一起滚动,因为指针下每个 `Div` 和 `List` 的滚动监听器都会处理每个滚轮事件。现在 `Window` 持有一个 `ScrollLatch`(`crates/gpui/src/gestures.rs`):指针下沿手势方向(增量较大的那一轴)真正滚动了的最内层容器取得这次手势,直到手势结束,滚到尽头也不放手。它外层的容器(监听器在它之后运行)把这次手势的事件留给它,滚动中移到指针下的容器也不接手。滚不动的容器让事件继续向外传,所以框已在尽头时,下一次手势交给页面,与浏览器的滚动链一致;vendored GPUI Kit 的 `ScrollableMask` 在边缘把事件交给外层容器的行为也保持不变。手势在以下情况结束:收到 `TouchPhase::Started` 事件、滚轮事件位�
��改变、或取得手势的容器 300 毫秒内没有处理它的事件。以 `TouchPhase::Started` 开始的手势,要在收到 `TouchPh [...]
+Maka 自己的改动。滚动容器嵌在另一个滚动容器里时(例如对话列表里的工具输出框),在它上面的一次滚轮或触控板手势(或一次触摸拖动)会让两者一起滚动,因为指针下每个 `Div` 和 `List` 的滚动监听器都会处理每个滚轮事件。现在 `Window` 持有一个 `ScrollLatch`(`crates/gpui/src/gestures.rs`):指针下沿手势方向(增量较大的那一轴)真正滚动了的最内层容器取得这次手势,直到手势结束,滚到尽头也不放手。它外层的容器(监听器在它之后运行)把这次手势的事件留给它,滚动中移到指针下的容器也不接手。滚不动的容器让事件继续向外传,所以框已在尽头时,下一次手势交给页面,与浏览器的滚动链一致;vendored GPUI Kit 的 `ScrollableMask` 在边缘把事件交给外层容器的行为也保持不变。手势在以下情况结束:收到 `TouchPhase::Started` 事件、滚轮事件位�
��改变、或取得手势的容器 300 毫秒内没有处理它的事件。以 `TouchPhase::Started` 开始的手势,要在收到 `TouchPh [...]
diff --git a/third-party/gpui-kit/crates/base/src/scrollable_mask.rs b/third-party/gpui-kit/crates/base/src/scrollable_mask.rs
index 7da56f6c8d..6bbef18b60 100644
--- a/third-party/gpui-kit/crates/base/src/scrollable_mask.rs
+++ b/third-party/gpui-kit/crates/base/src/scrollable_mask.rs
@@ -1307,15 +1307,19 @@ mod tests {
fn vertical_wheel_leaves_trackpads_and_horizontal_wheels_to_the_scroller(
cx: &mut TestAppContext,
) {
- let (cx, strip, page) = vertical_wheel_test(cx);
- let line_height = cx.update(|window, _| window.line_height());
+ {
+ let (cx, strip, page) = vertical_wheel_test(cx);
- // A vertical trackpad swipe keeps the strip's axis lock.
- wheel(cx, ScrollDelta::Pixels(point(px(0.), px(-20.))));
- assert_eq!(strip.offset().x, px(0.));
- assert_eq!(page.offset().y, px(-20.));
+ // A vertical trackpad swipe keeps the strip's axis lock.
+ wheel(cx, ScrollDelta::Pixels(point(px(0.), px(-20.))));
+ assert_eq!(strip.offset().x, px(0.));
+ assert_eq!(page.offset().y, px(-20.));
+ }
- // A horizontal wheel reaches the strip's own listener.
+ // A separate gesture reaches the strip's own horizontal listener,
+ // without inheriting the preceding trackpad gesture's native latch.
+ let (cx, strip, _) = vertical_wheel_test(cx);
+ let line_height = cx.update(|window, _| window.line_height());
wheel(cx, ScrollDelta::Lines(point(-1., 0.)));
assert_eq!(strip.offset().x, -line_height);
}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
(maka) 06/11: chore(gpui): sync Kit to upstream 0.8.0
Posted by wy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch rust-preview
in repository https://gitbox.apache.org/repos/asf/maka.git
commit 0c2720ad07d3203bebce79f6aa94b185d4294933
Author: M4n5ter <m4...@gmail.com>
AuthorDate: Tue Oct 20 15:54:36 2026 +0800
chore(gpui): sync Kit to upstream 0.8.0
Import the fixed 77ff6f7 Kit source and coherent 0.8.0 dependency closure, preserving Maka input, selection, retained geometry and scoped runtime ownership. Use upstream list and HTML fixes and connect ExternalEditor links to native targets.
Generated-by: OpenAI Codex
---
Cargo.lock | 451 ++++++-
Cargo.toml | 3 +
LICENSE | 6 +-
third-party/gpui-kit/README.md | 4 +
third-party/gpui-kit/SOURCE.md | 36 +-
third-party/gpui-kit/crates/base/Cargo.toml | 8 +-
.../gpui-kit/crates/base/src/a11y_disabled.rs | 304 +++++
third-party/gpui-kit/crates/base/src/button.rs | 13 +-
third-party/gpui-kit/crates/base/src/checkbox.rs | 9 +-
.../gpui-kit/crates/base/src/date_picker.rs | 2 +
.../dock/fixtures/legacy_empty_stack_panel.json | 3 +
.../fixtures/placeholder_wrapped_empty_stack.json | 16 +
.../gpui-kit/crates/base/src/dock/state_convert.rs | 59 +-
third-party/gpui-kit/crates/base/src/focus_trap.rs | 51 +-
.../crates/base/src/input/base/blink_cursor.rs | 40 +-
.../gpui-kit/crates/base/src/input/base/element.rs | 456 ++++---
.../gpui-kit/crates/base/src/input/base/state.rs | 711 ++++++++++-
.../src/input/editor/display_map/display_map.rs | 7 +
.../base/src/input/editor/lsp/completions.rs | 176 ++-
.../crates/base/src/input/editor/lsp/mod.rs | 41 +
.../crates/base/src/input/editor/search.rs | 16 +
third-party/gpui-kit/crates/base/src/input/mod.rs | 9 +-
third-party/gpui-kit/crates/base/src/lib.rs | 2 +
third-party/gpui-kit/crates/base/src/link.rs | 6 +-
third-party/gpui-kit/crates/base/src/motion.rs | 2 +-
.../gpui-kit/crates/base/src/motion/presence.rs | 5 +-
.../gpui-kit/crates/base/src/number_input.rs | 18 +-
third-party/gpui-kit/crates/base/src/overscroll.rs | 463 +++++++
third-party/gpui-kit/crates/base/src/popover.rs | 13 +-
third-party/gpui-kit/crates/base/src/radio.rs | 3 +-
third-party/gpui-kit/crates/base/src/root.rs | 352 +++++-
.../gpui-kit/crates/base/src/scroll_bounce.rs | 537 +++++++-
.../gpui-kit/crates/base/src/scrollable_mask.rs | 168 ++-
third-party/gpui-kit/crates/base/src/scrollbar.rs | 218 +++-
third-party/gpui-kit/crates/base/src/select.rs | 138 +-
third-party/gpui-kit/crates/base/src/slider.rs | 140 ++-
third-party/gpui-kit/crates/base/src/switch.rs | 11 +-
third-party/gpui-kit/crates/base/src/tabs.rs | 3 +-
.../gpui-kit/crates/base/src/test_support.rs | 9 +
.../gpui-kit/crates/base/src/text/document.rs | 40 +-
.../gpui-kit/crates/base/src/text/editor_host.rs | 1324 ++++++++++++++++++++
.../gpui-kit/crates/base/src/text/editor_seam.rs | 86 ++
.../gpui-kit/crates/base/src/text/format/html.rs | 297 ++++-
.../crates/base/src/text/format/html5minify/mod.rs | 34 +-
.../crates/base/src/text/format/markdown.rs | 1 +
.../gpui-kit/crates/base/src/text/inline.rs | 432 +++++--
.../gpui-kit/crates/base/src/text/inline_flow.rs | 194 ++-
.../gpui-kit/crates/base/src/text/inline_object.rs | 52 +-
third-party/gpui-kit/crates/base/src/text/link.rs | 407 +++++-
third-party/gpui-kit/crates/base/src/text/mod.rs | 2 +
third-party/gpui-kit/crates/base/src/text/node.rs | 541 +++++++-
.../crates/base/src/text/selection_adapter.rs | 83 +-
third-party/gpui-kit/crates/base/src/text/state.rs | 29 +-
third-party/gpui-kit/crates/base/src/text/style.rs | 33 +-
.../gpui-kit/crates/base/src/text/text_view.rs | 472 ++++++-
.../gpui-kit/crates/base/src/text_selection.rs | 158 ++-
third-party/gpui-kit/crates/base/src/time_field.rs | 3 +-
third-party/gpui-kit/crates/base/src/toggle.rs | 3 +-
third-party/gpui-kit/crates/base/src/tree.rs | 154 ++-
.../gpui-kit/crates/base/src/window_border.rs | 17 +-
third-party/gpui-kit/crates/shell/Cargo.toml | 21 +-
third-party/gpui-kit/crates/shell/README.md | 107 +-
.../gpui-kit/crates/shell/component-inventory.json | 17 +
.../gpui-kit/crates/shell/src/capability.rs | 321 ++++-
.../crates/shell/src/component_registry.rs | 147 ++-
.../gpui-kit/crates/shell/src/components/mod.rs | 40 +-
.../crates/shell/src/components/shell/chat.rs | 187 ++-
.../shell/src/components/shell/layout/textarea.rs | 1 +
.../crates/shell/src/components/shell/media/mod.rs | 4 +
.../shell/src/components/shell/media/webview.rs | 456 +++++++
.../src/components/shell/retained_forms/mod.rs | 1 +
.../shell/src/components/shell/scroll/scroll.rs | 2 +
third-party/gpui-kit/crates/shell/src/default-env | 92 ++
.../crates/shell/src/engine/quickjs/dock_api.rs | 39 +-
.../crates/shell/src/engine/quickjs/entity_api.rs | 190 ++-
.../crates/shell/src/engine/quickjs/host.rs | 248 +++-
.../shell/src/engine/quickjs/host_modules.rs | 34 +-
.../crates/shell/src/engine/quickjs/js_error.rs | 66 +
.../crates/shell/src/engine/quickjs/mod.rs | 955 ++++++++++----
.../crates/shell/src/engine/quickjs/overlay.rs | 102 +-
.../crates/shell/src/engine/quickjs/sandbox.rs | 584 ++++++++-
.../crates/shell/src/engine/quickjs/scheduler.rs | 157 ++-
.../shell/src/engine/quickjs/standard/encoding.rs | 307 +++++
.../shell/src/engine/quickjs/standard/fetch.rs | 59 +-
.../crates/shell/src/engine/quickjs/standard/fs.rs | 8 +
.../shell/src/engine/quickjs/standard/mod.rs | 6 +
.../shell/src/engine/quickjs/standard/net.rs | 9 +-
.../crates/shell/src/engine/quickjs/standard/os.rs | 18 +-
.../shell/src/engine/quickjs/standard/process.rs | 2 +-
.../shell/src/engine/quickjs/standard/websocket.rs | 41 +-
.../crates/shell/src/engine/quickjs/template.rs | 65 +-
.../crates/shell/src/engine/quickjs/theme_api.rs | 22 +-
.../crates/shell/src/engine/quickjs/window_api.rs | 27 +-
third-party/gpui-kit/crates/shell/src/entities.rs | 2 +-
third-party/gpui-kit/crates/shell/src/host.rs | 148 ++-
.../gpui-kit/crates/shell/src/host/agents.md | 64 +-
.../gpui-kit/crates/shell/src/host/installed.rs | 4 +-
.../gpui-kit/crates/shell/src/host/scaffold.rs | 4 +-
third-party/gpui-kit/crates/shell/src/lib.rs | 5 +-
.../gpui-kit/crates/shell/src/materialize.rs | 16 +
.../shell/src/materialize/components/list.rs | 32 +-
.../shell/src/materialize/components/tooltip.rs | 6 +-
.../src/materialize/components/virtual_list.rs | 60 +-
third-party/gpui-kit/crates/shell/src/path.rs | 32 +-
third-party/gpui-kit/crates/shell/src/plugin.rs | 232 +++-
third-party/gpui-kit/crates/shell/src/policy.rs | 26 +-
third-party/gpui-kit/crates/shell/src/process.rs | 730 ++++++++++-
third-party/gpui-kit/crates/shell/src/root.rs | 37 +-
third-party/gpui-kit/crates/shell/src/runtime.rs | 549 ++++++--
third-party/gpui-kit/crates/shell/src/spec.rs | 61 +-
third-party/gpui-kit/crates/shell/src/style.rs | 210 +++-
third-party/gpui-kit/crates/shell/src/tests/fs.rs | 316 +++++
.../gpui-kit/crates/shell/src/tests/network.rs | 123 +-
.../gpui-kit/crates/shell/src/tests/process.rs | 301 ++++-
.../gpui-kit/crates/shell/src/tests/render.rs | 293 ++++-
.../gpui-kit/crates/shell/src/tests/snapshot.rs | 101 ++
.../crates/shell/src/tests/standard_runtime.rs | 144 ++-
.../gpui-kit/crates/shell/src/tests/template.rs | 37 +
third-party/gpui-kit/crates/shell/src/typings.rs | 461 ++++++-
third-party/gpui-kit/crates/shell/src/value.rs | 120 +-
third-party/gpui-kit/crates/shell/src/view.rs | 32 +-
third-party/gpui-kit/crates/shell/src/watch.rs | 74 +-
third-party/gpui-kit/upstream.json | 230 ++--
123 files changed, 15628 insertions(+), 2028 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index f17b9387a7..9a3ce16cfc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -349,6 +349,17 @@ dependencies = [
"libc",
]
+[[package]]
+name = "annotate-snippets"
+version = "0.12.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1"
+dependencies = [
+ "anstyle",
+ "memchr",
+ "unicode-width",
+]
+
[[package]]
name = "anstyle"
version = "1.0.14"
@@ -992,6 +1003,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6"
+[[package]]
+name = "base62"
+version = "2.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3a647dce9631f152a99ec16cfc09267ae55fe280d5dc288964027d458076323"
+
[[package]]
name = "base64"
version = "0.22.1"
@@ -2658,7 +2675,7 @@ dependencies = [
"uuid",
"windows 0.58.0",
"zeroize",
- "zune-core",
+ "zune-core 0.5.1",
]
[[package]]
@@ -3915,6 +3932,26 @@ dependencies = [
"syn 2.0.119",
]
+[[package]]
+name = "enum-iterator"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016"
+dependencies = [
+ "enum-iterator-derive",
+]
+
+[[package]]
+name = "enum-iterator-derive"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
[[package]]
name = "enumflags2"
version = "0.7.12"
@@ -4759,6 +4796,16 @@ dependencies = [
"polyval 0.7.3",
]
+[[package]]
+name = "gif"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b"
+dependencies = [
+ "color_quant",
+ "weezl",
+]
+
[[package]]
name = "gif"
version = "0.14.2"
@@ -5649,6 +5696,17 @@ dependencies = [
"regex-syntax",
]
+[[package]]
+name = "globwalk"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
+dependencies = [
+ "bitflags 1.3.2",
+ "ignore",
+ "walkdir",
+]
+
[[package]]
name = "glow"
version = "0.17.0"
@@ -5717,7 +5775,7 @@ dependencies = [
[[package]]
name = "gpui-base"
-version = "0.7.3"
+version = "0.8.0"
dependencies = [
"aho-corasick",
"anyhow",
@@ -5761,6 +5819,64 @@ dependencies = [
"windows 0.58.0",
]
+[[package]]
+name = "gpui-component"
+version = "0.8.0"
+source = "git+https://github.com/longbridge/gpui-kit?rev=77ff6f794dca3bcee2e90977cedf12a64aed17e4#77ff6f794dca3bcee2e90977cedf12a64aed17e4"
+dependencies = [
+ "anyhow",
+ "chrono",
+ "core-text",
+ "enum-iterator",
+ "gpui-base",
+ "gpui-component-macros",
+ "gpui-fast",
+ "gpui-kit-assets",
+ "gpui-pre",
+ "gpui-pre-macros",
+ "gpui-pre-sum-tree",
+ "instant",
+ "itertools 0.13.0",
+ "log",
+ "lsp-types",
+ "markdown",
+ "notify",
+ "num-traits",
+ "objc2 0.6.4",
+ "objc2-app-kit 0.3.2",
+ "objc2-foundation 0.3.2",
+ "once_cell",
+ "paste",
+ "raw-window-handle",
+ "regex",
+ "resvg 0.45.1",
+ "ropey 2.0.0-beta.1",
+ "rust-i18n",
+ "schemars 1.2.2",
+ "serde",
+ "serde_json",
+ "serde_repr",
+ "similar 2.7.0",
+ "smallvec",
+ "smol",
+ "tracing",
+ "unicode-segmentation",
+ "unicode-width",
+ "uuid",
+ "windows 0.58.0",
+]
+
+[[package]]
+name = "gpui-component-macros"
+version = "0.8.0"
+source = "git+https://github.com/longbridge/gpui-kit?rev=77ff6f794dca3bcee2e90977cedf12a64aed17e4#77ff6f794dca3bcee2e90977cedf12a64aed17e4"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
[[package]]
name = "gpui-fast"
version = "0.1.10"
@@ -5809,7 +5925,7 @@ dependencies = [
"rand 0.9.5",
"raw-window-handle",
"regex",
- "resvg",
+ "resvg 0.46.0",
"schemars 1.2.2",
"seahash",
"serde",
@@ -5822,7 +5938,7 @@ dependencies = [
"thiserror 2.0.20",
"ttf-parser",
"url",
- "usvg",
+ "usvg 0.46.0",
"uuid",
"waker-fn",
"web-time",
@@ -6068,8 +6184,8 @@ dependencies = [
[[package]]
name = "gpui-fps"
-version = "0.7.3"
-source = "git+https://github.com/longbridge/gpui-kit?rev=c5f8eca4a0759f8c0c7ed624eddfd03f1c93a5f2#c5f8eca4a0759f8c0c7ed624eddfd03f1c93a5f2"
+version = "0.8.0"
+source = "git+https://github.com/longbridge/gpui-kit?rev=77ff6f794dca3bcee2e90977cedf12a64aed17e4#77ff6f794dca3bcee2e90977cedf12a64aed17e4"
dependencies = [
"core-graphics 0.24.0",
"gpui-fast",
@@ -6084,6 +6200,38 @@ dependencies = [
"windows 0.58.0",
]
+[[package]]
+name = "gpui-kit"
+version = "0.8.0"
+source = "git+https://github.com/longbridge/gpui-kit?rev=77ff6f794dca3bcee2e90977cedf12a64aed17e4#77ff6f794dca3bcee2e90977cedf12a64aed17e4"
+dependencies = [
+ "gpui-base",
+ "gpui-component",
+ "gpui-fast",
+ "gpui-fast-platform",
+ "gpui-fast-web",
+ "gpui-kit-assets",
+ "gpui-pre",
+ "gpui-pre-macros",
+ "gpui-pre-platform",
+ "gpui-pre-web",
+]
+
+[[package]]
+name = "gpui-kit-assets"
+version = "0.8.0"
+source = "git+https://github.com/longbridge/gpui-kit?rev=77ff6f794dca3bcee2e90977cedf12a64aed17e4#77ff6f794dca3bcee2e90977cedf12a64aed17e4"
+dependencies = [
+ "anyhow",
+ "gpui-fast",
+ "gpui-pre",
+ "gpui-pre-reqwest",
+ "log",
+ "rust-embed",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+]
+
[[package]]
name = "gpui-pre"
version = "0.3.9"
@@ -6133,7 +6281,7 @@ dependencies = [
"rand 0.9.5",
"raw-window-handle",
"regex",
- "resvg",
+ "resvg 0.46.0",
"schemars 1.2.2",
"seahash",
"serde",
@@ -6146,7 +6294,7 @@ dependencies = [
"thiserror 2.0.20",
"ttf-parser",
"url",
- "usvg",
+ "usvg 0.46.0",
"uuid",
"waker-fn",
"web-time",
@@ -6616,7 +6764,7 @@ checksum = "4b0d73e1bc02eef886a5947f41b8512708792e9e5d3053d181843311a487f96f"
[[package]]
name = "gpui-shell"
-version = "0.7.3"
+version = "0.8.0"
dependencies = [
"anyhow",
"async-channel",
@@ -6628,6 +6776,7 @@ dependencies = [
"gpui-fast",
"gpui-fast-platform",
"gpui-fps",
+ "gpui-kit",
"gpui-pre",
"gpui-pre-platform",
"gpui-pre-reqwest",
@@ -6647,11 +6796,21 @@ dependencies = [
"tar",
"tracing",
"tungstenite 0.29.0",
- "usvg",
+ "usvg 0.46.0",
"wait-timeout",
"windows 0.58.0",
]
+[[package]]
+name = "granit-parser"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e20f99e46474f56bd905c56e817ebddcf377a611f94c53ac4649e4d3fa3c0cd0"
+dependencies = [
+ "arraydeque",
+ "smallvec",
+]
+
[[package]]
name = "grep-matcher"
version = "0.1.9"
@@ -7400,7 +7559,7 @@ dependencies = [
"byteorder-lite",
"color_quant",
"exr",
- "gif",
+ "gif 0.14.2",
"image-webp",
"moxcms",
"num-traits",
@@ -7409,8 +7568,8 @@ dependencies = [
"ravif",
"rayon",
"tiff",
- "zune-core",
- "zune-jpeg",
+ "zune-core 0.5.1",
+ "zune-jpeg 0.5.15",
]
[[package]]
@@ -7421,6 +7580,12 @@ dependencies = [
"quick-error 2.0.1",
]
+[[package]]
+name = "imagesize"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285"
+
[[package]]
name = "imagesize"
version = "0.14.0"
@@ -7626,6 +7791,15 @@ dependencies = [
"once_cell",
]
+[[package]]
+name = "itertools"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
+dependencies = [
+ "either",
+]
+
[[package]]
name = "itertools"
version = "0.13.0"
@@ -7976,6 +8150,17 @@ dependencies = [
"libc",
]
+[[package]]
+name = "kurbo"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62"
+dependencies = [
+ "arrayvec",
+ "euclid",
+ "smallvec",
+]
+
[[package]]
name = "kurbo"
version = "0.13.1"
@@ -9468,7 +9653,7 @@ dependencies = [
"schemars 1.2.2",
"serde",
"serde_json",
- "similar",
+ "similar 3.2.0",
"syntect",
"tempfile",
"tokio",
@@ -10315,6 +10500,12 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "nohash-hasher"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
+
[[package]]
name = "nom"
version = "1.2.4"
@@ -10352,6 +10543,15 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
+[[package]]
+name = "normpath"
+version = "1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b11ce00d2594068e8a27c9146fdc5cf9f3ac38eb42c7cd34d05fea618873ac9f"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "notify"
version = "8.2.0"
@@ -12925,21 +13125,38 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7"
+[[package]]
+name = "resvg"
+version = "0.45.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43"
+dependencies = [
+ "gif 0.13.3",
+ "image-webp",
+ "log",
+ "pico-args",
+ "rgb",
+ "svgtypes 0.15.3",
+ "tiny-skia 0.11.4",
+ "usvg 0.45.1",
+ "zune-jpeg 0.4.21",
+]
+
[[package]]
name = "resvg"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b563218631706d614e23059436526d005b50ab5f2d506b55a17eb65c5eb83419"
dependencies = [
- "gif",
+ "gif 0.14.2",
"image-webp",
"log",
"pico-args",
"rgb",
- "svgtypes",
- "tiny-skia",
- "usvg",
- "zune-jpeg",
+ "svgtypes 0.16.1",
+ "tiny-skia 0.11.4",
+ "usvg 0.46.0",
+ "zune-jpeg 0.5.15",
]
[[package]]
@@ -13109,6 +13326,91 @@ dependencies = [
"sqlite-wasm-rs",
]
+[[package]]
+name = "rust-embed"
+version = "8.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19afa5b4b6a611de00bd1bdae6ae6f39084c9399f0679c3f52d8469cf335cc23"
+dependencies = [
+ "rust-embed-impl",
+ "rust-embed-utils",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-impl"
+version = "8.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0d8afda6374eac59e066abee06d265247ebbaf3006cf878e2879e8356e34053"
+dependencies = [
+ "mime_guess",
+ "proc-macro2",
+ "quote",
+ "rust-embed-utils",
+ "shellexpand",
+ "syn 2.0.119",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-utils"
+version = "8.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d84e8ba78bd384263e5922f084cbe1b081c3b7e69add59c8fb097b879ba968a"
+dependencies = [
+ "globset",
+ "sha2 0.11.1",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-i18n"
+version = "4.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8502e73e6461c738659455c5d2cf95bc132760a6f203c717e66ee72b78906d7f"
+dependencies = [
+ "globwalk",
+ "regex",
+ "rust-i18n-macro",
+ "rust-i18n-support",
+ "smallvec",
+]
+
+[[package]]
+name = "rust-i18n-macro"
+version = "4.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d54a28dd9d46108d56b9f0c85b220908844233e561c03087890325104f90f2c6"
+dependencies = [
+ "glob",
+ "proc-macro2",
+ "quote",
+ "rust-i18n-support",
+ "serde",
+ "serde_json",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "rust-i18n-support"
+version = "4.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e806c2a9701cbd6043992cd935c30cb1fdb128fa15e7b506f3b46f97045737c0"
+dependencies = [
+ "arc-swap",
+ "base62",
+ "foldhash 0.2.0",
+ "globwalk",
+ "itertools 0.11.0",
+ "normpath",
+ "serde",
+ "serde-saphyr",
+ "serde_json",
+ "siphasher",
+ "toml 0.8.23",
+ "triomphe",
+]
+
[[package]]
name = "rust_decimal"
version = "1.42.1"
@@ -13635,6 +13937,23 @@ dependencies = [
"serde_derive",
]
+[[package]]
+name = "serde-saphyr"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8050abb251097357e24aff63ba2c52a6309ecb7d23a5474023df960a02694d8"
+dependencies = [
+ "annotate-snippets",
+ "base64 0.23.1",
+ "encoding_rs_io",
+ "granit-parser",
+ "nohash-hasher",
+ "num-traits",
+ "serde_core",
+ "smallvec",
+ "zmij",
+]
+
[[package]]
name = "serde_bytes"
version = "0.11.19"
@@ -13939,6 +14258,15 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
+[[package]]
+name = "shellexpand"
+version = "3.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8"
+dependencies = [
+ "dirs",
+]
+
[[package]]
name = "shlex"
version = "1.3.0"
@@ -14055,6 +14383,16 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+[[package]]
+name = "similar"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
+dependencies = [
+ "bstr",
+ "unicode-segmentation",
+]
+
[[package]]
name = "similar"
version = "3.2.0"
@@ -14587,13 +14925,23 @@ version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb"
+[[package]]
+name = "svgtypes"
+version = "0.15.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc"
+dependencies = [
+ "kurbo 0.11.3",
+ "siphasher",
+]
+
[[package]]
name = "svgtypes"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "695b5790b3131dafa99b3bbfd25a216edb3d216dad9ca208d4657bfb8f2abc3d"
dependencies = [
- "kurbo",
+ "kurbo 0.13.1",
"siphasher",
]
@@ -15004,7 +15352,7 @@ dependencies = [
"half",
"quick-error 2.0.1",
"weezl",
- "zune-jpeg",
+ "zune-jpeg 0.5.15",
]
[[package]]
@@ -15472,6 +15820,17 @@ dependencies = [
"petgraph",
]
+[[package]]
+name = "triomphe"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d15f4f5898bcfc5d8a8e45ed30b2f108d710c1a2e5489dbf80e078f707181bc"
+dependencies = [
+ "arc-swap",
+ "serde",
+ "stable_deref_trait",
+]
+
[[package]]
name = "try-lock"
version = "0.2.5"
@@ -15854,6 +16213,33 @@ dependencies = [
"url",
]
+[[package]]
+name = "usvg"
+version = "0.45.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef"
+dependencies = [
+ "base64 0.22.1",
+ "data-url",
+ "flate2",
+ "fontdb",
+ "imagesize 0.13.0",
+ "kurbo 0.11.3",
+ "log",
+ "pico-args",
+ "roxmltree 0.20.0",
+ "rustybuzz",
+ "simplecss",
+ "siphasher",
+ "strict-num",
+ "svgtypes 0.15.3",
+ "tiny-skia-path 0.11.4",
+ "unicode-bidi",
+ "unicode-script",
+ "unicode-vo",
+ "xmlwriter",
+]
+
[[package]]
name = "usvg"
version = "0.46.0"
@@ -15865,7 +16251,7 @@ dependencies = [
"flate2",
"fontdb",
"imagesize 0.14.0",
- "kurbo",
+ "kurbo 0.13.1",
"log",
"pico-args",
"roxmltree 0.21.1",
@@ -15873,8 +16259,8 @@ dependencies = [
"simplecss",
"siphasher",
"strict-num",
- "svgtypes",
- "tiny-skia-path",
+ "svgtypes 0.16.1",
+ "tiny-skia-path 0.11.4",
"unicode-bidi",
"unicode-script",
"unicode-vo",
@@ -18112,6 +18498,12 @@ dependencies = [
"pkg-config",
]
+[[package]]
+name = "zune-core"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
+
[[package]]
name = "zune-core"
version = "0.5.1"
@@ -18127,13 +18519,22 @@ dependencies = [
"simd-adler32",
]
+[[package]]
+name = "zune-jpeg"
+version = "0.4.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713"
+dependencies = [
+ "zune-core 0.4.12",
+]
+
[[package]]
name = "zune-jpeg"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
dependencies = [
- "zune-core",
+ "zune-core 0.5.1",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index b231218357..9a87dc9abc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -203,3 +203,6 @@ gpui-fast-macos = { path = "third-party/gpui-fast/crates/gpui_macos" }
gpui-fast-windows = { path = "third-party/gpui-fast/crates/gpui_windows" }
gpui-fast-linux = { path = "third-party/gpui-fast/crates/gpui_linux" }
gpui-pre-scheduler = { path = "third-party/gpui-fast/crates/scheduler" }
+
+[patch."https://github.com/longbridge/gpui-kit"]
+gpui-base = { path = "third-party/gpui-kit/crates/base" }
diff --git a/LICENSE b/LICENSE
index 8d898e341b..93f36e64eb 100644
--- a/LICENSE
+++ b/LICENSE
@@ -339,11 +339,11 @@ Copyright 2022 - 2025 Zed Industries, Inc.
Original license texts, finite file provenance and modifications are recorded in
third-party/gpui-fast/{LICENSE,LICENSE-APACHE,upstream.json,SOURCE.md}.
-GPUI Kit Base source (adapted, Apache License 2.0)
+GPUI Kit Base and Shell sources (adapted, Apache License 2.0)
-third-party/gpui-kit contains gpui-base from
+third-party/gpui-kit contains gpui-base and gpui-shell from
https://github.com/longbridge/gpui-kit at revision
-29c04574dc838a8e59e84c9c9f3c4d15983222ce. Upstream license texts,
+77ff6f794dca3bcee2e90977cedf12a64aed17e4. Upstream license texts,
original hashes and the bounded local modifications are recorded in
third-party/gpui-kit/{LICENSE-APACHE,LICENSE-DOCS.md,upstream.json,SOURCE.md}.
diff --git a/third-party/gpui-kit/README.md b/third-party/gpui-kit/README.md
index 268b4a2603..3c7afba2e8 100644
--- a/third-party/gpui-kit/README.md
+++ b/third-party/gpui-kit/README.md
@@ -253,6 +253,10 @@ Check out [CONTRIBUTING.md](CONTRIBUTING.md) for more details.
See the [comparison with Iced, egui and Qt 6](https://gpui-kit.com/docs/comparison) on the site.
+## Related project: Native Shell
+
+[Native Shell](https://nsh.dev) ([source](https://github.com/longbridge/native-shell)) is a sibling project that lets AI generate UI and renders it natively on iOS, Android, Linux (GTK) and macOS (AppKit). Its GPUI desktop backend is in development and will be built on GPUI Kit.
+
## License
Software source and documentation code examples: [Apache-2.0](LICENSE-APACHE).
diff --git a/third-party/gpui-kit/SOURCE.md b/third-party/gpui-kit/SOURCE.md
index 0f930b3fc3..388004d8e1 100644
--- a/third-party/gpui-kit/SOURCE.md
+++ b/third-party/gpui-kit/SOURCE.md
@@ -19,7 +19,7 @@
# GPUI Kit local presentation projection
-The `gpui-base` crate is copied from [GPUI Kit](https://github.com/longbridge/gpui-kit/tree/c5f8eca4a0759f8c0c7ed624eddfd03f1c93a5f2), revision `c5f8eca4a0759f8c0c7ed624eddfd03f1c93a5f2`, under Apache-2.0. The `gpui-shell` crate is copied from the same revision; see the section below. Other Kit crates remain dependencies from that exact revision. `upstream.json` records the finite imported file inventory and original SHA-256 hashes before modifications; licenses are retained byte-for-byt [...]
+The `gpui-base` crate is copied from [GPUI Kit](https://github.com/longbridge/gpui-kit/tree/77ff6f794dca3bcee2e90977cedf12a64aed17e4), revision `77ff6f794dca3bcee2e90977cedf12a64aed17e4`, under Apache-2.0. The `gpui-shell` crate is copied from the same revision; see the section below. Other Kit crates remain dependencies from that exact revision. `upstream.json` records the finite imported file inventory and original SHA-256 hashes before modifications; licenses are retained byte-for-byt [...]
The bounded changes preserve the single native editing engine:
@@ -162,11 +162,11 @@ Plain-text paint compares its own attached document's copy projection, rather th
## Upstream synchronization (2026-10-14)
-Kit is pinned to `c5f8eca4a0759f8c0c7ed624eddfd03f1c93a5f2` (0.7.3). The input-menu callback and parser acknowledgement now use upstream's implementations, replacing the equivalent local patches. Upstream's immutable run/copy projections and quiet participant registration replace the corresponding mutable query paths. Its reflow selection preservation, table rendering and native controls are retained.
+This synchronization pinned Kit to `c5f8eca4a0759f8c0c7ed624eddfd03f1c93a5f2` (0.7.3); the current revision is recorded in the 2026-10-20 synchronization section below. The input-menu callback and parser acknowledgement now use upstream's implementations, replacing the equivalent local patches. Upstream's immutable run/copy projections and quiet participant registration replace the corresponding mutable query paths. Its reflow selection preservation, table rendering and native controls a [...]
The local physical input groups, exact byte selections, per-hitbox retained geometry and locally projected theme values remain: the upstream global traversal stack and paint-order counter do not provide those behaviors. Quiet registration still passes a participant's first or changed group through original custody reconciliation; completed-frame pruning remains tied to the native focus tree and hitboxes. This update does not merge the separate UI performance branch or migrate Maka to gpu [...]
-同步上游后,菜单回调、解析确认和部分只读选区投影直接采用上游实现。仍保留上游尚未覆盖的物理输入隔离、精确字节选区、保留帧几何与局部主题投影;本次不合并独立性能分支,也不代表完成 gpui-shell 迁移。
+该次同步将 Kit 固定到 `c5f8eca4a0759f8c0c7ed624eddfd03f1c93a5f2`(0.7.3);当前版本见下方 2026-10-20 同步节。菜单回调、解析确认和部分只读选区投影直接采用上游实现。仍保留上游尚未覆盖的物理输入隔离、精确字节选区、保留帧几何与局部主题投影;本次不合并独立性能分支,也不代表完成 gpui-shell 迁移。
Participant enablement checks the current value before acquiring a write lease. Repeating the same admission state during layout must not invalidate retained rows under upstream's finer-grained entity-write tracking; real enablement changes retain the original revocation and group withdrawal path.
@@ -174,18 +174,18 @@ Plain input modes can install an exact whitespace-delimited literal-token regist
## gpui-shell runtime (2026-10-16)
-`crates/shell` (`gpui-shell`) is copied from revision `c5f8eca4a0759f8c0c7ed624eddfd03f1c93a5f2` with its `rquickjs-compat` facade, README and component inventory; `upstream.json` records each original file. Its integration tests are omitted because most of them need `gpui-component`. The facade's manifest declares MIT to match the `rquickjs` crate it replaces; its source comes from the GPUI Kit repository. The workspace includes both crates as members and patches crates.io `rquickjs` to [...]
+`crates/shell` (`gpui-shell`) is copied from revision `77ff6f794dca3bcee2e90977cedf12a64aed17e4` with its `rquickjs-compat` facade, README and component inventory; `upstream.json` records each original file. Its integration tests are omitted because most of them need `gpui-component`. The facade's manifest declares MIT to match the `rquickjs` crate it replaces; its source comes from the GPUI Kit repository. The workspace includes both crates as members and patches crates.io `rquickjs` to [...]
-The manifest is normalized to explicit upstream dependencies and lints. `gpui-fast` and `quickjs` are default features. The `components` feature and its `gpui-component` dependency are removed: `gpui-component` at this revision does not build against the adapted Base above, so an embedding host registers its own component catalog. Check-cfg declares the removed feature name, so the code it guards compiles out without warnings.
+The manifest is normalized to explicit upstream dependencies and lints. `gpui-fast` and `quickjs` are default features. The `components` and `webview` features and their `gpui-component`, `gpui-webview` and `wry` dependencies are removed: `gpui-component` at this revision does not build against the adapted Base above, so an embedding host registers its own component catalog. Check-cfg declares the removed feature names, so the code it guards compiles out without warnings.
-The bounded source changes adapt the runtime to the adapted Base, add two development switches, and backport four upstream pull requests (#3547, #3549 and #3550 have since been merged; #3548 is open):
+The bounded source changes adapt the runtime to the adapted Base and add two development switches. The four former backports (#3547, #3548, #3549 and #3550) now use their merged upstream implementations:
- `TextView` link handling uses Base's `on_link_activation`. Both handlers return a decision that prevents Base's default URL opening: the script callback receives the URL, or, without one, the shell opens only URLs its own rules admit. Accessibility activation counts as keyboard activation.
- Two element helpers that call `ElementExt::on_prepaint` add the `IntoElement` bound the adapted trait requires.
- `GPUI_SHELL_PASSIVE` opens the window without activating the application or taking focus. `GPUI_SHELL_METRICS_LOG` prints the runtime's script counters to stderr once a second. Both are off unless the variable is set.
-- From longbridge/gpui-kit #3547, #3548, #3549 and #3550: a `list` keeps the reader on the item at the top of the view when its item count changes; `with_follow_mode("tail")` makes a `list` follow its end; `on_visible_range_change` reports which items a `list` shows after a frame in which that changed; and `TextareaState.set_submit_on_enter` makes Enter submit without inserting a line break. The next sync replaces them with the upstream versions.
+- From the merged upstream #3547, #3548, #3549 and #3550: a `list` keeps the reader on the item at the top of the view when its item count changes; `with_follow_mode("tail")` makes a `list` follow its end; `on_visible_range_change` reports which items a `list` shows after a frame in which that changed; and `TextareaState.set_submit_on_enter` makes Enter submit without inserting a line break.
-`gpui-shell` 及其 `rquickjs-compat` 兼容层自同一上游版本复制,`upstream.json` 记录每个原始文件;集成测试大多依赖 `gpui-component`,因此未导入。兼容层清单声明 MIT 以对应其替代的 `rquickjs`,源码来自 GPUI Kit 仓库。工作区将两者作为成员,并把 crates.io 的 `rquickjs` 指向本地兼容层,使 LLRT 与 shell 共用同一 QuickJS VM。清单改为显式上游依赖与 lints,默认启用 `gpui-fast` 与 `quickjs`;该版本的 `gpui-component` 无法基于上面改过的 Base 编译,因此移除 `components` 特性及其依赖,由嵌入方注册自己的组件目录。源码改动仅限:链接处理改用 Base 的 `on_link_activation` 并阻止 Base 默认打开 URL,由脚本回调或 shell 自身的 URL 规则决定;两个调用 `on_prepaint` 的辅助函数补上 `IntoElement` 约束;新增仅在设置环境 [...]
+`gpui-shell` 及其 `rquickjs-compat` 兼容层自同一上游版本复制,`upstream.json` 记录每个原始文件;集成测试大多依赖 `gpui-component`,因此未导入。兼容层清单声明 MIT 以对应其替代的 `rquickjs`,源码来自 GPUI Kit 仓库。工作区将两者作为成员,并把 crates.io 的 `rquickjs` 指向本地兼容层,使 LLRT 与 shell 共用同一 QuickJS VM。清单改为显式上游依赖与 lints,默认启用 `gpui-fast` 与 `quickjs`;该版本的 `gpui-component` 无法基于上面改过的 Base 编译,因此同时移除 `components`、`webview` 特性及其 `gpui-component`、`gpui-webview`、`wry` 依赖,由嵌入方注册自己的组件目录;本地投影不提供上游 WebView 组件。源码改动仅限:链接处理改用 Base 的 `on_link_activation` 并阻止 Base 默认打开 URL,由脚本回调或 [...]
## Desktop theme fixes (2026-10-16)
@@ -221,19 +221,25 @@ One addition to the script API of `gpui-shell`, a candidate for an upstream pull
为 `gpui-shell` 的脚本 API 新增一项,可提交上游 PR:`InputState` 与 `TextareaState` 新增 `set_placeholder(text)`,在存活的状态上调用 gpui-base 的 `set_placeholder`。此前占位文字只能在 `new({ placeholder })` 时设定,应用在运行中切换语言时,必须连同监听器重建每个输入框,才能翻译空输入框显示的文字。
-## `<pre>` text kept as text (2026-10-17)
+## Explicit measured-list end requests (2026-10-18)
-One fix to Base's HTML minifier, submitted upstream as longbridge/gpui-kit#3570:
+The shell exposes `list.scroll_to_end(requestKey)` for an explicit local action such as an accepted message. A new non-null string scrolls once after applying the current item count; a stable key does not undo a later user scroll. Tail mode resumes following, while Normal mode only scrolls once. The request reuses the retained native list state and clears pending native scrolling before moving to the end. Native tests check row identity and in-row pixel offsets, including a final row tal [...]
-- `TextView::html` minifies the HTML and parses the result again. The text of a `<pre>` or `<textarea>` skips whitespace collapsing, and the minifier wrote it as it was decoded, so escaped markup such as `<b>` came back as an element and an `&` as a bare `&`. That text is now escaped with the function the collapsing path uses. The parser also drops a line feed right after `<pre>` or `<textarea>`, so text that starts the element with a line feed (comments before it, which the mi [...]
+shell 通过 `list.scroll_to_end(requestKey)` 表达消息受理等本地动作的滚动意图。新的非空字符串在应用当前条目数后滚动一次;相同键不会撤销用户之后的上滚。Tail 模式恢复跟随,Normal 模式只滚动一次。请求复用原生列表状态,先清除待执行的原生滚动。原生测试检查行身份和行内像素位置,包含末行高于视口的情况。导入的 WebSocket 请求头测试先准备脚本运行时,再开始原有的有界服务端等待,保留全部真实请求头断言。
-为 Base 的 HTML minifier 修了一处,已提交上游 longbridge/gpui-kit#3570:`TextView::html` 先压缩 HTML 再重新解析。`<pre>` 与 `<textarea>` 中的文本不折叠空白,minifier 原样写出解码后的文本,因此 `<b>` 这类转义标记会变回元素,`&` 变回单独的 `&`。现在这段文本用折叠路径的同一函数转义。解析器还会丢掉紧跟 `<pre>` 或 `<textarea>` 的换行,因此以换行开头的首段文本(其前被 minifier 略去的注释不计)会在前面多写一个换行,开头的空行在第二次解析后得以保留。
+## Upstream synchronization (2026-10-20)
-## Explicit measured-list end requests (2026-10-18)
+Base and Shell now track official `main` revision `77ff6f794dca3bcee2e90977cedf12a64aed17e4`, after the `v0.8.0` tag. Both crates and all remaining Kit dependencies use 0.8.0 from that same Git revision. The bounded inventory contains 406 original files and retains the component-to-Base window-border mapping. Fast remains at 0.1.10 and support snapshots at 0.3.9. Shell's new Kit dependency keeps default features off and resolves to the single adapted Base through the Git source patch; Ma [...]
-The shell exposes `list.scroll_to_end(requestKey)` for an explicit local action such as an accepted message. A new non-null string scrolls once after applying the current item count; a stable key does not undo a later user scroll. Tail mode resumes following, while Normal mode only scrolls once. The request reuses the retained native list state and clears pending native scrolling before moving to the end. Native tests check row identity and in-row pixel offsets, including a final row tal [...]
+The four list/submit backports and HTML minifier fix now use upstream implementations, including the newer nested `<pre><code>` whitespace preservation. Full-cycle focus trapping, overscroll, input viewport fixes, source copying across styles and external-editor rendering are imported. Maka retains original input provenance, post-dispatch Tab defaults, exact IME slots, local themes, selection groups, retained hitbox geometry, context-menu custody, native cleanup and scoped Host ownership [...]
-shell 通过 `list.scroll_to_end(requestKey)` 表达消息受理等本地动作的滚动意图。新的非空字符串在应用当前条目数后滚动一次;相同键不会撤销用户之后的上滚。Tail 模式恢复跟随,Normal 模式只滚动一次。请求复用原生列表状态,先清除待执行的原生滚动。原生测试检查行身份和行内像素位置,包含末行高于视口的情况。导入的 WebSocket 请求头测试先准备脚本运行时,再开始原有的有界服务端等待,保留全部真实请求头断言。
+Immediate copying uses the upstream current snapshot and full-update revision guard. Its immutable copy projections belong to the existing per-hitbox contribution, including native retained translation and pruning; no parallel frame or copy registry is added. External-editor links use an explicit rendering source and ordinary native fragmented click targets, while TextView retains its original occurrence and admission owner.
+
+Base 与 Shell 更新到官方 `main` 固定版本 `77ff6f794dca3bcee2e90977cedf12a64aed17e4`,位于 `v0.8.0` 标签之后。两者及其余 Kit 依赖统一为同一 Git revision 的 0.8.0;有限来源清单包含 406 个原始文件,并保留 Component 到 Base 的 window-border 映射。Fast 保持 0.1.10,支持快照保持 0.3.9。Shell 新增的 Kit 依赖关闭默认 features,并通过 Git source patch 共用唯一适配 Base;Maka 继续注册自己的组件目录。QuickJS、兼容层及 LLRT 的 VM 固定版本不变。
+
+四项列表/提交回移及 HTML minifier 修复现采用上游实现,包含嵌套 `<pre><code>` 空白保留的新修复。引入完整一圈的 focus trap、overscroll、输入视口修复、跨样式源码复制及外部编辑器呈现。Maka 原输入来源、分发后的 Tab 默认行为、准确 IME 槽位、局部主题、选区分组、保留帧 hitbox 几何、菜单 custody、原生清理和 scoped Host owner 均保留。脚本占位文字更新及 measured-list 末尾请求仍使用原实现,因为新版上游 API 并不提供等价行为。
+
+即时复制采用上游当前 snapshot 与 full-update revision 检查;不可变复制投影附着既有的逐 hitbox contribution,共用原生保留帧平移和 pruning,不新增并行 frame 或复制 registry。外部编辑器链接使用明确的呈现来源与普通原生分片点击 target;TextView 保留其原 occurrence 和准入 owner。
## Scrollable with a maximum size (2026-10-20)
diff --git a/third-party/gpui-kit/crates/base/Cargo.toml b/third-party/gpui-kit/crates/base/Cargo.toml
index cb8d2871ed..2eab19d47d 100644
--- a/third-party/gpui-kit/crates/base/Cargo.toml
+++ b/third-party/gpui-kit/crates/base/Cargo.toml
@@ -8,7 +8,7 @@ exclude = ["examples/"]
repository = "https://github.com/longbridge/gpui-kit"
homepage = "https://gpui-kit.com"
documentation = "https://docs.rs/gpui-base"
-version = "0.7.3"
+version = "0.8.0"
publish = false
edition = "2024"
autoexamples = false
@@ -46,7 +46,7 @@ version = "=0.3.9"
[dependencies.gpui-fast]
package = "gpui-fast"
-version = "0.1.7"
+version = "=0.1.10"
features = ["windows-manifest-provided-by-host"]
optional = true
@@ -152,7 +152,7 @@ features = ["test-support"]
[dev-dependencies.gpui-fast]
package = "gpui-fast"
-version = "0.1.7"
+version = "=0.1.10"
features = ["windows-manifest-provided-by-host", "test-support"]
[dev-dependencies.gpui_platform]
@@ -162,7 +162,7 @@ features = ["font-kit", "x11", "wayland", "runtime_shaders"]
[dev-dependencies.gpui-fast-platform]
package = "gpui-fast-platform"
-version = "0.1.7"
+version = "=0.1.10"
features = ["font-kit", "x11", "wayland", "runtime_shaders"]
[dev-dependencies.reqwest_client]
diff --git a/third-party/gpui-kit/crates/base/src/a11y_disabled.rs b/third-party/gpui-kit/crates/base/src/a11y_disabled.rs
new file mode 100644
index 0000000000..061e2004b5
--- /dev/null
+++ b/third-party/gpui-kit/crates/base/src/a11y_disabled.rs
@@ -0,0 +1,304 @@
+//! Disabled semantics on a control's accessibility node.
+//!
+//! GPUI has no `aria_disabled` setter, so a disabled control would reach
+//! assistive technology as one that accepts input. [`A11yDisabledExt`] wraps the
+//! control's root element without adding a layout node, a focus stop or an
+//! accessibility node, and only marks the node it writes disabled.
+use gpui::{
+ App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, InteractiveElement,
+ Interactivity, IntoElement, LayoutId, Pixels, Window, accesskit,
+};
+
+/// A control's root element whose accessibility node may be marked disabled.
+pub(crate) struct A11yDisabled<E> {
+ inner: E,
+ disabled: bool,
+}
+
+impl<E> A11yDisabled<E> {
+ pub(crate) fn new(inner: E, disabled: bool) -> Self {
+ Self { inner, disabled }
+ }
+}
+
+/// Marks a control's accessibility node disabled while `disabled` is true.
+///
+/// Call it at the end of the control's builder chain. With `test-support`, the
+/// wrapper goes inside the observation, so its snapshot reports the flag too.
+pub(crate) trait A11yDisabledExt: Sized {
+ type Output: IntoElement;
+
+ fn a11y_disabled(self, disabled: bool) -> Self::Output;
+}
+
+#[cfg(not(feature = "test-support"))]
+impl<E: Element + InteractiveElement> A11yDisabledExt for E {
+ type Output = A11yDisabled<E>;
+
+ fn a11y_disabled(self, disabled: bool) -> Self::Output {
+ A11yDisabled::new(self, disabled)
+ }
+}
+
+#[cfg(feature = "test-support")]
+impl<E: Element + InteractiveElement> A11yDisabledExt for gpui::Stateful<E> {
+ type Output = A11yDisabled<Self>;
+
+ fn a11y_disabled(self, disabled: bool) -> Self::Output {
+ A11yDisabled::new(self, disabled)
+ }
+}
+
+#[cfg(feature = "test-support")]
+impl<E: Element<PrepaintState = Option<gpui::Hitbox>> + InteractiveElement> A11yDisabledExt
+ for crate::test_support::Observed<E>
+{
+ type Output = crate::test_support::Observed<A11yDisabled<E>>;
+
+ fn a11y_disabled(self, disabled: bool) -> Self::Output {
+ self.map_inner(|inner| A11yDisabled::new(inner, disabled))
+ }
+}
+
+impl<E: Element> IntoElement for A11yDisabled<E> {
+ type Element = Self;
+
+ fn into_element(self) -> Self {
+ self
+ }
+}
+
+impl<E: InteractiveElement> InteractiveElement for A11yDisabled<E> {
+ fn interactivity(&mut self) -> &mut Interactivity {
+ self.inner.interactivity()
+ }
+}
+
+impl<E: Element> Element for A11yDisabled<E> {
+ type RequestLayoutState = E::RequestLayoutState;
+ type PrepaintState = E::PrepaintState;
+
+ fn id(&self) -> Option<ElementId> {
+ self.inner.id()
+ }
+
+ fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
+ self.inner.source_location()
+ }
+
+ fn request_layout(
+ &mut self,
+ id: Option<&GlobalElementId>,
+ inspector: Option<&InspectorElementId>,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> (LayoutId, Self::RequestLayoutState) {
+ self.inner.request_layout(id, inspector, window, cx)
+ }
+
+ fn prepaint(
+ &mut self,
+ id: Option<&GlobalElementId>,
+ inspector: Option<&InspectorElementId>,
+ bounds: Bounds<Pixels>,
+ layout: &mut Self::RequestLayoutState,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Self::PrepaintState {
+ self.inner
+ .prepaint(id, inspector, bounds, layout, window, cx)
+ }
+
+ fn paint(
+ &mut self,
+ id: Option<&GlobalElementId>,
+ inspector: Option<&InspectorElementId>,
+ bounds: Bounds<Pixels>,
+ layout: &mut Self::RequestLayoutState,
+ prepaint: &mut Self::PrepaintState,
+ window: &mut Window,
+ cx: &mut App,
+ ) {
+ self.inner
+ .paint(id, inspector, bounds, layout, prepaint, window, cx)
+ }
+
+ fn a11y_role(&self) -> Option<gpui::Role> {
+ self.inner.a11y_role()
+ }
+
+ fn write_a11y_info(&self, node: &mut accesskit::Node) {
+ self.inner.write_a11y_info(node);
+ if self.disabled {
+ node.set_disabled();
+ }
+ }
+
+ fn a11y_synthetic_children(
+ &mut self,
+ prepaint: &mut Self::PrepaintState,
+ builder: &mut gpui::A11ySubtreeBuilder,
+ ) {
+ self.inner.a11y_synthetic_children(prepaint, builder);
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::sync::{Arc, Mutex};
+
+ use gpui::{AppContext as _, Context, IntoElement, Render, RenderOnce, TestAppContext, canvas};
+
+ use super::*;
+ use crate::{
+ Button, Checkbox, DatePicker, Link, Radio, Select, Slider, Switch, Tab, TimeField,
+ TimeFieldState, Toggle, slider::SliderState,
+ };
+
+ /// The accessibility node a rendered control writes.
+ fn node_of(element: impl IntoElement) -> accesskit::Node {
+ let element = element.into_element();
+ let mut node = accesskit::Node::new(element.a11y_role().unwrap_or(gpui::Role::Unknown));
+ element.write_a11y_info(&mut node);
+ node
+ }
+
+ type Captured = Arc<Mutex<Vec<(&'static str, bool, accesskit::Node)>>>;
+
+ struct Probe {
+ captured: Captured,
+ }
+
+ impl Render for Probe {
+ fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+ let captured = self.captured.clone();
+ canvas(
+ move |_, window, cx| {
+ let slider = cx.new(|_| SliderState::new());
+ let time = cx.new(|cx| TimeFieldState::new(window, cx));
+ let focus = cx.focus_handle();
+ let mut nodes = captured.lock().unwrap();
+ nodes.clear();
+ for disabled in [false, true] {
+ nodes.push((
+ "button",
+ disabled,
+ node_of(Button::new("button").disabled(disabled).render(window, cx)),
+ ));
+ nodes.push((
+ "link",
+ disabled,
+ node_of(Link::new("link").disabled(disabled).render(window, cx)),
+ ));
+ nodes.push((
+ "switch",
+ disabled,
+ node_of(Switch::new("switch").disabled(disabled).render(window, cx)),
+ ));
+ nodes.push((
+ "radio",
+ disabled,
+ node_of(Radio::new("radio").disabled(disabled).render(window, cx)),
+ ));
+ nodes.push((
+ "toggle",
+ disabled,
+ node_of(Toggle::new("toggle").disabled(disabled).render(window, cx)),
+ ));
+ nodes.push((
+ "checkbox",
+ disabled,
+ node_of(
+ Checkbox::new("checkbox")
+ .disabled(disabled)
+ .render(window, cx),
+ ),
+ ));
+ nodes.push((
+ "select",
+ disabled,
+ node_of(Select::new("select").disabled(disabled).render(window, cx)),
+ ));
+ nodes.push((
+ "tab",
+ disabled,
+ node_of(Tab::new("tab").disabled(disabled).render(window, cx)),
+ ));
+ nodes.push((
+ "date_picker",
+ disabled,
+ node_of(
+ DatePicker::new("date-picker", &focus)
+ .disabled(disabled)
+ .render(window, cx),
+ ),
+ ));
+ nodes.push((
+ "time_field",
+ disabled,
+ node_of(
+ TimeField::new("time-field", &time)
+ .disabled(disabled)
+ .render(window, cx),
+ ),
+ ));
+ nodes.push((
+ "slider",
+ disabled,
+ node_of(Slider::new(&slider).disabled(disabled).render(window, cx)),
+ ));
+ }
+ },
+ |_, _, _, _| {},
+ )
+ }
+ }
+
+ #[gpui::test]
+ fn a_disabled_control_marks_its_node_disabled(cx: &mut TestAppContext) {
+ let captured: Captured = Arc::default();
+ let (_, cx) = cx.add_window_view({
+ let captured = captured.clone();
+ move |_, _| Probe { captured }
+ });
+ cx.update(|window, cx| window.draw(cx).clear(cx));
+
+ let nodes = captured.lock().unwrap();
+ assert_eq!(nodes.len(), 22);
+ for (name, disabled, node) in nodes.iter() {
+ assert_ne!(node.role(), gpui::Role::Unknown, "{name} wrote no node");
+ assert_eq!(node.is_disabled(), *disabled, "{name}, disabled {disabled}");
+ if *name == "slider" {
+ // Nor does assistive technology move a disabled slider.
+ assert_eq!(
+ node.supports_action(accesskit::Action::Increment),
+ !disabled,
+ "slider, disabled {disabled}"
+ );
+ }
+ }
+ }
+
+ struct Observed {
+ disabled: bool,
+ }
+
+ impl Render for Observed {
+ fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+ Switch::new("switch").disabled(self.disabled)
+ }
+ }
+
+ #[gpui::test]
+ fn the_test_support_snapshot_sees_the_flag(cx: &mut TestAppContext) {
+ for disabled in [false, true] {
+ let (_, cx) = cx.add_window_view(move |_, _| Observed { disabled });
+ cx.update(|window, cx| {
+ window.draw(cx).clear(cx);
+ let snapshot = crate::test_support::find(window, &[], &"switch".into())
+ .expect("switch is observed");
+ assert_eq!(snapshot.disabled(), disabled.then_some(true));
+ });
+ }
+ }
+}
diff --git a/third-party/gpui-kit/crates/base/src/button.rs b/third-party/gpui-kit/crates/base/src/button.rs
index 84a870c454..972cb3d17c 100644
--- a/third-party/gpui-kit/crates/base/src/button.rs
+++ b/third-party/gpui-kit/crates/base/src/button.rs
@@ -8,7 +8,10 @@ use gpui::{
};
use smallvec::SmallVec;
-use crate::{RoleOverride, Selectable, StateStyle, StyledExt as _, TestSupportExt as _};
+use crate::{
+ RoleOverride, Selectable, StateStyle, StyledExt as _, TestSupportExt as _,
+ a11y_disabled::A11yDisabledExt as _,
+};
type ClickHandler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>;
@@ -254,6 +257,7 @@ impl RenderOnce for Button {
)
.children(self.children)
.refine_style(&style)
+ .a11y_disabled(disabled)
}
}
@@ -547,10 +551,7 @@ mod tests {
assert_eq!(disabled.role(), Role::Button);
assert!(!disabled.supports_action(accesskit::Action::Click));
-
- // GPUI's current StatefulInteractiveElement interface has no
- // aria-disabled setter even though AccessKit can represent it. This
- // assertion records that upstream gap instead of claiming support.
- assert!(!disabled.is_disabled());
+ assert!(!enabled.is_disabled());
+ assert!(disabled.is_disabled());
}
}
diff --git a/third-party/gpui-kit/crates/base/src/checkbox.rs b/third-party/gpui-kit/crates/base/src/checkbox.rs
index 670f22516f..6e29b6193c 100644
--- a/third-party/gpui-kit/crates/base/src/checkbox.rs
+++ b/third-party/gpui-kit/crates/base/src/checkbox.rs
@@ -8,7 +8,10 @@ use gpui::{
};
use smallvec::SmallVec;
-use crate::{RoleOverride, StateStyle, StyledExt as _, TestSupportExt as _};
+use crate::{
+ RoleOverride, StateStyle, StyledExt as _, TestSupportExt as _,
+ a11y_disabled::A11yDisabledExt as _,
+};
type ChangeHandler = Rc<dyn Fn(CheckboxState, &ClickEvent, &mut Window, &mut App)>;
@@ -126,6 +129,9 @@ impl Checkbox {
}
/// Sets the label exposed to accessibility clients.
+ ///
+ /// A checkbox without text children has no accessible name, so set this
+ /// whenever it is drawn without visible text.
pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
self.accessibility_label = Some(label.into());
self
@@ -395,6 +401,7 @@ impl RenderOnce for Checkbox {
)
.children(self.children)
.refine_style(&style)
+ .a11y_disabled(disabled)
}
}
diff --git a/third-party/gpui-kit/crates/base/src/date_picker.rs b/third-party/gpui-kit/crates/base/src/date_picker.rs
index 9b2433b91a..fb87e8c268 100644
--- a/third-party/gpui-kit/crates/base/src/date_picker.rs
+++ b/third-party/gpui-kit/crates/base/src/date_picker.rs
@@ -9,6 +9,7 @@ use gpui::{
use crate::{
StyledExt as _,
+ a11y_disabled::A11yDisabledExt as _,
actions::{Cancel, Confirm},
};
@@ -108,5 +109,6 @@ impl RenderOnce for DatePicker {
})
.refine_style(&self.style)
.children(self.children)
+ .a11y_disabled(disabled)
}
}
diff --git a/third-party/gpui-kit/crates/base/src/dock/fixtures/legacy_empty_stack_panel.json b/third-party/gpui-kit/crates/base/src/dock/fixtures/legacy_empty_stack_panel.json
new file mode 100644
index 0000000000..7d5e24ca3e
--- /dev/null
+++ b/third-party/gpui-kit/crates/base/src/dock/fixtures/legacy_empty_stack_panel.json
@@ -0,0 +1,3 @@
+{
+ "center": { "panel_name": "StackPanel", "children": [], "info": { "panel": null } }
+}
diff --git a/third-party/gpui-kit/crates/base/src/dock/fixtures/placeholder_wrapped_empty_stack.json b/third-party/gpui-kit/crates/base/src/dock/fixtures/placeholder_wrapped_empty_stack.json
new file mode 100644
index 0000000000..204397a3aa
--- /dev/null
+++ b/third-party/gpui-kit/crates/base/src/dock/fixtures/placeholder_wrapped_empty_stack.json
@@ -0,0 +1,16 @@
+{
+ "center": {
+ "panel_name": "StackPanel",
+ "children": [
+ {
+ "panel_name": "TabPanel",
+ "children": [
+ { "panel_name": "StackPanel", "children": [], "info": { "panel": null } },
+ { "panel_name": "Alpha", "children": [], "info": { "panel": null } }
+ ],
+ "info": { "tabs": { "active_index": 0 } }
+ }
+ ],
+ "info": { "stack": { "sizes": [0.0], "axis": 0 } }
+ }
+}
diff --git a/third-party/gpui-kit/crates/base/src/dock/state_convert.rs b/third-party/gpui-kit/crates/base/src/dock/state_convert.rs
index 6857ed6b06..9b105b813c 100644
--- a/third-party/gpui-kit/crates/base/src/dock/state_convert.rs
+++ b/third-party/gpui-kit/crates/base/src/dock/state_convert.rs
@@ -88,7 +88,11 @@ impl PaneTree {
/// old reader had no such rule: it looked "TabPanel" up in the panel
/// registry, found nothing, and rendered an `InvalidPanel` placeholder
/// where an empty tab group belonged. This rule is a genuine fix, not a
- /// preserved behavior.
+ /// preserved behavior;
+ /// - a node named `StackPanel` carrying `PanelInfo::Panel` is the same
+ /// defect in the old split writer, and is read as an empty split. Inside
+ /// a tab group it is dropped: a placeholder built from it would dump the
+ /// defective state back unchanged, so the layout could never heal.
pub fn from_state(
state: &PanelState,
root_kind: RootKind,
@@ -155,9 +159,17 @@ fn build_node(tree: &mut PaneTree, state: &PanelState, builder: &mut dyn PanelBu
},
)
}
+ // A container name carrying a leaf info means the writer that produced
+ // this file had the empty-container defect.
+ PanelInfo::Panel(_) if state.panel_name == STACK_PANEL_NAME => PaneNode::new(
+ id,
+ NodeKind::Split {
+ axis: Axis::Horizontal,
+ children: Vec::new(),
+ sizes: Vec::new(),
+ },
+ ),
PanelInfo::Panel(_) => {
- // A container name carrying a leaf info means the writer that
- // produced this file had the empty-group defect.
let panels = if state.panel_name == TAB_PANEL_NAME {
Vec::new()
} else {
@@ -174,13 +186,17 @@ fn build_node(tree: &mut PaneTree, state: &PanelState, builder: &mut dyn PanelBu
}
}
+fn is_container_name(panel_name: &str) -> bool {
+ panel_name == TAB_PANEL_NAME || panel_name == STACK_PANEL_NAME
+}
+
/// Flatten one level of tab nesting, which the old writer could produce.
fn collect_tab_panels(children: &[PanelState], builder: &mut dyn PanelBuilder) -> Vec<PanelId> {
children
.iter()
.flat_map(|child| match &child.info {
PanelInfo::Tabs { .. } => collect_tab_panels(&child.children, builder),
- PanelInfo::Panel(_) if child.panel_name == TAB_PANEL_NAME => Vec::new(),
+ PanelInfo::Panel(_) if is_container_name(&child.panel_name) => Vec::new(),
_ => vec![builder.build(child, &child.info)],
})
.collect()
@@ -424,6 +440,8 @@ mod tests {
include_str!("fixtures/legacy_empty_tab_group.json"),
include_str!("fixtures/unregistered_panel.json"),
include_str!("fixtures/zero_size_sentinel.json"),
+ include_str!("fixtures/legacy_empty_stack_panel.json"),
+ include_str!("fixtures/placeholder_wrapped_empty_stack.json"),
] {
let once = canonicalize(json);
let twice = {
@@ -458,6 +476,39 @@ mod tests {
assert!(state.children.is_empty());
}
+ #[test]
+ fn the_legacy_empty_stack_panel_is_read_as_an_empty_split() {
+ let json = include_str!("fixtures/legacy_empty_stack_panel.json");
+ let state: DockAreaState = serde_json::from_str(json).unwrap();
+ let mut panels = PreservingPanels::default();
+ let tree = PaneTree::from_state(&state.center, RootKind::Split, &mut panels);
+
+ assert!(panels.states.is_empty(), "no placeholder is built for it");
+ let state = tree.to_state(&panels);
+ assert_eq!(state.panel_name, "StackPanel");
+ assert!(matches!(state.info, PanelInfo::Stack { .. }));
+ assert!(state.children.is_empty());
+ }
+
+ /// The shape a reader without the rule above saved back: the placeholder
+ /// it built sat in a tab group and dumped the defective state unchanged.
+ #[test]
+ fn an_empty_stack_panel_saved_inside_a_tab_group_is_dropped() {
+ let state = canonicalize(include_str!(
+ "fixtures/placeholder_wrapped_empty_stack.json"
+ ));
+ let group = &state.children[0];
+
+ assert_eq!(group.panel_name, "TabPanel");
+ assert_eq!(group.info, PanelInfo::tabs(0));
+ let names: Vec<_> = group
+ .children
+ .iter()
+ .map(|child| child.panel_name.as_str())
+ .collect();
+ assert_eq!(names, ["Alpha"]);
+ }
+
#[test]
fn nested_same_axis_splits_are_flattened_and_single_child_splits_collapse() {
let state = canonicalize(include_str!("fixtures/nested_splits.json"));
diff --git a/third-party/gpui-kit/crates/base/src/focus_trap.rs b/third-party/gpui-kit/crates/base/src/focus_trap.rs
index b7270ccd6e..bb311f6764 100644
--- a/third-party/gpui-kit/crates/base/src/focus_trap.rs
+++ b/third-party/gpui-kit/crates/base/src/focus_trap.rs
@@ -82,16 +82,25 @@ impl FocusTrapManager {
/// Find which focus trap contains the currently focused element
fn find_active_trap(window: &Window, cx: &App) -> Option<FocusHandle> {
- for (_id, container_handle) in Self::global(cx).traps.iter() {
+ let mut active: Option<FocusHandle> = None;
+ for container_handle in Self::global(cx).traps.values() {
let Some(container) = container_handle.upgrade() else {
continue;
};
- if container.contains_focused(window, cx) {
- return Some(container.clone());
+ if container.contains_focused(window, cx)
+ && active
+ .as_ref()
+ .is_none_or(|current| current.contains(&container, window))
+ {
+ // A child scope must confine navigation even if its ancestor
+ // was registered first (or visited first by the HashMap).
+ // Use the rendered focus tree so closing and reopening a
+ // retained scope cannot leave registration order in control.
+ active = Some(container);
}
}
- None
+ active
}
/// Cleanup any traps with dropped handles
@@ -242,3 +251,37 @@ impl<E: InteractiveElement + ParentElement + Styled + Element + 'static> Element
pub fn active_focus_trap(window: &Window, cx: &App) -> Option<FocusHandle> {
FocusTrapManager::find_active_trap(window, cx)
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::TestSupportExt as _;
+ use gpui::{Role, TestAppContext, accesskit, div};
+
+ #[gpui::test]
+ fn trap_exports_inner_semantics_through_the_outer_native_hooks(cx: &mut TestAppContext) {
+ cx.update(|cx| {
+ for (role, name) in [
+ (Role::Dialog, "Edit server"),
+ (Role::AlertDialog, "Delete server?"),
+ ] {
+ let trap = div()
+ .id("inner-host")
+ .test_support()
+ .role(role)
+ .aria_label(name)
+ .aria_description("Server settings")
+ .focus_trap("outer-trap", &cx.focus_handle());
+ // GPUI calls hooks on the outer Element. An observed inner
+ // div alone can claim these semantics while the real element
+ // silently drops them. Check the exact native boundary.
+ assert_eq!(Element::id(&trap), Some("outer-trap".into()));
+ assert_eq!(trap.a11y_role(), Some(role));
+ let mut node = accesskit::Node::new(role);
+ trap.write_a11y_info(&mut node);
+ assert_eq!(node.label(), Some(name));
+ assert_eq!(node.description(), Some("Server settings"));
+ }
+ });
+ }
+}
diff --git a/third-party/gpui-kit/crates/base/src/input/base/blink_cursor.rs b/third-party/gpui-kit/crates/base/src/input/base/blink_cursor.rs
index dc36700910..b41f2bb5a3 100644
--- a/third-party/gpui-kit/crates/base/src/input/base/blink_cursor.rs
+++ b/third-party/gpui-kit/crates/base/src/input/base/blink_cursor.rs
@@ -4,11 +4,7 @@ use instant::Duration;
static INTERVAL: Duration = Duration::from_millis(500);
static PAUSE_DELAY: Duration = Duration::from_millis(300);
-// On Windows, Linux, we should use integer to avoid blurry cursor.
-#[cfg(not(target_os = "macos"))]
-pub(super) const CURSOR_WIDTH: Pixels = px(2.);
-#[cfg(target_os = "macos")]
-pub(super) const CURSOR_WIDTH: Pixels = px(1.5);
+pub(super) const CURSOR_WIDTH: Pixels = px(1.);
/// To manage the Input cursor blinking.
///
@@ -34,9 +30,17 @@ impl BlinkCursor {
}
}
- /// Start the blinking
+ /// Start the blinking from a visible cursor.
+ ///
+ /// A single focus can start the cursor more than once: `InputState::focus`
+ /// starts it and then the focus observer starts it again, and window
+ /// activation starts it too. Resuming the running cycle would toggle the
+ /// cursor that was just shown off again, so every start restarts the cycle.
pub(crate) fn start(&mut self, cx: &mut Context<Self>) {
- self.blink(self.epoch, cx);
+ self.paused = false;
+ self.visible = false;
+ let epoch = self.next_epoch();
+ self.blink(epoch, cx);
}
/// Stop the blinking and clear the blink state, so the next [`Self::start`]
@@ -146,6 +150,28 @@ mod tests {
assert!(cursor.read_with(cx, |cursor, _| cursor.visible()));
}
+ #[gpui::test]
+ fn starting_a_blinking_cursor_keeps_it_visible(cx: &mut TestAppContext) {
+ let cursor = cx.new(|_| BlinkCursor::new());
+
+ // Clicking into an input starts the cursor from `focus` and again from
+ // the focus observer; the second start must not hide the new cursor.
+ cursor.update(cx, |cursor, cx| cursor.start(cx));
+ cursor.update(cx, |cursor, cx| cursor.start(cx));
+ cx.run_until_parked();
+ assert!(cursor.read_with(cx, |cursor, _| cursor.visible()));
+
+ // The restart drops the first cycle's timer, so the cursor stays
+ // visible for a full interval before it blinks off.
+ cx.executor()
+ .advance_clock(INTERVAL - Duration::from_millis(1));
+ cx.run_until_parked();
+ assert!(cursor.read_with(cx, |cursor, _| cursor.visible()));
+ cx.executor().advance_clock(Duration::from_millis(1));
+ cx.run_until_parked();
+ assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
+ }
+
#[gpui::test]
fn pausing_a_cursor_that_is_not_blinking_does_not_start_it(cx: &mut TestAppContext) {
// Never focused, so `start` was never called: the epoch is still zero.
diff --git a/third-party/gpui-kit/crates/base/src/input/base/element.rs b/third-party/gpui-kit/crates/base/src/input/base/element.rs
index fdd80a2fa7..c45c583b21 100644
--- a/third-party/gpui-kit/crates/base/src/input/base/element.rs
+++ b/third-party/gpui-kit/crates/base/src/input/base/element.rs
@@ -118,6 +118,8 @@ fn compose_decoration_collections<'a>(
struct EditorScrollbarLayout {
bounds: Bounds<Pixels>,
scroll_size: Size<Pixels>,
+ // Excludes editor padding from scroll range while the track still includes it.
+ scroll_viewport_size: Size<Pixels>,
}
/// What the unwrapped width of the longest line depends on. While it holds,
@@ -174,21 +176,24 @@ impl EditorScrollbarLayout {
paddings.left + line_number_width - LINE_NUMBER_RIGHT_MARGIN
};
- Self {
- bounds: Bounds::new(
- point(
- input_bounds.origin.x + left,
- input_bounds.origin.y - paddings.top,
- ),
- size(
- input_bounds.size.width - left + paddings.right,
- input_bounds.size.height + paddings.top + paddings.bottom,
- ),
+ let bounds = Bounds::new(
+ point(
+ input_bounds.origin.x + left,
+ input_bounds.origin.y - paddings.top,
),
+ size(
+ input_bounds.size.width - left + paddings.right,
+ input_bounds.size.height + paddings.top + paddings.bottom,
+ ),
+ );
+
+ Self {
+ bounds,
scroll_size: size(
scroll_size.width - left + paddings.right + RIGHT_MARGIN,
scroll_size.height,
),
+ scroll_viewport_size: size(bounds.size.width, input_bounds.size.height),
}
}
}
@@ -263,6 +268,7 @@ impl<M: InputModeKind> Element for EditorScrollbar<M> {
Scrollbar::vertical(&scroll_handle)
}
.viewport_bounds(snapshot.layout.bounds)
+ .scroll_viewport_size(snapshot.layout.scroll_viewport_size)
.scroll_size(snapshot.layout.scroll_size);
if let Some(theme) = &state.scrollbar_theme {
scrollbar = scrollbar.theme(theme.clone());
@@ -406,6 +412,24 @@ pub(super) fn cursor_surrounding_padding(
raw.min(viewport_half)
}
+/// Reveal the whole visual row without changing the usual edge clearance.
+/// Reserve the row's height below its insertion position before allocating
+/// top padding. A viewport shorter than a row can only align its start.
+pub(super) fn reveal_row(
+ offset: Pixels,
+ row_top: Pixels,
+ line_height: Pixels,
+ viewport_height: Pixels,
+ edge_padding: Pixels,
+) -> Pixels {
+ let row_height = line_height.min(viewport_height);
+ let bottom = edge_padding.max(row_height).min(viewport_height);
+ let top = edge_padding.min(viewport_height - bottom);
+ offset
+ .clamp(top - row_top, viewport_height - bottom - row_top)
+ .min(px(0.))
+}
+
/// Pixel height of the empty area below the last line in the editor's
/// scrollable region. Backs [`InputBaseState::scroll_beyond_last_line`].
///
@@ -455,6 +479,23 @@ impl<M: InputModeKind> TextElement<M> {
}
fn paint_mouse_listeners(&mut self, hitbox: &Hitbox, window: &mut Window, _: &mut App) {
+ // Text with room to scroll keeps the wheel from the scroll areas
+ // around it; its own listener scrolls it in the bubble phase.
+ window.on_mouse_event({
+ let state = self.state.clone();
+ let hitbox = hitbox.id;
+ move |event: &gpui::ScrollWheelEvent, phase, window, cx| {
+ if phase.capture()
+ && hitbox.should_handle_scroll(window)
+ && state
+ .read(cx)
+ .takes_scroll(event.delta.pixel_delta(window.line_height()))
+ {
+ crate::scroll_bounce::claim_scroll(hitbox, cx);
+ }
+ }
+ });
+
// Every touch is offered as a drag first; that is how a tap's mouse
// events are later told apart from a mouse's.
window.on_mouse_event(move |event: &TouchDragEvent, phase, _, cx| {
@@ -526,9 +567,9 @@ impl<M: InputModeKind> TextElement<M> {
last_layout: &LastLayout,
bounds: &mut Bounds<Pixels>,
scroll_size: Size<Pixels>,
- _: &mut Window,
+ window: &mut Window,
cx: &mut App,
- ) -> (Vec<CursorRenderInfo>, Point<Pixels>, Option<usize>) {
+ ) -> (Vec<CursorRenderInfo>, Point<Pixels>, Option<usize>, bool) {
let state = self.state.read(cx);
let line_height = last_layout.line_height;
@@ -539,6 +580,7 @@ impl<M: InputModeKind> TextElement<M> {
let active_id = state.active_selection().id;
let mut scroll_offset = state.scroll_handle.offset();
let mut current_row = None;
+ let mut resize_reveal_owned = false;
let mut cursor_infos: Vec<CursorRenderInfo> = Vec::with_capacity(state.selections.len());
// Padding kept between the cursor and the viewport's top/bottom
@@ -550,6 +592,35 @@ impl<M: InputModeKind> TextElement<M> {
line_height,
);
+ // Keep a visible, focused caret visible when its viewport reflows, but
+ // do not undo deliberate scrolling away from an unchanged selection.
+ let resized = state.last_bounds.is_some_and(|old| old.size != bounds.size)
+ || state
+ .last_layout
+ .as_ref()
+ .is_some_and(|old| old.line_height != line_height);
+ let reveal_on_resize = resized
+ && state.deferred_scroll_offset.is_none()
+ && state.focus_handle.is_focused(window)
+ && state.prepaint_caret_geometry.is_some_and(|old| {
+ // An unshaped caret has provisional x until the notified
+ // repaint. Consecutive resize frames retain ownership only
+ // while no intervening scroll changed the viewport.
+ if old.resize_reveal_owned {
+ return old.scroll_offset() == scroll_offset;
+ }
+ let caret = old.cursor_bounds();
+ let viewport = old.input_bounds();
+ let top = caret.top() + scroll_offset.y;
+ // Cached x already contains its painted scroll; account only
+ // for a wheel offset change since that geometry was produced.
+ let left = caret.left() + scroll_offset.x - old.scroll_offset().x;
+ top >= viewport.top()
+ && top + caret.size.height <= viewport.bottom()
+ && left >= viewport.left()
+ && left + caret.size.width <= viewport.right()
+ });
+
// Resolve a cursor or selection endpoint to a content-space position.
let visible_buffer_lines = &last_layout.visible_buffer_lines;
let caret_for = |row: usize, offset: usize, affinity: bool| -> Point<Pixels> {
@@ -565,7 +636,13 @@ impl<M: InputModeKind> TextElement<M> {
return line_origin + pos;
}
}
- line_origin
+ if state.masked {
+ return line_origin;
+ }
+ point(
+ px(0.),
+ line_height * state.display_map.offset_to_display_row(offset, affinity),
+ )
};
let cursor_height = 0.85 * line_height;
@@ -612,50 +689,43 @@ impl<M: InputModeKind> TextElement<M> {
let selection_changed = state.last_selected_range != Some(selected_range);
let auto_scrolling = state.auto_scroll.is_active();
- if selection_changed && !is_selected_all {
- // For Right alignment use 0 margin: cursor is clamped to bounds separately,
- // so we never scroll the text for cursor-at-edge, avoiding a first-click jump.
- let safety_margin = match last_layout.text_align {
- TextAlign::Left => RIGHT_MARGIN,
- TextAlign::Right => px(0.),
- TextAlign::Center => CURSOR_WIDTH,
- };
-
- scroll_offset.x = if scroll_offset.x + cursor_pos.x
- > (bounds.size.width - line_number_width - safety_margin)
- {
- // cursor is out of right
- bounds.size.width - line_number_width - safety_margin - cursor_pos.x
- } else if scroll_offset.x + cursor_pos.x < px(0.) {
- // cursor is out of left
- -cursor_pos.x
- } else {
- scroll_offset.x
- };
+ if (selection_changed || reveal_on_resize) && !is_selected_all {
+ // Resize-follow owns only vertical visibility. An unshaped
+ // target has no reliable x; retain the existing x offset.
+ if selection_changed {
+ // Right-aligned carets are clamped instead of scrolling.
+ let safety_margin = match last_layout.text_align {
+ TextAlign::Left => RIGHT_MARGIN,
+ TextAlign::Right => px(0.),
+ TextAlign::Center => CURSOR_WIDTH,
+ };
+ scroll_offset.x = if scroll_offset.x + cursor_pos.x
+ > (bounds.size.width - line_number_width - safety_margin)
+ {
+ bounds.size.width - line_number_width - safety_margin - cursor_pos.x
+ } else if scroll_offset.x + cursor_pos.x < px(0.) {
+ -cursor_pos.x
+ } else {
+ scroll_offset.x
+ };
+ }
// Vertical cursor-follow is suppressed while auto-scroll manages the y axis,
// to prevent fighting the background scroll task.
if !auto_scrolling {
- // Scroll straight to the caret's line. This runs only on
- // the frame the selection changed, so a one-line step
- // would leave a far-off caret (e.g. after typing at the
- // end of a long paste) outside the viewport.
- scroll_offset.y = if scroll_offset.y + cursor_pos.y
- > bounds.size.height - top_bottom_margin
- {
- // cursor is out of bottom
- bounds.size.height - top_bottom_margin - cursor_pos.y
- } else if scroll_offset.y + cursor_pos.y < top_bottom_margin {
- // cursor is out of top
- (top_bottom_margin - cursor_pos.y).min(px(0.))
- } else {
- scroll_offset.y
- };
+ scroll_offset.y = reveal_row(
+ scroll_offset.y,
+ cursor_pos.y,
+ line_height,
+ bounds.size.height,
+ top_bottom_margin,
+ );
+ resize_reveal_owned = reveal_on_resize;
}
// For selection to move scroll
if selection.reversed {
- if scroll_offset.x + cursor_start.x < px(0.) {
+ if selection_changed && scroll_offset.x + cursor_start.x < px(0.) {
// selection start is out of left
scroll_offset.x = -cursor_start.x;
}
@@ -666,7 +736,7 @@ impl<M: InputModeKind> TextElement<M> {
} else {
// TODO: Consider to remove this part,
// maybe is not necessary (But selection_reversed is needed).
- if scroll_offset.x + cursor_end.x <= px(0.) {
+ if selection_changed && scroll_offset.x + cursor_end.x <= px(0.) {
// selection end is out of left
scroll_offset.x = -cursor_end.x;
}
@@ -714,7 +784,12 @@ impl<M: InputModeKind> TextElement<M> {
bounds.origin = bounds.origin + scroll_offset;
- (cursor_infos, scroll_offset, current_row)
+ (
+ cursor_infos,
+ scroll_offset,
+ current_row,
+ resize_reveal_owned,
+ )
}
/// Layout the match range to a Path.
@@ -2327,6 +2402,17 @@ impl PrepaintState {
}
}
+/// Align a caret to whole device pixels.
+///
+/// Text positions are fractional, and a one-pixel caret at a fractional x is
+/// spread over two device pixels by antialiasing, which draws it faint and
+/// thin. Rounding its edges to the device grid keeps every caret crisp.
+fn snap_caret_to_device_pixels(bounds: Bounds<Pixels>, scale_factor: f32) -> Bounds<Pixels> {
+ let x = (bounds.origin.x * scale_factor).round() / scale_factor;
+ let width = (bounds.size.width * scale_factor).round().max(px(1.)) / scale_factor;
+ Bounds::new(point(x, bounds.origin.y), size(width, bounds.size.height))
+}
+
impl<M: InputModeKind> IntoElement for TextElement<M> {
type Element = Self;
@@ -2522,7 +2608,10 @@ impl<M: InputModeKind> Element for TextElement<M> {
window.text_system().layout_width(font_id, text_size, ' ')
};
- self.state.update(cx, |state, cx| {
+ // Text stretched past an end is drawn that far from where it scrolled
+ // to, inside the frame that clips it, and springs back over the next
+ // frames. Scrolling and everything measured from it stay exact.
+ let mut stretch = self.state.update(cx, |state, cx| {
if state
.selecting
.as_ref()
@@ -2532,6 +2621,12 @@ impl<M: InputModeKind> Element for TextElement<M> {
}
state.display_map.set_font(font, text_size, cx);
state.display_map.ensure_text_prepared(&state.text, cx);
+ if cx.reduce_motion() {
+ state.overscroll.reset();
+ } else if state.overscroll.step(cx.background_executor().now()) {
+ window.request_animation_frame();
+ }
+ state.overscroll.offset()
});
let state = self.state.read(cx);
@@ -2857,8 +2952,18 @@ impl<M: InputModeKind> Element for TextElement<M> {
// regardless of horizontal scroll position.
let input_bounds = bounds;
let original_x = bounds.origin.x;
+ // Resolved content and viewport sizes can change during a gesture.
+ // A non-scrolling input must not retain a drag that will no longer
+ // receive its Ended packet through the overscroll path.
+ if scroll_size.height <= input_bounds.size.height {
+ self.state.update(cx, |state, _| state.overscroll.reset());
+ stretch = 0.;
+ }
+ // Keep viewport geometry for scrollbars and hitboxes fixed; only
+ // text, its carets, selections, and gutter contents are displaced.
+ bounds.origin.y += px(stretch);
- let (cursor_infos, cursor_scroll_offset, current_row) =
+ let (cursor_infos, cursor_scroll_offset, current_row, resize_reveal_owned) =
self.layout_cursors(&last_layout, &mut bounds, scroll_size, window, cx);
// Completion/code-action menus position at the active caret.
last_layout.cursor_bounds = cursor_infos
@@ -2888,7 +2993,10 @@ impl<M: InputModeKind> Element for TextElement<M> {
input_bounds.size.width,
last_layout.text_align,
);
- PrepaintCaretGeometry::new(cursor_bounds, line_height, input_bounds, scroll_offset)
+ let mut geometry =
+ PrepaintCaretGeometry::new(cursor_bounds, line_height, input_bounds, scroll_offset);
+ geometry.resize_reveal_owned = resize_reveal_owned;
+ geometry
});
self.state.update(cx, |state, _| {
state.prepaint_caret_geometry = caret_geometry;
@@ -3251,7 +3359,10 @@ impl<M: InputModeKind> Element for TextElement<M> {
// Paint blinking cursors (shared blink state for all carets)
if focused && show_cursor {
for cursor_info in prepaint.cursor_infos_with_scroll() {
- window.paint_quad(fill(cursor_info.bounds, editor_style.caret));
+ window.paint_quad(fill(
+ snap_caret_to_device_pixels(cursor_info.bounds, window.scale_factor()),
+ editor_style.caret,
+ ));
}
}
@@ -3665,12 +3776,115 @@ mod tests {
struct TextareaHarness(Entity<crate::input::TextareaState>);
+ #[gpui::test]
+ fn scroll_bounce_keeps_the_scrollbar_viewport_fixed(cx: &mut TestAppContext) {
+ let text = (0..60)
+ .map(|line| format!("line {line}"))
+ .collect::<Vec<_>>()
+ .join("\n");
+ let (editor, window) = decoration_editor(cx, &text, false);
+ let mut cx = VisualTestContext::from_window(window.into(), cx);
+ cx.update(|window, cx| {
+ window.draw(cx).clear(cx);
+ let viewport = editor
+ .read(cx)
+ .editor_scrollbar_snapshot
+ .get()
+ .unwrap()
+ .layout
+ .bounds;
+ let text_origin = editor.read(cx).last_bounds.unwrap().origin;
+ editor.update(cx, |state, cx| {
+ state.set_scroll_bounce(true, cx);
+ state.on_scroll_wheel(
+ &gpui::ScrollWheelEvent {
+ delta: gpui::ScrollDelta::Pixels(point(px(0.), px(60.))),
+ touch_phase: gpui::TouchPhase::Started,
+ ..Default::default()
+ },
+ window,
+ cx,
+ );
+ });
+ window.draw(cx).clear(cx);
+ let state = editor.read(cx);
+ let stretch = state.overscroll.offset();
+ assert!(stretch > 0.);
+ assert_eq!(
+ state.editor_scrollbar_snapshot.get().unwrap().layout.bounds,
+ viewport
+ );
+ assert_eq!(
+ state.last_bounds.unwrap().origin.y,
+ text_origin.y + px(stretch)
+ );
+ });
+ }
+
impl Render for TextareaHarness {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div().size_full().child(self.0.clone())
}
}
+ /// A textarea 100 px down a taller bouncing scroll area.
+ struct NestedTextarea(Entity<crate::input::TextareaState>, gpui::ScrollHandle);
+ use gpui::StatefulInteractiveElement as _;
+
+ impl Render for NestedTextarea {
+ fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+ crate::ScrollBounce::new(
+ "outer",
+ &self.1,
+ div()
+ .id("outer-viewport")
+ .w(px(240.))
+ .h(px(300.))
+ .overflow_y_scroll()
+ .track_scroll(&self.1)
+ .child(div().h(px(100.)))
+ .child(div().h(px(120.)).child(self.0.clone()))
+ .child(div().h(px(600.))),
+ )
+ .enabled(true)
+ }
+ }
+
+ #[gpui::test]
+ fn a_textarea_keeps_its_scroll_from_the_area_around_it(cx: &mut TestAppContext) {
+ cx.update(crate::init);
+ let text = (1..=40)
+ .map(|line| format!("line {line}"))
+ .collect::<Vec<_>>()
+ .join("\n");
+ let outer = gpui::ScrollHandle::new();
+ let mut textarea = None;
+ let window = cx.open_window(size(px(240.), px(400.)), |window, cx| {
+ let state =
+ cx.new(|cx| crate::input::TextareaState::new(window, cx).default_value(text));
+ textarea = Some(state.clone());
+ NestedTextarea(state, outer.clone())
+ });
+ let textarea = textarea.unwrap();
+ let mut cx = VisualTestContext::from_window(window.into(), cx);
+ cx.update(|window, cx| window.draw(cx).clear(cx));
+ let swipe = |cx: &mut VisualTestContext, y: f32| {
+ cx.simulate_event(gpui::ScrollWheelEvent {
+ position: point(px(100.), px(y)),
+ delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-40.))),
+ touch_phase: gpui::TouchPhase::Moved,
+ ..Default::default()
+ });
+ cx.update(|window, cx| window.draw(cx).clear(cx));
+ };
+ swipe(&mut cx, 160.);
+ let scrolled = cx.update(|_, cx| textarea.read(cx).scroll_handle.offset().y);
+ assert!(scrolled < px(0.), "the textarea scrolls");
+ assert_eq!(outer.offset().y, px(0.), "the area around it does not");
+ swipe(&mut cx, 40.);
+ assert_eq!(outer.offset().y, px(-40.), "outside the textarea it does");
+ }
+
#[gpui::test]
fn set_scroll_offset_past_the_end_is_clamped(cx: &mut TestAppContext) {
cx.update(crate::init);
@@ -3853,7 +4067,7 @@ mod tests {
let scroll_size = input_bounds.size;
let element = TextElement::new(editor.clone());
let mut bounds = input_bounds;
- let (baseline, _, _) =
+ let (baseline, _, _, _) =
element.layout_cursors(&layout, &mut bounds, scroll_size, window, cx);
// Both a stale offset after deletion and a deferred scroll target must
@@ -3864,7 +4078,7 @@ mod tests {
state.deferred_scroll_offset = deferred;
});
let mut bounds = input_bounds;
- let (carets, offset, _) =
+ let (carets, offset, _, _) =
element.layout_cursors(&layout, &mut bounds, scroll_size, window, cx);
assert_eq!(offset.x, px(0.));
assert_eq!(bounds.origin.x, input_bounds.origin.x);
@@ -3907,7 +4121,7 @@ mod tests {
state.deferred_scroll_offset = Some(point(minimum, px(0.)));
});
let mut bounds = input_bounds;
- let (expected, _, _) =
+ let (expected, _, _, _) =
element.layout_cursors(&layout, &mut bounds, scroll_size, window, cx);
for target in [minimum - px(100.), px(100.)] {
@@ -3915,7 +4129,7 @@ mod tests {
state.deferred_scroll_offset = Some(point(target, px(0.)));
});
let mut bounds = input_bounds;
- let (carets, offset, _) =
+ let (carets, offset, _, _) =
element.layout_cursors(&layout, &mut bounds, scroll_size, window, cx);
let expected_offset = if target < minimum { minimum } else { px(0.) };
assert_eq!(offset.x, expected_offset);
@@ -3966,13 +4180,13 @@ mod tests {
state.deferred_scroll_offset = None;
});
let mut bounds = input_bounds;
- let (baseline, _, _) =
+ let (baseline, _, _, _) =
element.layout_cursors(&layout, &mut bounds, scroll_size, window, cx);
editor.update(cx, |state, _| {
state.deferred_scroll_offset = Some(point(px(-60.), px(0.)));
});
let mut bounds = input_bounds;
- let (carets, offset, _) =
+ let (carets, offset, _, _) =
element.layout_cursors(&layout, &mut bounds, scroll_size, window, cx);
assert_eq!(offset.x, px(-60.));
assert_eq!(carets.len(), 3);
@@ -4472,6 +4686,7 @@ mod tests {
Bounds::new(point(px(51.), px(18.)), size(px(262.), px(87.)))
);
assert_eq!(layout.scroll_size, size(px(972.), px(200.)));
+ assert_eq!(layout.scroll_viewport_size, size(px(262.), px(80.)));
let layout_without_gutter =
EditorScrollbarLayout::new(input_bounds, px(0.), size(px(500.), px(120.)), paddings);
@@ -4481,6 +4696,10 @@ mod tests {
Bounds::new(point(px(10.), px(18.)), size(px(303.), px(87.)))
);
assert_eq!(layout_without_gutter.scroll_size, size(px(513.), px(120.)));
+ assert_eq!(
+ layout_without_gutter.scroll_viewport_size,
+ size(px(303.), px(80.))
+ );
}
#[test]
@@ -4881,100 +5100,41 @@ mod tests {
}
#[test]
- fn test_cursor_surrounding_padding_auto_grow() {
- // Auto-grow inputs always pad by one line, regardless of any
- // override or visible-lines count.
- let line_height = px(20.);
- for override_lines in [None, Some(0), Some(3), Some(99)] {
- for visible_lines in [0_usize, 1, 8, 64] {
- assert_eq!(
- cursor_surrounding_padding(true, override_lines, visible_lines, line_height,),
- line_height,
- );
- }
- }
- }
-
- #[test]
- fn test_cursor_surrounding_padding_default() {
- // `None`: historical heuristic — `BOTTOM_MARGIN_ROWS` for normal
- // viewports, falls back to one line on small viewports (less
- // than `BOTTOM_MARGIN_ROWS × 8` rows tall).
+ fn row_reveal_contains_the_whole_row_and_has_a_stable_offset() {
let line_height = px(20.);
-
- // Small viewport → 1-line fallback.
- let small = BOTTOM_MARGIN_ROWS * 8 - 1;
- assert_eq!(
- cursor_surrounding_padding(false, None, small, line_height),
- line_height,
- );
-
- // Boundary at `BOTTOM_MARGIN_ROWS × 8` flips to the full margin.
- let boundary = BOTTOM_MARGIN_ROWS * 8;
- assert_eq!(
- cursor_surrounding_padding(false, None, boundary, line_height),
- BOTTOM_MARGIN_ROWS * line_height,
- );
-
- // Comfortably-large viewport.
- assert_eq!(
- cursor_surrounding_padding(false, None, 100, line_height),
- BOTTOM_MARGIN_ROWS * line_height,
- );
- }
-
- #[test]
- fn test_cursor_surrounding_padding_explicit() {
- // `Some(n)`: exactly `n × line_height` when the viewport has
- // room for it; saturated against half the viewport when it
- // doesn't.
- let line_height = px(20.);
-
- for lines in [0_usize, 1, 2, 5, 50] {
- let raw = lines as f32 * line_height;
- for visible_lines in [0_usize, 1, 8, 100] {
- let viewport_half = (visible_lines as f32 * line_height).half();
- assert_eq!(
- cursor_surrounding_padding(false, Some(lines), visible_lines, line_height,),
- raw.min(viewport_half),
- );
+ let row_top = px(1000.);
+ for viewport_height in [px(20.), px(30.), px(600.)] {
+ for auto_grow in [false, true] {
+ for surrounding in [None, Some(0), Some(50)] {
+ let padding = cursor_surrounding_padding(
+ auto_grow,
+ surrounding,
+ viewport_visible_lines(viewport_height, line_height),
+ line_height,
+ );
+ for previous in [px(-1500.), px(-1000.), px(0.)] {
+ let offset =
+ reveal_row(previous, row_top, line_height, viewport_height, padding);
+ assert!(row_top + offset >= px(0.));
+ assert!(row_top + offset + line_height <= viewport_height);
+ assert_eq!(
+ reveal_row(offset, row_top, line_height, viewport_height, padding),
+ offset,
+ "unchanged caret must not oscillate",
+ );
+ }
+ }
}
}
}
#[test]
- fn test_cursor_surrounding_padding_saturates_against_viewport() {
- // An aggressive override on a small viewport must not produce a
- // padding larger than half the visible region — otherwise the
- // bottom-edge auto-scroll-into-view threshold sinks below the
- // top-edge threshold and the per-frame scroll adjustment loses
- // a stable fixed point.
- let line_height = px(20.);
-
- // Override much larger than viewport → clamped to half.
- let visible_lines = 10;
- let viewport_half = (visible_lines as f32 * line_height).half();
- assert_eq!(
- cursor_surrounding_padding(false, Some(50), visible_lines, line_height),
- viewport_half,
- );
-
- // Override that fits → returned unchanged.
- let visible_lines = 40;
- assert_eq!(
- cursor_surrounding_padding(false, Some(3), visible_lines, line_height),
- 3.0 * line_height,
- );
-
- // Default heuristic still saturates if BOTTOM_MARGIN_ROWS would
- // exceed the half-viewport bound (only possible at extreme
- // sizes — kept for defensive completeness).
- let visible_lines = BOTTOM_MARGIN_ROWS * 8;
- let half = (visible_lines as f32 * line_height).half();
- let raw = BOTTOM_MARGIN_ROWS * line_height;
+ fn undersized_row_reveal_aligns_the_start_without_oscillating() {
+ let offset = reveal_row(px(0.), px(80.), px(20.), px(10.), px(60.));
+ assert_eq!(px(80.) + offset, px(0.));
assert_eq!(
- cursor_surrounding_padding(false, None, visible_lines, line_height),
- raw.min(half),
+ reveal_row(offset, px(80.), px(20.), px(10.), px(60.)),
+ offset
);
}
}
diff --git a/third-party/gpui-kit/crates/base/src/input/base/state.rs b/third-party/gpui-kit/crates/base/src/input/base/state.rs
index 89d5d6e5eb..5dab8fc5f4 100644
--- a/third-party/gpui-kit/crates/base/src/input/base/state.rs
+++ b/third-party/gpui-kit/crates/base/src/input/base/state.rs
@@ -10,6 +10,7 @@ use gpui::{
Render, ScrollHandle, ScrollWheelEvent, SharedString, Styled as _, Subscription,
UTF16Selection, WeakFocusHandle, Window, actions, div, point, prelude::FluentBuilder as _, px,
};
+use gpui::{Autocapitalize, TextInputAction, TextInputConfiguration};
use ropey::{Rope, RopeSlice};
use serde::Deserialize;
use std::borrow::Cow;
@@ -40,6 +41,7 @@ use crate::input::movement::MoveDirection;
use crate::input::{
InputExtras as _, Position, RopeExt as _, element::RIGHT_MARGIN, layout::LastLayout,
};
+use crate::overscroll::Overscroll;
use crate::{AutoScroll, StepAction};
/// A complete projection preserves every explicit colour, including transparency.
@@ -358,6 +360,8 @@ pub struct PrepaintCaretGeometry {
line_height: Pixels,
input_bounds: Bounds<Pixels>,
scroll_offset: Point<Pixels>,
+ /// A resize owns visibility while its requested repaint settles.
+ pub(super) resize_reveal_owned: bool,
}
impl PrepaintCaretGeometry {
@@ -372,6 +376,7 @@ impl PrepaintCaretGeometry {
line_height,
input_bounds,
scroll_offset,
+ resize_reveal_owned: false,
}
}
@@ -482,6 +487,10 @@ pub struct InputBaseState<M: InputModeKind> {
pub(super) masked: bool,
pub(super) clean_on_escape: bool,
pub(super) submit_on_enter: bool,
+ /// Overrides for [`Self::text_input_configuration`].
+ pub(super) input_action: Option<TextInputAction>,
+ pub(super) autocorrect: Option<bool>,
+ pub(super) autocapitalize: Option<Autocapitalize>,
pub(super) show_whitespaces: bool,
/// This flag tells the renderer to prefer the end of the current visual line.
pub(crate) cursor_line_end_affinity: bool,
@@ -496,6 +505,10 @@ pub struct InputBaseState<M: InputModeKind> {
/// The maximum value for [`super::NumberInput`]. See [`Self::max`].
pub(crate) number_max: Option<f64>,
pub(crate) scroll_handle: ScrollHandle,
+ /// Whether the text stretches past its ends, see [`Self::scroll_bounce`].
+ scroll_bounce: bool,
+ /// The text's stretch past an end, while it lasts.
+ pub(super) overscroll: Overscroll,
/// The deferred scroll offset to apply on next layout.
pub(crate) deferred_scroll_offset: Option<Point<Pixels>>,
/// The size of the scrollable content.
@@ -657,6 +670,49 @@ impl<M: InputModeKind> InputBaseState<M> {
self.last_bounds
}
+ /// The cached display map for querying wrapped rows and display positions.
+ ///
+ /// Reading this does not prepare text or force layout. Before the first
+ /// layout, [`DisplayMap::wrap_row_count`] can be zero; prepared empty text
+ /// has one row. The count describes content, independent of minimum rows,
+ /// auto-grow limits and viewport height.
+ ///
+ /// Text edits use the cached wrapping parameters. Layout refreshes the
+ /// cache after width or font changes; consume that count on a subsequent
+ /// render, or use [`Self::prewrap`] to prepare it before the first layout.
+ pub fn display_map(&self) -> &DisplayMap {
+ &self.display_map
+ }
+
+ /// Wrap the text for `font`, `font_size` and `width` before the first
+ /// layout, so that [`DisplayMap::wrap_row_count`] is known on the first frame.
+ ///
+ /// Call it after setting the value and the wrapping indent. `width` is the
+ /// width of the input's text area: its width less its padding, and less
+ /// the line-number gutter for an editor with line numbers. It wraps as
+ /// layout does, a little short of that width and with the state's wrapping
+ /// indent. Once the input has been laid out this does nothing, and layout
+ /// keeps the wrap up to date. The font and font size must match the input's
+ /// actual text style.
+ pub fn prewrap(
+ &mut self,
+ font: gpui::Font,
+ font_size: Pixels,
+ width: Pixels,
+ cx: &mut Context<Self>,
+ ) {
+ if self.last_layout.is_some() {
+ return;
+ }
+ let wrap_width = (width - RIGHT_MARGIN).max(px(1.));
+ self.display_map.set_font(font, font_size, cx);
+ self.display_map
+ .on_layout_changed(self.soft_wrap.then_some(wrap_width), cx);
+ self.display_map
+ .set_wrapping_indent(self.wrapping_indent, cx);
+ self.display_map.ensure_text_prepared(&self.text, cx);
+ }
+
pub fn diagnostic_popover(&self) -> Option<Rc<crate::input::DiagnosticEntry>> {
self.diagnostic_popover.clone()
}
@@ -862,6 +918,9 @@ impl<M: InputModeKind> InputBaseState<M> {
masked: false,
clean_on_escape: false,
submit_on_enter: false,
+ input_action: None,
+ autocorrect: None,
+ autocapitalize: None,
show_whitespaces: false,
loading: false,
pattern: None,
@@ -878,6 +937,8 @@ impl<M: InputModeKind> InputBaseState<M> {
column_select_start: None,
last_cursor: None,
scroll_handle: ScrollHandle::new(),
+ scroll_bounce: crate::ScrollBounce::enabled_by_default(),
+ overscroll: Overscroll::default(),
scroll_size: gpui::size(px(0.), px(0.)),
editor_scrollbar_snapshot: Cell::new(None),
longest_line_width: Cell::new(None),
@@ -1302,6 +1363,8 @@ impl<M: InputModeKind> InputBaseState<M> {
}
/// Set true to clear the input by pressing Escape key.
+ ///
+ /// Not on Android, where Escape is also the back button.
pub fn clean_on_escape(mut self) -> Self {
self.clean_on_escape = true;
self
@@ -1326,6 +1389,59 @@ impl<M: InputModeKind> InputBaseState<M> {
cx.notify();
}
+ /// Set the action a software keyboard shows on its enter key, like Search
+ /// or Send. Pressing it still emits [`InputEvent::PressEnter`].
+ ///
+ /// Default is [`TextInputAction::Done`] for a single-line input (and a
+ /// multi-line one that submits on enter), and [`TextInputAction::Enter`]
+ /// (a line break) for a multi-line input.
+ pub fn input_action(mut self, action: TextInputAction) -> Self {
+ self.input_action = Some(action);
+ self
+ }
+
+ /// Set whether the platform may autocorrect the text and offer word
+ /// suggestions.
+ ///
+ /// Default is `false`. Enable this explicitly for fields that accept prose;
+ /// masking or revealing text does not change the setting.
+ pub fn autocorrect(mut self, autocorrect: bool) -> Self {
+ self.autocorrect = Some(autocorrect);
+ self
+ }
+
+ /// Set how a software keyboard capitalizes the text.
+ ///
+ /// Default is [`Autocapitalize::None`]. Enable capitalization explicitly for
+ /// fields where it is appropriate, independently of autocorrection.
+ pub fn autocapitalize(mut self, autocapitalize: Autocapitalize) -> Self {
+ self.autocapitalize = Some(autocapitalize);
+ self
+ }
+
+ /// The text assistance a platform IME (a software keyboard) should give
+ /// this input, from [`Self::input_action`], [`Self::autocorrect`] and
+ /// [`Self::autocapitalize`] or the defaults they describe.
+ pub fn text_input_configuration(&self) -> TextInputConfiguration {
+ // Masking is a presentation choice, not a reliable content type: a
+ // revealed password must not acquire text assistance. Keep assistance
+ // opt-in for structured fields such as passwords, email and URLs.
+ let autocorrect = self.autocorrect.unwrap_or(false);
+ let input_action =
+ self.input_action
+ .unwrap_or(if self.is_multi_line() && !self.submit_on_enter {
+ TextInputAction::Enter
+ } else {
+ TextInputAction::Done
+ });
+ TextInputConfiguration {
+ autocorrect,
+ autocapitalize: self.autocapitalize.unwrap_or(Autocapitalize::None),
+ suggestions: autocorrect,
+ input_action,
+ }
+ }
+
/// Set whether to show whitespace characters.
#[doc(hidden)]
pub fn show_whitespaces(mut self, show: bool) -> Self {
@@ -1376,7 +1492,10 @@ impl<M: InputModeKind> InputBaseState<M> {
///
/// - `None` (default): [`BOTTOM_MARGIN_ROWS`] lines, falling back to
/// one line on small viewports.
- /// - `Some(n)`: exactly `n` lines, clamped to half the viewport.
+ /// - `Some(n)`: requests `n` lines, clamped to half the viewport.
+ ///
+ /// In a constrained viewport, edge clearance yields to the cursor's
+ /// complete visual row so surrounding space cannot clip the text being edited.
pub fn cursor_surrounding_lines(mut self, lines: Option<usize>) -> Self {
self.cursor_surrounding_lines = lines;
self
@@ -2278,7 +2397,9 @@ impl<M: InputModeKind> InputBaseState<M> {
return;
}
- if self.clean_on_escape {
+ // Android delivers its back button as escape, and back leaves a field's
+ // text alone: it closes what is open or navigates.
+ if self.clean_on_escape && !cfg!(target_os = "android") {
return self.clean(window, cx);
}
@@ -2500,6 +2621,13 @@ impl<M: InputModeKind> InputBaseState<M> {
}
let gesture = window.input_gesture();
self.undo_manager.break_transaction_coalescing();
+ // Treat every press like a cursor move: take focus now rather than
+ // after GPUI's focus observer runs, show the caret at once and restart
+ // the idle delay, whatever phase the blink was in.
+ if !self.disabled {
+ self.focus(window, cx);
+ self.pause_blink_cursor(cx);
+ }
// Input has its own text selection; suppress the window-level text
// selection (Root) so it does not start a drag from here.
crate::global_state::GlobalState::suppress_text_selection(cx);
@@ -2684,7 +2812,57 @@ impl<M: InputModeKind> InputBaseState<M> {
let delta = event.delta.pixel_delta(line_height);
let old_offset = self.scroll_handle.offset();
- self.update_scroll_offset(Some(old_offset + delta), cx);
+ let precise = matches!(event.delta, gpui::ScrollDelta::Pixels(_));
+ if !precise || !self.scroll_bounce || cx.reduce_motion() || !self.overflows_vertically() {
+ let displaced = self.overscroll.offset() != 0.;
+ self.overscroll.reset();
+ if displaced {
+ cx.notify();
+ }
+ }
+ let bounces = self.scroll_bounce
+ && precise
+ && !cx.reduce_motion()
+ && delta.y.abs() >= delta.x.abs()
+ && self.overflows_vertically();
+ if bounces {
+ let mut overscroll = std::mem::take(&mut self.overscroll);
+ let stretched = overscroll.packet(
+ delta.y.as_f32(),
+ event.touch_phase,
+ self.input_bounds.size.height.as_f32(),
+ cx.background_executor().now(),
+ |dy| {
+ let before = self.scroll_handle.offset();
+ self.update_scroll_offset(
+ Some(point(before.x + delta.x, before.y + px(dy))),
+ cx,
+ );
+ (self.scroll_handle.offset().y - before.y).as_f32()
+ },
+ );
+ self.overscroll = overscroll;
+ if stretched {
+ cx.notify();
+ }
+ } else {
+ // Axis drift must not drop the end of a vertical drag.
+ if matches!(
+ event.touch_phase,
+ gpui::TouchPhase::Ended | gpui::TouchPhase::Cancelled
+ ) && self.overscroll.offset() != 0.
+ {
+ self.overscroll.packet(
+ 0.,
+ event.touch_phase,
+ self.input_bounds.size.height.as_f32(),
+ cx.background_executor().now(),
+ |_| 0.,
+ );
+ cx.notify();
+ }
+ self.update_scroll_offset(Some(old_offset + delta), cx);
+ }
// Text that has room to scroll keeps a scroll over it even where the
// text ends, so reaching the end does not scroll the region around the
@@ -2701,19 +2879,35 @@ impl<M: InputModeKind> InputBaseState<M> {
self.edit_menu_on_scroll(event.touch_phase, cx);
}
+ /// Whether the text takes a wheel event moving by `delta` from the
+ /// scroll areas around it: it has room to scroll along the axis the
+ /// event mostly moves on. Unlike [`Self::overflows_along`], an event that
+ /// moves on neither axis, such as a gesture's start or end, goes to text
+ /// that can scroll vertically.
+ pub(super) fn takes_scroll(&self, delta: Point<Pixels>) -> bool {
+ if delta.y.abs() >= delta.x.abs() {
+ self.overflows_vertically()
+ } else {
+ self.scroll_size.width > self.input_bounds.size.width
+ }
+ }
+
/// Whether the text has room to scroll along the axis `delta` mostly
/// moves on. A trackpad swipe drifts a little off its axis, so the drift
/// alone does not claim the scroll.
fn overflows_along(&self, delta: Point<Pixels>) -> bool {
if delta.y.abs() >= delta.x.abs() {
- self.is_multi_line()
- && delta.y != px(0.)
- && self.scroll_size.height > self.input_bounds.size.height
+ delta.y != px(0.) && self.overflows_vertically()
} else {
self.scroll_size.width > self.input_bounds.size.width
}
}
+ /// Whether multi-line text is taller than the input.
+ fn overflows_vertically(&self) -> bool {
+ self.is_multi_line() && self.scroll_size.height > self.input_bounds.size.height
+ }
+
pub(super) fn update_scroll_offset(
&mut self,
offset: Option<Point<Pixels>>,
@@ -2782,20 +2976,9 @@ impl<M: InputModeKind> InputBaseState<M> {
let row = point.row;
// Resolve the wrapped row even when the target is outside the last layout.
- let display_pos = self
+ let display_row = self
.display_map
- .buffer_pos_to_display_pos(crate::input::BufferPoint::new(row, point.column));
- let affinity = offset == self.cursor() && self.cursor_line_end_affinity;
- let display_row = if affinity {
- let wrap = self
- .display_map
- .offset_to_wrap_display_point_with_affinity(offset, true);
- self.display_map
- .wrap_row_to_display_row(wrap.row)
- .unwrap_or(display_pos.row)
- } else {
- display_pos.row
- };
+ .offset_to_display_row(offset, self.line_end_affinity_at(offset));
let row_offset_y = line_height * display_row;
// For Right alignment use 0 margin: the cursor indicator is clamped inside bounds
@@ -2837,15 +3020,15 @@ impl<M: InputModeKind> InputBaseState<M> {
line_height,
)
} else {
- line_height
+ px(0.)
};
- if row_offset_y - edge_height + line_height < -scroll_offset.y {
- // Scroll up
- scroll_offset.y = -row_offset_y + edge_height - line_height;
- } else if row_offset_y + edge_height > -scroll_offset.y + bounds.size.height {
- // Scroll down
- scroll_offset.y = -(row_offset_y - bounds.size.height + edge_height);
- }
+ scroll_offset.y = super::element::reveal_row(
+ scroll_offset.y,
+ row_offset_y,
+ line_height,
+ bounds.size.height,
+ edge_height,
+ );
// Avoid necessary scroll, when it was already in the correct position.
if direction == Some(MoveDirection::Up) {
@@ -4784,6 +4967,14 @@ impl<M: InputModeKind> EntityInputHandler for InputBaseState<M> {
None
}
+
+ fn text_input_configuration(
+ &mut self,
+ _window: &mut Window,
+ _cx: &mut Context<Self>,
+ ) -> TextInputConfiguration {
+ InputBaseState::text_input_configuration(self)
+ }
}
impl<M: InputModeKind> Focusable for InputBaseState<M> {
@@ -4974,6 +5165,76 @@ impl<M: InputModeKind> Render for InputBaseState<M> {
mod tests {
use super::*;
+ #[gpui::test]
+ fn revealing_a_password_keeps_text_assistance_disabled(cx: &mut TestAppContext) {
+ cx.update(crate::init);
+ cx.open_window(size(px(400.), px(100.)), |window, cx| {
+ let state = cx.new(|cx| crate::input::InputState::new(window, cx).masked(true));
+ let hidden = state.read(cx).text_input_configuration();
+ assert!(!hidden.autocorrect && !hidden.suggestions);
+ assert_eq!(hidden.autocapitalize, Autocapitalize::None);
+
+ // The component's reveal button uses this same state transition.
+ state.update(cx, |state, cx| state.toggle_masked(window, cx));
+ assert_eq!(state.read(cx).text_input_configuration(), hidden);
+ state.update(cx, |state, cx| state.toggle_masked(window, cx));
+ assert_eq!(state.read(cx).text_input_configuration(), hidden);
+ gpui::EmptyView
+ });
+ }
+
+ #[gpui::test]
+ fn text_input_configuration_follows_the_input(cx: &mut TestAppContext) {
+ use crate::input::{InputState, TextareaState};
+
+ cx.update(crate::init);
+ let mut states = None;
+ cx.open_window(size(px(400.), px(100.)), |window, cx| {
+ states = Some((
+ cx.new(|cx| InputState::new(window, cx)),
+ cx.new(|cx| InputState::new(window, cx).masked(true)),
+ cx.new(|cx| TextareaState::new(window, cx)),
+ cx.new(|cx| {
+ InputState::new(window, cx)
+ .input_action(TextInputAction::Search)
+ .autocorrect(true)
+ .autocapitalize(Autocapitalize::Words)
+ }),
+ ));
+ gpui::EmptyView
+ });
+ let (plain, masked, textarea, search) = states.unwrap();
+
+ cx.read(|cx| {
+ assert_eq!(
+ plain.read(cx).text_input_configuration(),
+ TextInputConfiguration {
+ input_action: TextInputAction::Done,
+ ..Default::default()
+ }
+ );
+ assert_eq!(
+ masked.read(cx).text_input_configuration(),
+ TextInputConfiguration {
+ input_action: TextInputAction::Done,
+ ..Default::default()
+ }
+ );
+ assert_eq!(
+ textarea.read(cx).text_input_configuration(),
+ TextInputConfiguration {
+ input_action: TextInputAction::Enter,
+ ..Default::default()
+ }
+ );
+ let search = search.read(cx).text_input_configuration();
+ assert_eq!(search.input_action, TextInputAction::Search);
+ assert_eq!(search.autocapitalize, Autocapitalize::Words);
+ assert!(search.autocorrect);
+ assert!(search.suggestions);
+ });
+ }
+
#[gpui::test]
fn test_multicursor_unicode_completion_uses_post_edit_start(cx: &mut TestAppContext) {
use crate::input::{CompletionProvider, EditorState};
@@ -6160,6 +6421,254 @@ mod tests {
}
}
+ /// Sends one trackpad packet straight to the input.
+ fn swipe<M: InputModeKind>(
+ view: &InputView<M>,
+ cx: &mut VisualTestContext,
+ dy: f32,
+ phase: gpui::TouchPhase,
+ ) {
+ cx.update(|window, cx| {
+ view.input.update(cx, |state, cx| {
+ state.on_scroll_wheel(
+ &ScrollWheelEvent {
+ delta: gpui::ScrollDelta::Pixels(point(px(0.), px(dy))),
+ touch_phase: phase,
+ ..Default::default()
+ },
+ window,
+ cx,
+ )
+ })
+ });
+ }
+
+ fn long_text() -> String {
+ (0..60)
+ .map(|n| format!("line {n}"))
+ .collect::<Vec<_>>()
+ .join("\n")
+ }
+
+ #[gpui::test]
+ fn test_scroll_bounce_stretches_past_the_end_and_springs_back(cx: &mut TestAppContext) {
+ use gpui::TouchPhase::{Ended, Moved, Started};
+
+ let view = InputView::build_textarea(cx, |state| {
+ state.scroll_bounce(true).default_value(long_text())
+ });
+ let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
+ cx.update(|window, cx| window.draw(cx).clear(cx));
+ let drawn_at = |cx: &mut VisualTestContext| {
+ view.input
+ .read_with(cx, |state, _| state.last_bounds.unwrap().origin.y)
+ };
+
+ swipe(&view, &mut cx, -20., Started);
+ swipe(&view, &mut cx, -100_000., Moved);
+ let (end, stretch) = view.input.read_with(&cx, |state, _| {
+ (state.scroll_handle.offset().y, state.overscroll.offset())
+ });
+ assert!(end < px(0.), "the text scrolled to its end");
+ assert!(stretch < 0., "the rest stretched the edge: {stretch}");
+ cx.update(|window, cx| window.draw(cx).clear(cx));
+ let stretched_at = drawn_at(&mut cx);
+
+ swipe(&view, &mut cx, 0., Ended);
+ for _ in 0..120 {
+ cx.executor()
+ .advance_clock(std::time::Duration::from_millis(16));
+ cx.update(|window, cx| window.draw(cx).clear(cx));
+ }
+ view.input.read_with(&cx, |state, _| {
+ assert_eq!(state.overscroll.offset(), 0., "the edge sprang back");
+ assert_eq!(
+ state.scroll_handle.offset().y,
+ end,
+ "scrolling stayed exact"
+ );
+ });
+ let moved = (stretched_at - drawn_at(&mut cx)).as_f32();
+ assert!(
+ (moved - stretch).abs() < 0.001,
+ "the stretch only moved where the text was drawn: {moved} vs {stretch}"
+ );
+ }
+
+ /// Swipes far past the end of the text and returns where the text
+ /// scrolled to and how far its edge stretched.
+ fn swipe_past_the_end(
+ view: &InputView<TextareaMode>,
+ cx: &mut VisualTestContext,
+ ) -> (Pixels, f32) {
+ use gpui::TouchPhase::{Moved, Started};
+
+ cx.update(|window, cx| window.draw(cx).clear(cx));
+ swipe(view, cx, -20., Started);
+ swipe(view, cx, -100_000., Moved);
+ view.input.read_with(cx, |state, _| {
+ (state.scroll_handle.offset().y, state.overscroll.offset())
+ })
+ }
+
+ #[gpui::test]
+ fn test_scroll_bounce_stays_put_under_reduced_motion(cx: &mut TestAppContext) {
+ cx.update(|cx| cx.set_reduce_motion(true));
+ let view = InputView::build_textarea(cx, |state| {
+ state.scroll_bounce(true).default_value(long_text())
+ });
+ let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
+ let (end, stretch) = swipe_past_the_end(&view, &mut cx);
+ assert!(end < px(0.), "the text still scrolls");
+ assert_eq!(stretch, 0., "the edge does not stretch");
+ }
+
+ #[gpui::test]
+ fn test_scroll_bounce_follows_the_platform_by_default(cx: &mut TestAppContext) {
+ let view = InputView::build_textarea(cx, |state| state.default_value(long_text()));
+ let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
+ let (end, stretch) = swipe_past_the_end(&view, &mut cx);
+ assert!(end < px(0.), "the text scrolls");
+ assert_eq!(
+ stretch != 0.,
+ crate::ScrollBounce::enabled_by_default(),
+ "the edge stretches where the platform's scroll views do"
+ );
+ }
+
+ #[gpui::test]
+ fn test_scroll_bounce_leaves_text_that_fits(cx: &mut TestAppContext) {
+ let view = InputView::build_textarea(cx, |state| {
+ state.scroll_bounce(true).default_value("one line")
+ });
+ let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
+ let (end, stretch) = swipe_past_the_end(&view, &mut cx);
+ assert_eq!((end, stretch), (px(0.), 0.));
+ }
+
+ #[gpui::test]
+ fn test_scroll_bounce_resets_when_replacement_text_fits(cx: &mut TestAppContext) {
+ let view = InputView::build_textarea(cx, |state| {
+ state.scroll_bounce(true).default_value(long_text())
+ });
+ let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
+ let (_, stretch) = swipe_past_the_end(&view, &mut cx);
+ assert!(stretch < 0.);
+ cx.update(|window, cx| {
+ view.input
+ .update(cx, |state, cx| state.set_value("short", window, cx));
+ window.draw(cx).clear(cx);
+ });
+ swipe(&view, &mut cx, 0., gpui::TouchPhase::Ended);
+ view.input.read_with(&cx, |state, _| {
+ assert_eq!(state.overscroll.offset(), 0.);
+ assert_eq!(state.scroll_handle.offset().y, px(0.));
+ });
+ }
+
+ #[gpui::test]
+ fn test_scroll_bounce_releases_when_the_end_packet_is_horizontal(cx: &mut TestAppContext) {
+ let view = InputView::build_textarea(cx, |state| {
+ state.scroll_bounce(true).default_value(long_text())
+ });
+ let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
+ let (_, stretch) = swipe_past_the_end(&view, &mut cx);
+ assert!(stretch < 0.);
+ cx.update(|window, cx| {
+ view.input.update(cx, |state, cx| {
+ state.on_scroll_wheel(
+ &ScrollWheelEvent {
+ delta: gpui::ScrollDelta::Pixels(point(px(1.), px(0.))),
+ touch_phase: gpui::TouchPhase::Ended,
+ ..Default::default()
+ },
+ window,
+ cx,
+ );
+ });
+ });
+ for _ in 0..120 {
+ cx.executor()
+ .advance_clock(std::time::Duration::from_millis(16));
+ cx.update(|window, cx| window.draw(cx).clear(cx));
+ }
+ assert_eq!(
+ view.input
+ .read_with(&cx, |state, _| state.overscroll.offset()),
+ 0.
+ );
+ }
+
+ #[gpui::test]
+ fn test_scroll_bounce_ordinary_mouse_wheel_does_not_stretch(cx: &mut TestAppContext) {
+ let view = InputView::build_textarea(cx, |state| {
+ state.scroll_bounce(true).default_value(long_text())
+ });
+ let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
+ cx.update(|window, cx| {
+ window.draw(cx).clear(cx);
+ view.input.update(cx, |state, cx| {
+ state.on_scroll_wheel(
+ &ScrollWheelEvent {
+ delta: gpui::ScrollDelta::Lines(point(0., -100_000.)),
+ touch_phase: gpui::TouchPhase::Moved,
+ ..Default::default()
+ },
+ window,
+ cx,
+ );
+ assert!(state.scroll_handle.offset().y < px(0.));
+ assert_eq!(state.overscroll.offset(), 0.);
+ });
+ });
+ }
+
+ #[gpui::test]
+ fn test_scroll_bounce_new_wheel_input_interrupts_the_return(cx: &mut TestAppContext) {
+ let view = InputView::build_textarea(cx, |state| {
+ state.scroll_bounce(true).default_value(long_text())
+ });
+ let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
+ let (end, stretch) = swipe_past_the_end(&view, &mut cx);
+ assert!(stretch < 0.);
+ swipe(&view, &mut cx, 0., gpui::TouchPhase::Ended);
+ cx.executor()
+ .advance_clock(std::time::Duration::from_millis(16));
+ cx.update(|window, cx| {
+ window.draw(cx).clear(cx);
+ view.input.update(cx, |state, cx| {
+ state.on_scroll_wheel(
+ &ScrollWheelEvent {
+ delta: gpui::ScrollDelta::Lines(point(0., 2.)),
+ touch_phase: gpui::TouchPhase::Moved,
+ ..Default::default()
+ },
+ window,
+ cx,
+ );
+ assert!(state.scroll_handle.offset().y > end);
+ assert_eq!(state.overscroll.offset(), 0.);
+ });
+ });
+ }
+
+ #[gpui::test]
+ fn test_turning_scroll_bounce_off_drops_the_stretch(cx: &mut TestAppContext) {
+ let view = InputView::build_textarea(cx, |state| {
+ state.scroll_bounce(true).default_value(long_text())
+ });
+ let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
+ let (_, stretch) = swipe_past_the_end(&view, &mut cx);
+ assert!(stretch < 0., "the edge stretched: {stretch}");
+ view.input
+ .update(&mut cx, |state, cx| state.set_scroll_bounce(false, cx));
+ assert_eq!(
+ view.input
+ .read_with(&cx, |state, _| state.overscroll.offset()),
+ 0.
+ );
+ }
+
/// The frame is laid out by the application, which should not have to
/// center a single line inside it.
#[gpui::test]
@@ -9005,6 +9514,54 @@ mod tests {
});
}
+ #[gpui::test]
+ fn test_textarea_prewrap_prepares_content_rows_before_layout(cx: &mut TestAppContext) {
+ cx.update(crate::init);
+ for (value, expected_rows) in [("", 1), ("one\ntwo", 2)] {
+ cx.open_window(size(px(400.), px(100.)), |window, cx| {
+ let font = window.text_style().font();
+ cx.new(|cx| {
+ let mut state = crate::input::TextareaState::new(window, cx)
+ .rows(12)
+ .default_value(value);
+ assert_eq!(state.display_map().wrap_row_count(), 0);
+ state.prewrap(font, px(14.), px(120.), cx);
+ assert_eq!(state.display_map().wrap_row_count(), expected_rows);
+ assert_eq!(state.value(), value);
+ state
+ });
+ gpui::EmptyView
+ });
+ }
+ }
+
+ #[gpui::test]
+ fn test_textarea_prewrap_respects_soft_wrap_before_layout(cx: &mut TestAppContext) {
+ cx.update(crate::init);
+ let value = "select id, name, email, phone from users where active\nlimit 10";
+ for soft_wrap in [true, false] {
+ cx.open_window(size(px(400.), px(100.)), |window, cx| {
+ let font = window.text_style().font();
+ cx.new(|cx| {
+ let mut state = crate::input::TextareaState::new(window, cx)
+ .rows(1)
+ .soft_wrap(soft_wrap)
+ .default_value(value);
+ state.prewrap(font, px(14.), px(120.), cx);
+ let rows = state.display_map().wrap_row_count();
+ if soft_wrap {
+ assert!(rows > 2, "the first logical line must wrap");
+ } else {
+ assert_eq!(rows, 2);
+ }
+ assert_eq!(state.value(), value);
+ state
+ });
+ gpui::EmptyView
+ });
+ }
+ }
+
#[gpui::test]
fn test_prewrap_counts_wrapped_rows_before_the_first_paint(cx: &mut TestAppContext) {
cx.update(crate::init);
@@ -9416,6 +9973,48 @@ mod tests {
}
}
+ #[gpui::test]
+ fn test_mouse_down_shows_cursor_like_a_cursor_move(cx: &mut TestAppContext) {
+ cx.update(crate::init);
+ let view = InputView::<EditorMode>::new(cx);
+ let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
+ setup_cursors(&mut cx, &view.input, "abcd|efgh");
+ let position = |offset: Pixels, cx: &mut VisualTestContext| {
+ view.input.read_with(cx, |state, _| {
+ let layout = state.last_layout.as_ref().unwrap();
+ state.last_bounds.unwrap().origin
+ + point(layout.line_number_width + offset, layout.line_height * 0.5)
+ })
+ };
+ let visible = |cx: &mut VisualTestContext| {
+ view.input
+ .read_with(cx, |state, cx| state.blink_cursor.read(cx).visible())
+ };
+
+ // The press itself shows the caret, without waiting for the focus
+ // observer, and the idle delay holds it there like a cursor move.
+ let first = position(px(2.), &mut cx);
+ cx.simulate_mouse_down(first, MouseButton::Left, Default::default());
+ assert!(visible(&mut cx));
+ cx.simulate_mouse_up(first, MouseButton::Left, Default::default());
+ cx.run_until_parked();
+
+ // Pressing elsewhere in the focused input, in the hidden phase of the
+ // blink, shows the caret again at the press.
+ cx.executor()
+ .advance_clock(std::time::Duration::from_millis(400));
+ cx.run_until_parked();
+ assert!(!visible(&mut cx));
+ let second = position(px(30.), &mut cx);
+ cx.simulate_mouse_down(second, MouseButton::Left, Default::default());
+ assert!(visible(&mut cx));
+ cx.executor()
+ .advance_clock(std::time::Duration::from_millis(250));
+ cx.run_until_parked();
+ assert!(visible(&mut cx));
+ cx.simulate_mouse_up(second, MouseButton::Left, Default::default());
+ }
+
#[gpui::test]
fn test_multi_cursor_actions_reveal_hidden_carets(cx: &mut TestAppContext) {
let view = InputView::<EditorMode>::new(cx);
@@ -11772,6 +12371,29 @@ impl InputBaseState<crate::input::InputMode> {
/// Methods shared by the two multi-line modes, and reachable on neither a
/// single-line input nor anything else.
impl<M: crate::input::MultiLineMode> InputBaseState<M> {
+ /// Set whether text taller than the input stretches past its top and
+ /// bottom and springs back when precise trackpad or Magic Mouse scrolling
+ /// or its momentum runs into an end. Ordinary line-wheel scrolling does
+ /// not bounce. The default follows
+ /// [`crate::ScrollBounce::enabled_by_default`]: on on iOS, Android and
+ /// macOS, off elsewhere. Under reduced motion the text stops at its ends
+ /// without stretching.
+ pub fn scroll_bounce(mut self, scroll_bounce: bool) -> Self {
+ self.scroll_bounce = scroll_bounce;
+ self
+ }
+
+ /// Update whether the text stretches past its ends, see
+ /// [`InputBaseState::scroll_bounce`]. Turning it off drops a stretch in
+ /// progress.
+ pub fn set_scroll_bounce(&mut self, scroll_bounce: bool, cx: &mut Context<Self>) {
+ if self.scroll_bounce != scroll_bounce {
+ self.scroll_bounce = scroll_bounce;
+ self.overscroll.reset();
+ cx.notify();
+ }
+ }
+
/// Whether the built-in search panel and its shortcut are enabled. Off by
/// default, on for the code editor.
///
@@ -12013,41 +12635,6 @@ impl InputBaseState<crate::input::EditorMode> {
self
}
- /// The editor's display map for querying wrapped rows and display positions.
- ///
- /// Text is prepared during layout or by [`Self::prewrap`].
- pub fn display_map(&self) -> &DisplayMap {
- &self.display_map
- }
-
- /// Wrap the text for `font`, `font_size` and `width` before the first
- /// layout, so that [`DisplayMap::wrap_row_count`] is known on the first frame.
- ///
- /// Call it after setting the value and the wrapping indent. `width` is the
- /// width of the editor's text area: its width less its padding, and less
- /// the line-number gutter when line numbers are shown. It wraps as layout
- /// does, a little short of that width and with the state's wrapping
- /// indent. Once the editor has been laid out this does nothing, and layout
- /// keeps the wrap up to date.
- pub fn prewrap(
- &mut self,
- font: gpui::Font,
- font_size: Pixels,
- width: Pixels,
- cx: &mut Context<Self>,
- ) {
- if self.last_layout.is_some() {
- return;
- }
- let wrap_width = (width - RIGHT_MARGIN).max(px(1.));
- self.display_map.set_font(font, font_size, cx);
- self.display_map
- .on_layout_changed(self.soft_wrap.then_some(wrap_width), cx);
- self.display_map
- .set_wrapping_indent(self.wrapping_indent, cx);
- self.display_map.ensure_text_prepared(&self.text, cx);
- }
-
/// Set line number.
pub fn set_line_number(&mut self, line_number: bool, _: &mut Window, cx: &mut Context<Self>) {
if let LayoutMode::CodeEditor { line_number: l, .. } = &mut self.mode {
diff --git a/third-party/gpui-kit/crates/base/src/input/editor/display_map/display_map.rs b/third-party/gpui-kit/crates/base/src/input/editor/display_map/display_map.rs
index 650cd71802..3bd072a31f 100644
--- a/third-party/gpui-kit/crates/base/src/input/editor/display_map/display_map.rs
+++ b/third-party/gpui-kit/crates/base/src/input/editor/display_map/display_map.rs
@@ -302,6 +302,13 @@ impl DisplayMap {
.offset_to_display_point_with_affinity(offset, line_end_affinity)
}
+ /// Resolve the caret's visual row through both wrapping and folded regions.
+ pub(crate) fn offset_to_display_row(&self, offset: usize, line_end_affinity: bool) -> usize {
+ let point = self.offset_to_wrap_display_point_with_affinity(offset, line_end_affinity);
+ self.wrap_row_to_display_row(point.row)
+ .unwrap_or_else(|| self.nearest_visible_display_row(point.row))
+ }
+
/// Convert wrap display point to byte offset.
#[inline]
pub(crate) fn wrap_display_point_to_offset(&self, point: WrapDisplayPoint) -> usize {
diff --git a/third-party/gpui-kit/crates/base/src/input/editor/lsp/completions.rs b/third-party/gpui-kit/crates/base/src/input/editor/lsp/completions.rs
index 8c4ebfba15..a0b13d02a2 100644
--- a/third-party/gpui-kit/crates/base/src/input/editor/lsp/completions.rs
+++ b/third-party/gpui-kit/crates/base/src/input/editor/lsp/completions.rs
@@ -14,10 +14,116 @@ use crate::input::{InputBaseState, ShowCompletions};
/// Default debounce duration for inline completions.
const DEFAULT_INLINE_COMPLETION_DEBOUNCE: Duration = Duration::from_millis(300);
+/// Decides which bytes of a completion label the menu paints as the match.
+///
+/// `query` is the word typed since the menu opened. Returned ranges are byte
+/// offsets into [`CompletionItem::label`].
+pub type CompletionHighlighter = dyn Fn(&str, &CompletionItem) -> Vec<Range<usize>>;
+
+/// Highlight a prefix of the completion label, matching today's menu.
+///
+/// The painted length is [`CompletionItem::filter_text`] when the item sets
+/// one, otherwise the query. It is shortened so the range never splits a
+/// character. An empty length paints nothing.
+pub fn highlight_prefix(query: &str, item: &CompletionItem) -> Vec<Range<usize>> {
+ let matched_len = item
+ .filter_text
+ .as_ref()
+ .map(|text| text.len())
+ .unwrap_or(query.len())
+ .min(item.label.len());
+ let end = item.label.floor_char_boundary(matched_len);
+ if end == 0 { Vec::new() } else { vec![0..end] }
+}
+
+/// Highlight the first case-insensitive occurrence of `query` in the label.
+///
+/// An empty query, or a query that does not occur as a contiguous span,
+/// paints nothing.
+pub fn highlight_contains(query: &str, item: &CompletionItem) -> Vec<Range<usize>> {
+ let query: Vec<char> = query.chars().collect();
+ if query.is_empty() {
+ return Vec::new();
+ }
+
+ let label: Vec<(usize, char)> = item.label.char_indices().collect();
+ let Some(last_start) = label.len().checked_sub(query.len()) else {
+ return Vec::new();
+ };
+
+ for start in 0..=last_start {
+ let matched = query
+ .iter()
+ .zip(label[start..].iter())
+ .all(|(query_char, (_, label_char))| chars_eq_ignore_case(*query_char, *label_char));
+ if !matched {
+ continue;
+ }
+
+ let byte_start = label[start].0;
+ let (last_index, last_char) = label[start + query.len() - 1];
+ return vec![byte_start..last_index + last_char.len_utf8()];
+ }
+ Vec::new()
+}
+
+/// Highlight each character of `query` in order, skipping characters in between.
+///
+/// Matching is case-insensitive. Adjacent hits merge into one range, so a
+/// contiguous match still paints as a single span. A query that cannot be
+/// matched in full paints nothing.
+pub fn highlight_characters(query: &str, item: &CompletionItem) -> Vec<Range<usize>> {
+ if query.is_empty() {
+ return Vec::new();
+ }
+
+ let mut ranges: Vec<Range<usize>> = Vec::new();
+ let mut rest = item.label.as_str();
+ let mut base = 0usize;
+ for query_char in query.chars() {
+ let Some((index, matched)) = rest
+ .char_indices()
+ .find(|(_, label_char)| chars_eq_ignore_case(query_char, *label_char))
+ else {
+ return Vec::new();
+ };
+
+ let start = base + index;
+ let end = start + matched.len_utf8();
+ let next = index + matched.len_utf8();
+ base += next;
+ rest = &rest[next..];
+ if let Some(last) = ranges.last_mut() {
+ if last.end == start {
+ last.end = end;
+ continue;
+ }
+ }
+ ranges.push(start..end);
+ }
+ ranges
+}
+
+fn chars_eq_ignore_case(left: char, right: char) -> bool {
+ left == right
+ || left.eq_ignore_ascii_case(&right)
+ || left.to_lowercase().eq(right.to_lowercase())
+}
+
/// Display options for the LSP completion popover.
///
/// Accessed through [`super::Lsp::completion_menu`] so embedders can tweak the
-/// popover without growing the [`InputBaseState`] API.
+/// popover without growing the [`InputBaseState`] API. Label highlighting is
+/// configured separately, on [`super::Lsp::completion_highlighter`].
+///
+/// ```
+/// use gpui_base::input::CompletionMenuOptions;
+/// let options = CompletionMenuOptions {
+/// max_width: gpui::px(480.),
+/// };
+/// let _copied = options;
+/// let _again = options;
+/// ```
#[derive(Debug, Clone, Copy)]
pub struct CompletionMenuOptions {
/// Maximum width of the popover.
@@ -453,3 +559,71 @@ impl InputBaseState<EditorMode> {
true
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn item(label: &str, filter_text: Option<&str>) -> CompletionItem {
+ CompletionItem {
+ label: label.to_string(),
+ filter_text: filter_text.map(str::to_string),
+ ..Default::default()
+ }
+ }
+
+ #[test]
+ fn prefix_uses_the_query_length_unless_filter_text_is_set() {
+ assert_eq!(highlight_prefix("fl", &item("flag", None)), vec![0..2]);
+ assert_eq!(highlight_prefix("fl", &item("flag", Some("f"))), vec![0..1]);
+ assert_eq!(
+ highlight_prefix("flagpole", &item("flag", None)),
+ vec![0..4]
+ );
+ assert!(highlight_prefix("", &item("flag", None)).is_empty());
+ }
+
+ #[test]
+ fn prefix_does_not_split_a_character() {
+ assert_eq!(highlight_prefix("你", &item("你好", None)), vec![0..3]);
+ assert!(highlight_prefix("a", &item("你", None)).is_empty());
+ }
+
+ #[test]
+ fn contains_highlights_the_first_contiguous_match() {
+ // "dialog" itself ends in "log", so that hit comes before the suffix.
+ assert_eq!(
+ highlight_contains("log", &item("dialog_log", None)),
+ vec![3..6]
+ );
+ assert_eq!(
+ highlight_contains("LOG", &item("dialog_log", None)),
+ vec![3..6]
+ );
+ assert_eq!(
+ highlight_contains("log", &item("catalog", None)),
+ vec![4..7]
+ );
+ assert_eq!(
+ highlight_contains("好世", &item("你好世界", None)),
+ vec![3..9]
+ );
+ assert!(highlight_contains("log", &item("flag", None)).is_empty());
+ assert!(highlight_contains("", &item("flag", None)).is_empty());
+ }
+
+ #[test]
+ fn characters_highlight_gaps_and_merge_adjacent_hits() {
+ assert_eq!(highlight_characters("fl", &item("flag", None)), vec![0..2]);
+ assert_eq!(
+ highlight_characters("fl", &item("file_loader", None)),
+ vec![0..1, 2..3]
+ );
+ assert_eq!(
+ highlight_characters("你世", &item("你好世界", None)),
+ vec![0..3, 6..9]
+ );
+ assert!(highlight_characters("fx", &item("flag", None)).is_empty());
+ assert!(highlight_characters("", &item("flag", None)).is_empty());
+ }
+}
diff --git a/third-party/gpui-kit/crates/base/src/input/editor/lsp/mod.rs b/third-party/gpui-kit/crates/base/src/input/editor/lsp/mod.rs
index b194f44c5e..c5daa6a2a9 100644
--- a/third-party/gpui-kit/crates/base/src/input/editor/lsp/mod.rs
+++ b/third-party/gpui-kit/crates/base/src/input/editor/lsp/mod.rs
@@ -58,6 +58,12 @@ pub struct Lsp {
/// Display options for the completion popover.
pub completion_menu: CompletionMenuOptions,
+ /// Decides which bytes of each completion label the menu paints.
+ ///
+ /// Defaults to [`highlight_prefix`]. Replace it to paint a contiguous
+ /// match, a character-by-character match, or ranges the host computed.
+ completion_highlighter: Rc<CompletionHighlighter>,
+
pub(crate) document_colors: Vec<(lsp_types::Range, Hsla)>,
/// Cached semantic tokens as absolute position ranges + theme token-type
/// names. Color is resolved from the name at paint time so theme switches
@@ -78,6 +84,7 @@ impl Default for Lsp {
definition_provider: None,
document_color_provider: None,
completion_menu: CompletionMenuOptions::default(),
+ completion_highlighter: Rc::new(highlight_prefix),
semantic_tokens_provider: None,
show_document: None,
document_colors: vec![],
@@ -91,6 +98,24 @@ impl Default for Lsp {
}
impl Lsp {
+ /// Set the completion label highlighter when constructing the LSP configuration.
+ pub fn with_completion_highlighter(mut self, highlighter: Rc<CompletionHighlighter>) -> Self {
+ self.set_completion_highlighter(highlighter);
+ self
+ }
+
+ /// Return the completion label highlighter.
+ pub fn completion_highlighter(&self) -> &Rc<CompletionHighlighter> {
+ &self.completion_highlighter
+ }
+
+ /// Replace the completion label highlighter.
+ ///
+ /// Notify the editor after updating its LSP configuration to repaint an open menu.
+ pub fn set_completion_highlighter(&mut self, highlighter: Rc<CompletionHighlighter>) {
+ self.completion_highlighter = highlighter;
+ }
+
/// Update the LSP when the text changes.
pub(crate) fn update(
&mut self,
@@ -130,3 +155,19 @@ impl InputBaseState<EditorMode> {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_lsp_builder() {
+ let highlighter: Rc<CompletionHighlighter> = Rc::new(highlight_contains);
+ let mut lsp = Lsp::default().with_completion_highlighter(highlighter.clone());
+ assert!(Rc::ptr_eq(lsp.completion_highlighter(), &highlighter));
+
+ let replacement: Rc<CompletionHighlighter> = Rc::new(highlight_characters);
+ lsp.set_completion_highlighter(replacement.clone());
+ assert!(Rc::ptr_eq(lsp.completion_highlighter(), &replacement));
+ }
+}
diff --git a/third-party/gpui-kit/crates/base/src/input/editor/search.rs b/third-party/gpui-kit/crates/base/src/input/editor/search.rs
index c4f1c97d12..64680353cb 100644
--- a/third-party/gpui-kit/crates/base/src/input/editor/search.rs
+++ b/third-party/gpui-kit/crates/base/src/input/editor/search.rs
@@ -169,15 +169,31 @@ impl<M: InputModeKind> InputBaseState<M> {
/// [`InputBaseState::previous_search_match`], read the count and the
/// current index from [`InputBaseState::search_session`], and end the
/// search with [`InputBaseState::close_search`].
+ ///
+ /// A changed query or case mode starts at the current viewport. Setting
+ /// the same query and case mode again retains the current occurrence.
pub fn set_search_query(
&mut self,
query: impl Into<String>,
case_insensitive: bool,
cx: &mut Context<Self>,
) {
+ let query = query.into();
+ let query_changed = query != self.search_session.query
+ || case_insensitive != self.search_session.case_insensitive;
+ self.search_session.anchor_offset = if query_changed {
+ self.last_layout
+ .as_ref()
+ .map(|layout| layout.visible_range_offset.start)
+ } else {
+ None
+ };
self.search_session.activate();
self.search_session.update_query(query, case_insensitive);
self.search_session.matcher.update(&self.text);
+ if query_changed && let Some(anchor) = self.search_session.anchor_offset {
+ self.search_session.matcher.update_cursor_by_offset(anchor);
+ }
cx.notify();
}
diff --git a/third-party/gpui-kit/crates/base/src/input/mod.rs b/third-party/gpui-kit/crates/base/src/input/mod.rs
index 23ec550e83..4ae8d6bfd5 100644
--- a/third-party/gpui-kit/crates/base/src/input/mod.rs
+++ b/third-party/gpui-kit/crates/base/src/input/mod.rs
@@ -112,10 +112,11 @@ pub use language::{LanguageProvider, set_language_config, set_language_provider}
pub(crate) use language_config::LanguageConfig;
pub use language_config::{AutoClosingPair, BracketPair, IndentationRules};
pub use lsp::{
- CodeActionItem, CodeActionMenuState, CodeActionProvider, CompletionMenuOptions,
- CompletionMenuState, CompletionProvider, DefinitionProvider, DocumentColorProvider,
- DocumentRangeSemanticTokensProvider, HoverPopoverState, HoverProvider, InputOverlayKind, Lsp,
- ShowDocumentHandler,
+ CodeActionItem, CodeActionMenuState, CodeActionProvider, CompletionHighlighter,
+ CompletionMenuOptions, CompletionMenuState, CompletionProvider, DefinitionProvider,
+ DocumentColorProvider, DocumentRangeSemanticTokensProvider, HoverPopoverState, HoverProvider,
+ InputOverlayKind, Lsp, ShowDocumentHandler, highlight_characters, highlight_contains,
+ highlight_prefix,
};
pub use lsp_types::Position;
pub use mask_pattern::MaskPattern;
diff --git a/third-party/gpui-kit/crates/base/src/lib.rs b/third-party/gpui-kit/crates/base/src/lib.rs
index 0749787ae4..6f960fb494 100644
--- a/third-party/gpui-kit/crates/base/src/lib.rs
+++ b/third-party/gpui-kit/crates/base/src/lib.rs
@@ -9,6 +9,7 @@ extern crate gpui_fast as gpui;
#[cfg(all(feature = "gpui-fast", test))]
extern crate gpui_fast_platform as gpui_platform;
+mod a11y_disabled;
mod accordion;
pub mod actions;
mod alert_dialog;
@@ -46,6 +47,7 @@ mod nav_stack;
mod number_input;
mod observe;
mod otp_input;
+mod overscroll;
mod pagination;
pub mod plot;
mod popover;
diff --git a/third-party/gpui-kit/crates/base/src/link.rs b/third-party/gpui-kit/crates/base/src/link.rs
index bbcfe13804..56aa4de827 100644
--- a/third-party/gpui-kit/crates/base/src/link.rs
+++ b/third-party/gpui-kit/crates/base/src/link.rs
@@ -8,7 +8,7 @@ use gpui::{
};
use smallvec::SmallVec;
-use crate::{StateStyle, StyledExt as _};
+use crate::{StateStyle, StyledExt as _, a11y_disabled::A11yDisabledExt as _};
type ActivationHandler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>;
type OpenHandler = Rc<dyn Fn(&str, &ClickEvent, &mut Window, &mut App)>;
@@ -199,6 +199,7 @@ impl RenderOnce for Link {
})
.children(self.children)
.refine_style(&style)
+ .a11y_disabled(disabled)
}
}
@@ -461,6 +462,7 @@ mod tests {
assert_eq!(disabled.role(), Role::Link);
assert!(!disabled.supports_action(accesskit::Action::Click));
- assert!(!disabled.is_disabled());
+ assert!(!enabled.is_disabled());
+ assert!(disabled.is_disabled());
}
}
diff --git a/third-party/gpui-kit/crates/base/src/motion.rs b/third-party/gpui-kit/crates/base/src/motion.rs
index 3c92f914d5..998ddfe4f5 100644
--- a/third-party/gpui-kit/crates/base/src/motion.rs
+++ b/third-party/gpui-kit/crates/base/src/motion.rs
@@ -331,7 +331,7 @@ where
} else {
1.0
};
- let duration = policy.duration.mul_f32(reversing_factor);
+ let duration = policy.duration.mul_f64(f64::from(reversing_factor));
state.update(cx, |state, _| {
state.from = sampled.clone();
state.target = target.clone();
diff --git a/third-party/gpui-kit/crates/base/src/motion/presence.rs b/third-party/gpui-kit/crates/base/src/motion/presence.rs
index 5522a83d3f..68bce29d0d 100644
--- a/third-party/gpui-kit/crates/base/src/motion/presence.rs
+++ b/third-party/gpui-kit/crates/base/src/motion/presence.rs
@@ -88,7 +88,10 @@ impl Presence {
} else {
1.0
};
- let duration = self.transition.duration.mul_f32(reversing_factor);
+ let duration = self
+ .transition
+ .duration
+ .mul_f64(f64::from(reversing_factor));
state.update(cx, |state, _| {
state.from = sampled;
state.target = target;
diff --git a/third-party/gpui-kit/crates/base/src/number_input.rs b/third-party/gpui-kit/crates/base/src/number_input.rs
index b3c53dc19a..bd3a9bc46b 100644
--- a/third-party/gpui-kit/crates/base/src/number_input.rs
+++ b/third-party/gpui-kit/crates/base/src/number_input.rs
@@ -4,8 +4,8 @@ use std::rc::Rc;
use gpui::Focusable;
use gpui::{
AnyElement, App, Entity, EventEmitter, InteractiveElement as _, IntoElement, KeyBinding,
- ParentElement, RenderOnce, Role, StatefulInteractiveElement as _, StyleRefinement, Styled,
- Window, actions, div, prelude::FluentBuilder as _,
+ ParentElement, RenderOnce, Role, SharedString, StatefulInteractiveElement as _,
+ StyleRefinement, Styled, Window, actions, div, prelude::FluentBuilder as _,
};
use crate::{Button, InputBase, StyledExt as _};
@@ -105,6 +105,7 @@ pub struct NumberInput {
increment_button: Option<ButtonDecorator>,
input: Option<AnyElement>,
controls_right: bool,
+ accessibility_label: Option<SharedString>,
}
/// The built-in text region of a [`NumberInput`].
@@ -166,6 +167,7 @@ impl NumberInput {
increment_button: None,
input: None,
controls_right: false,
+ accessibility_label: None,
}
}
@@ -205,6 +207,15 @@ impl NumberInput {
self.controls_right = true;
self
}
+
+ /// Sets the name exposed to accessibility clients.
+ ///
+ /// A number input has no visible text of its own to be named by, so set
+ /// this unless the input is named some other way.
+ pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
+ self.accessibility_label = Some(label.into());
+ self
+ }
}
impl Styled for NumberInput {
@@ -302,6 +313,9 @@ impl RenderOnce for NumberInput {
.items_center()
.disabled(disabled)
.role(Role::SpinButton)
+ .when_some(self.accessibility_label, |this, label| {
+ this.aria_label(label)
+ })
.when_some(value, |this, value| this.aria_numeric_value(value))
.key_context(CONTEXT)
.on_action({
diff --git a/third-party/gpui-kit/crates/base/src/overscroll.rs b/third-party/gpui-kit/crates/base/src/overscroll.rs
new file mode 100644
index 0000000000..9c9389966f
--- /dev/null
+++ b/third-party/gpui-kit/crates/base/src/overscroll.rs
@@ -0,0 +1,463 @@
+//! Boundary displacement shared by inputs and scroll viewports.
+//!
+//! Viewport owners keep their logical scroll position clamped and apply this
+//! displacement only to content geometry. Inputs drive it directly, while
+//! `ScrollBounce` can own a list or scroll area's vertical wheel handling.
+
+use crate::scroll_bounce::Physics;
+use gpui::TouchPhase;
+use std::time::Duration;
+#[cfg(not(target_family = "wasm"))]
+use std::time::Instant;
+#[cfg(target_family = "wasm")]
+use web_time::Instant;
+
+/// A pause without packets that ends a gesture.
+///
+/// Momentum arrives once per frame with no phase of its own, and some precise
+/// scrolling devices send no phases, so a pause marks the end of their stream.
+/// `ScrollBounce` waits the same time for the same reason.
+const GESTURE_GAP: Duration = Duration::from_millis(250);
+
+/// The fraction of the text's speed the edge carries when the text runs into
+/// it or a finger lets go of it. Measured on `NSScrollView`, and the same as
+/// WebKit's rubber-band amplitude: a fling arriving at 5000 px/s throws the
+/// edge about 45 px out, 80 ms before it turns back.
+const CARRY: f32 = 0.31;
+
+/// A finger that sends nothing for this long before lifting has stopped:
+/// it lets go of the edge with no speed.
+const STILL: Duration = Duration::from_millis(50);
+
+/// The interval assumed for the first packet of a stream.
+const FRAME: f32 = 1. / 60.;
+
+/// How far back the stream's speed is read. Packets carry no timestamp, so a
+/// frame that takes long hands over several at once; read over a span, their
+/// distance still spreads over the time they took, instead of each one
+/// arriving at once and reading as a huge speed. That happens most on a fling
+/// through a long list, just as it runs into the top.
+const SPEED_SPAN: Duration = Duration::from_millis(50);
+
+#[derive(Default)]
+pub(crate) struct Overscroll {
+ physics: Physics,
+ sampled_at: Option<Instant>,
+ last_packet_at: Option<Instant>,
+ /// When the stream last moved, and how fast, in pixels per second.
+ moved_at: Option<Instant>,
+ velocity: f32,
+ /// The packets that moved within the last `SPEED_SPAN`, and the one
+ /// before them, which marks where the span starts.
+ moves: Vec<(Instant, f32)>,
+ /// A finger rests on the trackpad but has not moved since it touched.
+ resting: bool,
+ /// The edge already answered the rest of this gesture: a fling ran into
+ /// it, or a finger let go of it stretched. The momentum that follows
+ /// still scrolls the text back from the edge, but no longer moves the
+ /// edge.
+ spent: bool,
+}
+
+impl Overscroll {
+ pub(crate) fn set_motion(&mut self, motion: crate::ScrollBounceMotion) {
+ self.physics.set_motion(motion);
+ }
+ /// How far the text is drawn from where it scrolled to.
+ pub(crate) fn offset(&self) -> f32 {
+ self.physics.offset()
+ }
+
+ /// A new gesture starts: it may stretch the edge again.
+ fn start_gesture(&mut self) {
+ self.spent = false;
+ self.velocity = 0.;
+ self.moves.clear();
+ }
+
+ /// Track the stream's speed from packets that moved: the distance they
+ /// covered over the time since the span started.
+ fn sample(&mut self, dy: f32, now: Instant) {
+ if dy == 0. {
+ return;
+ }
+ if self.moves.last().is_some_and(|&(_, last)| last * dy < 0.) {
+ self.moves.clear();
+ }
+ self.moves.push((now, dy));
+ let start = self
+ .moves
+ .iter()
+ .rposition(|&(at, _)| now.saturating_duration_since(at) > SPEED_SPAN)
+ .unwrap_or(0);
+ self.moves.drain(..start);
+ let (first, _) = self.moves[0];
+ let seconds = now.saturating_duration_since(first).as_secs_f32();
+ self.velocity = if seconds > 0. && self.moves.len() > 1 {
+ self.moves[1..].iter().map(|&(_, dy)| dy).sum::<f32>() / seconds
+ } else {
+ // The first packet, or several handed over at once.
+ self.moves.iter().map(|&(_, dy)| dy).sum::<f32>() / FRAME
+ };
+ self.moved_at = Some(now);
+ }
+
+ /// Handle one vertical packet of `dy` over a viewport `extent` tall.
+ /// `scroll` moves the text by a delta and returns how much of it the text
+ /// used. Returns whether the stretch changed.
+ pub(crate) fn packet(
+ &mut self,
+ dy: f32,
+ phase: TouchPhase,
+ extent: f32,
+ now: Instant,
+ mut scroll: impl FnMut(f32) -> f32,
+ ) -> bool {
+ let before = self.offset();
+ let mut thrown = false;
+ let gap = self
+ .last_packet_at
+ .replace(now)
+ .map(|at| now.saturating_duration_since(at))
+ .filter(|gap| *gap < GESTURE_GAP);
+ // A finger resting on the trackpad sends nothing, however long it
+ // holds the edge; only its own `Ended` lets go. A pause ends the
+ // other streams.
+ if gap.is_none() && !self.physics.is_dragging() {
+ self.start_gesture();
+ self.moved_at = None;
+ }
+ self.sample(dy, now);
+ if phase == TouchPhase::Started {
+ // Grab the edge, a returning one included, where it is.
+ self.physics.begin(extent);
+ self.spent = false;
+ self.resting = dy == 0.;
+ } else if dy != 0. {
+ self.resting = false;
+ } else if self.resting && self.physics.is_dragging() {
+ // A finger that touches and lifts without moving cancels its
+ // gesture, and GPUI reports the cancel as a move of nothing: it
+ // lets go of the edge where it held it.
+ self.resting = false;
+ self.physics.let_go(0.);
+ self.spent = self.offset() != 0.;
+ if self.spent {
+ self.sampled_at = Some(now);
+ }
+ return self.physics.is_moving();
+ }
+ if self.physics.is_dragging() {
+ // A stretched edge takes the packet first; only what crosses back
+ // over the edge scrolls the text.
+ let rest = if self.offset() != 0. {
+ self.physics.pull(dy)
+ } else {
+ dy
+ };
+ if rest != 0. {
+ let unused = rest - scroll(rest);
+ if unused != 0. {
+ self.physics.pull(unused);
+ }
+ }
+ if matches!(phase, TouchPhase::Ended | TouchPhase::Cancelled) {
+ let moving = self
+ .moved_at
+ .is_some_and(|at| now.saturating_duration_since(at) < STILL);
+ let velocity = if moving { self.velocity } else { 0. };
+ self.physics.let_go(velocity * CARRY);
+ thrown = true;
+ self.spent = self.offset() != 0.;
+ }
+ } else if self.spent {
+ // Momentum after the edge answered: back into the text it
+ // scrolls; against the edge it has nothing left to move.
+ scroll(dy);
+ } else {
+ // Momentum or phaseless precise scrolling: it scrolls the text,
+ // and when the text runs into the edge it throws the edge as
+ // fast as it arrived, once.
+ let unused = dy - scroll(dy);
+ if unused != 0. {
+ self.physics.let_go(self.velocity * CARRY);
+ thrown = true;
+ self.spent = true;
+ }
+ }
+ let changed = (thrown && self.physics.is_moving()) || self.offset() != before;
+ // Start the clock when the edge starts moving, and again when it is
+ // let go, however long it was held; a packet that arrives while it
+ // moves must not lose the time since the last frame.
+ if thrown || (changed && self.sampled_at.is_none()) {
+ self.sampled_at = Some(now);
+ }
+ changed
+ }
+
+ /// Advance the return to `now`. Returns whether it is still moving.
+ pub(crate) fn step(&mut self, now: Instant) -> bool {
+ let elapsed = self
+ .sampled_at
+ .replace(now)
+ .map_or(0., |at| now.saturating_duration_since(at).as_secs_f32());
+ let moving = self.physics.step(elapsed);
+ if !moving {
+ self.sampled_at = None;
+ }
+ moving
+ }
+
+ /// Drop any stretch at once, as when bouncing is turned off.
+ pub(crate) fn reset(&mut self) {
+ let mut physics = std::mem::take(&mut self.physics);
+ physics.reset();
+ *self = Self {
+ physics,
+ ..Self::default()
+ };
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::time::Duration;
+
+ /// Text that can scroll `room` further toward its end.
+ fn text(room: &mut f32) -> impl FnMut(f32) -> f32 + '_ {
+ move |dy: f32| {
+ let used = dy.max(-*room);
+ *room += used;
+ used
+ }
+ }
+
+ #[test]
+ fn a_swipe_past_the_end_stretches_and_springs_back() {
+ let now = Instant::now();
+ let mut overscroll = Overscroll::default();
+ let mut room = 30.;
+ overscroll.packet(-10., TouchPhase::Started, 400., now, text(&mut room));
+ overscroll.packet(-100., TouchPhase::Moved, 400., now, text(&mut room));
+ // The text used the 30px it had left; the rest stretches the edge.
+ assert_eq!(room, 0.);
+ let stretched = overscroll.offset();
+ assert!(stretched < 0. && stretched > -80., "{stretched}");
+
+ overscroll.packet(0., TouchPhase::Ended, 400., now, text(&mut room));
+ // Momentum after the release is answered by the spring alone.
+ assert!(!overscroll.packet(-40., TouchPhase::Moved, 400., now, text(&mut room)));
+ let mut later = now;
+ while overscroll.step(later) {
+ later += Duration::from_millis(16);
+ }
+ assert_eq!(overscroll.offset(), 0.);
+ }
+
+ #[test]
+ fn pulling_back_unstretches_before_scrolling_the_text() {
+ let now = Instant::now();
+ let mut overscroll = Overscroll::default();
+ let mut room = 0.;
+ overscroll.packet(0., TouchPhase::Started, 400., now, text(&mut room));
+ overscroll.packet(-50., TouchPhase::Moved, 400., now, text(&mut room));
+ assert!(overscroll.offset() < 0.);
+ // Back the other way: the first 50px return the edge, the next 20px
+ // scroll the text back from its end.
+ let mut scrolled = 0.;
+ overscroll.packet(70., TouchPhase::Moved, 400., now, |dy| {
+ scrolled += dy;
+ dy
+ });
+ assert_eq!(overscroll.offset(), 0.);
+ assert_eq!(scrolled, 20.);
+ }
+
+ /// A momentum stream of `speed` px/s decaying as AppKit's does, against
+ /// text already at its end. Returns the edge's offset at each frame.
+ fn fling(speed: f32) -> Vec<f32> {
+ fling_through(speed, 0., 0)
+ }
+
+ /// The same stream through `room` px of text first, with every packet
+ /// from frame `stall` to the impact handed over in one go, as when the
+ /// frames that scroll the text take long to draw.
+ fn fling_through(speed: f32, mut room: f32, stall: u32) -> Vec<f32> {
+ let start = Instant::now();
+ let frame = Duration::from_micros(16_667);
+ let mut overscroll = Overscroll::default();
+ let mut dy = -speed / 60.;
+ let mut held = Vec::new();
+ let mut path = Vec::new();
+ for n in 0..120 {
+ let at = start + frame * n;
+ // Momentum keeps arriving well after the edge has settled.
+ if n < 100 {
+ held.push(dy);
+ dy *= 0.95;
+ if stall == 0 || n < stall || room == 0. || -held.iter().sum::<f32>() > room {
+ for dy in held.drain(..) {
+ overscroll.packet(dy, TouchPhase::Moved, 300., at, text(&mut room));
+ }
+ }
+ }
+ overscroll.step(at);
+ path.push(overscroll.offset());
+ }
+ path
+ }
+
+ #[cfg(not(any(target_os = "ios", target_os = "android")))]
+ #[test]
+ fn a_fling_throws_the_edge_as_far_as_nsscrollview_does() {
+ // Measured on NSScrollView: a fling arriving at 1500 px/s peaks about
+ // 14 px out, roughly 80 ms after impact, and settles within 0.65 s.
+ let path = fling(1500.);
+ let peak = path.iter().copied().fold(0., f32::min);
+ assert!((-16. ..=-12.).contains(&peak), "peak {peak}");
+ let turned = path.iter().position(|&offset| offset == peak).unwrap();
+ assert!((4..=6).contains(&turned), "turned at frame {turned}");
+ assert_eq!(path[50], 0., "the edge settles");
+ // The momentum still arriving after impact never throws it again.
+ assert!(path[turned..].windows(2).all(|pair| pair[1] >= pair[0]));
+ assert!(path[50..].iter().all(|&offset| offset == 0.));
+ // Twice the speed throws it about twice as far.
+ let faster = fling(3000.).iter().copied().fold(0., f32::min);
+ assert!((faster / peak - 2.).abs() < 0.15, "{faster} vs {peak}");
+ // But no fling throws it past the limit, as none does in Finder.
+ let hardest = fling(20_000.).iter().copied().fold(0., f32::min);
+ assert!((-36. ..=-34.).contains(&hardest), "{hardest}");
+ }
+
+ #[cfg(not(any(target_os = "ios", target_os = "android")))]
+ #[test]
+ fn packets_handed_over_at_once_do_not_throw_the_edge_further() {
+ let peak = |path: Vec<f32>| path.iter().copied().fold(0., f32::min);
+ let even = peak(fling_through(4000., 500., 0));
+ let stalled = peak(fling_through(4000., 500., 3));
+ assert!(even < -10., "{even}");
+ // Read per packet, the burst threw it out to the limit. Read over
+ // the span, only the longer span's slower decay shows.
+ assert!((stalled / even - 1.).abs() < 0.25, "{stalled} vs {even}");
+ }
+
+ #[test]
+ fn a_touch_that_lifts_without_moving_lets_go_of_the_edge() {
+ let start = Instant::now();
+ let mut overscroll = Overscroll::default();
+ let mut room = 0.;
+ overscroll.packet(-80., TouchPhase::Moved, 400., start, text(&mut room));
+ let later = start + Duration::from_millis(30);
+ overscroll.step(later);
+ let held = overscroll.offset();
+ // Touch, then lift: GPUI reports `MayBegin` as `Started` and the
+ // cancel as a `Moved` of nothing.
+ overscroll.packet(0., TouchPhase::Started, 400., later, text(&mut room));
+ assert!(overscroll.packet(0., TouchPhase::Moved, 400., later, text(&mut room)));
+ let mut at = later;
+ while overscroll.step(at) {
+ at += Duration::from_millis(16);
+ }
+ assert!(held < 0.);
+ assert_eq!(overscroll.offset(), 0.);
+ }
+
+ #[cfg(not(any(target_os = "ios", target_os = "android")))]
+ #[test]
+ fn a_stretch_follows_the_travel_as_nsscrollview_does() {
+ // Measured on a 300 px NSScrollView: 100 px of travel past the end
+ // stretches it 7 px. Further on, AppKit reads unaccelerated travel
+ // and Finder never goes past 36 px; the stretch eases into that.
+ let stretch = |travel: f32| {
+ let start = Instant::now();
+ let mut overscroll = Overscroll::default();
+ let mut room = 0.;
+ overscroll.packet(0., TouchPhase::Started, 300., start, text(&mut room));
+ overscroll.packet(-travel, TouchPhase::Moved, 300., start, text(&mut room));
+ -overscroll.offset()
+ };
+ assert!((6.5..=8.).contains(&stretch(100.)), "{}", stretch(100.));
+ assert!((28. ..=34.).contains(&stretch(764.)), "{}", stretch(764.));
+ assert!((35. ..36.).contains(&stretch(2400.)), "{}", stretch(2400.));
+ assert!(stretch(100_000.) < 36.);
+ }
+
+ #[cfg(not(any(target_os = "ios", target_os = "android")))]
+ #[test]
+ fn a_still_release_returns_at_full_speed() {
+ // NSScrollView lets go of a stretch held still and halves it in
+ // 55 ms, starting at once rather than easing out.
+ let start = Instant::now();
+ let mut overscroll = Overscroll::default();
+ let mut room = 0.;
+ overscroll.packet(0., TouchPhase::Started, 300., start, text(&mut room));
+ overscroll.packet(-800., TouchPhase::Moved, 300., start, text(&mut room));
+ let held = overscroll.offset();
+ let lifted = start + Duration::from_millis(200);
+ overscroll.packet(0., TouchPhase::Ended, 300., lifted, text(&mut room));
+ overscroll.step(lifted + Duration::from_millis(8));
+ assert!(overscroll.offset() / held < 0.92, "starts at once");
+ overscroll.step(lifted + Duration::from_millis(55));
+ let half = overscroll.offset() / held;
+ assert!((0.45..=0.55).contains(&half), "{half}");
+ }
+
+ #[test]
+ fn after_a_pause_the_next_flick_throws_the_edge_again() {
+ let start = Instant::now();
+ let mut overscroll = Overscroll::default();
+ let mut room = 0.;
+ assert!(overscroll.packet(-40., TouchPhase::Moved, 400., start, text(&mut room)));
+ let mut at = start;
+ while overscroll.step(at) {
+ at += Duration::from_millis(16);
+ }
+ let later = at + GESTURE_GAP;
+ assert!(overscroll.packet(-40., TouchPhase::Moved, 400., later, text(&mut room)));
+ }
+
+ #[test]
+ fn momentum_back_into_the_text_scrolls_while_the_edge_returns() {
+ let start = Instant::now();
+ let mut overscroll = Overscroll::default();
+ let mut room = 0.;
+ overscroll.packet(0., TouchPhase::Started, 400., start, text(&mut room));
+ overscroll.packet(-800., TouchPhase::Moved, 400., start, text(&mut room));
+ let lifted = start + Duration::from_millis(100);
+ overscroll.packet(0., TouchPhase::Ended, 400., lifted, text(&mut room));
+ let later = lifted + Duration::from_millis(16);
+ overscroll.step(later);
+ let before = overscroll.offset();
+ assert!(before < 0.);
+ overscroll.packet(20., TouchPhase::Moved, 400., later, text(&mut room));
+ assert_eq!(room, 20.);
+ assert_eq!(overscroll.offset(), before, "the edge does not jump");
+ overscroll.step(later + Duration::from_millis(16));
+ assert!(overscroll.offset() > before && overscroll.offset() < 0.);
+ }
+
+ #[test]
+ fn a_new_precise_gesture_takes_over_without_jumping() {
+ let start = Instant::now();
+ let mut overscroll = Overscroll::default();
+ let mut room = 0.;
+ overscroll.packet(-80., TouchPhase::Moved, 400., start, text(&mut room));
+ overscroll.step(start + Duration::from_millis(16));
+ let later = start + Duration::from_millis(200);
+ overscroll.step(later);
+ assert!(overscroll.offset() < 0.);
+ let before = overscroll.offset();
+ overscroll.packet(0., TouchPhase::Started, 400., later, text(&mut room));
+ assert!(
+ (overscroll.offset() - before).abs() < 0.001,
+ "interruption does not jump"
+ );
+ assert!(overscroll.packet(-40., TouchPhase::Moved, 400., later, text(&mut room)));
+ overscroll.step(later + Duration::from_millis(16));
+ assert!(
+ overscroll.offset() < before,
+ "the new input pushes outward again"
+ );
+ }
+}
diff --git a/third-party/gpui-kit/crates/base/src/popover.rs b/third-party/gpui-kit/crates/base/src/popover.rs
index 1d814e78dc..8a5cdfac1b 100644
--- a/third-party/gpui-kit/crates/base/src/popover.rs
+++ b/third-party/gpui-kit/crates/base/src/popover.rs
@@ -13,12 +13,19 @@ use crate::{
};
const CONTEXT: &str = "Popover";
+const CONTENT_CONTEXT: &str = "PopoverContent";
pub(crate) fn init(cx: &mut App) {
cx.bind_keys([
KeyBinding::new("escape", Cancel, Some(CONTEXT)),
KeyBinding::new("enter", Confirm { secondary: false }, Some(CONTEXT)),
KeyBinding::new("space", Confirm { secondary: false }, Some(CONTEXT)),
+ KeyBinding::new("escape", Cancel, Some(CONTENT_CONTEXT)),
+ // The content is still a descendant of the trigger's dispatch node.
+ // Leave activation to its controls, not the trigger or an outer dialog.
+ // More deeply scoped input bindings retain precedence.
+ KeyBinding::new("enter", gpui::NoAction, Some(CONTENT_CONTEXT)),
+ KeyBinding::new("space", gpui::NoAction, Some(CONTENT_CONTEXT)),
]);
}
@@ -168,6 +175,10 @@ type ContentBuilder =
Box<dyn FnOnce(&mut PopoverState, &mut Window, &mut Context<PopoverState>) -> AnyElement>;
/// An unstyled popover owning trigger interaction, open state, dismissal, and focus.
+///
+/// Enter and Space on the trigger toggle the popover. Inside its content, those
+/// keys belong to the focused control instead; Escape dismisses the popover and
+/// restores the previous focus.
#[derive(IntoElement)]
pub struct Popover {
id: ElementId,
@@ -357,7 +368,7 @@ impl RenderOnce for Popover {
.occlude()
.tab_group()
.track_focus(&focus_handle)
- .key_context(CONTEXT)
+ .key_context(CONTENT_CONTEXT)
.on_action(window.listener_for(&state, PopoverState::on_action_cancel))
.when_some(self.content, |this, content| {
this.child(state.update(cx, |state, cx| (content)(state, window, cx)))
diff --git a/third-party/gpui-kit/crates/base/src/radio.rs b/third-party/gpui-kit/crates/base/src/radio.rs
index 1940f655c0..dc9424dbc6 100644
--- a/third-party/gpui-kit/crates/base/src/radio.rs
+++ b/third-party/gpui-kit/crates/base/src/radio.rs
@@ -8,7 +8,7 @@ use gpui::{
};
use smallvec::SmallVec;
-use crate::{StateStyle, StyledExt as _, TestSupportExt as _};
+use crate::{StateStyle, StyledExt as _, TestSupportExt as _, a11y_disabled::A11yDisabledExt as _};
type ChangeHandler = Rc<dyn Fn(bool, &ClickEvent, &mut Window, &mut App)>;
@@ -234,6 +234,7 @@ impl RenderOnce for Radio {
)
.children(self.children)
.refine_style(&style)
+ .a11y_disabled(disabled)
}
}
diff --git a/third-party/gpui-kit/crates/base/src/root.rs b/third-party/gpui-kit/crates/base/src/root.rs
index cee3940567..35d190c38b 100644
--- a/third-party/gpui-kit/crates/base/src/root.rs
+++ b/third-party/gpui-kit/crates/base/src/root.rs
@@ -7,6 +7,29 @@ use gpui::{
};
use std::{any::TypeId, rc::Rc};
+/// Scan at most one cycle of the rendered tab order, preserving containment
+/// regardless of how many controls exist behind a modal. GPUI exposes traversal
+/// but not candidate enumeration. This scan is synchronous: only the final
+/// focus path is published at the next draw, not the background candidates.
+fn advance_focus(window: &mut Window, cx: &mut App, advance: fn(&mut Window, &mut App)) {
+ let Some(container) = crate::active_focus_trap(window, cx) else {
+ advance(window, cx);
+ return;
+ };
+
+ advance(window, cx);
+ let first_candidate = window.focused(cx);
+ while !container.contains_focused(window, cx) {
+ advance(window, cx);
+ if window.focused(cx) == first_candidate {
+ // The trap has no rendered tab stops. Its own handle remains a
+ // safe focus target even when excluded from ordinary tab order.
+ container.focus(window, cx);
+ break;
+ }
+ }
+}
+
/// A presentation layer's retained, per-window facilities.
///
/// Register during explicit application initialization, before creating windows.
@@ -252,73 +275,11 @@ impl Root {
}
fn focus_next(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- // Check if we're inside a focus trap
- if let Some(container_focus_handle) = crate::active_focus_trap(window, cx) {
- // We're in a focus trap - try to focus next, then check if we're still inside
- let before_focus = window.focused(cx);
-
- // Try normal focus navigation
- window.focus_next(cx);
-
- // Check if we're still in the trap
- if !container_focus_handle.contains_focused(window, cx) {
- // We jumped out of the trap - need to cycle back to the beginning
- // Find the first focusable element in the trap by continuing to focus_next
- let mut attempts = 0;
- const MAX_ATTEMPTS: usize = 100; // Prevent infinite loop
-
- while !container_focus_handle.contains_focused(window, cx)
- && attempts < MAX_ATTEMPTS
- {
- window.focus_next(cx);
- attempts += 1;
-
- // If we cycled back to where we started, restore original focus
- if window.focused(cx) == before_focus {
- break;
- }
- }
- }
- return;
- }
-
- // Normal tab navigation
- window.focus_next(cx);
+ advance_focus(window, cx, Window::focus_next);
}
fn focus_prev(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- // Check if we're inside a focus trap
- if let Some(container_focus_handle) = crate::active_focus_trap(window, cx) {
- // We're in a focus trap - try to focus previous, then check if we're still inside
- let before_focus = window.focused(cx);
-
- // Try normal focus navigation
- window.focus_prev(cx);
-
- // Check if we're still in the trap
- if !container_focus_handle.contains_focused(window, cx) {
- // We jumped out of the trap - need to cycle back to the end
- // Find the last focusable element in the trap by continuing to focus_prev
- let mut attempts = 0;
- const MAX_ATTEMPTS: usize = 100; // Prevent infinite loop
-
- while !container_focus_handle.contains_focused(window, cx)
- && attempts < MAX_ATTEMPTS
- {
- window.focus_prev(cx);
- attempts += 1;
-
- // If we cycled back to where we started, restore original focus
- if window.focused(cx) == before_focus {
- break;
- }
- }
- }
- return;
- }
-
- // Normal tab navigation
- window.focus_prev(cx);
+ advance_focus(window, cx, Window::focus_prev);
}
fn copy_selection(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
@@ -371,7 +332,183 @@ impl Render for Root {
#[cfg(test)]
mod tests {
use super::*;
- use gpui::TestAppContext;
+ use crate::FocusTrapElement as _;
+ use gpui::{FocusHandle, Subscription, TestAppContext, VisualTestContext, px};
+ use std::cell::Cell;
+
+ struct TrapContent {
+ background: Vec<FocusHandle>,
+ container: FocusHandle,
+ controls: Vec<FocusHandle>,
+ _focus_subscriptions: Vec<Subscription>,
+ }
+
+ impl Render for TrapContent {
+ fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+ div()
+ .tab_group()
+ .size_full()
+ .children(self.background.iter().enumerate().map(|(index, handle)| {
+ div()
+ .id(("background", index))
+ .track_focus(handle)
+ .tab_index(0)
+ .size(px(1.))
+ }))
+ .child(
+ div()
+ .id("trap")
+ .track_focus(&self.container)
+ .tab_stop(false)
+ .children(self.controls.iter().enumerate().map(|(index, handle)| {
+ div()
+ .id(("control", index))
+ .track_focus(handle)
+ .tab_index(0)
+ .size(px(1.))
+ }))
+ .focus_trap("modal", &self.container),
+ )
+ }
+ }
+
+ fn trap_harness(
+ cx: &mut TestAppContext,
+ control_count: usize,
+ ) -> (
+ &mut VisualTestContext,
+ FocusHandle,
+ Vec<FocusHandle>,
+ Vec<FocusHandle>,
+ Rc<Cell<usize>>,
+ ) {
+ cx.update(crate::init);
+ let (container, controls, background) = cx.update(|cx| {
+ (
+ cx.focus_handle(),
+ (0..control_count)
+ .map(|_| cx.focus_handle().tab_stop(true))
+ .collect::<Vec<_>>(),
+ // Regression: traversing past the modal must cross more than
+ // the former 100-attempt limit before reaching its other end.
+ (0..256)
+ .map(|_| cx.focus_handle().tab_stop(true))
+ .collect::<Vec<_>>(),
+ )
+ });
+ let background_focuses = Rc::new(Cell::new(0));
+ let (_, cx) = cx.add_window_view({
+ let container = container.clone();
+ let controls = controls.clone();
+ let background = background.clone();
+ let background_focuses = background_focuses.clone();
+ move |window, cx| {
+ let content = cx.new(|cx| {
+ let subscriptions = background
+ .iter()
+ .map(|handle| {
+ let focuses = background_focuses.clone();
+ cx.on_focus(handle, window, move |_, _, _| {
+ focuses.set(focuses.get() + 1)
+ })
+ })
+ .collect();
+ TrapContent {
+ background,
+ container,
+ controls,
+ _focus_subscriptions: subscriptions,
+ }
+ });
+ Root::new(content, window, cx)
+ }
+ });
+ cx.update(|window, _| window.activate_window());
+ cx.executor().run_until_parked();
+ cx.update(|window, cx| window.draw(cx).clear(cx));
+ (cx, container, controls, background, background_focuses)
+ }
+
+ fn assert_trapped_focus(
+ cx: &mut VisualTestContext,
+ container: &FocusHandle,
+ expected: &FocusHandle,
+ ) {
+ cx.update(|window, cx| {
+ window.draw(cx).clear(cx);
+ assert_eq!(window.focused(cx).as_ref(), Some(expected));
+ assert!(container.contains_focused(window, cx));
+ });
+ }
+
+ #[gpui::test]
+ fn focus_trap_wraps_forward_past_more_than_100_background_stops(cx: &mut TestAppContext) {
+ let (cx, container, controls, _, background_focuses) = trap_harness(cx, 2);
+ cx.update(|window, cx| {
+ controls[1].focus(window, cx);
+ window.draw(cx).clear(cx);
+ });
+
+ cx.dispatch_keystrokes("tab");
+ assert_trapped_focus(cx, &container, &controls[0]);
+ cx.dispatch_keystrokes("tab");
+ assert_trapped_focus(cx, &container, &controls[1]);
+ assert_eq!(background_focuses.get(), 0);
+ }
+
+ #[gpui::test]
+ fn focus_trap_wraps_backward_past_more_than_100_background_stops(cx: &mut TestAppContext) {
+ let (cx, container, controls, _, background_focuses) = trap_harness(cx, 2);
+ cx.update(|window, cx| {
+ controls[0].focus(window, cx);
+ window.draw(cx).clear(cx);
+ });
+
+ cx.dispatch_keystrokes("shift-tab");
+ assert_trapped_focus(cx, &container, &controls[1]);
+ cx.dispatch_keystrokes("shift-tab");
+ assert_trapped_focus(cx, &container, &controls[0]);
+ assert_eq!(background_focuses.get(), 0);
+ }
+
+ #[gpui::test]
+ fn focus_trap_without_tab_stops_keeps_its_container_focused(cx: &mut TestAppContext) {
+ let (cx, container, _, _, background_focuses) = trap_harness(cx, 0);
+ cx.update(|window, cx| {
+ container.focus(window, cx);
+ window.draw(cx).clear(cx);
+ });
+
+ cx.dispatch_keystrokes("tab");
+ assert_trapped_focus(cx, &container, &container);
+ cx.dispatch_keystrokes("shift-tab");
+ assert_trapped_focus(cx, &container, &container);
+ assert_eq!(background_focuses.get(), 0);
+ }
+
+ #[gpui::test]
+ fn inactive_focus_trap_does_not_capture_background_navigation(cx: &mut TestAppContext) {
+ let (cx, _, _, background, background_focuses) = trap_harness(cx, 2);
+ cx.update(|window, cx| {
+ background[0].focus(window, cx);
+ window.draw(cx).clear(cx);
+ });
+ // Prove that the observers used by the containment regressions really
+ // receive native focus notifications when a background control is used.
+ assert_eq!(background_focuses.get(), 1);
+
+ cx.dispatch_keystrokes("tab");
+ cx.update(|window, cx| {
+ window.draw(cx).clear(cx);
+ assert_eq!(window.focused(cx), Some(background[1].clone()));
+ });
+ cx.dispatch_keystrokes("shift-tab");
+ cx.update(|window, cx| {
+ window.draw(cx).clear(cx);
+ assert_eq!(window.focused(cx), Some(background[0].clone()));
+ });
+ assert_eq!(background_focuses.get(), 3);
+ }
struct Content;
impl Render for Content {
@@ -578,6 +715,91 @@ mod tests {
}
assert_ne!(ids[0], ids[1]);
}
+
+ mod nested_traps {
+ use super::*;
+
+ struct NestedTraps {
+ containers: Vec<FocusHandle>,
+ controls: Vec<[FocusHandle; 2]>,
+ depth: usize,
+ }
+
+ impl Render for NestedTraps {
+ fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+ let mut child = div().into_any_element();
+ for index in (0..self.depth).rev() {
+ let control = |side: usize| {
+ div()
+ .id(("nested-control", index * 2 + side))
+ .track_focus(&self.controls[index][side])
+ .tab_index(0)
+ .size(px(10.))
+ };
+ child = div()
+ .tab_stop(false)
+ .child(control(0))
+ .child(child)
+ .child(control(1))
+ .focus_trap(("nested-trap", index), &self.containers[index])
+ .into_any_element();
+ }
+ div().tab_group().size_full().child(child)
+ }
+ }
+
+ #[gpui::test]
+ fn nested_traps_keep_the_innermost_scope_across_close_and_reopen(cx: &mut TestAppContext) {
+ cx.update(crate::init);
+ let (containers, controls) = cx.update(|cx| {
+ (
+ (0..6).map(|_| cx.focus_handle()).collect::<Vec<_>>(),
+ (0..6)
+ .map(|_| {
+ [
+ cx.focus_handle().tab_stop(true),
+ cx.focus_handle().tab_stop(true),
+ ]
+ })
+ .collect::<Vec<_>>(),
+ )
+ });
+ let (root, cx) = cx.add_window_view({
+ let containers = containers.clone();
+ let controls = controls.clone();
+ move |window, cx| {
+ let view = cx.new(|_| NestedTraps {
+ containers,
+ controls,
+ depth: 6,
+ });
+ Root::new(view, window, cx)
+ }
+ });
+ let view = root.read_with(cx, |root, _| {
+ root.view.clone().downcast::<NestedTraps>().unwrap()
+ });
+
+ // Retain every container handle while inner scopes disappear and
+ // return. Rendered containment, not registration order or ownership
+ // lifetime, must decide which remaining scope handles Tab.
+ for depth in [6, 5, 4, 3, 2, 1, 2, 3, 4, 5, 6] {
+ view.update(cx, |view, cx| {
+ view.depth = depth;
+ cx.notify();
+ });
+ let index = depth - 1;
+ cx.update(|window, cx| {
+ controls[index][1].focus(window, cx);
+ window.draw(cx).clear(cx);
+ });
+ cx.dispatch_keystrokes("tab");
+ assert_trapped_focus(cx, &containers[index], &controls[index][0]);
+ cx.dispatch_keystrokes("shift-tab");
+ assert_trapped_focus(cx, &containers[index], &controls[index][1]);
+ }
+ }
+ }
}
#[cfg(test)]
diff --git a/third-party/gpui-kit/crates/base/src/scroll_bounce.rs b/third-party/gpui-kit/crates/base/src/scroll_bounce.rs
index 78ff285a05..77a2184b1f 100644
--- a/third-party/gpui-kit/crates/base/src/scroll_bounce.rs
+++ b/third-party/gpui-kit/crates/base/src/scroll_bounce.rs
@@ -1,10 +1,11 @@
//! Boundary displacement only: the list keeps its clamped logical position.
+use crate::overscroll::Overscroll;
use crate::{OngoingScrollExt as _, ScrollbarHandle};
use gpui::{
- AnyElement, App, Bounds, ContentMask, DispatchPhase, Element, ElementId, GlobalElementId,
- Hitbox, HitboxBehavior, InspectorElementId, IntoElement, LayoutId, OngoingScroll, Pixels,
- ScrollDelta, ScrollWheelEvent, TouchPhase, Window, point, px,
+ AnyElement, App, Bounds, ContentMask, DispatchPhase, Element, ElementId, Global,
+ GlobalElementId, Hitbox, HitboxBehavior, HitboxId, InspectorElementId, IntoElement, LayoutId,
+ OngoingScroll, Pixels, ScrollDelta, ScrollWheelEvent, TouchPhase, Window, point, px,
};
#[cfg(not(target_family = "wasm"))]
use std::time::Instant;
@@ -16,19 +17,38 @@ use web_time::Instant;
/// past an edge, and how quickly a released edge returns.
///
/// Base plays the stretch and the return; the feel belongs to the caller.
-/// The default is tuned to feel like a `UIScrollView` bounce.
+/// The default is tuned to feel like a `UIScrollView` bounce on iOS and
+/// Android, and like an `NSScrollView` bounce elsewhere.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ScrollBounceMotion {
tracking: f32,
response: Duration,
+ max_stretch: Option<f32>,
}
impl Default for ScrollBounceMotion {
- /// Tuned to feel like a `UIScrollView` bounce; these are not UIKit constants.
+ /// Tuned to feel like a `UIScrollView` bounce on touch platforms; these
+ /// are not UIKit constants. Elsewhere the input is a trackpad, whose
+ /// accelerated deltas run far past a finger's travel, so the default is
+ /// fitted to an `NSScrollView` measured on macOS: the stretch follows
+ /// about 0.075 of the travel, the return decays at 12.5 per second, and
+ /// the edge stops 36 px out. AppKit stretches by the unaccelerated
+ /// travel, which GPUI does not report; the limit keeps a hard swipe as
+ /// short as Finder's.
fn default() -> Self {
- Self {
- tracking: 0.55,
- response: Duration::from_millis(524),
+ if cfg!(any(target_os = "ios", target_os = "android")) {
+ Self {
+ tracking: 0.55,
+ response: Duration::from_millis(524),
+ max_stretch: None,
+ }
+ } else {
+ Self {
+ tracking: 0.075,
+ // 2π / 12.5
+ response: Duration::from_micros(502_655),
+ max_stretch: Some(36.),
+ }
}
}
}
@@ -64,6 +84,22 @@ impl ScrollBounceMotion {
self
}
+ /// The farthest the edge goes past its end, in pixels, whether stretched
+ /// or thrown by a fling. The stretch eases into it rather than stopping
+ /// at it.
+ ///
+ /// # Panics
+ ///
+ /// Panics when `max_stretch` is not finite or not positive.
+ pub fn with_max_stretch(mut self, max_stretch: f32) -> Self {
+ assert!(
+ max_stretch.is_finite() && max_stretch > 0.,
+ "scroll bounce max stretch must be finite and positive"
+ );
+ self.max_stretch = Some(max_stretch);
+ self
+ }
+
/// Fraction of finger travel the stretched edge follows at first.
pub fn tracking(&self) -> f32 {
self.tracking
@@ -74,6 +110,12 @@ impl ScrollBounceMotion {
self.response
}
+ /// The farthest the edge goes past its end, or `None` when only the
+ /// viewport height bounds it.
+ pub fn max_stretch(&self) -> Option<f32> {
+ self.max_stretch
+ }
+
/// Undamped angular frequency of the return, or `None` when it snaps.
fn omega(&self) -> Option<f32> {
let seconds = self.response.as_secs_f32();
@@ -88,15 +130,22 @@ impl ScrollBounceMotion {
/// The stable `id` owns gesture and spring state. Change it when replacing the
/// document. Put fixed chrome (scrollbars, toolbars) outside this wrapper.
///
-/// Enabled by default on iOS and Android. Other platforms pass through unless
-/// explicitly enabled; their input must emit `Ended` at finger release, before momentum.
-/// Reduced motion disables displacement. Keyboard, focus, and line-wheel input
-/// remain owned by the child. No colors, padding, or dimensions are imposed.
+/// Enabled by default where the platform's own scroll views bounce: iOS,
+/// Android and macOS. Reduced motion disables displacement. Keyboard and
+/// focus remain owned by the child. No colors, padding, or dimensions are
+/// imposed.
+///
+/// Nested scroll areas follow the platform: the innermost one under the
+/// pointer takes the whole gesture, at its ends too, and bounces there
+/// rather than handing the rest to the area around it. A disabled
+/// `ScrollBounce` still scrolls its child and keeps the gesture from the
+/// areas around it; it only stops at the ends instead of stretching.
pub struct ScrollBounce {
id: ElementId,
handle: Rc<dyn ScrollbarHandle>,
child: AnyElement,
enabled: bool,
+ scroll_wheel: bool,
motion: ScrollBounceMotion,
on_scroll: Option<Rc<dyn Fn(&mut Window, &mut App)>>,
}
@@ -111,18 +160,41 @@ impl ScrollBounce {
id: id.into(),
handle: Rc::new(handle.clone()),
child: child.into_any_element(),
- enabled: cfg!(any(target_os = "ios", target_os = "android")),
+ enabled: Self::enabled_by_default(),
+ scroll_wheel: true,
motion: ScrollBounceMotion::default(),
on_scroll: None,
}
}
- /// Opt in on a platform with compatible touch phase semantics.
+ /// Whether scroll areas bounce on this platform unless told otherwise:
+ /// on iOS, Android and macOS, whose own scroll views do.
+ pub const fn enabled_by_default() -> bool {
+ cfg!(any(
+ target_os = "ios",
+ target_os = "android",
+ target_os = "macos"
+ ))
+ }
+
+ /// Turn the bounce on or off. Defaults to [`Self::enabled_by_default`].
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
+ /// Own vertical scrolling using the shared input overscroll engine.
+ /// Precise pixel input can bounce; ordinary line-wheel input only scrolls
+ /// and cancels any existing displacement. Keep fixed chrome outside it.
+ ///
+ /// On by default. Turned off, the child scrolls itself and only its
+ /// unused deltas stretch the edge; that needs input that emits `Ended`
+ /// at finger release, before momentum, as touch does.
+ pub fn scroll_wheel(mut self, enabled: bool) -> Self {
+ self.scroll_wheel = enabled;
+ self
+ }
+
/// Set how far a drag stretches past an edge and how the edge returns.
pub fn motion(mut self, motion: ScrollBounceMotion) -> Self {
self.motion = motion;
@@ -141,6 +213,7 @@ impl ScrollBounce {
#[derive(Default)]
struct State {
physics: Physics,
+ overscroll: Overscroll,
sampled_at: Option<Instant>,
ongoing_scroll: OngoingScroll,
short_drag_distance: Option<f32>,
@@ -173,6 +246,29 @@ const CATCH_DRAG_SLOP: f32 = 8.;
// otherwise ends suppression, and would stay locked after one bounce.
const MOMENTUM_GAP: Duration = Duration::from_millis(250);
+/// Which scroll area under the pointer takes the current wheel event.
+///
+/// Capture runs from the outside in, so every area that could scroll writes
+/// itself here before its child paints, and the innermost one is the last
+/// to write. Each then acts, from the inside out, only if it is still the
+/// one written.
+#[derive(Default)]
+struct ScrollOwner(Option<HitboxId>);
+
+impl Global for ScrollOwner {}
+
+/// Take the current wheel event from the scroll areas around `hitbox`.
+/// Call it in the capture phase, from a listener registered before the
+/// children paint, whenever the area would scroll for the event.
+pub(crate) fn claim_scroll(hitbox: HitboxId, cx: &mut App) {
+ cx.default_global::<ScrollOwner>().0 = Some(hitbox);
+}
+
+fn owns_scroll(hitbox: HitboxId, cx: &App) -> bool {
+ cx.try_global::<ScrollOwner>()
+ .is_some_and(|owner| owner.0 == Some(hitbox))
+}
+
/// `ScrollbarHandle` has no `max_offset`; recover it from the definition
/// `content_size = viewport + max_offset`. Both dispatch phases clamp against
/// this bound and must agree on it.
@@ -235,16 +331,27 @@ impl Element for ScrollBounce {
if !self.enabled || cx.reduce_motion() {
*state = State::default();
}
- state.physics.motion = self.motion;
- let now = Instant::now();
- let elapsed = state
- .sampled_at
- .replace(now)
- .map_or(0., |at| now.duration_since(at).as_secs_f32());
- if state.physics.step(elapsed) {
- window.request_animation_frame();
+ if self.scroll_wheel {
+ state.overscroll.set_motion(self.motion);
+ if max_scroll_extent(self.handle.as_ref()) <= px(0.) {
+ state.overscroll.reset();
+ }
+ if state.overscroll.step(cx.background_executor().now()) {
+ window.request_animation_frame();
+ }
+ state.overscroll.offset()
+ } else {
+ state.physics.motion = self.motion;
+ let now = Instant::now();
+ let elapsed = state
+ .sampled_at
+ .replace(now)
+ .map_or(0., |at| now.duration_since(at).as_secs_f32());
+ if state.physics.step(elapsed) {
+ window.request_animation_frame();
+ }
+ state.physics.offset()
}
- state.physics.offset()
};
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
window.with_content_mask(Some(ContentMask { bounds }), |window| {
@@ -252,6 +359,16 @@ impl Element for ScrollBounce {
self.child.prepaint(window, cx);
});
});
+ // The child's fresh metrics can remove overflow during a drag.
+ // Reset against those metrics as well, and repaint any displaced frame.
+ if self.scroll_wheel && max_scroll_extent(self.handle.as_ref()) <= px(0.) {
+ let mut state = state.borrow_mut();
+ let displaced = state.overscroll.offset() != 0.;
+ state.overscroll.reset();
+ if displaced {
+ window.request_animation_frame();
+ }
+ }
ScrollBouncePrepaintState { state, hitbox }
}
@@ -265,7 +382,19 @@ impl Element for ScrollBounce {
window: &mut Window,
cx: &mut App,
) {
- if self.enabled && !cx.reduce_motion() {
+ if self.scroll_wheel {
+ let hitbox = prepaint.hitbox.id;
+ let handle = self.handle.clone();
+ window.on_mouse_event(move |_: &ScrollWheelEvent, phase, window, cx| {
+ if phase == DispatchPhase::Capture
+ && hitbox.should_handle_scroll(window)
+ && max_scroll_extent(handle.as_ref()) > px(0.)
+ {
+ claim_scroll(hitbox, cx);
+ }
+ });
+ }
+ if self.enabled && !self.scroll_wheel && !cx.reduce_motion() {
let state = prepaint.state.clone();
let hitbox = prepaint.hitbox.id;
let handle = self.handle.clone();
@@ -275,6 +404,15 @@ impl Element for ScrollBounce {
let mut allow_end_bounce = false;
window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| {
let ScrollDelta::Pixels(mut delta) = event.delta else {
+ if phase == DispatchPhase::Capture && hitbox.should_handle_scroll(window) {
+ let mut state = state.borrow_mut();
+ let displaced = state.physics.offset() != 0.;
+ *state = State::default();
+ drop(state);
+ if displaced {
+ cx.notify(view);
+ }
+ }
return;
};
if !hitbox.should_handle_scroll(window) {
@@ -408,11 +546,104 @@ impl Element for ScrollBounce {
window.with_content_mask(Some(ContentMask { bounds }), |window| {
self.child.paint(window, cx)
});
+ // After the children, so that the areas inside act first.
+ if self.scroll_wheel {
+ let bounces = self.enabled && !cx.reduce_motion();
+ let state = prepaint.state.clone();
+ let hitbox = prepaint.hitbox.id;
+ let handle = self.handle.clone();
+ let view = window.current_view();
+ let on_scroll = self.on_scroll.clone();
+ window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| {
+ if phase != DispatchPhase::Capture
+ || !hitbox.should_handle_scroll(window)
+ || !owns_scroll(hitbox, cx)
+ {
+ return;
+ }
+ let mut delta = event.delta.pixel_delta(window.line_height());
+ let mut state = state.borrow_mut();
+ state
+ .ongoing_scroll
+ .lock_axis(&mut delta, event.touch_phase);
+ let max = max_scroll_extent(handle.as_ref());
+ if max <= px(0.) {
+ state.overscroll.reset();
+ return;
+ }
+ if delta.x.abs() > delta.y.abs() {
+ return;
+ }
+ let before = handle.offset();
+ let changed = if !bounces || matches!(event.delta, ScrollDelta::Lines(_)) {
+ let displaced = state.overscroll.offset() != 0.;
+ state.overscroll.reset();
+ let mut offset = before;
+ offset.y = (before.y + delta.y).clamp(-max, px(0.));
+ handle.set_offset(offset);
+ displaced
+ } else {
+ state.overscroll.packet(
+ delta.y.as_f32(),
+ event.touch_phase,
+ bounds.size.height.as_f32(),
+ cx.background_executor().now(),
+ |dy| {
+ let before = handle.offset();
+ let mut offset = before;
+ offset.y = (before.y + px(dy)).clamp(-max, px(0.));
+ handle.set_offset(offset);
+ (offset.y - before.y).as_f32()
+ },
+ )
+ };
+ let scrolled = handle.offset() != before;
+ drop(state);
+ cx.stop_propagation();
+ if changed || scrolled {
+ cx.notify(view);
+ }
+ if scrolled && let Some(handler) = &on_scroll {
+ handler(window, cx);
+ }
+ });
+ }
}
}
+/// The largest outward speed, at most `speed`, that lets an edge at
+/// `position` go no farther out than `max`.
+///
+/// The edge then follows `(position + speed * t) * e^(-ωt)`, which peaks at
+/// `speed / ω * e^(ω * position / speed - 1)` when the speed is enough to
+/// carry it outward at all.
+fn limit_throw(position: f32, speed: f32, omega: f32, max: f32) -> f32 {
+ let peak = |speed: f32| {
+ if speed <= omega * position {
+ position
+ } else {
+ speed / omega * (omega * position / speed - 1.).exp()
+ }
+ };
+ if peak(speed) <= max {
+ return speed;
+ }
+ let (mut low, mut high) = (0., speed);
+ for _ in 0..24 {
+ let mid = (low + high) / 2.;
+ if peak(mid) <= max {
+ low = mid;
+ } else {
+ high = mid;
+ }
+ }
+ low
+}
+
+/// The stretch past an edge and its return, shared with the input's own
+/// scrolling.
#[derive(Default)]
-struct Physics {
+pub(crate) struct Physics {
position: f32,
velocity: f32,
dragging: bool,
@@ -422,35 +653,63 @@ struct Physics {
}
impl Physics {
- fn offset(&self) -> f32 {
+ pub(crate) fn set_motion(&mut self, motion: ScrollBounceMotion) {
+ self.motion = motion;
+ }
+
+ pub(crate) fn reset(&mut self) {
+ *self = Self {
+ motion: self.motion,
+ ..Self::default()
+ };
+ }
+ pub(crate) fn is_dragging(&self) -> bool {
+ self.dragging
+ }
+
+ /// Whether a released edge is still on its way.
+ pub(crate) fn is_moving(&self) -> bool {
+ !self.dragging && (self.position != 0. || self.velocity != 0.)
+ }
+
+ pub(crate) fn offset(&self) -> f32 {
if self.dragging {
let d = self.extent.max(1.);
let tracking = self.motion.tracking;
- self.position * tracking / (1. + tracking * self.position.abs() / d)
+ let stretch = self.position * tracking / (1. + tracking * self.position.abs() / d);
+ match self.motion.max_stretch {
+ // Follows the stretch closely at first, then eases into the limit.
+ Some(max) => max * (stretch / max).tanh(),
+ None => stretch,
+ }
} else {
self.position
}
}
- fn begin(&mut self, extent: f32) {
+ pub(crate) fn begin(&mut self, extent: f32) {
let offset = self.offset();
// A displaced edge keeps the extent it was stretched under. The
// rubber-band curve saturates at the extent, so re-reading a viewport
// that shrank mid-return (rotation, keyboard) could not place the
// finger where the edge is: it would snap, then need a long pull back.
- if offset == 0. {
+ if offset == 0. || self.extent <= 0. {
self.extent = extent.max(1.);
}
- // Invert the rubber-band curve so grabbing a returning edge is continuous.
+ // Invert the curves so grabbing a returning edge is continuous.
+ let stretch = match self.motion.max_stretch {
+ Some(max) => max * (offset / max).clamp(-0.999, 0.999).atanh(),
+ None => offset,
+ };
let tracking = self.motion.tracking;
- self.position = offset / (tracking * (1. - offset.abs() / self.extent).max(0.01));
+ self.position = stretch / (tracking * (1. - stretch.abs() / self.extent).max(0.01));
self.velocity = 0.;
self.dragging = true;
self.suppress_momentum = false;
}
/// Apply finger displacement. Return the part that crosses back into the list.
- fn pull(&mut self, delta: f32) -> f32 {
+ pub(crate) fn pull(&mut self, delta: f32) -> f32 {
let previous = self.position;
let next = previous + delta;
if previous != 0. && previous.signum() != next.signum() {
@@ -462,7 +721,7 @@ impl Physics {
}
}
- fn release(&mut self) {
+ pub(crate) fn release(&mut self) {
self.position = self.offset();
self.dragging = false;
if self.position != 0. {
@@ -470,9 +729,35 @@ impl Physics {
}
}
+ /// Let go of the edge the way AppKit does: from wherever it is, it
+ /// returns at full speed, and it carries `velocity` outward on the way.
+ /// Its path is `(x + velocity * t) * e^(-ωt)`, so an edge at rest is
+ /// thrown out and back, and an edge let go while still being pushed
+ /// returns a little later. It never crosses the boundary: velocity
+ /// pointing back inward is dropped.
+ pub(crate) fn let_go(&mut self, velocity: f32) {
+ self.position = self.offset();
+ self.dragging = false;
+ let Some(omega) = self.motion.omega() else {
+ self.position = 0.;
+ self.velocity = 0.;
+ return;
+ };
+ let mut outward = if self.position * velocity >= 0. {
+ velocity
+ } else {
+ 0.
+ };
+ if let Some(max) = self.motion.max_stretch {
+ outward =
+ outward.signum() * limit_throw(self.position.abs(), outward.abs(), omega, max);
+ }
+ self.velocity = outward - omega * self.position;
+ }
+
/// Exact critically damped spring integration, independent of refresh rate.
- fn step(&mut self, seconds: f32) -> bool {
- if self.dragging || self.position == 0. {
+ pub(crate) fn step(&mut self, seconds: f32) -> bool {
+ if self.dragging || (self.position == 0. && self.velocity == 0.) {
return false;
}
let Some(omega) = self.motion.omega() else {
@@ -521,7 +806,8 @@ mod tests {
.track_scroll(&self.handle)
.child(div().h(px(600.)).w_full()),
)
- .enabled(self.enabled),
+ .enabled(self.enabled)
+ .scroll_wheel(false),
)
}
}
@@ -539,6 +825,180 @@ mod tests {
});
}
+ struct WheelViewport(ScrollHandle);
+
+ impl Render for WheelViewport {
+ fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+ ScrollBounce::new(
+ "wheel-bounce",
+ &self.0,
+ div()
+ .id("wheel-viewport")
+ .w(px(200.))
+ .h(px(200.))
+ .overflow_y_scroll()
+ .track_scroll(&self.0)
+ .child(div().h(px(600.)).w_full()),
+ )
+ .enabled(true)
+ .scroll_wheel(true)
+ }
+ }
+
+ #[gpui::test]
+ fn ordinary_mouse_wheel_keeps_the_shared_viewport_undisplaced(cx: &mut TestAppContext) {
+ let handle = ScrollHandle::new();
+ let (_, cx) = cx.add_window_view({
+ let handle = handle.clone();
+ move |_, _| WheelViewport(handle)
+ });
+ draw(cx);
+ let origin = handle.bounds().origin;
+ cx.simulate_event(ScrollWheelEvent {
+ position: point(px(100.), px(100.)),
+ delta: ScrollDelta::Lines(point(0., -100.)),
+ ..Default::default()
+ });
+ draw(cx);
+ cx.executor().advance_clock(Duration::from_millis(16));
+ draw(cx);
+ assert_eq!(handle.offset().y, -handle.max_offset().y);
+ assert_eq!(handle.bounds().origin, origin);
+ }
+
+ #[gpui::test]
+ fn wheel_scrolling_interrupts_the_shared_return(cx: &mut TestAppContext) {
+ let handle = ScrollHandle::new();
+ let (_, cx) = cx.add_window_view({
+ let handle = handle.clone();
+ move |_, _| WheelViewport(handle)
+ });
+ draw(cx);
+ let wheel = |cx: &mut VisualTestContext, dy| {
+ cx.simulate_event(ScrollWheelEvent {
+ position: point(px(100.), px(100.)),
+ delta: ScrollDelta::Lines(point(0., dy)),
+ ..Default::default()
+ });
+ };
+ scroll(cx, -1000., TouchPhase::Started);
+ scroll(cx, 0., TouchPhase::Ended);
+ draw(cx);
+ assert_eq!(handle.offset().y, -handle.max_offset().y);
+ cx.executor().advance_clock(Duration::from_millis(200));
+ draw(cx);
+ let end = handle.offset().y;
+ wheel(cx, 2.);
+ assert!(
+ handle.offset().y > end,
+ "the new wheel input scrolls before the next frame"
+ );
+ draw(cx);
+ }
+
+ /// A scroll area with a shorter one inside it, 100 px down.
+ struct Nested {
+ outer: ScrollHandle,
+ inner: ScrollHandle,
+ inner_enabled: bool,
+ }
+
+ impl Render for Nested {
+ fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+ ScrollBounce::new(
+ "outer",
+ &self.outer,
+ div()
+ .id("outer-viewport")
+ .w(px(200.))
+ .h(px(300.))
+ .overflow_y_scroll()
+ .track_scroll(&self.outer)
+ .child(div().h(px(100.)).w_full())
+ .child(
+ ScrollBounce::new(
+ "inner",
+ &self.inner,
+ div()
+ .id("inner-viewport")
+ .w(px(200.))
+ .h(px(100.))
+ .overflow_y_scroll()
+ .track_scroll(&self.inner)
+ .child(div().h(px(400.)).w_full()),
+ )
+ .enabled(self.inner_enabled),
+ )
+ .child(div().h(px(600.)).w_full()),
+ )
+ .enabled(true)
+ }
+ }
+
+ fn nested(cx: &mut TestAppContext, inner_enabled: bool) -> (Nested, &mut VisualTestContext) {
+ let (outer, inner) = (ScrollHandle::new(), ScrollHandle::new());
+ let (_, cx) = cx.add_window_view({
+ let (outer, inner) = (outer.clone(), inner.clone());
+ move |_, _| Nested {
+ outer,
+ inner,
+ inner_enabled,
+ }
+ });
+ draw(cx);
+ (
+ Nested {
+ outer,
+ inner,
+ inner_enabled,
+ },
+ cx,
+ )
+ }
+
+ fn scroll_at(cx: &mut VisualTestContext, y: f32, delta: f32, phase: TouchPhase) {
+ cx.simulate_event(ScrollWheelEvent {
+ position: point(px(100.), px(y)),
+ delta: ScrollDelta::Pixels(point(px(0.), px(delta))),
+ touch_phase: phase,
+ ..Default::default()
+ });
+ draw(cx);
+ }
+
+ #[gpui::test]
+ fn the_innermost_area_takes_the_whole_gesture(cx: &mut TestAppContext) {
+ let (areas, cx) = nested(cx, true);
+ let inner_origin = areas.inner.bounds().origin.y;
+ scroll_at(cx, 150., 0., TouchPhase::Started);
+ scroll_at(cx, 150., -200., TouchPhase::Moved);
+ assert_eq!(areas.inner.offset().y, px(-200.), "the inner area scrolls");
+ assert_eq!(areas.outer.offset().y, px(0.), "the outer one does not");
+ // At its end the inner area stretches instead of handing over.
+ scroll_at(cx, 150., -200., TouchPhase::Moved);
+ assert_eq!(areas.inner.offset().y, px(-300.));
+ assert_eq!(areas.outer.offset().y, px(0.));
+ assert!(areas.inner.bounds().origin.y < inner_origin);
+ // Outside the inner area, the outer one scrolls.
+ scroll_at(cx, 150., 0., TouchPhase::Ended);
+ scroll_at(cx, 20., -50., TouchPhase::Moved);
+ assert_eq!(areas.outer.offset().y, px(-50.));
+ }
+
+ #[gpui::test]
+ fn a_disabled_inner_area_still_keeps_its_scroll(cx: &mut TestAppContext) {
+ let (areas, cx) = nested(cx, false);
+ let inner_origin = areas.inner.bounds().origin.y;
+ scroll_at(cx, 150., -50., TouchPhase::Moved);
+ assert_eq!(areas.inner.offset().y, px(-50.), "the inner area scrolls");
+ assert_eq!(areas.outer.offset().y, px(0.), "the outer one does not");
+ // At its end it stops without stretching.
+ scroll_at(cx, 150., -500., TouchPhase::Moved);
+ assert_eq!(areas.inner.offset().y, px(-300.));
+ assert_eq!(areas.inner.bounds().origin.y, inner_origin);
+ assert_eq!(areas.outer.offset().y, px(0.));
+ }
+
struct ListTest(gpui::ListState);
impl Render for ListTest {
@@ -553,6 +1013,7 @@ mod tests {
.h(px(200.)),
)
.enabled(true)
+ .scroll_wheel(false)
}
}
@@ -929,9 +1390,11 @@ mod tests {
fn motion_builder_configures_tracking_and_response() {
let motion = ScrollBounceMotion::default()
.with_tracking(0.4)
- .with_response(Duration::from_millis(300));
+ .with_response(Duration::from_millis(300))
+ .with_max_stretch(24.);
assert_eq!(motion.tracking(), 0.4);
assert_eq!(motion.response(), Duration::from_millis(300));
+ assert_eq!(motion.max_stretch(), Some(24.));
}
#[test]
diff --git a/third-party/gpui-kit/crates/base/src/scrollable_mask.rs b/third-party/gpui-kit/crates/base/src/scrollable_mask.rs
index 68740755fc..7da56f6c8d 100644
--- a/third-party/gpui-kit/crates/base/src/scrollable_mask.rs
+++ b/third-party/gpui-kit/crates/base/src/scrollable_mask.rs
@@ -4,7 +4,7 @@ use std::rc::Rc;
use gpui::{
App, Axis, BorderStyle, Bounds, ContentMask, Edges, Element, ElementId, GlobalElementId,
Hitbox, Hsla, InteractiveElement as _, IntoElement, IsZero as _, LayoutId, OngoingScroll,
- PaintQuad, ParentElement as _, Point, Position, ScrollHandle, ScrollWheelEvent,
+ PaintQuad, ParentElement as _, Point, Position, ScrollDelta, ScrollHandle, ScrollWheelEvent,
StatefulInteractiveElement as _, Style, StyleRefinement, Styled as _, Window, div, px,
relative,
};
@@ -297,10 +297,17 @@ fn corner_notch_path(
/// `overscroll-behavior: auto` chaining), while a horizontal mask keeps
/// consuming it — a bubbled horizontal delta would get mapped onto a
/// vertical-only ancestor by gpui's own wheel listener (see #2468).
+///
+/// A mouse wheel has no horizontal axis, so a horizontal mask leaves it to the
+/// parent. Over a native horizontal scroller, [`Self::vertical_wheel`] scrolls
+/// sideways with it instead.
pub struct ScrollableMask<H: ScrollbarHandle + Clone = ScrollHandle> {
axis: Axis,
id: ElementId,
scroll_handle: H,
+ /// Only turn a plain wheel's vertical lines into horizontal scrolling,
+ /// leaving every other event to the scroller below.
+ vertical_wheel: bool,
debug: Option<Hsla>,
}
@@ -312,10 +319,28 @@ impl<H: ScrollbarHandle + Clone> ScrollableMask<H> {
scroll_handle: scroll_handle.clone(),
axis,
id: caller_id(),
+ vertical_wheel: false,
debug: None,
}
}
+ /// Create a mask over a native horizontal scroller — one with
+ /// `overflow_x_scroll` and a locked scroll axis, such as a strip of tabs —
+ /// that scrolls it sideways with a plain mouse wheel's vertical lines, the
+ /// only axis such a wheel has.
+ ///
+ /// Once the scroller cannot move that way, the wheel goes on to the parent,
+ /// so a page still scrolls past the strip. Trackpad gestures and
+ /// horizontal wheels are left to the scroller, with its own axis lock and
+ /// bounce.
+ #[track_caller]
+ pub fn vertical_wheel(scroll_handle: &H) -> Self {
+ Self {
+ vertical_wheel: true,
+ ..Self::new(Axis::Horizontal, scroll_handle)
+ }
+ }
+
/// Set a specific element id, default is the [`std::panic::Location::caller`].
///
/// Only needed when one call site creates several masks of the same axis,
@@ -368,8 +393,12 @@ impl<H: ScrollbarHandle + Clone> Element for ScrollableMask<H> {
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let mut style = Style::default();
- // Set the layout style relative to the table view to get same size.
+ // Cover the parent whatever its layout: pinned to its top-left corner
+ // rather than left at a static position that depends on whether the
+ // parent stacks its children in a column or a row.
style.position = Position::Absolute;
+ style.inset.top = px(0.).into();
+ style.inset.left = px(0.).into();
style.flex_grow = 1.0;
style.flex_shrink = 1.0;
style.size.width = relative(1.).into();
@@ -387,16 +416,7 @@ impl<H: ScrollbarHandle + Clone> Element for ScrollableMask<H> {
window: &mut Window,
_: &mut App,
) -> Self::PrepaintState {
- // Move y to bounds height to cover the parent view.
- let cover_bounds = Bounds {
- origin: Point {
- x: bounds.origin.x,
- y: bounds.origin.y - bounds.size.height,
- },
- size: bounds.size,
- };
-
- window.insert_hitbox(cover_bounds, gpui::HitboxBehavior::Normal)
+ window.insert_hitbox(bounds, gpui::HitboxBehavior::Normal)
}
fn paint(
@@ -410,6 +430,7 @@ impl<H: ScrollbarHandle + Clone> Element for ScrollableMask<H> {
_: &mut App,
) {
let is_horizontal = self.axis.is_horizontal();
+ let vertical_wheel = self.vertical_wheel;
let line_height = window.line_height();
let bounds = hitbox.bounds;
let ongoing_scroll = global_id
@@ -457,6 +478,32 @@ impl<H: ScrollbarHandle + Clone> Element for ScrollableMask<H> {
let mut offset = scroll_handle.offset();
let mut delta = event.delta.pixel_delta(line_height);
+ // A plain mouse wheel's vertical lines move the scroller
+ // sideways; once it is at its edge the parent scrolls
+ // instead, as for a vertical mask.
+ if vertical_wheel {
+ let ScrollDelta::Lines(lines) = event.delta else {
+ return;
+ };
+ if lines.x != 0. || lines.y == 0. || !lines.y.is_finite() {
+ return;
+ }
+ let axis_max = (scroll_handle.content_size().width
+ - scroll_handle.viewport_bounds().size.width)
+ .max(px(0.));
+ let current = offset.x.clamp(-axis_max, px(0.));
+ let new_offset = (current + delta.y).clamp(-axis_max, px(0.));
+ if new_offset == current {
+ return;
+ }
+
+ offset.x = new_offset;
+ scroll_handle.set_offset(offset);
+ cx.notify(view_id);
+ cx.stop_propagation();
+ return;
+ }
+
// Lock the gesture to the axis it started on, so a diagonal
// trackpad swipe cannot flip which mask consumes it from one
// event to the next. Line deltas carry no touch phase.
@@ -1175,4 +1222,101 @@ mod tests {
let scroll_top = list_state.logical_scroll_top();
assert_eq!((scroll_top.item_ix, scroll_top.offset_in_item), (0, px(0.)));
}
+
+ /// A 100px strip of 300px content, locked to its axis like a tab strip,
+ /// with a vertical-wheel mask, inside a vertical scroller.
+ struct VerticalWheelTest {
+ strip: ScrollHandle,
+ page: ScrollHandle,
+ }
+
+ impl Render for VerticalWheelTest {
+ fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+ div()
+ .id("page")
+ .w(px(100.))
+ .h(px(60.))
+ .overflow_y_scroll()
+ .track_scroll(&self.page)
+ .child(
+ div()
+ .relative()
+ .w(px(100.))
+ .h(px(40.))
+ .child(
+ div()
+ .id("strip")
+ .size_full()
+ .flex()
+ .overflow_x_scroll()
+ .restrict_scroll_to_axis()
+ .track_scroll(&self.strip)
+ .child(div().flex_none().w(px(300.)).h(px(40.))),
+ )
+ .child(ScrollableMask::vertical_wheel(&self.strip)),
+ )
+ .child(div().h(px(200.)))
+ }
+ }
+
+ fn vertical_wheel_test(
+ cx: &mut TestAppContext,
+ ) -> (&mut VisualTestContext, ScrollHandle, ScrollHandle) {
+ let (strip, page) = (ScrollHandle::new(), ScrollHandle::new());
+ let (_, cx) = cx.add_window_view({
+ let (strip, page) = (strip.clone(), page.clone());
+ move |_, _| VerticalWheelTest { strip, page }
+ });
+ cx.run_until_parked();
+ cx.update(|window, cx| window.draw(cx).clear(cx));
+ (cx, strip, page)
+ }
+
+ fn wheel(cx: &mut VisualTestContext, delta: ScrollDelta) {
+ cx.simulate_event(ScrollWheelEvent {
+ position: point(px(10.), px(10.)),
+ delta,
+ ..Default::default()
+ });
+ cx.update(|window, cx| window.draw(cx).clear(cx));
+ }
+
+ #[gpui::test]
+ fn vertical_wheel_scrolls_the_strip_and_then_the_parent(cx: &mut TestAppContext) {
+ let (cx, strip, page) = vertical_wheel_test(cx);
+ let line_height = cx.update(|window, _| window.line_height());
+
+ wheel(cx, ScrollDelta::Lines(point(0., -1.)));
+ assert_eq!(strip.offset().x, -line_height);
+ assert_eq!(page.offset().y, px(0.));
+
+ // Past the strip's end, the wheel scrolls the page.
+ wheel(cx, ScrollDelta::Lines(point(0., -100.)));
+ assert_eq!(strip.offset().x, px(-200.));
+ assert_eq!(page.offset().y, px(0.));
+ wheel(cx, ScrollDelta::Lines(point(0., -1.)));
+ assert_eq!(strip.offset().x, px(-200.));
+ assert!(page.offset().y < px(0.));
+
+ // A delta that is no number does not move the strip.
+ wheel(cx, ScrollDelta::Lines(point(0., f32::NAN)));
+ assert_eq!(strip.offset().x, px(-200.));
+ }
+
+ #[gpui::test]
+ fn vertical_wheel_leaves_trackpads_and_horizontal_wheels_to_the_scroller(
+ cx: &mut TestAppContext,
+ ) {
+ let (cx, strip, page) = vertical_wheel_test(cx);
+ let line_height = cx.update(|window, _| window.line_height());
+
+ // A vertical trackpad swipe keeps the strip's axis lock.
+ wheel(cx, ScrollDelta::Pixels(point(px(0.), px(-20.))));
+ assert_eq!(strip.offset().x, px(0.));
+ assert_eq!(page.offset().y, px(-20.));
+
+ // A horizontal wheel reaches the strip's own listener.
+ wheel(cx, ScrollDelta::Lines(point(-1., 0.)));
+ assert_eq!(strip.offset().x, -line_height);
+ }
}
diff --git a/third-party/gpui-kit/crates/base/src/scrollbar.rs b/third-party/gpui-kit/crates/base/src/scrollbar.rs
index db7cc5e645..c502861fb1 100644
--- a/third-party/gpui-kit/crates/base/src/scrollbar.rs
+++ b/third-party/gpui-kit/crates/base/src/scrollbar.rs
@@ -839,6 +839,8 @@ pub struct Scrollbar {
scroll_handle: Rc<dyn ScrollbarHandle>,
scroll_size: Option<Size<Pixels>>,
viewport_bounds: Option<Bounds<Pixels>>,
+ // Scroll range may use a smaller viewport than the padded track bounds.
+ scroll_viewport_size: Option<Size<Pixels>>,
use_layout_bounds: bool,
/// Maximum frames per second for scrolling by drag. Default is 120 FPS.
///
@@ -864,6 +866,7 @@ impl Scrollbar {
max_fps: 120,
scroll_size: None,
viewport_bounds: None,
+ scroll_viewport_size: None,
use_layout_bounds: false,
styles: ScrollbarStyles::default(),
theme: None,
@@ -932,6 +935,11 @@ impl Scrollbar {
self
}
+ pub(crate) fn scroll_viewport_size(mut self, size: Size<Pixels>) -> Self {
+ self.scroll_viewport_size = Some(size);
+ self
+ }
+
/// Use the scrollbar element's layout bounds as its viewport.
///
/// This is useful for composite widgets whose scrollbar viewport excludes
@@ -1243,21 +1251,22 @@ struct ThumbGeometry {
impl ThumbGeometry {
fn new(
origin: Pixels,
- container: Pixels,
+ track_size: Pixels,
+ viewport_size: Pixels,
content: Pixels,
margin_end: Pixels,
inset: Pixels,
min_length: Pixels,
) -> Self {
- let track = (container - margin_end).max(px(0.));
- let logical_length = (container / content * container).max(min_length).min(track);
+ let track = (track_size - margin_end).max(px(0.));
+ let logical_length = (viewport_size / content * track).max(min_length).min(track);
let inset = inset.clamp(px(0.), logical_length / 2.);
Self {
origin,
inset,
length: logical_length - inset * 2.,
travel: track - logical_length,
- extent: content - container,
+ extent: content - viewport_size,
}
}
@@ -1424,6 +1433,7 @@ impl Element for Scrollbar {
let scroll_size = self
.scroll_size
.unwrap_or(self.scroll_handle.content_size());
+ let scroll_viewport_size = self.scroll_viewport_size.unwrap_or(hitbox.size);
for axis in self.axis.all().into_iter() {
let is_vertical = axis.is_vertical();
@@ -1433,19 +1443,22 @@ impl Element for Scrollbar {
.width
.or(self.resolved_theme(cx).scrollbar.styles().track.width)
.unwrap_or(WIDTH);
- let (scroll_area_size, container_size, scroll_position) = if is_vertical {
- (
- scroll_size.height,
- hitbox.size.height,
- self.scroll_handle.offset().y,
- )
- } else {
- (
- scroll_size.width,
- hitbox.size.width,
- self.scroll_handle.offset().x,
- )
- };
+ let (scroll_area_size, container_size, scroll_viewport_size, scroll_position) =
+ if is_vertical {
+ (
+ scroll_size.height,
+ hitbox.size.height,
+ scroll_viewport_size.height,
+ self.scroll_handle.offset().y,
+ )
+ } else {
+ (
+ scroll_size.width,
+ hitbox.size.width,
+ scroll_viewport_size.width,
+ self.scroll_handle.offset().x,
+ )
+ };
// The horizontal scrollbar is set avoid overlapping with the vertical scrollbar, if the vertical scrollbar is visible.
let margin_end = if has_both && !is_vertical {
@@ -1455,7 +1468,7 @@ impl Element for Scrollbar {
};
// Hide scrollbar, if the scroll area is smaller than the container.
- if scroll_area_size <= container_size {
+ if scroll_area_size <= scroll_viewport_size {
has_both = false;
continue;
}
@@ -1538,6 +1551,7 @@ impl Element for Scrollbar {
let geometry = ThumbGeometry::new(
origin,
container_size,
+ scroll_viewport_size,
scroll_area_size,
margin_end,
inset,
@@ -1547,6 +1561,7 @@ impl Element for Scrollbar {
let active_geometry = ThumbGeometry::new(
origin,
container_size,
+ scroll_viewport_size,
scroll_area_size,
margin_end,
active_inset,
@@ -1644,7 +1659,6 @@ impl Element for Scrollbar {
radius = px(0.);
}
radius = clamp_thumb_radius(radius, state.thumb_fill_bounds);
- let bounds = state.bounds;
let thumb_bounds = state.thumb_bounds;
let geometry = state.geometry;
let active_geometry = state.active_geometry;
@@ -1764,11 +1778,15 @@ impl Element for Scrollbar {
if is_visible {
window.on_mouse_event({
+ // The hitbox rather than the bounds, so that an
+ // element above the bar (a menu, a dialog) keeps
+ // the press.
+ let bar_hitbox = state.bar_hitbox.clone();
let state = scrollbar_state.clone();
let scroll_handle = self.scroll_handle.clone();
- move |event: &MouseDownEvent, phase, _, cx| {
- if phase.bubble() && bounds.contains(&event.position) {
+ move |event: &MouseDownEvent, phase, window, cx| {
+ if phase.bubble() && bar_hitbox.is_hovered(window) {
cx.stop_propagation();
if thumb_bounds.contains(&event.position) {
@@ -1805,6 +1823,7 @@ impl Element for Scrollbar {
}
window.on_mouse_event({
+ let bar_hitbox = state.bar_hitbox.clone();
let scroll_handle = self.scroll_handle.clone();
let state = scrollbar_state.clone();
let max_fps_duration = Duration::from_secs_f64(1. / self.max_fps as f64);
@@ -1815,7 +1834,8 @@ impl Element for Scrollbar {
// we need to update the hovered state and increase the last_scroll_time.
let need_hover_to_update = is_hover_to_show || is_visible;
// Update hovered state for scrollbar
- if bounds.contains(&event.position) && need_hover_to_update {
+ let bar_hovered = bar_hitbox.is_hovered(window);
+ if bar_hovered && need_hover_to_update {
let hover_changed = state.get().hovered_axis != Some(axis);
state.set(state.get().with_hovered(Some(axis), Instant::now()));
notify |= hover_changed;
@@ -1826,6 +1846,7 @@ impl Element for Scrollbar {
// Update hovered state for scrollbar thumb
if tracks_thumb_hover(mode, is_visible)
+ && bar_hovered
&& thumb_bounds.contains(&event.position)
{
if state.get().hovered_on_thumb != Some(axis) {
@@ -1904,8 +1925,8 @@ mod tests {
use std::cell::{Cell, RefCell};
use gpui::{
- AppContext as _, Context, Modifiers, MouseButton, ParentElement as _, Render, Styled as _,
- TestAppContext, VisualTestContext, div,
+ AppContext as _, Context, InteractiveElement as _, Modifiers, MouseButton,
+ ParentElement as _, Render, Styled as _, TestAppContext, VisualTestContext, div,
};
struct LocalProbe {
@@ -2638,14 +2659,17 @@ mod tests {
handle: TestHandle,
axis: ScrollbarAxis,
mode: ScrollbarMode,
+ scroll_viewport_size: Option<Size<Pixels>>,
}
impl Render for ScrollbarHarness {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
- div()
- .relative()
- .size(px(100.))
- .child(Scrollbar::new(&self.handle).axis(self.axis).mode(self.mode))
+ let mut scrollbar = Scrollbar::new(&self.handle).axis(self.axis).mode(self.mode);
+ if let Some(size) = self.scroll_viewport_size {
+ scrollbar = scrollbar.scroll_viewport_size(size);
+ }
+
+ div().relative().size(px(100.)).child(scrollbar)
}
}
@@ -2658,12 +2682,52 @@ mod tests {
let handle = TestHandle::new(content_size);
let (_, cx) = cx.add_window_view({
let handle = handle.clone();
- move |_, _| ScrollbarHarness { handle, axis, mode }
+ move |_, _| ScrollbarHarness {
+ handle,
+ axis,
+ mode,
+ scroll_viewport_size: None,
+ }
});
cx.update(|window, cx| window.draw(cx).clear(cx));
(cx, handle)
}
+ #[gpui::test]
+ fn thumb_drag_reaches_content_end_when_track_includes_padding(cx: &mut TestAppContext) {
+ let handle = TestHandle::new(size(px(100.), px(160.)));
+ let (_, cx) = cx.add_window_view({
+ let handle = handle.clone();
+ move |_, _| ScrollbarHarness {
+ handle,
+ axis: ScrollbarAxis::Vertical,
+ mode: ScrollbarMode::Always,
+ scroll_viewport_size: Some(size(px(100.), px(80.))),
+ }
+ });
+ cx.update(|window, cx| window.draw(cx).clear(cx));
+
+ cx.simulate_mouse_down(
+ point(px(95.), px(20.)),
+ MouseButton::Left,
+ Modifiers::default(),
+ );
+ cx.update(|window, cx| window.draw(cx).clear(cx));
+ cx.simulate_mouse_move(
+ point(px(95.), px(99.)),
+ Some(MouseButton::Left),
+ Modifiers::default(),
+ );
+ cx.update(|window, cx| window.draw(cx).clear(cx));
+ cx.simulate_mouse_up(
+ point(px(95.), px(99.)),
+ MouseButton::Left,
+ Modifiers::default(),
+ );
+
+ assert_eq!(handle.offset().y, px(-80.));
+ }
+
#[test]
fn explicit_viewport_bounds_override_handle_bounds() {
let expected = Bounds::new(point(px(12.), px(24.)), size(px(240.), px(96.)));
@@ -2897,6 +2961,78 @@ mod tests {
assert_eq!(handle.offset(), Point::default());
}
+ /// A vertical scrollbar with an occluding element over part of it, as a
+ /// menu or dialog would be.
+ struct CoveredHarness {
+ handle: TestHandle,
+ mode: ScrollbarMode,
+ cover: Bounds<Pixels>,
+ }
+
+ impl Render for CoveredHarness {
+ fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+ div()
+ .relative()
+ .size(px(100.))
+ .child(
+ Scrollbar::new(&self.handle)
+ .axis(ScrollbarAxis::Vertical)
+ .mode(self.mode),
+ )
+ .child(
+ div()
+ .absolute()
+ .left(self.cover.left())
+ .top(self.cover.top())
+ .w(self.cover.size.width)
+ .h(self.cover.size.height)
+ .occlude(),
+ )
+ }
+ }
+
+ fn covered_harness(
+ cx: &mut TestAppContext,
+ mode: ScrollbarMode,
+ cover: Bounds<Pixels>,
+ ) -> (&mut VisualTestContext, TestHandle) {
+ let handle = TestHandle::new(size(px(100.), px(500.)));
+ let (_, cx) = cx.add_window_view({
+ let handle = handle.clone();
+ move |_, _| CoveredHarness {
+ handle,
+ mode,
+ cover,
+ }
+ });
+ cx.update(|window, cx| window.draw(cx).clear(cx));
+ (cx, handle)
+ }
+
+ #[gpui::test]
+ fn a_press_on_an_element_over_the_track_is_left_to_it(cx: &mut TestAppContext) {
+ let cover = Bounds::new(point(px(50.), px(60.)), size(px(50.), px(40.)));
+ let (cx, handle) = covered_harness(cx, ScrollbarMode::Always, cover);
+ cx.simulate_click(point(px(95.), px(80.)), Modifiers::default());
+ assert_eq!(handle.offset(), Point::default());
+
+ // The track left uncovered still takes a press.
+ cx.simulate_click(point(px(95.), px(50.)), Modifiers::default());
+ assert!(handle.offset().y < px(0.));
+ }
+
+ #[gpui::test]
+ fn hovering_an_element_over_a_hover_scrollbar_does_not_reveal_it(cx: &mut TestAppContext) {
+ let cover = Bounds::new(point(px(50.), px(0.)), size(px(50.), px(50.)));
+ let (cx, handle) = covered_harness(cx, ScrollbarMode::Hover, cover);
+ cx.simulate_mouse_move(point(px(95.), px(20.)), None, Modifiers::default());
+ cx.run_until_parked();
+
+ // Uncovered, but the bar never showed, so it takes no press.
+ cx.simulate_click(point(px(95.), px(80.)), Modifiers::default());
+ assert_eq!(handle.offset(), Point::default());
+ }
+
#[gpui::test]
fn hovering_reveals_scrollbar_for_track_interaction(cx: &mut TestAppContext) {
let (cx, handle) = harness(
@@ -2922,6 +3058,7 @@ mod tests {
handle,
axis: ScrollbarAxis::Vertical,
mode: ScrollbarMode::Always,
+ scroll_viewport_size: None,
}
});
cx.update(|window, cx| window.draw(cx).clear(cx));
@@ -2987,7 +3124,8 @@ mod tests {
#[test]
fn full_track_thumb_does_not_reset_the_scroll_offset() {
- let geometry = ThumbGeometry::new(px(20.), px(40.), px(500.), px(0.), px(4.), px(64.));
+ let geometry =
+ ThumbGeometry::new(px(20.), px(40.), px(40.), px(500.), px(0.), px(4.), px(64.));
let offset = point(px(0.), px(-100.));
assert_eq!(
geometry.drag_offset(
@@ -3000,6 +3138,26 @@ mod tests {
);
}
+ #[test]
+ fn thumb_geometry_maps_content_viewport_to_padded_track() {
+ let geometry =
+ ThumbGeometry::new(px(20.), px(96.), px(80.), px(160.), px(0.), px(0.), px(0.));
+
+ assert_eq!(geometry.length, px(48.));
+ assert_eq!(geometry.travel, px(48.));
+ assert_eq!(geometry.extent, px(80.));
+ assert_eq!(geometry.offset(px(68.), px(0.)), px(-80.));
+
+ let barely_overflowing =
+ ThumbGeometry::new(px(20.), px(96.), px(80.), px(85.), px(0.), px(0.), px(0.));
+ assert_eq!(barely_overflowing.extent, px(5.));
+ assert!(barely_overflowing.travel > px(0.));
+ assert_eq!(
+ barely_overflowing.offset(px(20.) + barely_overflowing.travel, px(0.)),
+ px(-5.)
+ );
+ }
+
#[gpui::test]
fn repeated_touch_and_mouse_drags_keep_the_painted_grab_point(cx: &mut TestAppContext) {
struct Probe {
diff --git a/third-party/gpui-kit/crates/base/src/select.rs b/third-party/gpui-kit/crates/base/src/select.rs
index 71f9b88249..79253ad559 100644
--- a/third-party/gpui-kit/crates/base/src/select.rs
+++ b/third-party/gpui-kit/crates/base/src/select.rs
@@ -8,13 +8,18 @@ use gpui::{
};
use crate::actions::{Cancel, Confirm, SelectDown, SelectUp};
-use crate::{StyledExt as _, TestSupportExt as _};
+use crate::{StyledExt as _, TestSupportExt as _, a11y_disabled::A11yDisabledExt as _};
const CONTEXT: &str = "Select";
#[doc(hidden)]
pub fn init(cx: &mut App) {
cx.bind_keys([
+ KeyBinding::new(
+ "space",
+ Confirm { secondary: false },
+ Some("Select && !Input"),
+ ),
KeyBinding::new("up", SelectUp, Some(CONTEXT)),
KeyBinding::new("down", SelectDown, Some(CONTEXT)),
KeyBinding::new("enter", Confirm { secondary: false }, Some(CONTEXT)),
@@ -277,7 +282,6 @@ impl RenderOnce for Select {
return;
}
- cx.propagate();
if open {
if let Some(handler) = on_confirm.as_ref() {
handler(window, cx);
@@ -302,14 +306,17 @@ impl RenderOnce for Select {
})
.children(self.children)
.refine_style(&self.style)
+ .a11y_disabled(disabled)
}
}
#[cfg(test)]
mod tests {
use super::*;
+ use crate::input::InputState;
use gpui::{
- Context, Element as _, Focusable, Render, TestAppContext, VisualTestContext, accesskit, px,
+ AppContext as _, Context, Element as _, Entity, Focusable, Render, TestAppContext,
+ VisualTestContext, accesskit, px,
};
use std::sync::{Arc, Mutex};
@@ -318,7 +325,10 @@ mod tests {
disabled: bool,
focus_handle: FocusHandle,
content_focus_handle: FocusHandle,
+ search_input: Option<Entity<InputState>>,
changes: Arc<Mutex<Vec<bool>>>,
+ parent_confirms: Arc<Mutex<usize>>,
+ confirmations: Arc<Mutex<usize>>,
/// Every step of a close, in the order it ran.
closing: Arc<Mutex<Vec<&'static str>>>,
}
@@ -330,8 +340,11 @@ mod tests {
disabled,
focus_handle: cx.focus_handle(),
content_focus_handle: cx.focus_handle(),
+ search_input: None,
changes: Arc::new(Mutex::new(Vec::new())),
closing: Arc::new(Mutex::new(Vec::new())),
+ parent_confirms: Arc::new(Mutex::new(0)),
+ confirmations: Arc::new(Mutex::new(0)),
}
}
}
@@ -348,25 +361,41 @@ mod tests {
let changes = self.changes.clone();
let opened = self.closing.clone();
let dismissed = self.closing.clone();
-
- Select::new("select")
- .open(self.open)
- .disabled(self.disabled)
- .focus_handle(&self.focus_handle)
- .content_focus_handle(&self.content_focus_handle)
- .on_open_change(move |open, _, cx| {
- changes.lock().unwrap().push(open);
- opened
- .lock()
- .unwrap()
- .push(if open { "open" } else { "close" });
- state.update(cx, |state, cx| {
- state.open = open;
- cx.notify();
- });
- })
- .on_dismiss(move |_, _| dismissed.lock().unwrap().push("dismiss"))
- .child(div().track_focus(&self.content_focus_handle).size(px(20.)))
+ let confirmed = self.closing.clone();
+
+ let parent_confirms = self.parent_confirms.clone();
+ let confirmations = self.confirmations.clone();
+ div()
+ .on_action(move |_: &Confirm, _, _| *parent_confirms.lock().unwrap() += 1)
+ .child(
+ Select::new("select")
+ .open(self.open)
+ .disabled(self.disabled)
+ .focus_handle(&self.focus_handle)
+ .content_focus_handle(&self.content_focus_handle)
+ .on_open_change(move |open, _, cx| {
+ changes.lock().unwrap().push(open);
+ opened
+ .lock()
+ .unwrap()
+ .push(if open { "open" } else { "close" });
+ state.update(cx, |state, cx| {
+ state.open = open;
+ cx.notify();
+ });
+ })
+ .on_dismiss(move |_, _| dismissed.lock().unwrap().push("dismiss"))
+ .on_confirm(move |_, _| {
+ confirmed.lock().unwrap().push("confirm");
+ *confirmations.lock().unwrap() += 1;
+ })
+ .when_some(self.search_input.clone(), |this, input| {
+ this.child(crate::Input::new(&input))
+ })
+ .when(self.search_input.is_none(), |this| {
+ this.child(div().track_focus(&self.content_focus_handle).size(px(20.)))
+ }),
+ )
}
}
@@ -412,6 +441,56 @@ mod tests {
});
}
+ #[gpui::test]
+ fn space_opens_and_confirms_a_non_searchable_select(cx: &mut TestAppContext) {
+ let (cx, state) = harness(cx, false);
+ cx.simulate_keystrokes("space");
+ cx.update(|window, cx| {
+ assert!(state.read(cx).open);
+ assert!(state.read(cx).content_focus_handle.is_focused(window));
+ window.draw(cx).clear(cx);
+ });
+ cx.simulate_keystrokes("space");
+ assert_eq!(
+ &*state
+ .read_with(cx, |state, _| state.closing.clone())
+ .lock()
+ .unwrap(),
+ &["open", "confirm"]
+ );
+ }
+
+ #[gpui::test]
+ fn space_in_a_select_search_input_remains_text(cx: &mut TestAppContext) {
+ let (cx, state) = harness(cx, false);
+ let input = cx.update(|window, cx| {
+ let input = cx.new(|cx| InputState::new(window, cx));
+ state.update(cx, |state, cx| {
+ state.content_focus_handle = input.focus_handle(cx);
+ state.search_input = Some(input.clone());
+ cx.notify();
+ });
+ window.draw(cx).clear(cx);
+ input
+ });
+ cx.simulate_keystrokes("space");
+ cx.update(|window, cx| {
+ assert!(state.read(cx).open);
+ assert!(input.focus_handle(cx).is_focused(window));
+ window.draw(cx).clear(cx);
+ });
+ cx.simulate_keystrokes("t w o space w o r d s");
+ assert_eq!(input.read_with(cx, |input, _| input.value()), "two words");
+ assert_eq!(
+ &*state
+ .read_with(cx, |state, _| state.closing.clone())
+ .lock()
+ .unwrap(),
+ &["open"]
+ );
+ assert!(state.read_with(cx, |state, _| state.open));
+ }
+
#[gpui::test]
fn escape_closes_and_restores_trigger_focus(cx: &mut TestAppContext) {
let (cx, state) = harness(cx, false);
@@ -459,7 +538,7 @@ mod tests {
fn disabled_select_is_not_keyboard_interactive(cx: &mut TestAppContext) {
let (cx, state) = harness(cx, true);
- cx.simulate_keystrokes("down enter");
+ cx.simulate_keystrokes("down enter space");
assert!(!state.read_with(cx, |state, _| state.open));
assert!(
state
@@ -504,4 +583,17 @@ mod tests {
assert!(!disabled.supports_action(accesskit::Action::Click));
});
}
+
+ #[gpui::test]
+ fn confirm_is_consumed_when_opening_and_accepting(cx: &mut TestAppContext) {
+ let (cx, state) = harness(cx, false);
+ cx.simulate_keystrokes("enter");
+ cx.update(|window, cx| window.draw(cx).clear(cx));
+ cx.simulate_keystrokes("enter");
+ state.read_with(cx, |state, _| {
+ assert!(state.open);
+ assert_eq!(*state.confirmations.lock().unwrap(), 1);
+ assert_eq!(*state.parent_confirms.lock().unwrap(), 0);
+ });
+ }
}
diff --git a/third-party/gpui-kit/crates/base/src/slider.rs b/third-party/gpui-kit/crates/base/src/slider.rs
index 36b006be68..6d00348d1f 100644
--- a/third-party/gpui-kit/crates/base/src/slider.rs
+++ b/third-party/gpui-kit/crates/base/src/slider.rs
@@ -9,7 +9,7 @@ use gpui::{
TouchDragEvent, TouchPhase, Window, div, prelude::FluentBuilder as _, px,
};
-use crate::{element_ext::ElementExt, geometry::AxisExt};
+use crate::{a11y_disabled::A11yDisabledExt as _, element_ext::ElementExt, geometry::AxisExt};
/// Events emitted by the [`SliderState`].
pub enum SliderEvent {
@@ -522,28 +522,30 @@ impl RenderOnce for Slider {
} else {
Orientation::Horizontal
})
- .on_a11y_action(AccessibleAction::Increment, {
- let state = slider_state.clone();
- move |_, window, cx| {
- state.update(cx, |state, cx| {
- let value =
- (state.value().end() + state.step_value()).min(state.max_value());
- state.set_value(value, window, cx);
- });
- }
- })
- .on_a11y_action(AccessibleAction::Decrement, {
- let state = slider_state.clone();
- move |_, window, cx| {
- state.update(cx, |state, cx| {
- let value =
- (state.value().end() - state.step_value()).max(state.min_value());
- state.set_value(value, window, cx);
- });
- }
- })
+ // A disabled slider takes no value, from the pointer or from
+ // assistive technology.
.when(!self.disabled, |this| {
- this.on_mouse_up(
+ this.on_a11y_action(AccessibleAction::Increment, {
+ let state = slider_state.clone();
+ move |_, window, cx| {
+ state.update(cx, |state, cx| {
+ let value =
+ (state.value().end() + state.step_value()).min(state.max_value());
+ state.set_value(value, window, cx);
+ });
+ }
+ })
+ .on_a11y_action(AccessibleAction::Decrement, {
+ let state = slider_state.clone();
+ move |_, window, cx| {
+ state.update(cx, |state, cx| {
+ let value =
+ (state.value().end() - state.step_value()).max(state.min_value());
+ state.set_value(value, window, cx);
+ });
+ }
+ })
+ .on_mouse_up(
MouseButton::Left,
window.listener_for(&self.state, |state, _, _, cx| state.handle_release(cx)),
)
@@ -553,6 +555,7 @@ impl RenderOnce for Slider {
)
})
.children(self.children)
+ .a11y_disabled(self.disabled)
}
}
@@ -779,7 +782,14 @@ impl RenderOnce for SliderIndicator {
.children(self.children)
.on_prepaint({
let state = self.state;
- move |bounds, _, cx| state.update(cx, |state, _| state.set_bounds(bounds))
+ // Only when they moved: with retained views, an update while
+ // the window draws renders every view that reads the state
+ // again.
+ move |bounds, _, cx| {
+ if state.read(cx).bounds != bounds {
+ state.update(cx, |state, _| state.set_bounds(bounds));
+ }
+ }
})
}
}
@@ -935,4 +945,88 @@ mod tests {
fn legacy_logarithmic_validation_is_preserved() {
let _ = SliderState::new().scale(SliderScale::Logarithmic);
}
+
+ /// A slider's bar beside a view that reads its state, under retained views.
+ #[cfg(feature = "gpui-fast")]
+ mod retained {
+ use std::{cell::Cell, rc::Rc};
+
+ use gpui::{
+ AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, Styled as _,
+ TestAppContext, Window, div, px,
+ };
+
+ use super::super::{SliderIndicator, SliderState};
+
+ /// A view that shows a slider's value, counting its renders.
+ struct Readout {
+ state: Entity<SliderState>,
+ renders: Rc<Cell<usize>>,
+ }
+
+ impl Render for Readout {
+ fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ self.renders.set(self.renders.get() + 1);
+ div().child(self.state.read(cx).value().to_string())
+ }
+ }
+
+ /// A slider's bar in a view of its own.
+ struct Bar {
+ state: Entity<SliderState>,
+ }
+
+ impl Render for Bar {
+ fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+ div()
+ .w(px(120.))
+ .h(px(24.))
+ .child(SliderIndicator::new(&self.state).size_full())
+ }
+ }
+
+ /// The bar and the readout side by side.
+ struct Pair {
+ bar: Entity<Bar>,
+ readout: Entity<Readout>,
+ }
+
+ impl Render for Pair {
+ fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+ div()
+ .flex()
+ .child(self.bar.clone())
+ .child(self.readout.clone())
+ }
+ }
+
+ /// Drawing a slider's bar again where it was changes nothing in the
+ /// slider's state: a view beside it that reads the state is not built
+ /// again for it.
+ #[gpui::test]
+ fn drawing_a_slider_again_leaves_its_readers_retained(cx: &mut TestAppContext) {
+ let renders = Rc::new(Cell::new(0));
+ let (view, cx) = cx.add_window_view({
+ let renders = renders.clone();
+ move |_, cx| {
+ let state = cx.new(|_| SliderState::new());
+ Pair {
+ bar: cx.new(|_| Bar {
+ state: state.clone(),
+ }),
+ readout: cx.new(|_| Readout { state, renders }),
+ }
+ }
+ });
+ cx.update(|window, cx| window.draw(cx).clear(cx));
+ cx.update(|window, cx| window.draw(cx).clear(cx));
+ let rendered = renders.get();
+ for _ in 0..3 {
+ view.update(cx, |pair, cx| pair.bar.update(cx, |_, cx| cx.notify()));
+ cx.update(|window, cx| window.draw(cx).clear(cx));
+ }
+ cx.update(|window, cx| window.draw(cx).clear(cx));
+ assert_eq!(renders.get(), rendered, "the readout was built again");
+ }
+ }
}
diff --git a/third-party/gpui-kit/crates/base/src/switch.rs b/third-party/gpui-kit/crates/base/src/switch.rs
index ff72fe2ff6..cdd881dbd5 100644
--- a/third-party/gpui-kit/crates/base/src/switch.rs
+++ b/third-party/gpui-kit/crates/base/src/switch.rs
@@ -8,7 +8,7 @@ use gpui::{
};
use smallvec::SmallVec;
-use crate::{StateStyle, StyledExt as _, TestSupportExt as _};
+use crate::{StateStyle, StyledExt as _, TestSupportExt as _, a11y_disabled::A11yDisabledExt as _};
type ChangeHandler = Rc<dyn Fn(bool, &ClickEvent, &mut Window, &mut App)>;
@@ -299,6 +299,9 @@ impl Switch {
}
/// Sets the name exposed to accessibility clients.
+ ///
+ /// A switch without text children has no accessible name, so set this
+ /// whenever it is drawn without visible text.
pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
self.accessibility_label = Some(label.into());
self
@@ -392,6 +395,7 @@ impl RenderOnce for Switch {
)
.children(self.children)
.refine_style(&style)
+ .a11y_disabled(disabled)
}
}
@@ -732,8 +736,7 @@ mod tests {
assert_eq!(disabled.role(), Role::Switch);
assert_eq!(disabled.toggled(), Some(Toggled::False));
assert!(!disabled.supports_action(accesskit::Action::Click));
- // GPUI currently has no aria-disabled setter. Keep the limitation
- // explicit instead of claiming an AccessKit disabled state.
- assert!(!disabled.is_disabled());
+ assert!(!enabled.is_disabled());
+ assert!(disabled.is_disabled());
}
}
diff --git a/third-party/gpui-kit/crates/base/src/tabs.rs b/third-party/gpui-kit/crates/base/src/tabs.rs
index 782438c737..a27cf67cff 100644
--- a/third-party/gpui-kit/crates/base/src/tabs.rs
+++ b/third-party/gpui-kit/crates/base/src/tabs.rs
@@ -8,7 +8,7 @@ use gpui::{
};
use smallvec::SmallVec;
-use crate::{StateStyle, StyledExt as _, TestSupportExt as _};
+use crate::{StateStyle, StyledExt as _, TestSupportExt as _, a11y_disabled::A11yDisabledExt as _};
type ClickHandler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>;
@@ -179,6 +179,7 @@ impl RenderOnce for Tab {
)
.children(self.children)
.refine_style(&style)
+ .a11y_disabled(disabled)
}
}
diff --git a/third-party/gpui-kit/crates/base/src/test_support.rs b/third-party/gpui-kit/crates/base/src/test_support.rs
index 57ccc4e724..2368f827ed 100644
--- a/third-party/gpui-kit/crates/base/src/test_support.rs
+++ b/third-party/gpui-kit/crates/base/src/test_support.rs
@@ -266,6 +266,15 @@ impl<E: Element> Observed<E> {
}
}
+ /// Replaces the observed element while keeping its observation, so a
+ /// wrapper inside it is seen by the snapshot.
+ pub(crate) fn map_inner<F>(self, f: impl FnOnce(E) -> F) -> Observed<F> {
+ Observed {
+ inner: f(self.inner),
+ focus: self.focus,
+ }
+ }
+
pub(crate) fn new(inner: E) -> Self {
assert!(
Element::id(&inner).is_some(),
diff --git a/third-party/gpui-kit/crates/base/src/text/document.rs b/third-party/gpui-kit/crates/base/src/text/document.rs
index f28ae4ae6a..362357fbc0 100644
--- a/third-party/gpui-kit/crates/base/src/text/document.rs
+++ b/third-party/gpui-kit/crates/base/src/text/document.rs
@@ -10,7 +10,7 @@ use std::{
use crate::text::{
SelectionFormat,
- node::{BlockNode, NodeContext, SourceRangeSelection},
+ node::{BlockNode, NodeContext, SelectionStyles, SourceRangeSelection},
};
/// The parsed document AST.
@@ -53,8 +53,11 @@ pub(crate) enum PrevBlock {
/// that shows anything, and the visible block before it. Link definitions
/// and other blocks that render nothing are skipped, so a document opening
/// with `[ref]: url` still starts flush.
-pub(crate) fn flow_position(blocks: &[BlockNode], ix: usize) -> (bool, PrevBlock) {
- let prev = blocks[..ix].iter().rev().find(|block| {
+pub(crate) fn flow_position<B: std::borrow::Borrow<BlockNode>>(
+ blocks: &[B],
+ ix: usize,
+) -> (bool, PrevBlock) {
+ let prev = blocks[..ix].iter().rev().map(B::borrow).find(|block| {
!matches!(
block,
BlockNode::Definition { .. } | BlockNode::Break { .. } | BlockNode::Unknown
@@ -91,16 +94,13 @@ impl ParsedDocument {
/// top-level blocks are joined with a blank line so the result re-renders
/// with the same block structure.
///
- /// A block only learns its selection when it is painted, so in a scrollable
- /// (virtualized) view every block the user scrolled past reports nothing.
- /// The selection is one continuous range, so blocks it spans that came up
- /// empty are inside it and are emitted whole rather than dropped. `blocks`
- /// bounds that span; it comes from the selection endpoints, which hold on to
- /// their block index even after it scrolls out of view (the painted blocks
- /// alone cannot bound the span, because the press that starts a drag leaves
- /// an empty selection that never reaches paint). Without it, fall back to
- /// the painted blocks. (Source mode is only used by non-scrollable views,
- /// where `blocks` is `None` and every block paints.)
+ /// Paint and synchronous Copy project the current gesture onto retained
+ /// native layouts. In a scrollable (virtualized) view some covered blocks
+ /// have never been laid out and report nothing. The selection is continuous,
+ /// so its interior blocks are emitted whole rather than dropped. `blocks`
+ /// bounds that span; it comes from the selection endpoints, which hold on
+ /// to their block index even after scrolling out of view. Without it, fall
+ /// back to the blocks with projected selections.
///
/// A standalone image (a paragraph that is only an image) has no selectable
/// text run, so it never carries a selection of its own. It is therefore
@@ -132,6 +132,20 @@ impl ParsedDocument {
None => (painted_first, painted_last),
};
+ // A selection within one style copies as the text it renders; one
+ // that is only partly painted cannot tell, and keeps its source.
+ let format = match format {
+ SelectionFormat::SourceAcrossStyles
+ if first == last
+ && painted[first]
+ && self.blocks[first].selection_styles() != SelectionStyles::Mixed =>
+ {
+ SelectionFormat::Plain
+ }
+ SelectionFormat::SourceAcrossStyles => SelectionFormat::Source,
+ format => format,
+ };
+
if format == SelectionFormat::Plain {
let mut text = String::new();
for (ix, block) in self.blocks.iter().enumerate().take(last + 1).skip(first) {
diff --git a/third-party/gpui-kit/crates/base/src/text/editor_host.rs b/third-party/gpui-kit/crates/base/src/text/editor_host.rs
new file mode 100644
index 0000000000..f908a4349f
--- /dev/null
+++ b/third-party/gpui-kit/crates/base/src/text/editor_host.rs
@@ -0,0 +1,1324 @@
+//! Host API for external editors.
+//!
+//! An editor that lives outside gpui-kit keeps its own document model and
+//! hands gpui-kit a [`BlockNode`] tree, the same tree [`TextView`] renders.
+//! gpui-kit renders it and gives back, per text leaf, the geometry the editor
+//! needs for hit-testing, caret placement and platform input (IME) bounds.
+//!
+//! The boundary is deliberately narrow:
+//!
+//! - **In:** a [`BlockNode`] tree whose text leaves carry a host-chosen `u64`
+//! key (see [`Paragraph::with_leaf_key`] and [`CodeBlock::with_leaf_key`]),
+//! a [`RenderContext`], and per-key decoration ranges.
+//! - **Out:** one opaque [`LeafGeometry`] per key from
+//! [`collect_leaf_geometry`].
+//!
+//! Nothing lower is exposed: no `TextLayout`, no layout internals, no sink
+//! trait, no retained-layout or projection caches.
+//!
+//! The host owns editing semantics, document revisions, accessibility and native
+//! input integration. This module provides horizontal TextView block rendering;
+//! it is not a complete visual-caret navigation or pagination API. Extend the
+//! public boundary additively while keeping rendering internals private.
+//!
+//! [`TextView`]: super::TextView
+
+use std::{cell::RefCell, collections::HashMap, ops::Range, rc::Rc, sync::Arc};
+
+use gpui::{
+ AnyElement, App, Bounds, ClickEvent, ElementId, HighlightStyle, Hsla, ImageSource,
+ InteractiveElement as _, IntoElement as _, ParentElement as _, Pixels, Point, SharedString,
+ SharedUri, TextLayout, Window, WrappedLineLayout, div, point, px,
+};
+use unicode_segmentation::UnicodeSegmentation as _;
+
+use super::{
+ TextViewDefaults, TextViewStyle,
+ document::{NodeRenderOptions, PrevBlock, flow_position},
+ editor_seam::{self, EditorSink},
+ format::markdown,
+ markdown_ext::MarkdownExtensions,
+ node::NodeContext,
+};
+
+pub use super::node::{
+ BlockNode, BlockState, CodeBlock, ColumnumnAlign as ColumnAlign, ImageNode, InlineNode,
+ LinkMark, Paragraph, Span, Table, TextMark,
+};
+
+/// Parses Markdown with the parser [`TextView`](super::TextView) uses,
+/// including the given extensions, into the block tree.
+pub fn parse_markdown(
+ source: &str,
+ extensions: Arc<MarkdownExtensions>,
+) -> Result<Vec<BlockNode>, SharedString> {
+ let mut cx = NodeContext {
+ markdown_extensions: extensions,
+ ..Default::default()
+ };
+ let parsed = markdown::parse(source, &mut cx)?;
+ Ok(Arc::unwrap_or_clone(parsed.blocks))
+}
+
+/// What [`BlockNode::render`] needs besides the node: the text style, code
+/// highlighter and Markdown extensions.
+#[derive(Clone)]
+pub struct RenderContext {
+ node: NodeContext,
+}
+
+impl RenderContext {
+ /// Creates a local rendering context without reading application globals.
+ /// The host supplies body typography through its container or
+ /// `with_body_font_size`.
+ pub fn new(style: TextViewStyle) -> Self {
+ Self {
+ node: NodeContext {
+ style: Arc::new(style),
+ link_source: super::link::LinkSource::ExternalEditor(None),
+ ..Default::default()
+ },
+ }
+ }
+
+ /// Replaces code highlighting for this context only.
+ pub fn with_code_block_highlighter<F>(mut self, highlighter: F) -> Self
+ where
+ F: Fn(&CodeBlock) -> Vec<(Range<usize>, HighlightStyle)> + Send + Sync + 'static,
+ {
+ self.node.code_block_highlighter = Some(Arc::new(highlighter));
+ self
+ }
+
+ /// Resolves document image URIs. The host owns resource access policy.
+ pub fn with_image_source<F>(mut self, resolver: F) -> Self
+ where
+ F: Fn(&SharedUri) -> ImageSource + Send + Sync + 'static,
+ {
+ self.node.image_source = Some(Arc::new(resolver));
+ self
+ }
+
+ /// Handles link activation. Without a handler, links use App::open_url.
+ pub fn on_link_click<F>(mut self, handler: F) -> Self
+ where
+ F: Fn(&SharedString, &ClickEvent, &mut Window, &mut App) + Send + Sync + 'static,
+ {
+ self.node.link_source = super::link::LinkSource::ExternalEditor(Some(Arc::new(handler)));
+ self
+ }
+
+ /// Registers a reference-link definition from the host's document.
+ /// Supply definitions for the entire document, including virtualized blocks.
+ pub fn with_link_reference(
+ mut self,
+ identifier: impl Into<SharedString>,
+ link: LinkMark,
+ ) -> Self {
+ self.node.link_refs.insert(identifier.into(), link);
+ self
+ }
+
+ /// The context TextView would start from: the global [`TextViewDefaults`]
+ /// style and code-block highlighter when set, else the theme's style.
+ pub fn from_app(cx: &App) -> Self {
+ let theme = crate::Theme::global(cx);
+ let defaults = cx.try_global::<TextViewDefaults>();
+ let style = defaults
+ .and_then(|defaults| defaults.style().cloned())
+ .unwrap_or_else(|| TextViewStyle::from_theme(&theme));
+ Self {
+ node: NodeContext {
+ style: Arc::new(style),
+ link_source: super::link::LinkSource::ExternalEditor(None),
+ code_block_highlighter: defaults.and_then(|d| d.code_block_highlighter()),
+ ..Default::default()
+ },
+ }
+ }
+
+ /// Replaces the text style.
+ pub fn with_style(mut self, style: TextViewStyle) -> Self {
+ self.node.style = Arc::new(style);
+ self
+ }
+
+ /// Sets the Markdown extensions that render custom nodes.
+ pub fn with_markdown_extensions(mut self, extensions: Arc<MarkdownExtensions>) -> Self {
+ self.node.markdown_extensions = extensions;
+ self
+ }
+
+ /// Sets the font size body text inherits, which the block gaps and
+ /// heading sizes are relative to.
+ pub fn with_body_font_size(mut self, size: Pixels) -> Self {
+ self.node.body_font_size = size;
+ self
+ }
+
+ /// The text style blocks render with.
+ pub fn style(&self) -> &TextViewStyle {
+ &self.node.style
+ }
+}
+
+/// Where a block stands in the flow it is rendered in, which decides the gaps
+/// around it: the first block takes no gap above, the last none below, and a
+/// heading or rule counts the block before it.
+#[derive(Clone, Copy, Debug)]
+pub struct FlowPosition {
+ is_first: bool,
+ is_last: bool,
+ prev: PrevBlock,
+}
+
+impl FlowPosition {
+ /// The position of `blocks[ix]` among its siblings. Blocks that render
+ /// nothing (definitions, breaks) are skipped when looking for the block
+ /// before it.
+ pub fn at(blocks: &[&BlockNode], ix: usize, is_last: bool) -> Self {
+ let (is_first, prev) = flow_position(blocks, ix);
+ Self {
+ is_first,
+ is_last,
+ prev,
+ }
+ }
+
+ /// Overrides whether the block opens its flow, e.g. for a chunk of a
+ /// long list rendered as a list of its own.
+ pub fn first(mut self, is_first: bool) -> Self {
+ self.is_first = is_first;
+ self
+ }
+
+ /// Overrides whether the block closes its flow.
+ pub fn last(mut self, is_last: bool) -> Self {
+ self.is_last = is_last;
+ self
+ }
+}
+
+impl BlockNode {
+ /// Renders under a stable host-owned identity. Use a domain ID, not a list
+ /// position, when inserting, reordering or virtualizing blocks. Identity
+ /// scopes GPUI element state independently of the leaf keys used for geometry.
+ pub fn render_with_id(
+ &self,
+ id: impl Into<ElementId>,
+ flow: FlowPosition,
+ render_context: &RenderContext,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> AnyElement {
+ div()
+ .id(id)
+ .child(self.render(0, flow, render_context, window, cx))
+ .into_any_element()
+ }
+
+ /// Renders the block as TextView does. `ix` identifies the block among
+ /// its siblings (element ids derive from it).
+ pub fn render(
+ &self,
+ ix: usize,
+ flow: FlowPosition,
+ cx: &RenderContext,
+ window: &mut Window,
+ app: &mut App,
+ ) -> AnyElement {
+ self.render_block(
+ NodeRenderOptions {
+ ix,
+ is_first: flow.is_first,
+ is_last: flow.is_last,
+ prev: flow.prev,
+ ..Default::default()
+ },
+ &cx.node,
+ window,
+ app,
+ )
+ }
+}
+
+/// Runs `f`, which should prepaint rendered blocks, and returns its result
+/// with the geometry every keyed text leaf reported meanwhile.
+///
+/// `decorations` maps a leaf key to byte ranges of that leaf's text (atoms
+/// count as one `U+FFFC` each) to paint behind the text: the host's selection,
+/// find matches, anything. Leaves without a key, or without an entry, paint as
+/// usual. Atom decorations cover the whole atom and paint over its content
+/// so selections remain visible over opaque images. Supply translucent colors
+/// for overlays. Ranges are half-open; empty ranges paint nothing.
+/// Scopes nest, and the previous one is restored even if `f` panics.
+///
+/// Keys must be unique within one collection scope. Nested scopes are isolated:
+/// an outer scope does not collect the inner scope's leaves. Only prepainted
+/// leaves are returned; absence is not evidence that a document leaf was removed.
+/// The host owns document revisions, virtualization, focus, native input and
+/// UTF-16/UTF-8 conversion for platform input APIs.
+pub fn collect_leaf_geometry<R>(
+ decorations: HashMap<u64, Vec<(Range<usize>, Hsla)>>,
+ f: impl FnOnce() -> R,
+) -> (R, HashMap<u64, LeafGeometry>) {
+ let collector = Rc::new(Collector {
+ pieces: RefCell::default(),
+ decorations,
+ });
+ let result = editor_seam::with_sink(collector.clone(), f);
+ let mut pieces = collector.pieces.take();
+ pieces.retain(|_, pieces| !pieces.is_empty());
+ let leaves = pieces
+ .into_iter()
+ .map(|(key, pieces)| (key, LeafGeometry { pieces }))
+ .collect();
+ (result, leaves)
+}
+
+struct Collector {
+ pieces: RefCell<HashMap<u64, Vec<Piece>>>,
+ decorations: HashMap<u64, Vec<(Range<usize>, Hsla)>>,
+}
+
+impl EditorSink for Collector {
+ fn report(&self, key: u64, layout: &TextLayout) {
+ self.pieces
+ .borrow_mut()
+ .insert(key, vec![Piece::Whole(LayoutSnapshot::new(layout))]);
+ }
+
+ fn begin_pieces(&self, key: u64) {
+ self.pieces.borrow_mut().insert(key, Vec::new());
+ }
+
+ fn report_text_piece(&self, key: u64, start: usize, layout: &TextLayout) {
+ self.pieces
+ .borrow_mut()
+ .entry(key)
+ .or_default()
+ .push(Piece::Text {
+ start,
+ layout: LayoutSnapshot::new(layout),
+ });
+ }
+
+ fn report_box_piece(&self, key: u64, range: Range<usize>, bounds: Bounds<Pixels>) {
+ self.pieces
+ .borrow_mut()
+ .entry(key)
+ .or_default()
+ .push(Piece::Atom { range, bounds });
+ }
+
+ fn report_empty_piece(&self, key: u64, start: usize, bounds: Bounds<Pixels>) {
+ self.pieces
+ .borrow_mut()
+ .entry(key)
+ .or_default()
+ .push(Piece::Empty { start, bounds });
+ }
+
+ fn backgrounds(&self, key: u64) -> Vec<(Range<usize>, Hsla)> {
+ self.decorations.get(&key).cloned().unwrap_or_default()
+ }
+}
+
+/// Copy mutable frame state, sharing only immutable shaped lines.
+#[derive(Clone)]
+struct LayoutSnapshot {
+ bounds: Bounds<Pixels>,
+ len: usize,
+ line_height: Pixels,
+ text: SharedString,
+ lines: Vec<Arc<WrappedLineLayout>>,
+}
+
+impl LayoutSnapshot {
+ fn new(layout: &TextLayout) -> Self {
+ Self {
+ bounds: layout.bounds(),
+ len: layout.len(),
+ line_height: layout.line_height(),
+ text: layout.text().into(),
+ lines: layout.line_layouts().into_iter().collect(),
+ }
+ }
+
+ fn bounds(&self) -> Bounds<Pixels> {
+ self.bounds
+ }
+ fn len(&self) -> usize {
+ self.len
+ }
+ fn line_height(&self) -> Pixels {
+ self.line_height
+ }
+ fn text(&self) -> &str {
+ &self.text
+ }
+ fn line_layouts(&self) -> &[Arc<WrappedLineLayout>] {
+ &self.lines
+ }
+
+ fn position_for_index(&self, index: usize) -> Option<Point<Pixels>> {
+ let mut origin = self.bounds.origin;
+ let mut start = 0;
+ for line in &self.lines {
+ if index <= start + line.len() {
+ return line
+ .position_for_index(index - start, self.line_height)
+ .map(|p| origin + p);
+ }
+ start += line.len() + 1;
+ origin.y += line.size(self.line_height).height;
+ }
+ None
+ }
+}
+
+/// How a leaf was laid out: one layout for the whole leaf, or (for a mixed
+/// paragraph rendered as a flow) single-line text fragments and atom boxes in
+/// leaf byte order.
+#[derive(Clone)]
+enum Piece {
+ Whole(LayoutSnapshot),
+ Text {
+ start: usize,
+ layout: LayoutSnapshot,
+ },
+ Empty {
+ start: usize,
+ bounds: Bounds<Pixels>,
+ },
+ Atom {
+ range: Range<usize>,
+ bounds: Bounds<Pixels>,
+ },
+}
+
+impl Piece {
+ fn bounds(&self) -> Bounds<Pixels> {
+ match self {
+ Piece::Whole(layout) | Piece::Text { layout, .. } => layout.bounds(),
+ Piece::Atom { bounds, .. } | Piece::Empty { bounds, .. } => *bounds,
+ }
+ }
+
+ fn range(&self) -> Range<usize> {
+ match self {
+ Piece::Whole(layout) => 0..layout.len(),
+ Piece::Text { start, layout } => *start..start + layout.len(),
+ Piece::Atom { range, .. } => range.clone(),
+ Piece::Empty { start, .. } => *start..*start,
+ }
+ }
+}
+
+/// Where one keyed text leaf was laid out in the last frame.
+///
+/// Indices are byte offsets into the leaf's text, where each inline atom
+/// (image, custom node) is one `U+FFFC` (3 bytes). Coordinates are window
+/// pixels and are valid for the frame that produced them. It is a value, not a
+/// handle: it owns its bounds and shares immutable shaped lines, never the
+/// renderer's mutable layout state. Later frames do not mutate this snapshot.
+/// The host must replace it after content, width, typography or scrolling changes.
+#[derive(Clone)]
+pub struct LeafGeometry {
+ pieces: Vec<Piece>,
+}
+
+impl LeafGeometry {
+ fn whole(&self) -> Option<&LayoutSnapshot> {
+ match self.pieces.as_slice() {
+ [Piece::Whole(layout)] => Some(layout),
+ _ => None,
+ }
+ }
+
+ /// The bounds of the leaf: the union of its pieces.
+ pub fn bounds(&self) -> Bounds<Pixels> {
+ self.pieces
+ .iter()
+ .map(Piece::bounds)
+ .reduce(|a, b| a.union(&b))
+ .unwrap_or_default()
+ }
+
+ /// The length of the leaf's text in bytes, atoms included.
+ pub fn len(&self) -> usize {
+ self.pieces
+ .iter()
+ .map(|piece| piece.range().end)
+ .max()
+ .unwrap_or(0)
+ }
+
+ /// Whether the leaf's text is empty (it still has a line box).
+ pub fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+
+ /// The height of a line.
+ pub fn line_height(&self) -> Pixels {
+ self.pieces
+ .iter()
+ .find_map(|piece| match piece {
+ Piece::Whole(layout) | Piece::Text { layout, .. } => Some(layout.line_height()),
+ Piece::Empty { bounds, .. } => Some(bounds.size.height),
+ Piece::Atom { .. } => None,
+ })
+ .unwrap_or_else(|| self.bounds().size.height)
+ }
+
+ /// The height of a caret: the font's ascent plus descent, not the whole
+ /// line box, which includes leading.
+ pub fn caret_height(&self) -> Pixels {
+ let line_height = self.line_height();
+ self.pieces
+ .iter()
+ .find_map(|piece| match piece {
+ Piece::Whole(layout) | Piece::Text { layout, .. } => {
+ let line = layout.lines.first()?;
+ let h = line.unwrapped_layout.ascent + line.unwrapped_layout.descent;
+ (h > Pixels::ZERO).then_some(h)
+ }
+ Piece::Atom { .. } | Piece::Empty { .. } => None,
+ })
+ .unwrap_or(line_height * 0.85)
+ .min(line_height)
+ }
+
+ /// The top of a caret placed at byte `index`, in window coordinates.
+ pub fn position_for_index(&self, index: usize) -> Point<Pixels> {
+ if let Some(layout) = self.whole() {
+ return layout
+ .position_for_index(index.min(layout.len()))
+ .unwrap_or_else(|| self.bounds().origin);
+ }
+ // The piece the index starts, else the one it ends (a caret at a
+ // fragment boundary sits at the start of the next fragment).
+ let piece = self
+ .pieces
+ .iter()
+ .find(|p| p.range().contains(&index))
+ .or_else(|| self.pieces.iter().rev().find(|p| p.range().end == index))
+ .or(self.pieces.last());
+ let Some(piece) = piece else {
+ return self.bounds().origin;
+ };
+ match piece {
+ Piece::Whole(layout) | Piece::Text { layout, .. } => {
+ let local = index.saturating_sub(piece.range().start).min(layout.len());
+ layout
+ .position_for_index(local)
+ .unwrap_or_else(|| layout.bounds().origin)
+ }
+ Piece::Empty { bounds, .. } => bounds.origin,
+ Piece::Atom { range, bounds } => {
+ let x = if index >= range.end {
+ bounds.right()
+ } else {
+ bounds.left()
+ };
+ // Align with the text line: an atom taller than the line
+ // keeps the caret at its bottom line.
+ let y = (bounds.bottom() - self.line_height()).max(bounds.top());
+ point(x, y)
+ }
+ }
+ }
+
+ /// The byte index of the caret position closest to `at`: glyph halves
+ /// round to the nearer side, unlike `TextLayout::index_for_position`.
+ pub fn index_for_position(&self, at: Point<Pixels>) -> usize {
+ if let Some(layout) = self.whole() {
+ return whole_index_at(layout, at);
+ }
+ // The visual line under the point (pieces whose vertical extent
+ // holds it, else the nearest), then the horizontally nearest piece.
+ let dy = |p: &Piece| {
+ let b = p.bounds();
+ if at.y < b.top() {
+ b.top() - at.y
+ } else if at.y > b.bottom() {
+ at.y - b.bottom()
+ } else {
+ px(0.)
+ }
+ };
+ let dx = |p: &Piece| {
+ let b = p.bounds();
+ if at.x < b.left() {
+ b.left() - at.x
+ } else if at.x > b.right() {
+ at.x - b.right()
+ } else {
+ px(0.)
+ }
+ };
+ let Some(piece) = self.pieces.iter().min_by(|a, b| {
+ (dy(a), dx(a))
+ .partial_cmp(&(dy(b), dx(b)))
+ .unwrap_or(std::cmp::Ordering::Equal)
+ }) else {
+ return 0;
+ };
+ let index = match piece {
+ Piece::Whole(layout) => whole_index_at(layout, at),
+ // Fragments are one line: take the caret position nearest to the
+ // point (`closest_index_for_position` rounds up at an exact
+ // boundary inside scaled code text).
+ // Only grapheme boundaries are candidates: an interior byte of a
+ // multi-byte character maps to the next glyph's x, so it could
+ // tie with (and beat) the real boundary and split the character.
+ Piece::Text { start, layout } => {
+ let text = layout.text();
+ let nearest = text
+ .grapheme_indices(true)
+ .map(|(i, _)| i)
+ .chain(std::iter::once(text.len()))
+ .filter_map(|i| {
+ layout
+ .position_for_index(i)
+ .map(|p| (i, (p.x - at.x).abs()))
+ })
+ .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
+ .map_or(0, |(i, _)| i);
+ start + nearest
+ }
+ Piece::Empty { start, .. } => *start,
+ Piece::Atom { range, bounds } => {
+ if at.x < bounds.center().x {
+ range.start
+ } else {
+ range.end
+ }
+ }
+ };
+ index.min(self.len())
+ }
+
+ /// The byte range of the inline atom drawn under `at`, if any.
+ pub fn atom_at(&self, at: Point<Pixels>) -> Option<Range<usize>> {
+ self.pieces.iter().find_map(|piece| match piece {
+ Piece::Atom { range, bounds } if bounds.contains(&at) => Some(range.clone()),
+ _ => None,
+ })
+ }
+}
+
+/// The closest index in a whole-leaf layout for a window point.
+fn whole_index_at(layout: &LayoutSnapshot, at: Point<Pixels>) -> usize {
+ let bounds = layout.bounds();
+ let line_height = layout.line_height();
+ let mut top = bounds.top();
+ let mut start = 0;
+ let lines = layout.line_layouts();
+ for (ix, line) in lines.iter().enumerate() {
+ let height = line.size(line_height).height.max(line_height);
+ let is_last = ix + 1 == lines.len();
+ if at.y < top + height || is_last {
+ let local = point(
+ (at.x - bounds.left()).max(px(0.)),
+ (at.y - top).clamp(px(0.), (height - px(1.)).max(px(0.))),
+ );
+ let index = match line.closest_index_for_position(local, line_height) {
+ Ok(ix) | Err(ix) => ix,
+ };
+ return start + index;
+ }
+ top += height;
+ start += line.len() + 1;
+ }
+ 0
+}
+
+#[cfg(test)]
+mod tests {
+ use std::cell::Cell;
+
+ use gpui::{
+ Context, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId,
+ Render, Styled as _, TestAppContext, VisualTestContext, div, rgb, rgba,
+ };
+
+ use super::*;
+ use crate::text::{InlineElement, MarkdownNode, markdown_ext::TestInlinePlugin};
+
+ type Geometries = Rc<RefCell<HashMap<u64, LeafGeometry>>>;
+
+ /// Stands in for an editor's root element: prepaints its child inside a
+ /// `collect_leaf_geometry` scope and keeps what the leaves reported.
+ struct Probe {
+ child: Option<AnyElement>,
+ decorations: HashMap<u64, Vec<(Range<usize>, Hsla)>>,
+ out: Geometries,
+ /// Replaces `collect_leaf_geometry` with a custom sink.
+ sink: Option<Rc<dyn EditorSink>>,
+ }
+
+ impl IntoElement for Probe {
+ type Element = Self;
+ fn into_element(self) -> Self::Element {
+ self
+ }
+ }
+
+ impl Element for Probe {
+ type RequestLayoutState = AnyElement;
+ type PrepaintState = ();
+
+ fn id(&self) -> Option<ElementId> {
+ None
+ }
+
+ fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
+ None
+ }
+
+ fn request_layout(
+ &mut self,
+ _: Option<&GlobalElementId>,
+ _: Option<&InspectorElementId>,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> (LayoutId, AnyElement) {
+ let mut child = self.child.take().unwrap();
+ (child.request_layout(window, cx), child)
+ }
+
+ fn prepaint(
+ &mut self,
+ _: Option<&GlobalElementId>,
+ _: Option<&InspectorElementId>,
+ _: Bounds<Pixels>,
+ child: &mut AnyElement,
+ window: &mut Window,
+ cx: &mut App,
+ ) {
+ if let Some(sink) = self.sink.take() {
+ editor_seam::with_sink(sink, || child.prepaint(window, cx));
+ return;
+ }
+ let decorations = std::mem::take(&mut self.decorations);
+ let (_, geometries) = collect_leaf_geometry(decorations, || child.prepaint(window, cx));
+ *self.out.borrow_mut() = geometries;
+ }
+
+ fn paint(
+ &mut self,
+ _: Option<&GlobalElementId>,
+ _: Option<&InspectorElementId>,
+ _: Bounds<Pixels>,
+ child: &mut AnyElement,
+ _: &mut (),
+ window: &mut Window,
+ cx: &mut App,
+ ) {
+ child.paint(window, cx);
+ }
+ }
+
+ struct Host {
+ blocks: Vec<BlockNode>,
+ extensions: Arc<MarkdownExtensions>,
+ decorations: HashMap<u64, Vec<(Range<usize>, Hsla)>>,
+ out: Geometries,
+ width: Pixels,
+ sink: Option<Rc<dyn EditorSink>>,
+ }
+
+ impl Render for Host {
+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let rcx = RenderContext::from_app(cx)
+ .with_markdown_extensions(self.extensions.clone())
+ .with_body_font_size(px(16.));
+ let refs: Vec<&BlockNode> = self.blocks.iter().collect();
+ let children: Vec<AnyElement> = refs
+ .iter()
+ .enumerate()
+ .map(|(ix, block)| {
+ let flow = FlowPosition::at(&refs, ix, ix + 1 == refs.len());
+ block.render(ix, flow, &rcx, window, cx)
+ })
+ .collect();
+ Probe {
+ child: Some(div().w(self.width).children(children).into_any_element()),
+ decorations: self.decorations.clone(),
+ out: self.out.clone(),
+ sink: self.sink.clone(),
+ }
+ }
+ }
+
+ fn draw(
+ cx: &mut TestAppContext,
+ blocks: Vec<BlockNode>,
+ decorations: HashMap<u64, Vec<(Range<usize>, Hsla)>>,
+ extensions: Arc<MarkdownExtensions>,
+ width: Pixels,
+ ) -> HashMap<u64, LeafGeometry> {
+ draw_and_inspect(cx, blocks, decorations, extensions, width, |_| ()).0
+ }
+
+ fn draw_and_inspect<R>(
+ cx: &mut TestAppContext,
+ blocks: Vec<BlockNode>,
+ decorations: HashMap<u64, Vec<(Range<usize>, Hsla)>>,
+ extensions: Arc<MarkdownExtensions>,
+ width: Pixels,
+ inspect: impl FnOnce(&mut Window) -> R,
+ ) -> (HashMap<u64, LeafGeometry>, R) {
+ cx.update(crate::init);
+ let out = Geometries::default();
+ let (_, vcx) = cx.add_window_view({
+ let out = out.clone();
+ move |_, _| Host {
+ blocks,
+ extensions,
+ decorations,
+ out,
+ width,
+ sink: None,
+ }
+ });
+ settle(vcx);
+ let inspected = vcx.update(|window, _| inspect(window));
+ (out.take(), inspected)
+ }
+
+ fn settle(cx: &mut VisualTestContext) {
+ cx.run_until_parked();
+ cx.update(|window, cx| window.draw(cx).clear(cx));
+ cx.run_until_parked();
+ }
+
+ fn plain(text: &str) -> Paragraph {
+ let mut paragraph = Paragraph::default();
+ paragraph.push(InlineNode::new(text.to_string()));
+ paragraph
+ }
+
+ fn atom_plugin() -> Arc<MarkdownExtensions> {
+ Arc::new(
+ MarkdownExtensions::default().plugin(TestInlinePlugin::new("atom").render_with(
+ |_, _, _, _| {
+ Some(InlineElement::new(div().w(px(24.)).h(px(16.))).with_baseline(px(12.)))
+ },
+ )),
+ )
+ }
+
+ fn atom() -> InlineNode {
+ InlineNode::custom(MarkdownNode::new("atom", ()).text("\u{FFFC}"))
+ }
+
+ #[gpui::test]
+ fn keyed_leaves_report_geometry_and_unkeyed_leaves_do_not(cx: &mut TestAppContext) {
+ let blocks = vec![
+ BlockNode::Heading {
+ level: 1,
+ children: plain("Title").with_leaf_key(1),
+ span: None,
+ },
+ BlockNode::Paragraph(plain("first").with_leaf_key(2)),
+ BlockNode::Paragraph(plain("not an editor leaf")),
+ BlockNode::CodeBlock(
+ CodeBlock::new("let x = 1;".into(), Some("rust".into()), None::<Span>)
+ .with_leaf_key(3),
+ ),
+ BlockNode::Table(Table::new(
+ vec![
+ vec![plain("a").with_leaf_key(4), plain("b").with_leaf_key(5)],
+ vec![plain("c").with_leaf_key(6), plain("d").with_leaf_key(7)],
+ ],
+ vec![ColumnAlign::None, ColumnAlign::Right],
+ )),
+ BlockNode::List {
+ children: vec![BlockNode::ListItem {
+ children: vec![BlockNode::Paragraph(plain("item").with_leaf_key(8))],
+ spread: false,
+ checked: None,
+ span: None,
+ }],
+ ordered: false,
+ start: None,
+ span: None,
+ },
+ ];
+ let geometries = draw(cx, blocks, HashMap::new(), Default::default(), px(300.));
+ let mut keys: Vec<u64> = geometries.keys().copied().collect();
+ keys.sort_unstable();
+ assert_eq!(keys, (1..=8).collect::<Vec<_>>());
+ for key in [1, 2, 3, 8] {
+ assert!(geometries[&key].bounds().size.height > px(0.), "leaf {key}");
+ }
+ assert_eq!(geometries[&1].len(), "Title".len());
+ assert_eq!(geometries[&3].len(), "let x = 1;".len());
+ // Document order is top to bottom.
+ let top = |key: u64| geometries[&key].bounds().top();
+ assert!(top(1) < top(2) && top(2) < top(3) && top(3) < top(4) && top(7) < top(8));
+ // Cells of one row share a line; the second column sits to the right.
+ assert_eq!(top(4), top(5));
+ assert!(geometries[&5].bounds().left() > geometries[&4].bounds().right() - px(1.));
+ }
+
+ #[gpui::test]
+ fn wrapped_leaf_maps_indices_and_positions(cx: &mut TestAppContext) {
+ let text = "the quick brown fox jumps over the lazy dog and keeps running far away";
+ let blocks = vec![BlockNode::Paragraph(plain(text).with_leaf_key(1))];
+ let geometries = draw(cx, blocks, HashMap::new(), Default::default(), px(120.));
+ let leaf = &geometries[&1];
+ assert_eq!(leaf.len(), text.len());
+ assert!(!leaf.is_empty());
+ let line = leaf.line_height();
+ assert!(line > px(0.) && leaf.caret_height() <= line);
+ let mut last_y = px(-1.);
+ let mut lines = std::collections::BTreeSet::new();
+ for index in 0..=text.len() {
+ let caret = leaf.position_for_index(index);
+ assert!(caret.y >= last_y, "carets never move up");
+ last_y = caret.y;
+ lines.insert((caret.y.as_f32() * 100.) as i64);
+ // Hit-testing at a caret lands on that caret (at a soft wrap the
+ // end of a line and the start of the next share a caret index).
+ let hit = leaf.index_for_position(point(caret.x, caret.y + line / 2.));
+ assert_eq!(
+ leaf.position_for_index(hit),
+ caret,
+ "index {index} hit {hit}"
+ );
+ }
+ assert!(lines.len() >= 3, "the text wraps onto several lines");
+ // Far outside the leaf clamps to its ends.
+ assert_eq!(leaf.index_for_position(point(px(-500.), px(-500.))), 0);
+ assert_eq!(
+ leaf.index_for_position(point(px(5000.), px(5000.))),
+ text.len()
+ );
+ }
+
+ #[gpui::test]
+ fn mixed_text_and_atoms_report_boxes(cx: &mut TestAppContext) {
+ let mut paragraph = Paragraph::default();
+ paragraph.push(InlineNode::new("ab"));
+ paragraph.push(atom());
+ paragraph.push(InlineNode::new("cd"));
+ let atom_len = '\u{FFFC}'.len_utf8();
+ let blocks = vec![BlockNode::Paragraph(paragraph.with_leaf_key(1))];
+ let geometries = draw(cx, blocks, HashMap::new(), atom_plugin(), px(300.));
+ let leaf = &geometries[&1];
+ assert_eq!(leaf.len(), 2 + atom_len + 2);
+ // The box is hit-testable, and only the box.
+ let range = 2..2 + atom_len;
+ let before = leaf.position_for_index(2);
+ let after = leaf.position_for_index(2 + atom_len);
+ assert!(after.x > before.x, "the caret moves across the atom");
+ let inside = point(
+ (before.x + after.x) / 2.,
+ before.y + leaf.line_height() / 2.,
+ );
+ assert_eq!(leaf.atom_at(inside), Some(range.clone()));
+ assert_eq!(leaf.atom_at(point(before.x - px(8.), inside.y)), None);
+ // Clicking either half of the atom puts the caret on its near edge.
+ let quarter = |t: f32| point(before.x + (after.x - before.x) * t, inside.y);
+ assert_eq!(leaf.index_for_position(quarter(0.25)), range.start);
+ assert_eq!(leaf.index_for_position(quarter(0.75)), range.end);
+ // Text on both sides keeps its own indices.
+ let end = leaf.position_for_index(leaf.len());
+ assert_eq!(
+ leaf.index_for_position(point(end.x + px(40.), inside.y)),
+ leaf.len()
+ );
+ assert_eq!(leaf.index_for_position(point(px(-40.), inside.y)), 0);
+ }
+
+ #[gpui::test]
+ fn mixed_flow_hits_land_on_grapheme_boundaries(cx: &mut TestAppContext) {
+ // Accented, CJK and emoji text (a skin-tone sequence is one grapheme
+ // of two chars) on both sides of atoms, so the leaf is a mixed flow.
+ let parts = ["éa", "\u{FFFC}", "中文", "\u{FFFC}", "👍🏽x"];
+ let mut paragraph = Paragraph::default();
+ for part in parts {
+ if part == "\u{FFFC}" {
+ paragraph.push(atom());
+ } else {
+ paragraph.push(InlineNode::new(part));
+ }
+ }
+ let text = parts.concat();
+ let blocks = vec![BlockNode::Paragraph(paragraph.with_leaf_key(1))];
+ let geometries = draw(cx, blocks, HashMap::new(), atom_plugin(), px(400.));
+ let leaf = &geometries[&1];
+ assert_eq!(leaf.len(), text.len());
+ let boundaries: Vec<usize> = text
+ .grapheme_indices(true)
+ .map(|(i, _)| i)
+ .chain(std::iter::once(text.len()))
+ .collect();
+
+ // Sweep the whole line: every hit is a grapheme boundary that an
+ // edit can use.
+ let bounds = leaf.bounds();
+ let y = bounds.top() + leaf.line_height() / 2.;
+ let mut x = bounds.left() - px(4.);
+ while x <= bounds.right() + px(4.) {
+ let index = leaf.index_for_position(point(x, y));
+ assert!(
+ boundaries.contains(&index),
+ "hit at {x:?} returned {index}, inside a grapheme of {text:?}"
+ );
+ let mut edited = text.clone();
+ edited.insert(index, '|');
+ x += px(0.5);
+ }
+
+ // The caret x of each boundary hits that boundary, never a byte
+ // inside the character before it (`é`: offsets 1 and 2 share an x).
+ for &index in &boundaries {
+ let at = leaf.position_for_index(index);
+ assert_eq!(
+ leaf.index_for_position(point(at.x, y)),
+ index,
+ "caret position of {index} in {text:?}"
+ );
+ }
+ }
+
+ #[gpui::test]
+ fn mixed_flow_keeps_empty_lines_and_trailing_breaks(cx: &mut TestAppContext) {
+ let mut paragraph = Paragraph::default();
+ paragraph.push(atom());
+ paragraph.push(InlineNode::new("a\n\n"));
+ let mut breaks = Paragraph::default();
+ breaks.push(atom());
+ breaks.push(InlineNode::new("\n\n"));
+ let leaves = draw(
+ cx,
+ vec![
+ BlockNode::Paragraph(paragraph.with_leaf_key(1)),
+ BlockNode::Paragraph(breaks.with_leaf_key(2)),
+ ],
+ HashMap::new(),
+ atom_plugin(),
+ px(300.),
+ );
+ let leaf = &leaves[&1];
+ assert_eq!(leaf.len(), 6);
+ let a = leaf.position_for_index(3);
+ let empty = leaf.position_for_index(5);
+ let end = leaf.position_for_index(6);
+ assert!(a.y < empty.y && empty.y < end.y);
+ for index in [5, 6] {
+ let caret = leaf.position_for_index(index);
+ assert_eq!(
+ leaf.index_for_position(point(
+ caret.x + px(20.),
+ caret.y + leaf.line_height() / 2.
+ )),
+ index
+ );
+ }
+ assert!(leaf.bounds().bottom() >= end.y + leaf.line_height());
+ let breaks = &leaves[&2];
+ assert_eq!(breaks.len(), 5);
+ assert!(breaks.line_height() < breaks.bounds().size.height);
+ assert!(breaks.position_for_index(5).y > breaks.position_for_index(4).y);
+ }
+
+ #[gpui::test]
+ fn atom_decorations_are_painted_for_custom_nodes_and_images(cx: &mut TestAppContext) {
+ let color: Hsla = rgba(0x12345680).into();
+ let mut custom = Paragraph::default();
+ custom.push(atom());
+ let mut image = Paragraph::default();
+ image.push(InlineNode::image(ImageNode {
+ url: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20'/>".into(),
+ width: Some(px(20.).into()), height: Some(px(20.).into()),
+ ..Default::default()
+ }));
+ let blocks = vec![
+ BlockNode::Paragraph(custom.with_leaf_key(1)),
+ BlockNode::Paragraph(image.with_leaf_key(2)),
+ ];
+ let decorations = HashMap::from([
+ (1, vec![(0..3, color), (1..1, color)]),
+ (2, vec![(0..3, color), (3..6, color)]),
+ ]);
+ let (_, painted) =
+ draw_and_inspect(cx, blocks, decorations, atom_plugin(), px(300.), |window| {
+ let background: gpui::Background = color.into();
+ window
+ .painted_quads()
+ .into_iter()
+ .filter(|quad| quad.background == background)
+ .count()
+ });
+ assert_eq!(
+ painted, 2,
+ "one overlay per atom; empty and adjacent ranges paint nothing"
+ );
+ }
+
+ #[gpui::test]
+ fn rekeying_cloned_leaves_preserves_each_identity(cx: &mut TestAppContext) {
+ let original = plain("shared source").with_leaf_key(1);
+ let copy = original.clone().with_leaf_key(2);
+ let code = CodeBlock::new("let x = 1;".into(), None, None::<Span>).with_leaf_key(3);
+ let copy_code = code.clone().with_leaf_key(4);
+ let leaves = draw(
+ cx,
+ vec![
+ BlockNode::Paragraph(original),
+ BlockNode::Paragraph(copy),
+ BlockNode::CodeBlock(code),
+ BlockNode::CodeBlock(copy_code),
+ ],
+ HashMap::new(),
+ Default::default(),
+ px(300.),
+ );
+ assert_eq!(leaves.len(), 4);
+ assert_eq!(leaves[&1].len(), "shared source".len());
+ assert_eq!(leaves[&3].len(), "let x = 1;".len());
+ assert!(leaves[&1].bounds().top() < leaves[&2].bounds().top());
+ assert!(leaves[&3].bounds().top() < leaves[&4].bounds().top());
+ }
+
+ #[gpui::test]
+ fn geometry_is_an_immutable_snapshot(cx: &mut TestAppContext) {
+ cx.update(crate::init);
+ let out = Geometries::default();
+ let (host, vcx) = cx.add_window_view({
+ let out = out.clone();
+ move |_, _| Host {
+ blocks: vec![BlockNode::Paragraph(
+ plain("a long sentence that wraps when the viewport gets narrower")
+ .with_leaf_key(1),
+ )],
+ extensions: Default::default(),
+ decorations: HashMap::new(),
+ out,
+ width: px(600.),
+ sink: None,
+ }
+ });
+ settle(vcx);
+ let old = out.borrow()[&1].clone();
+ let before = old.bounds();
+ vcx.update(|_, cx| {
+ host.update(cx, |host, cx| {
+ host.width = px(80.);
+ cx.notify();
+ })
+ });
+ settle(vcx);
+ assert!(out.borrow()[&1].bounds().size.height > before.size.height);
+ assert_eq!(old.bounds(), before);
+ assert_eq!(old.position_for_index(0), before.origin);
+ }
+
+ #[gpui::test]
+ fn empty_keyed_leaf_has_a_line_box_and_empty_unkeyed_leaf_stays_empty(cx: &mut TestAppContext) {
+ let blocks = vec![
+ BlockNode::Paragraph(Paragraph::default()),
+ BlockNode::Paragraph(Paragraph::default().with_leaf_key(9)),
+ ];
+ let geometries = draw(cx, blocks, HashMap::new(), Default::default(), px(300.));
+ assert_eq!(geometries.len(), 1);
+ let leaf = &geometries[&9];
+ assert!(leaf.is_empty());
+ assert!(leaf.bounds().size.height > px(0.));
+ assert_eq!(leaf.position_for_index(0).x, leaf.bounds().left());
+ assert_eq!(
+ leaf.index_for_position(point(px(50.), leaf.bounds().top())),
+ 0
+ );
+ }
+
+ #[gpui::test]
+ fn image_only_leaf_reports_its_box(cx: &mut TestAppContext) {
+ let image = ImageNode {
+ url: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20'/>".into(),
+ width: Some(px(20.).into()),
+ height: Some(px(20.).into()),
+ ..Default::default()
+ };
+ let mut paragraph = Paragraph::default();
+ paragraph.push(InlineNode::image(image));
+ let blocks = vec![BlockNode::Paragraph(paragraph.with_leaf_key(1))];
+ let geometries = draw(cx, blocks, HashMap::new(), Default::default(), px(300.));
+ let leaf = &geometries[&1];
+ let atom_len = '\u{FFFC}'.len_utf8();
+ assert_eq!(leaf.len(), atom_len);
+ let inside = leaf.bounds().center();
+ assert_eq!(leaf.atom_at(inside), Some(0..atom_len));
+ }
+
+ /// Records which leaves ask for backgrounds, to prove decorations are
+ /// requested for keyed leaves only.
+ #[derive(Default)]
+ struct Recorder {
+ asked: RefCell<Vec<u64>>,
+ }
+
+ impl EditorSink for Recorder {
+ fn report(&self, _: u64, _: &TextLayout) {}
+ fn begin_pieces(&self, _: u64) {}
+ fn report_text_piece(&self, _: u64, _: usize, _: &TextLayout) {}
+ fn report_box_piece(&self, _: u64, _: Range<usize>, _: Bounds<Pixels>) {}
+ fn report_empty_piece(&self, _: u64, _: usize, _: Bounds<Pixels>) {}
+ fn backgrounds(&self, key: u64) -> Vec<(Range<usize>, Hsla)> {
+ self.asked.borrow_mut().push(key);
+ vec![(0..1, rgb(0xff0000).into())]
+ }
+ }
+
+ #[gpui::test]
+ fn decorations_are_requested_per_keyed_leaf(cx: &mut TestAppContext) {
+ cx.update(crate::init);
+ let recorder = Rc::new(Recorder::default());
+ let mut mixed = Paragraph::default();
+ mixed.push(InlineNode::new("ab"));
+ mixed.push(atom());
+ let blocks = vec![
+ BlockNode::Paragraph(plain("one").with_leaf_key(1)),
+ BlockNode::Paragraph(plain("unkeyed")),
+ BlockNode::Paragraph(mixed.with_leaf_key(2)),
+ ];
+ let (_, vcx) = cx.add_window_view({
+ let sink: Rc<dyn EditorSink> = recorder.clone();
+ move |_, _| Host {
+ blocks,
+ extensions: atom_plugin(),
+ decorations: HashMap::new(),
+ out: Geometries::default(),
+ width: px(300.),
+ sink: Some(sink),
+ }
+ });
+ settle(vcx);
+ let asked = recorder.asked.borrow();
+ assert!(asked.contains(&1) && asked.contains(&2), "{asked:?}");
+ assert!(asked.iter().all(|key| *key == 1 || *key == 2), "{asked:?}");
+ }
+
+ #[gpui::test]
+ fn decorated_leaves_still_report_geometry(cx: &mut TestAppContext) {
+ let blocks = vec![BlockNode::Paragraph(plain("hello world").with_leaf_key(1))];
+ let decorations = HashMap::from([(
+ 1,
+ vec![
+ (0..5, rgba(0x3366ff80).into()),
+ (6..11, rgb(0xffcc00).into()),
+ ],
+ )]);
+ let plain_geometry = draw(
+ cx,
+ blocks.clone(),
+ HashMap::new(),
+ Default::default(),
+ px(300.),
+ );
+ let decorated = draw(cx, blocks, decorations, Default::default(), px(300.));
+ assert_eq!(
+ decorated[&1].bounds().size,
+ plain_geometry[&1].bounds().size,
+ "decorations paint behind the text and never change layout"
+ );
+ }
+
+ #[test]
+ fn collect_leaf_geometry_nests_and_restores_on_panic() {
+ let seen = Cell::new(0);
+ let (inner_ran, geometries) = collect_leaf_geometry(HashMap::new(), || {
+ assert!(editor_seam::current().is_some());
+ let outer = editor_seam::current().unwrap();
+ collect_leaf_geometry(HashMap::new(), || seen.set(seen.get() + 1));
+ let after = editor_seam::current().unwrap();
+ assert!(Rc::ptr_eq(&outer, &after), "the outer scope is restored");
+ true
+ });
+ assert!(inner_ran && geometries.is_empty() && seen.get() == 1);
+ assert!(editor_seam::current().is_none());
+
+ let result = std::panic::catch_unwind(|| {
+ collect_leaf_geometry(HashMap::new(), || panic!("boom"));
+ });
+ assert!(result.is_err());
+ assert!(editor_seam::current().is_none(), "cleared after a panic");
+ }
+
+ #[test]
+ fn parse_markdown_reads_what_an_editor_round_trips() {
+ let source = "```rust title=\"x\"\nfn main() {}\n```\n\n\
+ | a | b | c | d |\n|---|:--|:-:|--:|\n| 1 | 2 | 3 | 4 |\n\n\
+ [ref]: https://example.com \"Title\"\n";
+ let blocks = parse_markdown(source, Default::default()).unwrap();
+ let code = blocks
+ .iter()
+ .find_map(|block| match block {
+ BlockNode::CodeBlock(code) => Some(code),
+ _ => None,
+ })
+ .expect("code block");
+ assert_eq!(code.lang().as_deref(), Some("rust"));
+ assert_eq!(code.meta().as_deref(), Some("title=\"x\""));
+ let table = blocks
+ .iter()
+ .find_map(|block| match block {
+ BlockNode::Table(table) => Some(table),
+ _ => None,
+ })
+ .expect("table");
+ assert_eq!(
+ table.column_aligns(),
+ [
+ ColumnAlign::None,
+ ColumnAlign::Left,
+ ColumnAlign::Center,
+ ColumnAlign::Right
+ ]
+ );
+ let cells: Vec<Vec<String>> = table
+ .rows()
+ .map(|row| {
+ row.map(|cell| cell.children().iter().map(|c| c.text()).collect())
+ .collect()
+ })
+ .collect();
+ assert_eq!(cells, [["a", "b", "c", "d"], ["1", "2", "3", "4"]]);
+ assert!(blocks.iter().any(|block| matches!(
+ block,
+ BlockNode::Definition { identifier, .. } if identifier.as_ref() == "ref"
+ )));
+ assert!(
+ blocks
+ .iter()
+ .all(|block| block.span().is_some() || matches!(block, BlockNode::Unknown))
+ );
+ }
+
+ #[test]
+ fn parse_markdown_matches_text_view_parse() {
+ let source = "# Title\n\nSome **bold** and `code` and ==mark==.\n\n- one\n- two\n";
+ let mut cx = NodeContext::default();
+ let expected = markdown::parse(source, &mut cx).unwrap();
+ let blocks = parse_markdown(source, Default::default()).unwrap();
+ assert_eq!(blocks, *expected.blocks);
+ }
+
+ #[test]
+ fn table_and_paragraph_builders_read_back() {
+ let mut paragraph = Paragraph::default();
+ paragraph.push(InlineNode::new("hi").marks(vec![(0..2, TextMark::default().bold())]));
+ assert_eq!(paragraph.children()[0].text(), "hi");
+ assert!(paragraph.children()[0].mark_runs()[0].1.bold);
+ assert!(paragraph.children()[0].as_image().is_none());
+ assert!(paragraph.children()[0].as_custom().is_none());
+ let table = Table::new(vec![vec![paragraph]], vec![ColumnAlign::Center]);
+ assert_eq!(table.rows().count(), 1);
+ assert_eq!(table.column_aligns(), [ColumnAlign::Center]);
+ }
+}
diff --git a/third-party/gpui-kit/crates/base/src/text/editor_seam.rs b/third-party/gpui-kit/crates/base/src/text/editor_seam.rs
new file mode 100644
index 0000000000..869432f04b
--- /dev/null
+++ b/third-party/gpui-kit/crates/base/src/text/editor_seam.rs
@@ -0,0 +1,86 @@
+//! Crate-private plumbing behind [`editor_host`](super::editor_host).
+//!
+//! A text leaf whose [`InlineState`](super::inline::InlineState) carries an
+//! `editor_key` reports its laid-out [`TextLayout`] to the sink installed for
+//! the duration of `collect_leaf_geometry`, and takes extra range backgrounds
+//! (the host's selection and decorations) from it. `TextView` never sets a key
+//! or installs a sink, so read-only rendering is unchanged: outside a scope the
+//! only cost is one thread-local read per laid-out text element.
+//!
+//! The sink is frame-local: it is written during prepaint and read by the
+//! host afterwards; nothing here reaches back into an entity.
+
+use std::{cell::RefCell, ops::Range, rc::Rc};
+
+use gpui::{Bounds, Hsla, Pixels, TextLayout};
+
+pub(crate) trait EditorSink {
+ /// The leaf `key` was laid out this frame as one `TextLayout`.
+ fn report(&self, key: u64, layout: &TextLayout);
+ /// The leaf `key` is laid out in pieces this frame (a mixed text /
+ /// image / custom-inline paragraph rendered by `InlineFlow`); drops
+ /// pieces reported for it earlier in the frame.
+ fn begin_pieces(&self, key: u64);
+ /// One single-line text fragment of `key`, whose index 0 is the leaf's
+ /// byte `start`.
+ fn report_text_piece(&self, key: u64, start: usize, layout: &TextLayout);
+ /// An inline atom (image, custom node) of `key` covering the leaf bytes
+ /// `range`, drawn in `bounds`.
+ fn report_box_piece(&self, key: u64, range: Range<usize>, bounds: Bounds<Pixels>);
+ /// A line with no fragment (e.g. after a trailing hard break).
+ fn report_empty_piece(&self, key: u64, start: usize, bounds: Bounds<Pixels>);
+ /// Extra backgrounds to paint behind the leaf `key`, in its text's bytes.
+ fn backgrounds(&self, key: u64) -> Vec<(Range<usize>, Hsla)>;
+}
+
+thread_local! {
+ static SINK: RefCell<Option<Rc<dyn EditorSink>>> = const { RefCell::new(None) };
+}
+
+/// Run `f` with `sink` installed; restores the previous sink afterwards.
+pub(crate) fn with_sink<R>(sink: Rc<dyn EditorSink>, f: impl FnOnce() -> R) -> R {
+ /// Restores the previous sink on drop, so a panic inside `f` cannot leave
+ /// a stale sink installed for later frames.
+ struct Restore(Option<Rc<dyn EditorSink>>);
+ impl Drop for Restore {
+ fn drop(&mut self) {
+ let previous = self.0.take();
+ SINK.with(|s| *s.borrow_mut() = previous);
+ }
+ }
+ let _restore = Restore(SINK.with(|s| s.replace(Some(sink))));
+ f()
+}
+
+pub(crate) fn current() -> Option<Rc<dyn EditorSink>> {
+ SINK.with(|s| s.borrow().clone())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ struct Nop;
+ impl EditorSink for Nop {
+ fn report(&self, _: u64, _: &TextLayout) {}
+ fn begin_pieces(&self, _: u64) {}
+ fn report_text_piece(&self, _: u64, _: usize, _: &TextLayout) {}
+ fn report_box_piece(&self, _: u64, _: Range<usize>, _: Bounds<Pixels>) {}
+ fn report_empty_piece(&self, _: u64, _: usize, _: Bounds<Pixels>) {}
+ fn backgrounds(&self, _: u64) -> Vec<(Range<usize>, Hsla)> {
+ Vec::new()
+ }
+ }
+
+ #[test]
+ fn with_sink_restores_on_panic() {
+ let result = std::panic::catch_unwind(|| {
+ with_sink(Rc::new(Nop), || {
+ assert!(current().is_some());
+ panic!("boom");
+ })
+ });
+ assert!(result.is_err());
+ assert!(current().is_none(), "sink is cleared after a panic");
+ }
+}
diff --git a/third-party/gpui-kit/crates/base/src/text/format/html.rs b/third-party/gpui-kit/crates/base/src/text/format/html.rs
index f0c4066409..c564e79892 100644
--- a/third-party/gpui-kit/crates/base/src/text/format/html.rs
+++ b/third-party/gpui-kit/crates/base/src/text/format/html.rs
@@ -53,6 +53,45 @@ const BLOCK_ELEMENTS: [&str; 35] = [
"script",
];
+/// A paragraph under construction. A literal line feed is content; only a
+/// trailing HTML break is removed when the complete text block is taken.
+#[derive(Default)]
+struct HtmlParagraph {
+ content: Paragraph,
+ ends_with_break: bool,
+}
+
+impl HtmlParagraph {
+ fn push(&mut self, node: InlineNode) {
+ self.content.push(node);
+ self.ends_with_break = false;
+ }
+
+ fn push_str(&mut self, text: &str) {
+ if !text.is_empty() {
+ self.content.push_str(text);
+ self.ends_with_break = false;
+ }
+ }
+
+ fn push_image(&mut self, image: ImageNode) {
+ self.push(InlineNode::image(image));
+ }
+
+ fn push_break(&mut self) {
+ self.content.push(InlineNode::new("\n"));
+ self.ends_with_break = true;
+ }
+
+ fn take(&mut self) -> Paragraph {
+ if self.ends_with_break {
+ trim_trailing_break(&mut self.content);
+ }
+ self.ends_with_break = false;
+ self.content.take()
+ }
+}
+
/// Parse HTML into AST Node.
pub(crate) fn parse(
source: &str,
@@ -76,7 +115,7 @@ pub(crate) fn parse(
fragment_start,
next_anchor: 0,
};
- let mut paragraph = Paragraph::default();
+ let mut paragraph = HtmlParagraph::default();
// NOTE: The outer paragraph is not used.
let node: BlockNode =
parse_node(&dom.document, &mut paragraph, cx, &mut links).unwrap_or(BlockNode::Unknown);
@@ -270,13 +309,13 @@ fn parse_table_cell(
attrs: &RefCell<Vec<html5ever::Attribute>>,
links: &mut HtmlLinks,
) {
- let mut paragraph = Paragraph::default();
+ let mut paragraph = HtmlParagraph::default();
for child in node.children.borrow().iter() {
parse_paragraph(&mut paragraph, child, links);
}
let width = attr_width_height(attrs).0;
let table_cell = node::TableCell {
- children: paragraph,
+ children: paragraph.take(),
width,
};
row.children.push(table_cell);
@@ -302,9 +341,9 @@ fn trim_text(text: &str) -> String {
out
}
-fn parse_paragraph(paragraph: &mut Paragraph, node: &Rc<Node>, links: &mut HtmlLinks) {
+fn parse_paragraph(paragraph: &mut HtmlParagraph, node: &Rc<Node>, links: &mut HtmlLinks) {
fn push_merged(
- paragraph: &mut Paragraph,
+ paragraph: &mut HtmlParagraph,
text: String,
marks: Vec<(Range<usize>, TextMark)>,
new_mark: Option<TextMark>,
@@ -329,18 +368,22 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &Rc<Node>, links: &mut HtmlL
fn merge_children_with_mark(
node: &Node,
- paragraph: &mut Paragraph,
+ paragraph: &mut HtmlParagraph,
new_mark: Option<TextMark>,
links: &mut HtmlLinks,
) {
let mut merged_text = String::new();
let mut merged_marks = Vec::new();
+ let mut ends_with_break = None;
for child in node.children.borrow().iter() {
- let mut child_paragraph = Paragraph::default();
+ let mut child_paragraph = HtmlParagraph::default();
parse_paragraph(&mut child_paragraph, &child, links);
- for node in child_paragraph.children {
+ if !child_paragraph.content.is_empty() {
+ ends_with_break = Some(child_paragraph.ends_with_break);
+ }
+ for node in child_paragraph.content.children {
let offset = merged_text.len();
merged_text.push_str(&node.text);
for (range, child_mark) in node.marks {
@@ -365,6 +408,9 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &Rc<Node>, links: &mut HtmlL
}
push_merged(paragraph, merged_text, merged_marks, new_mark.clone());
+ if let Some(ends_with_break) = ends_with_break {
+ paragraph.ends_with_break = ends_with_break;
+ }
}
match &node.data {
@@ -429,6 +475,9 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &Rc<Node>, links: &mut HtmlL
links,
);
}
+ local_name!("br") => {
+ paragraph.push_break();
+ }
local_name!("img") => {
let Some(src) = attr_value(attrs, local_name!("src")) else {
if cfg!(debug_assertions) {
@@ -462,7 +511,7 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &Rc<Node>, links: &mut HtmlL
fn parse_node(
node: &Rc<Node>,
- paragraph: &mut Paragraph,
+ paragraph: &mut HtmlParagraph,
cx: &mut NodeContext,
links: &mut HtmlLinks,
) -> Option<BlockNode> {
@@ -480,10 +529,13 @@ fn parse_node(
ref attrs,
..
} => match name.local {
- local_name!("br") => Some(BlockNode::Break {
- html: true,
- span: None,
- }),
+ // A line break inside the paragraph being built, as a markdown
+ // hard break becomes. As a block of its own it landed apart from
+ // the text on both sides, which then stayed on one line.
+ local_name!("br") => {
+ paragraph.push_break();
+ None
+ }
local_name!("h1")
| local_name!("h2")
| local_name!("h3")
@@ -501,14 +553,14 @@ fn parse_node(
.to_digit(10)
.unwrap_or(6) as u8;
- let mut paragraph = Paragraph::default();
+ let mut paragraph = HtmlParagraph::default();
for child in node.children.borrow().iter() {
parse_paragraph(&mut paragraph, child, links);
}
let heading = BlockNode::Heading {
level,
- children: paragraph,
+ children: paragraph.take(),
span: None,
};
if children.len() > 0 {
@@ -537,7 +589,7 @@ fn parse_node(
let title = attr_value(&attrs, local_name!("title"));
let (width, height) = attr_width_height(&attrs);
- let mut paragraph = Paragraph::default();
+ let mut paragraph = HtmlParagraph::default();
paragraph.push_image(ImageNode {
url: src.into(),
title: title.map(Into::into),
@@ -548,13 +600,13 @@ fn parse_node(
});
if children.len() > 0 {
- children.push(BlockNode::Paragraph(paragraph));
+ children.push(BlockNode::Paragraph(paragraph.take()));
Some(BlockNode::Root {
children,
span: None,
})
} else {
- Some(BlockNode::Paragraph(paragraph))
+ Some(BlockNode::Paragraph(paragraph.take()))
}
}
local_name!("ul") | local_name!("ol") => {
@@ -571,24 +623,30 @@ fn parse_node(
let mut children = vec![];
consume_paragraph(&mut children, paragraph);
+ let mut ends_with_break = false;
for child in node.children.borrow().iter() {
- let mut child_paragraph = Paragraph::default();
+ let mut child_paragraph = HtmlParagraph::default();
if let Some(child_node) = parse_node(child, &mut child_paragraph, cx, links) {
children.push(child_node);
+ ends_with_break = false;
}
- if child_paragraph.text_len() > 0 {
+ if child_paragraph.content.text_len() > 0 {
+ ends_with_break = child_paragraph.ends_with_break;
// If last child is paragraph, merge child
if let Some(last_child) = children.last_mut() {
if let BlockNode::Paragraph(last_paragraph) = last_child {
- last_paragraph.merge(child_paragraph);
+ last_paragraph.merge(child_paragraph.content);
continue;
}
}
- children.push(BlockNode::Paragraph(child_paragraph));
+ children.push(BlockNode::Paragraph(child_paragraph.content));
}
}
+ if ends_with_break && let Some(BlockNode::Paragraph(last)) = children.last_mut() {
+ trim_trailing_break(last);
+ }
consume_paragraph(&mut children, paragraph);
Some(BlockNode::ListItem {
@@ -670,7 +728,7 @@ fn parse_node(
// Others to as Inline
parse_paragraph(paragraph, node, links);
- if paragraph.is_image() {
+ if paragraph.content.is_image() {
Some(BlockNode::Paragraph(paragraph.take()))
} else {
None
@@ -693,7 +751,7 @@ fn parse_node(
fn consume_children_nodes(
node: &Node,
- paragraph: &mut Paragraph,
+ paragraph: &mut HtmlParagraph,
cx: &mut NodeContext,
links: &mut HtmlLinks,
) -> Vec<BlockNode> {
@@ -709,14 +767,41 @@ fn consume_children_nodes(
children
}
-fn consume_paragraph(children: &mut Vec<BlockNode>, paragraph: &mut Paragraph) {
- if paragraph.is_empty() {
+fn consume_paragraph(children: &mut Vec<BlockNode>, paragraph: &mut HtmlParagraph) {
+ if paragraph.content.is_empty() {
return;
}
children.push(BlockNode::Paragraph(paragraph.take()));
}
+/// Drops one trailing HTML break after the parser has established its
+/// provenance. The break may be a separate node or part of a marked run.
+/// A standalone break is retained for Markdown's inline-HTML parser.
+fn trim_trailing_break(paragraph: &mut Paragraph) {
+ if let [only] = paragraph.children.as_slice()
+ && only.text == "\n"
+ {
+ return;
+ }
+ let Some(last) = paragraph.children.last_mut() else {
+ return;
+ };
+ let Some(text) = last.text.strip_suffix('\n').map(str::to_string) else {
+ return;
+ };
+ if text.is_empty() {
+ paragraph.children.pop();
+ return;
+ }
+ let len = text.len();
+ last.text = text.into();
+ last.marks.retain_mut(|(range, _)| {
+ range.end = range.end.min(len);
+ range.start < range.end
+ });
+}
+
#[cfg(test)]
mod tests {
use gpui::{px, relative};
@@ -779,6 +864,35 @@ mod tests {
}
}
+ #[test]
+ fn test_pre_keeps_whitespace_in_nested_elements() {
+ let mut cx = NodeContext::default();
+ for (html, text) in [
+ // Code in `<pre><code>` keeps its line feeds and indent.
+ (
+ "<pre><code>fn main() {\n one();\n}</code></pre>",
+ "fn main() {\n one();\n}",
+ ),
+ // So does text deeper down, and the space between elements.
+ (
+ "<pre><code><b>one</b> <i>two\n\tthree</i></code></pre>",
+ "one two\n\tthree",
+ ),
+ // The parser keeps a line feed after `<code>`, unlike `<pre>`.
+ ("<pre><code>\none</code></pre>", "\none"),
+ // Escaped markup is still text.
+ ("<pre><code><b> &amp;</code></pre>", "<b> &"),
+ // Outside `<pre>`, whitespace is collapsed as before.
+ ("<p><code>one\n two</code></p>", "one two"),
+ ] {
+ let document = super::parse(html, 0, &mut cx).unwrap();
+ let BlockNode::Paragraph(paragraph) = &document.blocks[0] else {
+ panic!("expected one paragraph, got {:?}", document.blocks);
+ };
+ assert_eq!(paragraph.text(), text, "{html}");
+ }
+ }
+
#[test]
fn test_trim_text() {
assert_eq!(trim_text(" \n\tHello world \t\r "), " Hello world ",);
@@ -837,6 +951,137 @@ mod tests {
);
}
+ /// The text of the one paragraph `html` parses to.
+ fn paragraph_text(html: &str) -> String {
+ let mut cx = NodeContext::default();
+ let document = super::parse(html, 0, &mut cx).unwrap();
+ let BlockNode::Paragraph(paragraph) = &document.blocks[0] else {
+ panic!("expected one paragraph, got {:?}", document.blocks);
+ };
+ paragraph.text()
+ }
+
+ #[test]
+ fn test_br_breaks_the_line_where_it_stands() {
+ assert_eq!(paragraph_text("<p>one<br>two</p>"), "one\ntwo");
+ assert_eq!(
+ paragraph_text("one<br>two<br><br>three"),
+ "one\ntwo\n\nthree"
+ );
+ assert_eq!(paragraph_text("<b>one<br>two</b>"), "one\ntwo");
+ // The minifier trims the spaces around it.
+ assert_eq!(paragraph_text("<p>one <br> two</p>"), "one\ntwo");
+ // As in a browser, a break that ends a block adds no line.
+ assert_eq!(paragraph_text("<p>one<br></p>"), "one");
+ assert_eq!(paragraph_text("<p>one<br><br></p>"), "one\n");
+ // Markdown parses inline HTML one tag at a time and needs the break.
+ assert_eq!(paragraph_text("<br>"), "\n");
+ }
+
+ #[test]
+ fn test_br_that_ends_inline_markup() {
+ // Inside inline markup the break joins the text before it in one
+ // run; at the end of the block it still adds no line.
+ for tag in ["b", "span"] {
+ assert_eq!(
+ paragraph_text(&format!("<p><{tag}>one<br></{tag}></p>")),
+ "one",
+ "{tag}"
+ );
+ assert_eq!(
+ paragraph_text(&format!("<p><{tag}>one<br><br></{tag}></p>")),
+ "one\n",
+ "{tag}"
+ );
+ assert_eq!(
+ paragraph_text(&format!("<p><{tag}>one<br>two</{tag}></p>")),
+ "one\ntwo",
+ "{tag}"
+ );
+ }
+ assert_eq!(paragraph_text("<p>one<b><br></b></p>"), "one");
+ assert_eq!(paragraph_text("<p><b>one<br></b><br></p>"), "one\n");
+
+ // The marks end with the text they kept.
+ let mut cx = NodeContext::default();
+ let document = super::parse("<p><b>one<br></b></p>", 0, &mut cx).unwrap();
+ let BlockNode::Paragraph(paragraph) = &document.blocks[0] else {
+ panic!("expected one paragraph, got {:?}", document.blocks);
+ };
+ let [node] = paragraph.children.as_slice() else {
+ panic!("expected one node, got {:?}", paragraph.children);
+ };
+ assert!(
+ node.marks.iter().all(|(range, _)| *range == (0..3)),
+ "{:?}",
+ node.marks
+ );
+ assert!(node.marks.iter().any(|(_, mark)| mark.bold));
+ }
+
+ #[test]
+ fn test_literal_trailing_line_feeds_are_preserved() {
+ for (html, expected) in [
+ ("<pre>one\n</pre>", "one\n"),
+ ("<pre>one\n\n</pre>", "one\n\n"),
+ ("<pre>one\n<br></pre>", "one\n"),
+ ("<pre>one<br>\n</pre>", "one\n\n"),
+ ("<textarea>one\n</textarea>", "one\n"),
+ ] {
+ assert_eq!(paragraph_text(html), expected, "{html}");
+ }
+ }
+
+ #[test]
+ fn test_trailing_breaks_at_text_block_boundaries() {
+ for tag in ["p", "h1", "h6", "td", "th", "li"] {
+ for (content, expected) in [
+ ("one<br>", "one"),
+ ("one<br><br>", "one\n"),
+ ("<b>one<br></b>", "one"),
+ ("<span>one<br><br></span>", "one\n"),
+ ("one<br>two", "one\ntwo"),
+ ("one<br><span></span>", "one"),
+ ] {
+ let html = match tag {
+ "td" | "th" => format!("<table><tr><{tag}>{content}</{tag}></tr></table>"),
+ "li" => format!("<ul><li>{content}</li></ul>"),
+ _ => format!("<{tag}>{content}</{tag}>"),
+ };
+ let mut cx = NodeContext::default();
+ let document = super::parse(&html, 0, &mut cx).unwrap();
+ let paragraph = match &document.blocks[0] {
+ BlockNode::Paragraph(paragraph)
+ | BlockNode::Heading {
+ children: paragraph,
+ ..
+ } => paragraph,
+ BlockNode::Table(table) => &table.children[0].children[0].children,
+ BlockNode::List { children, .. } => {
+ let BlockNode::ListItem { children, .. } = &children[0] else {
+ panic!("expected list item: {html}");
+ };
+ let [BlockNode::Paragraph(paragraph)] = children.as_slice() else {
+ panic!("expected one paragraph: {html}");
+ };
+ paragraph
+ }
+ _ => panic!("expected text block: {html}"),
+ };
+ assert_eq!(paragraph.text(), expected, "{html}");
+ for node in ¶graph.children {
+ assert!(
+ node.marks
+ .iter()
+ .all(|(range, _)| range.start < range.end
+ && range.end <= node.text.len()),
+ "mark outside text: {html}"
+ );
+ }
+ }
+ }
+ }
+
#[test]
fn test_value_to_length() {
assert_eq!(super::value_to_length("100px"), Some(px(100.).into()));
diff --git a/third-party/gpui-kit/crates/base/src/text/format/html5minify/mod.rs b/third-party/gpui-kit/crates/base/src/text/format/html5minify/mod.rs
index c3d02de3bc..28d10194c8 100644
--- a/third-party/gpui-kit/crates/base/src/text/format/html5minify/mod.rs
+++ b/third-party/gpui-kit/crates/base/src/text/format/html5minify/mod.rs
@@ -116,6 +116,15 @@ impl<'a> Context<'a> {
None
}
}
+
+ /// Whether the parent keeps its whitespace, or an element above it does:
+ /// text in `<pre><code>` keeps its whitespace as well.
+ fn preserves_whitespace(&self) -> bool {
+ matches!(&self.parent.data, NodeData::Element { name, .. } if preserve_whitespace(name.local.as_ref()))
+ || self
+ .parent_context
+ .is_some_and(Context::preserves_whitespace)
+ }
}
impl<'a, W> Minifier<'a, W>
@@ -192,7 +201,7 @@ where
}
// Check if parent is whitespace preserving element or contains code (<script>, <style>)
- let (skip_collapse_whitespace, contains_code) =
+ let (parent_preserves_whitespace, contains_code) =
ctx.as_ref().map_or((false, false), |ctx| {
if let NodeData::Element { name, .. } = &ctx.parent.data {
let name = name.local.as_ref();
@@ -202,20 +211,25 @@ where
(false, false)
}
});
+ let skip_collapse_whitespace =
+ ctx.as_ref().is_some_and(Context::preserves_whitespace);
if skip_collapse_whitespace {
// The parser drops a line feed right after `<pre>` or
// `<textarea>`, so one that starts the text needs another
// in front of it to survive the next parse. Comments
- // before it are left out, so they do not count.
- let first = ctx.as_ref().is_some_and(|ctx| {
- ctx.left.is_none_or(|left| {
- !self.preserve_comments
- && left
- .iter()
- .all(|node| matches!(node.data, NodeData::Comment { .. }))
- })
- });
+ // before it are left out, so they do not count. A line
+ // feed after a nested start tag, as in `<pre><code>`, is
+ // kept by the parser and needs nothing.
+ let first = parent_preserves_whitespace
+ && ctx.as_ref().is_some_and(|ctx| {
+ ctx.left.is_none_or(|left| {
+ !self.preserve_comments
+ && left
+ .iter()
+ .all(|node| matches!(node.data, NodeData::Comment { .. }))
+ })
+ });
if first && contents.starts_with('\n') {
self.w.write_all(b"\n")?;
}
diff --git a/third-party/gpui-kit/crates/base/src/text/format/markdown.rs b/third-party/gpui-kit/crates/base/src/text/format/markdown.rs
index 1c757a9858..f62940c452 100644
--- a/third-party/gpui-kit/crates/base/src/text/format/markdown.rs
+++ b/third-party/gpui-kit/crates/base/src/text/format/markdown.rs
@@ -1060,6 +1060,7 @@ fn ast_to_node(source: &str, value: mdast::Node, cx: &mut NodeContext) -> BlockN
let segments = code_source_segments(source, &raw.value, span, cx.offset);
BlockNode::CodeBlock(
CodeBlock::new(raw.value.into(), raw.lang.map(Into::into), span)
+ .with_meta(raw.meta.map(Into::into))
.source_segments(segments),
)
}
diff --git a/third-party/gpui-kit/crates/base/src/text/inline.rs b/third-party/gpui-kit/crates/base/src/text/inline.rs
index d8367fd9d3..232584ef6a 100644
--- a/third-party/gpui-kit/crates/base/src/text/inline.rs
+++ b/third-party/gpui-kit/crates/base/src/text/inline.rs
@@ -215,7 +215,7 @@ pub(super) struct Inline {
range_backgrounds: Vec<(Range<usize>, Hsla)>,
/// The start of a pending reveal, when it is in this text.
reveal: Option<RevealAt>,
- text_view: Option<gpui::WeakEntity<super::TextViewState>>,
+ link_source: super::link::LinkSource,
link_fragments: Option<super::link::Fragments>,
element_id: Option<ElementId>,
/// What this frame's layout was shaped with, to hand the shaped text to
@@ -224,6 +224,9 @@ pub(super) struct Inline {
/// The shaped text is in the table, not in `styled_text`, until paint
/// takes it back.
handed_over: bool,
+ /// A fragment of an editor leaf laid out by `InlineFlow`: the leaf's key
+ /// and the leaf byte offset of this text's index 0.
+ editor_piece: Option<(u64, usize)>,
state: Arc<Mutex<InlineState>>,
}
@@ -234,6 +237,10 @@ pub(crate) struct InlineState {
/// The text that actually rendering, matched with selection.
pub(super) text: SharedString,
pub(super) selection: Option<Selection>,
+ /// Set only through the editor host API (`Paragraph::with_leaf_key`):
+ /// the leaf reports its geometry to the collector installed by
+ /// `editor_host::collect_leaf_geometry`. `None` for every plain `TextView`.
+ pub(crate) editor_key: Option<u64>,
}
/// One frame's [`StyledText`], kept for the next frame's [`Inline`] of the
@@ -311,7 +318,124 @@ impl InlineState {
}
}
+/// The native lines and placement of a completed paint. `TextLayout::clone`
+/// shares mutable measurement state; these immutable shaped lines do not.
+struct PaintedSelectionLayout {
+ lines: smallvec::SmallVec<[Arc<gpui::WrappedLineLayout>; 1]>,
+ bounds: Bounds<Pixels>,
+ line_height: Pixels,
+ len: usize,
+}
+
+impl PaintedSelectionLayout {
+ fn new(layout: &TextLayout) -> Self {
+ Self {
+ lines: layout.line_layouts(),
+ bounds: layout.bounds(),
+ line_height: layout.line_height(),
+ len: layout.len(),
+ }
+ }
+
+ fn position_for_index(&self, index: usize) -> Option<Point<Pixels>> {
+ let mut origin = self.bounds.origin;
+ let mut start = 0;
+ for line in &self.lines {
+ let end = start + line.len();
+ if index < start {
+ break;
+ }
+ if index <= end {
+ return Some(origin + line.position_for_index(index - start, self.line_height)?);
+ }
+ origin.y += line.size(self.line_height).height;
+ start = end + 1;
+ }
+ None
+ }
+
+ fn rows_extent(&self) -> (Pixels, Pixels) {
+ let top = self.bounds.top();
+ let mut last_line_top = top;
+ for line in self.lines.iter().take(self.lines.len().saturating_sub(1)) {
+ last_line_top += line.size(self.line_height).height;
+ }
+ let last_row_top = self.lines.last().map_or(top, |line| {
+ last_line_top + line.wrap_boundaries.len() as f32 * self.line_height
+ });
+ (top, last_row_top + self.line_height)
+ }
+}
+
+/// Targets the document-owned source state, never an ephemeral flow fragment.
+pub(super) struct InlineSelectionProjection {
+ source: Weak<Mutex<InlineState>>,
+ range: Range<usize>,
+ text: SharedString,
+ layout: PaintedSelectionLayout,
+ selection_bounds: Option<Bounds<Pixels>>,
+}
+
+impl InlineSelectionProjection {
+ pub(super) fn reset(&self) {
+ if let Some(source) = self.source.upgrade()
+ && let Ok(mut source) = source.lock()
+ {
+ source.selection = None;
+ }
+ }
+
+ pub(super) fn project(&self, start: Point<Pixels>, end: Point<Pixels>) {
+ let Some(source) = self.source.upgrade() else {
+ return;
+ };
+ let selection =
+ project_selection(&self.text, &self.layout, self.selection_bounds, start, end);
+ if let Some(selection) = selection
+ && let Ok(mut source) = source.lock()
+ {
+ let start = self.range.start + selection.start;
+ let end = self.range.start + selection.end;
+ source.selection = Some(match source.selection {
+ Some(previous) => Selection::new(previous.start.min(start), previous.end.max(end)),
+ None => Selection::new(start, end),
+ });
+ }
+ }
+}
+
impl Inline {
+ /// Editor seam (design D3): report this leaf's layout and take the
+ /// editor's backgrounds. Inert unless the state carries an editor key
+ /// and an editor sink is installed.
+ fn report_to_editor(&mut self) {
+ // The sink (a thread-local read) is checked first: outside an
+ // editor's prepaint no lock is taken.
+ let Some(sink) = super::editor_seam::current() else {
+ return;
+ };
+ if let Some((key, start)) = self.editor_piece {
+ let layout = self.styled_text.layout();
+ let end = start + layout.len();
+ sink.report_text_piece(key, start, layout);
+ self.range_backgrounds
+ .extend(
+ sink.backgrounds(key)
+ .into_iter()
+ .filter_map(|(range, color)| {
+ let (lo, hi) = (range.start.max(start), range.end.min(end));
+ (lo < hi).then(|| (lo - start..hi - start, color))
+ }),
+ );
+ return;
+ }
+ let Some(key) = self.state.lock().ok().and_then(|state| state.editor_key) else {
+ return;
+ };
+ sink.report(key, self.styled_text.layout());
+ self.range_backgrounds.extend(sink.backgrounds(key));
+ }
+
/// Hands the shaped text to the next frame (see [`RetainedLayout`]).
/// Called after prepaint, so an element that is laid out but never
/// painted (scrolled out of view) keeps its layout too; paint takes it
@@ -377,11 +501,12 @@ impl Inline {
selection_source: None,
range_backgrounds: Vec::new(),
reveal: None,
- text_view: None,
+ link_source: super::link::LinkSource::None,
link_fragments: None,
element_id: None,
retained_key: None,
handed_over: false,
+ editor_piece: None,
state,
}
}
@@ -392,6 +517,13 @@ impl Inline {
self
}
+ /// Report this text as a fragment of the editor leaf `key`, starting at
+ /// the leaf's byte `start` (see `report_to_editor`).
+ pub(super) fn editor_piece(mut self, piece: Option<(u64, usize)>) -> Self {
+ self.editor_piece = piece;
+ self
+ }
+
/// Preserve the shared inline-flow baseline through GPUI's element-bound snapping.
pub(super) fn paint_origin(mut self, origin: Point<Pixels>) -> Self {
self.paint_origin = Some(origin);
@@ -474,11 +606,8 @@ impl Inline {
);
}
- pub(super) fn text_view(
- mut self,
- view: Option<gpui::WeakEntity<super::TextViewState>>,
- ) -> Self {
- self.text_view = view;
+ pub(super) fn link_source(mut self, source: super::link::LinkSource) -> Self {
+ self.link_source = source;
self
}
pub(super) fn link_fragments(mut self, fragments: super::link::Fragments) -> Self {
@@ -490,7 +619,7 @@ impl Inline {
self
}
- fn link_regions(&self, window: &Window) -> Vec<(super::node::LinkOccurrence, Bounds<Pixels>)> {
+ fn link_regions(&self, window: &Window) -> Vec<super::link::Fragment> {
let layout = self.styled_text.layout();
let bounds = layout.bounds();
let line_height = layout.line_height();
@@ -498,26 +627,31 @@ impl Inline {
self.links
.iter()
.flat_map(|(range, mark)| {
- let occurrence = mark.occurrence;
- range_boxes(&glyphs, range.clone()).into_iter().filter_map(
- move |(row, left, right)| {
- occurrence.map(|id| {
- (
- id,
- Bounds::from_corners(
- point(
- bounds.left() + left,
- bounds.top() + line_height * row as f32,
- ),
- point(
- bounds.left() + right,
- bounds.top() + line_height * (row + 1) as f32,
- ),
+ range_boxes(&glyphs, range.clone())
+ .into_iter()
+ .enumerate()
+ .map(move |(fragment_ix, (row, left, right))| {
+ super::link::Fragment::new(
+ mark.clone(),
+ if fragment_ix == 0 {
+ SharedString::from(
+ self.text.get(range.clone()).unwrap_or_default().to_string(),
+ )
+ } else {
+ SharedString::default()
+ },
+ Bounds::from_corners(
+ point(
+ bounds.left() + left,
+ bounds.top() + line_height * row as f32,
),
- )
- })
- },
- )
+ point(
+ bounds.left() + right,
+ bounds.top() + line_height * (row + 1) as f32,
+ ),
+ ),
+ )
+ })
})
.collect()
}
@@ -539,10 +673,12 @@ impl Inline {
&self,
text_layout: &TextLayout,
bounds: &Bounds<Pixels>,
- window: &mut Window,
cx: &mut App,
) -> (bool, bool, Option<Selection>) {
- let Some(text_view_state) = self.text_view.as_ref().and_then(gpui::WeakEntity::upgrade)
+ let Some(text_view_state) = self
+ .link_source
+ .text_view()
+ .and_then(gpui::WeakEntity::upgrade)
else {
return (false, false, None);
};
@@ -603,97 +739,17 @@ impl Inline {
let Some((selection_start, selection_end)) = text_view_state.selection_points(cx) else {
return (is_selectable, false, None);
};
- let line_height = window.line_height();
-
- // Use for debug selection bounds
- // self.paint_selected_bounds(Bounds::from_corners(selection_start, selection_end), window, cx);
-
- // NOTE: the selection is computed purely from the geometric band
- // (`selection_start`..`selection_end`), NOT from what is currently
- // visible. Every glyph of a *painted* element is laid out (its
- // `position_for_index` is valid) even when it is scrolled out of, or
- // clipped by, an ancestor's viewport — the content mask only clips the
- // painted pixels. Because the copied text is derived from
- // `InlineState.selection`, gating the selection on `content_mask` here
- // used to drop scrolled-out-but-selected glyphs, so a selection taller
- // than the viewport (e.g. a long chat message, or a drag with
- // auto-scroll) copied only the portion that happened to be on screen.
- //
- // This does not resurrect the #2156 clipped-hit-testing behavior: a
- // selection can only START on visible text (window selection resolves
- // endpoints with hitbox hover testing against visible Inline bounds),
- // so the band's endpoints are always anchored to on-screen text.
- // Content that is merely `overflow_hidden`
- // (not scrolled) lies outside that band and is still excluded, while
- // the highlight quads painted for off-screen glyphs are clipped away by
- // GPUI's content mask as before.
- //
- // Each character is tested with its row's top and height, so an inline
- // whose rows all miss the band, or all lie strictly inside it with no
- // endpoint on any row, has the same answer for every character. Decide
- // those without the walk below, which scans the layout twice per
- // character: one long code block alone made every paint of a held
- // selection cost tens of milliseconds.
- if self.text.is_empty() {
- return (true, true, None);
- }
- let (rows_top, rows_bottom) = match self.selection_bounds {
- Some(bounds) => (bounds.top(), bounds.top() + bounds.size.height),
- None => text_rows_extent(text_layout, line_height),
- };
- let band_top = selection_start.y.min(selection_end.y);
- let band_bottom = selection_start.y.max(selection_end.y);
- if rows_bottom <= band_top || rows_top > band_bottom {
- return (true, true, None);
- }
- if band_top < rows_top && band_bottom >= rows_bottom && text_layout.len() >= self.text.len()
- {
- return (true, true, Some((0..self.text.len()).into()));
- }
-
- let mut selection: Option<Selection> = None;
- let mut offset = 0;
- let mut chars = self.text.chars().peekable();
- while let Some(c) = chars.next() {
- let Some(pos) = text_layout.position_for_index(offset) else {
- offset += c.len_utf8();
- continue;
- };
-
- let next_offset = offset + c.len_utf8();
- let mut char_width = line_height.half();
- if let Some(next_pos) = text_layout.position_for_index(next_offset) {
- if next_pos.y == pos.y {
- char_width = next_pos.x - pos.x;
- }
- }
-
- let selection_pos = self
- .selection_bounds
- .map_or(pos, |bounds| point(pos.x, bounds.top()));
- let selection_height = self
- .selection_bounds
- .map_or(line_height, |bounds| bounds.size.height);
- if point_in_text_selection(
- selection_pos,
- char_width,
+ (
+ true,
+ true,
+ project_selection(
+ &self.text,
+ text_layout,
+ self.selection_bounds,
selection_start,
selection_end,
- selection_height,
- ) {
- if selection.is_none() {
- selection = Some((offset..offset).into());
- }
-
- if let Some(selection) = selection.as_mut() {
- selection.end = next_offset;
- }
- }
-
- offset = next_offset;
- }
-
- (true, true, selection)
+ ),
+ )
}
/// One unclipped box per laid-out row, from its start to its last
@@ -709,9 +765,12 @@ impl Inline {
/// a line height past its last glyph: the character walk gave that width
/// to a character whose successor sits on the next row, and the selection
/// geometry was tuned against it.
- fn text_line_bounds(text_layout: &TextLayout, line_height: Pixels) -> Vec<Bounds<Pixels>> {
- let origin = text_layout.bounds().origin;
- let lines = text_layout.line_layouts();
+ fn text_line_bounds(
+ text_layout: &PaintedSelectionLayout,
+ line_height: Pixels,
+ ) -> Vec<Bounds<Pixels>> {
+ let origin = text_layout.bounds.origin;
+ let lines = &text_layout.lines;
let row_count: usize = lines
.iter()
.map(|line| line.wrap_boundaries.len() + 1)
@@ -719,7 +778,7 @@ impl Inline {
let mut line_bounds = Vec::with_capacity(row_count);
let mut row_ix = 0;
let mut y = origin.y;
- for line in &lines {
+ for line in lines {
let layout = &line.unwrapped_layout;
let mut row_start = 0;
let row_ends = line
@@ -932,6 +991,7 @@ impl Element for Inline {
let bounds = Bounds::new(self.paint_origin.unwrap_or(bounds.origin), bounds.size);
self.styled_text
.prepaint(id, inspector_id, bounds, &mut (), window, cx);
+ self.report_to_editor();
self.request_reveal(window);
@@ -941,7 +1001,7 @@ impl Element for Inline {
sink.borrow_mut().extend(fragments);
Vec::new()
} else {
- super::link::targets(fragments, &self.text_view, window, cx)
+ super::link::targets(fragments, &self.link_source, window, cx)
};
self.retain_styled_text();
InlinePrepaintState { hitbox, links }
@@ -974,7 +1034,7 @@ impl Element for Inline {
// layout selections
let (is_selectable, is_selection, selection) =
- self.layout_selections(&text_layout, &bounds, window, cx);
+ self.layout_selections(&text_layout, &bounds, cx);
let Ok(mut state) = self.state.lock() else {
_ = text_layout.paint_foreground(window, cx);
@@ -1000,8 +1060,8 @@ impl Element for Inline {
if let Some(selection) = &state.selection {
let color = self
- .text_view
- .as_ref()
+ .link_source
+ .text_view()
.and_then(gpui::WeakEntity::upgrade)
.map(|state| state.read(cx).text_view_style.selection())
.unwrap_or_else(|| crate::Theme::global(cx).tokens.colors.selection);
@@ -1011,14 +1071,22 @@ impl Element for Inline {
_ = text_layout.paint_foreground(window, cx);
if is_selectable {
- if let Some(text_view_state) =
- self.text_view.as_ref().and_then(gpui::WeakEntity::upgrade)
+ if let Some(text_view_state) = self
+ .link_source
+ .text_view()
+ .and_then(gpui::WeakEntity::upgrade)
{
- let text_bounds = Self::text_line_bounds(&text_layout, text_layout.line_height());
+ let copy_layout = PaintedSelectionLayout::new(&text_layout);
+ let text_bounds = Self::text_line_bounds(©_layout, text_layout.line_height());
let edges = state
.selection
.as_ref()
.and_then(|selection| Self::selection_edges(selection, &text_layout));
+ let (source, range) = self
+ .selection_source
+ .as_ref()
+ .map(|(source, range)| (source, range.clone()))
+ .unwrap_or((&self.state, 0..self.text.len()));
text_view_state.query(cx, |state, _| {
state.selection_adapter.report_inline(
hitbox,
@@ -1029,6 +1097,15 @@ impl Element for Inline {
hitbox.bounds,
)),
edges,
+ Some(super::selection_adapter::CopyProjection::Text(
+ InlineSelectionProjection {
+ source: Arc::downgrade(source),
+ range,
+ text: self.text.clone(),
+ layout: copy_layout,
+ selection_bounds: self.selection_bounds,
+ },
+ )),
);
});
}
@@ -1038,7 +1115,7 @@ impl Element for Inline {
let text_layout = text_layout.clone();
let inline_state = self.state.clone();
let text = self.text.clone();
- let owner = self.text_view.clone();
+ let owner = self.link_source.text_view().cloned();
let line_bounds = self.selection_bounds;
let admission = owner
.as_ref()
@@ -1274,6 +1351,98 @@ fn selection_for_multi_click(
}
}
+trait SelectionGeometry {
+ fn len(&self) -> usize;
+ fn line_height(&self) -> Pixels;
+ fn rows_extent(&self) -> (Pixels, Pixels);
+ fn position_for_index(&self, index: usize) -> Option<Point<Pixels>>;
+}
+
+impl SelectionGeometry for TextLayout {
+ fn len(&self) -> usize {
+ TextLayout::len(self)
+ }
+ fn line_height(&self) -> Pixels {
+ TextLayout::line_height(self)
+ }
+ fn rows_extent(&self) -> (Pixels, Pixels) {
+ text_rows_extent(self, self.line_height())
+ }
+ fn position_for_index(&self, index: usize) -> Option<Point<Pixels>> {
+ TextLayout::position_for_index(self, index)
+ }
+}
+
+impl SelectionGeometry for PaintedSelectionLayout {
+ fn len(&self) -> usize {
+ self.len
+ }
+ fn line_height(&self) -> Pixels {
+ self.line_height
+ }
+ fn rows_extent(&self) -> (Pixels, Pixels) {
+ self.rows_extent()
+ }
+ fn position_for_index(&self, index: usize) -> Option<Point<Pixels>> {
+ self.position_for_index(index)
+ }
+}
+
+/// Shared by paint and Copy: project the current gesture onto the same shaped
+/// UTF-8 text, including rendered inline-code spacing.
+fn project_selection(
+ text: &str,
+ text_layout: &impl SelectionGeometry,
+ selection_bounds: Option<Bounds<Pixels>>,
+ selection_start: Point<Pixels>,
+ selection_end: Point<Pixels>,
+) -> Option<Selection> {
+ if text.is_empty() {
+ return None;
+ }
+ let line_height = text_layout.line_height();
+ let (rows_top, rows_bottom) = match selection_bounds {
+ Some(bounds) => (bounds.top(), bounds.bottom()),
+ None => text_layout.rows_extent(),
+ };
+ let band_top = selection_start.y.min(selection_end.y);
+ let band_bottom = selection_start.y.max(selection_end.y);
+ if rows_bottom <= band_top || rows_top > band_bottom {
+ return None;
+ }
+ if band_top < rows_top && band_bottom >= rows_bottom && text_layout.len() >= text.len() {
+ return Some((0..text.len()).into());
+ }
+
+ let mut selection: Option<Selection> = None;
+ for (offset, c) in text.char_indices() {
+ let Some(pos) = text_layout.position_for_index(offset) else {
+ continue;
+ };
+ let next_offset = offset + c.len_utf8();
+ let mut char_width = line_height.half();
+ if let Some(next_pos) = text_layout.position_for_index(next_offset)
+ && next_pos.y == pos.y
+ {
+ char_width = next_pos.x - pos.x;
+ }
+ let selection_pos = selection_bounds.map_or(pos, |bounds| point(pos.x, bounds.top()));
+ let selection_height = selection_bounds.map_or(line_height, |bounds| bounds.size.height);
+ if point_in_text_selection(
+ selection_pos,
+ char_width,
+ selection_start,
+ selection_end,
+ selection_height,
+ ) {
+ selection
+ .get_or_insert_with(|| Selection::new(offset, offset))
+ .end = next_offset;
+ }
+ }
+ selection
+}
+
/// Check if a `pos` is within a `bounds`, considering multi-line selections.
pub(super) fn point_in_text_selection(
pos: Point<Pixels>,
@@ -1468,9 +1637,10 @@ mod line_bounds_tests {
);
element.prepaint_at(origin, window, cx);
let line_height = layout.line_height();
+ let painted_layout = PaintedSelectionLayout::new(&layout);
for mask in [mask, clipped] {
let expected = by_character(text, &layout, line_height, mask);
- let actual: Vec<_> = Inline::text_line_bounds(&layout, line_height)
+ let actual: Vec<_> = Inline::text_line_bounds(&painted_layout, line_height)
.into_iter()
.map(|bounds| bounds.intersect(&mask))
.filter(|bounds| {
diff --git a/third-party/gpui-kit/crates/base/src/text/inline_flow.rs b/third-party/gpui-kit/crates/base/src/text/inline_flow.rs
index 3947cbc4c1..0603eb5d14 100644
--- a/third-party/gpui-kit/crates/base/src/text/inline_flow.rs
+++ b/third-party/gpui-kit/crates/base/src/text/inline_flow.rs
@@ -27,9 +27,16 @@ pub(super) const INLINE_CODE_PADDING: f32 = 4.;
pub(super) struct InlineFlow {
id: ElementId,
items: Vec<InlineFlowItem>,
- text_view: Option<gpui::WeakEntity<super::TextViewState>>,
+ link_source: super::link::LinkSource,
+ /// The block editor leaf this flow renders (design D3): its fragments
+ /// and atoms are reported to the editor sink as pieces.
+ editor_key: Option<u64>,
}
+/// Bytes an image or custom inline node occupies in an editor leaf (one
+/// U+FFFC).
+const EDITOR_ATOM_LEN: usize = '\u{FFFC}'.len_utf8();
+
pub(super) type InlineRenderer = dyn Fn(&super::InlineRenderContext, &mut Window, &mut App) -> Option<super::InlineElement>
+ Send
+ Sync;
@@ -58,6 +65,7 @@ pub(super) enum InlineFlowItem {
Image {
source: ImageSource,
link: Option<LinkMark>,
+ title: SharedString,
width: Option<DefiniteLength>,
height: Option<DefiniteLength>,
},
@@ -167,6 +175,7 @@ struct InlineFlowTypography {
#[derive(Default)]
struct InlineFlowLayout {
fragments: Vec<PositionedFragment>,
+ empty_lines: Vec<(usize, Bounds<Pixels>)>,
size: Size<Pixels>,
wrap_width: Option<Pixels>,
}
@@ -238,20 +247,33 @@ enum LineFragmentKind {
Image,
}
+/// Atom decorations are overlays so they remain visible over opaque content.
+pub(super) struct FlowPaintItem {
+ element: AnyElement,
+ background: Option<(Bounds<Pixels>, Hsla)>,
+ overlays: Vec<(Bounds<Pixels>, Hsla)>,
+}
+
impl InlineFlow {
pub(super) fn new(id: impl Into<ElementId>, items: Vec<InlineFlowItem>) -> Self {
Self {
id: id.into(),
items,
- text_view: None,
+ link_source: super::link::LinkSource::None,
+ editor_key: None,
}
}
- pub(super) fn text_view(
- mut self,
- view: Option<gpui::WeakEntity<super::TextViewState>>,
- ) -> Self {
- self.text_view = view;
+ pub(super) fn editor_key(mut self, key: Option<u64>) -> Self {
+ if let Some(key) = key {
+ self.id = ("editor-leaf", key).into();
+ }
+ self.editor_key = key;
+ self
+ }
+
+ pub(super) fn link_source(mut self, source: super::link::LinkSource) -> Self {
+ self.link_source = source;
self
}
@@ -276,7 +298,7 @@ impl IntoElement for InlineFlow {
#[derive(Default)]
pub(crate) struct InlineFlowPrepaintState {
- elements: Vec<(AnyElement, Option<(Bounds<Pixels>, gpui::Hsla)>)>,
+ elements: Vec<FlowPaintItem>,
links: Vec<AnyElement>,
}
@@ -437,6 +459,72 @@ impl Element for InlineFlow {
.reduce(|found, next| if found.1 { found } else { next })
.map(|(ix, _)| ix);
+ // Editor leaf byte offset where each item starts: text items count
+ // their bytes, images and custom nodes one U+FFFC.
+ let editor = self
+ .editor_key
+ .and_then(|key| super::editor_seam::current().map(|sink| (key, sink)));
+ // Ordinary TextViews do not allocate editor bookkeeping.
+ let item_starts: Vec<usize> = if let Some((key, sink)) = &editor {
+ sink.begin_pieces(*key);
+ let mut at = 0;
+ self.items
+ .iter()
+ .map(|item| {
+ let start = at;
+ at += match item {
+ InlineFlowItem::Text { text, .. } => text.len(),
+ _ => EDITOR_ATOM_LEN,
+ };
+ start
+ })
+ .collect()
+ } else {
+ Vec::new()
+ };
+ if let Some((key, sink)) = &editor {
+ for (start, line_bounds) in &layout.empty_lines {
+ // Flow layout uses a one-byte object placeholder; the public
+ // leaf coordinate space uses the three-byte U+FFFC instead.
+ let mut flow_start = 0;
+ let mut leaf_start = 0;
+ for item in &self.items {
+ let len = match item {
+ InlineFlowItem::Text { text, .. } => text.len(),
+ _ => IMAGE_LEN,
+ };
+ if flow_start + len > *start {
+ break;
+ }
+ flow_start += len;
+ leaf_start += match item {
+ InlineFlowItem::Text { text, .. } => text.len(),
+ _ => EDITOR_ATOM_LEN,
+ };
+ }
+ sink.report_empty_piece(
+ *key,
+ leaf_start + start - flow_start,
+ Bounds::new(bounds.origin + line_bounds.origin, line_bounds.size),
+ );
+ }
+ }
+ let decorations = editor.as_ref().map(|(key, sink)| sink.backgrounds(*key));
+ let report_box = |item_ix: usize, bounds: Bounds<Pixels>| {
+ let mut overlays = Vec::new();
+ if let Some((key, sink)) = &editor {
+ let start = item_starts[item_ix];
+ let end = start + EDITOR_ATOM_LEN;
+ sink.report_box_piece(*key, start..end, bounds);
+ for (range, color) in decorations.as_deref().unwrap_or_default() {
+ if range.start < range.end && range.start < end && range.end > start {
+ overlays.push((bounds, *color));
+ }
+ }
+ }
+ overlays
+ };
+
let mut text_fragment_count = 0;
for (fragment_ix, fragment) in layout.fragments.iter().enumerate() {
match fragment {
@@ -458,6 +546,8 @@ impl Element for InlineFlow {
continue;
};
let object_size = object.metrics.size;
+ let overlays =
+ report_box(*item_ix, Bounds::new(bounds.origin + *origin, object_size));
let mut element = InlineObject::new(
("inline-object", *id),
text.clone(),
@@ -473,7 +563,7 @@ impl Element for InlineFlow {
size(bounds.size.width, selection_bounds.size.height),
),
)
- .text_view(self.text_view.clone())
+ .text_view(self.link_source.text_view().cloned())
.into_any_element();
element.prepaint_as_root(
bounds.origin + *origin,
@@ -484,12 +574,18 @@ impl Element for InlineFlow {
window,
cx,
);
- if let Some(id) = link.as_ref().and_then(|link| link.occurrence) {
- fragments
- .borrow_mut()
- .push((id, Bounds::new(bounds.origin + *origin, object_size)));
+ if let Some(link) = link {
+ fragments.borrow_mut().push(super::link::Fragment::new(
+ link.clone(),
+ accessibility_label.clone(),
+ Bounds::new(bounds.origin + *origin, object_size),
+ ));
}
- elements.push((element, None));
+ elements.push(FlowPaintItem {
+ element,
+ background: None,
+ overlays,
+ });
}
PositionedFragment::Text {
item_ix,
@@ -539,7 +635,7 @@ impl Element for InlineFlow {
fragment_style.line_height = fragment_size.height.into();
fragment_style.white_space = WhiteSpace::Nowrap;
let mut element = Inline::new(state, links.clone(), highlights.clone())
- .text_view(self.text_view.clone())
+ .link_source(self.link_source.clone())
.link_fragments(fragments.clone())
.selection_source(source_state.clone(), source_range.clone())
.range_backgrounds(slice_ranges(
@@ -555,6 +651,11 @@ impl Element for InlineFlow {
.map(|reveal| reveal.clamp(source_range.start, source_range.end)),
)
.text_style(fragment_style.clone())
+ .editor_piece(
+ editor
+ .as_ref()
+ .map(|(key, _)| (*key, item_starts[*item_ix] + source_range.start)),
+ )
.selection_bounds(Bounds::new(
point(bounds.left(), bounds.top() + selection_bounds.top()),
size(bounds.size.width, selection_bounds.size.height),
@@ -582,17 +683,29 @@ impl Element for InlineFlow {
},
);
});
- elements.push((element, background));
+ elements.push(FlowPaintItem {
+ element,
+ background,
+ overlays: Vec::new(),
+ });
}
PositionedFragment::Image {
item_ix,
origin,
size: fragment_size,
} => {
- let InlineFlowItem::Image { source, link, .. } = &self.items[*item_ix] else {
+ let InlineFlowItem::Image {
+ source,
+ link,
+ title,
+ ..
+ } = &self.items[*item_ix]
+ else {
continue;
};
let (origin, fragment_size) = (*origin, *fragment_size);
+ let overlays =
+ report_box(*item_ix, Bounds::new(bounds.origin + origin, fragment_size));
let mut element = Self::image_element(elements.len(), source, fragment_size);
element.prepaint_as_root(
bounds.origin + origin,
@@ -603,12 +716,18 @@ impl Element for InlineFlow {
window,
cx,
);
- if let Some(id) = link.as_ref().and_then(|link| link.occurrence) {
- fragments
- .borrow_mut()
- .push((id, Bounds::new(bounds.origin + origin, fragment_size)));
+ if let Some(link) = link {
+ fragments.borrow_mut().push(super::link::Fragment::new(
+ link.clone(),
+ title.clone(),
+ Bounds::new(bounds.origin + origin, fragment_size),
+ ));
}
- elements.push((element, None));
+ elements.push(FlowPaintItem {
+ element,
+ background: None,
+ overlays,
+ });
}
}
}
@@ -619,7 +738,7 @@ impl Element for InlineFlow {
.truncate(text_fragment_count);
let fragments = std::mem::take(&mut *fragments.borrow_mut());
- let links = super::link::targets(fragments, &self.text_view, window, cx);
+ let links = super::link::targets(fragments, &self.link_source, window, cx);
InlineFlowPrepaintState { elements, links }
}
@@ -634,8 +753,8 @@ impl Element for InlineFlow {
cx: &mut App,
) {
let preserve_selection = self
- .text_view
- .as_ref()
+ .link_source
+ .text_view()
.and_then(gpui::WeakEntity::upgrade)
.is_some_and(|view| view.read(cx).preserve_inline_selection);
for item in &self.items {
@@ -647,11 +766,19 @@ impl Element for InlineFlow {
}
}
let radius = crate::Theme::global(cx).tokens.radius.sm;
- for (element, background) in &mut prepaint.elements {
+ for FlowPaintItem {
+ element,
+ background,
+ overlays,
+ } in &mut prepaint.elements
+ {
if let Some((bounds, color)) = background {
window.paint_quad(gpui::fill(*bounds, *color).corner_radii(radius));
}
element.paint(window, cx);
+ for (bounds, color) in overlays {
+ window.paint_quad(gpui::fill(*bounds, *color));
+ }
}
for link in &mut prepaint.links {
link.paint(window, cx);
@@ -861,6 +988,7 @@ fn layout_measured_flow(
let line_ranges = line_ranges(items, image_sizes, &objects, text_style, wrap_width, window);
let font_size = text_style.font_size.to_pixels(rem_size);
let mut fragments = Vec::new();
+ let mut empty_lines = Vec::new();
let mut max_width = Pixels::ZERO;
let mut y = Pixels::ZERO;
@@ -1006,6 +1134,15 @@ fn layout_measured_flow(
item_start = item_end;
}
+ if line_range.is_empty() {
+ empty_lines.push((
+ line_range.start,
+ Bounds::new(
+ point(Pixels::ZERO, y),
+ size(Pixels::ZERO, line_ascent + line_descent),
+ ),
+ ));
+ }
let mut x = Pixels::ZERO;
for mut fragment in line_fragments {
// Give a text run's glyph box equal leading on both sides, as
@@ -1075,6 +1212,7 @@ fn layout_measured_flow(
InlineFlowLayout {
fragments,
+ empty_lines,
size: size(max_width, y),
wrap_width,
}
@@ -1155,6 +1293,10 @@ fn line_ranges(
let mut ranges = Vec::new();
for hard_line in hard_lines {
+ if hard_line.is_empty() {
+ ranges.push(hard_line);
+ continue;
+ }
let mut item_start = 0;
let mut wrap_fragments = Vec::new();
for (ix, item) in items.iter().enumerate() {
diff --git a/third-party/gpui-kit/crates/base/src/text/inline_object.rs b/third-party/gpui-kit/crates/base/src/text/inline_object.rs
index f85fabe6af..28d9460449 100644
--- a/third-party/gpui-kit/crates/base/src/text/inline_object.rs
+++ b/third-party/gpui-kit/crates/base/src/text/inline_object.rs
@@ -146,6 +146,35 @@ impl MeasuredInlineObject {
}
}
+pub(super) struct InlineObjectSelectionProjection {
+ selected: std::sync::Weak<Mutex<bool>>,
+ bounds: Bounds<Pixels>,
+}
+
+impl InlineObjectSelectionProjection {
+ pub(super) fn project(&self, start: gpui::Point<Pixels>, end: gpui::Point<Pixels>) {
+ if let Some(selected) = self.selected.upgrade()
+ && let Ok(mut selected) = selected.lock()
+ {
+ *selected = object_in_selection(self.bounds, start, end);
+ }
+ }
+}
+
+fn object_in_selection(
+ bounds: Bounds<Pixels>,
+ start: gpui::Point<Pixels>,
+ end: gpui::Point<Pixels>,
+) -> bool {
+ point_in_text_selection(
+ bounds.origin,
+ bounds.size.width,
+ start,
+ end,
+ bounds.size.height,
+ )
+}
+
/// Atomic selection wrapper that leaves child styling and interaction to GPUI.
pub(super) struct InlineObject {
text_view: Option<gpui::WeakEntity<super::TextViewState>>,
@@ -298,13 +327,7 @@ impl Element for InlineObject {
)
})
|| state.selection_points(cx).is_some_and(|(start, end)| {
- point_in_text_selection(
- self.selection_bounds.origin,
- self.selection_bounds.size.width,
- start,
- end,
- self.selection_bounds.size.height,
- )
+ object_in_selection(self.selection_bounds, start, end)
}))
});
if let Ok(mut value) = self.selected.lock() {
@@ -317,9 +340,18 @@ impl Element for InlineObject {
if selectable {
window.set_cursor_style(CursorStyle::IBeam, hitbox);
view.as_ref().unwrap().query(cx, |state, _| {
- state
- .selection_adapter
- .report_inline(hitbox, vec![bounds], None, None);
+ state.selection_adapter.report_inline(
+ hitbox,
+ vec![bounds],
+ None,
+ None,
+ Some(super::selection_adapter::CopyProjection::InlineObject(
+ InlineObjectSelectionProjection {
+ selected: Arc::downgrade(&self.selected),
+ bounds: self.selection_bounds,
+ },
+ )),
+ );
});
let hitbox = hitbox.clone();
let selected_state = self.selected.clone();
diff --git a/third-party/gpui-kit/crates/base/src/text/link.rs b/third-party/gpui-kit/crates/base/src/text/link.rs
index 29566de1c4..2cde8dddc3 100644
--- a/third-party/gpui-kit/crates/base/src/text/link.rs
+++ b/third-party/gpui-kit/crates/base/src/text/link.rs
@@ -35,6 +35,44 @@ use super::{
state::{ParsedContent, TextViewState},
};
+pub(super) type EditorLinkHandler =
+ dyn Fn(&SharedString, &ClickEvent, &mut Window, &mut App) + Send + Sync;
+
+/// Rendering has exactly one activation authority. A retired TextView never
+/// falls through to the external editor's default URL handling.
+#[derive(Clone, Default)]
+pub(crate) enum LinkSource {
+ #[default]
+ None,
+ TextView(WeakEntity<TextViewState>),
+ ExternalEditor(Option<Arc<EditorLinkHandler>>),
+}
+
+impl LinkSource {
+ pub(super) fn text_view(&self) -> Option<&WeakEntity<TextViewState>> {
+ match self {
+ Self::TextView(view) => Some(view),
+ Self::None | Self::ExternalEditor(_) => None,
+ }
+ }
+}
+
+pub(super) struct Fragment {
+ mark: LinkMark,
+ label: SharedString,
+ bounds: Bounds<Pixels>,
+}
+
+impl Fragment {
+ pub(super) fn new(mark: LinkMark, label: SharedString, bounds: Bounds<Pixels>) -> Self {
+ Self {
+ mark,
+ label,
+ bounds,
+ }
+ }
+}
+
struct LinkOwner {
mark: LinkMark,
label: SharedString,
@@ -170,48 +208,132 @@ impl Links {
}
/// Per successful paragraph prepaint. It does not escape into document/window state.
-pub(super) type Fragments = Rc<std::cell::RefCell<Vec<(LinkOccurrence, Bounds<Pixels>)>>>;
+pub(super) type Fragments = Rc<std::cell::RefCell<Vec<Fragment>>>;
+
+#[derive(Clone)]
+enum Activation {
+ TextView {
+ view: WeakEntity<TextViewState>,
+ id: LinkOccurrence,
+ lease: Weak<()>,
+ },
+ ExternalEditor {
+ url: SharedString,
+ handler: Option<Arc<EditorLinkHandler>>,
+ },
+}
+
+impl Activation {
+ fn admitted(&self, cx: &App) -> bool {
+ match self {
+ Self::TextView { view, id, lease } => admitted(view, *id, lease, cx),
+ Self::ExternalEditor { .. } => true,
+ }
+ }
+
+ fn activate(&self, event: &ClickEvent, window: &mut Window, cx: &mut App) {
+ match self {
+ Self::TextView { view, id, lease } => activate(view, *id, lease, event, window, cx),
+ Self::ExternalEditor { url, handler } => {
+ if let Some(handler) = handler {
+ handler(url, event, window, cx);
+ } else if activates(event) && !window.default_prevented() {
+ cx.open_url(url);
+ }
+ }
+ }
+ }
+}
pub(super) fn targets(
- fragments: Vec<(LinkOccurrence, Bounds<Pixels>)>,
- view: &Option<WeakEntity<TextViewState>>,
+ fragments: Vec<Fragment>,
+ source: &LinkSource,
window: &mut Window,
cx: &mut App,
) -> Vec<AnyElement> {
- let Some(view) = view.as_ref().and_then(WeakEntity::upgrade) else {
+ if matches!(source, LinkSource::None) {
return Vec::new();
- };
- let mut groups: Vec<(LinkOccurrence, Vec<Bounds<Pixels>>)> = Vec::new();
- for (id, bounds) in fragments {
+ }
+ let mut groups: Vec<(LinkMark, String, Vec<Bounds<Pixels>>)> = Vec::new();
+ for Fragment {
+ mark,
+ label,
+ bounds,
+ } in fragments
+ {
if bounds.size.width <= Pixels::ZERO || bounds.size.height <= Pixels::ZERO {
continue;
}
- if let Some((_, regions)) = groups.iter_mut().find(|(key, _)| *key == id) {
+ let Some(id) = mark.occurrence else { continue };
+ if let Some((_, text, regions)) = groups
+ .iter_mut()
+ .find(|(mark, _, _)| mark.occurrence == Some(id))
+ {
+ text.push_str(&label);
regions.push(bounds);
} else {
- groups.push((id, vec![bounds]));
+ groups.push((mark, label.to_string(), vec![bounds]));
}
}
let mut result = Vec::new();
- for (id, regions) in groups {
- let prepared = view.update(cx, |state, _| {
- if !state.interaction_enabled || state.parsed_error.is_some() {
- return None;
+ for (mark, label, regions) in groups {
+ let id = mark.occurrence.unwrap();
+ let (target_id, focus, activation, label, url, color) = match source {
+ LinkSource::None => unreachable!(),
+ LinkSource::TextView(weak) => {
+ let Some(view) = weak.upgrade() else { continue };
+ let prepared = view.update(cx, |state, _| {
+ if !state.interaction_enabled || state.parsed_error.is_some() {
+ return None;
+ }
+ let color = state.text_view_style.link();
+ let owner = state.links.0.get_mut(&id)?;
+ let lease = Rc::downgrade(owner.lease.get_or_insert_with(|| Rc::new(())));
+ owner.focus = owner.focus.clone().tab_stop(true);
+ Some((
+ owner.focus.clone(),
+ lease,
+ owner.label.clone(),
+ owner.mark.url.clone(),
+ color,
+ ))
+ });
+ let Some((focus, lease, label, url, color)) = prepared else {
+ continue;
+ };
+ (
+ SharedString::from(format!("link/{id:?}")),
+ focus,
+ Activation::TextView {
+ view: weak.clone(),
+ id,
+ lease,
+ },
+ label,
+ url,
+ color,
+ )
+ }
+ LinkSource::ExternalEditor(handler) => {
+ let target_id = SharedString::from(format!("editor-link/{id:?}/{}", mark.url));
+ let focus = window
+ .use_keyed_state(target_id.clone(), cx, |_, cx| {
+ cx.focus_handle().tab_stop(true)
+ })
+ .read(cx)
+ .clone();
+ (
+ target_id,
+ focus,
+ Activation::ExternalEditor {
+ url: mark.url.clone(),
+ handler: handler.clone(),
+ },
+ SharedString::from(label),
+ mark.url,
+ window.text_style().color,
+ )
}
- let color = state.text_view_style.link();
- let owner = state.links.0.get_mut(&id)?;
- let lease = Rc::downgrade(owner.lease.get_or_insert_with(|| Rc::new(())));
- owner.focus = owner.focus.clone().tab_stop(true);
- Some((
- owner.focus.clone(),
- lease,
- owner.label.clone(),
- owner.mark.url.clone(),
- color,
- ))
- });
- let Some((focus, lease, label, url, color)) = prepared else {
- continue;
};
let bounds = regions.iter().copied().reduce(|a, b| a.union(&b)).unwrap();
let regions: Arc<[Bounds<Pixels>]> = regions
@@ -219,20 +341,14 @@ pub(super) fn targets(
.map(|r| Bounds::new(r.origin - bounds.origin, r.size))
.collect::<Vec<_>>()
.into();
- let weak = view.downgrade();
- let mouse_view = weak.clone();
- let mouse_lease = lease.clone();
- let key_view = weak.clone();
- let key_lease = lease.clone();
- let click_view = weak.clone();
- let click_lease = lease.clone();
- let aux_view = weak.clone();
- let aux_lease = lease.clone();
- let focus_view = weak.clone();
- let focus_lease = lease.clone();
+ let mouse_activation = activation.clone();
+ let key_activation = activation.clone();
+ let click_activation = activation.clone();
+ let aux_activation = activation.clone();
+ let focus_activation = activation.clone();
let ax_focus = focus.clone();
let element = div()
- .id(SharedString::from(format!("link/{id:?}")))
+ .id(target_id)
.w(bounds.size.width)
.h(bounds.size.height)
.hitbox_regions(regions.clone())
@@ -242,33 +358,27 @@ pub(super) fn targets(
.aria_label(if label.is_empty() { url } else { label })
.cursor_pointer()
.on_a11y_action(gpui::accesskit::Action::Focus, move |_, window, cx| {
- if admitted(&focus_view, id, &focus_lease, cx) {
+ if focus_activation.admitted(cx) {
window.focus(&ax_focus, cx);
}
})
.capture_any_mouse_down(move |_, window, cx| {
- if !admitted(&mouse_view, id, &mouse_lease, cx) {
+ if !mouse_activation.admitted(cx) {
window.prevent_default();
cx.stop_propagation();
}
})
.capture_key_down(move |_, window, cx| {
- if !admitted(&key_view, id, &key_lease, cx) {
+ if !key_activation.admitted(cx) {
window.prevent_default();
cx.stop_propagation();
}
})
- .on_click(move |event, window, cx| {
- activate(&click_view, id, &click_lease, event, window, cx)
- })
- .on_aux_click(move |event, window, cx| {
- activate(&aux_view, id, &aux_lease, event, window, cx)
- });
+ .on_click(move |event, window, cx| click_activation.activate(event, window, cx))
+ .on_aux_click(move |event, window, cx| aux_activation.activate(event, window, cx));
let mut target = LinkTarget {
element,
- view: weak,
- id,
- lease,
+ activation,
focus,
regions,
color,
@@ -334,26 +444,27 @@ fn activate(
if decision.stop_propagation {
cx.stop_propagation();
}
- let activates = match event {
+ if activates(event) && !window.default_prevented() && admitted(view, id, lease, cx) {
+ crate::TextSelection::end(window, cx);
+ cx.open_url(&url);
+ }
+}
+
+fn activates(event: &ClickEvent) -> bool {
+ match event {
ClickEvent::Mouse(click) => matches!(
click.up.button,
gpui::MouseButton::Left | gpui::MouseButton::Middle
),
ClickEvent::Touch(click) => !click.long_press,
ClickEvent::Keyboard(_) | ClickEvent::Accessibility(_) => true,
- };
- if activates && !window.default_prevented() && admitted(view, id, lease, cx) {
- crate::TextSelection::end(window, cx);
- cx.open_url(&url);
}
}
/// Delegates all native stages; only records the weak locator of its actual Div state.
struct LinkTarget {
element: Stateful<Div>,
- view: WeakEntity<TextViewState>,
- id: LinkOccurrence,
- lease: Weak<()>,
+ activation: Activation,
focus: FocusHandle,
regions: Arc<[Bounds<Pixels>]>,
color: gpui::Hsla,
@@ -395,10 +506,12 @@ impl Element for LinkTarget {
) -> (LayoutId, Self::RequestLayoutState) {
let layout = self.element.request_layout(id, inspector, window, cx);
let click = self.element.interactivity().click_handle(id, window);
- if let Some(view) = self.view.upgrade() {
+ if let Activation::TextView { view, id, lease } = &self.activation
+ && let Some(view) = view.upgrade()
+ {
view.update(cx, |state, _| {
- if state.links.admits(self.id, &self.lease) {
- state.links.0.get_mut(&self.id).unwrap().click = click;
+ if state.links.admits(*id, lease) {
+ state.links.0.get_mut(id).unwrap().click = click;
}
});
}
@@ -430,7 +543,7 @@ impl Element for LinkTarget {
.paint(id, inspector, bounds, layout, prepaint, window, cx);
if self.focus.is_focused(window)
&& window.last_input_was_keyboard()
- && admitted(&self.view, self.id, &self.lease, cx)
+ && self.activation.admitted(cx)
{
for region in self.regions.iter() {
window.paint_quad(gpui::quad(
@@ -575,6 +688,174 @@ mod tests {
cx.simulate_event(gpui::KeyUpEvent { keystroke });
}
+ #[gpui::test]
+ fn external_editor_links_use_native_fragments_and_their_explicit_source(
+ cx: &mut TestAppContext,
+ ) {
+ use crate::text::editor_host::{FlowPosition, RenderContext};
+ use crate::text::node::{ImageNode, InlineNode, TextMark};
+
+ struct Host {
+ blocks: Vec<BlockNode>,
+ context: RenderContext,
+ }
+ impl Render for Host {
+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let blocks: Vec<_> = self.blocks.iter().collect();
+ div().w(px(180.)).children(
+ blocks
+ .iter()
+ .enumerate()
+ .map(|(ix, block)| {
+ block.render_with_id(
+ ix,
+ FlowPosition::at(&blocks, ix, ix + 1 == blocks.len()),
+ &self.context,
... 16258 lines suppressed ...
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
(maka) 01/11: test(providers): specify Codex fixture language
Posted by wy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch rust-preview
in repository https://gitbox.apache.org/repos/asf/maka.git
commit d553a8374cfacd3200656bdf2a3e7a61b8167542
Author: M4n5ter <m4...@gmail.com>
AuthorDate: Tue Oct 20 13:02:59 2026 +0800
test(providers): specify Codex fixture language
Set the inventory wire fixture language explicitly so it compiles against the localized Responses request contract. The four Codex integration tests pass.
Generated-by: OpenAI Codex
---
plugins/agent/providers/tests/codex/inventory.rs | 1 +
1 file changed, 1 insertion(+)
diff --git a/plugins/agent/providers/tests/codex/inventory.rs b/plugins/agent/providers/tests/codex/inventory.rs
index d69ea3e46e..4435866ff2 100644
--- a/plugins/agent/providers/tests/codex/inventory.rs
+++ b/plugins/agent/providers/tests/codex/inventory.rs
@@ -202,6 +202,7 @@ async fn discovery_and_preparation_share_account_facts_and_explicit_policy() {
fn wire(model: &maka_agent_ports::provider::Model) -> serde_json::Value {
maka_responses::request::Request {
+ language: maka_agent_facts::language::Language::En,
model: &model.info.id,
prompt: &[],
tools: &[],
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
(maka) 02/11: chore(gpui): sync Fast to the official 0.1.10 release
Posted by wy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch rust-preview
in repository https://gitbox.apache.org/repos/asf/maka.git
commit b4f6c339fed41b153dea836207002b950a5e63bc
Author: M4n5ter <m4...@gmail.com>
AuthorDate: Tue Oct 20 13:25:53 2026 +0800
chore(gpui): sync Fast to the official 0.1.10 release
Import the normalized b836dd97 snapshot and matching nine-package dependency closure while preserving Maka input admission, retained selection, timer cancellation and native cleanup patches. Keep Kit at its existing compatible revision and record original-byte provenance.
Generated-by: OpenAI Codex
---
Cargo.lock | 372 ++++++++--
LICENSE | 2 +-
third-party/gpui-fast/SOURCE.md | 8 +-
third-party/gpui-fast/SOURCE.zh-CN.md | 8 +-
third-party/gpui-fast/crates/gpui/Cargo.toml | 87 ++-
third-party/gpui-fast/crates/gpui/build.rs | 10 +
third-party/gpui-fast/crates/gpui/src/app.rs | 148 +++-
.../gpui-fast/crates/gpui/src/app/test_context.rs | 68 ++
.../gpui-fast/crates/gpui/src/elements/div.rs | 90 +++
.../gpui-fast/crates/gpui/src/fast/composition.rs | 770 ++++++++++++++++++++-
.../crates/gpui/src/fast/layers/hover_freeze.rs | 43 +-
.../gpui-fast/crates/gpui/src/fast/layers/input.rs | 4 +
.../gpui-fast/crates/gpui/src/fast/layers/lists.rs | 16 +-
.../crates/gpui/src/fast/layers/overlay.rs | 12 +
.../gpui-fast/crates/gpui/src/fast/layers/paint.rs | 12 +-
.../crates/gpui/src/fast/layers/policy.rs | 2 +
.../gpui-fast/crates/gpui/src/fast/layers/reuse.rs | 23 +-
.../gpui-fast/crates/gpui/src/fast/layers/scene.rs | 3 +
.../gpui-fast/crates/gpui/src/fast/layers/tiles.rs | 10 +
.../crates/gpui/src/fast/layers/verify.rs | 3 +
.../gpui-fast/crates/gpui/src/fast/layout.rs | 115 ++-
third-party/gpui-fast/crates/gpui/src/fast/mod.rs | 2 +
.../gpui-fast/crates/gpui/src/fast/retained.rs | 88 +--
.../gpui-fast/crates/gpui/src/fast/rounded_clip.rs | 472 +++++++++++++
.../crates/gpui/src/fast/rounded_clip_fixtures.rs | 556 +++++++++++++++
.../crates/gpui/src/fast/rounded_clip_scene.rs | 133 ++++
.../gpui-fast/crates/gpui/src/fast/scene.rs | 4 +
.../gpui-fast/crates/gpui/src/fast/stats.rs | 3 +-
.../gpui-fast/crates/gpui/src/fast/tests/layers.rs | 63 ++
.../crates/gpui/src/fast/tests/layers_lists.rs | 254 ++++++-
.../gpui-fast/crates/gpui/src/fast/tests/layout.rs | 73 ++
.../crates/gpui/src/fast/tests/retained.rs | 81 +++
.../crates/gpui/src/fast/tests/support.rs | 24 +-
third-party/gpui-fast/crates/gpui/src/gestures.rs | 112 ++-
third-party/gpui-fast/crates/gpui/src/gpui.rs | 13 +
.../gpui-fast/crates/gpui/src/hot_reload.rs | 248 +++++++
third-party/gpui-fast/crates/gpui/src/input.rs | 126 ++++
.../gpui-fast/crates/gpui/src/key_dispatch.rs | 20 +
third-party/gpui-fast/crates/gpui/src/platform.rs | 68 +-
.../crates/gpui/src/platform/test/platform.rs | 27 +-
.../gpui-fast/crates/gpui/src/profiler/journal.rs | 2 +-
third-party/gpui-fast/crates/gpui/src/scene.rs | 12 +-
third-party/gpui-fast/crates/gpui/src/style.rs | 2 +-
third-party/gpui-fast/crates/gpui/src/tab_stop.rs | 9 +-
third-party/gpui-fast/crates/gpui/src/view.rs | 26 +
third-party/gpui-fast/crates/gpui/src/window.rs | 247 +++++--
third-party/gpui-fast/crates/gpui_linux/Cargo.toml | 14 +-
.../crates/gpui_linux/src/fast/composition/mod.rs | 3 +
.../gpui_linux/src/fast/composition/occluders.rs | 265 +++++++
.../gpui_linux/src/fast/composition/wayland.rs | 138 +++-
.../crates/gpui_linux/src/fast/composition/x11.rs | 335 ++++-----
.../crates/gpui_linux/src/linux/wayland/client.rs | 13 +-
.../crates/gpui_linux/src/linux/x11/client.rs | 44 +-
third-party/gpui-fast/crates/gpui_macos/Cargo.toml | 16 +-
.../crates/gpui_macos/src/fast/composition.rs | 427 +++++++++++-
.../gpui-fast/crates/gpui_macos/src/window.rs | 12 +-
.../gpui-fast/crates/gpui_windows/Cargo.toml | 5 +-
third-party/gpui-fast/crates/gpui_windows/build.rs | 7 +-
.../crates/gpui_windows/src/directx_renderer.rs | 5 +-
.../gpui-fast/crates/gpui_windows/src/events.rs | 31 +-
.../crates/gpui_windows/src/fast/composition.rs | 13 -
.../crates/gpui_windows/src/fast/frame.rs | 67 +-
.../gpui_windows/src/fast/layers/composite.rs | 20 +-
.../crates/gpui_windows/src/fast/layers/raster.rs | 1 +
.../crates/gpui_windows/src/fast/layers/tests.rs | 36 +
.../gpui-fast/crates/gpui_windows/src/fast/mod.rs | 1 +
.../crates/gpui_windows/src/fast/rounded_clip.rs | 271 ++++++++
.../src/fast/rounded_clip_shader_build.rs | 131 ++++
.../gpui-fast/crates/gpui_windows/src/platform.rs | 47 +-
.../gpui-fast/crates/gpui_windows/src/shaders.hlsl | 4 +-
third-party/gpui-fast/upstream.json | 151 ++--
71 files changed, 5754 insertions(+), 774 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index baa59ae140..f17b9387a7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1096,6 +1096,15 @@ dependencies = [
"bit-vec 0.9.1",
]
+[[package]]
+name = "bit-set"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d"
+dependencies = [
+ "bit-vec 0.9.1",
+]
+
[[package]]
name = "bit-vec"
version = "0.8.0"
@@ -1473,7 +1482,7 @@ dependencies = [
"cap-primitives",
"cap-std",
"io-lifetimes 3.0.1",
- "windows-sys 0.61.2",
+ "windows-sys 0.60.2",
]
[[package]]
@@ -1490,7 +1499,7 @@ dependencies = [
"maybe-owned",
"rustix 1.1.4",
"rustix-linux-procfs",
- "windows-sys 0.61.2",
+ "windows-sys 0.60.2",
"winx",
]
@@ -3971,7 +3980,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -4490,7 +4499,7 @@ checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a"
dependencies = [
"io-lifetimes 2.0.4",
"rustix 1.1.4",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -5683,7 +5692,7 @@ dependencies = [
"log",
"presser",
"thiserror 2.0.20",
- "windows 0.62.2",
+ "windows 0.58.0",
]
[[package]]
@@ -5754,7 +5763,7 @@ dependencies = [
[[package]]
name = "gpui-fast"
-version = "0.1.7"
+version = "0.1.10"
dependencies = [
"accesskit",
"anyhow",
@@ -5823,9 +5832,9 @@ dependencies = [
[[package]]
name = "gpui-fast-apple"
-version = "0.1.7"
+version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7430efcfe8fadf11c1b923bcc435a3b646566f77a8e386e0505a4827c3d08046"
+checksum = "cd2f932eea173f357ace60f2267cd0c8031123fc788160b61f8566efe51f6bd0"
dependencies = [
"anyhow",
"async-task",
@@ -5865,7 +5874,7 @@ dependencies = [
[[package]]
name = "gpui-fast-linux"
-version = "0.1.7"
+version = "0.1.10"
dependencies = [
"accesskit",
"accesskit_unix",
@@ -5908,7 +5917,7 @@ dependencies = [
[[package]]
name = "gpui-fast-macos"
-version = "0.1.7"
+version = "0.1.10"
dependencies = [
"accesskit",
"accesskit_macos",
@@ -5946,9 +5955,9 @@ dependencies = [
[[package]]
name = "gpui-fast-macros"
-version = "0.1.7"
+version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07a2291699eb41f1cc9f956cbd359a16a397bab44d496cb44ad2b5900a935b9f"
+checksum = "0ce9b7a528e074c62bf71f05d886d6c8db88d567de2ccd8ab3d228ea2a6f6711"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -5958,9 +5967,9 @@ dependencies = [
[[package]]
name = "gpui-fast-platform"
-version = "0.1.7"
+version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4367bf7c733230468d44823d356aef89b1586b391afaf7f9a280d1a6703ac02c"
+checksum = "b1a7f82c182e1c0f1b0cc0d28493e2f7dc1c00bc7b57e581b33502b1154bc5a0"
dependencies = [
"anyhow",
"console_error_panic_hook",
@@ -5969,13 +5978,14 @@ dependencies = [
"gpui-fast-macos",
"gpui-fast-web",
"gpui-fast-windows",
+ "gpui-pre-ios",
]
[[package]]
name = "gpui-fast-web"
-version = "0.1.7"
+version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "007dd7d6aea0c857037b8a655c824082de1124265c3da666186dcb5f09a63c99"
+checksum = "44dac167a7e6c1d43a836df0f7e4cf1db6b3a843ec95653d26b1df2a88d52a74"
dependencies = [
"anyhow",
"async-channel",
@@ -6003,9 +6013,9 @@ dependencies = [
[[package]]
name = "gpui-fast-wgpu"
-version = "0.1.7"
+version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d250af726008e598b78a603b340539c8df1734199680635df61556398114f7eb"
+checksum = "cac7d34083b2611b49b2f415a16aa9e33620b127a25e981f835db8fe72f07423"
dependencies = [
"anyhow",
"bytemuck",
@@ -6024,13 +6034,13 @@ dependencies = [
"unicode-bidi",
"unicode-segmentation",
"web-sys",
- "wgpu",
+ "wgpu 30.0.1",
"zed-font-kit",
]
[[package]]
name = "gpui-fast-windows"
-version = "0.1.7"
+version = "0.1.10"
dependencies = [
"accesskit",
"accesskit_windows",
@@ -6239,6 +6249,28 @@ dependencies = [
"webpki-roots 1.0.9",
]
+[[package]]
+name = "gpui-pre-ios"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13e735b73ae85ed188764287df4d24837045d71a122c51c2264aed8c38ffd51b"
+dependencies = [
+ "anyhow",
+ "block2 0.6.2",
+ "futures",
+ "gpui-pre",
+ "gpui-pre-apple",
+ "log",
+ "objc2 0.6.4",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.2",
+ "objc2-quartz-core 0.3.2",
+ "objc2-ui-kit",
+ "parking_lot",
+ "raw-window-handle",
+ "uuid",
+]
+
[[package]]
name = "gpui-pre-linux"
version = "0.3.9"
@@ -6532,7 +6564,7 @@ dependencies = [
"unicode-bidi",
"unicode-segmentation",
"web-sys",
- "wgpu",
+ "wgpu 29.0.4",
"zed-font-kit",
]
@@ -7151,7 +7183,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
- "socket2 0.6.5",
+ "socket2 0.5.10",
"tokio",
"tower-service",
"tracing",
@@ -7169,7 +7201,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
- "windows-core 0.62.2",
+ "windows-core 0.58.0",
]
[[package]]
@@ -10100,7 +10132,7 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08"
dependencies = [
- "windows-sys 0.61.2",
+ "windows-sys 0.60.2",
]
[[package]]
@@ -10165,6 +10197,44 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "naga"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130"
+dependencies = [
+ "arrayvec",
+ "bit-set 0.10.0",
+ "bitflags 2.13.2",
+ "cfg-if",
+ "cfg_aliases",
+ "codespan-reporting",
+ "half",
+ "hashbrown 0.17.1",
+ "indexmap 2.14.2",
+ "libm",
+ "log",
+ "naga-types",
+ "num-traits",
+ "once_cell",
+ "rustc-hash 1.1.0",
+ "spirv",
+ "thiserror 2.0.20",
+ "unicode-ident",
+]
+
+[[package]]
+name = "naga-types"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "590afbf58a6f4f62873cd5cff4468061844bafa1cdf399cc954537c22d768d49"
+dependencies = [
+ "hashbrown 0.17.1",
+ "indexmap 2.14.2",
+ "rustc-hash 1.1.0",
+ "thiserror 2.0.20",
+]
+
[[package]]
name = "native-tls"
version = "0.2.18"
@@ -10605,7 +10675,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags 2.13.2",
+ "block2 0.6.2",
"dispatch2",
+ "libc",
"objc2 0.6.4",
]
@@ -10778,6 +10850,7 @@ dependencies = [
"bitflags 2.13.2",
"objc2 0.6.4",
"objc2-core-foundation",
+ "objc2-core-graphics",
"objc2-foundation 0.3.2",
"objc2-metal 0.3.2",
]
@@ -12041,7 +12114,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf"
dependencies = [
"anyhow",
- "itertools 0.14.0",
+ "itertools 0.13.0",
"proc-macro2",
"quote",
"syn 2.0.119",
@@ -12242,7 +12315,7 @@ dependencies = [
"quinn-udp",
"rustc-hash 2.1.3",
"rustls",
- "socket2 0.6.5",
+ "socket2 0.5.10",
"thiserror 2.0.20",
"tokio",
"tracing",
@@ -12282,9 +12355,9 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
- "socket2 0.6.5",
+ "socket2 0.5.10",
"tracing",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -13099,7 +13172,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.4.15",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -13112,7 +13185,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.12.1",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -13214,7 +13287,7 @@ dependencies = [
"security-framework 3.7.0",
"security-framework-sys",
"webpki-root-certs",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -14099,7 +14172,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [
"libc",
- "windows-sys 0.61.2",
+ "windows-sys 0.60.2",
]
[[package]]
@@ -14686,7 +14759,7 @@ dependencies = [
"ntapi",
"objc2-core-foundation",
"objc2-io-kit",
- "windows 0.61.3",
+ "windows 0.59.0",
]
[[package]]
@@ -14795,7 +14868,7 @@ dependencies = [
"getrandom 0.4.3",
"once_cell",
"rustix 1.1.4",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -15473,7 +15546,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset 0.9.1",
"tempfile",
- "windows-sys 0.61.2",
+ "windows-sys 0.60.2",
]
[[package]]
@@ -16319,7 +16392,7 @@ dependencies = [
"hashbrown 0.16.1",
"js-sys",
"log",
- "naga",
+ "naga 29.0.4",
"parking_lot",
"portable-atomic",
"profiling",
@@ -16329,9 +16402,39 @@ dependencies = [
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
- "wgpu-core",
- "wgpu-hal",
- "wgpu-types",
+ "wgpu-core 29.0.4",
+ "wgpu-hal 29.0.4",
+ "wgpu-types 29.0.4",
+]
+
+[[package]]
+name = "wgpu"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "527ccdf43dd5b2e8676eed9984ce00e2bbb0a1b85b70c1969dcb6cd2eb55ab9e"
+dependencies = [
+ "arrayvec",
+ "bitflags 2.13.2",
+ "bytemuck",
+ "cfg-if",
+ "cfg_aliases",
+ "document-features",
+ "hashbrown 0.17.1",
+ "js-sys",
+ "log",
+ "naga 30.0.1",
+ "parking_lot",
+ "portable-atomic",
+ "profiling",
+ "raw-window-handle",
+ "smallvec",
+ "static_assertions",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "wgpu-core 30.0.1",
+ "wgpu-hal 30.0.1",
+ "wgpu-types 30.0.1",
]
[[package]]
@@ -16350,7 +16453,7 @@ dependencies = [
"hashbrown 0.16.1",
"indexmap 2.14.2",
"log",
- "naga",
+ "naga 29.0.4",
"once_cell",
"parking_lot",
"portable-atomic",
@@ -16359,13 +16462,48 @@ dependencies = [
"rustc-hash 1.1.0",
"smallvec",
"thiserror 2.0.20",
- "wgpu-core-deps-apple",
- "wgpu-core-deps-emscripten",
- "wgpu-core-deps-wasm",
- "wgpu-core-deps-windows-linux-android",
- "wgpu-hal",
- "wgpu-naga-bridge",
- "wgpu-types",
+ "wgpu-core-deps-apple 29.0.4",
+ "wgpu-core-deps-emscripten 29.0.4",
+ "wgpu-core-deps-wasm 29.0.4",
+ "wgpu-core-deps-windows-linux-android 29.0.4",
+ "wgpu-hal 29.0.4",
+ "wgpu-naga-bridge 29.0.4",
+ "wgpu-types 29.0.4",
+]
+
+[[package]]
+name = "wgpu-core"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c018fce9b6270aa203c2fdd56f3cce996713534bd757e4ea58c8560b121f14"
+dependencies = [
+ "arrayvec",
+ "bit-set 0.10.0",
+ "bit-vec 0.9.1",
+ "bitflags 2.13.2",
+ "bytemuck",
+ "cfg_aliases",
+ "document-features",
+ "hashbrown 0.17.1",
+ "indexmap 2.14.2",
+ "log",
+ "naga 30.0.1",
+ "naga-types",
+ "once_cell",
+ "parking_lot",
+ "portable-atomic",
+ "profiling",
+ "raw-window-handle",
+ "rustc-hash 1.1.0",
+ "smallvec",
+ "thiserror 2.0.20",
+ "wgpu-core-deps-apple 30.0.1",
+ "wgpu-core-deps-emscripten 30.0.1",
+ "wgpu-core-deps-wasm 30.0.1",
+ "wgpu-core-deps-windows-linux-android 30.0.1",
+ "wgpu-hal 30.0.1",
+ "wgpu-naga-bridge 30.0.1",
+ "wgpu-types 30.0.1",
]
[[package]]
@@ -16374,7 +16512,16 @@ version = "29.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5e39e26c4c0e07589e67d18546cf79ff45383659fc72fca4dd293358a0347f3"
dependencies = [
- "wgpu-hal",
+ "wgpu-hal 29.0.4",
+]
+
+[[package]]
+name = "wgpu-core-deps-apple"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "061f3d319a40d39d00b1ecc2c33b89fe21d4e6fe01859df3500a3a8ecccd6b68"
+dependencies = [
+ "wgpu-hal 30.0.1",
]
[[package]]
@@ -16383,7 +16530,16 @@ version = "29.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01e09be551dc939498bdd5f6b2c66e55ab275dad25825267a08605a80fc9f0af"
dependencies = [
- "wgpu-hal",
+ "wgpu-hal 29.0.4",
+]
+
+[[package]]
+name = "wgpu-core-deps-emscripten"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98b86cf4abf524a902dd35f18ca6a3f08fc2ae9847c8f10b48e30491b1f0b86"
+dependencies = [
+ "wgpu-hal 30.0.1",
]
[[package]]
@@ -16392,7 +16548,16 @@ version = "29.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af1fb1798be2a912497d4c224f72d39bb0cb34af50e8bcc29865bc339c943059"
dependencies = [
- "wgpu-hal",
+ "wgpu-hal 29.0.4",
+]
+
+[[package]]
+name = "wgpu-core-deps-wasm"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d260ca2ca8adc654f513a869bf3cac0b47f5b8d3f87803d6942015f8de597aae"
+dependencies = [
+ "wgpu-hal 30.0.1",
]
[[package]]
@@ -16401,7 +16566,16 @@ version = "29.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e592c1bbef6ad047647ae6e666ebd8cee7a32bb4544d9700ec96cbf73230257"
dependencies = [
- "wgpu-hal",
+ "wgpu-hal 29.0.4",
+]
+
+[[package]]
+name = "wgpu-core-deps-windows-linux-android"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7586165fd5f6d881cb9ce4bb71f40d6caab2c0f1837e3fc1d9788a197fb6004f"
+dependencies = [
+ "wgpu-hal 30.0.1",
]
[[package]]
@@ -16429,10 +16603,65 @@ dependencies = [
"libc",
"libloading",
"log",
- "naga",
+ "naga 29.0.4",
+ "ndk-sys",
+ "objc2 0.6.4",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.2",
+ "objc2-metal 0.3.2",
+ "objc2-quartz-core 0.3.2",
+ "once_cell",
+ "ordered-float",
+ "parking_lot",
+ "portable-atomic",
+ "portable-atomic-util",
+ "profiling",
+ "range-alloc",
+ "raw-window-handle",
+ "raw-window-metal",
+ "renderdoc-sys",
+ "smallvec",
+ "thiserror 2.0.20",
+ "wasm-bindgen",
+ "wayland-sys",
+ "web-sys",
+ "wgpu-naga-bridge 29.0.4",
+ "wgpu-types 29.0.4",
+ "windows 0.62.2",
+ "windows-core 0.62.2",
+ "windows-result 0.4.1",
+]
+
+[[package]]
+name = "wgpu-hal"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6b7fb58561a792bc237628ba0792e332de418fefe145f13b5ed8201e6d52f58"
+dependencies = [
+ "android_system_properties",
+ "arrayvec",
+ "ash",
+ "bit-set 0.10.0",
+ "bitflags 2.13.2",
+ "block2 0.6.2",
+ "bytemuck",
+ "cfg-if",
+ "cfg_aliases",
+ "glow",
+ "glutin_wgl_sys",
+ "gpu-allocator",
+ "hashbrown 0.17.1",
+ "js-sys",
+ "khronos-egl",
+ "libc",
+ "libloading",
+ "log",
+ "naga 30.0.1",
+ "naga-types",
"ndk-sys",
"objc2 0.6.4",
"objc2-core-foundation",
+ "objc2-core-graphics",
"objc2-foundation 0.3.2",
"objc2-metal 0.3.2",
"objc2-quartz-core 0.3.2",
@@ -16447,12 +16676,13 @@ dependencies = [
"raw-window-metal",
"renderdoc-sys",
"smallvec",
+ "static_assertions",
"thiserror 2.0.20",
"wasm-bindgen",
"wayland-sys",
"web-sys",
- "wgpu-naga-bridge",
- "wgpu-types",
+ "wgpu-naga-bridge 30.0.1",
+ "wgpu-types 30.0.1",
"windows 0.62.2",
"windows-core 0.62.2",
"windows-result 0.4.1",
@@ -16464,8 +16694,18 @@ version = "29.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95226013f547544b223281cd16a4fb549aa9dcb562adbda0faae4c73ffbbc161"
dependencies = [
- "naga",
- "wgpu-types",
+ "naga 29.0.4",
+ "wgpu-types 29.0.4",
+]
+
+[[package]]
+name = "wgpu-naga-bridge"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2f62e73117bb7a62bfd9c5a5841438a823f6566c6442a808ee269d2d055c081"
+dependencies = [
+ "naga 30.0.1",
+ "wgpu-types 30.0.1",
]
[[package]]
@@ -16482,6 +16722,22 @@ dependencies = [
"web-sys",
]
+[[package]]
+name = "wgpu-types"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99dad6f1fbdbbdb4c278a6508b059d44688f5cebddf78d005a46a31340269286"
+dependencies = [
+ "bitflags 2.13.2",
+ "bytemuck",
+ "js-sys",
+ "log",
+ "naga-types",
+ "raw-window-handle",
+ "static_assertions",
+ "web-sys",
+]
+
[[package]]
name = "which"
version = "6.0.3"
@@ -16537,7 +16793,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -17243,7 +17499,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d"
dependencies = [
"bitflags 2.13.2",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
]
[[package]]
diff --git a/LICENSE b/LICENSE
index fe5db23180..8d898e341b 100644
--- a/LICENSE
+++ b/LICENSE
@@ -334,7 +334,7 @@ GPUI Fast platform source (adapted, Apache License 2.0)
third-party/gpui-fast contains five crates from
https://github.com/longbridge/gpui-fast at revision
-8e1e1987fdb512bb8b0ab27261906a598aff2ab8.
+b836dd970b26830f2124b7cf91b0e19018fd12da.
Copyright 2022 - 2025 Zed Industries, Inc.
Original license texts, finite file provenance and modifications are recorded in
third-party/gpui-fast/{LICENSE,LICENSE-APACHE,upstream.json,SOURCE.md}.
diff --git a/third-party/gpui-fast/SOURCE.md b/third-party/gpui-fast/SOURCE.md
index 800792ec0c..5b86dfd05f 100644
--- a/third-party/gpui-fast/SOURCE.md
+++ b/third-party/gpui-fast/SOURCE.md
@@ -19,7 +19,7 @@
# GPUI Fast platform cleanup patch
-Five crates are copied from [GPUI Fast](https://github.com/longbridge/gpui-fast/tree/371769f1ff378bc0bd886440e0f97fc94aaeae5d), revision `371769f1ff378bc0bd886440e0f97fc94aaeae5d`: `gpui`, `gpui_macos`, `gpui_windows`, `gpui_linux`, `scheduler`. The sources track the pinned upstream `main` revision; normalized manifests use the official `0.1.7` release dependency rules. Core and platform crates retain their Rust library names under the published `gpui-fast*` package names; the modified s [...]
+Five crates are copied from [GPUI Fast](https://github.com/longbridge/gpui-fast/tree/b836dd970b26830f2124b7cf91b0e19018fd12da), revision `b836dd970b26830f2124b7cf91b0e19018fd12da`: `gpui`, `gpui_macos`, `gpui_windows`, `gpui_linux`, `scheduler`. The sources track the pinned upstream `main` revision; normalized manifests use the official `0.1.10` release dependency rules. Core and platform crates retain their Rust library names under the published `gpui-fast*` package names; the modified [...]
`upstream.json` records the finite original file inventory and SHA-256 hashes before changes. Original license texts and upstream attribution are retained. New Maka source is not included in this inventory. Workspace dependencies are normalized for the isolated five-crate workspace, with explicit upstream license-file paths.
@@ -99,9 +99,11 @@ The accessibility log participates in existing prepaint rollback without cloning
Programmatic list offsets still bump their original dependency version, including with layers enabled; layers separately classify that version as a scroll. Without this, an unnotified offset assignment can leave a retained ancestor displaying the previous offset (also reproduced with unmodified upstream). Upstream's deferred-view splice guard is retained: an ancestor with deferred draws outside rebuilt gaps rebuilds to avoid replaying stale deferred views. The local causal-paint test now [...]
-## List rows keep their layout-time element states (2026-10-16)
+## Upstream synchronization (2026-10-20)
-Backported from upstream pull request [#73](https://github.com/longbridge/gpui-fast/pull/73) (commit `97bff7005ef4e5971237fcf890262947cff21b46`) until it is merged. When a frame keeps a list row on the list's scroll layer, the element states that the row's render and `request_layout` took are now kept with the row and carried with its prepaint records; before, they were dropped at the end of the first frame that kept the row. gpui-kit's `TextView` keeps a parsed document of more than 4 K [...]
+The five adapted crates now track `b836dd970b26830f2124b7cf91b0e19018fd12da` (`v0.1.10`). Their manifests follow upstream `script/prepare-release.ts`; all nine Fast packages resolve to 0.1.10, support snapshots and the adapted scheduler remain at 0.3.9, and the published WGPU backend uses registry `wgpu =30.0.1` with upstream's release adaptations. This published WGPU version does not include upstream's unreleased Vulkan surface-drop fix. Kit remains at its existing revision, whose Fast [...]
+
+Three-way integration preserves Maka's physical input provenance, exact published IME-handler slot, selection context, scheduler cancellation and native cleanup receipts. Upstream field identity and handler restoration notifications use those same existing owners. The native timer patch retains its explicit async-io dependency. The profiler counter uses stable `AtomicUsize::try_update` in place of its deprecated alias, with the same orderings and checked subtraction.
## A wheel gesture scrolls one container (2026-10-20)
diff --git a/third-party/gpui-fast/SOURCE.zh-CN.md b/third-party/gpui-fast/SOURCE.zh-CN.md
index 1c64d211c7..362bd422e0 100644
--- a/third-party/gpui-fast/SOURCE.zh-CN.md
+++ b/third-party/gpui-fast/SOURCE.zh-CN.md
@@ -19,7 +19,7 @@
# GPUI Fast 平台清理补丁
-从 [GPUI Fast](https://github.com/longbridge/gpui-fast/tree/371769f1ff378bc0bd886440e0f97fc94aaeae5d) `main` 固定 revision(manifest 采用正式 `0.1.7` 发布依赖规则) 保留 core、macOS、Windows、Linux 和 scheduler 五个 crate。按上游发布规则使用 `gpui-fast*` 包名,保持原 Rust library 名;修改过的 scheduler 对应 `gpui-pre-scheduler` 0.3.9。未修改 helper 使用同一正式发布及其 0.3.9 支持快照。Kit 选择官方 `gpui-fast` feature,根 registry source patch 提供修改过的实现;移除旧 gpui-pre Git 兼容 facade。
+从 [GPUI Fast](https://github.com/longbridge/gpui-fast/tree/b836dd970b26830f2124b7cf91b0e19018fd12da) `main` 固定 revision(manifest 采用正式 `0.1.10` 发布依赖规则) 保留 core、macOS、Windows、Linux 和 scheduler 五个 crate。按上游发布规则使用 `gpui-fast*` 包名,保持原 Rust library 名;修改过的 scheduler 对应 `gpui-pre-scheduler` 0.3.9。未修改 helper 使用同一正式发布及其 0.3.9 支持快照。Kit 选择官方 `gpui-fast` feature,根 registry source patch 提供修改过的实现;移除旧 gpui-pre Git 兼容 facade。
`upstream.json` 列出改动前的有限原始文件清单与 SHA-256。保留上游许可和署名;Maka 新文件不进入该清单。隔离 workspace 仅规范化 helper 依赖和显式许可文件路径。
@@ -74,9 +74,11 @@ Window 可根据原 ClickInputState interruption 创建不可 clone 的 InputGes
程序设置 List offset 时仍推进原依赖版本,使未显式 notify 的保留祖先也能看到偏移变化;图层单独将该版本识别为滚动,避免把它算作内容变化。未修补上游也可复现旧偏移残留。保留上游 deferred-view 拼接保护:重建 gap 外存在延迟绘制时重建祖先,防止复用过时视图;本地测试相应断言重建,并继续验证绘制因果顺序、缓存重放及与全量绘制的一致性。
-## 列表行保留布局期的元素状态(2026-10-16)
+## 上游同步(2026-10-20)
-在上游合并前,回移上游拉取请求 [#73](https://github.com/longbridge/gpui-fast/pull/73)(提交 `97bff7005ef4e5971237fcf890262947cff21b46`)。某帧沿用列表滚动图层里的行时,该行在渲染和 `request_layout` 中取得的元素状态现在随行保存,并随其 prepaint 记录一起带入新帧;此前它们在第一次沿用该行的帧结束时被丢弃。gpui-kit 的 `TextView` 把超过 4 KB 的已解析文档存为这种状态,状态新建时在后台重新解析,所以在高于视口的 markdown 行内滚动时,该行会有一帧按空文档的高度布局。在另一列表的行布局期间布局的行(内层 `ListSizingBehavior::Infer` 列表的行)属于那一行,不再记成外层列表的行。改动在 `crates/gpui/src/fast/layers/lists.rs`,另有 `crates/gpui/src/fast/layout.rs` 中的一个访问函数,以及 `crates/gpui/src/fas [...]
+五个适配 crate 现固定到 `b836dd970b26830f2124b7cf91b0e19018fd12da`(`v0.1.10`)。清单采用上游 `script/prepare-release.ts` 的发布规范;九个 Fast 包统一解析为 0.1.10,支持快照及适配 scheduler 保持 0.3.9。发布的 WGPU 后端按上游受检适配使用 registry `wgpu =30.0.1`,其中不含上游尚未发布的 Vulkan surface-drop 修复。Kit 保持原固定版本,其 Fast 版本约束接受本次更新。新增 rounded-clip、composition 和 hot-reload 源码已纳入原始字节清单;无关示例及 iOS 实现不导入,由 registry 依赖提供。
+
+三方整合保留 Maka 原物理输入来源、实际发布的 IME-handler 槽位、选区上下文、定时器取消及原生清理回执。上游字段身份和 handler 恢复通知接入同一既有 owner。原生定时器补丁保留显式 async-io 依赖;profiler 计数器改用稳定的 `AtomicUsize::try_update` 替代弃用别名,保持原内存顺序和 checked subtraction。
## 一次滚轮手势只滚动一个容器(2026-10-20)
diff --git a/third-party/gpui-fast/crates/gpui/Cargo.toml b/third-party/gpui-fast/crates/gpui/Cargo.toml
index b3a84c7702..49b0999fe6 100644
--- a/third-party/gpui-fast/crates/gpui/Cargo.toml
+++ b/third-party/gpui-fast/crates/gpui/Cargo.toml
@@ -1,17 +1,17 @@
[package]
name = "gpui-fast"
-version = "0.1.7"
+version = "0.1.10"
edition = "2024"
-authors = ["Nathan Sobo <na...@zed.dev>"]
+authors = [ "Nathan Sobo <na...@zed.dev>" ]
description = "Zed's GPU-accelerated UI framework"
repository = "https://github.com/sunli829/gpui-fast"
publish = false
license = "Apache-2.0"
homepage = "https://gpui.rs"
readme = "README.md"
-keywords = ["desktop", "gui", "immediate"]
-categories = ["gui"]
+keywords = [ "desktop", "gui", "immediate" ]
+categories = [ "gui" ]
autoexamples = false
autobenches = false
license-file = "../../LICENSE-APACHE"
@@ -47,20 +47,36 @@ level = "allow"
priority = -1
[features]
-default = ["font-kit", "wayland", "x11", "windows-manifest"]
-test-support = ["collections/test-support", "http_client/test-support", "wayland", "x11", "proptest", "dep:futures-concurrency", "dep:postage", "dep:web-time"]
-bench-support = ["profiler", "dep:criterion", "dep:bench_metrics"]
-bench = ["bench-support"]
-inspector = ["gpui_macros/inspector"]
-leak-detection = []
-font-kit = []
-wayland = []
-x11 = ["scap?/x11"]
-screen-capture = ["scap"]
-windows-manifest = ["dep:embed-resource"]
-windows-manifest-provided-by-host = []
-profiler = ["dep:hdrhistogram"]
-stacker = ["dep:stacksafe"]
+default = [ "font-kit", "wayland", "x11", "windows-manifest" ]
+test-support = [
+ "collections/test-support",
+ "http_client/test-support",
+ "wayland",
+ "x11",
+ "proptest",
+ "dep:futures-concurrency",
+ "dep:postage",
+ "dep:web-time"
+]
+bench-support = [ "profiler", "dep:criterion", "dep:bench_metrics" ]
+bench = [ "bench-support" ]
+inspector = [ "gpui_macros/inspector" ]
+leak-detection = [ ]
+font-kit = [ ]
+wayland = [ ]
+x11 = [ "scap?/x11" ]
+screen-capture = [ "scap", "dep:windows-capture" ]
+windows-manifest = [ "dep:embed-resource" ]
+windows-manifest-provided-by-host = [ ]
+profiler = [ "dep:hdrhistogram" ]
+stacker = [ "dep:stacksafe" ]
+hot-reload = [
+ "dep:async-tungstenite",
+ "dep:dioxus-cli-config",
+ "dep:dioxus-devtools-types",
+ "dep:smol",
+ "dep:subsecond"
+]
[lib]
path = "src/gpui.rs"
@@ -124,7 +140,7 @@ optional = true
[dependencies.gpui_macros]
package = "gpui-fast-macros"
-version = "=0.1.7"
+version = "=0.1.10"
[dependencies.gpui_shared_string]
package = "gpui-pre-shared-string"
@@ -421,9 +437,9 @@ version = "1.1.0"
[dev-dependencies.gpui_platform]
default-features = false
-features = ["font-kit", "wayland", "x11", "runtime_shaders"]
+features = [ "font-kit", "wayland", "x11", "runtime_shaders" ]
package = "gpui-fast-platform"
-version = "=0.1.7"
+version = "=0.1.10"
[dev-dependencies.gpui_util]
package = "gpui-pre-util"
@@ -451,3 +467,32 @@ version = "1.10"
[build-dependencies.embed-resource]
version = "3.0"
optional = true
+
+[target."cfg(target_os = \"windows\")".dependencies.windows-capture]
+version = "=1.4.2"
+optional = true
+
+
+[target."cfg(any(target_os = \"macos\", target_os = \"linux\", target_os = \"windows\", target_os = \"ios\"))".dependencies.async-tungstenite]
+version = "0.33"
+optional = true
+
+
+[target."cfg(any(target_os = \"macos\", target_os = \"linux\", target_os = \"windows\", target_os = \"ios\"))".dependencies.dioxus-cli-config]
+version = "=0.7.10"
+optional = true
+
+
+[target."cfg(any(target_os = \"macos\", target_os = \"linux\", target_os = \"windows\", target_os = \"ios\"))".dependencies.dioxus-devtools-types]
+version = "=0.7.10"
+optional = true
+
+
+[target."cfg(any(target_os = \"macos\", target_os = \"linux\", target_os = \"windows\", target_os = \"ios\"))".dependencies.smol]
+version = "2.0"
+optional = true
+
+
+[target."cfg(any(target_os = \"macos\", target_os = \"linux\", target_os = \"windows\", target_os = \"ios\"))".dependencies.subsecond]
+version = "=0.7.10"
+optional = true
diff --git a/third-party/gpui-fast/crates/gpui/build.rs b/third-party/gpui-fast/crates/gpui/build.rs
index 13fed1df2e..501dbd02dc 100644
--- a/third-party/gpui-fast/crates/gpui/build.rs
+++ b/third-party/gpui-fast/crates/gpui/build.rs
@@ -22,6 +22,16 @@ fn main() {
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
+ // Hot reloading is limited to debug builds so the feature can't ship in release binaries,
+ // and to the platforms whose patch transport is implemented.
+ println!("cargo::rustc-check-cfg=cfg(gpui_hot_reload)");
+ if std::env::var_os("CARGO_FEATURE_HOT_RELOAD").is_some()
+ && std::env::var_os("CARGO_CFG_DEBUG_ASSERTIONS").is_some()
+ && matches!(target_os.as_str(), "macos" | "linux" | "windows" | "ios")
+ {
+ println!("cargo::rustc-cfg=gpui_hot_reload");
+ }
+
if target_os == "windows" {
#[cfg(feature = "windows-manifest")]
fast_manifest::embed_resource_unless_host_provides_it();
diff --git a/third-party/gpui-fast/crates/gpui/src/app.rs b/third-party/gpui-fast/crates/gpui/src/app.rs
index d1fe5d6bb3..c6af679468 100644
--- a/third-party/gpui-fast/crates/gpui/src/app.rs
+++ b/third-party/gpui-fast/crates/gpui/src/app.rs
@@ -8,7 +8,7 @@ use std::{
ops::{Deref, DerefMut},
path::{Path, PathBuf},
rc::{Rc, Weak},
- sync::{Arc, atomic::Ordering::SeqCst},
+ sync::Arc,
time::Duration,
};
@@ -45,17 +45,17 @@ use crate::InspectorElementRegistry;
use crate::asset_cache::CachedLoad;
use crate::{
Action, ActionBuildError, ActionRegistry, ActivationPolicy, ActivityGuard, Any, AnyView,
- AnyWindowHandle, AppContext, Arena, ArenaBox, Asset, AssetSource, BackgroundExecutor, Bounds,
- ClipboardItem, ClipboardReadError, CursorStyle, DispatchPhase, DisplayChanges, DisplayEvent,
- DisplayId, EventEmitter, ExternalDragPayload, FocusHandle, FocusMap, ForegroundExecutor,
- Global, KeyBinding, KeyContext, Keymap, Keystroke, LayoutId, Menu, MenuItem, MissingGlyph,
- OwnedMenu, PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout,
- PlatformKeyboardMapper, Point, Priority, PromptBuilder, PromptButton, PromptHandle,
- PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource,
- SharedString, SubscriberSet, Subscription, SvgRenderer, SystemNotification,
- SystemNotificationResponse, Task, TextRenderingMode, TextSystem, ThermalState, Window,
- WindowAppearance, WindowButtonLayout, WindowHandle, WindowId, WindowInvalidator,
- WindowingRequest,
+ AnyWindowHandle, AppContext, AppLifecyclePhase, Arena, ArenaBox, Asset, AssetSource,
+ BackgroundExecutor, Bounds, ClipboardItem, ClipboardReadError, CursorStyle, DispatchPhase,
+ DisplayChanges, DisplayEvent, DisplayId, EventEmitter, ExternalDragPayload, FocusHandle,
+ FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext, Keymap, Keystroke, LayoutId,
+ Menu, MenuItem, MissingGlyph, OwnedMenu, PathPromptOptions, Pixels, Platform, PlatformDisplay,
+ PlatformKeyboardLayout, PlatformKeyboardMapper, Point, Priority, PromptBuilder, PromptButton,
+ PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation,
+ ScreenCaptureSource, SharedString, SubscriberSet, Subscription, SvgRenderer,
+ SystemNotification, SystemNotificationResponse, Task, TextRenderingMode, TextSystem,
+ ThermalState, Window, WindowAppearance, WindowButtonLayout, WindowHandle, WindowId,
+ WindowInvalidator, WindowingRequest,
colors::{Colors, GlobalColors},
hash, init_app_menus,
};
@@ -256,6 +256,8 @@ impl Application {
let cx = &mut *this.borrow_mut();
// Some platforms only connect to their display server in `run`.
cx.displays_changed();
+ #[cfg(gpui_hot_reload)]
+ crate::hot_reload::init(cx);
on_finish_launching(cx);
}));
@@ -282,6 +284,8 @@ impl Application {
let cx = &mut *this.borrow_mut();
// Some platforms only connect to their display server in `run`.
cx.displays_changed();
+ #[cfg(gpui_hot_reload)]
+ crate::hot_reload::init(cx);
on_finish_launching(cx);
}));
ApplicationHandle { app: self.0 }
@@ -334,6 +338,7 @@ impl Application {
}
type Handler = Box<dyn FnMut(&mut App) -> bool + 'static>;
+type AppLifecycleHandler = Box<dyn FnMut(AppLifecyclePhase, &mut App) -> bool + 'static>;
type DisplayHandler = Box<dyn FnMut(DisplayEvent, &mut App) -> bool + 'static>;
/// The properties of a display that GPUI reports changes to.
@@ -833,7 +838,7 @@ pub struct App {
pub(crate) new_entity_observers: SubscriberSet<TypeId, NewEntityListener>,
pub(crate) windows: SlotMap<WindowId, Option<Box<Window>>>,
pub(crate) window_handles: FxHashMap<WindowId, AnyWindowHandle>,
- pub(crate) focus_handles: Arc<FocusMap>,
+ pub(crate) focus_handles: Arc<RwLock<FocusMap>>,
pub(crate) keymap: Rc<RefCell<Keymap>>,
pub(crate) keyboard_layout: Box<dyn PlatformKeyboardLayout>,
pub(crate) keyboard_mapper: Rc<dyn PlatformKeyboardMapper>,
@@ -854,6 +859,8 @@ pub struct App {
displays: HashMap<DisplayId, DisplayState>,
pub(crate) system_sleep_observers: SubscriberSet<(), Handler>,
pub(crate) system_wake_observers: SubscriberSet<(), Handler>,
+ app_lifecycle_observers: SubscriberSet<(), AppLifecycleHandler>,
+ memory_warning_observers: SubscriberSet<(), Handler>,
pub(crate) release_listeners: SubscriberSet<EntityId, ReleaseListener>,
pub(crate) global_observers: SubscriberSet<TypeId, Handler>,
pub(crate) quit_observers: SubscriberSet<(), QuitHandler>,
@@ -972,7 +979,7 @@ impl App {
windows: SlotMap::with_key(),
window_update_stack: Vec::new(),
window_handles: FxHashMap::default(),
- focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
+ focus_handles: Arc::new(RwLock::new(FocusMap::default())),
keymap: Rc::new(RefCell::new(Keymap::default())),
keyboard_layout,
keyboard_mapper,
@@ -996,6 +1003,8 @@ impl App {
displays: read_displays(platform.as_ref()),
system_sleep_observers: SubscriberSet::new(),
system_wake_observers: SubscriberSet::new(),
+ app_lifecycle_observers: SubscriberSet::new(),
+ memory_warning_observers: SubscriberSet::new(),
global_observers: SubscriberSet::new(),
quit_observers: SubscriberSet::new(),
quit_request: None,
@@ -1092,6 +1101,32 @@ impl App {
}
}));
+ platform.on_app_lifecycle(Box::new({
+ let app = Rc::downgrade(&app);
+ move |phase| {
+ if let Some(app) = app.upgrade() {
+ app.borrow_mut().update(|cx| {
+ cx.app_lifecycle_observers
+ .clone()
+ .retain(&(), |callback| callback(phase, cx));
+ });
+ }
+ }
+ }));
+
+ platform.on_memory_warning(Box::new({
+ let app = Rc::downgrade(&app);
+ move || {
+ if let Some(app) = app.upgrade() {
+ app.borrow_mut().update(|cx| {
+ cx.memory_warning_observers
+ .clone()
+ .retain(&(), |callback| callback(cx));
+ });
+ }
+ }
+ }));
+
platform.on_quit(Box::new({
let cx = Rc::downgrade(&app);
move || {
@@ -1719,6 +1754,42 @@ impl App {
subscription
}
+ /// Observes mobile application lifecycle changes. Desktop platforms do not
+ /// emit these events. On iOS these currently describe the single connected
+ /// scene, not individual windows.
+ ///
+ /// Save important changes incrementally: neither disconnection nor quit is
+ /// guaranteed before the system terminates a backgrounded application.
+ /// Dropping the returned subscription removes the observer.
+ pub fn on_app_lifecycle(
+ &self,
+ mut callback: impl FnMut(AppLifecyclePhase, &mut App) + 'static,
+ ) -> Subscription {
+ let (subscription, activate) = self.app_lifecycle_observers.insert(
+ (),
+ Box::new(move |phase, cx| {
+ callback(phase, cx);
+ true
+ }),
+ );
+ activate();
+ subscription
+ }
+
+ /// Observes mobile memory-pressure warnings so applications can release
+ /// expendable caches. Dropping the subscription removes the observer.
+ pub fn on_memory_warning(&self, mut callback: impl FnMut(&mut App) + 'static) -> Subscription {
+ let (subscription, activate) = self.memory_warning_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()
@@ -2119,25 +2190,36 @@ impl App {
/// Repeatedly called during `flush_effects` to handle a focused handle being dropped.
fn release_dropped_focus_handles(&mut self) {
- self.focus_handles
- .clone()
- .write()
- .retain(|handle_id, focus| {
- if focus.ref_count.load(SeqCst) == 0 {
- for window_handle in self.windows() {
- window_handle
- .update(self, |_, window, cx| {
- if window.focus == Some(handle_id) {
- window.blur(cx);
- }
- })
- .unwrap();
- }
- false
- } else {
- true
- }
- });
+ let focus_handles = self.focus_handles.clone();
+ let mut focus_handles = focus_handles.write();
+ let FocusMap {
+ handles,
+ dropped_handles,
+ } = &mut *focus_handles;
+ if dropped_handles.is_empty() {
+ return;
+ }
+
+ let windows = self
+ .windows
+ .values()
+ .filter_map(|window| {
+ let window = window.as_ref()?;
+ dropped_handles
+ .contains(&window.focus?)
+ .then_some(window.handle)
+ })
+ .collect::<SmallVec<[_; 1]>>();
+ for handle_id in dropped_handles.drain(..) {
+ handles.remove(handle_id);
+ }
+ drop(focus_handles);
+
+ for window_handle in windows {
+ window_handle
+ .update(self, |_, window, cx| window.blur(cx))
+ .ok();
+ }
}
fn apply_notify_effect(&mut self, emitter: EntityId) {
diff --git a/third-party/gpui-fast/crates/gpui/src/app/test_context.rs b/third-party/gpui-fast/crates/gpui/src/app/test_context.rs
index 23224edd78..7845737082 100644
--- a/third-party/gpui-fast/crates/gpui/src/app/test_context.rs
+++ b/third-party/gpui-fast/crates/gpui/src/app/test_context.rs
@@ -1403,6 +1403,74 @@ mod tests {
use std::path::PathBuf;
use std::rc::Rc;
+ #[gpui::test]
+ fn test_mobile_lifecycle_subscriptions(cx: &mut TestAppContext) {
+ use crate::AppLifecyclePhase;
+
+ let phases = Rc::new(RefCell::new(Vec::new()));
+ let subscription = cx.update(|cx| {
+ cx.on_app_lifecycle({
+ let phases = phases.clone();
+ move |phase, cx| {
+ phases.borrow_mut().push(phase);
+ cx.defer(|cx| {
+ cx.set_global(LifecycleObserved);
+ });
+ }
+ })
+ });
+ struct LifecycleObserved;
+ impl crate::Global for LifecycleObserved {}
+
+ let mut callback = cx
+ .test_platform
+ .app_lifecycle_callback
+ .borrow_mut()
+ .take()
+ .expect("App registers the native lifecycle callback");
+ for phase in [
+ AppLifecyclePhase::Foreground,
+ AppLifecyclePhase::Active,
+ AppLifecyclePhase::Inactive,
+ AppLifecyclePhase::Background,
+ AppLifecyclePhase::Disconnected,
+ ] {
+ callback(phase);
+ }
+ assert_eq!(phases.borrow().len(), 5);
+ assert_eq!(
+ phases.borrow().last(),
+ Some(&AppLifecyclePhase::Disconnected)
+ );
+ cx.update(|cx| assert!(cx.has_global::<LifecycleObserved>()));
+ drop(subscription);
+ callback(AppLifecyclePhase::Foreground);
+ assert_eq!(phases.borrow().len(), 5);
+ }
+
+ #[gpui::test]
+ fn test_memory_warning_subscriptions(cx: &mut TestAppContext) {
+ let warnings = Rc::new(std::cell::Cell::new(0));
+ let subscription = cx.update(|cx| {
+ cx.on_memory_warning({
+ let warnings = warnings.clone();
+ move |_| warnings.set(warnings.get() + 1)
+ })
+ });
+ let mut callback = cx
+ .test_platform
+ .memory_warning_callback
+ .borrow_mut()
+ .take()
+ .expect("App registers the native memory warning callback");
+ callback();
+ callback();
+ assert_eq!(warnings.get(), 2);
+ drop(subscription);
+ callback();
+ assert_eq!(warnings.get(), 2);
+ }
+
#[gpui::test]
async fn test_system_notifications_require_identity_and_replace_matching_tags(
cx: &mut TestAppContext,
diff --git a/third-party/gpui-fast/crates/gpui/src/elements/div.rs b/third-party/gpui-fast/crates/gpui/src/elements/div.rs
index d29e134d28..14ab003d6c 100644
--- a/third-party/gpui-fast/crates/gpui/src/elements/div.rs
+++ b/third-party/gpui-fast/crates/gpui/src/elements/div.rs
@@ -5957,6 +5957,96 @@ mod tests {
);
}
+ #[test]
+ fn touch_tap_out_fires_once_after_release() {
+ let mut test_app = TestAppContext::single();
+ let mouse_down_out_count = Rc::new(RefCell::new(0));
+ let window = test_app.add_window({
+ let mouse_down_out_count = mouse_down_out_count.clone();
+ move |_, _| MouseDownOutOwner {
+ mouse_down_out_count,
+ }
+ });
+ let any_window: AnyWindowHandle = window.into();
+
+ test_app
+ .update_window(any_window, |_, window, cx| {
+ window.draw(cx).clear(cx);
+ let started = TouchEvent {
+ id: TouchId(1),
+ phase: TouchPhase::Started,
+ position: point(px(75.), px(75.)),
+ force: None,
+ predicted_position: None,
+ };
+ window.dispatch_event(started.clone().to_platform_input(), cx);
+ assert_eq!(*mouse_down_out_count.borrow(), 0);
+ window.dispatch_event(
+ TouchEvent {
+ phase: TouchPhase::Ended,
+ ..started
+ }
+ .to_platform_input(),
+ cx,
+ );
+ })
+ .unwrap();
+
+ assert_eq!(
+ *mouse_down_out_count.borrow(),
+ 1,
+ "a touch tap outside the element should use mouse-down-out listeners"
+ );
+ }
+
+ #[crate::test]
+ fn touch_pan_and_cancel_do_not_fire_mouse_down_out(cx: &mut TestAppContext) {
+ let mouse_down_out_count = Rc::new(RefCell::new(0));
+ let window = cx.add_window({
+ let mouse_down_out_count = mouse_down_out_count.clone();
+ move |_, _| MouseDownOutOwner {
+ mouse_down_out_count,
+ }
+ });
+ cx.update_window(window.into(), |_, window, cx| {
+ window.draw(cx).clear(cx);
+ for (id, samples) in [
+ (
+ TouchId(1),
+ [
+ (TouchPhase::Started, 100.),
+ (TouchPhase::Moved, 75.),
+ (TouchPhase::Ended, 75.),
+ ],
+ ),
+ (
+ TouchId(2),
+ [
+ (TouchPhase::Started, 100.),
+ (TouchPhase::Moved, 100.),
+ (TouchPhase::Cancelled, 100.),
+ ],
+ ),
+ ] {
+ for (phase, vertical_position) in samples {
+ window.dispatch_event(
+ TouchEvent {
+ id,
+ phase,
+ position: point(px(75.), px(vertical_position)),
+ predicted_position: None,
+ force: None,
+ }
+ .to_platform_input(),
+ cx,
+ );
+ }
+ }
+ })
+ .unwrap();
+ assert_eq!(*mouse_down_out_count.borrow(), 0);
+ }
+
#[test]
fn test_accessibility_id_builder_writes_author_id() {
let mut element = div()
diff --git a/third-party/gpui-fast/crates/gpui/src/fast/composition.rs b/third-party/gpui-fast/crates/gpui/src/fast/composition.rs
index 2637196e63..3483fe5bbc 100644
--- a/third-party/gpui-fast/crates/gpui/src/fast/composition.rs
+++ b/third-party/gpui-fast/crates/gpui/src/fast/composition.rs
@@ -25,9 +25,20 @@
//! Unlike the upstream pull request, a view drawn again from last frame
//! (every retained view, here) carries the surface switches it made with it:
//! [`reuse_starts`] copies them along with its scene.
+//!
+//! Also unlike it, a native surface stacks with the element hosting it. One
+//! positioned while a deferred draw is prepainted, a web page in a dialog or
+//! sheet, is hosted on the overlay surface, so it moves above that surface
+//! instead of under the dialog's own background; positioned while the root
+//! view is prepainted, it moves back below it. Overlays drawn over a native
+//! surface hosted on the overlay surface are then covered by it.
use crate::window::PaintIndex;
-use crate::{Bounds, DevicePixels, PlatformWindow, Point, Scene, Window, scene::PaintOperation};
+use crate::{
+ Bounds, DevicePixels, DispatchEventResult, Hitbox, HitboxBehavior, HitboxId, MouseButton,
+ MouseExitEvent, MouseMoveEvent, Pixels, PlatformInput, PlatformWindow, Point, Scene, Window,
+ scene::PaintOperation,
+};
use anyhow::{Context as _, Result, anyhow, bail};
use collections::FxHashMap;
use gpui_util::ResultExt as _;
@@ -325,6 +336,11 @@ pub trait PlatformSurfaceAttachment {
}
/// Updates whether the native surface participates in composition.
fn set_visible(&self, visible: bool) -> Result<()>;
+ /// Whether GPUI forwards pointer input to a windowless consumer.
+ /// OS-managed child views keep their native event path by default.
+ fn input_forwarding(&self) -> bool {
+ false
+ }
/// Returns the platform attachment object: an `NSView` pointer as `usize`
/// on macOS, an `IDCompositionVisual` as `windows::core::IUnknown` on
/// Windows, a `raw_window_handle::RawWindowHandle` on Linux.
@@ -344,11 +360,30 @@ pub struct ComposedSceneLayer {
pub struct ComposedScene<'a> {
scene: &'a Scene,
layers: Vec<ComposedSceneLayer>,
+ input: &'a SurfaceInput,
}
impl<'a> ComposedScene<'a> {
- pub(crate) fn new(scene: &'a Scene, layers: Vec<ComposedSceneLayer>) -> Self {
- Self { scene, layers }
+ pub(crate) fn new(
+ scene: &'a Scene,
+ layers: Vec<ComposedSceneLayer>,
+ input: &'a SurfaceInput,
+ ) -> Self {
+ Self {
+ scene,
+ layers,
+ input,
+ }
+ }
+
+ /// The prepared, clipped input regions belonging to this GPUI surface.
+ /// Regions retain their stacking order and mouse/scroll behavior.
+ pub fn layer_hitboxes(&self, layer: &ComposedSceneLayer) -> &[Hitbox] {
+ self.input
+ .hitboxes
+ .get(&layer.surface)
+ .map(Vec::as_slice)
+ .unwrap_or(&[])
}
/// Returns the complete scene containing all GPUI planes.
@@ -435,8 +470,8 @@ impl Scene {
/// Returns whether the scene contains no drawable primitives.
///
/// A scene may have paint operations that only open and close empty layers,
- /// so `len() == 0` is not equivalent to having no visible/input-relevant
- /// overlay content.
+ /// so `len() == 0` is not equivalent to having no visible primitives.
+ /// Transparent input regions are prepared separately from this scene.
pub fn is_empty(&self) -> bool {
self.shadows.is_empty()
&& self.quads.is_empty()
@@ -465,6 +500,7 @@ impl Scene {
.get(range.clone())
.context("scene replay range ends past the scene")?;
let mut active_layers = Vec::new();
+ let mut active_clips = Vec::new();
for operation in prefix {
match operation {
PaintOperation::StartLayer(bounds) => active_layers.push(*bounds),
@@ -475,6 +511,9 @@ impl Scene {
);
}
PaintOperation::Primitive(_) => {}
+ PaintOperation::FastRoundedClip(clip) => {
+ crate::fast::rounded_clip::track(*clip, &mut active_clips)?
+ }
}
}
@@ -482,6 +521,12 @@ impl Scene {
for bounds in &active_layers {
self.push_layer(*bounds);
}
+ for shape in &active_clips {
+ crate::fast::rounded_clip::record(
+ self,
+ crate::fast::rounded_clip::ClipOperation::Begin(*shape),
+ );
+ }
for operation in operations {
match operation {
PaintOperation::Primitive(primitive) => self.insert_primitive(primitive.clone()),
@@ -496,11 +541,18 @@ impl Scene {
);
self.pop_layer();
}
+ PaintOperation::FastRoundedClip(clip) => {
+ crate::fast::rounded_clip::track(*clip, &mut active_clips)?;
+ crate::fast::rounded_clip::record(self, *clip);
+ }
}
}
for _ in active_layers {
self.pop_layer();
}
+ for _ in active_clips {
+ crate::fast::rounded_clip::record(self, crate::fast::rounded_clip::ClipOperation::End);
+ }
Ok(())
}
}
@@ -510,6 +562,144 @@ impl Scene {
#[derive(Default)]
pub(crate) struct SurfaceStarts(Vec<(CompositionSurfaceId, usize)>);
+/// Composition ownership accompanies existing hitbox identities through retained
+/// and scroll-layer replay; no second layout or input tree is built.
+#[derive(Default)]
+pub(crate) struct SurfaceInput {
+ order: Vec<(CompositionSurfaceId, Option<Bounds<Pixels>>)>,
+ scope: Option<CompositionSurfaceId>,
+ base: Option<CompositionSurfaceId>,
+ owners: FxHashMap<HitboxId, CompositionSurfaceId>,
+ hitboxes: FxHashMap<CompositionSurfaceId, Vec<Hitbox>>,
+}
+
+impl SurfaceInput {
+ pub(crate) fn clear(&mut self) {
+ self.scope = None;
+ self.base = None;
+ self.order.clear();
+ self.owners.clear();
+ for hitboxes in self.hitboxes.values_mut() {
+ hitboxes.clear();
+ }
+ }
+}
+
+fn begin_input(window: &mut Window, overlay: bool) {
+ let state = window.fast_composition.0.borrow();
+ state
+ .prepainting
+ .set(Some(if overlay { Host::Overlay } else { Host::Base }));
+ let input = &mut window.next_frame.fast_composition_input;
+ input.base = (!state.platform_surfaces.is_empty()).then_some(state.base_surface);
+ input.scope = state.enabled.then_some(if overlay {
+ state.overlay_surface
+ } else {
+ state.base_surface
+ });
+}
+
+pub(crate) fn input_surface(window: &Window) -> Option<CompositionSurfaceId> {
+ window.next_frame.fast_composition_input.scope
+}
+
+pub(crate) fn record_input(window: &mut Window, id: HitboxId) {
+ let input = &mut window.next_frame.fast_composition_input;
+ if let (Some(base), Some(surface)) = (input.base, input.scope) {
+ if surface != base {
+ input.owners.insert(id, surface);
+ }
+ }
+}
+
+pub(crate) fn finish_input(window: &mut Window) {
+ synchronize_geometry(window);
+ let scale = window.scale_factor();
+ let state = window.fast_composition.0.borrow();
+ let input = &mut window.next_frame.fast_composition_input;
+ if !state.enabled || state.platform_surfaces.is_empty() {
+ input.hitboxes.clear();
+ return;
+ }
+ input
+ .hitboxes
+ .retain(|surface, _| state.tree.kind(*surface).is_ok());
+ for surface in state.input_order.iter().copied() {
+ let native = state.platform_surfaces.get(&surface).map(|native| {
+ input_bounds(&state, surface, native.bounds().to_pixels(scale), scale)
+ .unwrap_or_default()
+ });
+ input.order.push((surface, native));
+ }
+ for hitbox in &window.next_frame.hitboxes {
+ let surface = input
+ .owners
+ .get(&hitbox.id)
+ .or_else(|| {
+ window
+ .rendered_frame
+ .fast_composition_input
+ .owners
+ .get(&hitbox.id)
+ })
+ .copied()
+ .unwrap_or(state.base_surface);
+ if !matches!(state.tree.kind(surface), Ok(CompositionSurfaceKind::Gpui)) {
+ continue;
+ }
+ if surface != state.base_surface {
+ input.owners.insert(hitbox.id, surface);
+ }
+ let bounds = hitbox.bounds.intersect(&hitbox.content_mask.bounds);
+ let Some(bounds) = input_bounds(&state, surface, bounds, scale) else {
+ continue;
+ };
+ if !bounds.is_empty() {
+ let mut clipped = hitbox.clone();
+ clipped.bounds = bounds;
+ input.hitboxes.entry(surface).or_default().push(clipped);
+ }
+ }
+}
+
+/// Native content blocks GPUI hover and click targets behind it, while keeping
+/// the existing GPUI scroll blockers and capture dispatch unchanged.
+pub(crate) fn hit_test(
+ frame: &crate::window::Frame,
+ point: Point<Pixels>,
+) -> Option<crate::window::HitTest> {
+ let input = &frame.fast_composition_input;
+ if input.order.is_empty() {
+ return None;
+ }
+ let mut result = crate::window::HitTest::default();
+ let mut hover = None;
+ 'surfaces: for (surface, native) in input.order.iter().rev() {
+ if let Some(bounds) = native {
+ if bounds.contains(&point) {
+ break;
+ }
+ continue;
+ }
+ let Some(hitboxes) = input.hitboxes.get(surface) else {
+ continue;
+ };
+ for hitbox in hitboxes.iter().rev() {
+ if hitbox.bounds.contains(&point) {
+ result.ids.push(hitbox.id);
+ if hover.is_none() && hitbox.behavior == HitboxBehavior::BlockMouseExceptScroll {
+ hover = Some(result.ids.len());
+ }
+ if hitbox.behavior == HitboxBehavior::BlockMouse {
+ break 'surfaces;
+ }
+ }
+ }
+ }
+ result.hover_hitbox_count = hover.unwrap_or(result.ids.len());
+ Some(result)
+}
+
impl SurfaceStarts {
pub(crate) fn clear(&mut self) {
self.0.clear();
@@ -527,17 +717,36 @@ impl SurfaceStarts {
}
}
+/// The GPUI surface an element hosting a native surface is drawn on.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+enum Host {
+ /// The root view, drawn on the base surface.
+ Base,
+ /// A deferred or window-level draw, drawn on the overlay surface.
+ Overlay,
+}
+
/// A window's composition: its surface tree, the platform surfaces in it and
/// whether the window presents through it.
pub(crate) struct WindowCompositionState {
tree: CompositionTree,
- platform_surfaces: FxHashMap<CompositionSurfaceId, Rc<dyn PlatformSurfaceAttachment>>,
+ platform_surfaces: FxHashMap<CompositionSurfaceId, Rc<ManagedPlatformSurface>>,
base_surface: CompositionSurfaceId,
overlay_surface: CompositionSurfaceId,
geometry_dirty: Rc<Cell<bool>>,
+ /// What the window is prepainting, while it prepaints a frame.
+ prepainting: Rc<Cell<Option<Host>>>,
enabled: bool,
+ input_order: Vec<CompositionSurfaceId>,
+ visible: FxHashMap<CompositionSurfaceId, Rc<Cell<bool>>>,
+ input_handlers: FxHashMap<CompositionSurfaceId, Rc<RefCell<NativeInputHandler>>>,
+ pointer_capture: Option<CompositionSurfaceId>,
+ pressed_buttons: Vec<MouseButton>,
+ hovered_native: Option<CompositionSurfaceId>,
}
+type NativeInputHandler = Box<dyn FnMut(&PlatformInput) -> DispatchEventResult>;
+
/// The [`Window`] field holding its composition.
pub(crate) struct WindowCompositionHandle(Rc<RefCell<WindowCompositionState>>);
@@ -556,7 +765,14 @@ impl Default for WindowCompositionHandle {
base_surface,
overlay_surface,
geometry_dirty: Rc::new(Cell::new(false)),
+ prepainting: Rc::new(Cell::new(None)),
enabled: false,
+ input_order: Vec::new(),
+ visible: FxHashMap::default(),
+ input_handlers: FxHashMap::default(),
+ pointer_capture: None,
+ pressed_buttons: Vec::new(),
+ hovered_native: None,
})))
}
}
@@ -567,10 +783,41 @@ struct ManagedPlatformSurface {
platform_surface: Rc<dyn PlatformSurfaceAttachment>,
bounds: Cell<Bounds<DevicePixels>>,
geometry_dirty: Rc<Cell<bool>>,
+ visible: Rc<Cell<bool>>,
+ input_forwarding: Cell<bool>,
+ prepainting: Rc<Cell<Option<Host>>>,
+ /// The surface its host was last prepainted on.
+ host: Cell<Host>,
+ /// Whether `host` changed since the surface was last restacked.
+ restack: Cell<bool>,
+}
+
+impl ManagedPlatformSurface {
+ fn new(
+ platform_surface: Rc<dyn PlatformSurfaceAttachment>,
+ state: &WindowCompositionState,
+ ) -> Self {
+ Self {
+ bounds: Cell::new(platform_surface.bounds()),
+ platform_surface,
+ geometry_dirty: state.geometry_dirty.clone(),
+ visible: Rc::new(Cell::new(true)),
+ input_forwarding: Cell::new(false),
+ prepainting: state.prepainting.clone(),
+ host: Cell::new(Host::Base),
+ restack: Cell::new(false),
+ }
+ }
}
impl PlatformSurfaceAttachment for ManagedPlatformSurface {
fn set_bounds(&self, bounds: Bounds<DevicePixels>) -> Result<()> {
+ if let Some(host) = self.prepainting.get()
+ && self.host.replace(host) != host
+ {
+ self.restack.set(true);
+ self.geometry_dirty.set(true);
+ }
if self.bounds.get() == bounds {
return Ok(());
}
@@ -601,7 +848,17 @@ impl PlatformSurfaceAttachment for ManagedPlatformSurface {
}
fn set_visible(&self, visible: bool) -> Result<()> {
- self.platform_surface.set_visible(visible)
+ if self.visible.get() == visible {
+ return Ok(());
+ }
+ self.platform_surface.set_visible(visible)?;
+ self.visible.set(visible);
+ self.geometry_dirty.set(true);
+ Ok(())
+ }
+
+ fn input_forwarding(&self) -> bool {
+ self.input_forwarding.get() && self.visible.get()
}
fn platform_handle(&self) -> Result<Box<dyn Any>> {
@@ -640,6 +897,42 @@ pub struct WindowComposition<'a> {
}
impl WindowComposition<'_> {
+ /// Registers a cooperating windowless producer's mouse/wheel consumer.
+ /// Capture runs first; coordinates are in window-content logical pixels.
+ /// OS-managed child views need no handler and keep their platform input path.
+ pub fn on_input(
+ &self,
+ surface: CompositionSurfaceId,
+ handler: impl FnMut(&PlatformInput) -> DispatchEventResult + 'static,
+ ) -> Result<()> {
+ let previous = {
+ let mut state = self.state.borrow_mut();
+ anyhow::ensure!(
+ state.platform_surfaces.contains_key(&surface),
+ "input consumer requires a native surface"
+ );
+ state.platform_surfaces[&surface].input_forwarding.set(true);
+ state
+ .input_handlers
+ .insert(surface, Rc::new(RefCell::new(Box::new(handler))))
+ };
+ if let Err(error) = self.synchronize_platform_order() {
+ let mut state = self.state.borrow_mut();
+ state.platform_surfaces[&surface]
+ .input_forwarding
+ .set(previous.is_some());
+ if let Some(previous) = previous {
+ state.input_handlers.insert(surface, previous);
+ } else {
+ state.input_handlers.remove(&surface);
+ }
+ drop(state);
+ self.synchronize_platform_order().log_err();
+ return Err(error);
+ }
+ Ok(())
+ }
+
/// Returns the GPUI surface initially rendered below native content.
pub fn base_surface(&self) -> CompositionSurfaceId {
self.state.borrow().base_surface
@@ -737,6 +1030,10 @@ impl WindowComposition<'_> {
}
let previous_tree = state.tree.clone();
let platform_surface = state.platform_surfaces.get(&surface).cloned();
+ let was_visible = state
+ .visible
+ .get(&surface)
+ .is_none_or(|visible| visible.get());
if let Some(platform_surface) = &platform_surface {
platform_surface.set_visible(false)?;
}
@@ -747,13 +1044,23 @@ impl WindowComposition<'_> {
let mut state = self.state.borrow_mut();
state.tree = previous_tree;
if let Some(platform_surface) = platform_surface {
- platform_surface.set_visible(true).log_err();
+ platform_surface.set_visible(was_visible).log_err();
state.platform_surfaces.insert(surface, platform_surface);
}
drop(state);
self.synchronize_platform_order().log_err();
return Err(error);
}
+ let mut state = self.state.borrow_mut();
+ state.visible.remove(&surface);
+ state.input_handlers.remove(&surface);
+ if state.pointer_capture == Some(surface) {
+ state.pointer_capture = None;
+ state.pressed_buttons.clear();
+ }
+ if state.hovered_native == Some(surface) {
+ state.hovered_native = None;
+ }
Ok(())
}
@@ -793,23 +1100,23 @@ impl WindowComposition<'_> {
kind: CompositionSurfaceKind,
platform_surface: Rc<dyn PlatformSurfaceAttachment>,
) -> Result<WindowCompositionSurface> {
- let initial_bounds = platform_surface.bounds();
- let geometry_dirty = self.state.borrow().geometry_dirty.clone();
- let platform_surface: Rc<dyn PlatformSurfaceAttachment> = Rc::new(ManagedPlatformSurface {
+ let platform_surface = Rc::new(ManagedPlatformSurface::new(
platform_surface,
- bounds: Cell::new(initial_bounds),
- geometry_dirty,
- });
+ &self.state.borrow(),
+ ));
+ let visible = platform_surface.visible.clone();
let mut state = self.state.borrow_mut();
let id = state.tree.insert(kind, None)?;
let overlay_surface = state.overlay_surface;
state.tree.place_below(id, overlay_surface)?;
state.platform_surfaces.insert(id, platform_surface.clone());
+ state.visible.insert(id, visible);
drop(state);
if let Err(error) = self.synchronize_platform_order() {
platform_surface.set_visible(false).log_err();
let mut state = self.state.borrow_mut();
state.platform_surfaces.remove(&id);
+ state.visible.remove(&id);
state.tree.remove(id)?;
drop(state);
self.synchronize_platform_order().log_err();
@@ -867,6 +1174,8 @@ impl WindowComposition<'_> {
}
self.platform_window.set_composition_order(&surfaces)?;
state.geometry_dirty.set(false);
+ drop(state);
+ self.state.borrow_mut().input_order = surfaces.iter().map(|surface| surface.id).collect();
Ok(())
}
}
@@ -897,8 +1206,9 @@ impl Window {
Ok(composition)
}
- /// Paints a scope of scene operations onto a GPUI surface in this window's
- /// composition tree, then restores the previous surface.
+ /// Prepaints or paints an element scope on a GPUI composition surface,
+ /// then restores the previous surface. Wrap both phases when selecting a
+ /// surface explicitly so its input regions follow its painted content.
///
/// Inside a scroll layer, whose content is composited from tiles, the
/// scope paints where the layer does.
@@ -907,7 +1217,7 @@ impl Window {
surface: CompositionSurfaceId,
paint: impl FnOnce(&mut Self) -> R,
) -> Result<R> {
- self.invalidator.debug_assert_paint();
+ self.invalidator.debug_assert_paint_or_prepaint();
anyhow::ensure!(
self.fast_composition.0.borrow().tree.kind(surface)? == CompositionSurfaceKind::Gpui,
"paint operations can only target GPUI composition surfaces"
@@ -915,6 +1225,16 @@ impl Window {
if self.fast_layers.painting.is_some() {
return Ok(paint(self));
}
+ if self.invalidator.inner.borrow().draw_phase == crate::window::DrawPhase::Prepaint {
+ let previous = self
+ .next_frame
+ .fast_composition_input
+ .scope
+ .replace(surface);
+ let result = paint(self);
+ self.next_frame.fast_composition_input.scope = previous;
+ return Ok(result);
+ }
let previous_surface = self
.next_frame
.fast_composition_starts
@@ -935,12 +1255,43 @@ fn switch_surface(window: &mut Window, surface: CompositionSurfaceId) {
.switch_to(surface, scene_len);
}
+/// Stacks what the root view positions, as it prepaints, below the overlay
+/// surface.
+pub(crate) fn begin_base_prepaint(window: &mut Window) {
+ begin_input(window, false);
+}
+
+/// Stacks what deferred and window-level draws position, as they prepaint,
+/// above the overlay surface.
+pub(crate) fn begin_overlay_prepaint(window: &mut Window) {
+ begin_input(window, true);
+}
+
/// Paints what follows, the root view, onto the base surface.
pub(crate) fn begin_base(window: &mut Window) {
- let surface = window.fast_composition.0.borrow().base_surface;
+ let state = window.fast_composition.0.borrow();
+ state.prepainting.set(None);
+ let surface = state.base_surface;
+ drop(state);
switch_surface(window, surface);
}
+/// Moves each root native surface whose host changed surface to that
+/// surface's side of the overlay surface.
+fn restack_native_surfaces(state: &mut WindowCompositionState) -> Result<()> {
+ let overlay = state.overlay_surface;
+ for (id, surface) in &state.platform_surfaces {
+ if !surface.restack.replace(false) || state.tree.parent(*id)?.is_some() {
+ continue;
+ }
+ match surface.host.get() {
+ Host::Base => state.tree.place_below(*id, overlay)?,
+ Host::Overlay => state.tree.place_above(*id, overlay)?,
+ }
+ }
+ Ok(())
+}
+
/// Paints what follows, deferred and window-level draws, onto the overlay
/// surface.
pub(crate) fn begin_overlay(window: &mut Window) {
@@ -977,23 +1328,35 @@ pub(crate) fn reuse_starts(window: &mut Window, range: &Range<PaintIndex>) {
.switch_to(surface, end);
}
-/// Presents the frame last drawn: on its one surface, or split along the
-/// composition tree once the window composes.
-pub(crate) fn present(window: &mut Window) {
+// Host changes must reach input ownership before the frame's hit test.
+fn synchronize_geometry(window: &mut Window) {
let state = window.fast_composition.0.clone();
if !state.borrow().enabled {
- window.platform_window.draw(&window.rendered_frame.scene);
return;
}
if state.borrow().geometry_dirty.get() {
+ if let Err(error) = restack_native_surfaces(&mut state.borrow_mut()) {
+ log::error!("restacking native composition surfaces: {error:#}");
+ }
let composition = WindowComposition {
platform_window: window.platform_window.as_ref(),
- state: state.clone(),
+ state,
};
if let Err(error) = composition.synchronize_platform_order() {
log::error!("updating window composition geometry: {error:#}");
}
}
+}
+
+/// Presents the frame last drawn: on its one surface, or split along the
+/// composition tree once the window composes.
+pub(crate) fn present(window: &mut Window) {
+ let state = window.fast_composition.0.clone();
+ if !state.borrow().enabled {
+ window.platform_window.draw(&window.rendered_frame.scene);
+ return;
+ }
+ synchronize_geometry(window);
let state = state.borrow();
let scene = &window.rendered_frame.scene;
let layers = composed_scene_layers(
@@ -1009,21 +1372,252 @@ pub(crate) fn present(window: &mut Window) {
}]
});
drop(state);
- window
- .platform_window
- .draw_composed(ComposedScene::new(scene, layers));
+ window.platform_window.draw_composed(ComposedScene::new(
+ scene,
+ layers,
+ &window.rendered_frame.fast_composition_input,
+ ));
+}
+
+/// Visibility and ancestor clips come from the existing managed surface tree.
+fn input_bounds(
+ state: &WindowCompositionState,
+ surface: CompositionSurfaceId,
+ mut bounds: Bounds<Pixels>,
+ scale: f32,
+) -> Option<Bounds<Pixels>> {
+ let mut current = Some(surface);
+ while let Some(id) = current {
+ state.tree.kind(id).ok()?;
+ if state.visible.get(&id).is_some_and(|visible| !visible.get()) {
+ return None;
+ }
+ if let Some(native) = state.platform_surfaces.get(&id) {
+ bounds = bounds.intersect(&native.bounds().to_pixels(scale));
+ }
+ current = state.tree.parent(id).ok()?;
+ }
+ (!bounds.is_empty()).then_some(bounds)
+}
+
+fn visible_surface(state: &WindowCompositionState, surface: CompositionSurfaceId) -> bool {
+ let mut current = Some(surface);
+ while let Some(id) = current {
+ if state.tree.kind(id).is_err() || state.visible.get(&id).is_some_and(|v| !v.get()) {
+ return false;
+ }
+ current = state.tree.parent(id).ok().flatten();
+ }
+ true
+}
+
+fn native_target(window: &Window, scroll: bool) -> Option<CompositionSurfaceId> {
+ let state = window.fast_composition.0.borrow();
+ if !state.enabled || state.platform_surfaces.is_empty() {
+ return None;
+ }
+ if !scroll {
+ if let Some(captured) = state
+ .pointer_capture
+ .filter(|id| visible_surface(&state, *id))
+ {
+ return Some(captured);
+ }
+ }
+ let point = window.mouse_position;
+ for surface in state.input_order.iter().rev().copied() {
+ if !visible_surface(&state, surface) {
+ continue;
+ }
+ if let Some(native) = state.platform_surfaces.get(&surface) {
+ if input_bounds(
+ &state,
+ surface,
+ native.bounds().to_pixels(window.scale_factor()),
+ window.scale_factor(),
+ )
+ .is_some_and(|bounds| bounds.contains(&point))
+ {
+ return Some(surface);
+ }
+ } else if let Some(regions) = window
+ .rendered_frame
+ .fast_composition_input
+ .hitboxes
+ .get(&surface)
+ {
+ if regions.iter().rev().any(|hitbox| {
+ hitbox.bounds.contains(&point)
+ && (!scroll || hitbox.behavior == HitboxBehavior::BlockMouse)
+ }) {
+ return None;
+ }
+ }
+ }
+ None
+}
+
+/// OS-directed native events still run GPUI capture, but cannot activate
+/// unrelated GPUI widgets underneath the native view.
+fn native_mouse(window: &Window, event: &dyn Any) -> bool {
+ if window.captured_hitbox.is_some() {
+ let state = window.fast_composition.0.borrow();
+ return state
+ .pointer_capture
+ .is_some_and(|id| visible_surface(&state, id));
+ }
+ if event
+ .downcast_ref::<MouseMoveEvent>()
+ .is_some_and(|event| event.pressed_button.is_some())
+ && window.fast_composition.0.borrow().pointer_capture.is_none()
+ {
+ return false;
+ }
+ native_target(window, false).is_some()
+}
+
+pub(crate) fn bubble_mouse(window: &Window, event: &dyn Any, cx: &crate::App) -> bool {
+ cx.propagate_event && (cx.has_active_drag() || !native_mouse(window, event))
+}
+
+pub(crate) fn event_target(window: &Window, event: &PlatformInput) -> Option<CompositionSurfaceId> {
+ if !matches!(
+ event,
+ PlatformInput::MouseDown(_)
+ | PlatformInput::MouseUp(_)
+ | PlatformInput::MouseMove(_)
+ | PlatformInput::ScrollWheel(_)
+ ) {
+ return None;
+ }
+ let scroll = matches!(event, PlatformInput::ScrollWheel(_));
+ if !scroll {
+ let state = window.fast_composition.0.borrow();
+ if let Some(captured) = state.pointer_capture {
+ return visible_surface(&state, captured).then_some(captured);
+ }
+ }
+ if window.captured_hitbox.is_some() || matches!(event, PlatformInput::MouseUp(_)) {
+ return None;
+ }
+ if matches!(
+ event,
+ PlatformInput::MouseMove(MouseMoveEvent {
+ pressed_button: Some(_),
+ ..
+ })
+ ) && window.fast_composition.0.borrow().pointer_capture.is_none()
+ {
+ return None;
+ }
+ native_target(window, scroll)
+}
+
+/// Called after the existing dispatcher; windowless producers get one delivery,
+/// while OS-managed views continue through the platform's original event path.
+pub(crate) fn dispatch_native(
+ window: &mut Window,
+ event: &PlatformInput,
+ target: Option<CompositionSurfaceId>,
+ propagate: &mut bool,
+) {
+ let pointer = matches!(
+ event,
+ PlatformInput::MouseDown(_)
+ | PlatformInput::MouseUp(_)
+ | PlatformInput::MouseMove(_)
+ | PlatformInput::MouseExited(_)
+ | PlatformInput::ScrollWheel(_)
+ );
+ if !pointer || !window.fast_composition.0.borrow().enabled {
+ return;
+ }
+ let target = target.filter(|id| visible_surface(&window.fast_composition.0.borrow(), *id));
+ let (handler, leave) = {
+ let mut state = window.fast_composition.0.borrow_mut();
+ let leave = if matches!(
+ event,
+ PlatformInput::MouseMove(_) | PlatformInput::MouseExited(_)
+ ) {
+ let current = if matches!(event, PlatformInput::MouseExited(_)) {
+ None
+ } else {
+ target
+ };
+ let previous = std::mem::replace(&mut state.hovered_native, current);
+ previous
+ .filter(|old| Some(*old) != current)
+ .and_then(|id| state.input_handlers.get(&id).cloned())
+ } else {
+ None
+ };
+ if *propagate && !window.default_prevented {
+ if let PlatformInput::MouseDown(down) = event {
+ if let Some(id) = target {
+ state.pointer_capture = Some(id);
+ if !state.pressed_buttons.contains(&down.button) {
+ state.pressed_buttons.push(down.button);
+ }
+ }
+ }
+ }
+ let owned_release = matches!(event, PlatformInput::MouseUp(_))
+ && target.is_some()
+ && state.pointer_capture == target;
+ let handler = if (*propagate && !window.default_prevented) || owned_release {
+ target.and_then(|id| state.input_handlers.get(&id).cloned())
+ } else {
+ None
+ };
+ if let PlatformInput::MouseUp(up) = event {
+ // A single press ends even if platform normalization changed its button
+ // (AppKit ctrl-click with Control released before mouse-up).
+ if state.pressed_buttons.len() == 1 {
+ state.pressed_buttons.clear();
+ } else {
+ state.pressed_buttons.retain(|button| *button != up.button);
+ }
+ if state.pressed_buttons.is_empty() {
+ state.pointer_capture = None;
+ }
+ }
+ (handler, leave)
+ };
+ if let Some(leave) = leave {
+ let (position, pressed_button, modifiers) = match event {
+ PlatformInput::MouseMove(event) => {
+ (event.position, event.pressed_button, event.modifiers)
+ }
+ PlatformInput::MouseExited(event) => {
+ (event.position, event.pressed_button, event.modifiers)
+ }
+ _ => unreachable!(),
+ };
+ leave.borrow_mut()(&PlatformInput::MouseExited(MouseExitEvent {
+ position,
+ pressed_button,
+ modifiers,
+ }));
+ }
+ if let Some(handler) = handler {
+ let result = handler.borrow_mut()(event);
+ *propagate &= result.propagate;
+ window.default_prevented |= result.default_prevented;
+ }
}
#[cfg(test)]
mod tests {
use super::{
- CompositionSurfaceId, CompositionSurfaceKind, CompositionTree, ManagedPlatformSurface,
- PlatformSurfaceAttachment, composed_scene_layers,
+ CompositionSurfaceId, CompositionSurfaceKind, CompositionTree, Host,
+ ManagedPlatformSurface, PlatformSurfaceAttachment, composed_scene_layers,
+ restack_native_surfaces,
};
use crate::{
AppContext as _, Bounds, ContentMask, Context, DevicePixels, Entity, IntoElement,
ParentElement as _, Point, Quad, Render, ScaledPixels, Scene, Size, StyleRefinement,
- Styled as _, TestAppContext, Window, canvas, div, fill, px, scene::PaintOperation, size,
+ Styled as _, TestAppContext, Window, canvas, deferred, div, fill, px,
+ scene::PaintOperation, size,
};
use anyhow::Result;
use std::{any::Any, cell::Cell, rc::Rc};
@@ -1164,6 +1758,11 @@ mod tests {
platform_surface: platform_surface.clone(),
bounds: Cell::new(Bounds::default()),
geometry_dirty: geometry_dirty.clone(),
+ visible: Rc::new(Cell::new(true)),
+ input_forwarding: Cell::new(false),
+ prepainting: Rc::new(Cell::new(None)),
+ host: Cell::new(Host::Base),
+ restack: Cell::new(false),
};
let bounds = Bounds {
origin: Point {
@@ -1200,6 +1799,11 @@ mod tests {
platform_surface: platform.clone(),
bounds: Cell::new(Bounds::default()),
geometry_dirty: dirty.clone(),
+ visible: Rc::new(Cell::new(true)),
+ input_forwarding: Cell::new(false),
+ prepainting: Rc::new(Cell::new(None)),
+ host: Cell::new(Host::Base),
+ restack: Cell::new(false),
};
surface.set_bounds(bounds)?;
dirty.set(false);
@@ -1239,6 +1843,11 @@ mod tests {
platform_surface: platform.clone(),
bounds: Cell::new(Bounds::default()),
geometry_dirty: dirty.clone(),
+ visible: Rc::new(Cell::new(true)),
+ input_forwarding: Cell::new(false),
+ prepainting: Rc::new(Cell::new(None)),
+ host: Cell::new(Host::Base),
+ restack: Cell::new(false),
};
assert!(surface.set_bounds(bounds).is_err());
assert_eq!(surface.bounds(), Bounds::default());
@@ -1447,4 +2056,107 @@ mod tests {
assert_eq!(builds.get(), 1, "the badge is drawn again from last frame");
assert_eq!(second, first);
}
+
+ fn test_platform_surface() -> Rc<TestPlatformSurface> {
+ Rc::new(TestPlatformSurface {
+ bounds_writes: Cell::new(0),
+ fail_next_bounds: Cell::new(false),
+ bounds: Cell::new(Bounds::default()),
+ parent_origin: Cell::new(Point::default()),
+ })
+ }
+
+ /// Positions one native surface from the root view and another from a
+ /// deferred draw, or both from the root view.
+ struct Hosts {
+ page: Rc<ManagedPlatformSurface>,
+ dialog_page: Rc<ManagedPlatformSurface>,
+ dialog_open: bool,
+ }
+
+ fn native_host(surface: Rc<ManagedPlatformSurface>) -> impl IntoElement {
+ canvas(
+ move |bounds, window, _| {
+ surface
+ .set_bounds(bounds.to_device_pixels(window.scale_factor()))
+ .unwrap()
+ },
+ |_, _, _, _| {},
+ )
+ .size(px(50.))
+ }
+
+ impl Render for Hosts {
+ fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+ let dialog_page = native_host(self.dialog_page.clone());
+ let root = div().size_full().child(native_host(self.page.clone()));
+ if self.dialog_open {
+ root.child(deferred(
+ div().size(px(80.)).bg(crate::white()).child(dialog_page),
+ ))
+ } else {
+ root.child(dialog_page)
+ }
+ }
+ }
+
+ /// A native surface hosted in a deferred draw, a web page in a dialog,
+ /// stacks above the overlay surface the dialog's background is drawn on,
+ /// and returns below it once hosted by the root view again.
+ #[crate::test]
+ fn native_surfaces_stack_with_their_hosts(cx: &mut TestAppContext) {
+ let names = Rc::new(std::cell::RefCell::new(Vec::new()));
+ let window = cx.add_window({
+ let names = names.clone();
+ move |window, _| {
+ let mut state = window.fast_composition.0.borrow_mut();
+ let overlay = state.overlay_surface;
+ let mut names = names.borrow_mut();
+ names.push((state.base_surface, "base"));
+ names.push((overlay, "overlay"));
+ let mut native = |name| {
+ let id = state
+ .tree
+ .insert(CompositionSurfaceKind::Native, None)
+ .unwrap();
+ state.tree.place_below(id, overlay).unwrap();
+ let surface =
+ Rc::new(ManagedPlatformSurface::new(test_platform_surface(), &state));
+ state.platform_surfaces.insert(id, surface.clone());
+ names.push((id, name));
+ surface
+ };
+ Hosts {
+ page: native("page"),
+ dialog_page: native("dialog page"),
+ dialog_open: true,
+ }
+ }
+ });
+ let draw = |cx: &mut TestAppContext| {
+ cx.update_window(window.into(), |_, window, cx| {
+ window.draw(cx).clear(cx);
+ let mut state = window.fast_composition.0.borrow_mut();
+ restack_native_surfaces(&mut state).unwrap();
+ let names = names.borrow();
+ state
+ .tree
+ .flattened()
+ .into_iter()
+ .map(|id| names.iter().find(|(named, _)| *named == id).unwrap().1)
+ .collect::<Vec<_>>()
+ })
+ .unwrap()
+ };
+
+ assert_eq!(draw(cx), ["base", "page", "overlay", "dialog page"]);
+
+ window
+ .update(cx, |hosts, _, cx| {
+ hosts.dialog_open = false;
+ cx.notify();
+ })
+ .unwrap();
+ assert_eq!(draw(cx), ["base", "page", "dialog page", "overlay"]);
+ }
}
diff --git a/third-party/gpui-fast/crates/gpui/src/fast/layers/hover_freeze.rs b/third-party/gpui-fast/crates/gpui/src/fast/layers/hover_freeze.rs
index 193cbe0826..dc5f379ecb 100644
--- a/third-party/gpui-fast/crates/gpui/src/fast/layers/hover_freeze.rs
+++ b/third-party/gpui-fast/crates/gpui/src/fast/layers/hover_freeze.rs
@@ -8,14 +8,19 @@
//! costs without layers.
//!
//! So for [`HOVER_FREEZE`] after a wheel event, while the pointer stays where
-//! the event was, nothing is hovered: the frame's hit test, and the one a
-//! list foretells for its rows, are empty. When the wheel stops the window is
-//! drawn again, and hover follows the pointer as before. A pointer that moves
-//! ends the freeze at once.
+//! the event was, hover styling is held still: while a frame is drawn,
+//! [`crate::HitboxId::is_hovered`] is false, and the hit test a list foretells
+//! for its rows is empty. Only drawing is held. The frame's hit test stays the
+//! geometry under the pointer, so the cursor follows it, and events dispatched
+//! between frames read it: a click at the spot the wheel scrolled under
+//! reaches its handler. When the wheel stops the window is drawn again, and
+//! hover follows the pointer as before. A pointer that moves ends the freeze
+//! at once.
-use std::time::{Duration, Instant};
+use scheduler::Instant;
+use std::time::Duration;
-use crate::{App, HitTest, Pixels, Point, Window};
+use crate::{App, Pixels, Point, Window};
/// How long after the last wheel event hover stays held still.
pub(crate) const HOVER_FREEZE: Duration = Duration::from_millis(100);
@@ -72,22 +77,16 @@ pub(crate) fn frozen(window: &Window) -> bool {
})
}
-/// Empties the last frame's hit test as a frame begins to be drawn while
-/// hover is held still, so that nothing rendered counts as hovered.
-pub(crate) fn begin_draw(window: &mut Window) {
- if frozen(window) {
- window.mouse_hit_test = HitTest::default();
- }
-}
-
-/// The hit test of the frame being drawn, taken between its prepaint and its
-/// paint: empty while hover is held still.
-pub(crate) fn hit_test(window: &Window) -> HitTest {
- if frozen(window) {
- HitTest::default()
- } else {
- window.next_frame.hit_test(window.mouse_position)
- }
+/// Whether hover reads as nothing for a hitbox the pointer has not captured,
+/// for [`crate::HitboxId::is_hovered`] and what a retained subtree records of
+/// it: after keyboard input, as upstream has it, and while a frame is drawn
+/// with hover held still.
+///
+/// Event dispatch is not drawing. A mouse listener asks `is_hovered` before it
+/// runs, so holding hover there too would drop a click made under a still
+/// pointer right after the wheel.
+pub(crate) fn hides_hover(window: &Window) -> bool {
+ window.last_input_was_keyboard() || (!window.invalidator.not_drawing() && frozen(window))
}
impl Window {
diff --git a/third-party/gpui-fast/crates/gpui/src/fast/layers/input.rs b/third-party/gpui-fast/crates/gpui/src/fast/layers/input.rs
index 000c4d5b7a..56794b2c1c 100644
--- a/third-party/gpui-fast/crates/gpui/src/fast/layers/input.rs
+++ b/third-party/gpui-fast/crates/gpui/src/fast/layers/input.rs
@@ -38,6 +38,10 @@ pub(crate) struct LayerInput {
/// painted: how far behind the positions its closures and element
/// states hold are.
pub(crate) stale: Point<Pixels>,
+ /// How far the content shown scrolled since last frame, on a frame that
+ /// composites it: the rendered frame holds the content's debug bounds
+ /// where it showed then.
+ pub(crate) scrolled: Point<Pixels>,
/// Whether the content is to be painted again at the offset shown the
/// next time the container is prepainted, though nothing it is built
/// from changed: it is about to see input (see [`before_dispatch`]).
diff --git a/third-party/gpui-fast/crates/gpui/src/fast/layers/lists.rs b/third-party/gpui-fast/crates/gpui/src/fast/layers/lists.rs
index 78802c8374..75045f41f8 100644
--- a/third-party/gpui-fast/crates/gpui/src/fast/layers/lists.rs
+++ b/third-party/gpui-fast/crates/gpui/src/fast/layers/lists.rs
@@ -1214,14 +1214,24 @@ fn carry_paint_rows(window: &mut Window, id: &GlobalElementId) {
let carried = frame.carried.clone();
let leaving = frame.leaving.clone();
let (translation, viewport) = (frame.translation, frame.viewport);
+ let last_translation = layer.rows.last_translation;
let mut rows = mem::take(&mut layer.rows.rows);
for ix in &carried {
let Some(row) = rows.get_mut(ix) else {
continue;
};
let delta = row.delta(translation, scale_factor);
+ // The rendered frame holds the row's debug bounds where it showed.
+ let scrolled = last_translation.map_or(delta, |last| delta - row.delta(last, scale_factor));
let element_states = !row.dropped && !leaving.contains(ix);
- row.paint = reuse::carry_paint_records(window, &row.paint, delta, viewport, element_states);
+ row.paint = reuse::carry_paint_records(
+ window,
+ &row.paint,
+ delta,
+ scrolled,
+ viewport,
+ element_states,
+ );
}
if let Some(layer) = window.fast_layers.layers.get_mut(id) {
layer.rows.rows = rows;
@@ -1630,6 +1640,9 @@ fn drawn_alike(a: &Row, b: &Row) -> bool {
size: bounds.size,
}),
PaintOperation::EndLayer => PaintOperation::EndLayer,
+ PaintOperation::FastRoundedClip(clip) => {
+ PaintOperation::FastRoundedClip(clip.translated(to_row))
+ }
})
.collect();
let (tiles, _) = part_tile_hashes(&operations, paint::TILE_SIZE);
@@ -2834,6 +2847,7 @@ pub(crate) fn end_paint_rows(window: &mut Window, cx: &mut App, id: Option<&Glob
PaintOperation::Primitive(primitive) => move_primitive(primitive, to_content),
PaintOperation::StartLayer(bounds) => bounds.origin = bounds.origin + to_content,
PaintOperation::EndLayer => {}
+ PaintOperation::FastRoundedClip(clip) => *clip = clip.translated(to_content),
}
}
let (row_operations, overlay, whole) =
diff --git a/third-party/gpui-fast/crates/gpui/src/fast/layers/overlay.rs b/third-party/gpui-fast/crates/gpui/src/fast/layers/overlay.rs
index 031166eca9..5cfda71c8c 100644
--- a/third-party/gpui-fast/crates/gpui/src/fast/layers/overlay.rs
+++ b/third-party/gpui-fast/crates/gpui/src/fast/layers/overlay.rs
@@ -73,6 +73,14 @@ pub(crate) fn split(
if !painted_path {
return None;
}
+ // A lifted primitive cannot carry scope markers. Keep scoped paths in
+ // their recorded scene and use the retained paint replay path instead.
+ if operations
+ .iter()
+ .any(|operation| matches!(operation, PaintOperation::FastRoundedClip(_)))
+ {
+ return None;
+ }
// Every primitive in the order the renderer draws them: by draw order,
// then by kind, then as inserted.
let mut sequence: Vec<((u32, u8, usize), &Primitive)> = operations
@@ -110,6 +118,7 @@ pub(crate) fn split(
PaintOperation::Primitive(primitive) => PaintOperation::Primitive(primitive.clone()),
PaintOperation::StartLayer(bounds) => PaintOperation::StartLayer(*bounds),
PaintOperation::EndLayer => PaintOperation::EndLayer,
+ PaintOperation::FastRoundedClip(clip) => PaintOperation::FastRoundedClip(*clip),
})
.collect();
Some((tiles, overlay))
@@ -127,6 +136,9 @@ pub(crate) fn lift(content: Scene) -> (Scene, Rc<[Primitive]>) {
PaintOperation::Primitive(primitive) => rest.insert_primitive(primitive),
PaintOperation::StartLayer(bounds) => rest.push_layer(bounds),
PaintOperation::EndLayer => rest.pop_layer(),
+ PaintOperation::FastRoundedClip(clip) => {
+ crate::fast::rounded_clip::record(&mut rest, clip)
+ }
}
}
rest.finish();
diff --git a/third-party/gpui-fast/crates/gpui/src/fast/layers/paint.rs b/third-party/gpui-fast/crates/gpui/src/fast/layers/paint.rs
index d199391743..bcedfc90ec 100644
--- a/third-party/gpui-fast/crates/gpui/src/fast/layers/paint.rs
+++ b/third-party/gpui-fast/crates/gpui/src/fast/layers/paint.rs
@@ -664,6 +664,9 @@ pub(crate) fn draw_into_frame<'a>(
scene.push_layer(bounds.intersect(&viewport));
}
PaintOperation::EndLayer => scene.pop_layer(),
+ PaintOperation::FastRoundedClip(clip) => {
+ crate::fast::rounded_clip::record(scene, clip.translated(delta))
+ }
}
}
}
@@ -748,6 +751,10 @@ fn repaint(
size: region.size,
};
let (content, overlay) = crate::fast::layers::overlay::lift(content);
+ let has_paths = content
+ .paint_operations
+ .iter()
+ .any(|operation| matches!(operation, PaintOperation::Primitive(Primitive::Path(_))));
let mut visible = painting.viewport.scale(scale_factor);
visible.origin += to_content;
let mut rendered_work = 0;
@@ -787,7 +794,7 @@ fn repaint(
hovers,
dependencies: painting.dependencies.union(&paint_dependencies),
views,
- has_paths: false,
+ has_paths,
overlay,
view_layouts: Rc::new(mem::take(&mut painting.view_layouts)),
a11y: painting.a11y.take(),
@@ -914,6 +921,9 @@ fn translated_scene(scene: &Scene, delta: Point<ScaledPixels>) -> Scene {
size: bounds.size,
}),
PaintOperation::EndLayer => translated.pop_layer(),
+ PaintOperation::FastRoundedClip(clip) => {
+ crate::fast::rounded_clip::record(&mut translated, clip.translated(delta))
+ }
}
}
translated.finish();
diff --git a/third-party/gpui-fast/crates/gpui/src/fast/layers/policy.rs b/third-party/gpui-fast/crates/gpui/src/fast/layers/policy.rs
index 7a97fdc952..4f29485a69 100644
--- a/third-party/gpui-fast/crates/gpui/src/fast/layers/policy.rs
+++ b/third-party/gpui-fast/crates/gpui/src/fast/layers/policy.rs
@@ -114,6 +114,7 @@ impl LayerPolicy {
/// not composited in another.
#[derive(Clone, PartialEq)]
struct LayerContext {
+ composition_surface: Option<crate::fast::composition::CompositionSurfaceId>,
bounds: Bounds<Pixels>,
content_size: Size<Pixels>,
content_mask: ContentMask<Pixels>,
@@ -127,6 +128,7 @@ struct LayerContext {
impl LayerContext {
fn current(window: &Window, bounds: Bounds<Pixels>, content_size: Size<Pixels>) -> Self {
LayerContext {
+ composition_surface: crate::fast::composition::input_surface(window),
bounds,
content_size,
content_mask: window.content_mask(),
diff --git a/third-party/gpui-fast/crates/gpui/src/fast/layers/reuse.rs b/third-party/gpui-fast/crates/gpui/src/fast/layers/reuse.rs
index 9b0d7e05ba..200b8a1c62 100644
--- a/third-party/gpui-fast/crates/gpui/src/fast/layers/reuse.rs
+++ b/third-party/gpui-fast/crates/gpui/src/fast/layers/reuse.rs
@@ -40,6 +40,7 @@ pub(crate) fn carry_prepaint(
let a11y = record.a11y.clone();
let input = &mut layer.input;
let moved = delta != input.stale;
+ input.scrolled = delta - input.stale;
input.stale = delta;
input.viewport = viewport;
input.handle_offset.set(delta);
@@ -148,8 +149,9 @@ pub(crate) fn carry_paint(window: &mut Window, id: &GlobalElementId) {
};
let range = record.paint_range.clone();
let delta = layer.input.stale;
+ let scrolled = layer.input.scrolled;
let viewport = layer.input.viewport;
- let mut carried = carry_paint_records(window, &range, delta, viewport, true);
+ let mut carried = carry_paint_records(window, &range, delta, scrolled, viewport, true);
let Some(layer) = window.fast_layers.layers.get_mut(id) else {
return;
};
@@ -167,13 +169,15 @@ pub(crate) fn carry_paint(window: &mut Window, id: &GlobalElementId) {
/// again, returning where they lie in it: its window control hitboxes,
/// moved by `delta` and clipped to `viewport`, cursor styles, input
/// handlers, mouse listeners, element states if `element_states`, tab stops,
-/// line layouts and, for tests, debug bounds, moved by `delta`. The scene is
-/// not carried.
+/// line layouts and, for tests, debug bounds, moved by `scrolled`: the
+/// rendered frame holds them where it showed what added them, which has
+/// scrolled by as much since. The scene is not carried.
#[inline]
pub(crate) fn carry_paint_records(
window: &mut Window,
range: &Range<PaintIndex>,
delta: Point<Pixels>,
+ scrolled: Point<Pixels>,
viewport: Bounds<Pixels>,
element_states: bool,
) -> Range<PaintIndex> {
@@ -225,15 +229,16 @@ pub(crate) fn carry_paint_records(
[range.start.tab_handle_index..range.end.tab_handle_index],
);
crate::fast::a11y::reuse_paint(window, range);
- let next = &mut window.next_frame;
- let rendered = &mut window.rendered_frame;
#[cfg(any(test, feature = "test-support"))]
- for (selector, bounds) in
- &rendered.debug_bounds_records[range.start.debug_bounds_index..range.end.debug_bounds_index]
+ for (selector, bounds) in &window.rendered_frame.debug_bounds_records
+ [range.start.debug_bounds_index..range.end.debug_bounds_index]
{
- let moved = Bounds::new(bounds.origin + delta, bounds.size);
- next.record_debug_bounds(selector.clone(), moved);
+ let moved = Bounds::new(bounds.origin + scrolled, bounds.size);
+ window
+ .next_frame
+ .record_debug_bounds(selector.clone(), moved);
}
+ let _ = scrolled;
window
.text_system
.reuse_layouts(range.start.line_layout_index.clone()..range.end.line_layout_index.clone());
diff --git a/third-party/gpui-fast/crates/gpui/src/fast/layers/scene.rs b/third-party/gpui-fast/crates/gpui/src/fast/layers/scene.rs
index b6509ec3cb..d5c26b688e 100644
--- a/third-party/gpui-fast/crates/gpui/src/fast/layers/scene.rs
+++ b/third-party/gpui-fast/crates/gpui/src/fast/layers/scene.rs
@@ -125,6 +125,9 @@ impl LayerFrame {
scene.push_layer(translate_bounds(*layer_bounds, delta))
}
PaintOperation::EndLayer => scene.pop_layer(),
+ PaintOperation::FastRoundedClip(clip) => {
+ crate::fast::rounded_clip::record(&mut scene, clip.translated(delta))
+ }
}
}
scene.finish();
diff --git a/third-party/gpui-fast/crates/gpui/src/fast/layers/tiles.rs b/third-party/gpui-fast/crates/gpui/src/fast/layers/tiles.rs
index 87b6376ede..fd4f9c87a7 100644
--- a/third-party/gpui-fast/crates/gpui/src/fast/layers/tiles.rs
+++ b/third-party/gpui-fast/crates/gpui/src/fast/layers/tiles.rs
@@ -66,6 +66,15 @@ pub(crate) fn tile_hashes(
});
}
}
+ PaintOperation::FastRoundedClip(clip) => {
+ for (coord, hasher) in &mut hashers {
+ let delta = point(
+ ScaledPixels(-(coord.x as f32) * tile_size as f32),
+ ScaledPixels(-(coord.y as f32) * tile_size as f32),
+ );
+ clip.translated(delta).hash(hasher);
+ }
+ }
}
}
hashers
@@ -105,6 +114,7 @@ pub(crate) fn part_tile_hashes(
}
PaintOperation::StartLayer(_) => hasher.write_u8(1),
PaintOperation::EndLayer => hasher.write_u8(2),
+ PaintOperation::FastRoundedClip(clip) => clip.hash(&mut hasher),
}
}
let hash = hasher.finish();
diff --git a/third-party/gpui-fast/crates/gpui/src/fast/layers/verify.rs b/third-party/gpui-fast/crates/gpui/src/fast/layers/verify.rs
index 1078d8788e..efe52d95cd 100644
--- a/third-party/gpui-fast/crates/gpui/src/fast/layers/verify.rs
+++ b/third-party/gpui-fast/crates/gpui/src/fast/layers/verify.rs
@@ -93,6 +93,9 @@ fn expand_layer(
size: bounds.size,
}),
PaintOperation::EndLayer => scratch.pop_layer(),
+ PaintOperation::FastRoundedClip(clip) => {
+ crate::fast::rounded_clip::record(&mut scratch, clip.translated(translation))
+ }
}
}
scratch.finish();
diff --git a/third-party/gpui-fast/crates/gpui/src/fast/layout.rs b/third-party/gpui-fast/crates/gpui/src/fast/layout.rs
index 9f4221abaa..b3cf8b8ab6 100644
--- a/third-party/gpui-fast/crates/gpui/src/fast/layout.rs
+++ b/third-party/gpui-fast/crates/gpui/src/fast/layout.rs
@@ -7,7 +7,7 @@ use crate::{
taffy::{MeasureFn, NodeContext, ToTaffy as _},
util::round_to_device_pixel,
};
-use collections::{FxHashMap, FxHasher};
+use collections::{FxHashMap, FxHashSet, FxHasher};
use smallvec::SmallVec;
use std::{
any::Any,
@@ -17,7 +17,7 @@ use std::{
mem,
rc::Rc,
};
-use taffy::TaffyTree;
+use taffy::{TaffyTree, TraversePartialTree as _};
/// What a Taffy call that cannot fail by construction panics with, as
/// upstream's layout engine does.
@@ -80,6 +80,17 @@ pub(crate) struct LayoutRetention {
/// How many list rows are being laid out, nested. See
/// [`RetainedNode::lingers`].
list_rows: usize,
+ /// Nodes another node took a child from. Taffy detaches a child from its
+ /// old parent when a new one is given it, so the old parent no longer
+ /// holds the tree it was laid out with. Requesting it again writes its
+ /// children back, but a subtree kept without being requested would keep
+ /// it as it is, so [`TaffyLayoutEngine::try_keep_retained`] refuses one
+ /// holding any of these.
+ robbed_parents: FxHashSet<LayoutId>,
+ /// Whether a child taken from a node that claimed it earlier in the same
+ /// frame has been reported. See [`TaffyLayoutEngine::set_children`].
+ #[cfg(debug_assertions)]
+ reported_double_parent: bool,
}
/// How many frames a node of a list row is kept unclaimed before it is
@@ -210,9 +221,6 @@ struct RetainedNode {
/// Frame in which this node was last claimed. A node that goes a whole
/// frame unclaimed has left the element tree and is released.
claimed_in_frame: u64,
- /// The children the node was last given, kept here because reading them
- /// back out of Taffy allocates.
- children: SmallVec<[LayoutId; 8]>,
/// Whether the node measures its own size.
measured: bool,
/// [`layout_fingerprint`] of the style the node was last asked for. While
@@ -510,11 +518,11 @@ impl TaffyLayoutEngine {
pub(crate) fn try_keep_retained(&mut self, keys: &[u64]) -> bool {
let frame = self.retention.frame;
+ let robbed = &self.retention.robbed_parents;
let all_there = keys.iter().all(|key| {
- self.retention
- .retained
- .get(key)
- .is_some_and(|node| node.claimed_in_frame != frame)
+ self.retention.retained.get(key).is_some_and(|node| {
+ node.claimed_in_frame != frame && (robbed.is_empty() || !robbed.contains(&node.id))
+ })
});
if all_there {
self.keep_retained(keys);
@@ -558,14 +566,7 @@ impl TaffyLayoutEngine {
/// Records a freshly allocated node under `key`, or as transient when there
/// is no key to record it under.
- fn retain(
- &mut self,
- key: Option<u64>,
- id: LayoutId,
- children: &[LayoutId],
- measured: bool,
- style_fingerprint: u64,
- ) {
+ fn retain(&mut self, key: Option<u64>, id: LayoutId, measured: bool, style_fingerprint: u64) {
let retention = &mut self.retention;
let Some(key) = key else {
retention.transient.push(id);
@@ -576,7 +577,6 @@ impl TaffyLayoutEngine {
RetainedNode {
id,
claimed_in_frame: retention.frame,
- children: SmallVec::from_slice(children),
measured,
style_fingerprint,
measurement: None,
@@ -648,26 +648,68 @@ impl TaffyLayoutEngine {
.expect(EXPECT_MESSAGE);
}
- /// Writes `children` to a retained node, but only if the list changed.
- fn apply_children(&mut self, key: u64, id: LayoutId, children: &[LayoutId]) {
+ /// Writes `children` to a retained node, but only if Taffy holds a
+ /// different list. Comparing against Taffy rather than against the last
+ /// request catches a child another node took since.
+ fn apply_children(&mut self, id: LayoutId, children: &[LayoutId]) {
let retention = &mut self.retention;
- let node = retention
- .retained
- .get_mut(&key)
- .expect("a claimed key is always present");
- if node.children.as_slice() == children {
+ if !retention.robbed_parents.is_empty() {
+ retention.robbed_parents.remove(&id);
+ }
+ if self
+ .taffy
+ .child_ids(id.into())
+ .eq(LayoutId::to_taffy_slice(children).iter().copied())
+ {
return;
}
- node.children.clear();
- node.children.extend_from_slice(children);
retention.stats.children_writes += 1;
retention.layout_changes += 1;
+ self.set_children(id, children);
+ }
+
+ /// Gives `id` its `children`, noting each node a child is taken from in
+ /// [`LayoutRetention::robbed_parents`].
+ fn set_children(&mut self, id: LayoutId, children: &[LayoutId]) {
+ for child in children {
+ if let Some(parent) = self.taffy.parent((*child).into())
+ && parent != id.into()
+ {
+ let parent = LayoutId::from(parent);
+ #[cfg(debug_assertions)]
+ self.report_double_parent(parent, *child);
+ self.retention.robbed_parents.insert(parent);
+ }
+ }
self.taffy
// This is safe because LayoutId is repr(transparent) to taffy::tree::NodeId.
.set_children(id.into(), LayoutId::to_taffy_slice(children))
.expect(EXPECT_MESSAGE);
}
+ /// Reports, once, a child requested under two parents in the same frame.
+ /// Each takes it from the other as it is requested, so both, and every
+ /// node above them, are laid out again every frame.
+ #[cfg(debug_assertions)]
+ fn report_double_parent(&mut self, parent: LayoutId, child: LayoutId) {
+ let retention = &mut self.retention;
+ if retention.reported_double_parent {
+ return;
+ }
+ let frame = retention.frame;
+ if retention
+ .retained
+ .values()
+ .any(|node| node.id == parent && node.claimed_in_frame == frame)
+ {
+ retention.reported_double_parent = true;
+ log::warn!(
+ "layout node {child:?} was given to two parents in one frame; \
+ an element is passing the same LayoutId as a child twice"
+ );
+ }
+ }
+
/// Adds a self-measuring leaf to the layout tree as
/// [`request_retained_measured_layout`] does, but lets the element
/// take over the measurement of the node retained under `key`, rather
@@ -889,6 +931,9 @@ pub(crate) fn release_unclaimed_nodes(engine: &mut TaffyLayoutEngine) {
for id in retention.transient.drain(..) {
retention.unstretched_styles.remove(&id);
+ if !retention.robbed_parents.is_empty() {
+ retention.robbed_parents.remove(&id);
+ }
remove_node(&mut engine.taffy, id);
retention.stats.nodes_freed += 1;
}
@@ -899,6 +944,7 @@ pub(crate) fn release_unclaimed_nodes(engine: &mut TaffyLayoutEngine) {
let frame = retention.frame;
let taffy = &mut engine.taffy;
let unstretched_styles = &mut retention.unstretched_styles;
+ let robbed_parents = &mut retention.robbed_parents;
let freed = &mut retention.stats.nodes_freed;
retention.retained.retain(|_, node| {
if node.claimed_in_frame == frame
@@ -907,6 +953,7 @@ pub(crate) fn release_unclaimed_nodes(engine: &mut TaffyLayoutEngine) {
return true;
}
unstretched_styles.remove(&node.id);
+ robbed_parents.remove(&node.id);
remove_node(taffy, node.id);
*freed += 1;
false
@@ -959,7 +1006,7 @@ pub(crate) fn request_retained_layout(
let key = match engine.claim(key) {
Claim::Reused(key, id) => {
engine.apply_requested_style(key, id, style, rem_size, scale_factor);
- engine.apply_children(key, id, children);
+ engine.apply_children(id, children);
// A node that measured itself on an earlier frame no longer does.
if engine
.retention
@@ -1002,13 +1049,9 @@ pub(crate) fn request_retained_layout(
// child would lose its parent link, and with it the offset
// `layout_bounds` adds up from its ancestors. `set_children`
// detaches each child from wherever it was first.
- engine
- .taffy
- // This is safe because LayoutId is repr(transparent) to taffy::tree::NodeId.
- .set_children(id.into(), LayoutId::to_taffy_slice(children))
- .expect(EXPECT_MESSAGE);
+ engine.set_children(id, children);
}
- engine.retain(key, id, children, false, style_fingerprint);
+ engine.retain(key, id, false, style_fingerprint);
id
}
@@ -1091,7 +1134,7 @@ fn request_measured_node(
.new_leaf_with_context(taffy_style, NodeContext { measure })
.expect(EXPECT_MESSAGE)
.into();
- engine.retain(key, id, &[], true, style_fingerprint);
+ engine.retain(key, id, true, style_fingerprint);
if let Some(node) = key.and_then(|key| engine.retention.retained.get_mut(&key))
&& node.id == id
{
@@ -1102,7 +1145,7 @@ fn request_measured_node(
};
engine.apply_requested_style(key, id, style, rem_size, scale_factor);
- engine.apply_children(key, id, &[]);
+ engine.apply_children(id, &[]);
// Nothing says whether the measurement still stands, and what it
// produces lives in state the element made afresh this frame, so it
diff --git a/third-party/gpui-fast/crates/gpui/src/fast/mod.rs b/third-party/gpui-fast/crates/gpui/src/fast/mod.rs
index b85b4271b4..33b83c3f43 100644
--- a/third-party/gpui-fast/crates/gpui/src/fast/mod.rs
+++ b/third-party/gpui-fast/crates/gpui/src/fast/mod.rs
@@ -30,6 +30,8 @@ pub(crate) mod misc;
pub(crate) mod number_shaping;
pub(crate) mod path_cache;
pub(crate) mod retained;
+pub(crate) mod rounded_clip;
+pub(crate) mod rounded_clip_scene;
pub(crate) mod scene;
pub(crate) mod splice;
pub(crate) mod stats;
diff --git a/third-party/gpui-fast/crates/gpui/src/fast/retained.rs b/third-party/gpui-fast/crates/gpui/src/fast/retained.rs
index 4090ed77ba..b9f0517896 100644
--- a/third-party/gpui-fast/crates/gpui/src/fast/retained.rs
+++ b/third-party/gpui-fast/crates/gpui/src/fast/retained.rs
@@ -21,7 +21,8 @@ use crate::fast::splice::{Prebuilt, Rebuild, Splice, SplicedPrepaint};
use crate::window::{PaintIndex, PrepaintStateIndex};
use crate::{
AnyElement, App, AvailableSpace, Bounds, ContentMask, ElementId, EntityId, GlobalElementId,
- HitboxId, IntoElement, LayoutId, Pixels, Size, Style, TextStyle, View, ViewElement, Window,
+ HitboxId, LayoutId, Pixels, PlatformInputHandler, Size, Style, TextStyle, View, ViewElement,
+ Window,
};
use collections::{FxHashMap, FxHashSet};
use refineable::Refineable;
@@ -33,7 +34,6 @@ use std::{any::TypeId, cell::RefCell, mem, ops::Range, rc::Rc};
#[inline(always)]
pub(crate) fn begin_frame(window: &mut Window, cx: &App) {
window.fast_layout.phase_times.begin();
- crate::fast::layers::hover_freeze::begin_draw(window);
crate::fast::a11y::new_frame(window);
window.mark_changed_retained_views_dirty(cx);
}
@@ -48,6 +48,23 @@ pub(crate) fn draw_outside_frames(window: &mut Window) {
window.refreshing = true;
}
+/// Forwarded to by `Window::draw` as it hands the platform this frame's input
+/// handler.
+pub(crate) fn note_installed_input_handler(window: &mut Window, handler: &PlatformInputHandler) {
+ window.retained_state.installed_input_focus = Some(handler.focus_id());
+}
+
+/// Forwarded to by `Window::draw` as it takes back the input handler it gave
+/// the platform. A view drawn again from last frame hands the platform that
+/// handler again, which is only right if it is the one GPUI gave it: when
+/// the platform hands back another, nothing is drawn again from last frame,
+/// so the views that paint input handlers paint them anew.
+pub(crate) fn note_restored_input_handler(window: &mut Window, handler: &PlatformInputHandler) {
+ if window.retained_state.installed_input_focus != Some(handler.focus_id()) {
+ window.refreshing = true;
+ }
+}
+
/// The retained subtrees being prepainted right now, for something deferred
/// from them to be counted as theirs.
#[inline(always)]
@@ -131,6 +148,7 @@ pub(crate) enum PaintStatus {
/// place it was drawn in and what it inherited there.
#[derive(PartialEq)]
pub(crate) struct RetainedContext {
+ pub(crate) composition_surface: Option<crate::fast::composition::CompositionSurfaceId>,
pub(crate) bounds: Bounds<Pixels>,
pub(crate) content_mask: ContentMask<Pixels>,
pub(crate) text_style: TextStyle,
@@ -258,6 +276,9 @@ pub(crate) struct RetainedState {
/// handed the elements it built, rather than what they wrote as they
/// were laid out. See [`note_rendered`].
pub(crate) render_writes: FxHashMap<EntityId, Range<u64>>,
+ /// The focus of the input handler last given to the platform window, if
+ /// one was. See [`note_restored_input_handler`].
+ pub(crate) installed_input_focus: Option<Option<crate::FocusId>>,
}
impl RetainedState {
@@ -276,6 +297,7 @@ impl RetainedState {
splice_keys: FxHashSet::default(),
rendering_since: FxHashMap::default(),
render_writes: FxHashMap::default(),
+ installed_input_focus: None,
}
}
@@ -433,7 +455,7 @@ impl RetainedSubtrees {
}
/// Whether any subtree was drawn from last frame.
- #[cfg(any(test, feature = "test-support"))]
+ #[cfg(test)]
pub(crate) fn reused_any(&self) -> bool {
self.reused_any
}
@@ -630,6 +652,7 @@ impl Window {
&& context.content_mask == self.content_mask()
&& context.text_style == self.text_style()
&& context.selection_context == self.selection_context
+ && context.composition_surface == crate::fast::composition::input_surface(self)
}
/// Lays out the view last frame's record `previous` stands for as it was
@@ -904,6 +927,7 @@ impl Window {
paint: PaintStatus::Unpainted,
nested: 0,
context: Rc::new(RetainedContext {
+ composition_surface: None,
bounds: Bounds::default(),
content_mask: ContentMask::default(),
text_style: TextStyle::default(),
@@ -975,6 +999,7 @@ impl Window {
};
}
let context = RetainedContext {
+ composition_surface: crate::fast::composition::input_surface(self),
bounds,
content_mask: self.content_mask(),
text_style: self.text_style(),
@@ -1384,18 +1409,6 @@ pub(crate) fn finish_retained_frame(window: &mut Window) {
window.next_frame.retained.finish_frame();
crate::fast::layers::paint::finish_frame(window);
crate::fast::layers::finish_frame(window);
- #[cfg(any(test, feature = "test-support"))]
- if window.next_frame.retained.reused_any() {
- // Reused subtrees do not paint, and the bounds they would have
- // recorded for tests to find them by are last frame's.
- for (selector, bounds) in &window.rendered_frame.debug_bounds {
- window
- .next_frame
- .debug_bounds
- .entry(selector.clone())
- .or_insert(*bounds);
- }
- }
}
// These reach into the window's layout state, for views drawn again at the
@@ -1530,12 +1543,8 @@ impl<V: View> ViewElement<V> {
}
let start = note_rendering(window, cx, entity_id);
let mut recording = window.begin_retained_layout(cx);
- let mut element = self
- .view
- .take()
- .unwrap()
- .render(window, cx)
- .into_any_element();
+ let mut element =
+ crate::view::render_view(self.view.take().unwrap(), window, cx);
note_rendered(window, cx, entity_id, start);
recording.rendered(cx);
let layout_id = element.request_layout(window, cx);
@@ -1549,12 +1558,8 @@ impl<V: View> ViewElement<V> {
)
}
_ => {
- let mut element = self
- .view
- .take()
- .unwrap()
- .render(window, cx)
- .into_any_element();
+ let mut element =
+ crate::view::render_view(self.view.take().unwrap(), window, cx);
let layout_id = element.request_layout(window, cx);
(
layout_id,
@@ -1571,12 +1576,8 @@ impl<V: View> ViewElement<V> {
window.with_id(
ElementId::Name(std::any::type_name::<V>().into()),
|window| {
- let mut element = self
- .view
- .take()
- .unwrap()
- .render(window, cx)
- .into_any_element();
+ let mut element =
+ crate::view::render_view(self.view.take().unwrap(), window, cx);
let layout_id = element.request_layout(window, cx);
(
layout_id,
@@ -1713,12 +1714,8 @@ impl<V: View> ViewElement<V> {
}
let start = note_rendering(window, cx, entity_id);
let recording = window.begin_retained(global_id, cx);
- let mut element = self
- .view
- .take()
- .unwrap()
- .render(window, cx)
- .into_any_element();
+ let mut element =
+ crate::view::render_view(self.view.take().unwrap(), window, cx);
note_rendered(window, cx, entity_id, start);
let render = recording.rendered(cx);
element.layout_as_root(bounds.size.into(), window, cx);
@@ -1794,7 +1791,7 @@ impl<V: View> ViewElement<V> {
let start = entity_id.map(|entity_id| note_rendering(window, cx, entity_id));
let view = self.view.take().unwrap();
let (mut element, layout_id) = window.with_layout_key_of_prepainting_element(|window| {
- let mut element = view.render(window, cx).into_any_element();
+ let mut element = crate::view::render_view(view, window, cx);
if let (Some(entity_id), Some(start)) = (entity_id, start) {
note_rendered(window, cx, entity_id, start);
}
@@ -1887,7 +1884,7 @@ pub(crate) fn request_view_layout<V: View>(
#[cfg(feature = "profiler")]
fn end_view_profile<V: View>(
view: &ViewElement<V>,
- start: Option<std::time::Instant>,
+ start: Option<scheduler::Instant>,
phase: crate::profiler::ViewPhase,
window: &mut Window,
) {
@@ -1993,7 +1990,12 @@ impl HitboxId {
/// Whether the hitbox is hovered, as [`HitboxId::is_hovered`] answers,
/// without noting that it was asked.
pub(crate) fn hovered_now(self, window: &Window) -> bool {
- window.captured_hitbox == Some(self)
- || (!window.last_input_was_keyboard() && self.hit_test(window))
+ if window.captured_hitbox == Some(self) {
+ return true;
+ }
+ if crate::fast::layers::hover_freeze::hides_hover(window) {
+ return false;
+ }
+ self.hit_test(window)
}
}
diff --git a/third-party/gpui-fast/crates/gpui/src/fast/rounded_clip.rs b/third-party/gpui-fast/crates/gpui/src/fast/rounded_clip.rs
new file mode 100644
index 0000000000..f8df69fa20
--- /dev/null
+++ b/third-party/gpui-fast/crates/gpui/src/fast/rounded_clip.rs
@@ -0,0 +1,472 @@
+//! Rounded overflow clips keep their own padding-edge geometry. Intersecting
+//! rectangular culling bounds must never move a corner or give a descendant
+//! its ancestor's radius (zed#37480).
+//!
+//! Scopes refer to the unsorted paint operation stream, rather than GPU batch
+//! indices: batching can reorder non-overlapping primitives. Replaying a view
+//! copies the scopes owned by that view under the caller's current scope.
+
+use std::ops::Range;
+
+use crate::{
+ App, Bounds, Corners, Overflow, Pixels, ScaledPixels, Scene, Size, Style, Window, point, size,
+};
+
+/// The curved padding edge, in device pixels. Adjacent borders can have
+/// different widths, so the inner corner is generally an ellipse.
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub(crate) struct Shape {
+ pub(crate) bounds: Bounds<ScaledPixels>,
+ pub(crate) radii: Corners<Size<ScaledPixels>>,
+}
+
+#[derive(Clone, Copy, Debug)]
+pub(crate) enum ClipOperation {
+ Begin(Shape),
+ End,
+}
+
+impl ClipOperation {
+ pub(crate) fn translated(self, delta: crate::Point<ScaledPixels>) -> Self {
+ match self {
+ Self::Begin(mut shape) => {
+ shape.bounds.origin += delta;
+ Self::Begin(shape)
+ }
+ Self::End => Self::End,
+ }
+ }
+
+ pub(crate) fn hash(self, hasher: &mut impl std::hash::Hasher) {
+ match self {
+ Self::Begin(shape) => {
+ hasher.write_u8(3);
+ for value in [
+ shape.bounds.origin.x,
+ shape.bounds.origin.y,
+ shape.bounds.size.width,
+ shape.bounds.size.height,
+ shape.radii.top_left.width,
+ shape.radii.top_left.height,
+ shape.radii.top_right.width,
+ shape.radii.top_right.height,
+ shape.radii.bottom_right.width,
+ shape.radii.bottom_right.height,
+ shape.radii.bottom_left.width,
+ shape.radii.bottom_left.height,
+ ] {
+ hasher.write_u32(value.0.to_bits());
+ }
+ }
+ Self::End => hasher.write_u8(4),
+ }
+ }
+}
+
+impl Shape {
+ fn for_style(style: &Style, bounds: Bounds<Pixels>, rem: Pixels, scale: f32) -> Option<Self> {
+ // CSS curves overflow's padding edge only when both axes clip.
+ if style.overflow.x == Overflow::Visible || style.overflow.y == Overflow::Visible {
+ return None;
+ }
+ // Match the radii used to paint this element's background and border.
+ let radii = style
+ .corner_radii
+ .to_pixels(rem)
+ .clamp_radii_for_quad_size(bounds.size);
+ if radii.max() <= Pixels::ZERO {
+ return None;
+ }
+ let widths = style.border_widths.to_pixels(rem);
+ let inner = |radius: Pixels, horizontal: Pixels, vertical: Pixels| {
+ size(
+ ScaledPixels(f32::from((radius - horizontal).max(Pixels::ZERO)) * scale),
+ ScaledPixels(f32::from((radius - vertical).max(Pixels::ZERO)) * scale),
+ )
+ };
+ Some(Self {
+ bounds: Bounds {
+ origin: (bounds.origin + point(widths.left, widths.top)).scale(scale),
+ size: size(
+ (bounds.size.width - widths.left - widths.right).max(Pixels::ZERO),
+ (bounds.size.height - widths.top - widths.bottom).max(Pixels::ZERO),
+ )
+ .scale(scale),
+ },
+ radii: Corners {
+ top_left: inner(radii.top_left, widths.left, widths.top),
+ top_right: inner(radii.top_right, widths.right, widths.top),
+ bottom_right: inner(radii.bottom_right, widths.right, widths.bottom),
+ bottom_left: inner(radii.bottom_left, widths.left, widths.bottom),
+ },
+ })
+ }
+
+ /// CPU reference for testing the geometric intersection independently of
+ /// antialiasing. Check every corner: an unusually thick opposite border
+ /// can cut through a corner's quarter ellipse.
+ #[cfg(test)]
+ fn contains(&self, position: crate::Point<ScaledPixels>) -> bool {
+ let b = self.bounds;
+ if b.is_empty() || !b.contains(&position) {
+ return false;
+ }
+ let corners = [
+ (b.origin, self.radii.top_left, 1., 1.),
+ (b.top_right(), self.radii.top_right, -1., 1.),
+ (b.bottom_right(), self.radii.bottom_right, -1., -1.),
+ (b.bottom_left(), self.radii.bottom_left, 1., -1.),
+ ];
+ corners.into_iter().all(|(corner, radius, dx, dy)| {
+ let rx = radius.width.0;
+ let ry = radius.height.0;
+ if rx == 0. || ry == 0. {
+ return true;
+ }
+ let x = (position.x.0 - corner.x.0) * dx;
+ let y = (position.y.0 - corner.y.0) * dy;
+ x >= rx || y >= ry || ((x - rx) / rx).powi(2) + ((y - ry) / ry).powi(2) <= 1.
+ })
+ }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub(crate) struct Scope {
+ pub(crate) operations: Range<usize>,
+ pub(crate) shape: Shape,
+ pub(crate) parent: Option<usize>,
+}
+
+/// Private scene data; ContentMask and all public primitive layouts stay
+/// unchanged. An ordinary rectangular scene never allocates any scope data.
+#[derive(Default)]
+pub(crate) struct SceneClips {
+ pub(crate) scopes: Vec<Scope>,
+ active: Vec<usize>,
+ pub(crate) compiled: bool,
+}
+
+impl SceneClips {
+ fn begin(&mut self, operation: usize, shape: Shape) -> usize {
+ let index = self.scopes.len();
+ self.scopes.push(Scope {
+ operations: operation..operation,
+ shape,
+ parent: self.active.last().copied(),
+ });
+ self.active.push(index);
+ index
+ }
+
+ fn end(&mut self, scope: usize, operation: usize) {
+ let active = self.active.pop();
+ debug_assert_eq!(active, Some(scope));
+ self.scopes[scope].operations.end = operation;
+ }
+}
+
+pub(crate) fn clear(scene: &mut Scene) {
+ scene.fast_rounded_clip.scopes.clear();
+ scene.fast_rounded_clip.active.clear();
+ scene.fast_rounded_clip.compiled = false;
+}
+
+/// The continuation paints descendants; the element's own border and drop
+/// shadow remain outside this scope. The shape uses the original element
+/// bounds, never the already-intersected rectangular content mask.
+pub(crate) fn paint_children(
+ style: &Style,
+ bounds: Bounds<Pixels>,
+ window: &mut Window,
+ cx: &mut App,
+ paint: impl FnOnce(&mut Window, &mut App),
+) {
+ let Some(shape) = Shape::for_style(style, bounds, window.rem_size(), window.scale_factor())
+ else {
+ return paint(window, cx);
+ };
+ record(&mut window.next_frame.scene, ClipOperation::Begin(shape));
+ paint(window, cx);
+ record(&mut window.next_frame.scene, ClipOperation::End);
+}
+
+/// Explicit operation markers distinguish scope ownership even when a parent
+/// and its only child paint exactly the same primitive range. Markers change
+/// neither the bounds tree nor the primitive draw orders.
+pub(crate) fn record(scene: &mut Scene, operation: ClipOperation) {
+ scene.fast_rounded_clip.compiled = false;
+ let index = scene.len();
+ match operation {
+ ClipOperation::Begin(shape) => {
+ scene.fast_rounded_clip.begin(index, shape);
+ }
+ ClipOperation::End => {
+ let scope = *scene
+ .fast_rounded_clip
+ .active
+ .last()
+ .expect("balanced rounded clips");
+ scene.fast_rounded_clip.end(scope, index + 1);
+ }
+ }
+ scene
+ .paint_operations
+ .push(crate::scene::PaintOperation::FastRoundedClip(operation));
+}
+
+pub(crate) fn track(operation: ClipOperation, active: &mut Vec<Shape>) -> anyhow::Result<()> {
+ match operation {
+ ClipOperation::Begin(shape) => active.push(shape),
+ ClipOperation::End => {
+ anyhow::ensure!(
+ active.pop().is_some(),
+ "source scene closes a rounded clip that is not open"
+ );
+ }
+ }
+ Ok(())
+}
+
+#[cfg(any(test, feature = "test-support"))]
+impl Window {
+ /// The complete rendered scene, including clip scopes and scroll layers,
+ /// for tests that rasterize through a platform's headless GPU renderer.
+ pub fn painted_scene(&self) -> &Scene {
+ &self.rendered_frame.scene
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{ClipOperation, SceneClips, Shape, record};
+ use crate::{
+ Bounds, Corners, Edges, Overflow, Quad, ScaledPixels, Scene, Style, point, px, size,
+ };
+
+ fn style(radius: f32) -> Style {
+ Style {
+ overflow: point(Overflow::Hidden, Overflow::Hidden),
+ corner_radii: Corners::all(px(radius)).map(|radius| (*radius).into()),
+ ..Default::default()
+ }
+ }
+
+ fn shape() -> Shape {
+ Shape::for_style(
+ &style(24.),
+ Bounds::new(point(px(0.), px(0.)), size(px(64.), px(64.))),
+ px(16.),
+ 1.,
+ )
+ .unwrap()
+ }
+
+ fn at(x: f32, y: f32) -> crate::Point<ScaledPixels> {
+ point(ScaledPixels(x), ScaledPixels(y))
+ }
+
+ fn quad() -> Quad {
+ let bounds = shape().bounds;
+ Quad {
+ bounds,
+ content_mask: crate::ContentMask { bounds },
+ ..Default::default()
+ }
+ }
+
+ #[test]
+ fn rounded_clip_preserves_original_geometry_under_rectangular_cropping() {
+ let mut shape = shape();
+ shape.bounds.size.height = ScaledPixels(200.);
+ let crop = Bounds::new(at(0., 0.), size(ScaledPixels(64.), ScaledPixels(50.)));
+ assert!(!shape.contains(at(1., 1.)));
+ assert!(shape.contains(at(1., 49.)) && crop.contains(&at(1., 49.)));
+ assert!(!crop.contains(&at(32., 51.)));
+ }
+
+ #[test]
+ fn rounded_clip_does_not_give_an_inset_rect_an_ancestor_radius() {
+ let parent = shape();
+ let inset = Bounds::new(at(16., 16.), size(ScaledPixels(32.), ScaledPixels(32.)));
+ assert!(parent.contains(at(17., 17.)) && inset.contains(&at(17., 17.)));
+ assert!(
+ !Shape {
+ bounds: inset,
+ ..parent
+ }
+ .contains(at(17., 17.))
+ );
+ }
+
+ #[test]
+ fn transparent_unequal_borders_produce_an_elliptical_padding_edge() {
+ let mut style = style(24.);
+ style.border_widths = Edges {
+ left: px(8.).into(),
+ top: px(4.).into(),
+ right: px(2.).into(),
+ bottom: px(6.).into(),
+ };
+ let shape = Shape::for_style(
+ &style,
+ Bounds::new(point(px(0.), px(0.)), size(px(64.), px(64.))),
+ px(16.),
+ 2.,
+ )
+ .unwrap();
+ assert_eq!(shape.bounds.origin, at(16., 8.));
+ assert_eq!(
+ shape.radii.top_left,
+ size(ScaledPixels(32.), ScaledPixels(40.))
+ );
+ assert_eq!(
+ shape.radii.bottom_right,
+ size(ScaledPixels(44.), ScaledPixels(36.))
+ );
+ assert!(!shape.contains(at(17., 9.)));
+ assert!(shape.contains(at(48., 9.)));
+ }
+
+ #[test]
+ fn visible_axis_and_square_corners_need_no_rounded_clip() {
+ let bounds = Bounds::new(point(px(0.), px(0.)), size(px(64.), px(64.)));
+ assert!(Shape::for_style(&style(0.), bounds, px(16.), 1.).is_none());
+ let mut style = style(24.);
+ style.overflow.x = Overflow::Visible;
+ assert!(Shape::for_style(&style, bounds, px(16.), 1.).is_none());
+ }
+
+ #[test]
+ fn replay_reparents_only_scopes_owned_by_the_replayed_view() {
+ let mut source = Scene::default();
+ record(&mut source, ClipOperation::Begin(shape()));
+ source.insert_primitive(quad());
+ let start = source.len();
+ record(&mut source, ClipOperation::Begin(shape()));
+ source.insert_primitive(quad());
+ record(&mut source, ClipOperation::End);
+ let end = source.len();
+ source.insert_primitive(quad());
+ record(&mut source, ClipOperation::End);
+
+ let mut target = Scene::default();
+ target.insert_primitive(quad());
+ record(&mut target, ClipOperation::Begin(shape()));
+ target.replay(start..end, &source);
+ record(&mut target, ClipOperation::End);
+ assert_eq!(target.fast_rounded_clip.scopes.len(), 2);
+ assert_eq!(target.fast_rounded_clip.scopes[1].operations, 2..5);
+ assert_eq!(target.fast_rounded_clip.scopes[1].parent, Some(0));
+ target.clear();
+ assert!(target.fast_rounded_clip.scopes.is_empty());
+ assert!(target.fast_rounded_clip.active.is_empty());
+ }
+
+ #[test]
+ fn replay_copies_nested_scopes_and_keeps_siblings_separate() {
+ let mut source = Scene::default();
+ record(&mut source, ClipOperation::Begin(shape()));
+ record(&mut source, ClipOperation::Begin(shape()));
+ source.insert_primitive(quad());
+ record(&mut source, ClipOperation::End);
+ record(&mut source, ClipOperation::End);
+ record(&mut source, ClipOperation::Begin(shape()));
+ source.insert_primitive(quad());
+ record(&mut source, ClipOperation::End);
+ let mut target = Scene::default();
+ target.replay(0..source.len(), &source);
+ let scopes = &target.fast_rounded_clip.scopes;
+ assert_eq!(scopes.len(), 3);
+ assert_eq!(scopes[0].parent, None);
+ assert_eq!(scopes[1].parent, Some(0));
+ assert_eq!(scopes[2].parent, None);
+ assert_eq!(scopes, &source.fast_rounded_clip.scopes);
+ }
+
+ #[test]
+ fn replaying_an_only_child_does_not_capture_its_parents_clip() {
+ let mut source = Scene::default();
+ record(&mut source, ClipOperation::Begin(shape()));
+ let child_start = source.len();
+ source.insert_primitive(quad());
+ let child_end = source.len();
+ record(&mut source, ClipOperation::End);
+
+ let mut target = Scene::default();
+ target.replay(child_start..child_end, &source);
+ assert!(target.fast_rounded_clip.scopes.is_empty());
+ assert_eq!(target.quads.len(), 1);
+ }
+
+ #[test]
+ fn composition_ranges_restore_and_close_enclosing_rounded_clips() {
+ let mut source = Scene::default();
+ record(&mut source, ClipOperation::Begin(shape()));
+ let start = source.len();
+ source.insert_primitive(quad());
+ let end = source.len();
+ record(&mut source, ClipOperation::End);
+ let mut surface = Scene::default();
+ surface.replay_balanced(start..end, &source).unwrap();
+ assert_eq!(surface.fast_rounded_clip.scopes.len(), 1);
+ assert_eq!(surface.fast_rounded_clip.scopes[0].shape, shape());
+ assert_eq!(surface.fast_rounded_clip.scopes[0].operations, 0..3);
+ assert!(surface.fast_rounded_clip.active.is_empty());
+ }
+
+ #[test]
+ fn scrolling_translates_corner_centers_without_changing_radii() {
+ let mut source = Scene::default();
+ record(&mut source, ClipOperation::Begin(shape()));
+ source.insert_primitive(quad());
+ record(&mut source, ClipOperation::End);
+ let layer = crate::LayerFrame {
+ key: crate::LayerKey(1),
+ generation: 1,
+ background: crate::Rgba::default(),
+ tile_size: 32,
+ content: source.into(),
+ dirty_tiles: Vec::new(),
+ };
+ let tile = layer.tile_scene(crate::TileCoord { x: 1, y: 0 });
+ let moved = tile.fast_rounded_clip.scopes[0].shape;
+ assert_eq!(moved.bounds.origin, at(-32., 0.));
+ assert_eq!(moved.radii, shape().radii);
+ assert!(tile.fast_rounded_clip.active.is_empty());
+ }
+
+ #[test]
+ fn changing_only_the_clip_invalidates_scroll_tile_pixels() {
+ fn scene(radius: f32) -> Scene {
+ let mut scene = Scene::default();
+ let mut shape = shape();
+ shape.radii = shape
+ .radii
+ .map(|_| size(ScaledPixels(radius), ScaledPixels(radius)));
+ record(&mut scene, ClipOperation::Begin(shape));
+ scene.insert_primitive(quad());
+ record(&mut scene, ClipOperation::End);
+ scene
+ }
+ let original = scene(24.);
+ let changed = scene(8.);
+ let hashes =
+ |scene: &Scene| crate::fast::layers::tiles::tile_hashes(scene, 32, shape().bounds);
+ assert_ne!(hashes(&original), hashes(&changed));
+ let hashes = |scene: &Scene| {
+ crate::fast::layers::tiles::part_tile_hashes(&scene.paint_operations, 32).0
+ };
+ assert_ne!(hashes(&original), hashes(&changed));
+ }
+
+ #[test]
+ fn rectangular_replay_does_not_allocate_clip_state() {
+ let mut source = Scene::default();
+ source.insert_primitive(quad());
+ let mut target = Scene::default();
+ target.replay(0..1, &source);
+ assert_eq!(target.fast_rounded_clip.scopes.capacity(), 0);
+ assert_eq!(target.fast_rounded_clip.active.capacity(), 0);
+ assert_eq!(SceneClips::default().scopes.capacity(), 0);
+ }
+}
diff --git a/third-party/gpui-fast/crates/gpui/src/fast/rounded_clip_fixtures.rs b/third-party/gpui-fast/crates/gpui/src/fast/rounded_clip_fixtures.rs
new file mode 100644
index 0000000000..1b474c6b9d
--- /dev/null
+++ b/third-party/gpui-fast/crates/gpui/src/fast/rounded_clip_fixtures.rs
@@ -0,0 +1,556 @@
+//! Identical real-div pixel regressions run by each GPU renderer.
+
+use super::{assert_same_pixels, pixel, render_div_fixture};
+use gpui::{Path, px};
+
+/// The command-palette regression from zed#37480: a rounded palette has a
+/// header above the input and list. Its radius belongs only to the palette's
+/// outer corners, never to the input's or list row's own clipping rectangle.
+#[gpui::test]
+fn rounded_overflow_command_palette_rows_keep_square_corners(cx: &mut gpui::TestAppContext) {
+ use gpui::{ParentElement as _, Styled as _};
+ let (pixels, side) = render_div_fixture(cx, || {
+ let row = |height, color| {
+ gpui::div()
+ .w_full()
+ .h(px(height))
+ .flex_shrink_0()
+ .overflow_hidden()
+ .bg(color)
+ };
+ gpui::div().size(px(64.)).bg(gpui::white()).child(
+ gpui::div()
+ .flex()
+ .flex_col()
+ .size(px(64.))
+ .rounded(px(16.))
+ .overflow_hidden()
+ .child(row(16., gpui::black()))
+ .child(row(16., gpui::red()))
+ .child(row(8., gpui::blue()))
+ .child(row(8., gpui::black()))
+ .child(row(16., gpui::green())),
+ )
+ });
+ let sample = |x, y| pixel(&pixels, side, x * side / 64, y * side / 64);
+ // Check all four input and selected-row corners, not just their centres.
+ for x in [0, 63] {
+ for y in [16, 31] {
+ assert_eq!(
+ sample(x, y),
+ [255, 0, 0, 255],
+ "input corner acquired the palette radius at ({x}, {y})"
+ );
+ }
+ for y in [32, 39] {
+ assert_eq!(
+ sample(x, y),
+ [0, 0, 255, 255],
+ "selected row acquired the palette radius at ({x}, {y})"
+ );
+ }
+ assert_eq!(sample(x, 0), [255, 255, 255, 255]);
+ assert_eq!(sample(x, 63), [255, 255, 255, 255]);
+ }
+ assert_eq!(sample(32, 0), [0, 0, 0, 255]);
+ assert_eq!(sample(32, 63), [0, 128, 0, 255]);
+}
+
+#[gpui::test]
+fn rounded_overflow_deep_stack_preserves_clips(cx: &mut gpui::TestAppContext) {
+ use gpui::{ParentElement as _, Styled as _};
+ let (pixels, side) = render_div_fixture(cx, || {
+ let mut child = gpui::div().size(px(64.)).bg(gpui::black());
+ // Distinct geometry prevents identical-scope deduplication. This also
+ // exceeds Metal's 4096-byte inline fragment parameter limit.
+ for depth in 0..100 {
+ child = gpui::div()
+ .size(px(64.))
+ .rounded(px(20. + depth as f32 * 0.01))
+ .overflow_hidden()
+ .child(child);
+ }
+ gpui::div().size(px(64.)).bg(gpui::white()).child(child)
+ });
+ assert_eq!(pixel(&pixels, side, 0, 0), [255, 255, 255, 255]);
+ assert_eq!(pixel(&pixels, side, side / 2, side / 2), [0, 0, 0, 255]);
+}
+
+#[gpui::test]
+fn rounded_overflow_transparent_unequal_borders_clip_padding_edge(cx: &mut gpui::TestAppContext) {
+ use gpui::{ParentElement as _, Styled as _};
+ let (pixels, side) = render_div_fixture(cx, || {
+ let mut parent = gpui::div().size(px(64.)).rounded(px(24.)).overflow_hidden();
+ parent.style().border_widths.left = Some(px(8.).into());
+ parent.style().border_widths.top = Some(px(4.).into());
+ parent.style().border_widths.right = Some(px(2.).into());
+ parent.style().border_widths.bottom = Some(px(6.).into());
+ gpui::div()
+ .size(px(64.))
+ .bg(gpui::white())
+ .child(parent.child(gpui::div().size(px(64.)).bg(gpui::black())))
+ });
+ let sample = |x, y| pixel(&pixels, side, x * side / 64, y * side / 64);
+ assert_eq!(sample(9, 5), [255, 255, 255, 255]);
+ assert_eq!(sample(24, 5), [0, 0, 0, 255]);
+ assert_eq!(sample(9, 24), [0, 0, 0, 255]);
+ assert_eq!(sample(60, 24), [0, 0, 0, 255]);
+ assert_eq!(sample(63, 32), [255, 255, 255, 255]);
+ assert_eq!(sample(32, 60), [255, 255, 255, 255]);
+}
+
+#[gpui::test]
+fn rounded_overflow_empty_padding_edge_hides_descendants(cx: &mut gpui::TestAppContext) {
+ use gpui::{ParentElement as _, Styled as _};
+ let (pixels, _) = render_div_fixture(cx, || {
+ let mut parent = gpui::div().size(px(64.)).rounded(px(24.)).overflow_hidden();
+ parent.style().border_widths.left = Some(px(40.).into());
+ parent.style().border_widths.right = Some(px(40.).into());
+ gpui::div()
+ .size(px(64.))
+ .bg(gpui::white())
+ .child(parent.child(gpui::div().size(px(64.)).bg(gpui::black())))
+ });
+ assert!(
+ pixels
+ .chunks_exact(4)
+ .all(|pixel| pixel == [255, 255, 255, 255])
+ );
+}
+
+/// Change only the ancestor's clip while the child's rendered output remains
+/// retained. The replayed child must not bring its previous parent's clip back.
+pub(super) fn retained_frames(
+ cx: &mut gpui::TestAppContext,
+ mut rasterize: impl FnMut(&gpui::Scene, gpui::Size<gpui::DevicePixels>) -> Vec<u8>,
+) {
+ use gpui::{
+ AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, Styled as _,
+ };
+ use std::{cell::Cell, rc::Rc};
+ struct Child(Rc<Cell<usize>>);
+ impl Render for Child {
+ fn render(&mut self, _: &mut gpui::Window, _: &mut Context<Self>) -> impl IntoElement {
+ self.0.set(self.0.get() + 1);
+ gpui::div().size(px(64.)).bg(gpui::black())
+ }
+ }
+ struct Parent {
+ radius: f32,
+ child: Entity<Child>,
+ }
+ impl Render for Parent {
+ fn render(&mut self, _: &mut gpui::Window, _: &mut Context<Self>) -> impl IntoElement {
+ gpui::div().size(px(64.)).bg(gpui::white()).child(
+ gpui::div()
+ .size(px(64.))
+ .rounded(px(self.radius))
+ .overflow_hidden()
+ .child(self.child.clone()),
+ )
+ }
+ }
+ let renders = Rc::new(Cell::new(0));
+ let window = cx.update(|cx| {
+ let child = cx.new(|_| Child(renders.clone()));
+ cx.open_window(Default::default(), |_, cx| {
+ cx.new(|_| Parent { radius: 24., child })
+ })
+ .unwrap()
+ });
+ for radius in [24., 0., 24.] {
+ window
+ .update(cx, |parent, _, cx| {
+ parent.radius = radius;
+ cx.notify();
+ })
+ .unwrap();
+ cx.update_window(window.into(), |_, window, cx| {
+ window.draw(cx).clear(cx);
+ let side = (64. * window.scale_factor()) as usize;
+ let extent = gpui::size(
+ gpui::DevicePixels(side as i32),
+ gpui::DevicePixels(side as i32),
+ );
+ let pixels = rasterize(window.painted_scene(), extent);
+ let corner = if radius == 0. {
+ [0, 0, 0, 255]
+ } else {
+ [255, 255, 255, 255]
+ };
+ assert_eq!(pixel(&pixels, side, 1, 1), corner);
+ assert_eq!(pixel(&pixels, side, side / 2, side / 2), [0, 0, 0, 255]);
+ })
+ .unwrap();
+ }
+ assert_eq!(
+ renders.get(),
+ 1,
+ "child must remain retained while ancestor clip changes"
+ );
+}
+
+pub(super) fn square_fixture() -> gpui::Div {
+ use gpui::{ParentElement as _, Styled as _};
+ gpui::div().size(px(64.)).bg(gpui::white()).child(
+ gpui::div()
+ .size(px(64.))
+ .rounded(px(24.))
+ .overflow_hidden()
+ .child(gpui::div().size(px(64.)).bg(gpui::black())),
+ )
+}
+
+pub(super) fn tiled_scene(
+ source: &gpui::Scene,
+ extent: gpui::Size<gpui::DevicePixels>,
+) -> gpui::Scene {
+ let mut content = gpui::Scene::default();
+ content.replay(0..source.len(), source);
+ content.finish();
+ let frame = gpui::LayerFrame {
+ key: gpui::LayerKey(123),
+ generation: 1,
+ background: gpui::rgba(0xffffffff),
+ tile_size: 512,
+ content: content.into(),
+ dirty_tiles: Vec::new(),
+ };
+ let coord = gpui::TileCoord { x: 0, y: 0 };
+ let bounds = frame.tile_bounds(coord);
+ let mask = gpui::Bounds::new(
+ gpui::point(gpui::ScaledPixels(0.), gpui::ScaledPixels(0.)),
+ gpui::size(
+ gpui::ScaledPixels(extent.width.0 as f32),
+ gpui::ScaledPixels(extent.height.0 as f32),
+ ),
+ );
+ let mut scene = gpui::Scene::default();
+ scene.insert_primitive(gpui::PolychromeSprite {
+ order: 0,
+ pad: 0,
+ grayscale: false.into(),
+ opacity: 1.,
+ bounds,
+ content_mask: gpui::ContentMask { bounds: mask },
+ corner_radii: Default::default(),
+ tile: gpui::AtlasTile {
+ texture_id: gpui::layer_tile_texture_id(frame.key),
+ tile_id: gpui::layer_tile_id(coord),
+ padding: 0,
+ bounds: gpui::Bounds::new(
+ gpui::point(gpui::DevicePixels(0), gpui::DevicePixels(0)),
+ gpui::size(gpui::DevicePixels(512), gpui::DevicePixels(512)),
+ ),
+ },
+ });
+ scene.layers.frames.push(frame);
+ scene.finish();
+ scene
+}
+
+pub(super) fn sibling_fixture() -> gpui::Div {
+ use gpui::{ParentElement as _, Styled as _};
+ gpui::div()
+ .flex()
+ .flex_wrap()
+ .size(px(64.))
+ .bg(gpui::white())
+ .children((0..64).map(|_| {
+ gpui::div()
+ .size(px(8.))
+ .rounded(px(3.))
+ .overflow_hidden()
+ .child(gpui::div().size(px(8.)).bg(gpui::black()))
+ }))
+}
+
+#[gpui::test]
+fn rounded_overflow_many_siblings_remain_independent(cx: &mut gpui::TestAppContext) {
+ let (pixels, side) = render_div_fixture(cx, sibling_fixture);
+ let cell = side / 8;
+ for row in 0..8 {
+ for col in 0..8 {
+ assert_eq!(
+ pixel(&pixels, side, col * cell, row * cell),
+ [255, 255, 255, 255]
+ );
+ assert_eq!(
+ pixel(&pixels, side, col * cell + cell / 2, row * cell + cell / 2),
+ [0, 0, 0, 255]
+ );
+ }
+ }
+}
+
+/// Exercise div layout and painting before rasterizing the complete scene.
+/// A square child must not cover the cutout of its rounded overflow container.
+#[gpui::test]
+fn rounded_overflow_clips_square_child(cx: &mut gpui::TestAppContext) {
+ let (pixels, side) = render_div_fixture(cx, square_fixture);
+ assert_eq!(pixel(&pixels, side, side / 2, side / 2), [0, 0, 0, 255]);
+ assert_eq!(
+ pixel(&pixels, side, 1, 1),
+ [255, 255, 255, 255],
+ "square child leaked outside rounded overflow container"
+ );
+}
+
+#[gpui::test]
+fn rounded_overflow_visible_preserves_square_child(cx: &mut gpui::TestAppContext) {
+ use gpui::{ParentElement as _, Styled as _};
+
+ let (pixels, side) = render_div_fixture(cx, || {
+ gpui::div().size(px(64.)).bg(gpui::white()).child(
+ gpui::div()
+ .size(px(64.))
+ .rounded(px(24.))
+ .child(gpui::div().size(px(64.)).bg(gpui::black())),
+ )
+ });
+ assert_eq!(pixel(&pixels, side, 1, 1), [0, 0, 0, 255]);
+}
+
+/// The regression in zed#37480: an inset rectangular clip must not inherit
+/// the radius of its parent. Its own corner lies inside the parent's shape.
+#[gpui::test]
+fn rounded_overflow_does_not_round_inset_child(cx: &mut gpui::TestAppContext) {
+ use gpui::{ParentElement as _, Styled as _};
+
+ let (pixels, side) = render_div_fixture(cx, || {
+ gpui::div().size(px(64.)).bg(gpui::white()).child(
+ gpui::div()
+ .size(px(64.))
+ .rounded(px(24.))
+ .overflow_hidden()
+ .p(px(16.))
+ .child(
+ gpui::div()
+ .size(px(32.))
+ .overflow_hidden()
+ .child(gpui::div().size(px(32.)).bg(gpui::black())),
+ ),
+ )
+ });
+ let inset = side / 4;
+ assert_eq!(
+ pixel(&pixels, side, inset + 1, inset + 1),
+ [0, 0, 0, 255],
+ "inset child acquired its ancestor's rounded corner"
+ );
+}
+
+/// Cropping a tall rounded container must not move its bottom corners up
+/// to the bottom edge of the shorter rectangular ancestor.
+#[gpui::test]
+fn rounded_overflow_cropped_child_keeps_original_corners(cx: &mut gpui::TestAppContext) {
+ use gpui::{ParentElement as _, Styled as _};
+
+ let (pixels, side) = render_div_fixture(cx, || {
+ gpui::div().size(px(64.)).bg(gpui::white()).child(
+ gpui::div().w(px(64.)).h(px(32.)).overflow_hidden().child(
+ gpui::div()
+ .w(px(64.))
+ .min_h(px(128.))
+ .rounded(px(24.))
+ .overflow_hidden()
+ .child(gpui::div().w(px(64.)).min_h(px(128.)).bg(gpui::black())),
+ ),
+ )
+ });
+ assert_eq!(
+ pixel(&pixels, side, 1, side / 2 - 2),
+ [0, 0, 0, 255],
+ "ancestor clipping moved the child's bottom rounded corner"
+ );
+ assert_eq!(pixel(&pixels, side, 1, side / 2 + 2), [255, 255, 255, 255]);
+ assert_eq!(pixel(&pixels, side, 1, 1), [255, 255, 255, 255]);
+}
+
+/// These siblings overlap, but their rounded masks belong to separate
+/// branches. The later sibling cannot mask away the earlier one's pixels.
+#[gpui::test]
+fn rounded_overflow_overlapping_siblings_keep_independent_clips(cx: &mut gpui::TestAppContext) {
+ use gpui::{ParentElement as _, Styled as _};
+
+ let (pixels, side) = render_div_fixture(cx, || {
+ gpui::div()
+ .relative()
+ .size(px(64.))
+ .bg(gpui::white())
+ .child(
+ gpui::div()
+ .absolute()
+ .size(px(48.))
+ .rounded(px(16.))
+ .overflow_hidden()
+ .child(gpui::div().size(px(48.)).bg(gpui::red())),
+ )
+ .child(
+ gpui::div()
+ .absolute()
+ .left(px(24.))
+ .top(px(16.))
+ .size(px(32.))
+ .rounded(px(12.))
+ .overflow_hidden()
+ .child(gpui::div().size(px(32.)).bg(gpui::black())),
+ )
+ });
+ assert_eq!(pixel(&pixels, side, side / 2, side / 2), [0, 0, 0, 255]);
+ let exposed = pixel(&pixels, side, side * 3 / 8 + 1, side / 4 + 1);
+ assert!(
+ exposed[0] > 200 && exposed[1] < 100 && exposed[2] < 100 && exposed[3] == 255,
+ "later sibling's corner must reveal the earlier red sibling: {exposed:?}"
+ );
+ assert_eq!(pixel(&pixels, side, 1, 1), [255, 255, 255, 255]);
+}
+
+pub(super) fn render_with(
+ cx: &mut gpui::TestAppContext,
+ build: fn() -> gpui::Div,
+ mut rasterize: impl FnMut(&gpui::Scene, gpui::Size<gpui::DevicePixels>) -> Vec<u8>,
+) -> (Vec<u8>, usize) {
+ use gpui::{AppContext as _, Context, IntoElement, Render};
+
+ struct View(fn() -> gpui::Div);
+ impl Render for View {
+ fn render(&mut self, _: &mut gpui::Window, _: &mut Context<Self>) -> impl IntoElement {
+ (self.0)()
+ }
+ }
+ let window = cx.update(|cx| {
+ cx.open_window(Default::default(), |_, cx| cx.new(|_| View(build)))
+ .unwrap()
+ });
+ window
+ .update(cx, |_, window, cx| {
+ window.draw(cx).clear(cx);
+ let side = (64. * window.scale_factor()) as usize;
+ let size = gpui::size(
+ gpui::DevicePixels(side as i32),
+ gpui::DevicePixels(side as i32),
+ );
+ (rasterize(window.painted_scene(), size), side)
+ })
+ .unwrap()
+}
+
+#[gpui::test]
+fn rounded_overflow_identical_nested_clips_do_not_darken_edges(cx: &mut gpui::TestAppContext) {
+ use gpui::{ParentElement as _, Styled as _};
+
+ let (single, side) = render_div_fixture(cx, || {
+ gpui::div().size(px(64.)).bg(gpui::white()).child(
+ gpui::div()
+ .size(px(64.))
+ .rounded(px(24.))
+ .overflow_hidden()
+ .child(gpui::div().size(px(64.)).bg(gpui::black())),
+ )
+ });
+ let (nested, _) = render_div_fixture(cx, || {
+ gpui::div().size(px(64.)).bg(gpui::white()).child(
+ gpui::div()
+ .size(px(64.))
+ .rounded(px(24.))
+ .overflow_hidden()
+ .child(
+ gpui::div()
+ .size(px(64.))
+ .rounded(px(24.))
+ .overflow_hidden()
+ .child(gpui::div().size(px(64.)).bg(gpui::black())),
+ ),
+ )
+ });
+ assert_same_pixels(&nested, &single, side);
+}
+
+#[gpui::test]
+fn rounded_overflow_clips_paths(cx: &mut gpui::TestAppContext) {
+ use gpui::{ParentElement as _, Styled as _};
+
+ let (pixels, side) = render_div_fixture(cx, || {
+ gpui::div().size(px(64.)).bg(gpui::white()).child(
+ gpui::div()
+ .size(px(64.))
+ .rounded(px(24.))
+ .overflow_hidden()
+ .child(
+ gpui::canvas(
+ |_, _, _| (),
+ |bounds, (), window, _| {
+ let mut path = Path::new(bounds.origin);
+ path.line_to(bounds.top_right());
+ path.line_to(bounds.bottom_right());
+ path.line_to(bounds.bottom_left());
+ path.line_to(bounds.origin);
+ window.paint_path(path, gpui::black());
+ },
+ )
+ .size(px(64.)),
+ ),
+ )
+ });
+ assert_eq!(pixel(&pixels, side, side / 2, side / 2), [0, 0, 0, 255]);
+ assert_eq!(pixel(&pixels, side, 1, 1), [255, 255, 255, 255]);
+}
+
+/// Disjoint path batches may share one intermediate rasterization group.
+/// A clip mask drawn between them must not overwrite the later path's pixels
+/// or leave that later path under the child's clip.
+#[gpui::test]
+fn rounded_overflow_between_path_batches_restores_path_pixels(cx: &mut gpui::TestAppContext) {
+ use gpui::{ParentElement as _, Styled as _};
+
+ let (pixels, side) = render_div_fixture(cx, || {
+ gpui::div()
+ .relative()
+ .size(px(64.))
+ .bg(gpui::white())
+ .child(
+ gpui::canvas(
+ |_, _, _| (),
+ |bounds, (), window, _| {
+ let start = bounds.origin;
+ let mut path = Path::new(start);
+ path.line_to(start + gpui::point(px(16.), px(0.)));
+ path.line_to(start + gpui::point(px(16.), px(16.)));
+ path.line_to(start + gpui::point(px(0.), px(16.)));
+ path.line_to(start);
+ window.paint_path(path, gpui::rgb(0xff0000));
+ },
+ )
+ .absolute()
+ .size(px(64.)),
+ )
+ .child(
+ gpui::div()
+ .absolute()
+ .size(px(64.))
+ .rounded(px(24.))
+ .overflow_hidden()
+ .child(gpui::div().size(px(64.)).bg(gpui::black())),
+ )
+ .child(
+ gpui::canvas(
+ |_, _, _| (),
+ |bounds, (), window, _| {
+ let start = bounds.origin + gpui::point(px(48.), px(0.));
+ let mut path = Path::new(start);
+ path.line_to(start + gpui::point(px(16.), px(0.)));
+ path.line_to(start + gpui::point(px(16.), px(16.)));
+ path.line_to(start + gpui::point(px(0.), px(16.)));
+ path.line_to(start);
+ window.paint_path(path, gpui::rgb(0x00ff00));
+ },
+ )
+ .absolute()
+ .size(px(64.)),
+ )
+ });
+ assert_eq!(pixel(&pixels, side, 1, 1), [255, 0, 0, 255]);
+ assert_eq!(pixel(&pixels, side, side - 2, 1), [0, 255, 0, 255]);
+ assert_eq!(pixel(&pixels, side, side / 2, side / 2), [0, 0, 0, 255]);
+}
diff --git a/third-party/gpui-fast/crates/gpui/src/fast/rounded_clip_scene.rs b/third-party/gpui-fast/crates/gpui/src/fast/rounded_clip_scene.rs
new file mode 100644
index 0000000000..c78beec0d4
--- /dev/null
+++ b/third-party/gpui-fast/crates/gpui/src/fast/rounded_clip_scene.rs
@@ -0,0 +1,133 @@
+//! Transport rounded clip groups through the existing renderer layer graph.
+//! No public primitive or ContentMask layout changes. Texture ids in
+//! 0xF1000000.. identify clip groups, not atlas sprites or scroll tiles. Such a
+//! group's LayerFrame has tile_size=0 and contains its drawable scene plus
+//! a geometry LayerFrame (key u32::MAX). Horizontal radii are on the group
+//! sprite and vertical radii occupy that frame's background RGBA, in
+//! TL/TR/BR/BL order. This frame is metadata and is never composited.
+//! Renderers evaluate the independent padding edges in colour fragments;
+//! rounded groups do not allocate intermediate render targets.
+
+use std::mem;
+
+use crate::fast::rounded_clip::{ClipOperation, Shape};
+use crate::{
+ AtlasTextureId, AtlasTextureKind, AtlasTile, ContentMask, LayerContent, LayerFrame, LayerKey,
+ PolychromeSprite, Rgba, Scene, TileId, scene::PaintOperation,
+};
+
+const CLIP_TEXTURE_BASE: u32 = 0xF100_0000;
+const GEOMETRY_KEY: LayerKey = LayerKey(u32::MAX);
+
+/// Materialize private paint scopes as existing, renderer-readable layer
+/// objects. Preserve the original operation stream for retained paint ranges.
+pub(crate) fn compile(scene: &mut Scene) {
+ if scene.fast_rounded_clip.scopes.is_empty() || scene.fast_rounded_clip.compiled {
+ return;
+ }
+ let operations = mem::take(&mut scene.paint_operations);
+ let layers: Vec<LayerFrame> = scene
+ .layers
+ .frames
+ .iter()
+ .filter(|frame| frame.tile_size != 0)
+ .cloned()
+ .collect();
+ let new_scene = || {
+ let mut scene = Scene::default();
+ scene.layers.frames = layers.clone();
+ scene
+ };
+ let mut stack = vec![new_scene()];
+ let mut shapes = Vec::new();
+ let mut redundant = Vec::new();
+ for operation in &operations {
+ match operation {
+ PaintOperation::FastRoundedClip(ClipOperation::Begin(shape)) => {
+ let already_active = shapes.contains(shape);
+ redundant.push(already_active);
+ shapes.push(*shape);
+ if !already_active {
+ stack.push(new_scene());
+ }
+ }
+ PaintOperation::FastRoundedClip(ClipOperation::End) => {
+ let shape = shapes.pop().expect("balanced rounded clips");
+ if redundant.pop().expect("rounded clip ownership") {
+ continue;
+ }
+ let mut child = stack.pop().expect("rounded clip scene");
+ if child.is_empty() {
+ continue;
+ }
+ child.layers.frames.push(geometry(shape));
+ child.finish();
+ let parent = stack.last_mut().expect("parent rounded clip scene");
+ let key = LayerKey(0x0100_0000 + parent.layers.frames.len() as u32);
+ parent.layers.frames.push(LayerFrame {
+ key,
+ generation: 0,
+ background: Rgba::default(),
+ tile_size: 0,
+ content: LayerContent::from(child),
+ dirty_tiles: Vec::new(),
+ });
+ parent.insert_primitive(PolychromeSprite {
+ order: 0,
+ pad: 0,
+ grayscale: false.into(),
+ opacity: 1.,
+ bounds: shape.bounds,
+ content_mask: ContentMask {
+ bounds: shape.bounds,
+ },
+ corner_radii: shape.radii.map(|radius| radius.width),
+ tile: AtlasTile {
+ texture_id: AtlasTextureId {
+ index: CLIP_TEXTURE_BASE + key.0 - 0x0100_0000,
+ kind: AtlasTextureKind::Polychrome,
+ },
+ tile_id: TileId(0),
+ padding: 0,
+ bounds: Default::default(),
+ },
+ });
+ }
+ PaintOperation::Primitive(primitive) => stack
+ .last_mut()
+ .unwrap()
+ .insert_primitive(primitive.clone()),
+ PaintOperation::StartLayer(bounds) => stack.last_mut().unwrap().push_layer(*bounds),
+ PaintOperation::EndLayer => stack.last_mut().unwrap().pop_layer(),
+ }
+ }
+ assert!(
+ shapes.is_empty() && stack.len() == 1,
+ "balanced rounded clips"
+ );
+ let mut compiled = stack.pop().unwrap();
+ compiled.paint_operations = operations;
+ compiled.fast_rounded_clip = mem::take(&mut scene.fast_rounded_clip);
+ compiled.fast_rounded_clip.compiled = true;
+ compiled.sort_scratch = mem::take(&mut scene.sort_scratch);
+ *scene = compiled;
+}
+
+fn geometry(shape: Shape) -> LayerFrame {
+ let r = shape.radii;
+ LayerFrame {
+ key: GEOMETRY_KEY,
+ generation: 0,
+ // Private analytic geometry: horizontal radii are on the group sprite;
+ // this non-composited frame carries the corresponding vertical radii.
+ background: Rgba {
+ r: r.top_left.height.0,
+ g: r.top_right.height.0,
+ b: r.bottom_right.height.0,
+ a: r.bottom_left.height.0,
+ },
+ tile_size: 0,
+ content: Scene::default().into(),
+ dirty_tiles: Vec::new(),
+ }
+}
diff --git a/third-party/gpui-fast/crates/gpui/src/fast/scene.rs b/third-party/gpui-fast/crates/gpui/src/fast/scene.rs
index edc96b2619..2f2a4a4ad1 100644
--- a/third-party/gpui-fast/crates/gpui/src/fast/scene.rs
+++ b/third-party/gpui-fast/crates/gpui/src/fast/scene.rs
@@ -183,6 +183,9 @@ impl Scene {
lines.push(format!("layer {bounds:?}"))
}
crate::PaintOperation::EndLayer => lines.push("end layer".into()),
+ crate::PaintOperation::FastRoundedClip(clip) => {
+ lines.push(format!("rounded clip {clip:?}"))
+ }
crate::PaintOperation::Primitive(..) => {}
}
}
@@ -226,6 +229,7 @@ impl Scene {
/// numbers from zero, would interleave them.
#[inline]
pub(crate) fn sort_in_drawing_order(scene: &mut Scene) {
+ crate::fast::rounded_clip_scene::compile(scene);
let scratch = &mut scene.sort_scratch;
macro_rules! sort {
($field:ident, $order:expr) => {
diff --git a/third-party/gpui-fast/crates/gpui/src/fast/stats.rs b/third-party/gpui-fast/crates/gpui/src/fast/stats.rs
index 5f1c0348c3..c035010bae 100644
--- a/third-party/gpui-fast/crates/gpui/src/fast/stats.rs
+++ b/third-party/gpui-fast/crates/gpui/src/fast/stats.rs
@@ -1,7 +1,8 @@
//! `LayoutStats`: counters and timings describing the work a frame did, for benchmarks and profiling.
use crate::{TaffyLayoutEngine, Window};
-use std::time::{Duration, Instant};
+use scheduler::Instant;
+use std::time::Duration;
/// Counters describing the work the layout engine performed, for benchmarking
/// and profiling.
diff --git a/third-party/gpui-fast/crates/gpui/src/fast/tests/layers.rs b/third-party/gpui-fast/crates/gpui/src/fast/tests/layers.rs
index caf7150df1..063b08b2a0 100644
--- a/third-party/gpui-fast/crates/gpui/src/fast/tests/layers.rs
+++ b/third-party/gpui-fast/crates/gpui/src/fast/tests/layers.rs
@@ -225,6 +225,9 @@ fn content_in_parts_draws_each_tile_as_one_scene_does() {
PaintOperation::Primitive(primitive) => whole.insert_primitive(primitive.clone()),
PaintOperation::StartLayer(bounds) => whole.push_layer(*bounds),
PaintOperation::EndLayer => whole.pop_layer(),
+ PaintOperation::FastRoundedClip(clip) => {
+ crate::fast::rounded_clip::record(&mut whole, *clip)
+ }
}
}
whole.finish();
@@ -239,6 +242,7 @@ fn content_in_parts_draws_each_tile_as_one_scene_does() {
}
PaintOperation::StartLayer(bounds) => PaintOperation::StartLayer(*bounds),
PaintOperation::EndLayer => PaintOperation::EndLayer,
+ PaintOperation::FastRoundedClip(clip) => PaintOperation::FastRoundedClip(*clip),
})
.collect();
LayerPart {
@@ -3685,6 +3689,65 @@ mod input {
assert_eq!(drags_with, drags(cx, without));
}
+ /// A 200 × 100 px scroll container of forty 20 px rows, 100 px wide, at
+ /// the top left of the window, each row found in tests as `row-<index>`.
+ struct LabelledRows {
+ handle: ScrollHandle,
+ }
+
+ impl Render for LabelledRows {
+ fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+ div().size_full().bg(rgb(0xffffff)).child(
+ div()
+ .id("scroller")
+ .overflow_y_scroll()
+ .track_scroll(&self.handle)
+ .w(px(200.))
+ .h(px(100.))
+ .children((0..ROWS).map(|index| {
+ div()
+ .debug_selector(move || format!("row-{index}"))
+ .w(px(100.))
+ .h(px(ROW_HEIGHT))
+ .bg(rgb(0x100000 + index as u32 * 0x10))
+ })),
+ )
+ }
+ }
+
+ /// The content of a layer composited on frame after frame is found
+ /// where it shows on each of them.
+ #[crate::test]
+ fn a_composited_layer_s_content_is_found_where_it_shows(cx: &mut TestAppContext) {
+ if !crate::fast::layers::COMPILED {
+ return;
+ }
+ let handle = cx.add_window(|_, _| LabelledRows {
+ handle: ScrollHandle::new(),
+ });
+ let window: AnyWindowHandle = handle.into();
+ draw(cx, window);
+ draw(cx, window);
+ let mut composited = 0;
+ for step in 0..8 {
+ if scroll(cx, window, -7.) == Some(Decision::Composite) {
+ composited += 1;
+ }
+ let offset = handle
+ .update(cx, |page, _, _| page.handle.offset())
+ .unwrap();
+ let found = with_window(cx, window, |window, _| {
+ window.rendered_frame.debug_bounds.get("row-3").copied()
+ });
+ assert_eq!(
+ found.map(|bounds| bounds.origin),
+ Some(point(px(0.), px(3. * ROW_HEIGHT) + offset.y)),
+ "step {step}, scrolled to {offset:?}"
+ );
+ }
+ assert!(composited >= 4, "the layer was composited ({composited})");
+ }
+
#[crate::test]
fn wheel_events_do_not_rebuild(cx: &mut TestAppContext) {
if !crate::fast::layers::COMPILED {
diff --git a/third-party/gpui-fast/crates/gpui/src/fast/tests/layers_lists.rs b/third-party/gpui-fast/crates/gpui/src/fast/tests/layers_lists.rs
index d34a4003e3..61b60b7cab 100644
--- a/third-party/gpui-fast/crates/gpui/src/fast/tests/layers_lists.rs
+++ b/third-party/gpui-fast/crates/gpui/src/fast/tests/layers_lists.rs
@@ -847,9 +847,9 @@ mod list {
wheel, wheel_deltas, with_window,
};
use crate::{
- AnyWindowHandle, AppContext as _, Context, Entity, IntoElement, ListAlignment, ListState,
- ParentElement as _, Render, Styled as _, TestAppContext, Window, WindowHandle, div,
- prelude::FluentBuilder as _, px, rgb,
+ AnyWindowHandle, AppContext as _, Context, Entity, InteractiveElement as _, IntoElement,
+ ListAlignment, ListState, ParentElement as _, Render, Styled as _, TestAppContext, Window,
+ WindowHandle, div, prelude::FluentBuilder as _, px, rgb,
};
use std::{cell::RefCell, collections::BTreeSet, rc::Rc};
@@ -2645,6 +2645,91 @@ mod list {
let composited = compare_with_layers_off(cx, with, without, &[-7.; 8], "widened");
assert!(composited > 0, "the layer was composited");
}
+
+ /// A white panel holding a list of rows of varying heights, 100 px tall,
+ /// each row found in tests as `row-<ix>`.
+ struct LabelledListPage {
+ state: ListState,
+ }
+
+ impl Render for LabelledListPage {
+ fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+ div().size_full().bg(rgb(0xffffff)).child(
+ crate::list(self.state.clone(), move |row, _, _| {
+ div()
+ .debug_selector(move || format!("row-{row}"))
+ .w(px(VIEWPORT_WIDTH))
+ .h(px(row_height(row)))
+ .bg(row_color(row))
+ .into_any_element()
+ })
+ .w(px(VIEWPORT_WIDTH))
+ .h(px(VIEWPORT_HEIGHT)),
+ )
+ }
+ }
+
+ /// The rows of the list in `window` that its viewport shows, as the
+ /// frame last drawn records them for tests to find.
+ fn rows_shown(cx: &mut TestAppContext, window: AnyWindowHandle) -> Vec<(usize, String)> {
+ with_window(cx, window, |window, _| {
+ let viewport = crate::Bounds::new(
+ crate::point(px(0.), px(0.)),
+ crate::size(px(VIEWPORT_WIDTH), px(VIEWPORT_HEIGHT)),
+ );
+ let mut rows: Vec<(usize, String)> = window
+ .rendered_frame
+ .debug_bounds
+ .iter()
+ .filter_map(|(selector, bounds)| {
+ let row = selector.strip_prefix("row-")?.parse().ok()?;
+ viewport
+ .intersects(bounds)
+ .then(|| (row, format!("{bounds:?}")))
+ })
+ .collect();
+ rows.sort();
+ rows
+ })
+ }
+
+ /// The rows a list's layer keeps on a frame that only scrolled it are
+ /// found where they are drawn, as a list without a layer finds them.
+ #[crate::test]
+ fn rows_a_layer_keeps_are_found_where_they_are_drawn(cx: &mut TestAppContext) {
+ if !crate::fast::layers::COMPILED {
+ return;
+ }
+ let open = |cx: &mut TestAppContext| -> AnyWindowHandle {
+ let state = ListState::new(300, ListAlignment::Top, px(0.)).measure_all();
+ let window = cx.add_window(move |_, _| LabelledListPage { state });
+ open_at(cx, window.into(), 1.);
+ window.into()
+ };
+ let with_layers = open(cx);
+ let without_layers = open(cx);
+ with_window(cx, without_layers, |window, _| {
+ window.set_scroll_layers(false)
+ });
+ draw(cx, without_layers);
+
+ let extended = extended_frames();
+ for (step, dy) in wheel_deltas(40).into_iter().enumerate() {
+ wheel(cx, with_layers, dy);
+ wheel(cx, without_layers, dy);
+ let expected = rows_shown(cx, without_layers);
+ assert!(!expected.is_empty(), "step {step}: rows are shown");
+ assert_eq!(
+ rows_shown(cx, with_layers),
+ expected,
+ "step {step}, scrolled by {dy}"
+ );
+ }
+ assert!(
+ extended_frames() - extended > 20,
+ "frames kept the rows the layer held"
+ );
+ }
}
/// Lists whose rows hand the frame more than what they draw: hitboxes,
@@ -2655,9 +2740,9 @@ mod rows {
expanded_quads, held_rows, open_at, row_color, wheel, with_window,
};
use crate::{
- AnyWindowHandle, AppContext as _, Bounds, Context, Entity, IntoElement, ListAlignment,
- ListState, ParentElement as _, Pixels, Render, StatefulInteractiveElement as _,
- Styled as _, TestAppContext, Window, div, px, rgb,
+ AnyWindowHandle, AppContext as _, Bounds, Context, CursorStyle, Entity, IntoElement,
+ ListAlignment, ListState, ParentElement as _, Pixels, Render,
+ StatefulInteractiveElement as _, Styled as _, TestAppContext, Window, div, px, rgb,
};
use std::{cell::RefCell, collections::BTreeSet, rc::Rc};
@@ -2887,10 +2972,10 @@ mod rows {
}
}
- /// While a wheel scrolls rows under a still pointer, hover is held
- /// still: nothing is hovered, no row is rendered again for its hover,
- /// and the list stays composited on every frame. A pointer that moves
- /// hovers what is under it again at once.
+ /// While a wheel scrolls rows under a still pointer, hover styling is held
+ /// still: no row is rendered again for its hover, and the list stays
+ /// composited on every frame. The hit test stays the geometry under the
+ /// pointer. A pointer that moves hovers what is under it again at once.
#[crate::test]
fn hover_held_still_while_the_wheel_scrolls_renders_no_row_for_it(cx: &mut TestAppContext) {
if !crate::fast::layers::COMPILED {
@@ -2912,8 +2997,11 @@ mod rows {
"uniform {uniform}, frame {frame}"
);
assert!(
- with_window(cx, window, |window, _| window.mouse_hit_test.ids.is_empty()),
- "uniform {uniform}, frame {frame}: nothing is hovered"
+ with_window(cx, window, |window, _| {
+ !window.mouse_hit_test.ids.is_empty()
+ && crate::fast::layers::hover_freeze::frozen(window)
+ }),
+ "uniform {uniform}, frame {frame}: the geometry stays while hover is held still"
);
}
assert_eq!(
@@ -2934,15 +3022,149 @@ mod rows {
});
draw(cx, window);
assert!(
- with_window(cx, window, |window, _| !window
- .mouse_hit_test
- .ids
- .is_empty()),
+ with_window(cx, window, |window, _| {
+ window
+ .rendered_frame
+ .hitboxes
+ .iter()
+ .any(|hitbox| hitbox.is_hovered(window))
+ }),
"uniform {uniform}: the moved pointer hovers the row under it"
);
}
}
+ /// The platform cursor stays the text cursor while a wheel scrolls under a
+ /// still pointer. Hover styling is held still over that text. Moving the
+ /// pointer off the text sets the arrow.
+ #[crate::test]
+ fn the_cursor_follows_the_text_under_a_still_pointer_while_hover_is_frozen(
+ cx: &mut TestAppContext,
+ ) {
+ struct TextPage;
+
+ impl Render for TextPage {
+ fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+ div()
+ .size_full()
+ .child(div().w(px(40.)).h(px(40.)).cursor(CursorStyle::IBeam))
+ }
+ }
+
+ let window = cx.add_window(|_, _| TextPage).into();
+ with_window(cx, window, |window, _| {
+ window.set_owns_pointer(true);
+ window.set_hover_freeze(true);
+ });
+ open_at(cx, window, 1.);
+ assert_eq!(
+ cx.test_platform().active_cursor_style(),
+ CursorStyle::IBeam,
+ "the text under the pointer sets the cursor before the wheel"
+ );
+
+ for frame in 0..8 {
+ wheel(cx, window, -7.);
+ assert_eq!(
+ cx.test_platform().active_cursor_style(),
+ CursorStyle::IBeam,
+ "frame {frame}: the cursor stays the text cursor while hover is held still"
+ );
+ assert!(
+ with_window(cx, window, |window, _| {
+ !window.mouse_hit_test.ids.is_empty()
+ && crate::fast::layers::hover_freeze::frozen(window)
+ }),
+ "frame {frame}: the geometry stays while hover is held still"
+ );
+ }
+
+ with_window(cx, window, |window, cx| {
+ window.dispatch_event(
+ crate::PlatformInput::MouseMove(crate::MouseMoveEvent {
+ position: crate::point(px(80.), px(80.)),
+ pressed_button: None,
+ modifiers: Default::default(),
+ }),
+ cx,
+ );
+ });
+ draw(cx, window);
+ assert_eq!(
+ cx.test_platform().active_cursor_style(),
+ CursorStyle::Arrow,
+ "the pointer off the text sets the arrow"
+ );
+ }
+
+ /// A click under a still pointer right after the wheel reaches its
+ /// handler. Hover styling is held still while frames are drawn; events
+ /// dispatched between them read the geometry under the pointer.
+ #[crate::test]
+ fn a_click_under_a_still_pointer_right_after_the_wheel_reaches_its_handler(
+ cx: &mut TestAppContext,
+ ) {
+ use crate::InteractiveElement as _;
+
+ struct ClickPage {
+ clicks: Rc<RefCell<usize>>,
+ }
+
+ impl Render for ClickPage {
+ fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+ let clicks = self.clicks.clone();
+ div().size_full().child(
+ div()
+ .id("button")
+ .w(px(40.))
+ .h(px(40.))
+ .on_click(move |_, _, _| *clicks.borrow_mut() += 1),
+ )
+ }
+ }
+
+ let clicks = Rc::new(RefCell::new(0));
+ let window = cx
+ .add_window({
+ let clicks = clicks.clone();
+ move |_, _| ClickPage { clicks }
+ })
+ .into();
+ with_window(cx, window, |window, _| window.set_hover_freeze(true));
+ open_at(cx, window, 1.);
+ wheel(cx, window, -7.);
+ assert!(
+ with_window(cx, window, |window, _| {
+ crate::fast::layers::hover_freeze::frozen(window)
+ }),
+ "the wheel holds hover still"
+ );
+
+ let position = crate::point(px(20.), px(20.));
+ with_window(cx, window, |window, cx| {
+ window.dispatch_event(
+ crate::PlatformInput::MouseDown(crate::MouseDownEvent {
+ button: crate::MouseButton::Left,
+ position,
+ modifiers: Default::default(),
+ click_count: 1,
+ first_mouse: false,
+ }),
+ cx,
+ );
+ window.dispatch_event(
+ crate::PlatformInput::MouseUp(crate::MouseUpEvent {
+ button: crate::MouseButton::Left,
+ position,
+ modifiers: Default::default(),
+ click_count: 1,
+ }),
+ cx,
+ );
+ });
+ assert_eq!(*clicks.borrow(), 1, "the click reached its handler");
+ }
+
/// A list whose rows take input (a hover style: a hitbox, a listener,
/// element state) is composited from its layer while it scrolls under a
/// still pointer, its rows' records carried from frame to frame; the
diff --git a/third-party/gpui-fast/crates/gpui/src/fast/tests/layout.rs b/third-party/gpui-fast/crates/gpui/src/fast/tests/layout.rs
index e377450426..8f1365fa7e 100644
--- a/third-party/gpui-fast/crates/gpui/src/fast/tests/layout.rs
+++ b/third-party/gpui-fast/crates/gpui/src/fast/tests/layout.rs
@@ -374,6 +374,79 @@ fn a_node_adopted_from_another_parent_keeps_its_position() {
);
}
+/// Lays out a child under one parent, then has another parent take it in
+/// the next frame while the first is kept without being requested. Returns
+/// the engine with the child, the parent it was taken from and the one that
+/// took it.
+fn child_taken_from_a_kept_parent() -> (crate::TaffyLayoutEngine, [crate::LayoutId; 3]) {
+ let mut engine = crate::TaffyLayoutEngine::new();
+ let child = request(&mut engine, 1, &[]);
+ let original = request(&mut engine, 2, &[child]);
+ engine.clear();
+
+ assert_eq!(request(&mut engine, 1, &[]), child);
+ let adopter = request(&mut engine, 3, &[child]);
+ engine.keep_retained(&[2]);
+ engine.clear();
+ assert!(engine.taffy.children(original.into()).unwrap().is_empty());
+ assert_eq!(engine.taffy.parent(child.into()), Some(adopter.into()));
+ (engine, [child, original, adopter])
+}
+
+fn request(
+ engine: &mut crate::TaffyLayoutEngine,
+ key: u64,
+ children: &[crate::LayoutId],
+) -> crate::LayoutId {
+ engine.request_layout(Some(key), crate::Style::default(), px(14.), 1., children)
+}
+
+/// A parent requested again with the child another parent took from it
+/// takes the child back, though it asks for the children it asked for
+/// before, and is left alone once it has it.
+#[test]
+fn a_parent_requested_again_takes_back_a_child_taken_from_it() {
+ let (mut engine, [child, original, adopter]) = child_taken_from_a_kept_parent();
+
+ let writes = engine.retention.stats.children_writes;
+ assert_eq!(request(&mut engine, 1, &[]), child);
+ assert_eq!(request(&mut engine, 2, &[child]), original);
+ engine.clear();
+ assert_eq!(engine.retention.stats.children_writes, writes + 1);
+ assert_eq!(
+ engine.taffy.children(original.into()).unwrap(),
+ [child.into()]
+ );
+ assert!(engine.layout_reaches(original, child));
+ assert!(!engine.layout_reaches(adopter, child));
+
+ let changes = engine.layout_changes();
+ assert_eq!(request(&mut engine, 1, &[]), child);
+ assert_eq!(request(&mut engine, 2, &[child]), original);
+ engine.clear();
+ assert_eq!(engine.retention.stats.children_writes, writes + 1);
+ assert_eq!(engine.layout_changes(), changes);
+}
+
+/// A subtree whose parent lost a child to another parent cannot be kept as
+/// it is, since the parent no longer holds the child it was laid out with.
+/// It is laid out again instead, which gives the child back, and can be
+/// kept from then on.
+#[test]
+fn a_subtree_missing_a_taken_child_is_not_kept() {
+ let (mut engine, [child, original, _]) = child_taken_from_a_kept_parent();
+
+ assert!(!engine.try_keep_retained(&[1, 2]));
+ assert_eq!(request(&mut engine, 1, &[]), child);
+ assert_eq!(request(&mut engine, 2, &[child]), original);
+ engine.clear();
+ assert!(engine.layout_reaches(original, child));
+
+ assert!(engine.try_keep_retained(&[1, 2]));
+ engine.clear();
+ assert!(engine.layout_reaches(original, child));
+}
+
/// Rows of a uniform list five rows tall, scrolled to `scroll_top`.
struct ScrolledRows {
row_ids: Vec<u64>,
diff --git a/third-party/gpui-fast/crates/gpui/src/fast/tests/retained.rs b/third-party/gpui-fast/crates/gpui/src/fast/tests/retained.rs
index aaebc7bb1f..3d95f6969c 100644
--- a/third-party/gpui-fast/crates/gpui/src/fast/tests/retained.rs
+++ b/third-party/gpui-fast/crates/gpui/src/fast/tests/retained.rs
@@ -2721,3 +2721,84 @@ fn a_view_written_inside_a_cached_view_is_rendered_again_with_it() {
.unwrap();
assert_eq!(red, backgrounds(&mut cx, window.into()));
}
+
+/// A view that draws a box tests find by its selector, counting its builds.
+struct Labelled {
+ builds: Rc<Cell<usize>>,
+}
+
+impl Render for Labelled {
+ fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+ self.builds.set(self.builds.get() + 1);
+ div()
+ .debug_selector(|| "kept".into())
+ .size(px(40.))
+ .bg(crate::blue())
+ }
+}
+
+/// A root holding a [`Labelled`] view, and a box of its own while `shown`.
+struct Shelf {
+ kept: Entity<Labelled>,
+ shown: bool,
+}
+
+impl Render for Shelf {
+ fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+ div()
+ .size(px(300.))
+ .child(self.kept.clone())
+ .when(self.shown, |this| {
+ this.child(
+ div()
+ .debug_selector(|| "gone".into())
+ .size(px(20.))
+ .bg(crate::red()),
+ )
+ })
+ }
+}
+
+/// An element the frame no longer draws is gone from the frame's debug
+/// bounds, though a view beside it was drawn again from the last frame, and
+/// that view's elements are still found where they are drawn.
+#[test]
+fn an_element_no_longer_drawn_leaves_the_debug_bounds_of_a_retained_frame() {
+ let mut cx = TestAppContext::single();
+ let builds = Rc::new(Cell::new(0));
+ let window = cx.add_window({
+ let builds = builds.clone();
+ move |_, cx| Shelf {
+ kept: cx.new(|_| Labelled { builds }),
+ shown: true,
+ }
+ });
+ let bounds = |cx: &mut TestAppContext, selector: &str| {
+ cx.update_window(window.into(), |_, window, cx| {
+ window.draw(cx).clear(cx);
+ window.rendered_frame.debug_bounds.get(selector).copied()
+ })
+ .unwrap()
+ };
+ let kept = bounds(&mut cx, "kept").expect("the view's box is drawn");
+ assert!(bounds(&mut cx, "gone").is_some(), "the root's box is drawn");
+ let builds_before = builds.get();
+
+ window
+ .update(&mut cx, |shelf, _, cx| {
+ shelf.shown = false;
+ cx.notify();
+ })
+ .unwrap();
+ assert_eq!(
+ bounds(&mut cx, "gone"),
+ None,
+ "the root's box is no longer drawn"
+ );
+ assert_eq!(
+ builds.get(),
+ builds_before,
+ "the view beside it was drawn from the last frame"
+ );
+ assert_eq!(bounds(&mut cx, "kept"), Some(kept));
+}
diff --git a/third-party/gpui-fast/crates/gpui/src/fast/tests/support.rs b/third-party/gpui-fast/crates/gpui/src/fast/tests/support.rs
index 696d70fa47..41077e68c9 100644
--- a/third-party/gpui-fast/crates/gpui/src/fast/tests/support.rs
+++ b/third-party/gpui-fast/crates/gpui/src/fast/tests/support.rs
@@ -3,10 +3,30 @@
use std::{cell::RefCell, rc::Rc, sync::Arc};
use crate::{
- App, BackgroundExecutor, ForegroundExecutor, Platform as _, PlatformTextSystem, TestAppContext,
- TestDispatcher, TestPlatform, TextSystem, app::GpuiMode,
+ App, BackgroundExecutor, CursorStyle, ForegroundExecutor, Platform as _, PlatformTextSystem,
+ TestAppContext, TestDispatcher, TestPlatform, TextSystem, Window, app::GpuiMode,
};
+impl TestPlatform {
+ /// The cursor a window last set on the platform.
+ pub(crate) fn active_cursor_style(&self) -> CursorStyle {
+ *self.active_cursor.lock()
+ }
+}
+
+impl Window {
+ /// Marks this window as owning the pointer.
+ ///
+ /// The test platform reports a window as neither active nor hovered, and
+ /// the frame-end cursor update writes the platform cursor only while the
+ /// window owns the pointer: the active window on macOS, the hovered
+ /// window elsewhere.
+ pub(crate) fn set_owns_pointer(&mut self, owns: bool) {
+ self.active.set(owns);
+ self.hovered.set(owns);
+ }
+}
+
impl TestAppContext {
/// Creates a `TestAppContext` whose platform lays out and rasterizes text
/// with the given text system rather than the no-op one.
diff --git a/third-party/gpui-fast/crates/gpui/src/gestures.rs b/third-party/gpui-fast/crates/gpui/src/gestures.rs
index 653437e453..99e08363cd 100644
--- a/third-party/gpui-fast/crates/gpui/src/gestures.rs
+++ b/third-party/gpui-fast/crates/gpui/src/gestures.rs
@@ -1,14 +1,10 @@
//! Touch gesture recognition vocabulary.
//!
-//! GPUI recognizes gestures from raw [`TouchEvent`](crate::TouchEvent)s in a
-//! single, portable arena in gpui core: recognizers compete for in-flight
-//! touches, winners claim them, and losers are cancelled. Recognized gestures
-//! are surfaced through *existing* semantic events wherever possible, a tap
-//! becomes [`ClickEvent::Touch`](crate::ClickEvent), a pan becomes
-//! [`ScrollWheelEvent`](crate::ScrollWheelEvent)s carrying a
-//! [`TouchPhase`](crate::TouchPhase), and a pinch becomes
-//! [`PinchEvent`](crate::PinchEvent)s — so components written against
-//! `on_click` and scroll containers work untouched on mobile.
+//! GPUI recognizes gestures from raw [`TouchEvent`](crate::TouchEvent)s using
+//! a portable recognizer. Taps synthesize mouse presses and releases so
+//! existing click listeners and text selection work on mobile. Pans produce
+//! [`ScrollWheelEvent`](crate::ScrollWheelEvent)s, including post-release
+//! momentum. Long presses and touch drags can be claimed by element listeners.
use std::collections::VecDeque;
use std::mem;
@@ -449,7 +445,7 @@ mod friction_spline {
/// portable recognizers.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct GestureKinds {
- /// Tap (and multi-tap), surfaced as [`ClickEvent::Touch`](crate::ClickEvent).
+ /// Tap (and multi-tap), synthesized as mouse presses by the portable recognizer.
pub tap: bool,
/// Long press, surfaced as [`LongPressEvent`].
pub long_press: bool,
@@ -624,6 +620,10 @@ enum TouchGestureState {
touch: ActiveTouch,
axis: Axis,
},
+ /// The touch caught a fling: it is a pan from its first pixel, and can
+ /// never be a tap, but its axis is its own — decided by its first
+ /// movement, not inherited from the fling it stopped.
+ Caught(ActiveTouch),
LongPressing(ActiveTouch),
TouchDragging(ActiveTouch),
}
@@ -660,7 +660,6 @@ struct Momentum {
position: Point<Pixels>,
/// Unit vector of the release velocity.
direction: Point<f32>,
- axis: Axis,
/// Release speed in pixels per second.
speed: f32,
started_at: Instant,
@@ -705,9 +704,9 @@ impl TouchGestureRecognizer {
Point::default(),
TouchPhase::Ended,
)));
- Some(momentum.axis)
+ true
} else {
- None
+ false
};
if matches!(self.state, TouchGestureState::Idle) {
let mut velocity_tracker = VelocityTracker::default();
@@ -720,18 +719,20 @@ impl TouchGestureRecognizer {
last_movement: Point::default(),
velocity_tracker,
};
- if let Some(axis) = caught_fling {
+ if caught_fling {
// A touch that catches a fling is a drag from the
// first pixel: waiting out the slop would freeze the
// content mid-scroll and then jump. It can also never
// be a tap; releasing it just leaves the content
- // stopped, as on Android and iOS.
+ // stopped, as on Android and iOS. Which way it pans
+ // is its own to decide: a finger that stops a
+ // sideways fling and moves up scrolls up.
recognized.push(RecognizedTouchGesture::Scroll(scroll_event(
touch.start_position,
Point::default(),
TouchPhase::Started,
)));
- self.state = TouchGestureState::Panning { touch, axis };
+ self.state = TouchGestureState::Caught(touch);
} else {
self.state = TouchGestureState::Pending {
touch,
@@ -783,6 +784,18 @@ impl TouchGestureRecognizer {
};
}
}
+ TouchGestureState::Caught(touch) if touch.id == event.id => {
+ let accumulated = event.position - touch.start_position;
+ if accumulated == Point::default() {
+ self.state = TouchGestureState::Caught(touch);
+ } else {
+ // The first movement picks the axis; from here on this
+ // is an ordinary pan, so let the pan arm emit it.
+ let axis = dominant_axis(accumulated);
+ self.state = TouchGestureState::Panning { touch, axis };
+ return self.handle_event_at(event, now);
+ }
+ }
TouchGestureState::Panning { mut touch, axis } if touch.id == event.id => {
let mut raw_delta = event.position - touch.last_position;
lock_delta_to_axis(&mut raw_delta, axis);
@@ -867,6 +880,14 @@ impl TouchGestureRecognizer {
},
});
}
+ TouchGestureState::Caught(touch) if touch.id == event.id => {
+ // Never moved: the content it stopped stays where it is.
+ recognized.push(RecognizedTouchGesture::Scroll(scroll_event(
+ touch.start_position,
+ Point::default(),
+ TouchPhase::Ended,
+ )));
+ }
TouchGestureState::Panning { touch, axis } if touch.id == event.id => {
// The release deliberately contributes no velocity
// sample: it usually repeats the last movement's position
@@ -919,7 +940,6 @@ impl TouchGestureRecognizer {
self.momentum = Some(Momentum {
position: touch.start_position,
direction,
- axis,
speed,
started_at: now,
duration,
@@ -951,7 +971,9 @@ impl TouchGestureRecognizer {
},
TouchPhase::Cancelled => match mem::replace(&mut self.state, TouchGestureState::Idle) {
TouchGestureState::Pending { touch, .. } if touch.id == event.id => {}
- TouchGestureState::Panning { touch, .. } if touch.id == event.id => {
+ TouchGestureState::Panning { touch, .. } | TouchGestureState::Caught(touch)
+ if touch.id == event.id =>
+ {
recognized.push(RecognizedTouchGesture::Scroll(scroll_event(
touch.start_position,
Point::default(),
@@ -1867,6 +1889,60 @@ mod tests {
assert_eq!(scroll.touch_phase, TouchPhase::Ended);
}
+ #[test]
+ fn catching_a_fling_takes_the_axis_from_the_new_touch() {
+ let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
+ let now = Instant::now();
+
+ // A fast horizontal pan flings sideways.
+ recognizer.handle_event_at(
+ &touch_event(TouchId(1), TouchPhase::Started, 300., 300.),
+ now,
+ );
+ for step in 1..=3 {
+ recognizer.handle_event_at(
+ &touch_event(
+ TouchId(1),
+ TouchPhase::Moved,
+ 300. - step as f32 * 33.,
+ 300.,
+ ),
+ now + Duration::from_millis(step * 16),
+ );
+ }
+ recognizer.handle_event_at(
+ &touch_event(TouchId(1), TouchPhase::Ended, 200., 300.),
+ now + Duration::from_millis(64),
+ );
+ assert!(recognizer.has_momentum());
+
+ // A finger that catches it and moves up scrolls up: the fling's axis
+ // is not this touch's axis.
+ recognizer.handle_event_at(
+ &touch_event(TouchId(2), TouchPhase::Started, 200., 300.),
+ now + Duration::from_millis(200),
+ );
+ let recognized = recognizer.handle_event_at(
+ &touch_event(TouchId(2), TouchPhase::Moved, 201., 280.),
+ now + Duration::from_millis(216),
+ );
+ let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else {
+ panic!("expected scroll, got {recognized:?}");
+ };
+ assert_eq!(scroll.touch_phase, TouchPhase::Moved);
+ assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(-20.)));
+
+ // And stays on that axis for the rest of the pan.
+ let recognized = recognizer.handle_event_at(
+ &touch_event(TouchId(2), TouchPhase::Moved, 205., 260.),
+ now + Duration::from_millis(232),
+ );
+ let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else {
+ panic!("expected scroll, got {recognized:?}");
+ };
+ assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(-20.)));
+ }
+
#[test]
fn cancelled_pan_emits_cancelled_scroll_and_no_tap() {
let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
diff --git a/third-party/gpui-fast/crates/gpui/src/gpui.rs b/third-party/gpui-fast/crates/gpui/src/gpui.rs
index c59bd80cc1..aa5562885a 100644
--- a/third-party/gpui-fast/crates/gpui/src/gpui.rs
+++ b/third-party/gpui-fast/crates/gpui/src/gpui.rs
@@ -28,6 +28,8 @@ pub(crate) use platform_scheduler::PlatformScheduler;
mod geometry;
mod gestures;
mod global;
+#[cfg(gpui_hot_reload)]
+mod hot_reload;
mod input;
mod inspector;
mod interactive;
@@ -216,6 +218,17 @@ use std::{any::Any, future::Future};
pub use style::*;
pub use styled::*;
pub use subscription::*;
+/// Subsecond entry points for hot-patching callbacks outside GPUI's render boundaries.
+#[cfg(all(
+ feature = "hot-reload",
+ any(
+ target_os = "macos",
+ target_os = "linux",
+ target_os = "windows",
+ target_os = "ios"
+ )
+))]
+pub use subsecond;
pub use svg_renderer::*;
pub(crate) use tab_stop::*;
use taffy::TaffyLayoutEngine;
diff --git a/third-party/gpui-fast/crates/gpui/src/hot_reload.rs b/third-party/gpui-fast/crates/gpui/src/hot_reload.rs
new file mode 100644
index 0000000000..e92c6254bb
--- /dev/null
+++ b/third-party/gpui-fast/crates/gpui/src/hot_reload.rs
@@ -0,0 +1,248 @@
+use crate::{App, AppContext as _, Global, Task};
+use anyhow::{Context as _, Result};
+use async_tungstenite::tungstenite::Message;
+use dioxus_devtools_types::DevserverMsg;
+use futures::StreamExt;
+use std::net::SocketAddr;
+use subsecond::JumpTable;
+
+struct HotReload {
+ _task: Task<()>,
+}
+
+impl Global for HotReload {}
+
+pub(crate) fn init(cx: &mut App) {
+ if cx.has_global::<HotReload>() {
+ return;
+ }
+ let (Some(address), Some(endpoint)) = (
+ dioxus_cli_config::devserver_raw_addr(),
+ dioxus_cli_config::devserver_ws_endpoint(),
+ ) else {
+ log::warn!(
+ "GPUI hot reload is enabled, but no dx development server was found. \
+ Launch the app with `dx serve --hot-patch` to enable hot reloading."
+ );
+ return;
+ };
+
+ let build_id = dioxus_cli_config::build_id();
+ let process_id = std::process::id();
+ // Resolve the executable anchor on the foreground thread before any patch can load.
+ let reference = subsecond::aslr_reference();
+ let endpoint =
+ format!("{endpoint}?aslr_reference={reference}&build_id={build_id}&pid={process_id}");
+ let (sender, receiver) = async_channel::bounded(16);
+ let connection = cx.background_spawn(async move {
+ if let Err(error) = receive_patches(address, endpoint, build_id, process_id, sender).await {
+ log::error!("GPUI hot reload disconnected: {error:#}. Restart dx serve to reconnect.");
+ }
+ });
+ let task = cx.spawn(async move |cx| {
+ let _connection = connection;
+ while let Ok(table) = receiver.recv().await {
+ cx.update(|cx| {
+ // Patches are executable code from the developer's trusted dx server. Applying
+ // them between foreground updates avoids replacing code during a render.
+ match unsafe { subsecond::apply_patch(table) } {
+ Ok(()) => {
+ cx.refresh_windows();
+ log::info!("GPUI hot reload applied");
+ }
+ Err(error) => log::error!("GPUI hot reload failed: {error}"),
+ }
+ });
+ }
+ });
+ cx.set_global(HotReload { _task: task });
+}
+
+async fn receive_patches(
+ address: SocketAddr,
+ endpoint: String,
+ build_id: u64,
+ process_id: u32,
+ sender: async_channel::Sender<JumpTable>,
+) -> Result<()> {
+ let stream = smol::net::TcpStream::connect(address)
+ .await
+ .context("connecting to the dx development server")?;
+ let (mut websocket, _) = async_tungstenite::client_async(endpoint, stream)
+ .await
+ .context("opening the dx development websocket")?;
+ log::info!("GPUI hot reload connected");
+
+ while let Some(message) = websocket.next().await {
+ match message.context("reading the dx development websocket")? {
+ Message::Text(text) => {
+ let message = serde_json::from_str::<DevserverMsg>(&text)
+ .context("decoding a dx development message")?;
+ if let Some(table) = patch_for_process(message, build_id, process_id) {
+ if sender.send(table).await.is_err() {
+ return Ok(());
+ }
+ }
+ }
+ Message::Close(_) => break,
+ _ => {}
+ }
+ }
+ log::info!("GPUI hot reload disconnected");
+ Ok(())
+}
+
+fn patch_for_process(message: DevserverMsg, build_id: u64, process_id: u32) -> Option<JumpTable> {
+ match message {
+ DevserverMsg::HotReload(message)
+ if message.for_build_id == Some(build_id) && message.for_pid == Some(process_id) =>
+ {
+ message.jump_table
+ }
+ DevserverMsg::FullReloadCommand => {
+ log::warn!("GPUI hot reload requires a full application restart");
+ None
+ }
+ _ => None,
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{
+ AnyView, Context, Entity, IntoElement, Render, RenderOnce, StyleRefinement, TestAppContext,
+ ViewElement, Window, div, prelude::*,
+ };
+ use dioxus_devtools_types::HotReloadMsg;
+ use std::{cell::Cell, rc::Rc};
+
+ fn message(build_id: Option<u64>, process_id: Option<u32>) -> DevserverMsg {
+ DevserverMsg::HotReload(HotReloadMsg {
+ for_build_id: build_id,
+ for_pid: process_id,
+ jump_table: Some(JumpTable {
+ lib: "unused-test-patch".into(),
+ map: Default::default(),
+ aslr_reference: 0,
+ new_base_address: 0,
+ ifunc_count: 0,
+ }),
+ ..Default::default()
+ })
+ }
+
+ #[test]
+ fn receives_only_matching_patches_over_the_websocket() -> Result<()> {
+ smol::block_on(async {
+ let listener = smol::net::TcpListener::bind("127.0.0.1:0").await?;
+ let address = listener.local_addr()?;
+ let (sender, receiver) = async_channel::bounded(16);
+ let server = async {
+ let (stream, _) = listener.accept().await?;
+ let mut websocket = async_tungstenite::accept_async(stream).await?;
+ for message in [
+ DevserverMsg::HotPatchStart,
+ DevserverMsg::HotReload(HotReloadMsg::default()),
+ message(Some(3), Some(2)),
+ message(Some(1), Some(3)),
+ message(None, Some(2)),
+ message(Some(1), None),
+ message(Some(1), Some(2)),
+ ] {
+ websocket
+ .send(Message::Text(serde_json::to_string(&message)?.into()))
+ .await?;
+ }
+ websocket.close(None).await?;
+ anyhow::Ok(())
+ };
+ futures::try_join!(
+ server,
+ receive_patches(address, format!("ws://{address}/_dioxus"), 1, 2, sender)
+ )?;
+ assert_eq!(
+ receiver.recv().await?.lib,
+ std::path::Path::new("unused-test-patch")
+ );
+ assert!(receiver.recv().await.is_err());
+ Ok(())
+ })
+ }
+
+ struct Child {
+ count: usize,
+ renders: Rc<Cell<usize>>,
+ }
+
+ impl Render for Child {
+ fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+ self.renders.set(self.renders.get() + 1);
+ div()
+ }
+ }
+
+ struct Component(Rc<Cell<usize>>);
+
+ impl RenderOnce for Component {
+ fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
+ self.0.set(self.0.get() + 1);
+ div()
+ }
+ }
+
+ impl IntoElement for Component {
+ type Element = ViewElement<Self>;
+
+ fn into_element(self) -> Self::Element {
+ ViewElement::new(self)
+ }
+ }
+
+ struct Root {
+ child: Entity<Child>,
+ erased: bool,
+ component_renders: Rc<Cell<usize>>,
+ }
+
+ impl Render for Root {
+ fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+ let element = div().child(Component(self.component_renders.clone()));
+ if self.erased {
+ element.child(AnyView::from(self.child.clone()).cached(StyleRefinement::default()))
+ } else {
+ element.child(self.child.clone().cached(StyleRefinement::default()))
+ }
+ }
+ }
+
+ #[gpui::test]
+ fn refresh_renders_cached_views_without_recreating_state(cx: &mut TestAppContext) {
+ for erased in [false, true] {
+ let renders = Rc::new(Cell::new(0));
+ let component_renders = Rc::new(Cell::new(0));
+ let child = cx.new(|_| Child {
+ count: 7,
+ renders: renders.clone(),
+ });
+ let window = cx.add_window(|_, _| Root {
+ child: child.clone(),
+ erased,
+ component_renders: component_renders.clone(),
+ });
+ cx.update_window(window.into(), |_, window, cx| window.draw(cx).clear(cx))
+ .unwrap();
+ let initial_renders = renders.get();
+ let initial_component_renders = component_renders.get();
+ assert!(initial_renders > 0);
+ assert!(initial_component_renders > 0);
+
+ cx.update(|cx| cx.refresh_windows());
+ cx.update_window(window.into(), |_, window, cx| window.draw(cx).clear(cx))
+ .unwrap();
+ assert!(renders.get() > initial_renders);
+ assert!(component_renders.get() > initial_component_renders);
+ assert_eq!(child.read_with(cx, |child, _| child.count), 7);
+ }
+ }
+}
diff --git a/third-party/gpui-fast/crates/gpui/src/input.rs b/third-party/gpui-fast/crates/gpui/src/input.rs
index 0a5be89210..19ba5312ed 100644
--- a/third-party/gpui-fast/crates/gpui/src/input.rs
+++ b/third-party/gpui-fast/crates/gpui/src/input.rs
@@ -9,6 +9,19 @@ use std::ops::Range;
/// Once your view implements this trait, you can use it to construct an [`ElementInputHandler<V>`].
/// This input handler can then be assigned during paint by calling [`Window::handle_input`].
///
+/// Dismissing the software keyboard can leave the component focused. Call
+/// [`Window::request_virtual_keyboard`] from an editing gesture, such as tapping
+/// the field, to reopen it; repainting alone should not undo a dismissal.
+///
+/// After application-driven edits or selection changes, also call
+/// [`Window::notify_text_input_changed`] or [`Window::notify_text_selection_changed`]
+/// so native text services can invalidate their state.
+///
+/// UIKit text input also needs `text_length_utf16` (or a bounded
+/// `text_input_editable_range`) and native selection updates through
+/// `set_selected_text_range`. Supply `selection_bounds_for_range` for native
+/// selection rectangles; it should return line/run fragments in one batch.
+///
/// See [`InputHandler`] for details on how to implement each method.
pub trait EntityInputHandler: 'static + Sized {
/// See [`InputHandler::text_for_range`] for details
@@ -73,6 +86,17 @@ pub trait EntityInputHandler: 'static + Sized {
cx: &mut Context<Self>,
) -> Option<Bounds<Pixels>>;
+ /// See [`InputHandler::selection_bounds_for_range`] for details.
+ fn selection_bounds_for_range(
+ &mut self,
+ _range_utf16: Range<usize>,
+ _element_bounds: Bounds<Pixels>,
+ _window: &mut Window,
+ _cx: &mut Context<Self>,
+ ) -> Vec<Bounds<Pixels>> {
+ Vec::new()
+ }
+
/// See [`InputHandler::character_index_for_point`] for details
fn character_index_for_point(
&mut self,
@@ -224,6 +248,17 @@ impl<V: EntityInputHandler> InputHandler for ElementInputHandler<V> {
})
}
+ fn selection_bounds_for_range(
+ &mut self,
+ range_utf16: Range<usize>,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Vec<Bounds<Pixels>> {
+ self.view.update(cx, |view, cx| {
+ view.selection_bounds_for_range(range_utf16, self.element_bounds, window, cx)
+ })
+ }
+
fn character_index_for_point(
&mut self,
point: crate::Point<Pixels>,
@@ -310,6 +345,7 @@ mod tests {
move |_, cx| ConfigurationTestView {
focus_handle: cx.focus_handle(),
configuration: custom,
+ accepts_text_input: true,
}
});
let view = window.root(cx).unwrap();
@@ -387,9 +423,95 @@ mod tests {
);
}
+ #[gpui::test]
+ fn text_notifications_require_a_focused_input_and_track_field_changes(cx: &mut TestAppContext) {
+ let window = cx.add_window(|_, cx| ConfigurationTestView {
+ focus_handle: cx.focus_handle(),
+ configuration: TextInputConfiguration::default(),
+ accepts_text_input: true,
+ });
+ let view = window.root(cx).unwrap();
+ let mut test_window = cx.test_window(window.into());
+ let window = AnyWindowHandle::from(window);
+ let first_focus = view.read_with(cx, |view, _| view.focus_handle.clone());
+ cx.update_window(window, |_, window, cx| {
+ window.focus(&first_focus, cx);
+ window.draw(cx).clear(cx);
+ window.notify_text_input_changed(&first_focus);
+ window.notify_text_selection_changed(&first_focus);
+ })
+ .unwrap();
+
+ use crate::PlatformWindow as _;
+ let mut installed_handler = test_window.take_input_handler().unwrap();
+ assert!(installed_handler.query_accepts_focused_text_input());
+ assert_eq!(installed_handler.focus_id(), Some(first_focus.id));
+ let second_focus = cx
+ .update_window(window, |_, window, cx| {
+ let second_focus = view.update(cx, |view, cx| {
+ view.focus_handle = cx.focus_handle();
+ cx.notify();
+ view.focus_handle.clone()
+ });
+ window.focus(&second_focus, cx);
+ second_focus
+ })
+ .unwrap();
+ assert!(!installed_handler.query_accepts_focused_text_input());
+ test_window.set_input_handler(installed_handler);
+ cx.update_window(window, |_, window, cx| {
+ window.draw(cx).clear(cx);
+ window.notify_text_input_changed(&first_focus);
+ })
+ .unwrap();
+ assert_eq!(
+ test_window.text_input_state_changes(),
+ [
+ TextInputStateChange::FocusGained,
+ TextInputStateChange::ContentChanged,
+ TextInputStateChange::SelectionChanged,
+ ]
+ );
+
+ let installed_handler = test_window.take_input_handler().unwrap();
+ assert_eq!(installed_handler.focus_id(), Some(second_focus.id));
+ test_window.set_input_handler(installed_handler);
+ cx.update_window(window, |_, window, cx| {
+ window.draw(cx).clear(cx);
+ window.notify_text_selection_changed(&second_focus);
+ })
+ .unwrap();
+
+ view.update(cx, |view, cx| {
+ view.accepts_text_input = false;
+ cx.notify();
+ });
+ cx.update_window(window, |_, window, cx| {
+ window.draw(cx).clear(cx);
+ window.notify_text_input_changed(&second_focus);
+ window.notify_text_selection_changed(&second_focus);
+ let ordinary_focus = cx.focus_handle();
+ window.focus(&ordinary_focus, cx);
+ window.draw(cx).clear(cx);
+ window.notify_text_input_changed(&ordinary_focus);
+ })
+ .unwrap();
+ assert_eq!(
+ test_window.text_input_state_changes(),
+ [
+ TextInputStateChange::FocusGained,
+ TextInputStateChange::ContentChanged,
+ TextInputStateChange::SelectionChanged,
+ TextInputStateChange::SelectionChanged,
+ TextInputStateChange::FocusLost,
+ ]
+ );
+ }
+
struct ConfigurationTestView {
focus_handle: FocusHandle,
configuration: TextInputConfiguration,
+ accepts_text_input: bool,
}
impl Render for ConfigurationTestView {
@@ -413,6 +535,10 @@ mod tests {
}
impl EntityInputHandler for ConfigurationTestView {
+ fn accepts_text_input(&self, _window: &mut Window, _cx: &mut Context<Self>) -> bool {
+ self.accepts_text_input
+ }
+
fn text_for_range(
&mut self,
_range: std::ops::Range<usize>,
diff --git a/third-party/gpui-fast/crates/gpui/src/key_dispatch.rs b/third-party/gpui-fast/crates/gpui/src/key_dispatch.rs
index ea8325f176..898f4892aa 100644
--- a/third-party/gpui-fast/crates/gpui/src/key_dispatch.rs
+++ b/third-party/gpui-fast/crates/gpui/src/key_dispatch.rs
@@ -1363,6 +1363,14 @@ mod tests {
let count_after_focus_change = *pending_input_changed_count.borrow();
assert!(count_after_focus_change > *count_after_pending_for_assertion.borrow());
});
+ assert_eq!(
+ cx.test_window(cx.window_handle())
+ .text_input_state_changes(),
+ [
+ crate::TextInputStateChange::FocusGained,
+ crate::TextInputStateChange::FocusLost,
+ ]
+ );
cx.update(|window, cx| window.focus(&focus_handle, cx));
cx.simulate_keystrokes("ctrl-b");
@@ -1379,6 +1387,18 @@ mod tests {
assert!(*pending_input_changed_count.borrow() > count_before_blur);
});
+ let temporary_focus = cx.update(|window, cx| {
+ let handle = cx.focus_handle();
+ window.focus(&handle, cx);
+ handle
+ });
+ cx.simulate_keystrokes("ctrl-d");
+ cx.update(|window, _| assert!(window.has_pending_keystrokes()));
+ let count_before_drop = *pending_input_changed_count.borrow();
+ cx.update(|_, _| drop(temporary_focus));
+ cx.update(|window, _| assert!(window.pending_input_is_none()));
+ assert_eq!(*pending_input_changed_count.borrow(), count_before_drop + 1);
+
cx.update(|window, cx| window.disable_focus(cx));
cx.simulate_keystrokes("ctrl-d");
diff --git a/third-party/gpui-fast/crates/gpui/src/platform.rs b/third-party/gpui-fast/crates/gpui/src/platform.rs
index bc3a53ee75..f6f21bc04b 100644
--- a/third-party/gpui-fast/crates/gpui/src/platform.rs
+++ b/third-party/gpui-fast/crates/gpui/src/platform.rs
@@ -1146,6 +1146,10 @@ pub enum AppLifecyclePhase {
Background,
/// Becoming visible again, before input is restored.
Foreground,
+ /// The native scene has disconnected. Its windows may reconnect later.
+ /// This is not a process-termination notification or a guaranteed final
+ /// opportunity to save state.
+ Disconnected,
}
/// Regions of a window that are obscured or reserved by the system.
@@ -2037,6 +2041,14 @@ impl PlatformInputHandler {
}
}
+ /// Identifies the input field registered through [`Window::handle_input`].
+ ///
+ /// This identity survives adapter replacement during repaint. Backends can
+ /// use it to detect a field change without restarting the keyboard session.
+ pub fn focus_id(&self) -> Option<crate::FocusId> {
+ Some(self.focus)
+ }
+
pub(crate) fn presentation_is_admitted(&self, window: &Window) -> bool {
window.input_origin_is_presented(&self.origin)
}
@@ -2170,6 +2182,19 @@ impl PlatformInputHandler {
.flatten()
}
+ pub fn selection_bounds_for_range(&mut self, range_utf16: Range<usize>) -> Vec<Bounds<Pixels>> {
+ self.cx
+ .update(|window, cx| {
+ if window.input_handler_is_admitted(self.focus, &self.origin) {
+ self.handler
+ .selection_bounds_for_range(range_utf16, window, cx)
+ } else {
+ Vec::new()
+ }
+ })
+ .unwrap_or_default()
+ }
+
#[allow(dead_code)]
pub fn apple_press_and_hold_enabled(&mut self) -> bool {
self.handler.apple_press_and_hold_enabled()
@@ -2306,6 +2331,19 @@ impl PlatformInputHandler {
.unwrap_or(true)
}
+ /// Checks eligibility against the current focus, including before the next frame is drawn.
+ ///
+ /// Unlike the legacy acceptance query, this fails closed when the window cannot
+ /// be updated and requires registration through `Window::handle_input`.
+ pub fn query_accepts_focused_text_input(&mut self) -> bool {
+ self.cx
+ .update(|window, cx| {
+ window.input_handler_is_admitted(self.focus, &self.origin)
+ && self.handler.accepts_text_input(window, cx)
+ })
+ .unwrap_or(false)
+ }
+
/// See [`InputHandler::prefers_ime_for_printable_keys`].
///
/// This is not a pure delegation to the handler: while a multi-stroke binding is pending this
@@ -2358,10 +2396,10 @@ pub struct UTF16Selection {
pub reversed: bool,
}
-/// Zed's interface for handling text input from the platform's IME system
-/// This is currently a 1:1 exposure of the NSTextInputClient API:
+/// GPUI's interface for native text input, IME composition, and selection.
+/// Platform backends translate their native text protocols into these operations.
///
-/// <https://developer.apple.com/documentation/appkit/nstextinputclient>
+/// Text offsets and ranges use UTF-16 code units unless stated otherwise.
pub trait InputHandler: 'static {
/// Get the range of the user's currently selected text, if any
/// Corresponds to [selectedRange()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438242-selectedrange)
@@ -2435,7 +2473,7 @@ pub trait InputHandler: 'static {
}
}
- /// Get the bounds of the given document range in screen coordinates
+ /// Get the bounds of the given document range in window coordinates.
/// Corresponds to [firstRect(forCharacterRange:actualRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438240-firstrect)
///
/// This is used for positioning the IME candidate window
@@ -2446,6 +2484,21 @@ pub trait InputHandler: 'static {
cx: &mut App,
) -> Option<Bounds<Pixels>>;
+ /// Returns selection rectangles in document order, in window coordinates.
+ ///
+ /// Return line or shaped-run fragments, not one rectangle per character. The
+ /// first and last rectangles contain the range's start and end respectively.
+ /// Empty means native selection geometry is unavailable; caret/IME geometry can
+ /// still be supplied by `bounds_for_range`.
+ fn selection_bounds_for_range(
+ &mut self,
+ _range_utf16: Range<usize>,
+ _window: &mut Window,
+ _cx: &mut App,
+ ) -> Vec<Bounds<Pixels>> {
+ Vec::new()
+ }
+
/// Get the character offset for the given point in terms of UTF16 characters
///
/// Corresponds to [characterIndexForPoint:](https://developer.apple.com/documentation/appkit/nstextinputclient/characterindex(for:))
@@ -2682,6 +2735,9 @@ pub struct WindowOptions {
/// Tab group name, allows opening the window as a native tab on macOS 10.12+. Windows with the same tabbing identifier will be grouped together.
pub tabbing_identifier: Option<String>,
+
+ /// Whether to render the window using the Display P3 color space on macOS.
+ pub use_display_p3: bool,
}
/// The variables that can be configured when creating a new window
@@ -2747,6 +2803,9 @@ pub struct WindowParams {
#[cfg(target_os = "macos")]
pub tabbing_identifier: Option<String>,
+
+ #[cfg(target_os = "macos")]
+ pub use_display_p3: bool,
}
/// Represents the status of how a window should be opened.
@@ -2808,6 +2867,7 @@ impl Default for WindowOptions {
window_min_size: None,
window_decorations: None,
tabbing_identifier: None,
+ use_display_p3: false,
}
}
}
diff --git a/third-party/gpui-fast/crates/gpui/src/platform/test/platform.rs b/third-party/gpui-fast/crates/gpui/src/platform/test/platform.rs
index 9062c4fe5a..a61e034645 100644
--- a/third-party/gpui-fast/crates/gpui/src/platform/test/platform.rs
+++ b/third-party/gpui-fast/crates/gpui/src/platform/test/platform.rs
@@ -3,12 +3,13 @@ use crate::NoopTextSystem;
#[cfg(any(test, feature = "test-support"))]
use crate::PathPromptOptions;
use crate::{
- ActivityGuard, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
- DisplayId, DummyKeyboardMapper, ForegroundExecutor, Keymap, OwnedMenu, Platform,
- PlatformDisplay, PlatformHeadlessRenderer, PlatformKeyboardLayout, PlatformKeyboardMapper,
- PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
- SharedString, SourceMetadata, SystemNotification, SystemNotificationResponse, Task,
- TestDisplay, TestWindow, ThermalState, WindowAppearance, WindowParams, size,
+ ActivityGuard, AnyWindowHandle, AppLifecyclePhase, BackgroundExecutor, ClipboardItem,
+ CursorStyle, DevicePixels, DisplayId, DummyKeyboardMapper, ForegroundExecutor, Keymap,
+ OwnedMenu, Platform, PlatformDisplay, PlatformHeadlessRenderer, PlatformKeyboardLayout,
+ PlatformKeyboardMapper, PlatformTextSystem, PromptButton, ScreenCaptureFrame,
+ ScreenCaptureSource, ScreenCaptureStream, SharedString, SourceMetadata, SystemNotification,
+ SystemNotificationResponse, Task, TestDisplay, TestWindow, ThermalState, WindowAppearance,
+ WindowParams, size,
};
use anyhow::Result;
#[cfg(any(test, feature = "test-support"))]
@@ -34,7 +35,7 @@ pub(crate) struct TestPlatform {
pub(crate) active_window: RefCell<Option<TestWindow>>,
displays: RefCell<Vec<TestDisplay>>,
displays_changed_callback: RefCell<Option<Box<dyn FnMut()>>>,
- active_cursor: Mutex<CursorStyle>,
+ pub(crate) active_cursor: Mutex<CursorStyle>,
current_clipboard_item: Mutex<Option<ClipboardItem>>,
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
current_primary_item: Mutex<Option<ClipboardItem>>,
@@ -55,6 +56,8 @@ pub(crate) struct TestPlatform {
Option<Box<dyn Fn() -> anyhow::Result<Option<Box<dyn PlatformHeadlessRenderer>>>>>,
weak: Weak<Self>,
menus: RefCell<Vec<OwnedMenu>>,
+ pub(crate) app_lifecycle_callback: RefCell<Option<Box<dyn FnMut(AppLifecyclePhase)>>>,
+ pub(crate) memory_warning_callback: RefCell<Option<Box<dyn FnMut()>>>,
}
#[derive(Clone)]
@@ -225,6 +228,8 @@ impl TestPlatform {
text_system,
headless_renderer_factory,
menus: Default::default(),
+ app_lifecycle_callback: Default::default(),
+ memory_warning_callback: Default::default(),
})
}
@@ -657,6 +662,14 @@ impl Platform for TestPlatform {
fn on_system_wake(&self, _callback: Box<dyn FnMut()>) {}
+ fn on_app_lifecycle(&self, callback: Box<dyn FnMut(AppLifecyclePhase)>) {
+ *self.app_lifecycle_callback.borrow_mut() = Some(callback);
+ }
+
+ fn on_memory_warning(&self, callback: Box<dyn FnMut()>) {
+ *self.memory_warning_callback.borrow_mut() = Some(callback);
+ }
+
fn set_app_identity(&self, identifier: &str, name: &str) {
self.system_notifications.borrow_mut().app_identity =
Some((identifier.to_string().into(), name.to_string().into()));
diff --git a/third-party/gpui-fast/crates/gpui/src/profiler/journal.rs b/third-party/gpui-fast/crates/gpui/src/profiler/journal.rs
index 97d88852d7..d73640fc2e 100644
--- a/third-party/gpui-fast/crates/gpui/src/profiler/journal.rs
+++ b/third-party/gpui-fast/crates/gpui/src/profiler/journal.rs
@@ -440,7 +440,7 @@ impl ForegroundRunnableCounter {
fn finished(&self) {
let _ = self
.0
- .fetch_update(Ordering::AcqRel, Ordering::Acquire, |count| {
+ .try_update(Ordering::AcqRel, Ordering::Acquire, |count| {
count.checked_sub(1)
});
}
diff --git a/third-party/gpui-fast/crates/gpui/src/scene.rs b/third-party/gpui-fast/crates/gpui/src/scene.rs
index 992ba3c80f..11edd0262e 100644
--- a/third-party/gpui-fast/crates/gpui/src/scene.rs
+++ b/third-party/gpui-fast/crates/gpui/src/scene.rs
@@ -51,12 +51,14 @@ pub struct Scene {
pub polychrome_sprites: Vec<PolychromeSprite>,
pub surfaces: Vec<PaintSurface>,
pub(crate) sort_scratch: crate::fast::scene::SortScratch,
+ pub(crate) fast_rounded_clip: crate::fast::rounded_clip::SceneClips,
pub layers: crate::fast::layers::scene::SceneLayers,
}
#[expect(missing_docs)]
impl Scene {
pub fn clear(&mut self) {
+ crate::fast::rounded_clip::clear(self);
self.paint_operations.clear();
self.primitive_bounds.clear();
self.layer_stack.clear();
@@ -148,6 +150,9 @@ impl Scene {
PaintOperation::Primitive(primitive) => self.insert_primitive(primitive.clone()),
PaintOperation::StartLayer(bounds) => self.push_layer(*bounds),
PaintOperation::EndLayer => self.pop_layer(),
+ PaintOperation::FastRoundedClip(fast_clip) => {
+ crate::fast::rounded_clip::record(self, *fast_clip)
+ }
}
}
}
@@ -209,6 +214,7 @@ pub(crate) enum PaintOperation {
Primitive(Primitive),
StartLayer(Bounds<ScaledPixels>),
EndLayer,
+ FastRoundedClip(crate::fast::rounded_clip::ClipOperation),
}
#[derive(Clone)]
@@ -705,10 +711,10 @@ impl Default for TransformationMatrix {
pub struct MonochromeSprite {
pub order: DrawOrder,
pub pad: u32,
+ pub tile: AtlasTile,
pub bounds: Bounds<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
pub color: Hsla,
- pub tile: AtlasTile,
pub transformation: TransformationMatrix,
}
@@ -724,10 +730,10 @@ impl From<MonochromeSprite> for Primitive {
pub struct SubpixelSprite {
pub order: DrawOrder,
pub pad: u32, // align to 8 bytes
+ pub tile: AtlasTile,
pub bounds: Bounds<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
pub color: Hsla,
- pub tile: AtlasTile,
pub transformation: TransformationMatrix,
}
@@ -743,12 +749,12 @@ impl From<SubpixelSprite> for Primitive {
pub struct PolychromeSprite {
pub order: DrawOrder,
pub pad: u32,
+ pub tile: AtlasTile,
pub grayscale: PaddedBool32,
pub opacity: f32,
pub bounds: Bounds<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
pub corner_radii: Corners<ScaledPixels>,
- pub tile: AtlasTile,
}
impl From<PolychromeSprite> for Primitive {
diff --git a/third-party/gpui-fast/crates/gpui/src/style.rs b/third-party/gpui-fast/crates/gpui/src/style.rs
index c9be9ef954..c1e76a2c70 100644
--- a/third-party/gpui-fast/crates/gpui/src/style.rs
+++ b/third-party/gpui-fast/crates/gpui/src/style.rs
@@ -739,7 +739,7 @@ impl Style {
window.paint_inset_shadows(bounds, corner_radii, &self.box_shadow);
- continuation(window, cx);
+ crate::fast::rounded_clip::paint_children(self, bounds, window, cx, continuation);
if self.is_border_visible() {
let border_widths = self.border_widths.to_pixels(rem_size);
diff --git a/third-party/gpui-fast/crates/gpui/src/tab_stop.rs b/third-party/gpui-fast/crates/gpui/src/tab_stop.rs
index d4ad179d4c..5a85b2d045 100644
--- a/third-party/gpui-fast/crates/gpui/src/tab_stop.rs
+++ b/third-party/gpui-fast/crates/gpui/src/tab_stop.rs
@@ -329,13 +329,14 @@ mod sum_tree_impl {
#[cfg(test)]
mod tests {
use itertools::Itertools as _;
+ use parking_lot::RwLock;
use crate::{FocusHandle, FocusId, FocusMap, TabStopMap};
use std::sync::Arc;
#[test]
fn test_tab_handles() {
- let focus_map = Arc::new(FocusMap::default());
+ let focus_map = Arc::new(RwLock::new(FocusMap::default()));
let mut tab_index_map = TabStopMap::default();
let focus_handles = [
@@ -424,7 +425,7 @@ mod tests {
#[test]
fn test_tab_non_stop_filtering() {
- let focus_map = Arc::new(FocusMap::default());
+ let focus_map = Arc::new(RwLock::new(FocusMap::default()));
let mut tab_index_map = TabStopMap::default();
// Check that we can query next from a non-stop tab
@@ -447,7 +448,7 @@ mod tests {
#[must_use]
struct TabStopMapTest {
tab_map: TabStopMap,
- focus_map: Arc<FocusMap>,
+ focus_map: Arc<RwLock<FocusMap>>,
expected: Vec<(usize, FocusId)>,
}
@@ -456,7 +457,7 @@ mod tests {
fn new() -> Self {
Self {
tab_map: TabStopMap::default(),
- focus_map: Arc::new(FocusMap::default()),
+ focus_map: Arc::new(RwLock::new(FocusMap::default())),
expected: Vec::default(),
}
}
diff --git a/third-party/gpui-fast/crates/gpui/src/view.rs b/third-party/gpui-fast/crates/gpui/src/view.rs
index 551115c7a7..f12f76ca12 100644
--- a/third-party/gpui-fast/crates/gpui/src/view.rs
+++ b/third-party/gpui-fast/crates/gpui/src/view.rs
@@ -91,9 +91,17 @@ impl View for AnyView {
Some(self.entity.entity_id())
}
+ #[cfg(not(gpui_hot_reload))]
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
(self.render)(&self, window, cx)
}
+
+ // The stored function pointer was captured from the original binary, so it must be looked up
+ // in the jump table to reach the patched render.
+ #[cfg(gpui_hot_reload)]
+ fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
+ subsecond::HotFn::current(self.render).call((&self, window, cx))
+ }
}
impl<V: 'static + Render> IntoElement for Entity<V> {
@@ -310,6 +318,24 @@ impl<V: View> IntoElement for ViewElement<V> {
}
}
+#[cfg(not(gpui_hot_reload))]
+#[inline]
+pub(crate) fn render_view<V: View>(view: V, window: &mut Window, cx: &mut App) -> AnyElement {
+ view.render(window, cx).into_any_element()
+}
+
+/// Every view and component render passes through this hot-patchable boundary, so the
+/// patched code for that render and everything it calls runs on the next frame.
+#[cfg(gpui_hot_reload)]
+pub(crate) fn render_view<V: View>(view: V, window: &mut Window, cx: &mut App) -> AnyElement {
+ #[inline(never)]
+ fn render_view_impl<V: View>(view: V, window: &mut Window, cx: &mut App) -> AnyElement {
+ view.render(window, cx).into_any_element()
+ }
+
+ subsecond::HotFn::current(render_view_impl::<V>).call((view, window, cx))
+}
+
impl<V: View> Element for ViewElement<V> {
type RequestLayoutState = crate::fast::retained::ViewLayoutState;
type PrepaintState = crate::fast::retained::ViewPrepaintState;
diff --git a/third-party/gpui-fast/crates/gpui/src/window.rs b/third-party/gpui-fast/crates/gpui/src/window.rs
index a7d1c2ddfc..610dd771ca 100644
--- a/third-party/gpui-fast/crates/gpui/src/window.rs
+++ b/third-party/gpui-fast/crates/gpui/src/window.rs
@@ -21,8 +21,8 @@ use crate::{
SystemWindowTabController, TabStopMap, TaffyLayoutEngine, Task, TextInputConfiguration,
TextInputStateChange, TextRenderingMode, TextStyle, TextStyleRefinement, ThermalState,
TransformationMatrix, Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance,
- WindowBounds, WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem,
- WindowVisibility, point, prelude::*, px, rems, size, transparent_black,
+ WindowBounds, WindowControls, WindowDecorations, WindowInsets, WindowOptions, WindowParams,
+ WindowTextSystem, WindowVisibility, point, prelude::*, px, rems, size, transparent_black,
};
use crate::gestures::{GestureTuning, RecognizedTouchGesture, TouchGestureRecognizer};
@@ -39,7 +39,7 @@ use itertools::Itertools;
use objc2_core_foundation::CFRetained;
#[cfg(any(target_os = "macos", target_os = "ios"))]
use objc2_core_video::CVPixelBuffer;
-use parking_lot::RwLock;
+use parking_lot::{RwLock, RwLockUpgradableReadGuard};
use raw_window_handle::{HandleError, HasDisplayHandle, HasWindowHandle};
use scheduler::Instant;
use slotmap::SlotMap;
@@ -125,7 +125,7 @@ impl DispatchPhase {
}
}
-struct WindowInvalidatorInner {
+pub(crate) struct WindowInvalidatorInner {
#[cfg(feature = "profiler")]
pub window_id: WindowId,
pub dirty: bool,
@@ -133,7 +133,7 @@ struct WindowInvalidatorInner {
pub dirty_views: FxHashSet<EntityId>,
pub update_count: usize,
#[cfg(feature = "profiler")]
- pub frame_dirty: FrameDirtyAccumulator,
+ frame_dirty: FrameDirtyAccumulator,
pub platform_waker: Option<Rc<dyn Fn()>>,
}
@@ -160,7 +160,7 @@ enum FrameRateLimit {
#[derive(Clone)]
pub(crate) struct WindowInvalidator {
- inner: Rc<RefCell<WindowInvalidatorInner>>,
+ pub(crate) inner: Rc<RefCell<WindowInvalidatorInner>>,
}
impl WindowInvalidator {
@@ -502,7 +502,12 @@ impl ArenaClearNeeded {
}
}
-pub(crate) type FocusMap = RwLock<SlotMap<FocusId, FocusRef>>;
+#[derive(Default)]
+pub(crate) struct FocusMap {
+ pub(crate) handles: SlotMap<FocusId, FocusRef>,
+ pub(crate) dropped_handles: Vec<FocusId>,
+}
+
pub(crate) struct FocusRef {
pub(crate) ref_count: AtomicUsize,
pub(crate) tab_index: isize,
@@ -561,7 +566,7 @@ impl FocusId {
/// A handle which can be used to track and manipulate the focused element in a window.
pub struct FocusHandle {
pub(crate) id: FocusId,
- handles: Arc<FocusMap>,
+ handles: Arc<RwLock<FocusMap>>,
/// The index of this element in the tab order.
pub tab_index: isize,
/// Whether this element can be focused by tab navigation.
@@ -575,8 +580,8 @@ impl std::fmt::Debug for FocusHandle {
}
impl FocusHandle {
- pub(crate) fn new(handles: &Arc<FocusMap>) -> Self {
- let id = handles.write().insert(FocusRef {
+ pub(crate) fn new(handles: &Arc<RwLock<FocusMap>>) -> Self {
+ let id = handles.write().handles.insert(FocusRef {
ref_count: AtomicUsize::new(1),
tab_index: 0,
tab_stop: false,
@@ -590,9 +595,9 @@ impl FocusHandle {
}
}
- pub(crate) fn for_id(id: FocusId, handles: &Arc<FocusMap>) -> Option<Self> {
+ pub(crate) fn for_id(id: FocusId, handles: &Arc<RwLock<FocusMap>>) -> Option<Self> {
let lock = handles.read();
- let focus = lock.get(id)?;
+ let focus = lock.handles.get(id)?;
if atomic_incr_if_not_zero(&focus.ref_count) == 0 {
return None;
}
@@ -607,7 +612,7 @@ impl FocusHandle {
/// Sets the tab index of the element associated with this handle.
pub fn tab_index(mut self, index: isize) -> Self {
self.tab_index = index;
- if let Some(focus) = self.handles.write().get_mut(self.id) {
+ if let Some(focus) = self.handles.write().handles.get_mut(self.id) {
focus.tab_index = index;
}
self
@@ -618,7 +623,7 @@ impl FocusHandle {
/// When `false`, the element will not be included in the tab order.
pub fn tab_stop(mut self, tab_stop: bool) -> Self {
self.tab_stop = tab_stop;
- if let Some(focus) = self.handles.write().get_mut(self.id) {
+ if let Some(focus) = self.handles.write().handles.get_mut(self.id) {
focus.tab_stop = tab_stop;
}
self
@@ -692,12 +697,15 @@ impl Eq for FocusHandle {}
impl Drop for FocusHandle {
fn drop(&mut self) {
- self.handles
- .read()
- .get(self.id)
- .unwrap()
- .ref_count
- .fetch_sub(1, SeqCst);
+ let handles = self.handles.upgradable_read();
+ let focus = handles.handles.get(self.id).expect("focus handle missing");
+ let previous_count = focus.ref_count.fetch_sub(1, SeqCst);
+ assert_ne!(previous_count, 0, "over-released focus handle");
+ if previous_count == 1 {
+ RwLockUpgradableReadGuard::upgrade(handles)
+ .dropped_handles
+ .push(self.id);
+ }
}
}
@@ -705,7 +713,7 @@ impl Drop for FocusHandle {
#[derive(Clone, Debug)]
pub struct WeakFocusHandle {
pub(crate) id: FocusId,
- pub(crate) handles: Weak<FocusMap>,
+ pub(crate) handles: Weak<RwLock<FocusMap>>,
}
impl WeakFocusHandle {
@@ -822,7 +830,7 @@ impl HitboxId {
if window.captured_hitbox == Some(self) {
return true;
}
- if window.last_input_was_keyboard() {
+ if crate::fast::layers::hover_freeze::hides_hover(window) {
return false;
}
self.hit_test(window)
@@ -1130,6 +1138,7 @@ pub(crate) struct Frame {
pub(crate) dispatch_tree: DispatchTree,
pub(crate) scene: Scene,
pub(crate) fast_composition_starts: crate::fast::composition::SurfaceStarts,
+ pub(crate) fast_composition_input: crate::fast::composition::SurfaceInput,
pub(crate) hitboxes: Vec<Hitbox>,
placement_claims: Vec<ElementId>,
placed: FxHashSet<ElementId>,
@@ -1199,6 +1208,7 @@ impl Frame {
dispatch_tree,
scene: Scene::default(),
fast_composition_starts: crate::fast::composition::SurfaceStarts::default(),
+ fast_composition_input: crate::fast::composition::SurfaceInput::default(),
hitboxes: Vec::new(),
placement_claims: Vec::new(),
placed: FxHashSet::default(),
@@ -1238,6 +1248,7 @@ impl Frame {
self.hitboxes.clear();
self.placement_claims.clear();
self.placed.clear();
+ crate::fast::composition::SurfaceInput::clear(&mut self.fast_composition_input);
self.window_control_hitboxes.clear();
self.deferred_draws.clear();
self.deferred_paints.clear();
@@ -1276,6 +1287,9 @@ impl Frame {
}
pub(crate) fn hit_test(&self, position: Point<Pixels>) -> HitTest {
+ if let Some(hit_test) = crate::fast::composition::hit_test(self, position) {
+ return hit_test;
+ }
let mut set_hover_hitbox_count = false;
let mut hit_test = HitTest::default();
for hitbox in self.hitboxes.iter().rev() {
@@ -1430,7 +1444,7 @@ pub struct Window {
/// window, so that only actual changes are forwarded (reconfiguring a live
/// input session can restart the IME connection).
last_text_input_configuration: Option<TextInputConfiguration>,
- focused_text_input_active: bool,
+ focused_text_input: Option<FocusId>,
pub(crate) image_cache_stack: Vec<AnyImageCache>,
// Opaque inherited renderer context; selection facts remain in its adapter.
pub(crate) selection_context: crate::window::SelectionContext,
@@ -1444,7 +1458,7 @@ pub struct Window {
focus_listeners: SubscriberSet<(), AnyWindowFocusListener>,
pub(crate) focus_lost_listeners: SubscriberSet<(), AnyObserver>,
focus_lost_path: SmallVec<[FocusId; 8]>,
- default_prevented: bool,
+ pub(crate) default_prevented: bool,
pub(crate) mouse_position: Point<Pixels>,
pub(crate) mouse_hit_test: HitTest,
/// The scroll container the current wheel gesture belongs to.
@@ -1456,11 +1470,11 @@ pub struct Window {
appearance: WindowAppearance,
pub(crate) appearance_observers: SubscriberSet<(), AnyObserver>,
pub(crate) button_layout_observers: SubscriberSet<(), AnyObserver>,
- active: Rc<Cell<bool>>,
+ pub(crate) active: Rc<Cell<bool>>,
visibility: WindowVisibility,
pub(crate) visibility_observers:
SubscriberSet<(), Box<dyn FnMut(WindowVisibility, &mut Window, &mut App) -> bool>>,
- hovered: Rc<Cell<bool>>,
+ pub(crate) hovered: Rc<Cell<bool>>,
pub(crate) needs_present: Rc<Cell<bool>>,
/// Tracks recent input event timestamps to determine if input is arriving at a high rate.
/// Used to selectively enable VRR optimization only when input rate exceeds 60fps.
@@ -1797,6 +1811,8 @@ impl Window {
icon,
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))]
tabbing_identifier,
+ #[cfg_attr(not(target_os = "macos"), allow(unused_variables))]
+ use_display_p3,
} = options;
let initial_window_title = titlebar
@@ -1822,6 +1838,8 @@ impl Window {
icon,
#[cfg(target_os = "macos")]
tabbing_identifier,
+ #[cfg(target_os = "macos")]
+ use_display_p3,
},
)?;
@@ -2355,7 +2373,7 @@ impl Window {
autoscroll_focus_generation: None,
prepaint_visible: true,
last_text_input_configuration: None,
- focused_text_input_active: false,
+ focused_text_input: None,
rendered_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
next_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
next_frame_callbacks,
@@ -3155,6 +3173,11 @@ impl Window {
self.platform_window.bounds()
}
+ /// Returns the regions of this window obscured by system UI and the keyboard.
+ pub fn insets(&self) -> WindowInsets {
+ self.platform_window.insets()
+ }
+
/// Renders the current frame's 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.
@@ -3701,7 +3724,8 @@ impl Window {
self.last_input_modality == InputModality::Keyboard
}
- pub(crate) fn last_input_was_touch(&self) -> bool {
+ /// Returns whether the last input event came from a touch screen.
+ pub fn last_input_was_touch(&self) -> bool {
self.last_input_modality == InputModality::Touch
}
@@ -3741,6 +3765,7 @@ impl Window {
// Restore the platform's exact original slot for retained paint reuse.
// A focused canvas handler need not have a tracked geometry node.
if let Some(input_handler) = self.platform_window.take_input_handler() {
+ crate::fast::retained::note_restored_input_handler(self, &input_handler);
let index = self
.rendered_frame
.published_input_handler
@@ -3847,19 +3872,20 @@ impl Window {
.is_some_and(|handler| handler.presentation_is_admitted(self))
});
self.rendered_frame.published_input_handler = focused_handler;
- let (focused_text_input_active, has_focused_input) = if let Some(index) = focused_handler {
+ let (focused_text_input, has_focused_input) = if let Some(index) = focused_handler {
let mut input_handler = self.rendered_frame.input_handlers[index].1.take().unwrap();
let accepts_text_input = input_handler.presentation_accepts_text_input(self, cx);
+ crate::fast::retained::note_installed_input_handler(self, &input_handler);
self.platform_window.set_input_handler(input_handler);
- (accepts_text_input, true)
+ (self.focus.filter(|_| accepts_text_input), true)
} else {
- (false, false)
+ (None, false)
};
self.apply_text_input_configuration(cx);
- if focused_text_input_active != self.focused_text_input_active {
- self.focused_text_input_active = focused_text_input_active;
+ let previous = mem::replace(&mut self.focused_text_input, focused_text_input);
+ if previous.is_some() != focused_text_input.is_some() {
self.platform_window
- .text_input_state_changed(if focused_text_input_active {
+ .text_input_state_changed(if focused_text_input.is_some() {
TextInputStateChange::FocusGained
} else {
TextInputStateChange::FocusLost
@@ -4041,11 +4067,13 @@ impl Window {
.as_mut()
.unwrap()
.stretch_auto_size_to_fill(root_layout_id, root_size, scale_factor);
+ crate::fast::composition::begin_base_prepaint(self);
root_element.prepaint_as_root(Point::default(), root_size.into(), self, cx);
#[cfg(any(feature = "inspector", debug_assertions))]
let inspector_element = self.prepaint_inspector(_inspector_width, cx);
+ crate::fast::composition::begin_overlay_prepaint(self);
self.prepaint_deferred_draws(cx);
let mut prompt_element = None;
@@ -4072,7 +4100,8 @@ impl Window {
}
crate::fast::stats::FramePhaseTimes::end_prepaint(&mut self.fast_layout.phase_times);
- self.mouse_hit_test = crate::fast::layers::hover_freeze::hit_test(self);
+ crate::fast::composition::finish_input(self);
+ self.mouse_hit_test = self.next_frame.hit_test(self.mouse_position);
// Now actually paint the elements.
#[cfg(feature = "profiler")]
@@ -5815,6 +5844,7 @@ impl Window {
dispatch_node: self.next_frame.dispatch_tree.active_node_id(),
};
self.next_frame.hitboxes.push(hitbox.clone());
+ crate::fast::composition::record_input(self, id);
hitbox
}
@@ -6001,6 +6031,34 @@ impl Window {
));
}
+ /// Notifies native text services that the focused component's text changed.
+ ///
+ /// Text components should call this after application-driven edits, in addition
+ /// to notifying GPUI to redraw. Unfocused or unregistered components are ignored.
+ pub fn notify_text_input_changed(&self, focus_handle: &FocusHandle) {
+ if self.focused_text_input == Some(focus_handle.id)
+ && focus_handle.is_focused(self)
+ && self.focus_move_is_admitted(focus_handle)
+ {
+ self.platform_window
+ .text_input_state_changed(TextInputStateChange::ContentChanged);
+ }
+ }
+
+ /// Notifies native text services that the focused component's selection changed.
+ ///
+ /// Text components should call this after application-driven selection changes,
+ /// in addition to notifying GPUI to redraw.
+ pub fn notify_text_selection_changed(&self, focus_handle: &FocusHandle) {
+ if self.focused_text_input == Some(focus_handle.id)
+ && focus_handle.is_focused(self)
+ && self.focus_move_is_admitted(focus_handle)
+ {
+ self.platform_window
+ .text_input_state_changed(TextInputStateChange::SelectionChanged);
+ }
+ }
+
/// Forwards the focused input handler's [`TextInputConfiguration`] to the
/// platform window when it differs from the last forwarded value. With no
/// input handler the default configuration applies, so a field's
@@ -6359,6 +6417,7 @@ impl Window {
cx.background_executor().now(),
);
}
+ let native_target = crate::fast::composition::event_target(self, &event);
if let Some(any_mouse_event) = event.mouse_event() {
self.dispatch_mouse_event(any_mouse_event, cx);
} else if let Some(any_key_event) = event.keyboard_event() {
@@ -6366,6 +6425,12 @@ impl Window {
} else if let Some(touch_event) = event.touch_event() {
self.dispatch_touch_event(touch_event, cx);
}
+ crate::fast::composition::dispatch_native(
+ self,
+ &event,
+ native_target,
+ &mut cx.propagate_event,
+ );
crate::fast::layers::wheel::end_dispatch(self, cx);
if let PlatformInput::LongPress(long_press) = &event {
match long_press.phase {
@@ -6605,7 +6670,7 @@ impl Window {
}
// Bubble phase, where most normal handlers do their work.
- if cx.propagate_event {
+ if crate::fast::composition::bubble_mouse(self, event, cx) {
for listener in mouse_listeners.iter_mut().rev() {
let listener = listener.as_mut().unwrap();
if !self.input_origin_is_admitted(&listener.origin) {
@@ -8729,6 +8794,95 @@ mod tests {
canvas, div, hsla, point, px, size,
};
+ #[gpui::test]
+ async fn test_focus_handle_release(cx: &mut TestAppContext) {
+ let (handle, survivor) = cx.update(|cx| (cx.focus_handle(), cx.focus_handle()));
+ let handle_id = handle.id;
+ let weak_handle = handle.downgrade();
+ let retained = handle.clone();
+ drop(handle);
+
+ cx.update(|_| {});
+ cx.read(|cx| {
+ let handles = cx.focus_handles.read();
+ assert_eq!(handles.handles.len(), 2);
+ assert!(handles.dropped_handles.is_empty());
+ });
+ assert_eq!(weak_handle.upgrade().as_ref(), Some(&retained));
+
+ cx.background_spawn(async move { drop(retained) }).await;
+ assert_eq!(weak_handle.upgrade(), None);
+ cx.read(|cx| {
+ assert_eq!(cx.focus_handles.read().dropped_handles, [handle_id]);
+ });
+ cx.update(|_| {});
+ cx.read(|cx| {
+ let handles = cx.focus_handles.read();
+ assert_eq!(handles.handles.keys().collect::<Vec<_>>(), [survivor.id]);
+ assert!(handles.dropped_handles.is_empty());
+ });
+
+ let replacements = cx.update(|cx| [cx.focus_handle(), cx.focus_handle()]);
+ assert_eq!(weak_handle.upgrade(), None);
+ cx.update(|cx| {
+ drop(replacements);
+ cx.defer(move |_| drop(survivor));
+ });
+ cx.read(|cx| {
+ let handles = cx.focus_handles.read();
+ assert_eq!(handles.handles.len(), 0);
+ assert!(handles.dropped_handles.is_empty());
+ });
+ }
+
+ #[gpui::test]
+ fn test_dropped_focus_handle_blurs_only_its_windows(cx: &mut TestAppContext) {
+ let windows = [
+ cx.add_window(|_, _| EmptyView),
+ cx.add_window(|_, _| EmptyView),
+ cx.add_window(|_, _| EmptyView),
+ cx.add_window(|_, _| EmptyView),
+ cx.add_window(|_, _| EmptyView),
+ ];
+ let (handle, other_handle, survivor) =
+ cx.update(|cx| (cx.focus_handle(), cx.focus_handle(), cx.focus_handle()));
+ for window in &windows[..2] {
+ window
+ .update(cx, |_, window, cx| window.focus(&handle, cx))
+ .expect("window exists");
+ }
+ windows[2]
+ .update(cx, |_, window, cx| window.focus(&other_handle, cx))
+ .expect("window exists");
+ windows[3]
+ .update(cx, |_, window, cx| window.focus(&survivor, cx))
+ .expect("window exists");
+
+ cx.update(|cx| {
+ for _ in 0..1_000 {
+ drop(cx.focus_handle());
+ }
+ drop((handle, other_handle));
+ });
+ for window in &windows[..3] {
+ window
+ .update(cx, |_, window, _| assert_eq!(window.focus, None))
+ .expect("window exists");
+ }
+ windows[3]
+ .update(cx, |_, window, cx| {
+ assert_eq!(window.focused(cx).as_ref(), Some(&survivor));
+ assert_eq!(window.focus_generation, 1);
+ })
+ .expect("window exists");
+ windows[4]
+ .update(cx, |_, window, _| {
+ assert_eq!(window.focus, None);
+ assert_eq!(window.focus_generation, 0);
+ })
+ .expect("window exists");
+ }
+
#[cfg(feature = "profiler")]
#[gpui::test]
fn test_frame_requests_and_skips_are_journaled(cx: &mut TestAppContext) {
@@ -9069,7 +9223,7 @@ mod tests {
assert!(platform_window.frame_wake_count() > wakes);
platform_window.simulate_frame_request(RequestFrameOptions::default());
let wakes = platform_window.frame_wake_count();
- platform_window.simulate_insets_change(crate::WindowInsets {
+ let insets = crate::WindowInsets {
safe_area: crate::Edges {
top: px(60.),
right: px(20.),
@@ -9080,10 +9234,14 @@ mod tests {
bottom: px(350.),
..Default::default()
},
- });
+ };
+ platform_window.simulate_insets_change(insets.clone());
assert!(platform_window.frame_wake_count() > wakes);
window
- .update(cx, |_, window, _| {
+ .update(cx, |_, window, cx| {
+ let focus = cx.focus_handle();
+ window.focus(&focus, cx);
+ assert_eq!(window.insets(), insets);
assert_eq!(window.viewport_size(), size(px(400.), px(800.)));
assert_eq!(window.visual_viewport_bounds(), visual_bounds);
assert_eq!(
@@ -9092,6 +9250,7 @@ mod tests {
);
window.request_virtual_keyboard();
window.dismiss_virtual_keyboard();
+ assert!(focus.is_focused(window));
assert_eq!(window.viewport_size(), size(px(400.), px(800.)));
})
.unwrap();
@@ -9114,6 +9273,16 @@ mod tests {
);
})
.unwrap();
+
+ // Some backends update geometry snapshots without delivering inset callbacks.
+ use crate::PlatformWindow as _;
+ platform_window.on_insets_changed(Box::new(|_| {}));
+ platform_window.simulate_insets_change(crate::WindowInsets::default());
+ window
+ .update(cx, |_, window, _| {
+ assert_eq!(window.insets(), crate::WindowInsets::default());
+ })
+ .unwrap();
}
struct EmptyView;
diff --git a/third-party/gpui-fast/crates/gpui_linux/Cargo.toml b/third-party/gpui-fast/crates/gpui_linux/Cargo.toml
index 15b0a6813a..ded110d1fe 100644
--- a/third-party/gpui-fast/crates/gpui_linux/Cargo.toml
+++ b/third-party/gpui-fast/crates/gpui_linux/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "gpui-fast-linux"
-version = "0.1.7"
+version = "0.1.10"
edition = "2024"
publish = false
license = "Apache-2.0"
@@ -81,8 +81,7 @@ version = "0.3.34"
[target."cfg(any(target_os = \"linux\", target_os = \"freebsd\"))".dependencies.gpui]
default-features = false
package = "gpui-fast"
-version = "=0.1.7"
-path = "../gpui"
+version = "=0.1.10"
[target."cfg(any(target_os = \"linux\", target_os = \"freebsd\"))".dependencies.gpui_util]
package = "gpui-pre-util"
@@ -90,9 +89,9 @@ version = "=0.3.9"
[target."cfg(any(target_os = \"linux\", target_os = \"freebsd\"))".dependencies.gpui_wgpu]
optional = true
-features = ["font-kit"]
+features = [ "font-kit" ]
package = "gpui-fast-wgpu"
-version = "=0.1.7"
+version = "=0.1.10"
[target."cfg(any(target_os = \"linux\", target_os = \"freebsd\"))".dependencies.http_client]
package = "gpui-pre-http-client"
@@ -214,7 +213,6 @@ optional = true
[target."cfg(any(target_os = \"linux\", target_os = \"freebsd\"))".dev-dependencies.gpui]
default-features = false
-features = ["test-support"]
+features = [ "test-support" ]
package = "gpui-fast"
-version = "=0.1.7"
-path = "../gpui"
+version = "=0.1.10"
diff --git a/third-party/gpui-fast/crates/gpui_linux/src/fast/composition/mod.rs b/third-party/gpui-fast/crates/gpui_linux/src/fast/composition/mod.rs
index df4bb7d022..4f5d1e8492 100644
--- a/third-party/gpui-fast/crates/gpui_linux/src/fast/composition/mod.rs
+++ b/third-party/gpui-fast/crates/gpui_linux/src/fast/composition/mod.rs
@@ -5,3 +5,6 @@
pub(crate) mod wayland;
#[cfg(feature = "x11")]
pub(crate) mod x11;
+
+#[cfg(feature = "x11")]
+mod occluders;
diff --git a/third-party/gpui-fast/crates/gpui_linux/src/fast/composition/occluders.rs b/third-party/gpui-fast/crates/gpui_linux/src/fast/composition/occluders.rs
new file mode 100644
index 0000000000..edcd1c66f5
--- /dev/null
+++ b/third-party/gpui-fast/crates/gpui_linux/src/fast/composition/occluders.rs
@@ -0,0 +1,265 @@
+//! Conservative X11 occlusion rectangles. Rounded clip group markers are
+//! containers, not images: only their drawable descendants contribute bounds.
+
+use gpui::{Bounds, ContentMask, DevicePixels, ScaledPixels, Scene};
+
+/// Bound shape request size for text-heavy overlays, as before clip groups.
+const MAX_OCCLUDER_RECTANGLES: usize = 256;
+
+fn collect(
+ scene: &Scene,
+ opaque_only: bool,
+ enclosing: Option<Bounds<ScaledPixels>>,
+ rectangles: &mut Vec<Bounds<ScaledPixels>>,
+) {
+ let clipped = |bounds: Bounds<ScaledPixels>, mask: &ContentMask<ScaledPixels>| {
+ let bounds = bounds.intersect(&mask.bounds);
+ enclosing.map_or(bounds, |clip| bounds.intersect(&clip))
+ };
+ rectangles.extend(
+ scene
+ .quads
+ .iter()
+ .filter(|quad| !opaque_only || quad.background.is_opaque())
+ .map(|quad| clipped(quad.bounds, &quad.content_mask)),
+ );
+ rectangles.extend(
+ scene
+ .paths
+ .iter()
+ .map(|path| clipped(path.bounds, &path.content_mask)),
+ );
+ rectangles.extend(
+ scene
+ .underlines
+ .iter()
+ .map(|line| clipped(line.bounds, &line.content_mask)),
+ );
+ rectangles.extend(
+ scene
+ .monochrome_sprites
+ .iter()
+ .map(|sprite| clipped(sprite.bounds, &sprite.content_mask)),
+ );
+ rectangles.extend(
+ scene
+ .subpixel_sprites
+ .iter()
+ .map(|sprite| clipped(sprite.bounds, &sprite.content_mask)),
+ );
+ rectangles.extend(
+ scene
+ .surfaces
+ .iter()
+ .map(|surface| clipped(surface.bounds, &surface.content_mask)),
+ );
+ for sprite in &scene.polychrome_sprites {
+ let bounds = clipped(sprite.bounds, &sprite.content_mask);
+ let group = sprite
+ .tile
+ .texture_id
+ .index
+ .checked_sub(0xF000_0000)
+ .filter(|_| sprite.tile.texture_id.index >= 0xF100_0000)
+ .and_then(|key| {
+ scene
+ .layers
+ .frames
+ .iter()
+ .find(|frame| frame.key.0 == key && frame.tile_size == 0)
+ });
+ if let Some(group) = group {
+ if let Some(child) = group.content.scene() {
+ collect(child, opaque_only, Some(bounds), rectangles);
+ }
+ } else {
+ rectangles.push(bounds);
+ }
+ }
+}
+
+pub(crate) fn scene_occluders(scene: &Scene, opaque_only: bool) -> Vec<Bounds<DevicePixels>> {
+ let mut rectangles = Vec::new();
+ collect(scene, opaque_only, None, &mut rectangles);
+
+ let mut rectangles = rectangles
+ .into_iter()
+ .map(|bounds| {
+ let origin = bounds
+ .origin
+ .map(|value| DevicePixels(value.0.floor() as i32));
+ let corner = bounds
+ .bottom_right()
+ .map(|value| DevicePixels(value.0.ceil() as i32));
+ Bounds::from_corners(origin, corner)
+ })
+ .filter(|bounds| !bounds.is_empty())
+ .collect::<Vec<_>>();
+
+ // Content usually sits on a background quad, so most rectangles are
+ // covered by a larger one.
+ rectangles.sort_by_key(|bounds| std::cmp::Reverse(bounds.size.width.0 * bounds.size.height.0));
+ let mut kept: Vec<Bounds<DevicePixels>> = Vec::new();
+ for rectangle in rectangles {
+ let covered = kept.iter().any(|kept| {
+ kept.origin.x <= rectangle.origin.x
+ && kept.origin.y <= rectangle.origin.y
+ && kept.bottom_right().x >= rectangle.bottom_right().x
+ && kept.bottom_right().y >= rectangle.bottom_right().y
+ });
+ if !covered {
+ kept.push(rectangle);
+ }
+ }
+ if kept.len() > MAX_OCCLUDER_RECTANGLES {
+ let union = kept
+ .iter()
+ .copied()
+ .reduce(|union, bounds| union.union(&bounds))
+ .into_iter()
+ .collect();
+ return union;
+ }
+ kept
+}
+
+#[cfg(test)]
+mod tests {
+ use super::scene_occluders;
+ use gpui::{
+ Bounds, ContentMask, DevicePixels, Quad, ScaledPixels, Scene, Shadow, black, point, size,
+ };
+
+ fn quad(bounds: Bounds<ScaledPixels>, mask: Bounds<ScaledPixels>) -> Quad {
+ Quad {
+ bounds,
+ content_mask: ContentMask { bounds: mask },
+ ..Default::default()
+ }
+ }
+
+ fn scaled(x: f32, y: f32, width: f32, height: f32) -> Bounds<ScaledPixels> {
+ Bounds::new(
+ point(ScaledPixels(x), ScaledPixels(y)),
+ size(ScaledPixels(width), ScaledPixels(height)),
+ )
+ }
+
+ fn device(x: i32, y: i32, width: i32, height: i32) -> Bounds<DevicePixels> {
+ Bounds::new(
+ point(DevicePixels(x), DevicePixels(y)),
+ size(DevicePixels(width), DevicePixels(height)),
+ )
+ }
+
+ #[test]
+ fn scene_occluders_skip_shadows_and_covered_rectangles() {
+ let everything = scaled(0., 0., 1000., 1000.);
+ let mut scene = Scene::default();
+ scene.insert_primitive(Shadow {
+ order: 0,
+ blur_radius: ScaledPixels(24.),
+ bounds: scaled(500., 500., 100., 100.),
+ corner_radii: Default::default(),
+ content_mask: ContentMask { bounds: everything },
+ color: Default::default(),
+ element_bounds: scaled(500., 500., 100., 100.),
+ element_corner_radii: Default::default(),
+ inset: 0,
+ pad: 0,
+ });
+ scene.insert_primitive(quad(scaled(10., 10., 100., 50.), everything));
+ scene.insert_primitive(quad(scaled(20.5, 20.5, 10., 10.), everything));
+ scene.insert_primitive(quad(
+ scaled(200., 0., 100., 100.),
+ scaled(250., 0., 20., 40.),
+ ));
+ scene.finish();
+
+ let mut occluders = scene_occluders(&scene, false);
+ occluders.sort_by_key(|bounds| bounds.origin.x);
+ assert_eq!(occluders, [device(10, 10, 100, 50), device(250, 0, 20, 40)]);
+ }
+
+ #[test]
+ fn opaque_scene_occluders_skip_translucent_backgrounds() {
+ let everything = scaled(0., 0., 1000., 1000.);
+ let mut scene = Scene::default();
+ // A dialog's translucent backdrop over the whole window, and its panel.
+ scene.insert_primitive(Quad {
+ background: black().opacity(0.4).into(),
+ ..quad(everything, everything)
+ });
+ scene.insert_primitive(Quad {
+ background: black().into(),
+ ..quad(scaled(300., 200., 400., 300.), everything)
+ });
+ scene.finish();
+
+ assert_eq!(scene_occluders(&scene, true), [device(300, 200, 400, 300)]);
+ assert_eq!(scene_occluders(&scene, false), [device(0, 0, 1000, 1000)]);
+ }
+ fn group(child: Scene, bounds: Bounds<ScaledPixels>) -> Scene {
+ let key = gpui::LayerKey(0x0100_0000);
+ let mut scene = Scene::default();
+ scene.layers.frames.push(gpui::LayerFrame {
+ key,
+ generation: 0,
+ background: Default::default(),
+ tile_size: 0,
+ content: child.into(),
+ dirty_tiles: Vec::new(),
+ });
+ scene.insert_primitive(gpui::PolychromeSprite {
+ order: 0,
+ pad: 0,
+ grayscale: false.into(),
+ opacity: 1.,
+ bounds,
+ content_mask: ContentMask { bounds },
+ corner_radii: Default::default(),
+ tile: gpui::AtlasTile {
+ texture_id: gpui::AtlasTextureId {
+ index: 0xF100_0000,
+ kind: gpui::AtlasTextureKind::Polychrome,
+ },
+ tile_id: gpui::TileId(0),
+ padding: 0,
+ bounds: Default::default(),
+ },
+ });
+ scene.finish();
+ scene
+ }
+
+ #[test]
+ fn rounded_group_occludes_only_descendant_bounds_intersected_with_its_clip() {
+ let mut child = Scene::default();
+ let bounds = scaled(45., 45., 20., 20.);
+ child.insert_primitive(Quad {
+ background: black().into(),
+ ..quad(bounds, bounds)
+ });
+ child.finish();
+ let scene = group(
+ group(child, scaled(0., 0., 100., 100.)),
+ scaled(0., 0., 50., 50.),
+ );
+ assert_eq!(scene_occluders(&scene, true), [device(45, 45, 5, 5)]);
+ }
+
+ #[test]
+ fn rounded_group_is_not_itself_an_opaque_image() {
+ let mut child = Scene::default();
+ let bounds = scaled(10., 10., 20., 20.);
+ child.insert_primitive(Quad {
+ background: black().opacity(0.4).into(),
+ ..quad(bounds, bounds)
+ });
+ child.finish();
+ let scene = group(child, scaled(0., 0., 100., 100.));
+ assert!(scene_occluders(&scene, true).is_empty());
+ assert_eq!(scene_occluders(&scene, false), [device(10, 10, 20, 20)]);
+ assert!(scene_occluders(&group(Scene::default(), bounds), false).is_empty());
+ }
+}
diff --git a/third-party/gpui-fast/crates/gpui_linux/src/fast/composition/wayland.rs b/third-party/gpui-fast/crates/gpui_linux/src/fast/composition/wayland.rs
index ca9f137654..39055cc73e 100644
--- a/third-party/gpui-fast/crates/gpui_linux/src/fast/composition/wayland.rs
+++ b/third-party/gpui-fast/crates/gpui_linux/src/fast/composition/wayland.rs
@@ -5,12 +5,12 @@
//! are synchronized subsurfaces drawn by renderers sharing the window's sprite
//! atlas, native surfaces are desynchronized subsurfaces their producer
//! presents to. Pointer input on a GPUI overlay goes to its window, and an
-//! overlay without content has an empty input region so input reaches the
-//! surfaces below it.
+//! overlay without prepared hitboxes has an empty input region so input
+//! reaches the surfaces below it.
use std::{
any::Any,
- cell::{RefCell, RefMut},
+ cell::{Cell, RefCell, RefMut},
ffi::c_void,
ptr::NonNull,
rc::{Rc, Weak},
@@ -41,7 +41,14 @@ delegate_noop!(WaylandClientStatePtr: ignore wl_subsurface::WlSubsurface);
/// The GPUI composition surfaces that deliver pointer input to their window,
/// by `wl_surface`. Kept by the Wayland client.
#[derive(Default)]
-pub(crate) struct InputSurfaces(HashMap<ObjectId, WaylandWindowStatePtr>);
+pub(crate) struct InputSurfaces {
+ surfaces: HashMap<ObjectId, InputSurface>,
+ focused: Option<ObjectId>,
+}
+struct InputSurface {
+ window: WaylandWindowStatePtr,
+ native: Option<Weak<RefCell<WaylandNativeSurfaceState>>>,
+}
/// Resolves the window receiving pointer input on `surface_id`, including GPUI
/// composition subsurfaces, which share their window's coordinate space.
@@ -52,12 +59,53 @@ pub(crate) fn get_input_window(
get_window(state, surface_id).or_else(|| {
state
.fast_composition_input_surfaces
- .0
+ .surfaces
.get(surface_id)
- .cloned()
+ .filter(|surface| surface.native.is_none())
+ .map(|surface| surface.window.clone())
+ })
+}
+
+/// Pointer events on an owned native slot follow the same dispatcher as overlays.
+pub(crate) fn get_pointer_window(
+ state: &mut RefMut<WaylandClientState>,
+ surface_id: &ObjectId,
+) -> Option<WaylandWindowStatePtr> {
+ get_window(state, surface_id).or_else(|| {
+ let surface = state
+ .fast_composition_input_surfaces
+ .surfaces
+ .get(surface_id)?;
+ if let Some(native) = &surface.native {
+ if !native.upgrade()?.borrow().visible {
+ return None;
+ }
+ }
+ Some(surface.window.clone())
})
}
+/// Translate native-subsurface coordinates into the window coordinate space.
+pub(crate) fn pointer_position(
+ state: &mut WaylandClientState,
+ surface: Option<&ObjectId>,
+ position: Point<Pixels>,
+) -> Point<Pixels> {
+ let input = &mut state.fast_composition_input_surfaces;
+ if let Some(surface) = surface {
+ input.focused = Some(surface.clone());
+ }
+ input
+ .focused
+ .as_ref()
+ .and_then(|id| input.surfaces.get(id))
+ .and_then(|surface| surface.native.as_ref()?.upgrade())
+ .map_or(position, |native| {
+ let native = native.borrow();
+ position + native.bounds.to_pixels(native.scale.get()).origin
+ })
+}
+
/// Forgets the input surfaces of a window that closed.
pub(crate) fn forget_input_surfaces(
state: &mut WaylandClientState,
@@ -65,8 +113,8 @@ pub(crate) fn forget_input_surfaces(
) {
state
.fast_composition_input_surfaces
- .0
- .retain(|_, window| !window.ptr_eq(closed_window));
+ .surfaces
+ .retain(|_, surface| !surface.window.ptr_eq(closed_window));
}
impl WaylandClientStatePtr {
@@ -74,19 +122,20 @@ impl WaylandClientStatePtr {
&self,
surface_id: ObjectId,
window: WaylandWindowStatePtr,
+ native: Option<Weak<RefCell<WaylandNativeSurfaceState>>>,
) {
self.get_client()
.borrow_mut()
.fast_composition_input_surfaces
- .0
- .insert(surface_id, window);
+ .surfaces
+ .insert(surface_id, InputSurface { window, native });
}
fn unregister_composition_input_surface(&self, surface_id: &ObjectId) {
self.get_client()
.borrow_mut()
.fast_composition_input_surfaces
- .0
+ .surfaces
.remove(surface_id);
}
}
@@ -201,7 +250,7 @@ fn raw_window(surface: &wl_surface::WlSurface) -> anyhow::Result<RawWindow> {
struct WaylandGpuiSurface {
subsurface: WaylandSubsurface,
renderer: WgpuRenderer,
- input_active: Option<bool>,
+ input_regions: Vec<gpui::Bounds<gpui::Pixels>>,
}
impl WaylandGpuiSurface {
@@ -230,10 +279,12 @@ struct WaylandNativeSurfaceState {
bounds: Bounds<DevicePixels>,
parent_origin: Point<DevicePixels>,
visible: bool,
+ scale: Cell<f32>,
}
impl WaylandNativeSurfaceState {
fn update_geometry(&self, scale: f32) {
+ self.scale.set(scale);
if let Some(subsurface) = &self.subsurface.subsurface {
let position = logical_point(self.bounds.origin - self.parent_origin, scale);
subsurface.set_position(position.x, position.y);
@@ -248,6 +299,7 @@ impl WaylandNativeSurfaceState {
struct WaylandNativeSurface {
state: Rc<RefCell<WaylandNativeSurfaceState>>,
composition: Weak<RefCell<WaylandComposition>>,
+ client: WaylandClientStatePtr,
}
impl WaylandNativeSurface {
@@ -308,6 +360,8 @@ impl PlatformSurfaceAttachment for WaylandNativeSurface {
impl Drop for WaylandNativeSurface {
fn drop(&mut self) {
+ self.client
+ .unregister_composition_input_surface(&self.state.borrow().subsurface.surface.id());
self.state.borrow_mut().subsurface.destroy();
}
}
@@ -610,21 +664,30 @@ pub(crate) fn draw_composed(window: &WaylandWindow, scene: ComposedScene<'_>) {
if layer.surface == base_surface {
base_scene = surface_scene;
} else if let Some(surface) = composition.gpui_surfaces.get_mut(&layer.surface) {
- // An overlay without content must let input reach the surfaces
- // below it, like the AppKit overlay's hit testing.
- let input_active = !surface_scene.is_empty();
- if surface.input_active != Some(input_active) {
- if input_active {
- surface.subsurface.surface.set_input_region(None);
- } else {
- let region = composition
- .globals
- .compositor
- .create_region(&composition.globals.qh, ());
- surface.subsurface.surface.set_input_region(Some(®ion));
- region.destroy();
+ let input = scene.layer_hitboxes(layer);
+ if surface.input_regions.len() != input.len()
+ || surface
+ .input_regions
+ .iter()
+ .zip(input)
+ .any(|(old, new)| *old != new.bounds)
+ {
+ let region = composition
+ .globals
+ .compositor
+ .create_region(&composition.globals.qh, ());
+ surface.input_regions.clear();
+ for hitbox in input {
+ let bounds = hitbox.bounds;
+ let x = f32::from(bounds.origin.x).floor() as i32;
+ let y = f32::from(bounds.origin.y).floor() as i32;
+ let right = f32::from(bounds.bottom_right().x).ceil() as i32;
+ let bottom = f32::from(bounds.bottom_right().y).ceil() as i32;
+ region.add(x, y, right - x, bottom - y);
+ surface.input_regions.push(bounds);
}
- surface.input_active = Some(input_active);
+ surface.subsurface.surface.set_input_region(Some(®ion));
+ region.destroy();
}
// Synchronized subsurfaces cache this commit until the window
// surface commits the base frame below.
@@ -663,6 +726,7 @@ pub(crate) fn create_native_surface(
bounds: Bounds::default(),
parent_origin: Point::default(),
visible: true,
+ scale: Cell::new(state.fast_composition.0.borrow().scale),
}));
state
.fast_composition
@@ -670,7 +734,13 @@ pub(crate) fn create_native_surface(
.borrow_mut()
.native_surfaces
.push(Rc::downgrade(&native_state));
+ state.client.register_composition_input_surface(
+ native_state.borrow().subsurface.surface.id(),
+ window.0.clone(),
+ Some(Rc::downgrade(&native_state)),
+ );
Ok(Rc::new(WaylandNativeSurface {
+ client: state.client.clone(),
state: native_state,
composition: Rc::downgrade(&state.fast_composition.0),
}))
@@ -736,14 +806,22 @@ pub(crate) fn set_composition_order(
return Err(error);
}
};
+ let empty_input = state
+ .globals
+ .compositor
+ .create_region(&state.globals.qh, ());
+ subsurface.surface.set_input_region(Some(&empty_input));
+ empty_input.destroy();
let surface = WaylandGpuiSurface {
subsurface,
renderer,
- input_active: None,
+ input_regions: Vec::new(),
};
- state
- .client
- .register_composition_input_surface(surface.subsurface.surface.id(), window.0.clone());
+ state.client.register_composition_input_surface(
+ surface.subsurface.surface.id(),
+ window.0.clone(),
+ None,
+ );
composition.gpui_surfaces.insert(id, surface);
}
diff --git a/third-party/gpui-fast/crates/gpui_linux/src/fast/composition/x11.rs b/third-party/gpui-fast/crates/gpui_linux/src/fast/composition/x11.rs
index 2ab0d34463..59ebb11a37 100644
--- a/third-party/gpui-fast/crates/gpui_linux/src/fast/composition/x11.rs
+++ b/third-party/gpui-fast/crates/gpui_linux/src/fast/composition/x11.rs
@@ -12,7 +12,7 @@
//! a compositing manager blends windows with an ARGB visual, the GPUI content
//! stacked above the native surfaces is instead drawn into a transparent
//! override-redirect window kept over the window, which the compositing
-//! manager blends over the native content. Its input shape is its content, so
+//! manager blends over the native content. Its input shape uses GPUI hitboxes, so
//! input elsewhere reaches the windows below it, and its input is delivered
//! to the window it covers.
@@ -26,9 +26,9 @@ use std::{
use anyhow::{Context as _, anyhow};
use collections::{FxHashMap, FxHashSet};
use gpui::{
- Bounds, ComposedScene, CompositionSurfaceId, ContentMask, DevicePixels,
- PlatformCompositionSurface, PlatformCompositionSurfaceContent, PlatformSurfaceAttachment,
- PlatformWindow, Point, ScaledPixels, Scene, Size,
+ Bounds, ComposedScene, CompositionSurfaceId, DevicePixels, PlatformCompositionSurface,
+ PlatformCompositionSurfaceContent, PlatformSurfaceAttachment, PlatformWindow, Point, Scene,
+ Size,
};
use gpui_util::ResultExt as _;
use gpui_wgpu::{WgpuRenderer, WgpuSurfaceConfig, wgpu};
@@ -67,6 +67,7 @@ impl Composition {
order: Vec::new(),
occluders: FxHashMap::default(),
overlay: Overlay::Unknown,
+ input_windows: Vec::new(),
})))
}
@@ -87,6 +88,7 @@ struct X11Composition {
order: Vec<PlatformCompositionSurface>,
occluders: FxHashMap<CompositionSurfaceId, Vec<Bounds<DevicePixels>>>,
overlay: Overlay,
+ input_windows: Vec<xproto::Window>,
}
struct X11NativeSurfaceState {
@@ -203,6 +205,7 @@ impl X11NativeSurfaceState {
}
fn destroy(&mut self) {
+ INPUT_WINDOWS.with(|windows| windows.borrow_mut().remove(&self.x_window));
if std::mem::replace(&mut self.destroyed, true) {
return;
}
@@ -224,107 +227,8 @@ fn x11_rectangle(bounds: Bounds<DevicePixels>) -> xproto::Rectangle {
}
}
-/// Past this many rectangles an occluding surface is cut out by its bounding
-/// box, which keeps shape requests small for text-heavy overlays.
-const MAX_OCCLUDER_RECTANGLES: usize = 256;
-
-/// Returns the device-pixel rectangles covered by a scene's primitives.
-///
-/// Shadows are left out: a cutout can only show the window's pixels, not blend
-/// a translucent shadow over the native content, so cutting out a shadow would
-/// replace native content with GPUI's background around every overlay.
fn scene_occluders(scene: &Scene, opaque_only: bool) -> Vec<Bounds<DevicePixels>> {
- fn clipped(
- bounds: Bounds<ScaledPixels>,
- content_mask: &ContentMask<ScaledPixels>,
- ) -> Bounds<ScaledPixels> {
- bounds.intersect(&content_mask.bounds)
- }
-
- let mut rectangles = Vec::new();
- rectangles.extend(
- scene
- .quads
- .iter()
- .filter(|quad| !opaque_only || quad.background.is_opaque())
- .map(|quad| clipped(quad.bounds, &quad.content_mask)),
- );
- rectangles.extend(
- scene
- .paths
- .iter()
- .map(|path| clipped(path.bounds, &path.content_mask)),
- );
- rectangles.extend(
- scene
- .underlines
- .iter()
- .map(|underline| clipped(underline.bounds, &underline.content_mask)),
- );
- rectangles.extend(
- scene
- .monochrome_sprites
- .iter()
- .map(|sprite| clipped(sprite.bounds, &sprite.content_mask)),
- );
- rectangles.extend(
- scene
- .subpixel_sprites
- .iter()
- .map(|sprite| clipped(sprite.bounds, &sprite.content_mask)),
- );
- rectangles.extend(
- scene
- .polychrome_sprites
- .iter()
- .map(|sprite| clipped(sprite.bounds, &sprite.content_mask)),
- );
- rectangles.extend(
- scene
- .surfaces
- .iter()
- .map(|surface| clipped(surface.bounds, &surface.content_mask)),
- );
-
- let mut rectangles = rectangles
- .into_iter()
- .map(|bounds| {
- let origin = bounds
- .origin
- .map(|value| DevicePixels(value.0.floor() as i32));
- let corner = bounds
- .bottom_right()
- .map(|value| DevicePixels(value.0.ceil() as i32));
- Bounds::from_corners(origin, corner)
- })
- .filter(|bounds| !bounds.is_empty())
- .collect::<Vec<_>>();
-
- // Content usually sits on a background quad, so most rectangles are
- // covered by a larger one.
- rectangles.sort_by_key(|bounds| std::cmp::Reverse(bounds.size.width.0 * bounds.size.height.0));
- let mut kept: Vec<Bounds<DevicePixels>> = Vec::new();
- for rectangle in rectangles {
- let covered = kept.iter().any(|kept| {
- kept.origin.x <= rectangle.origin.x
- && kept.origin.y <= rectangle.origin.y
- && kept.bottom_right().x >= rectangle.bottom_right().x
- && kept.bottom_right().y >= rectangle.bottom_right().y
- });
- if !covered {
- kept.push(rectangle);
- }
- }
- if kept.len() > MAX_OCCLUDER_RECTANGLES {
- let union = kept
- .iter()
- .copied()
- .reduce(|union, bounds| union.union(&bounds))
- .into_iter()
- .collect();
- return union;
- }
- kept
+ super::occluders::scene_occluders(scene, opaque_only)
}
/// A child window slot for content produced outside GPUI's renderer. Its
@@ -402,6 +306,8 @@ impl X11Composition {
deviceid: XINPUT_ALL_DEVICE_GROUPS,
mask: vec![
xinput::XIEventMask::MOTION
+ | xinput::XIEventMask::ENTER
+ | xinput::XIEventMask::LEAVE
| xinput::XIEventMask::BUTTON_PRESS
| xinput::XIEventMask::BUTTON_RELEASE,
],
@@ -530,6 +436,12 @@ impl X11Composition {
}
fn destroy(&mut self) {
+ INPUT_WINDOWS.with(|windows| {
+ let mut windows = windows.borrow_mut();
+ for id in self.input_windows.drain(..) {
+ windows.remove(&id);
+ }
+ });
for surface in self
.native_surfaces
.drain(..)
@@ -656,8 +568,13 @@ impl X11Composition {
/// Draws `scene`, the GPUI content above the native surfaces, into the
/// overlay window, creating it on first use and unmapping it when the
- /// scene is empty.
- fn draw_overlay(&mut self, window: &X11Window, scene: &Scene) -> anyhow::Result<()> {
+ /// scene and its input region are empty.
+ fn draw_overlay(
+ &mut self,
+ window: &X11Window,
+ scene: &Scene,
+ input: Vec<Bounds<DevicePixels>>,
+ ) -> anyhow::Result<()> {
let Overlay::Supported {
root,
colormap,
@@ -667,7 +584,7 @@ impl X11Composition {
else {
return Ok(());
};
- if scene.is_empty() {
+ if scene.is_empty() && input.is_empty() {
if let Some(overlay) = overlay.take() {
*spare_renderer = Some(overlay.destroy(&self.xcb));
xcb_flush(&self.xcb);
@@ -712,9 +629,13 @@ impl X11Composition {
size,
)?;
INPUT_WINDOWS.with(|windows| {
- windows
- .borrow_mut()
- .insert(created.x_window, window.0.clone())
+ windows.borrow_mut().insert(
+ created.x_window,
+ InputWindow {
+ window: window.0.clone(),
+ native: None,
+ },
+ )
});
overlay.insert(created)
}
@@ -733,7 +654,7 @@ impl X11Composition {
),
size,
)?;
- overlay.set_input(&self.xcb, scene_occluders(scene, false))?;
+ overlay.set_input(&self.xcb, input)?;
overlay.map(&self.xcb)?;
overlay.raise_above(&self.xcb, self.x_window);
overlay.renderer.draw(scene);
@@ -760,16 +681,60 @@ impl X11Composition {
}
thread_local! {
- /// The windows that overlay windows cover, by overlay window, so input on
- /// an overlay window is delivered to the window below it.
- static INPUT_WINDOWS: RefCell<FxHashMap<xproto::Window, X11WindowStatePtr>> =
+ /// Existing overlay routing plus opted-in native pointer slots. Native
+ /// entries never participate in keyboard or window-configuration lookup.
+ static INPUT_WINDOWS: RefCell<FxHashMap<xproto::Window, InputWindow>> =
RefCell::default();
}
-/// `X11Client::get_window`'s fallback for an overlay window: the window it
-/// covers, which shares its coordinate space.
+struct InputWindow {
+ window: X11WindowStatePtr,
+ native: Option<Weak<dyn PlatformSurfaceAttachment>>,
+}
+
+/// Overlays and opted-in native slots share the existing window dispatcher.
pub(crate) fn input_window(x_window: xproto::Window) -> Option<X11WindowStatePtr> {
- INPUT_WINDOWS.with(|windows| windows.borrow().get(&x_window).cloned())
+ INPUT_WINDOWS.with(|windows| {
+ windows
+ .borrow()
+ .get(&x_window)
+ .filter(|input| input.native.is_none())
+ .map(|input| input.window.clone())
+ })
+}
+
+pub(crate) fn pointer_window(x_window: xproto::Window) -> Option<X11WindowStatePtr> {
+ INPUT_WINDOWS.with(|windows| {
+ let windows = windows.borrow();
+ let input = windows.get(&x_window)?;
+ if let Some(native) = &input.native {
+ native.upgrade()?;
+ }
+ Some(input.window.clone())
+ })
+}
+
+pub(crate) fn pointer_window_id(x_window: xproto::Window) -> xproto::Window {
+ pointer_window(x_window).map_or(x_window, |window| window.x_window)
+}
+
+/// Native XInput coordinates are local to the slot, including during its grab.
+pub(crate) fn pointer_position(
+ x_window: xproto::Window,
+ position: Point<gpui::Pixels>,
+ scale: f32,
+) -> Point<gpui::Pixels> {
+ let native = INPUT_WINDOWS.with(|windows| {
+ windows
+ .borrow()
+ .get(&x_window)
+ .and_then(|input| input.native.clone())
+ });
+ native
+ .and_then(|native| native.upgrade())
+ .map_or(position, |native| {
+ position + native.bounds().to_pixels(scale).origin
+ })
}
/// Whether a window draws the GPUI content above its native surfaces into an
@@ -1096,9 +1061,18 @@ pub(crate) fn draw_composed(window: &X11Window, scene: ComposedScene<'_>) {
// the shadows and backdrops blended over native content.
let mut window_scene = Scene::default();
let mut overlay_scene = Scene::default();
+ let mut overlay_input = Vec::new();
let mut occluders = FxHashMap::default();
for layer in scene.layers() {
let in_overlay = overlay_surfaces.contains(&layer.surface);
+ if in_overlay {
+ overlay_input.extend(
+ scene
+ .layer_hitboxes(layer)
+ .iter()
+ .map(|hitbox| hitbox.bounds.to_device_pixels(window.scale_factor())),
+ );
+ }
let cuts_out = track_occluders && layer.surface != base_surface;
let mut layer_scene = Scene::default();
for range in &layer.ranges {
@@ -1137,7 +1111,7 @@ pub(crate) fn draw_composed(window: &X11Window, scene: ComposedScene<'_>) {
}
if overlay_surfaces.is_empty() {
composition.hide_overlay();
- } else if let Err(error) = composition.draw_overlay(window, &overlay_scene) {
+ } else if let Err(error) = composition.draw_overlay(window, &overlay_scene, overlay_input) {
log::error!("drawing the X11 composition overlay window: {error:#}");
}
drop(composition);
@@ -1194,83 +1168,52 @@ pub(crate) fn set_composition_order(
composition
.occluders
.retain(|id, _| surfaces.iter().any(|surface| surface.id == *id));
- composition.apply_order()
-}
-
-#[cfg(test)]
-mod tests {
- use super::scene_occluders;
- use gpui::{
- Bounds, ContentMask, DevicePixels, Quad, ScaledPixels, Scene, Shadow, black, point, size,
- };
-
- fn quad(bounds: Bounds<ScaledPixels>, mask: Bounds<ScaledPixels>) -> Quad {
- Quad {
- bounds,
- content_mask: ContentMask { bounds: mask },
- ..Default::default()
+ composition.apply_order()?;
+ let inputs = surfaces
+ .iter()
+ .filter(|surface| match &surface.content {
+ PlatformCompositionSurfaceContent::Native(attachment)
+ | PlatformCompositionSurfaceContent::ExternalGpu(attachment) => {
+ attachment.input_forwarding()
+ }
+ PlatformCompositionSurfaceContent::Gpui => false,
+ })
+ .map(|surface| {
+ let attachment = match &surface.content {
+ PlatformCompositionSurfaceContent::Native(attachment)
+ | PlatformCompositionSurfaceContent::ExternalGpu(attachment) => attachment,
+ PlatformCompositionSurfaceContent::Gpui => {
+ anyhow::bail!("GPUI surface cannot forward native input")
+ }
+ };
+ let handle = attachment
+ .platform_handle()?
+ .downcast::<rwh::RawWindowHandle>()
+ .map_err(|_| anyhow!("native X11 input surface has no window handle"))?;
+ let id = match *handle {
+ rwh::RawWindowHandle::Xcb(handle) => handle.window.get(),
+ rwh::RawWindowHandle::Xlib(handle) => u32::try_from(handle.window)?,
+ _ => anyhow::bail!("native input surface requires an X11 window"),
+ };
+ anyhow::ensure!(id != 0, "native X11 input window is null");
+ Ok((
+ id,
+ InputWindow {
+ window: window.0.clone(),
+ native: Some(Rc::downgrade(attachment)),
+ },
+ ))
+ })
+ .collect::<anyhow::Result<Vec<_>>>()?;
+ INPUT_WINDOWS.with(|windows| {
+ let mut windows = windows.borrow_mut();
+ for id in composition.input_windows.drain(..) {
+ windows.remove(&id);
}
- }
-
- fn scaled(x: f32, y: f32, width: f32, height: f32) -> Bounds<ScaledPixels> {
- Bounds::new(
- point(ScaledPixels(x), ScaledPixels(y)),
- size(ScaledPixels(width), ScaledPixels(height)),
- )
- }
-
- fn device(x: i32, y: i32, width: i32, height: i32) -> Bounds<DevicePixels> {
- Bounds::new(
- point(DevicePixels(x), DevicePixels(y)),
- size(DevicePixels(width), DevicePixels(height)),
- )
- }
-
- #[test]
- fn scene_occluders_skip_shadows_and_covered_rectangles() {
- let everything = scaled(0., 0., 1000., 1000.);
- let mut scene = Scene::default();
- scene.insert_primitive(Shadow {
- order: 0,
- blur_radius: ScaledPixels(24.),
- bounds: scaled(500., 500., 100., 100.),
- corner_radii: Default::default(),
- content_mask: ContentMask { bounds: everything },
- color: Default::default(),
- element_bounds: scaled(500., 500., 100., 100.),
- element_corner_radii: Default::default(),
- inset: 0,
- pad: 0,
- });
- scene.insert_primitive(quad(scaled(10., 10., 100., 50.), everything));
- scene.insert_primitive(quad(scaled(20.5, 20.5, 10., 10.), everything));
- scene.insert_primitive(quad(
- scaled(200., 0., 100., 100.),
- scaled(250., 0., 20., 40.),
- ));
- scene.finish();
-
- let mut occluders = scene_occluders(&scene, false);
- occluders.sort_by_key(|bounds| bounds.origin.x);
- assert_eq!(occluders, [device(10, 10, 100, 50), device(250, 0, 20, 40)]);
- }
-
- #[test]
- fn opaque_scene_occluders_skip_translucent_backgrounds() {
- let everything = scaled(0., 0., 1000., 1000.);
- let mut scene = Scene::default();
- // A dialog's translucent backdrop over the whole window, and its panel.
- scene.insert_primitive(Quad {
- background: black().opacity(0.4).into(),
- ..quad(everything, everything)
- });
- scene.insert_primitive(Quad {
- background: black().into(),
- ..quad(scaled(300., 200., 400., 300.), everything)
- });
- scene.finish();
-
- assert_eq!(scene_occluders(&scene, true), [device(300, 200, 400, 300)]);
- assert_eq!(scene_occluders(&scene, false), [device(0, 0, 1000, 1000)]);
- }
+ for (id, input) in inputs {
+ windows.insert(id, input);
+ composition.input_windows.push(id);
+ }
+ });
+ Ok(())
}
diff --git a/third-party/gpui-fast/crates/gpui_linux/src/linux/wayland/client.rs b/third-party/gpui-fast/crates/gpui_linux/src/linux/wayland/client.rs
index aa81b3753b..56d78147d0 100644
--- a/third-party/gpui-fast/crates/gpui_linux/src/linux/wayland/client.rs
+++ b/third-party/gpui-fast/crates/gpui_linux/src/linux/wayland/client.rs
@@ -2417,12 +2417,17 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
..
} => {
let position = point(px(surface_x as f32), px(surface_y as f32));
+ let position = crate::fast::composition::wayland::pointer_position(
+ &mut state,
+ Some(&surface.id()),
+ position,
+ );
state.serial_tracker.update(SerialKind::MouseEnter, serial);
state.mouse_location = Some(position);
state.button_pressed = None;
if let Some(window) =
- crate::fast::composition::wayland::get_input_window(&mut state, &surface.id())
+ crate::fast::composition::wayland::get_pointer_window(&mut state, &surface.id())
{
state.mouse_focused_window = Some(window.clone());
@@ -2480,7 +2485,11 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
if state.mouse_focused_window.is_none() {
return;
}
- state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32)));
+ state.mouse_location = Some(crate::fast::composition::wayland::pointer_position(
+ &mut state,
+ None,
+ point(px(surface_x as f32), px(surface_y as f32)),
+ ));
state.restore_cursor_after_hide();
if let Some(window) = state.mouse_focused_window.clone() {
diff --git a/third-party/gpui-fast/crates/gpui_linux/src/linux/x11/client.rs b/third-party/gpui-fast/crates/gpui_linux/src/linux/x11/client.rs
index 7bf05cdb8e..9c4414909a 100644
--- a/third-party/gpui-fast/crates/gpui_linux/src/linux/x11/client.rs
+++ b/third-party/gpui-fast/crates/gpui_linux/src/linux/x11/client.rs
@@ -1233,7 +1233,9 @@ impl X11Client {
window.handle_input(PlatformInput::KeyUp(gpui::KeyUpEvent { keystroke }));
}
Event::XinputButtonPress(event) => {
- let window = self.get_window(event.event)?;
+ let window = self
+ .get_window(event.event)
+ .or_else(|| crate::fast::composition::x11::pointer_window(event.event))?;
let mut state = self.0.borrow_mut();
let modifiers = modifiers_from_xinput_info(event.mods);
@@ -1243,6 +1245,11 @@ impl X11Client {
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor),
);
+ let position = crate::fast::composition::x11::pointer_position(
+ event.event,
+ position,
+ state.scale_factor,
+ );
if state.composing && state.ximc.is_some() {
drop(state);
@@ -1310,7 +1317,9 @@ impl X11Client {
}
}
Event::XinputButtonRelease(event) => {
- let window = self.get_window(event.event)?;
+ let window = self
+ .get_window(event.event)
+ .or_else(|| crate::fast::composition::x11::pointer_window(event.event))?;
let mut state = self.0.borrow_mut();
let modifiers = modifiers_from_xinput_info(event.mods);
state.modifiers = modifiers;
@@ -1319,6 +1328,11 @@ impl X11Client {
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor),
);
+ let position = crate::fast::composition::x11::pointer_position(
+ event.event,
+ position,
+ state.scale_factor,
+ );
match button_or_scroll_from_event_detail(event.detail) {
Some(ButtonOrScroll::Button(button)) => {
let click_count = state.current_count;
@@ -1335,7 +1349,9 @@ impl X11Client {
}
}
Event::XinputMotion(event) => {
- let window = self.get_window(event.event)?;
+ let window = self
+ .get_window(event.event)
+ .or_else(|| crate::fast::composition::x11::pointer_window(event.event))?;
let mut state = self.0.borrow_mut();
state.restore_cursor_after_hide();
if window.is_blocked() {
@@ -1370,6 +1386,11 @@ impl X11Client {
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor),
);
+ let position = crate::fast::composition::x11::pointer_position(
+ event.event,
+ position,
+ state.scale_factor,
+ );
let modifiers = modifiers_from_xinput_info(event.mods);
state.modifiers = modifiers;
drop(state);
@@ -1396,10 +1417,14 @@ impl X11Client {
}
}
Event::XinputEnter(event) if event.mode == xinput::NotifyMode::NORMAL => {
- let window = self.get_window(event.event)?;
+ let window = self
+ .get_window(event.event)
+ .or_else(|| crate::fast::composition::x11::pointer_window(event.event))?;
window.set_hovered(true);
let mut state = self.0.borrow_mut();
- state.mouse_focused_window = Some(event.event);
+ state.mouse_focused_window = Some(
+ crate::fast::composition::x11::pointer_window_id(event.event),
+ );
state.restore_cursor_after_hide();
}
Event::XinputLeave(event) if event.mode == xinput::NotifyMode::NORMAL => {
@@ -1413,11 +1438,18 @@ impl X11Client {
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor),
);
+ let position = crate::fast::composition::x11::pointer_position(
+ event.event,
+ position,
+ state.scale_factor,
+ );
let modifiers = modifiers_from_xinput_info(event.mods);
state.modifiers = modifiers;
drop(state);
- let window = self.get_window(event.event)?;
+ let window = self
+ .get_window(event.event)
+ .or_else(|| crate::fast::composition::x11::pointer_window(event.event))?;
window.handle_input(PlatformInput::MouseExited(gpui::MouseExitEvent {
pressed_button,
position,
diff --git a/third-party/gpui-fast/crates/gpui_macos/Cargo.toml b/third-party/gpui-fast/crates/gpui_macos/Cargo.toml
index d4ceb14874..6631dc230d 100644
--- a/third-party/gpui-fast/crates/gpui_macos/Cargo.toml
+++ b/third-party/gpui-fast/crates/gpui_macos/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "gpui-fast-macos"
-version = "0.1.7"
+version = "0.1.10"
edition = "2024"
publish = false
license = "Apache-2.0"
@@ -51,8 +51,7 @@ screen-capture = ["gpui/screen-capture", "dep:objc2-core-graphics", "dep:objc2-c
[dependencies.gpui]
default-features = false
package = "gpui-fast"
-version = "=0.1.7"
-path = "../gpui"
+version = "=0.1.10"
[target]
@@ -105,7 +104,7 @@ version = "0.3.34"
[target."cfg(target_os = \"macos\")".dependencies.gpui_apple]
package = "gpui-fast-apple"
-version = "=0.1.7"
+version = "=0.1.10"
[target."cfg(target_os = \"macos\")".dependencies.gpui_util]
package = "gpui-pre-util"
@@ -198,12 +197,11 @@ features = ["CVBuffer", "CVImageBuffer", "CVHostTime"]
[target."cfg(target_os = \"macos\")".dev-dependencies.gpui]
default-features = false
-features = ["test-support"]
+features = [ "test-support" ]
package = "gpui-fast"
-version = "=0.1.7"
-path = "../gpui"
+version = "=0.1.10"
[target."cfg(target_os = \"macos\")".dev-dependencies.gpui_apple]
-features = ["test-support"]
+features = [ "test-support" ]
package = "gpui-fast-apple"
-version = "=0.1.7"
+version = "=0.1.10"
diff --git a/third-party/gpui-fast/crates/gpui_macos/src/fast/composition.rs b/third-party/gpui-fast/crates/gpui_macos/src/fast/composition.rs
index 2b4dbc1f8c..38b802235c 100644
--- a/third-party/gpui-fast/crates/gpui_macos/src/fast/composition.rs
+++ b/third-party/gpui-fast/crates/gpui_macos/src/fast/composition.rs
@@ -9,25 +9,24 @@
//! the window's view.
//!
//! An overlay view covers the whole window, so it takes mouse events only
-//! while its surface drew something last frame, and hands them to the
-//! window's view; otherwise AppKit's hit testing passes through it to the
-//! native views below.
+//! within the prepared GPUI hitboxes of its surface and hands them to the
+//! window's view. Elsewhere, AppKit hit testing reaches the native views below.
use std::{
any::Any,
- cell::Cell,
+ cell::{Cell, RefCell},
ffi::c_void,
ptr::{self, NonNull},
rc::Rc,
sync::{
Arc, Weak,
- atomic::{AtomicBool, AtomicPtr, Ordering},
+ atomic::{AtomicPtr, Ordering},
},
};
use anyhow::{Context as _, Result, anyhow};
use cocoa::{
- appkit::{NSView, NSViewHeightSizable, NSViewWidthSizable},
+ appkit::{NSEvent, NSEventType, NSView, NSViewHeightSizable, NSViewWidthSizable},
base::{id, nil},
foundation::{NSPoint, NSRect, NSSize},
};
@@ -40,7 +39,7 @@ use objc::{
class,
declare::ClassDecl,
msg_send,
- runtime::{Class, NO, Object, Sel, YES},
+ runtime::{BOOL, Class, NO, Object, Sel, YES},
sel, sel_impl,
};
use parking_lot::Mutex;
@@ -54,9 +53,10 @@ use crate::{
};
/// The ivar of a `GPUIOverlayView` holding its surface's
-/// [`MacGpuiSurface::input_active`], an `Arc<AtomicBool>` turned into a raw
+/// [`MacGpuiSurface::input_regions`], a UI-thread-local `Rc` turned into a raw
/// pointer.
-const OVERLAY_INPUT_IVAR: &str = "overlayInputActive";
+const OVERLAY_INPUT_IVAR: &str = "overlayInputRegions";
+const NATIVE_INPUT_IVAR: &str = "compositionInputActive";
/// The `GPUIOverlayView` class, registered by [`build_overlay_view_class`].
static OVERLAY_VIEW_CLASS: AtomicPtr<Class> = AtomicPtr::new(ptr::null_mut());
@@ -73,6 +73,14 @@ pub(crate) struct MacComposition {
renderer_context: renderer::Context,
/// Native identities, parents and sibling order, independent of geometry.
view_order: Vec<(CompositionSurfaceId, usize, usize)>,
+ native_views: Rc<Vec<usize>>,
+ input_order: Rc<Vec<InputLayer>>,
+ native_event: usize,
+ native_buttons: u64,
+ native_owner: Option<usize>,
+ input_forwarders: FxHashSet<usize>,
+ event_result: gpui::DispatchEventResult,
+ forwarding_scroll: bool,
}
impl MacComposition {
@@ -82,17 +90,32 @@ impl MacComposition {
gpui_surfaces: FxHashMap::default(),
renderer_context,
view_order: Vec::new(),
+ native_views: Rc::new(Vec::new()),
+ input_order: Rc::new(Vec::new()),
+ native_event: 0,
+ native_buttons: 0,
+ native_owner: None,
+ input_forwarders: FxHashSet::default(),
+ event_result: gpui::DispatchEventResult {
+ propagate: true,
+ default_prevented: false,
+ },
+ forwarding_scroll: false,
}
}
}
+enum InputLayer {
+ Gpui(Rc<RefCell<Vec<gpui::Hitbox>>>),
+ Native(usize),
+}
+
/// A GPUI surface above the base one: an overlay view and its renderer.
struct MacGpuiSurface {
view: NonNull<Object>,
renderer: renderer::Renderer,
- /// Whether the surface drew anything last frame, and so takes the mouse
- /// events over it; shared with the view.
- input_active: Arc<AtomicBool>,
+ /// Prepared GPUI hitboxes, shared with the overlay view on the UI thread.
+ input_regions: Rc<RefCell<Vec<gpui::Hitbox>>>,
}
/// Registers the `GPUIOverlayView` class, with the window's other classes.
@@ -139,18 +162,321 @@ pub(crate) unsafe fn build_overlay_view_class() {
OVERLAY_VIEW_CLASS.store(class as *mut Class, Ordering::Release);
}
-extern "C" fn overlay_hit_test(this: &Object, _: Sel, _: NSPoint) -> id {
- let active = unsafe {
+extern "C" fn overlay_hit_test(this: &Object, _: Sel, point: NSPoint) -> id {
+ let regions = unsafe {
let raw: *mut c_void = *this.get_ivar(OVERLAY_INPUT_IVAR);
- &*(raw as *const AtomicBool)
+ &*(raw as *const RefCell<Vec<gpui::Hitbox>>)
};
- if active.load(Ordering::Acquire) {
+ let local: NSPoint = unsafe {
+ let parent: id = msg_send![this, superview];
+ msg_send![this, convertPoint: point fromView: parent]
+ };
+ let flipped: objc::runtime::BOOL = unsafe { msg_send![this, isFlipped] };
+ let height = unsafe { NSView::bounds(this as *const Object as id) }
+ .size
+ .height;
+ let point = gpui::point(
+ px(local.x as f32),
+ px(if flipped == YES {
+ local.y
+ } else {
+ height - local.y
+ } as f32),
+ );
+ if regions
+ .borrow()
+ .iter()
+ .rev()
+ .any(|region| region.bounds.contains(&point))
+ {
this as *const Object as id
} else {
nil
}
}
+/// Native pointer input runs GPUI capture before AppKit delivers it.
+/// No handlers are run from hitTest, and no process-wide event monitor is installed.
+pub(crate) unsafe fn register_window_input(decl: &mut ClassDecl, superclass: &Class) {
+ extern "C" fn window(this: &Object, _: Sel, event: id) {
+ send_window_event(this, event, class!(NSWindow));
+ }
+ extern "C" fn panel(this: &Object, _: Sel, event: id) {
+ send_window_event(this, event, class!(NSPanel));
+ }
+ let handler: extern "C" fn(&Object, Sel, id) = if superclass == class!(NSPanel) {
+ panel
+ } else {
+ window
+ };
+ unsafe {
+ decl.add_ivar::<BOOL>(NATIVE_INPUT_IVAR);
+ decl.add_method(sel!(sendEvent:), handler);
+ }
+}
+
+fn send_window_event(this: &Object, event: id, superclass: &Class) {
+ if unsafe { *this.get_ivar::<BOOL>(NATIVE_INPUT_IVAR) } == NO {
+ unsafe {
+ let _: () = msg_send![super(this, superclass), sendEvent: event];
+ }
+ return;
+ }
+ let kind = unsafe { event.eventType() };
+ let observe = matches!(
+ kind,
+ NSEventType::NSLeftMouseDown
+ | NSEventType::NSRightMouseDown
+ | NSEventType::NSOtherMouseDown
+ | NSEventType::NSLeftMouseUp
+ | NSEventType::NSRightMouseUp
+ | NSEventType::NSOtherMouseUp
+ );
+ let wheel = kind == NSEventType::NSScrollWheel;
+ let dragged = matches!(
+ kind,
+ NSEventType::NSLeftMouseDragged
+ | NSEventType::NSRightMouseDragged
+ | NSEventType::NSOtherMouseDragged
+ );
+ let pointer = observe || wheel || dragged || kind == NSEventType::NSMouseMoved;
+ if !pointer {
+ unsafe {
+ let _: () = msg_send![super(this, superclass), sendEvent: event];
+ }
+ return;
+ }
+ let state = unsafe { get_window_state(this) };
+ let (root, native_views, captured, has_forwarders) = {
+ let state = state.lock();
+ (
+ state.native_view,
+ state.fast_composition.native_views.clone(),
+ state.fast_composition.native_owner,
+ !state.fast_composition.input_forwarders.is_empty(),
+ )
+ };
+ if kind == NSEventType::NSMouseMoved && !has_forwarders {
+ unsafe {
+ let _: () = msg_send![super(this, superclass), sendEvent: event];
+ }
+ return;
+ }
+ let up = matches!(
+ kind,
+ NSEventType::NSLeftMouseUp | NSEventType::NSRightMouseUp | NSEventType::NSOtherMouseUp
+ );
+ let button = if observe {
+ let number = unsafe { event.buttonNumber() };
+ 1u64.checked_shl(number as u32).unwrap_or(0)
+ } else {
+ 0
+ };
+ if up {
+ let mut state = state.lock();
+ state.fast_composition.native_buttons &= !button;
+ if state.fast_composition.native_buttons == 0 {
+ state.fast_composition.native_owner = None;
+ }
+ }
+ let native = if captured.is_some() && (dragged || up) {
+ captured
+ } else {
+ unsafe {
+ let parent: id = msg_send![root.as_ptr(), superview];
+ let point: NSPoint =
+ msg_send![parent, convertPoint: event.locationInWindow() fromView: nil];
+ let target: id = msg_send![root.as_ptr(), hitTest: point];
+ native_for_target(target, &native_views)
+ }
+ };
+ let forward =
+ native.is_some_and(|id| state.lock().fast_composition.input_forwarders.contains(&id));
+ // A windowless producer consumes its press instead of starting AppKit view
+ // tracking. Keep the recipient through drag/up, even when capture dismissed
+ // a popup and withheld the down event from the producer.
+ if forward && observe && !up {
+ let mut state = state.lock();
+ state.fast_composition.native_buttons |= button;
+ state.fast_composition.native_owner = native;
+ }
+ if native.is_some() && (observe || forward) {
+ state.lock().fast_composition.event_result = gpui::DispatchEventResult {
+ propagate: true,
+ default_prevented: false,
+ };
+ // This wheel will follow AppKit's original path if unhandled; it must
+ // not also be forwarded by the overlay's unhandled-wheel hook.
+ let forwarding = if wheel {
+ Some(std::mem::replace(
+ &mut state.lock().fast_composition.forwarding_scroll,
+ true,
+ ))
+ } else {
+ None
+ };
+ handle_view_event(unsafe { root.as_ref() }, sel!(sendEvent:), event);
+ let result = {
+ let mut state = state.lock();
+ if let Some(previous) = forwarding {
+ state.fast_composition.forwarding_scroll = previous;
+ }
+ state.fast_composition.event_result.clone()
+ };
+ if forward
+ || (!(captured.is_some() && up) && (!result.propagate || result.default_prevented))
+ {
+ return;
+ }
+ }
+ if native.is_none() {
+ unsafe {
+ let _: () = msg_send![super(this, superclass), sendEvent: event];
+ }
+ return;
+ }
+ let previous = {
+ let mut state = state.lock();
+ if observe && !up {
+ state.fast_composition.native_buttons |= button;
+ state.fast_composition.native_owner = native;
+ }
+ let previous = state.fast_composition.native_event;
+ state.fast_composition.native_event = event as usize;
+ previous
+ };
+ unsafe {
+ let _: () = msg_send![super(this, superclass), sendEvent: event];
+ }
+ state.lock().fast_composition.native_event = previous;
+}
+
+unsafe fn belongs_to_native(view: id, native: &[usize]) -> bool {
+ unsafe { native_for_target(view, native).is_some() }
+}
+
+unsafe fn native_for_target(mut view: id, native: &[usize]) -> Option<usize> {
+ let overlay = OVERLAY_VIEW_CLASS.load(Ordering::Acquire);
+ while !view.is_null() {
+ if !overlay.is_null() {
+ let gpui: BOOL = unsafe { msg_send![view, isKindOfClass: overlay] };
+ if gpui == YES {
+ return None;
+ }
+ }
+ if native.contains(&(view as usize)) {
+ return Some(view as usize);
+ }
+ view = unsafe { msg_send![view, superview] };
+ }
+ None
+}
+
+pub(crate) fn native_event_observed(state: &MacWindowState, event: id) -> bool {
+ state.fast_composition.native_event == event as usize
+}
+
+pub(crate) fn complete_event(
+ state: &Arc<Mutex<MacWindowState>>,
+ event: id,
+ callback: Box<dyn FnMut(gpui::PlatformInput) -> gpui::DispatchEventResult>,
+ result: gpui::DispatchEventResult,
+) {
+ let forward = {
+ let mut state = state.lock();
+ state.event_callback = Some(callback);
+ state.fast_composition.event_result = result.clone();
+ !state.fast_composition.native_views.is_empty()
+ && result.propagate
+ && !result.default_prevented
+ };
+ if forward {
+ forward_unhandled_scroll(state, event);
+ }
+}
+
+/// When a GPUI overlay permits scrolling underneath it, use the managed native
+/// views and AppKit's own hit testing. This never changes visibility or geometry.
+fn forward_unhandled_scroll(state: &Arc<Mutex<MacWindowState>>, event: id) {
+ if unsafe { event.eventType() } != NSEventType::NSScrollWheel {
+ return;
+ }
+ let (input_order, height) = {
+ let state = state.lock();
+ if state.fast_composition.forwarding_scroll {
+ return;
+ }
+ (
+ state.fast_composition.input_order.clone(),
+ state.content_size().height,
+ )
+ };
+ let location = unsafe { event.locationInWindow() };
+ let point = gpui::point(px(location.x as f32), height - px(location.y as f32));
+ for layer in input_order.iter().rev() {
+ match layer {
+ InputLayer::Gpui(regions) => {
+ if regions.borrow().iter().any(|region| {
+ region.behavior == gpui::HitboxBehavior::BlockMouse
+ && region.bounds.contains(&point)
+ }) {
+ return;
+ }
+ }
+ InputLayer::Native(native) => {
+ let target = unsafe { native_scroll_target(*native as id, location) };
+ if target.is_null() {
+ continue;
+ }
+ let previous = {
+ let mut state = state.lock();
+ state.fast_composition.forwarding_scroll = true;
+ std::mem::replace(&mut state.fast_composition.native_event, event as usize)
+ };
+ unsafe {
+ let _: () = msg_send![target, scrollWheel: event];
+ }
+ let mut state = state.lock();
+ state.fast_composition.native_event = previous;
+ state.fast_composition.forwarding_scroll = false;
+ return;
+ }
+ }
+ }
+}
+
+/// A nested GPUI overlay can decline a wheel event after AppKit selected it.
+/// Query its native siblings below it without changing the view tree or geometry.
+unsafe fn native_scroll_target(native: id, location: NSPoint) -> id {
+ let parent: id = unsafe { msg_send![native, superview] };
+ let point: NSPoint = unsafe { msg_send![parent, convertPoint: location fromView: nil] };
+ let mut selected: id = unsafe { msg_send![native, hitTest: point] };
+ if selected.is_null() || unsafe { belongs_to_native(selected, &[native as usize]) } {
+ return selected;
+ }
+ while selected != native && !selected.is_null() {
+ let parent: id = unsafe { msg_send![selected, superview] };
+ if parent.is_null() {
+ break;
+ }
+ let children: id = unsafe { msg_send![parent, subviews] };
+ let count: usize = unsafe { msg_send![children, count] };
+ let point: NSPoint = unsafe { msg_send![parent, convertPoint: location fromView: nil] };
+ for index in (0..count).rev() {
+ let child: id = unsafe { msg_send![children, objectAtIndex: index] };
+ if child == selected {
+ continue;
+ }
+ let target: id = unsafe { msg_send![child, hitTest: point] };
+ if !target.is_null() && unsafe { belongs_to_native(target, &[native as usize]) } {
+ return target;
+ }
+ }
+ selected = parent;
+ }
+ nil
+}
+
extern "C" fn handle_overlay_event(this: &Object, selector: Sel, native_event: id) {
let window_state = unsafe { get_window_state(this) };
let native_view = window_state.lock().native_view;
@@ -162,7 +488,7 @@ extern "C" fn dealloc_overlay_view(this: &Object, _: Sel) {
let _cleanup = get_window_state(this).lock().cleanup.guard();
drop_window_state(this);
let raw: *mut c_void = *this.get_ivar(OVERLAY_INPUT_IVAR);
- drop(Arc::from_raw(raw as *const AtomicBool));
+ drop(Rc::from_raw(raw as *const RefCell<Vec<gpui::Hitbox>>));
let _: () = msg_send![super(this, class!(NSView)), dealloc];
}
}
@@ -188,9 +514,11 @@ pub(crate) fn draw_composed(window_state: &Arc<Mutex<MacWindowState>>, scene: Co
if layer.surface == base_surface {
state.renderer.draw(&surface_scene);
} else if let Some(surface) = state.fast_composition.gpui_surfaces.get_mut(&layer.surface) {
- surface
- .input_active
- .store(!surface_scene.is_empty(), Ordering::Release);
+ {
+ let mut regions = surface.input_regions.borrow_mut();
+ regions.clear();
+ regions.extend(scene.layer_hitboxes(layer).iter().cloned());
+ }
surface.renderer.draw(&surface_scene);
} else {
log::error!(
@@ -338,8 +666,61 @@ pub(crate) fn set_composition_order(
for (_, view, parent) in &view_order {
(*parent as id).addSubview_(*view as id);
}
+ lock.fast_composition.native_views = Rc::new(
+ view_order
+ .iter()
+ .filter_map(|(id, view, _)| {
+ (*id != base_surface
+ && !lock.fast_composition.gpui_surfaces.contains_key(id))
+ .then_some(*view)
+ })
+ .collect(),
+ );
+ lock.fast_composition.input_order = Rc::new(
+ view_order
+ .iter()
+ .filter_map(|(id, view, _)| {
+ if *id == base_surface {
+ return None;
+ }
+ Some(match lock.fast_composition.gpui_surfaces.get(id) {
+ Some(surface) => InputLayer::Gpui(surface.input_regions.clone()),
+ None => InputLayer::Native(*view),
+ })
+ })
+ .collect(),
+ );
+ if lock.fast_composition.native_views.is_empty() {
+ lock.fast_composition.native_buttons = 0;
+ lock.fast_composition.native_owner = None;
+ }
lock.fast_composition.view_order = view_order;
}
+ lock.fast_composition.input_forwarders.clear();
+ for surface in surfaces.iter().filter(|surface| match &surface.content {
+ PlatformCompositionSurfaceContent::Native(attachment)
+ | PlatformCompositionSurfaceContent::ExternalGpu(attachment) => {
+ attachment.input_forwarding()
+ }
+ PlatformCompositionSurfaceContent::Gpui => false,
+ }) {
+ if let Some(view) = views_by_id.get(&surface.id) {
+ lock.fast_composition
+ .input_forwarders
+ .insert(*view as usize);
+ }
+ }
+ let window: id = msg_send![native_view, window];
+ if !window.is_null() {
+ (*window).set_ivar(
+ NATIVE_INPUT_IVAR,
+ if lock.fast_composition.native_views.is_empty() {
+ NO
+ } else {
+ YES
+ },
+ );
+ }
for surface in surfaces {
let Some(view) = views_by_id.get(&surface.id).copied() else {
continue;
@@ -365,7 +746,7 @@ unsafe fn create_gpui_surface(
window_state: &Arc<Mutex<MacWindowState>>,
state: &mut MacWindowState,
) -> Result<MacGpuiSurface> {
- let input_active = Arc::new(AtomicBool::new(false));
+ let input_regions = Rc::new(RefCell::new(Vec::<gpui::Hitbox>::new()));
let native_view = state.native_view.as_ptr() as id;
let class = OVERLAY_VIEW_CLASS.load(Ordering::Acquire) as *const Class;
anyhow::ensure!(!class.is_null(), "GPUIOverlayView is not registered");
@@ -379,7 +760,7 @@ unsafe fn create_gpui_surface(
);
(*view).set_ivar(
OVERLAY_INPUT_IVAR,
- Arc::into_raw(input_active.clone()) as *const c_void,
+ Rc::into_raw(input_regions.clone()) as *const c_void,
);
}
@@ -403,7 +784,7 @@ unsafe fn create_gpui_surface(
Ok(MacGpuiSurface {
view: NonNull::new(view).context("GPUI composition view is null")?,
renderer,
- input_active,
+ input_regions,
})
}
diff --git a/third-party/gpui-fast/crates/gpui_macos/src/window.rs b/third-party/gpui-fast/crates/gpui_macos/src/window.rs
index d07c40e919..4ba20b3465 100644
--- a/third-party/gpui-fast/crates/gpui_macos/src/window.rs
+++ b/third-party/gpui-fast/crates/gpui_macos/src/window.rs
@@ -431,6 +431,7 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
unsafe {
let mut decl = ClassDecl::new(name, superclass).unwrap();
decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
+ crate::fast::composition::register_window_input(&mut decl, superclass);
decl.add_method(sel!(dealloc), dealloc_window as extern "C" fn(&Object, Sel));
decl.add_method(
@@ -670,7 +671,7 @@ pub(crate) struct MacWindowState {
frame_source: Option<WindowFrameSource>,
pub(crate) renderer: renderer::Renderer,
request_frame_callback: Option<Box<dyn FnMut(RequestFrameOptions)>>,
- event_callback: Option<Box<dyn FnMut(PlatformInput) -> gpui::DispatchEventResult>>,
+ pub(crate) event_callback: Option<Box<dyn FnMut(PlatformInput) -> gpui::DispatchEventResult>>,
activate_callback: Option<Box<dyn FnMut(bool)>>,
visibility_callback: Option<Box<dyn FnMut(WindowVisibility)>>,
// `None` until a callback is registered, so notifications during
@@ -977,6 +978,7 @@ impl MacWindow {
display_id,
window_min_size,
tabbing_identifier,
+ use_display_p3,
..
}: WindowParams,
cursor_visible: Arc<AtomicBool>,
@@ -1113,6 +1115,7 @@ impl MacWindow {
native_view as *mut _,
bounds.size.map(|pixels| pixels.as_f32()),
false,
+ use_display_p3,
),
request_frame_callback: None,
event_callback: None,
@@ -2830,6 +2833,9 @@ pub(crate) extern "C" fn handle_view_event(this: &Object, _: Sel, native_event:
let window_state = unsafe { get_window_state(this) };
let weak_window_state = Arc::downgrade(&window_state);
let mut lock = window_state.as_ref().lock();
+ if crate::fast::composition::native_event_observed(&lock, native_event) {
+ return;
+ }
let window_height = lock.content_size().height;
let native_event_type = unsafe { native_event.eventType() };
match native_event_type {
@@ -2978,8 +2984,8 @@ pub(crate) extern "C" fn handle_view_event(this: &Object, _: Sel, native_event:
if let Some(mut callback) = lock.event_callback.take() {
drop(lock);
- callback(event);
- window_state.lock().event_callback = Some(callback);
+ let result = callback(event);
+ crate::fast::composition::complete_event(&window_state, native_event, callback, result);
}
}
}
diff --git a/third-party/gpui-fast/crates/gpui_windows/Cargo.toml b/third-party/gpui-fast/crates/gpui_windows/Cargo.toml
index ac80acf82f..fba62c776e 100644
--- a/third-party/gpui-fast/crates/gpui_windows/Cargo.toml
+++ b/third-party/gpui-fast/crates/gpui_windows/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "gpui-fast-windows"
-version = "0.1.7"
+version = "0.1.10"
edition = "2024"
publish = false
license = "Apache-2.0"
@@ -53,8 +53,7 @@ screen-capture = ["gpui/screen-capture", "scap"]
[dependencies.gpui]
default-features = false
package = "gpui-fast"
-version = "=0.1.7"
-path = "../gpui"
+version = "=0.1.10"
[target]
diff --git a/third-party/gpui-fast/crates/gpui_windows/build.rs b/third-party/gpui-fast/crates/gpui_windows/build.rs
index 46d5567ee3..ea7f8661c6 100644
--- a/third-party/gpui-fast/crates/gpui_windows/build.rs
+++ b/third-party/gpui-fast/crates/gpui_windows/build.rs
@@ -1,6 +1,10 @@
#![allow(clippy::disallowed_methods, reason = "build scripts are exempt")]
+#[path = "src/fast/rounded_clip_shader_build.rs"]
+mod fast_rounded_clip_shader;
+
fn main() {
+ crate::fast_rounded_clip_shader::stage();
#[cfg(not(all(target_os = "windows", not(debug_assertions))))]
println!("cargo:rerun-if-changed=build.rs");
#[cfg(target_os = "windows")]
@@ -21,8 +25,7 @@ mod shader_compilation {
};
pub fn compile_shaders() {
- let shader_path =
- PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("src/shaders.hlsl");
+ let shader_path = crate::fast_rounded_clip_shader::generated_path(std::env::var("OUT_DIR").unwrap());
let out_dir = std::env::var("OUT_DIR").unwrap();
println!("cargo:rerun-if-changed={}", shader_path.display());
diff --git a/third-party/gpui-fast/crates/gpui_windows/src/directx_renderer.rs b/third-party/gpui-fast/crates/gpui_windows/src/directx_renderer.rs
index 6397ea2699..7bd43b4e69 100644
--- a/third-party/gpui-fast/crates/gpui_windows/src/directx_renderer.rs
+++ b/third-party/gpui-fast/crates/gpui_windows/src/directx_renderer.rs
@@ -1514,7 +1514,7 @@ fn create_blend_state(device: &ID3D11Device) -> Result<ID3D11BlendState> {
desc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA;
desc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;
desc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
- desc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ONE;
+ desc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_INV_SRC_ALPHA;
desc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL.0 as u8;
unsafe {
let mut state = None;
@@ -1575,7 +1575,7 @@ fn create_blend_state_for_path_sprite(device: &ID3D11Device) -> Result<ID3D11Ble
desc.RenderTarget[0].SrcBlend = D3D11_BLEND_ONE;
desc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;
desc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
- desc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ONE;
+ desc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_INV_SRC_ALPHA;
desc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL.0 as u8;
unsafe {
let mut state = None;
@@ -1844,6 +1844,7 @@ pub(crate) mod shader_resources {
let shader_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join(&format!("src/{}", shader_name))
.canonicalize()?;
+ let shader_path = crate::fast::rounded_clip::shader_path(shader_path, env!("OUT_DIR"));
let entry_point = PCSTR::from_raw(entry.as_ptr());
let target_cstr = PCSTR::from_raw(target.as_ptr());
diff --git a/third-party/gpui-fast/crates/gpui_windows/src/events.rs b/third-party/gpui-fast/crates/gpui_windows/src/events.rs
index 05665d4312..4df35c372e 100644
--- a/third-party/gpui-fast/crates/gpui_windows/src/events.rs
+++ b/third-party/gpui-fast/crates/gpui_windows/src/events.rs
@@ -27,7 +27,6 @@ pub(crate) const WM_GPUI_DOCK_MENU_ACTION: u32 = WM_USER + 4;
pub(crate) const WM_GPUI_FORCE_UPDATE_WINDOW: u32 = WM_USER + 5;
pub(crate) const WM_GPUI_KEYBOARD_LAYOUT_CHANGED: u32 = WM_USER + 6;
pub(crate) const WM_GPUI_GPU_DEVICE_LOST: u32 = WM_USER + 7;
-pub(crate) const WM_GPUI_KEYDOWN: u32 = WM_USER + 8;
pub(crate) const WM_GPUI_DISPLAYS_CHANGED: u32 = WM_USER + 10;
const SIZE_MOVE_LOOP_TIMER_ID: usize = 1;
@@ -149,7 +148,6 @@ impl WindowsWindowInner {
WM_MOUSEHWHEEL => self.handle_mouse_horizontal_wheel_msg(handle, wparam, lparam),
WM_SYSKEYUP => self.handle_syskeyup_msg(wparam, lparam),
WM_KEYUP => self.handle_keyup_msg(wparam, lparam),
- WM_GPUI_KEYDOWN => self.handle_keydown_msg(wparam, lparam),
WM_CHAR => self.handle_char_msg(wparam),
WM_IME_STARTCOMPOSITION => self.handle_ime_position(handle),
WM_IME_COMPOSITION => self.handle_ime_composition(handle, lparam),
@@ -157,11 +155,11 @@ impl WindowsWindowInner {
WM_SETTINGCHANGE => self.handle_system_settings_changed(handle, wparam, lparam),
WM_INPUTLANGCHANGE => self.handle_input_language_changed(),
WM_SHOWWINDOW => self.handle_window_visibility_changed(handle, wparam),
- WM_GPUI_CURSOR_STYLE_CHANGED => self.handle_cursor_changed(lparam),
- WM_GPUI_FORCE_UPDATE_WINDOW => self.draw_window(handle, true),
- WM_GPUI_GPU_DEVICE_LOST => self.handle_device_lost(lparam),
DM_POINTERHITTEST => self.handle_dm_pointer_hit_test(wparam),
WM_GETOBJECT => self.handle_wm_getobject(wparam, lparam),
+ WM_GPUI_CURSOR_STYLE_CHANGED
+ | WM_GPUI_FORCE_UPDATE_WINDOW
+ | WM_GPUI_GPU_DEVICE_LOST => self.handle_gpui_events(handle, msg, wparam, lparam),
_ => None,
};
if let Some(n) = handled {
@@ -422,7 +420,7 @@ impl WindowsWindowInner {
// It's a known bug that you can't trigger `ctrl-shift-0`. See:
// https://superuser.com/questions/1455762/ctrl-shift-number-key-combination-has-stopped-working-for-a-few-numbers
- fn handle_keydown_msg(&self, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
+ pub(crate) fn handle_keydown_msg(&self, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
let Some(input) = handle_key_event(
wparam,
lparam,
@@ -1302,7 +1300,28 @@ impl WindowsWindowInner {
.detach();
}
+ fn handle_gpui_events(
+ &self,
+ handle: HWND,
+ message: u32,
+ wparam: WPARAM,
+ lparam: LPARAM,
+ ) -> Option<isize> {
+ if wparam.0 != self.validation_number {
+ return None;
+ }
+ match message {
+ WM_GPUI_CURSOR_STYLE_CHANGED => self.handle_cursor_changed(lparam),
+ WM_GPUI_FORCE_UPDATE_WINDOW => self.draw_window(handle, true),
+ WM_GPUI_GPU_DEVICE_LOST => self.handle_device_lost(lparam),
+ _ => unreachable!(),
+ }
+ }
+
fn handle_device_lost(&self, lparam: LPARAM) -> Option<isize> {
+ if lparam.0 == 0 {
+ return None;
+ }
let devices = lparam.0 as *const DirectXDevices;
let devices = unsafe { &*devices };
if let Err(err) = self
diff --git a/third-party/gpui-fast/crates/gpui_windows/src/fast/composition.rs b/third-party/gpui-fast/crates/gpui_windows/src/fast/composition.rs
index f29f8bb7ce..c7ce1104ad 100644
--- a/third-party/gpui-fast/crates/gpui_windows/src/fast/composition.rs
+++ b/third-party/gpui-fast/crates/gpui_windows/src/fast/composition.rs
@@ -40,7 +40,6 @@ use windows::{
},
Dxgi::{DXGI_PRESENT, DXGI_SWAP_CHAIN_FLAG, IDXGISwapChain1},
},
- UI::WindowsAndMessaging::GetClassNameW,
},
core::{IUnknown, Interface},
};
@@ -135,18 +134,6 @@ pub(crate) fn set_composition_order(
set_order(&mut window.state.renderer.borrow_mut(), surfaces)
}
-/// Forwarded to by `translate_accelerator`: whether `hwnd` is one of GPUI's
-/// windows, whose key downs GPUI handles before they are translated. Key
-/// downs of other windows, such as a WebView2 controller's child windows,
-/// are left to their own window procedures.
-pub(crate) fn is_gpui_window(hwnd: HWND) -> Option<()> {
- let class_name = unsafe { crate::window::WINDOW_CLASS_NAME.as_wide() };
- let mut buffer = [0u16; 64];
- let length = unsafe { GetClassNameW(hwnd, &mut buffer) };
- let length = usize::try_from(length).ok()?;
- (buffer.get(..length)? == class_name).then_some(())
-}
-
/// Forwarded to by `DirectXRenderer::handle_device_lost_impl` before it
/// releases the lost device: drops what was created on it.
pub(crate) fn release(renderer: &mut DirectXRenderer) {
diff --git a/third-party/gpui-fast/crates/gpui_windows/src/fast/frame.rs b/third-party/gpui-fast/crates/gpui_windows/src/fast/frame.rs
index b4df04614a..2618aaf748 100644
--- a/third-party/gpui-fast/crates/gpui_windows/src/fast/frame.rs
+++ b/third-party/gpui-fast/crates/gpui_windows/src/fast/frame.rs
@@ -41,6 +41,7 @@ pub(crate) struct FrameState {
path_sprites: Vec<PathSprite>,
/// The scroll layer tiles the frames composite.
pub(crate) layers: TileCache,
+ pub(crate) rounded_masks: crate::fast::rounded_clip::Masks,
}
/// The Direct3D objects a scene is drawn with.
@@ -57,6 +58,7 @@ pub(crate) struct Target<'a> {
/// The render target a frame's paths are drawn into and upstream's path
/// intermediate textures they are rasterized through: the window's, from its
/// [`DirectXResources`], or ones the layer pixel tests make for their target.
+#[derive(Clone, Copy)]
pub(crate) struct PathTargets<'a> {
pub(crate) render_target_view: &'a Option<ID3D11RenderTargetView>,
pub(crate) intermediate_texture: &'a ID3D11Texture2D,
@@ -150,29 +152,66 @@ pub(crate) fn draw_scene(
pipelines: &mut DirectXRenderPipelines,
frame: &mut FrameState,
scene: &Scene,
+) -> Result<()> {
+ let result = draw_scene_masked(target, pipelines, frame, scene, None);
+ crate::fast::rounded_clip::bind(target, &mut frame.rounded_masks, None)?;
+ result
+}
+
+pub(crate) fn draw_scene_masked(
+ target: &Target<'_>,
+ pipelines: &mut DirectXRenderPipelines,
+ frame: &mut FrameState,
+ scene: &Scene,
+ mask: Option<&ID3D11ShaderResourceView>,
) -> Result<()> {
let batch_params = target
.globals
.batch_params_buffer
.as_ref()
.context("batch params buffer missing")?;
- let FrameState {
- state,
- path_vertices,
- path_sprites,
- layers,
- ..
- } = frame;
let device_context = target.device_context;
- state.forget();
+ frame.state.forget();
+ let mut restore_upload = false;
- let mut draw = Draw {
- device_context,
- state,
- batch_params,
- sampler: &target.globals.sampler,
- };
for batch in scene.batches() {
+ if let PrimitiveBatch::PolychromeSprites {
+ texture_id,
+ ref range,
+ } = batch
+ && let Some(group) = crate::fast::rounded_clip::group(scene, texture_id)
+ {
+ for index in range.clone() {
+ crate::fast::rounded_clip::draw_group(
+ target,
+ pipelines,
+ frame,
+ group,
+ &scene.polychrome_sprites[index],
+ mask,
+ )?;
+ }
+ restore_upload = true;
+ continue;
+ }
+ if restore_upload {
+ upload(target, pipelines, scene)?;
+ restore_upload = false;
+ }
+ crate::fast::rounded_clip::bind(target, &mut frame.rounded_masks, mask)?;
+ let FrameState {
+ state,
+ path_vertices,
+ path_sprites,
+ layers,
+ ..
+ } = frame;
+ let mut draw = Draw {
+ device_context,
+ state,
+ batch_params,
+ sampler: &target.globals.sampler,
+ };
match batch {
PrimitiveBatch::Shadows(range) => {
draw.instances(&pipelines.shadow_pipeline, range.start, range.len(), None)
diff --git a/third-party/gpui-fast/crates/gpui_windows/src/fast/layers/composite.rs b/third-party/gpui-fast/crates/gpui_windows/src/fast/layers/composite.rs
index ba439b351d..5de452bbdb 100644
--- a/third-party/gpui-fast/crates/gpui_windows/src/fast/layers/composite.rs
+++ b/third-party/gpui-fast/crates/gpui_windows/src/fast/layers/composite.rs
@@ -18,11 +18,27 @@ use crate::fast::layers::TileCache;
/// The layer tiles `scene` composites, in the order it lists them.
pub(crate) fn composited_tiles(scene: &Scene) -> impl Iterator<Item = (LayerKey, TileCoord)> + '_ {
let has_layers = !scene.layers.frames.is_empty();
- scene
+ let direct = scene
.polychrome_sprites
.iter()
.take_while(move |_| has_layers)
- .filter_map(|sprite| decode_layer_tile(sprite.tile.texture_id, sprite.tile.tile_id))
+ .filter_map(|sprite| decode_layer_tile(sprite.tile.texture_id, sprite.tile.tile_id));
+ if !scene
+ .polychrome_sprites
+ .iter()
+ .any(|sprite| crate::fast::rounded_clip::group(scene, sprite.tile.texture_id).is_some())
+ {
+ return itertools::Either::Left(direct);
+ }
+ let mut tiles: Vec<_> = direct.collect();
+ for sprite in &scene.polychrome_sprites {
+ if let Some(child) = crate::fast::rounded_clip::group(scene, sprite.tile.texture_id)
+ .and_then(|frame| frame.content.scene())
+ {
+ tiles.extend(composited_tiles(child));
+ }
+ }
+ itertools::Either::Right(tiles.into_iter())
}
/// Whether sprites of `texture` are layer tiles rather than atlas sprites.
diff --git a/third-party/gpui-fast/crates/gpui_windows/src/fast/layers/raster.rs b/third-party/gpui-fast/crates/gpui_windows/src/fast/layers/raster.rs
index 2731b0cd08..0c3d005454 100644
--- a/third-party/gpui-fast/crates/gpui_windows/src/fast/layers/raster.rs
+++ b/third-party/gpui-fast/crates/gpui_windows/src/fast/layers/raster.rs
@@ -55,6 +55,7 @@ pub(crate) fn rasterize(
frame: &mut FrameState,
scene: &Scene,
) -> Result<()> {
+ crate::fast::rounded_clip::begin_frame(&mut frame.rounded_masks);
let planned = frame
.layers
.begin_frame(&scene.layers, composite::composited_tiles(scene));
diff --git a/third-party/gpui-fast/crates/gpui_windows/src/fast/layers/tests.rs b/third-party/gpui-fast/crates/gpui_windows/src/fast/layers/tests.rs
index 03e98cbb8f..c194beb8ad 100644
--- a/third-party/gpui-fast/crates/gpui_windows/src/fast/layers/tests.rs
+++ b/third-party/gpui-fast/crates/gpui_windows/src/fast/layers/tests.rs
@@ -49,6 +49,42 @@ use crate::fast::layers::tile_cache::create_globals;
const TILE: u32 = 512;
+#[cfg(feature = "test-support")]
+#[path = "../../../../gpui/src/fast/rounded_clip_fixtures.rs"]
+mod rounded_clip_tests;
+
+#[cfg(feature = "test-support")]
+#[gpui::test]
+fn rounded_overflow_retained_child_follows_ancestor_clip(cx: &mut gpui::TestAppContext) {
+ let mut harness = Harness::new().expect("retained clipping regression requires a GPU");
+ rounded_clip_tests::retained_frames(cx, |scene, size| {
+ harness.render(scene, size, rgba(0xffffffff))
+ });
+}
+
+#[cfg(feature = "test-support")]
+fn render_div_fixture(cx: &mut gpui::TestAppContext, build: fn() -> gpui::Div) -> (Vec<u8>, usize) {
+ let mut harness =
+ Harness::new().expect("rounded clipping regression requires Direct3D or WARP");
+ rounded_clip_tests::render_with(cx, build, |scene, size| {
+ harness.render(scene, size, rgba(0xffffffff))
+ })
+}
+
+#[cfg(feature = "test-support")]
+#[gpui::test]
+fn rounded_overflow_inside_scroll_tiles_matches_direct_pixels(cx: &mut gpui::TestAppContext) {
+ let mut harness =
+ Harness::new().expect("rounded clipping regression requires Direct3D or WARP");
+ rounded_clip_tests::render_with(cx, rounded_clip_tests::square_fixture, |scene, size| {
+ let direct = harness.render(scene, size, rgba(0xffffffff));
+ let tiled = rounded_clip_tests::tiled_scene(scene, size);
+ let actual = harness.render(&tiled, size, rgba(0xffffffff));
+ assert_same_pixels(&actual, &direct, size.width.0 as usize);
+ actual
+ });
+}
+
#[test]
fn harness_renders_a_quad_at_the_right_pixels() {
let Some(mut harness) = Harness::new() else {
diff --git a/third-party/gpui-fast/crates/gpui_windows/src/fast/mod.rs b/third-party/gpui-fast/crates/gpui_windows/src/fast/mod.rs
index c7c5f52739..ef70fad908 100644
--- a/third-party/gpui-fast/crates/gpui_windows/src/fast/mod.rs
+++ b/third-party/gpui-fast/crates/gpui_windows/src/fast/mod.rs
@@ -13,3 +13,4 @@ pub(crate) mod draw_state;
pub(crate) mod frame;
pub(crate) mod globals;
pub(crate) mod layers;
+pub(crate) mod rounded_clip;
diff --git a/third-party/gpui-fast/crates/gpui_windows/src/fast/rounded_clip.rs b/third-party/gpui-fast/crates/gpui_windows/src/fast/rounded_clip.rs
new file mode 100644
index 0000000000..2ab05e8b73
--- /dev/null
+++ b/third-party/gpui-fast/crates/gpui_windows/src/fast/rounded_clip.rs
@@ -0,0 +1,271 @@
+//! Rounded scopes evaluate an immutable geometry texture of padding edges
+//! directly in the colour shader. Each child appends its geometry and restores
+//! the enclosing stack afterward; no mask render targets or passes are needed.
+
+use std::{path::PathBuf, slice};
+
+use anyhow::{Context as _, Result};
+use gpui::{AtlasTextureId, LayerFrame, Scene};
+use windows::{
+ Win32::Graphics::Direct3D11::{
+ D3D11_BIND_CONSTANT_BUFFER, D3D11_BIND_SHADER_RESOURCE, D3D11_BUFFER_DESC,
+ D3D11_SUBRESOURCE_DATA, D3D11_TEXTURE2D_DESC, D3D11_USAGE_IMMUTABLE, ID3D11Buffer,
+ ID3D11Device, ID3D11RenderTargetView, ID3D11ShaderResourceView, ID3D11Texture2D,
+ },
+ Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_R32G32B32A32_FLOAT, DXGI_SAMPLE_DESC},
+ core::Interface as _,
+};
+
+use crate::directx_renderer::{
+ DirectXRenderPipelines, create_path_intermediate_msaa_texture_and_view,
+ create_path_intermediate_texture,
+};
+use crate::fast::frame::{FrameState, PathTargets, Target, draw_scene_masked, upload};
+
+pub(crate) fn shader_path(original: PathBuf, out_dir: &str) -> PathBuf {
+ if original
+ .file_name()
+ .is_some_and(|name| name == "shaders.hlsl")
+ {
+ PathBuf::from(out_dir).join("rounded_clip_shaders.hlsl")
+ } else {
+ original
+ }
+}
+
+pub(crate) fn group(scene: &Scene, texture: AtlasTextureId) -> Option<&LayerFrame> {
+ if texture.index < 0xF100_0000 {
+ return None;
+ }
+ let key = texture.index - 0xF000_0000;
+ scene
+ .layers
+ .frames
+ .iter()
+ .find(|frame| frame.key.0 == key && frame.tile_size == 0)
+}
+
+#[derive(Default)]
+pub(crate) struct Masks {
+ device: Option<ID3D11Device>,
+ flags: Option<ID3D11Buffer>,
+ clips: Vec<Clip>,
+ paths: Vec<PathSlot>,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+struct Clip {
+ bounds: [f32; 4],
+ horizontal: [f32; 4],
+ vertical: [f32; 4],
+}
+
+struct PathSlot {
+ size: (u32, u32),
+ used: bool,
+ image: PathImage,
+}
+
+#[derive(Clone)]
+struct PathImage {
+ intermediate: ID3D11Texture2D,
+ view: Option<ID3D11ShaderResourceView>,
+ msaa: ID3D11Texture2D,
+ msaa_target: Option<ID3D11RenderTargetView>,
+}
+
+impl Masks {
+ fn for_device(&mut self, device: &ID3D11Device) {
+ if self.device.as_ref() != Some(device) {
+ *self = Self {
+ device: Some(device.clone()),
+ ..Default::default()
+ };
+ }
+ }
+
+ fn clip_view(&self, device: &ID3D11Device) -> Result<Option<ID3D11ShaderResourceView>> {
+ // ps_4_1 does not support StructuredBuffer. Three full-precision
+ // texels per edge preserve the same geometry on the existing profile.
+ let desc = D3D11_TEXTURE2D_DESC {
+ Width: 3,
+ Height: u32::try_from(self.clips.len())?,
+ MipLevels: 1,
+ ArraySize: 1,
+ Format: DXGI_FORMAT_R32G32B32A32_FLOAT,
+ SampleDesc: DXGI_SAMPLE_DESC {
+ Count: 1,
+ Quality: 0,
+ },
+ Usage: D3D11_USAGE_IMMUTABLE,
+ BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32,
+ ..Default::default()
+ };
+ let data = D3D11_SUBRESOURCE_DATA {
+ pSysMem: self.clips.as_ptr().cast(),
+ SysMemPitch: std::mem::size_of::<Clip>() as u32,
+ ..Default::default()
+ };
+ let mut texture = None;
+ let mut view = None;
+ unsafe {
+ device.CreateTexture2D(&desc, Some(&data), Some(&mut texture))?;
+ device.CreateShaderResourceView(
+ texture.as_ref().context("clip geometry texture")?,
+ None,
+ Some(&mut view),
+ )?;
+ }
+ Ok(view)
+ }
+
+ fn paths(&mut self, device: &ID3D11Device, size: (u32, u32)) -> Result<PathImage> {
+ if let Some(slot) = self.paths.iter_mut().find(|slot| slot.size == size) {
+ slot.used = true;
+ return Ok(slot.image.clone());
+ }
+ let (intermediate, view) = create_path_intermediate_texture(device, size.0, size.1)?;
+ let (msaa, msaa_target) =
+ create_path_intermediate_msaa_texture_and_view(device, size.0, size.1)?;
+ let image = PathImage {
+ intermediate,
+ view,
+ msaa,
+ msaa_target,
+ };
+ self.paths.push(PathSlot {
+ size,
+ used: true,
+ image: image.clone(),
+ });
+ Ok(image)
+ }
+}
+
+pub(crate) fn begin_frame(masks: &mut Masks) {
+ masks.clips.clear();
+ masks.paths.retain(|slot| slot.used);
+ for slot in &mut masks.paths {
+ slot.used = false;
+ }
+}
+
+pub(crate) fn bind(
+ target: &Target<'_>,
+ masks: &mut Masks,
+ view: Option<&ID3D11ShaderResourceView>,
+) -> Result<()> {
+ masks.for_device(target.device);
+ if view.is_some() && masks.flags.is_none() {
+ let values = [1u32, 0, 0, 0];
+ let desc = D3D11_BUFFER_DESC {
+ ByteWidth: 16,
+ Usage: D3D11_USAGE_IMMUTABLE,
+ BindFlags: D3D11_BIND_CONSTANT_BUFFER.0 as u32,
+ ..Default::default()
+ };
+ let data = D3D11_SUBRESOURCE_DATA {
+ pSysMem: values.as_ptr().cast(),
+ ..Default::default()
+ };
+ unsafe {
+ target
+ .device
+ .CreateBuffer(&desc, Some(&data), Some(&mut masks.flags))?;
+ }
+ }
+ let flags = if view.is_some() {
+ masks.flags.clone()
+ } else {
+ None
+ };
+ unsafe {
+ target
+ .device_context
+ .PSSetConstantBuffers(2, Some(slice::from_ref(&flags)));
+ target
+ .device_context
+ .PSSetShaderResources(15, Some(slice::from_ref(&view.cloned())));
+ }
+ Ok(())
+}
+
+pub(crate) fn draw_group(
+ target: &Target<'_>,
+ pipelines: &mut DirectXRenderPipelines,
+ frame: &mut FrameState,
+ group: &LayerFrame,
+ sprite: &gpui::PolychromeSprite,
+ parent_mask: Option<&ID3D11ShaderResourceView>,
+) -> Result<()> {
+ let child = group.content.scene().context("rounded clip content")?;
+ let radii = child
+ .layers
+ .frames
+ .iter()
+ .find(|frame| frame.key.0 == u32::MAX)
+ .context("rounded clip geometry")?
+ .background;
+ frame.rounded_masks.for_device(target.device);
+ let b = sprite.bounds;
+ let r = sprite.corner_radii;
+ frame.rounded_masks.clips.push(Clip {
+ bounds: [b.origin.x.0, b.origin.y.0, b.right().0, b.bottom().0],
+ horizontal: [
+ r.top_left.0,
+ r.top_right.0,
+ r.bottom_right.0,
+ r.bottom_left.0,
+ ],
+ vertical: [radii.r, radii.g, radii.b, radii.a],
+ });
+ let result = (|| {
+ let view = frame.rounded_masks.clip_view(target.device)?;
+ let context = target.device_context;
+ let mut parent = [None];
+ unsafe {
+ context.OMGetRenderTargets(Some(&mut parent), None);
+ }
+ let render_target = parent[0].clone().context("rounded clip render target")?;
+ let resource = unsafe { render_target.GetResource()? };
+ let texture: ID3D11Texture2D = resource.cast()?;
+ let mut desc = D3D11_TEXTURE2D_DESC::default();
+ unsafe {
+ texture.GetDesc(&mut desc);
+ }
+ let size = (desc.Width, desc.Height);
+ let fallback = if target.paths.is_none() {
+ Some(frame.rounded_masks.paths(target.device, size)?)
+ } else {
+ None
+ };
+ let parent_view = Some(render_target);
+ let paths = target.paths.unwrap_or_else(|| {
+ let image = fallback.as_ref().unwrap();
+ PathTargets {
+ render_target_view: &parent_view,
+ intermediate_texture: &image.intermediate,
+ intermediate_srv: &image.view,
+ msaa_texture: &image.msaa,
+ msaa_view: &image.msaa_target,
+ }
+ });
+ let content_target = Target {
+ device: target.device,
+ device_context: context,
+ atlas: target.atlas,
+ globals: target.globals,
+ paths: Some(PathTargets {
+ render_target_view: &parent_view,
+ ..paths
+ }),
+ };
+ upload(&content_target, pipelines, child)?;
+ draw_scene_masked(&content_target, pipelines, frame, child, view.as_ref())
+ })();
+ frame.rounded_masks.clips.pop();
+ bind(target, &mut frame.rounded_masks, parent_mask)?;
+ frame.state.forget();
+ result
+}
diff --git a/third-party/gpui-fast/crates/gpui_windows/src/fast/rounded_clip_shader_build.rs b/third-party/gpui-fast/crates/gpui_windows/src/fast/rounded_clip_shader_build.rs
new file mode 100644
index 0000000000..eff027494d
--- /dev/null
+++ b/third-party/gpui-fast/crates/gpui_windows/src/fast/rounded_clip_shader_build.rs
@@ -0,0 +1,131 @@
+//! Both debug runtime compilation and release FXC compilation consume the
+//! same generated, self-contained shader. Upstream HLSL stays unchanged.
+
+use std::path::{Path, PathBuf};
+
+pub fn generated_path(out_dir: impl AsRef<Path>) -> PathBuf {
+ out_dir.as_ref().join("rounded_clip_shaders.hlsl")
+}
+
+pub fn stage() {
+ if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") {
+ return;
+ }
+ let root = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
+ let shader_path = root.join("src/shaders.hlsl");
+ let alpha_path = root.join("src/alpha_correction.hlsl");
+ println!("cargo:rerun-if-changed={}", shader_path.display());
+ println!("cargo:rerun-if-changed={}", alpha_path.display());
+ let source = source(
+ &std::fs::read_to_string(shader_path).unwrap(),
+ &std::fs::read_to_string(alpha_path).unwrap(),
+ );
+ std::fs::write(generated_path(std::env::var("OUT_DIR").unwrap()), source).unwrap();
+}
+
+pub fn source(original: &str, alpha: &str) -> String {
+ let mut source = original.replace("#include \"alpha_correction.hlsl\"", alpha);
+ for name in [
+ "quad_fragment",
+ "shadow_fragment",
+ "underline_fragment",
+ "monochrome_sprite_fragment",
+ "polychrome_sprite_fragment",
+ "path_sprite_fragment",
+ "subpixel_sprite_fragment",
+ ] {
+ let start = source
+ .find(&format!("{name}("))
+ .expect("upstream HLSL fragment entry point");
+ let body_start = start + source[start..].find('{').unwrap();
+ let mut depth = 1;
+ let mut body_end = body_start + 1;
+ for (offset, ch) in source[body_start + 1..].char_indices() {
+ match ch {
+ '{' => depth += 1,
+ '}' => depth -= 1,
+ _ => {}
+ }
+ if depth == 0 {
+ body_end = body_start + 1 + offset;
+ break;
+ }
+ }
+ let body = &source[body_start + 1..body_end];
+ let body = if name == "subpixel_sprite_fragment" {
+ assert!(body.contains("return output;"));
+ body.replace(
+ "return output;",
+ "output.alpha *= fast_clip_coverage(input.position.xy); return output;",
+ )
+ } else {
+ let mut result = String::new();
+ let mut rest = body;
+ while let Some(start) = rest.find("return ") {
+ let end = start + rest[start..].find(';').expect("fragment return semicolon");
+ result.push_str(&rest[..start]);
+ result.push_str(&format!(
+ "return fast_clip_color({}, input.position.xy, {});",
+ &rest[start + 7..end],
+ name == "path_sprite_fragment"
+ ));
+ rest = &rest[end + 1..];
+ }
+ result.push_str(rest);
+ result
+ };
+ source.replace_range(body_start + 1..body_end, &body);
+ }
+ format!(
+ r#"
+struct FastRoundedClip {{
+ float4 bounds;
+ float4 horizontal;
+ float4 vertical;
+}};
+float fast_rounded_clip_coverage(float2 position, FastRoundedClip shape) {{
+ float2 extent = shape.bounds.zw - shape.bounds.xy;
+ if (extent.x <= 0 || extent.y <= 0) return 0;
+ float2 local = position - shape.bounds.xy;
+ float distance = max(max(-local.x, -local.y), max(local.x - extent.x, local.y - extent.y));
+ [unroll] for (uint corner = 0; corner < 4; ++corner) {{
+ float2 radius = float2(shape.horizontal[corner], shape.vertical[corner]);
+ if (radius.x <= 0 || radius.y <= 0) continue;
+ bool right = corner == 1 || corner == 2;
+ bool bottom = corner >= 2;
+ float2 inward = float2(right ? extent.x - local.x : local.x, bottom ? extent.y - local.y : local.y);
+ if (inward.x < radius.x && inward.y < radius.y) {{
+ float2 q = (inward - radius) / radius;
+ float length_q = length(q);
+ float gradient = length(q / radius) / max(length_q, 0.000001f);
+ distance = max(distance, (length_q - 1) / max(gradient, 0.000001f));
+ }}
+ }}
+ return clamp(0.5f - distance, 0.0f, 1.0f);
+}}
+Texture2D<float4> fast_clip_shapes : register(t15);
+cbuffer FastClipParams : register(b2) {{ uint fast_clip_enabled; uint3 fast_clip_pad; }};
+float fast_clip_coverage(float2 position) {{
+ if (fast_clip_enabled == 0) return 1.0;
+ uint width = 0, count = 0;
+ fast_clip_shapes.GetDimensions(width, count);
+ float coverage = 1.0;
+ [loop] for (uint index = 0; index < count; ++index) {{
+ FastRoundedClip shape;
+ shape.bounds = fast_clip_shapes.Load(int3(0, index, 0));
+ shape.horizontal = fast_clip_shapes.Load(int3(1, index, 0));
+ shape.vertical = fast_clip_shapes.Load(int3(2, index, 0));
+ coverage = min(coverage, fast_rounded_clip_coverage(position, shape));
+ }}
+ return coverage;
+}}
+float4 fast_clip_color(float4 color, float2 position, bool premultiplied) {{
+ float coverage = fast_clip_coverage(position);
+ if (premultiplied) color.rgb *= coverage;
+ color.a *= coverage;
+ return color;
+}}
+{source}
+"#
+ )
+}
diff --git a/third-party/gpui-fast/crates/gpui_windows/src/platform.rs b/third-party/gpui-fast/crates/gpui_windows/src/platform.rs
index 117984e50b..2bac343f0a 100644
--- a/third-party/gpui-fast/crates/gpui_windows/src/platform.rs
+++ b/third-party/gpui-fast/crates/gpui_windows/src/platform.rs
@@ -288,11 +288,7 @@ impl WindowsPlatform {
}
pub(crate) fn window_from_hwnd(&self, hwnd: HWND) -> Option<Rc<WindowsWindowInner>> {
- self.raw_window_handles
- .read()
- .iter()
- .find(|entry| entry.as_raw() == hwnd)
- .and_then(|hwnd| window_from_hwnd(hwnd.as_raw()))
+ self.inner.window_from_hwnd(hwnd)
}
#[inline]
@@ -499,23 +495,6 @@ impl WindowsPlatform {
}
}
-fn translate_accelerator(msg: &MSG) -> Option<()> {
- if msg.message != WM_KEYDOWN && msg.message != WM_SYSKEYDOWN {
- return None;
- }
- crate::fast::composition::is_gpui_window(msg.hwnd)?;
-
- let result = unsafe {
- SendMessageW(
- msg.hwnd,
- WM_GPUI_KEYDOWN,
- Some(msg.wParam),
- Some(msg.lParam),
- )
- };
- (result.0 == 0).then_some(())
-}
-
fn encode_restart_arguments(arguments: &[OsString]) -> OsString {
// `Start-Process` accepts a single native command line, so quote each argument according to
// the Windows argv parsing rules before passing the complete string through the environment.
@@ -621,7 +600,7 @@ impl Platform for WindowsPlatform {
}
continue;
}
- if translate_accelerator(&msg).is_none() {
+ if !self.inner.translate_accelerator(&msg) {
_ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
@@ -996,7 +975,7 @@ impl Platform for WindowsPlatform {
if self.inner.state.current_cursor.get().map(|c| c.0) != hcursor.map(|c| c.0) {
self.post_message(
WM_GPUI_CURSOR_STYLE_CHANGED,
- WPARAM(0),
+ WPARAM(self.inner.validation_number),
LPARAM(hcursor.map_or(0, |c| c.0 as isize)),
);
self.inner.state.current_cursor.set(hcursor);
@@ -1177,6 +1156,24 @@ impl WindowsPlatformInner {
}))
}
+ fn window_from_hwnd(&self, hwnd: HWND) -> Option<Rc<WindowsWindowInner>> {
+ self.raw_window_handles
+ .upgrade()?
+ .read()
+ .iter()
+ .find(|window| window.as_raw() == hwnd)
+ .and_then(|window| window_from_hwnd(window.as_raw()))
+ }
+
+ fn translate_accelerator(&self, msg: &MSG) -> bool {
+ if msg.message != WM_KEYDOWN && msg.message != WM_SYSKEYDOWN {
+ return false;
+ }
+
+ self.window_from_hwnd(msg.hwnd)
+ .is_some_and(|window| window.handle_keydown_msg(msg.wParam, msg.lParam) == Some(0))
+ }
+
/// Calls `project` to project to the corresponding callback field, removes it from callbacks, calls `f` with the callback and then puts the callback back.
fn with_callback<T>(
&self,
@@ -1306,7 +1303,7 @@ impl WindowsPlatformInner {
// if we don't we might not for example process window quit events
let mut msg = MSG::default();
let process_message = |msg: &_| {
- if translate_accelerator(msg).is_none() {
+ if !self.translate_accelerator(msg) {
_ = unsafe { TranslateMessage(msg) };
unsafe { DispatchMessageW(msg) };
}
diff --git a/third-party/gpui-fast/crates/gpui_windows/src/shaders.hlsl b/third-party/gpui-fast/crates/gpui_windows/src/shaders.hlsl
index e667b786f1..ca8f23897a 100644
--- a/third-party/gpui-fast/crates/gpui_windows/src/shaders.hlsl
+++ b/third-party/gpui-fast/crates/gpui_windows/src/shaders.hlsl
@@ -1140,10 +1140,10 @@ float4 underline_fragment(UnderlineFragmentInput input): SV_Target {
struct MonochromeSprite {
uint order;
uint pad;
+ AtlasTile tile;
Bounds bounds;
Bounds content_mask;
Hsla color;
- AtlasTile tile;
TransformationMatrix transformation;
};
@@ -1213,12 +1213,12 @@ SubpixelSpriteFragmentOutput subpixel_sprite_fragment(MonochromeSpriteFragmentIn
struct PolychromeSprite {
uint order;
uint pad;
+ AtlasTile tile;
uint grayscale;
float opacity;
Bounds bounds;
Bounds content_mask;
Corners corner_radii;
- AtlasTile tile;
};
struct PolychromeSpriteVertexOutput {
diff --git a/third-party/gpui-fast/upstream.json b/third-party/gpui-fast/upstream.json
index 9fe25f47c9..a3357a62b9 100644
--- a/third-party/gpui-fast/upstream.json
+++ b/third-party/gpui-fast/upstream.json
@@ -1,6 +1,6 @@
{
"repository": "https://github.com/longbridge/gpui-fast",
- "revision": "371769f1ff378bc0bd886440e0f97fc94aaeae5d",
+ "revision": "b836dd970b26830f2124b7cf91b0e19018fd12da",
"files": {
"LICENSE": "5f78a733701db49b73da93addad8cb74f816f3789ef30c3c0e959e943ecd99b4",
"LICENSE-APACHE": "5f78a733701db49b73da93addad8cb74f816f3789ef30c3c0e959e943ecd99b4",
@@ -8,10 +8,10 @@
"assets/fonts/ibm-plex-sans/license.txt": "91c25c350d3cac39da2736d74f7ba37ef648f5237a4e330a240615bc8d8c4360",
"assets/fonts/lilex/Lilex-Regular.ttf": "8a467ad5a1941dc7c9cebaee34e3ed9364a73b48393034e363e4b528bd711d04",
"assets/fonts/lilex/OFL.txt": "a356708a399e3f54b937ec7d9b722c26dabba21365b60f95c56eb7ebca0dee40",
- "crates/gpui/Cargo.toml": "99ae5e0e0993935f4b9597e5d0db0c689567eace049a461e1c1f1dec1c43fb60",
+ "crates/gpui/Cargo.toml": "ae86e9f0e09506fcff6dcdc3dd734806952716f71e92fe803857ac309f2e8b4c",
"crates/gpui/LICENSE-APACHE": "5f78a733701db49b73da93addad8cb74f816f3789ef30c3c0e959e943ecd99b4",
"crates/gpui/README.md": "989b10e5914dcc8a1f2568303849aeab2d6aa26f2c5dfead96de0491330ada79",
- "crates/gpui/build.rs": "faf9c1df29ca77d633e6e809cd47a0b4fd760466de4df161c676e9830b54a1b6",
+ "crates/gpui/build.rs": "82245901888defc1371cf554ed505a141755ea412233ae242ff3ca6c62aa12ad",
"crates/gpui/docs/contexts.md": "2688efb86472685cf2ea1af254c7c4bc70d53dd369c3d30118bf5e356fe86574",
"crates/gpui/docs/key_dispatch.md": "56189467c43ca57c7cfa507f6ed7cdc31516144cbe648ef1f5fd97696862fe1a",
"crates/gpui/examples/image/exif-orientation-rotate-180.jpg": "cb6c28e3e6065d62790ba38662d14f3f1510180375cacba18547720e8b48d9d5",
@@ -20,14 +20,14 @@
"crates/gpui/src/_accessibility.rs": "bcdeb209fa6309f5c507d9e8918235e75c18751c1985d0ebed632de643d7ea2e",
"crates/gpui/src/_ownership_and_data_flow.rs": "4edbde237dc322ceac8969028811fd30d87a8bd108475a642c4fb961b2770258",
"crates/gpui/src/action.rs": "6566c34d4f4cf7f6a5280cbc4c07c747c5c6fc4c6554abd42c73fefe5716fc20",
- "crates/gpui/src/app.rs": "5eaaaa0a4a837b65d7d0862df876e7b29b2bb56dcba7678ac4bc26d7fd4f395d",
+ "crates/gpui/src/app.rs": "e90f94507da8db07013f254ea59da44550a39cd449772be1d0c0396959968c0e",
"crates/gpui/src/app/async_context.rs": "3a1948721bca22ccfae2926c6b40722ed3315a33e79c8d87885edc4bd610c115",
"crates/gpui/src/app/bench_context.rs": "6bef320831a933502651903f85b849b6ea371b84c6822b7b88d3e064dc8c9818",
"crates/gpui/src/app/context.rs": "b05a478fb75a5488887cb46e2ea151c5d85ea8543311aca9ac5e724200f03ecb",
"crates/gpui/src/app/entity_map.rs": "de7bf92f647c35631bce28100fb23aa18f4eca92f1cde3e434743fc8d452dffc",
"crates/gpui/src/app/headless_app_context.rs": "11f273fc2aeda076cb40b49b5a658d7ea2be50f72d3fe56524b2e78f7a04bc15",
"crates/gpui/src/app/test_app.rs": "ea41a502634c8eb8011a0f499b8a3900939228e6f6260bf8857766c799633316",
- "crates/gpui/src/app/test_context.rs": "edb6f93e0abfe66a0780cefadb46e468b81a0f4d7df9f6e4d114a2c57c9503e0",
+ "crates/gpui/src/app/test_context.rs": "3c0b814d4e62e77bfedb358231c0a08bc9affa8d6f8bd4bbdad51d6c7e126a8f",
"crates/gpui/src/app/visual_test_context.rs": "b6e0399349417b70cf067a90cf46a385a0b2efc999e30fd4a037d0ecdd87ff1c",
"crates/gpui/src/arena.rs": "262e6403f8fa505592dddb674028f43c0e41beb1f51aa97e37c917215052580b",
"crates/gpui/src/asset_cache.rs": "cc1eb306523094d2d9a0e15538ce9a4fe29408288bd73e27781ea9a92c1b059e",
@@ -42,7 +42,7 @@
"crates/gpui/src/elements/canvas.rs": "3e09646e725d31c10a88be290c146d6a6e829e0be173e6bc82c138461668e646",
"crates/gpui/src/elements/container_query.rs": "7e72aab18967e7e16ebbe38c535caf2ac6f881a62cf9f6045922fbeb8a06df07",
"crates/gpui/src/elements/deferred.rs": "0501cd9c0f041b849e51ea78a69abeb24781e5b5488311c0b87f4cf1ed01f451",
- "crates/gpui/src/elements/div.rs": "59dfffdcd801f6427a7c359c8c314c3decae7a007f7589f2c9086a1fa0d74b30",
+ "crates/gpui/src/elements/div.rs": "b08cb7bb0f95a4ac83a387b09333a357d3b6019ae0998ffe399f83c47c1295f2",
"crates/gpui/src/elements/image_cache.rs": "e4540d7e92bc63bbaaf703df42338bbfabca764e34b7244880460d38962e14c4",
"crates/gpui/src/elements/img.rs": "c196cb1ed3db30ab3141b036c84288a22e04de2b53b687e1d86fe732420374e3",
"crates/gpui/src/elements/list.rs": "e2cd2c043800ae228372d96cd215a0d6a39a495f090a6438208d24d08009c341",
@@ -52,70 +52,88 @@
"crates/gpui/src/elements/text.rs": "7e4a2e94298dc72509994782a8dfddfdda09a36dc6d870ebeb88b3bae16fbb55",
"crates/gpui/src/elements/uniform_list.rs": "786f2fc6f63812b6a9dd8caeddaab3e1c74823f0e035a765a41bd97e0ec54b8d",
"crates/gpui/src/executor.rs": "02e68407eb89156de31e05320a4d3565ea6f1c639dd378650b1703e8729b5640",
+ "crates/gpui/src/fast/a11y.rs": "67cd7377e909f1ecb9c7ea2557a101f4fe41ec4165a6afb7955772df69c2321f",
"crates/gpui/src/fast/bounds_tree.rs": "c9986acf0367c287c51453a86770c5fc4ff08906b4cd2aad589ef125424aa19b",
- "crates/gpui/src/fast/composition.rs": "e203b9b1c12e7c5ca2ea7bbf522e079a9d76a5812cb1de377e4a28b6cb49fa51",
+ "crates/gpui/src/fast/composition.rs": "b171e521a7ab96f06bb6941690f498a67b086d51225c74f55dcf3ac9fd5cba41",
"crates/gpui/src/fast/dependencies.rs": "7c11a23cf2595c23dadf0166ebe429e25225e6885689886b8da84cd4904ec93f",
"crates/gpui/src/fast/dispatch.rs": "08b67ad96464554cf92bfefef9075fe827902a5f680fad35bfca4ac3376ff13c",
"crates/gpui/src/fast/global_id.rs": "bf5c602aef9fbf6fcf8bbe4bb1038950730fa3e3911a0a3aa71f59f5b793b595",
"crates/gpui/src/fast/glyphs.rs": "ef4a80e125b0d2a64cddf1083d57cbf3f31c6861a458f3190e2f197afd351649",
"crates/gpui/src/fast/interactivity.rs": "8b130abf15eea71f7886693696a131e4d76145e3de54160e73dc1da7c2809eeb",
"crates/gpui/src/fast/layers/background.rs": "217322f6cfde5d48eb4269299acc28ef9e034595f3836ddb87b08a471b40491d",
- "crates/gpui/src/fast/layers/input.rs": "1042cac3091923c13175cdeb5d18d211b463f854ba7b09830da1da20ef830e4b",
+ "crates/gpui/src/fast/layers/hover_freeze.rs": "3fa1948b45832bc7cd3ad87c1f0cff11a683c6cfc3b826b2718d2d06353ee0de",
+ "crates/gpui/src/fast/layers/input.rs": "4063a41dcf43112dc1a5ad4edfb5439d6b68cc31289f40e07f0ec42a8b4feb05",
"crates/gpui/src/fast/layers/invalidate.rs": "00ac0cc62cbf816604a28ff7a572abcec821b84ee6f8abed18b3e5174ae05ad5",
- "crates/gpui/src/fast/layers/lists.rs": "bfef2fd8042fdd4b5e87aa44cbf1ac1b89ea8bdff766cbffb1197f71791b6c5f",
+ "crates/gpui/src/fast/layers/lists.rs": "f93595d071f60f2f1713f4c0eb2b9f32aa18d146043d666f73aed24258c0024f",
"crates/gpui/src/fast/layers/mod.rs": "6aa01a2e2db17bd65610ba555dca38b5a628e685e19934ae957eaa8db17d6ea3",
- "crates/gpui/src/fast/layers/paint.rs": "1887ca85af79cb1b8d3e55e386666c2aa558b23d0ce8616f2b5504fe322ca5dd",
- "crates/gpui/src/fast/layers/policy.rs": "a59be0717c55f1958351d961935edcfe5fc81d61e3851dd6206c07dfe2e564a3",
+ "crates/gpui/src/fast/layers/overlay.rs": "455affd0e077ae44db460db65e8472facd112e531758060a09cce5d01b8aef44",
+ "crates/gpui/src/fast/layers/paint.rs": "e7d3c4bc4f3d7419177c285757c15ea724546baea68a674410a3f2a43510f2c9",
+ "crates/gpui/src/fast/layers/policy.rs": "6576a4e6d8f0457d51f15c9482c5f0ce9202b52ba38435dfb2be3b69edd716ba",
"crates/gpui/src/fast/layers/record.rs": "1ba15dd5cbfd3133e5368b7ce3281b3122947aa4f88bea5244f76a5cc8f2f695",
- "crates/gpui/src/fast/layers/reuse.rs": "3ea74ab142d45031108e667dd5de76c4395c8948cc853c4e88cb28bb6ac984a7",
- "crates/gpui/src/fast/layers/scene.rs": "2bbed5b42868bb7d43012a20a8e283f5895f08a7b193df7d955cd3fa540a7636",
- "crates/gpui/src/fast/layers/tiles.rs": "e9ecdcce39aebcd248ced9c2cf6f92bc2ede039da08135abd67932b215289486",
- "crates/gpui/src/fast/layers/verify.rs": "ee186c02b95a622154846538c2dab41de6e6ce7bfae1056f4d155be2304a004e",
+ "crates/gpui/src/fast/layers/reuse.rs": "8ad726d63b0f8ddbe12cf287c17da980fb96668a222cda9f0bb8169d553e6e4b",
+ "crates/gpui/src/fast/layers/scene.rs": "0a36b8f1203dec2d14d8b5f389a8e55718d31262f4bb825a433c978c0f89f78e",
+ "crates/gpui/src/fast/layers/scroll_to_bottom.rs": "4e1e550f63ac20cf4b35add92618e673a1de99d354c2cdc9c952a1475a824879",
+ "crates/gpui/src/fast/layers/tiles.rs": "6f19fda58ad071c7269b36eddca6619b3e5ca655714beb8ace1950ec570a38fa",
+ "crates/gpui/src/fast/layers/verify.rs": "ea881a3d81acef65f2c05fbad777391570360cfee4e3212356fef0fb409aa940",
+ "crates/gpui/src/fast/layers/wheel.rs": "ebfcaba6fb3c95f7d1660710c2a266a4a7c9af63f0ada4a0e6ef892c6c91c0ce",
"crates/gpui/src/fast/layers/work.rs": "3854efa6e74d677897844d3b454612a25963d70f150deb8e2af943fe98e3f205",
- "crates/gpui/src/fast/layout.rs": "c5ef3743baa472c41e8cc7500f1eb13960decaf5dacaf5558b82d5b8a26a2871",
+ "crates/gpui/src/fast/layout.rs": "894e198f59ce27c93f74aaec6c61c7e2108526f6428e7aace91bc893da0d41a2",
"crates/gpui/src/fast/layout_bounds.rs": "6a76a1cb904e315c4a280c7630a4e43a591fe3f2fdce55709915a5a6df70eda0",
"crates/gpui/src/fast/layout_key.rs": "207bc5f474a48046e264eb0a88726d099e450cbd3694ec5e83dea642f114d048",
- "crates/gpui/src/fast/mod.rs": "5dde3db3553fee6e83245de20e6ba6d835acfdbfeae77129a807d5d762a5e603",
+ "crates/gpui/src/fast/line_break.rs": "a136de66c81ebeb3304916b4ec664762f4ed18da114f7009f61617b4ec90a8aa",
+ "crates/gpui/src/fast/misc/mod.rs": "c35c2b066dcca039e0013a7c4d8742eaa042352dd97f84ca4907406cc0a71808",
+ "crates/gpui/src/fast/misc/touch_fling.rs": "37ed876f8fe36abcae802948683482f6cc57ec83115b3cbe99470f596667034c",
+ "crates/gpui/src/fast/mod.rs": "01483dfb5d6998043a72a4c7bf9ae6fdd6fd8fca5c8f1a799c3d2777b589f35f",
"crates/gpui/src/fast/number_shaping.rs": "20282510326771e7bf5df622b49fa6b1a50d40d830485286fbd652db6a92413d",
"crates/gpui/src/fast/path_cache.rs": "a987001a9ea91419d280b0d8af78e5ea167bbde1390a7a532bc00ba56fe08ffe",
- "crates/gpui/src/fast/retained.rs": "b69876900592af14fa0bc7dd5263bd14f9fd55de59fc4b5e8bfbdf91226c39c2",
- "crates/gpui/src/fast/scene.rs": "a0207c22d41a761dbb7430e25902f0d7cdbba10643a12853e646df4fdb1aa0e9",
+ "crates/gpui/src/fast/retained.rs": "0fcb648212ffb25960c1008fe20e7d7955b51021bdbfd159f22821af6821d920",
+ "crates/gpui/src/fast/rounded_clip.rs": "2836a439d798e36b97777df8eb516a5a37053da032c56f4969f43a46da3e520f",
+ "crates/gpui/src/fast/rounded_clip_fixtures.rs": "3311656ed7d47e7c785729bbc4ca888c783f0c87a232e117b0e90dd3734918e9",
+ "crates/gpui/src/fast/rounded_clip_scene.rs": "ba9630fcc8a021857841492e69f4cc27184a5add36ebebf2c6e5b176a62a0088",
+ "crates/gpui/src/fast/scene.rs": "643cb067383d09a74241d3a4e989c6acb88e6cd7b7b5e70cc62fab1d6761d1b9",
"crates/gpui/src/fast/splice.rs": "b1dd1dc183b259ecf3cc88fca79243c19ab9d37ac60d24cc4805da407bc78d6b",
- "crates/gpui/src/fast/stats.rs": "84781cd88451b8d4612a1745adb2bda4186c5302b90e39311ad53bab2df402ed",
+ "crates/gpui/src/fast/stats.rs": "ef9f4d300f7f517af2b61b9ca372518917b98e0332b8e0941384181880346bc8",
+ "crates/gpui/src/fast/tests/a11y.rs": "bedcb90fef1418867b8d68154223f86d669cd9e5061c8574c51dfd62f2122e03",
+ "crates/gpui/src/fast/tests/cached_layout.rs": "9bf9240ce3e477bf9713211fde26412642d48dcf625ebf737e69e8312ada4216",
"crates/gpui/src/fast/tests/dependencies.rs": "fb43db6ed771235766ca50a7c19e7b0220b0610af3e23992283e1bc60ac413c6",
"crates/gpui/src/fast/tests/dispatch.rs": "80e60f28d65b094dc0a6ec14b86b2fa77022310382164c6553b6a861fa1c0529",
"crates/gpui/src/fast/tests/global_id.rs": "e114b24de4a1a82c6aa16537e8783e2a493b1f6a4cf3d45e8df9c3d5e88608db",
- "crates/gpui/src/fast/tests/layers.rs": "968f6023b98084cd60f14f75beaa68e3b6f44dac5d6f4f79d24b6609e7e37697",
- "crates/gpui/src/fast/tests/layers_lists.rs": "a6f45ce4f8e3867af006692fa67db1dcd912895931d6ff1594994ac67a0471e6",
+ "crates/gpui/src/fast/tests/layers.rs": "a290b6b46ecf52cb4747ada0b156e06a15417b59eb9f7f47939f7e1c234bb4ca",
+ "crates/gpui/src/fast/tests/layers_lists.rs": "351019cd9d9d99e1b19e0f2fffee1edc51f4997701f8741ca0a6367058199e89",
"crates/gpui/src/fast/tests/layers_oracle.rs": "6c5883a4f26ff4238e9355d50e583938e213dd2508de64fb6508afbf9386c799",
- "crates/gpui/src/fast/tests/layout.rs": "3dff17c6786f686c9c20610cdf8ce4e378dd262daa8dcbe9a1c78f3a8de04801",
+ "crates/gpui/src/fast/tests/layout.rs": "adc89337813fe43afb09a55e2185d9f3a2a58bff167e700e769066a855ba73e6",
+ "crates/gpui/src/fast/tests/misc/mod.rs": "d3edc13befd6096fd0735b791d1db2a24c079a1a1941bcec46a39a7e1d94cc12",
+ "crates/gpui/src/fast/tests/misc/touch_fling.rs": "2908337c4797a1280f5f77d5d6a3031be21f57637e9e43f2b5fae0efc559f381",
"crates/gpui/src/fast/tests/mod.rs": "44d1c520e5556d0851db1bcce1f18cf4bab04f81ac84247b4459b12026ba3785",
"crates/gpui/src/fast/tests/number_shaping.rs": "90ca2782c11c124ef64e1f38f8413e266363753964a1faf2a84edccc72dd533d",
"crates/gpui/src/fast/tests/oracle.rs": "46f74be40bc930b1cc369e92a918cdf4902c97b125018d9313b111ba4f27bbd3",
"crates/gpui/src/fast/tests/path_cache.rs": "38d69662c00114e3e1c9db2029c0e63ba56110783a01a9c3cd37104e0524ef91",
- "crates/gpui/src/fast/tests/retained.rs": "0b682768b6c96e94303df59cffb5b039b11b05087f0c4dc422e3c3fc302cf913",
+ "crates/gpui/src/fast/tests/retained.rs": "bf52dd59ec4a852e84bc74b5a5849d17b510b0fdbaea04ae5046e02ef279c6c1",
"crates/gpui/src/fast/tests/retained_bench.rs": "a225a193b5ff4954c28cd435d77b9c2e5614e4e59b6cd1e6201a663f1c704311",
"crates/gpui/src/fast/tests/scene_order.rs": "e0966c3c8f08eb388c0358672f6b1877a9f8606085a63d3a15ac3efc1b3f15dd",
+ "crates/gpui/src/fast/tests/scroll_to_bottom.rs": "ce19afc434fe0dd7ec464073d09abeacd9fa9b927061d9a3b633e7105888065c",
"crates/gpui/src/fast/tests/splice.rs": "8af04038af9454aaa6ecad9e72498d30444ab3f027c4c7b92242105f52e9716e",
- "crates/gpui/src/fast/tests/support.rs": "b119ee26c1cda6ec424043078fca61b39d589ebfa97436f080cf93be8b0ff21b",
+ "crates/gpui/src/fast/tests/support.rs": "499bdc891f21b3bdb82ca5883ce277afd190a42a1f24db3594de15fabe5de107",
"crates/gpui/src/fast/tests/text.rs": "fc79c6c46c3db1386ba9450d10dbef6e705e4f7c3f1b5130b038599d2627a667",
"crates/gpui/src/fast/tests/text_shaping.rs": "108f2e286a6a55f9ed89eacbb9d010beab386f4bb22ff44560c3a82b7ee337df",
+ "crates/gpui/src/fast/tests/wheel_notifies.rs": "226e8aa39a0523a2b48882ad36dad94178fecc16ced43cf326cff125b0d2b534",
"crates/gpui/src/fast/text.rs": "de7ecd08c71fe2bb8c4111a57ba862c84bab72d8b10d114414e20c12686d4c9a",
"crates/gpui/src/fast/text_style.rs": "37bdddfd10919e8281d3a6ae236a8f869c4b6f60f2e8508cebaca523fea31138",
"crates/gpui/src/fast/windows_manifest.rs": "2180789e5d545e4a77e4a6f56584de05908cf7f6a9bdc642f378c65a3723b9d6",
"crates/gpui/src/geometry.rs": "900fc506a964cda44ed0cf1f97c01ff28d7268c87c9eeadbf7a4f1bb50e33edd",
- "crates/gpui/src/gestures.rs": "377f4c6a16366f024bf57246db6577b5283601701f1f1ff9821ba18844e735be",
+ "crates/gpui/src/gestures.rs": "52a9dc16cb6c761bc0270a34b56dd7a058624885d211f2a3e3bc33c0b2f2f719",
"crates/gpui/src/global.rs": "c52ddc647f712282a3a1f3629f864d0868966169fee7aff5f1f8d27e67f3f077",
- "crates/gpui/src/gpui.rs": "3da43f4a892b0ed8fcf75e3bd46fee5212e57d0e722598ed86b4752f9bc20d31",
- "crates/gpui/src/input.rs": "1e27952de8d447b268db7894f74fb41dad65a65c7d872466f589917b023a295d",
+ "crates/gpui/src/gpui.rs": "2c8b75831e9cda1ad19b8d60466cac9a8316ec19e7e1f5f8da917f5e90f4ea50",
+ "crates/gpui/src/hot_reload.rs": "9f756bb1bc5c6e5dc9fd5000ea5812cc5440006166d54c8982c3bdea70062de9",
+ "crates/gpui/src/input.rs": "6128860a23946373d0c52c7eaabe980a165999640ec012c33a821fd11c9f7eda",
"crates/gpui/src/inspector.rs": "39a2790b992b2c354eccb4d60a7070e41628a86c5463d339f9c7b8fa250c1ede",
"crates/gpui/src/interactive.rs": "443b41c4f7d6d31ba288a49a151dc2280ec1e1e20df2f558551dc07009672087",
- "crates/gpui/src/key_dispatch.rs": "154ed21d42fceffcb4dd65a6f96fe36cc2a6e89d139ac2d1e7213c65306271bd",
+ "crates/gpui/src/key_dispatch.rs": "205fe979515048cfd043933c293816367fa31b01bac8577b794e5e6890f30dfb",
"crates/gpui/src/keymap.rs": "be60f705557a311e1a6ec7c6fbc9a1f5dc3f7a784630edeba98a8a14d7796c5b",
"crates/gpui/src/keymap/binding.rs": "7306cd42de8bd0dbbf111c7b1c9d30b2a814251ad0f00b7f7cb3bf042925f895",
"crates/gpui/src/keymap/context.rs": "dd3cf2e883caed26e201a8aed39fbc51de782e7f806ecc22fde617929ca223cf",
"crates/gpui/src/path_builder.rs": "3e5a02f2896f20dd5aed61a423a5a6fbf9d4e886bb9004f0958374c16b43d268",
- "crates/gpui/src/platform.rs": "c94a64a0c41b16136e99d61037fc97f99a340d36b3d48c64b8102bdf86b5509b",
+ "crates/gpui/src/platform.rs": "1370afcc7ec18e63759de74ba1992eb30b3381c2844aadef92ce608f7cff1a4f",
"crates/gpui/src/platform/app_menu.rs": "c751e70bf50cf2366dad9d6b2a66fc3a8e9cd1547128173dbfc59520feb454cc",
"crates/gpui/src/platform/keyboard.rs": "8de8d585f4238204fd14113abded703afa6bb0a532dacabdf400df8249390376",
"crates/gpui/src/platform/keystroke.rs": "6216205d7b3557d7ece9c91faa1931c2ff121bfb1157dce5aaf408d1459c0c34",
@@ -125,7 +143,7 @@
"crates/gpui/src/platform/test.rs": "52861874bfdd1433728d61bd7be3a40b1f8c27aae4e4cc9d31b311ef13bda994",
"crates/gpui/src/platform/test/dispatcher.rs": "dea03a78a0c64d486f2c5b7bc669ba281638a0f8ee53316a52993a0e5b5cabd0",
"crates/gpui/src/platform/test/display.rs": "f026713ec76569ea1402b6d1d4996636530d4f592245a0c97ac795d112f8debf",
- "crates/gpui/src/platform/test/platform.rs": "296df7c83bc522af226752183c2bb5dcdf17d7a38ee7dcb2ef356b41ceecf968",
+ "crates/gpui/src/platform/test/platform.rs": "3769b094a86b268114a40843aae731b747ce1eb4ca58e7c2a0c56b1e76efa037",
"crates/gpui/src/platform/test/window.rs": "3b359c9e7943ccb065096c718756b32400dac3394bfd9ab6f361dda8163e97bd",
"crates/gpui/src/platform/threaded_dispatcher.rs": "aff57da6e74ddb0dbdb57f2c58b08236c1b78a4971e0c6d2f095f0746096f09c",
"crates/gpui/src/platform/visual_test.rs": "50625b19b4607d8bf2c9d2308491e0efed9b1e1822ac27454c9d5250134bbbe6",
@@ -138,15 +156,15 @@
"crates/gpui/src/profiler/journal.rs": "78240995b3727fb3cdcf4f8c06f38f81c49f9a3a0793b414450e19bb7da00c3e",
"crates/gpui/src/queue.rs": "9753459cd04fedae7f2323bd9b1b2e7379e0cf10858f4b82922d62a84d474710",
"crates/gpui/src/randomized_element_tree.rs": "15cdb363696562468db34a923414385e01096253c7ebff59a605c4bd487a6091",
- "crates/gpui/src/scene.rs": "e0461bb783d64bc81b4b8d23e2841dee70f676e39a40a8e8293975480a59544d",
+ "crates/gpui/src/scene.rs": "9b191b9b392f9d4162fd898f9fe3e8ce50f98613a628d3c58dc5ee8299d7cb8a",
"crates/gpui/src/seeds.rs": "5ee929b48bf3f5b0fa6ba0dd99cb87371ba4dfcf0c09814494b3af3b4fd5971c",
"crates/gpui/src/shared_uri.rs": "7a0a4feb3e290bb3f2c15fd4c8ee4c8ca3a9a5974e04ec084c4f8f0f3fc89248",
"crates/gpui/src/spring.rs": "1cdc32dc3fd2d2b8cbdc7809e51c276ced938592dd08083fe7cc063a02086663",
- "crates/gpui/src/style.rs": "b4a0e838fc7050cef25f71aae623cf86010c2ec361600479d24ce1aaff91bdc1",
+ "crates/gpui/src/style.rs": "8603c7d5aba10e64bb17298fa459fe1ddbdbe75aa000aae59140f21bab547963",
"crates/gpui/src/styled.rs": "c6e62fe136bedf887ee755c886df5466d5fe0b398a004b35d1a3530fef809c8d",
"crates/gpui/src/subscription.rs": "cca3b5138cb23084932772c8e009cce8a08562c2c00b69f3e0d4b1676b88087a",
"crates/gpui/src/svg_renderer.rs": "fb12dae87c0cd4c5f5a1fbbd2f9548d52fde9440c21e3d576dfeb0233eda88a6",
- "crates/gpui/src/tab_stop.rs": "7f742d6d6b7e5cd6fc634f2764fc3937e1cdebcf086d4b7c166379724edd1881",
+ "crates/gpui/src/tab_stop.rs": "0c14aec0196ad632601015e166dfcd3df72228292646120bbc3857c3631cd680",
"crates/gpui/src/taffy.rs": "9095901a5e857b004a5331f7c04ae8b86a7b922548e9c4edff8ff14454d31dbd",
"crates/gpui/src/test.rs": "205f9f481fe843470dca43972853f4d64b23ddd03ee9b26eebbf2a2508a9c65f",
"crates/gpui/src/text_system.rs": "1061b423cdceccf3ea49c05dfff87e8ad39390cab387d4e6fe8a82bc7ae58334",
@@ -156,18 +174,21 @@
"crates/gpui/src/text_system/line_layout.rs": "c72a0c6c7ffdc737d314b2b0306e478b0bfc92fb102042c7642009fe15f8e4f1",
"crates/gpui/src/text_system/line_wrapper.rs": "e96b639b5399340b9c716f875deb5b35e2ca29562235336f1ae8489e83d6d376",
"crates/gpui/src/util.rs": "04397f667bf5d890cce690b96441cd5b8f5eb0a5dd2942ce27b0314be38fa636",
- "crates/gpui/src/view.rs": "eb93d6e4e2354be24d475862d66f97658f263bbc889788ce33bf5dac43576cac",
- "crates/gpui/src/window.rs": "3ec2852be7c88ad799506ae5157a925e1ee2e53bcdc31fb22d22a743be6e8bb1",
+ "crates/gpui/src/view.rs": "86d53d5aca92397279a9a8eefb3f819610194d55199e65ed98ba55a08ac93675",
+ "crates/gpui/src/window.rs": "974e54031e2c3d5d7a13b2e7f38b25105652a136749378685a4b94484dc35eb1",
"crates/gpui/src/window/a11y.rs": "e0dd2b67fb2e8ea5d44498c46d3379a9170dcb2a04a746d69cc227a931fb981c",
"crates/gpui/src/window/a11y/debug.rs": "83d0693effdc98e64a083b217dbb09623f03f8f2b55db4a10f567aee977985d8",
"crates/gpui/src/window/prompts.rs": "0d58ec14e8139af53b50f4ad67690f01959ed59ffafc4adb2cf522f3f6197164",
"crates/gpui/tests/action_macros.rs": "47b69024f403feb6cb63c9990a6993b8582856146f5a6ae16984714321956a84",
"crates/gpui_linux/Cargo.toml": "cb5b561eae5dfde5ce2dadefae5f9775c1c1d5871bac8f04b2a27c34d7012b8c",
"crates/gpui_linux/LICENSE-APACHE": "5f78a733701db49b73da93addad8cb74f816f3789ef30c3c0e959e943ecd99b4",
- "crates/gpui_linux/src/fast/composition/mod.rs": "442f1133ebeb57f594d3917cb22c0d7764fe023adf4518ab454927476ee38e78",
- "crates/gpui_linux/src/fast/composition/wayland.rs": "d0198c278d843222b2a552dbd324f4fdac532319373f941a5870a5a9d758b164",
- "crates/gpui_linux/src/fast/composition/x11.rs": "333bcabaf8a83f6de756df05dee193cccd33ff2af3533401e4b9e4db3895be69",
+ "crates/gpui_linux/src/fast/composition/mod.rs": "1bd6259cf207ae951bb3f2ba9a0d704bfa44517497a09495b1a6f08e0e3f8710",
+ "crates/gpui_linux/src/fast/composition/occluders.rs": "bad9fac0777025521b843926c8ad6dfa4ca8afe347ae974b92d00c133ac7dcec",
+ "crates/gpui_linux/src/fast/composition/wayland.rs": "3c038621284e9344967c755197de56031a01789c33df9678a77726c5ab21c4a3",
+ "crates/gpui_linux/src/fast/composition/x11.rs": "89c4b1404620329752c47bfce00a51ec344f96aa440689bdc489eed8d14d5114",
"crates/gpui_linux/src/fast/mod.rs": "2f8a5991eff2c657b47cf1e5d2067fa72ac32ba4b43288f563ff6b841338f354",
+ "crates/gpui_linux/src/fast/x11_scale.rs": "9806732b010bd4e72f88fc73760a4c5e049b745b0d1908c2a056856d4104b0aa",
+ "crates/gpui_linux/src/fast/xim.rs": "dbd3c3d2f2214b2106cae74087f7d00723cb8661b0d801e9e74424d21fee3017",
"crates/gpui_linux/src/gpui_linux.rs": "3e81d191a6e982fa697a4b767d1ec42eb7a2cd99757117208bbf305bfe2bc71f",
"crates/gpui_linux/src/linux.rs": "ecba9ad21adc2ae731cd539e375bfc211b645ba365f00cb1cc35ce62c156bfca",
"crates/gpui_linux/src/linux/dispatcher.rs": "ad54b0169239a2e8fef273a2eff73b50fe565b954fc423b8a1b430b302822363",
@@ -179,7 +200,7 @@
"crates/gpui_linux/src/linux/system_notifications.rs": "49030ee41edb842c996d32870c4d9846f12bd8a50f4a6db0bea9dc658329f1b5",
"crates/gpui_linux/src/linux/text_system.rs": "afc6712696eed6749cc277b800cfb62690af9cbfe275961ad1df1c8d190d4028",
"crates/gpui_linux/src/linux/wayland.rs": "d3b0faac9d726ecae306c52914bcec72253d7308f5b499235d51816397a4736a",
- "crates/gpui_linux/src/linux/wayland/client.rs": "e3a4f24504a9c1200dd99c63ab28ca2ca0d95e1c5756cd8f0922f94f2230370f",
+ "crates/gpui_linux/src/linux/wayland/client.rs": "8530a9aeb624f1eb632a0a83e3275e17d35861f43ffa095e7190735cacca7c8d",
"crates/gpui_linux/src/linux/wayland/clipboard.rs": "7dab6bf97638ed62a6f6ebb7d6f435c6663631ee759a48b69ca4037cbda54df1",
"crates/gpui_linux/src/linux/wayland/cursor.rs": "1645dc1e70fe9bb766ee65ab78fa6590538a7a52125de4d86dde18a26004c821",
"crates/gpui_linux/src/linux/wayland/display.rs": "79253d4d1860ea5e36e733e35f820600e51a288c7504c2e731d4b86866dc1113",
@@ -188,7 +209,7 @@
"crates/gpui_linux/src/linux/wayland/serial.rs": "e45add908e937b0af6897e1a462357e0fc99dcc0c53c802d912c5659ffedc72c",
"crates/gpui_linux/src/linux/wayland/window.rs": "251f24f46c9d99dc51c6f54ca288d72ade93e8787bb72217f6945bfe1a8d48f7",
"crates/gpui_linux/src/linux/x11.rs": "df1e4f889ecaccf9ea9d7cc9f539fa4cdaac463998bbbd8367ff919f49e4ddf3",
- "crates/gpui_linux/src/linux/x11/client.rs": "a0ae8b7547a5ba075d68608bf94400d32695476a5488c58f34409e0664d867e3",
+ "crates/gpui_linux/src/linux/x11/client.rs": "0456e08e6ebacedcaa9810a21ca3b5ff877da6efdc944572c4f9988da52650b3",
"crates/gpui_linux/src/linux/x11/clipboard.rs": "580eb2519d8c38422b3e5baba24a11e6fa5e942392c9a9e74abedfa3fa84c0f3",
"crates/gpui_linux/src/linux/x11/display.rs": "d625c6aa288b8d302cd86f704a507c947d4b09d3d746d3004ea917fd6dbaaa35",
"crates/gpui_linux/src/linux/x11/event.rs": "6303b0fd823f7fd7bcdab70ec085b31705f1e82fc254c385a523df309462e5b5",
@@ -200,7 +221,7 @@
"crates/gpui_macos/src/display.rs": "4be5fbeedecfc6d6f60afeb29704790bb67c78554c7ae8178a07d4a4303a9afa",
"crates/gpui_macos/src/display_link.rs": "3f9de99c94f9390abcc1f1d69729f35304b233468668e471420793b0f4e02513",
"crates/gpui_macos/src/events.rs": "a4027cd12e14953b0954c5c04981148125ff85e8460a7219200dbcc3e93d50f7",
- "crates/gpui_macos/src/fast/composition.rs": "2408bd9a56c5f657c732f26335a2ca18ccf51bd88e6e6e64d3b165d727f02412",
+ "crates/gpui_macos/src/fast/composition.rs": "140a29eb07f6b4646523bdb2f77d02852f186fc4cab360cfafa6d89621705d3c",
"crates/gpui_macos/src/fast/mod.rs": "751d08c82a29bb2a067b6a2bee94522e32686f935246c90f0c78c961a4c15467",
"crates/gpui_macos/src/gpui_macos.rs": "81aef744ceb3ffa7d4c9f53fea2218186eab76394a7c47bacb4bc8bf6f84a121",
"crates/gpui_macos/src/keyboard.rs": "d6cbb1b91f11a67836a094689989b1ea7d79df60d7fa7b1829e6e78e9a01ad2b",
@@ -208,11 +229,11 @@
"crates/gpui_macos/src/platform.rs": "1bf0adb67969e7389063de4d54efa70e9bb0a7ac7331a74ba74245d3bb87d801",
"crates/gpui_macos/src/screen_capture.rs": "996c4308d93aea1c809e27baa2ac42b18d038a677cfdd599e481abf5ad200d4b",
"crates/gpui_macos/src/system_notifications.rs": "df8374f075932996d58b99c043d97a82e1cefebab79e5b0f4648c8a3776ea5fe",
- "crates/gpui_macos/src/window.rs": "544f608f09b0799acd3b6d1ee4962b666bdb703dedc105a68cceb330eb508777",
+ "crates/gpui_macos/src/window.rs": "d43c57eee3d739d455150c0574f84b6118e80a09634b20f5e064bf0bd9d329af",
"crates/gpui_macos/src/window_appearance.rs": "b8a1950175c0184469606447fca9450ab39a194fde04b80da3f0ad468ce2bd03",
"crates/gpui_windows/Cargo.toml": "e85aaccd29acfb146a18b34ae2e57dc66050859d5663178eb80d29bea2a693e1",
"crates/gpui_windows/LICENSE-APACHE": "5f78a733701db49b73da93addad8cb74f816f3789ef30c3c0e959e943ecd99b4",
- "crates/gpui_windows/build.rs": "d01829e27c91591c6fc8ee12ea90f279debc7b378c42253739cd94c7eb37a1a4",
+ "crates/gpui_windows/build.rs": "543e093723fe0f930d5cb30c8b60148adb08c448a115a650bed50dbc5159b7d0",
"crates/gpui_windows/src/alpha_correction.hlsl": "d37b106f1266b40a0e17478455dfaac1205214f597e23fbd0a26455c49806bf4",
"crates/gpui_windows/src/clipboard.rs": "eb68368598663c85916328d03582435d923e2b6eeae87a7a365a386848b87bf9",
"crates/gpui_windows/src/color_text_raster.hlsl": "53bb7f737dcf5b7c7a2c474cf6e7f649cc4e7d49b5526c2ac5f75f75086ce2b8",
@@ -222,24 +243,27 @@
"crates/gpui_windows/src/direct_write.rs": "27ff8aa902705794c5b9c0405bb7c3063e4b63e0d3ac44d92148d9ed18ee866c",
"crates/gpui_windows/src/directx_atlas.rs": "eff897707e3b8b4e549cd247725a6cee9c14b6a8188e32d32ad6360d693ce455",
"crates/gpui_windows/src/directx_devices.rs": "83372170f6225db45033473d118fc43708942ec9687467de09e2b5215c835cf1",
- "crates/gpui_windows/src/directx_renderer.rs": "9781ee407151b3ef9b0be5436c518cd13e97f62131aed4145b74df1fc96d0390",
+ "crates/gpui_windows/src/directx_renderer.rs": "8c1bbb79ddf92a82ba7bc20df7e998a96101d88ef4451d7f4d2ee4dee4f00902",
"crates/gpui_windows/src/dispatcher.rs": "a24daa0105ceadc2a4d50f0d1afab5a4af441a6a1254412d9811a04095f8cdd4",
"crates/gpui_windows/src/display.rs": "466c0c2a4e3e9b001f26681f9f992d7dcc4a930954749942b980f228c5c0b83f",
- "crates/gpui_windows/src/events.rs": "0a367919fd5a2cfb02b17a9b38b408ce7df812cc494365ccce0bfa5fb83652cc",
- "crates/gpui_windows/src/fast/composition.rs": "8bba63b262e77d5e47e598ef437926b1aa0f4f571f14e79cf516d2126024eba3",
+ "crates/gpui_windows/src/events.rs": "e2b5602dba55212056e6e9a9d1f7bc04f8ec3530ef7247802a85b84bb62de481",
+ "crates/gpui_windows/src/fast/class_names.rs": "53c3a53d7afdcd7d14dbad534d07751ab23a8dce696b882fe504f050c00890e8",
+ "crates/gpui_windows/src/fast/composition.rs": "dec26761f2e55f80b3cc201ee6c8cc3b19bd4061a783fdf16bcf0b1c52edef47",
"crates/gpui_windows/src/fast/draw_state.rs": "aaf39581fbc365fc7832d65ead8c2ef2a7922e98a5552918ae78b105db657c61",
- "crates/gpui_windows/src/fast/frame.rs": "5ef34a60a1d8b848b1724d85387e502469bf526e241287eaeffc6fe400cbf76c",
+ "crates/gpui_windows/src/fast/frame.rs": "76d9724e33526d01fc47c3b9938852440e4bd5d54d31933340b94e3483348110",
"crates/gpui_windows/src/fast/globals.rs": "00a46a726d3d3161728e89374fc29ba012235cf1dc4db18cf3e47036337bfd31",
- "crates/gpui_windows/src/fast/layers/composite.rs": "5f4b6ee509646797df5fcf5b114abd7e6e968fc23bce43ba140a939cc80e77a1",
+ "crates/gpui_windows/src/fast/layers/composite.rs": "6f5290c01e9796d297c75793dc71868f0f504503ea150c0e48d7b1c059c74bdc",
"crates/gpui_windows/src/fast/layers/mod.rs": "92912cd1ffe70202a45998cdafceee4a69dfd64421f349a948b24df74b066e53",
- "crates/gpui_windows/src/fast/layers/raster.rs": "82f788b7315fdb7a6e56426863884fb384f7dd7ee6a96e1236092dab0380ad1e",
- "crates/gpui_windows/src/fast/layers/tests.rs": "b62f3cbcae112a02169e9c852da53efbdf3704c4d1dce54c0faa31fbd8453f76",
+ "crates/gpui_windows/src/fast/layers/raster.rs": "44c7171efffe1076775671c9745122703f777e95262e513914827caf4c05c2d5",
+ "crates/gpui_windows/src/fast/layers/tests.rs": "9f24e3f2c4afe035ca11e426ee785c1edcd22b14cbd1ae5504545b9afd8cc7f1",
"crates/gpui_windows/src/fast/layers/tile_cache.rs": "f7d7e78bc48f632f53b18ea18b7bfad111a95eceed16ae6e4f399af397ac244e",
- "crates/gpui_windows/src/fast/mod.rs": "2c2a6b7e1e9f167fed7d91be5cd70a5c45d0bd24acaddd8f3cae636541c53a5a",
+ "crates/gpui_windows/src/fast/mod.rs": "bfba1b21ef505a9fa8f2edb727969641533d86f87d77f29917f979a83b3faf68",
+ "crates/gpui_windows/src/fast/rounded_clip.rs": "08ece3ace4634b0c99e025a2afb41b6b990e2f18687ae47042cd429cbd630c55",
+ "crates/gpui_windows/src/fast/rounded_clip_shader_build.rs": "a3903edc52ca9ccb1e589db317de234901fc908101a3955dfe33728b7eaca8ec",
"crates/gpui_windows/src/gpui_windows.rs": "cc3fd2d5b1799e4535694255d598001c99120f7b67f58ebee7af177d4db39753",
"crates/gpui_windows/src/keyboard.rs": "4e373ce1a3f23b6527b1abb3051b59c96d5cc2b0f777d4d4097dd27dd1673516",
- "crates/gpui_windows/src/platform.rs": "8dfd1d4aec63a589d708d7efb8ba04b427bcd5da533aa9258ae9308fef101018",
- "crates/gpui_windows/src/shaders.hlsl": "9f45956583cfef19558315789afe433309cb96896279035703a6b782faab632d",
+ "crates/gpui_windows/src/platform.rs": "f1efe9f7ae2a2f197679da241918945d576680a5e1b89472e82b2bf3b9e54718",
+ "crates/gpui_windows/src/shaders.hlsl": "376b89937d9afeb86353c81d06e38c005976506f436290e5ccc9442d56cbe752",
"crates/gpui_windows/src/system_notifications.rs": "d0159dbf6fd10622a5b106806ea2f57b3fe983eb6e2fc8635ab38bdf6529879d",
"crates/gpui_windows/src/system_settings.rs": "a9d22571c2405f610d44b85d86fbcf0356f7cc6aeeffad269a534b42eeffbdd9",
"crates/gpui_windows/src/util.rs": "e9bdd79c80149f7d60d2a35f4e86398343762a6c2549fd67de1de4de9ab65147",
@@ -251,24 +275,7 @@
"crates/scheduler/src/executor.rs": "90d6d3a3bd099e67048618298c6fc9ad72a93a2dd13bc5e92d60fd844acee1c8",
"crates/scheduler/src/scheduler.rs": "31a820dbb11e2b0371a83a080a934bd150a29ff4db5c0c21a1e05401aa64a691",
"crates/scheduler/src/test_scheduler.rs": "2c07373f6651ed486b28bdcd4f1bce8e6eb653d78a7588bc37c838b2da7585c2",
- "crates/scheduler/src/tests.rs": "fc268a8ecfe19e00833c060ccf7b8b246a692bdc3e04727a03b6d39ac011d5fa",
- "crates/gpui/src/fast/a11y.rs": "67cd7377e909f1ecb9c7ea2557a101f4fe41ec4165a6afb7955772df69c2321f",
- "crates/gpui/src/fast/layers/hover_freeze.rs": "249d6048dbaeae018e741358815b6b272c6868dee73d5decb553080cf4675981",
- "crates/gpui/src/fast/layers/overlay.rs": "63031bca6d31064880514b63c07607431a87fb38dedd5aba0468b4b9b7cd6b5c",
- "crates/gpui/src/fast/layers/scroll_to_bottom.rs": "4e1e550f63ac20cf4b35add92618e673a1de99d354c2cdc9c952a1475a824879",
- "crates/gpui/src/fast/layers/wheel.rs": "ebfcaba6fb3c95f7d1660710c2a266a4a7c9af63f0ada4a0e6ef892c6c91c0ce",
- "crates/gpui/src/fast/line_break.rs": "a136de66c81ebeb3304916b4ec664762f4ed18da114f7009f61617b4ec90a8aa",
- "crates/gpui/src/fast/misc/mod.rs": "c35c2b066dcca039e0013a7c4d8742eaa042352dd97f84ca4907406cc0a71808",
- "crates/gpui/src/fast/misc/touch_fling.rs": "37ed876f8fe36abcae802948683482f6cc57ec83115b3cbe99470f596667034c",
- "crates/gpui/src/fast/tests/a11y.rs": "bedcb90fef1418867b8d68154223f86d669cd9e5061c8574c51dfd62f2122e03",
- "crates/gpui/src/fast/tests/cached_layout.rs": "9bf9240ce3e477bf9713211fde26412642d48dcf625ebf737e69e8312ada4216",
- "crates/gpui/src/fast/tests/misc/mod.rs": "d3edc13befd6096fd0735b791d1db2a24c079a1a1941bcec46a39a7e1d94cc12",
- "crates/gpui/src/fast/tests/misc/touch_fling.rs": "2908337c4797a1280f5f77d5d6a3031be21f57637e9e43f2b5fae0efc559f381",
- "crates/gpui/src/fast/tests/scroll_to_bottom.rs": "ce19afc434fe0dd7ec464073d09abeacd9fa9b927061d9a3b633e7105888065c",
- "crates/gpui/src/fast/tests/wheel_notifies.rs": "226e8aa39a0523a2b48882ad36dad94178fecc16ced43cf326cff125b0d2b534",
- "crates/gpui_linux/src/fast/x11_scale.rs": "9806732b010bd4e72f88fc73760a4c5e049b745b0d1908c2a056856d4104b0aa",
- "crates/gpui_linux/src/fast/xim.rs": "dbd3c3d2f2214b2106cae74087f7d00723cb8661b0d801e9e74424d21fee3017",
- "crates/gpui_windows/src/fast/class_names.rs": "53c3a53d7afdcd7d14dbad534d07751ab23a8dce696b882fe504f050c00890e8"
+ "crates/scheduler/src/tests.rs": "fc268a8ecfe19e00833c060ccf7b8b246a692bdc3e04727a03b6d39ac011d5fa"
},
"additionalImports": [
{
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
(maka) 11/11: chore(licenses): regenerate notices for GPUI upstream sync
Posted by wy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch rust-preview
in repository https://gitbox.apache.org/repos/asf/maka.git
commit f5598e34e80bb77ba095e3efd339f53689b097dd
Author: M4n5ter <m4...@gmail.com>
AuthorDate: Tue Oct 20 17:31:21 2026 +0800
chore(licenses): regenerate notices for GPUI upstream sync
Inventory the current rust-preview dependency closure after synchronizing Fast 0.1.10 and Kit 0.8.0.
Generated-by: OpenAI Codex
---
products/maka/cli/DEPENDENCIES.rust.tsv | 40 +-
products/maka/cli/THIRD_PARTY_NOTICES.txt | 2647 ++++++++++++++++++++---------
products/maka/cli/notices.json | 371 ++--
3 files changed, 2100 insertions(+), 958 deletions(-)
diff --git a/products/maka/cli/DEPENDENCIES.rust.tsv b/products/maka/cli/DEPENDENCIES.rust.tsv
index 999c5e4747..1dd0a2e839 100644
--- a/products/maka/cli/DEPENDENCIES.rust.tsv
+++ b/products/maka/cli/DEPENDENCIES.rust.tsv
@@ -80,6 +80,7 @@ base64ct@1.8.3 X X
bincode@1.3.3 X
bindgen@0.72.1 X
bit_field@0.10.3 X X
+bit-set@0.10.0 X X
bit-set@0.8.0 X X
bit-set@0.9.1 X X
bit-vec@0.8.0 X X
@@ -450,16 +451,17 @@ glutin_wgl_sys@0.6.1 X
gpu-allocator@0.28.0 X X
gpu-descriptor-types@0.2.0 X X
gpu-descriptor@0.3.2 X X
-gpui-base@0.7.3 X
-gpui-fast-apple@0.1.7 X
-gpui-fast-linux@0.1.7 X
-gpui-fast-macos@0.1.7 X
-gpui-fast-macros@0.1.7 X
-gpui-fast-platform@0.1.7 X
-gpui-fast-wgpu@0.1.7 X
-gpui-fast-windows@0.1.7 X
-gpui-fast@0.1.7 X
-gpui-fps@0.7.3 X
+gpui-base@0.8.0 X
+gpui-fast-apple@0.1.10 X
+gpui-fast-linux@0.1.10 X
+gpui-fast-macos@0.1.10 X
+gpui-fast-macros@0.1.10 X
+gpui-fast-platform@0.1.10 X
+gpui-fast-wgpu@0.1.10 X
+gpui-fast-windows@0.1.10 X
+gpui-fast@0.1.10 X
+gpui-fps@0.8.0 X
+gpui-kit@0.8.0 X
gpui-pre-apple@0.3.9 X
gpui-pre-collections@0.3.9 X
gpui-pre-derive-refineable@0.3.9 X
@@ -479,7 +481,7 @@ gpui-pre-windows@0.3.9 X
gpui-pre-ztracing-macro@0.3.9 X
gpui-pre-ztracing@0.3.9 X
gpui-pre@0.3.9 X
-gpui-shell@0.7.3 X
+gpui-shell@0.8.0 X
grep-matcher@0.1.9 X X
grep-regex@0.1.14 X X
grep-searcher@0.1.17 X X
@@ -717,7 +719,9 @@ miow@0.6.1 X X
moka@0.12.16 X X
moxcms@0.8.1 X X
msvc_spectre_libs@0.1.3 X X
+naga-types@30.0.1 X X
naga@29.0.4 X X
+naga@30.0.1 X X
native-tls@0.2.18 X X
new_debug_unreachable@1.0.6 X
nix@0.23.2 X
@@ -1197,12 +1201,19 @@ webpki-roots@0.26.11 X
webpki-roots@1.0.9 X
weezl@0.1.12 X X
wgpu-core-deps-apple@29.0.4 X X
+wgpu-core-deps-apple@30.0.1 X X
wgpu-core-deps-windows-linux-android@29.0.4 X X
+wgpu-core-deps-windows-linux-android@30.0.1 X X
wgpu-core@29.0.4 X X
+wgpu-core@30.0.1 X X
wgpu-hal@29.0.4 X X
+wgpu-hal@30.0.1 X X
wgpu-naga-bridge@29.0.4 X X
+wgpu-naga-bridge@30.0.1 X X
wgpu-types@29.0.4 X X
+wgpu-types@30.0.1 X X
wgpu@29.0.4 X X
+wgpu@30.0.1 X X
which@6.0.3 X
which@8.0.6 X
widestring@1.2.1 X X
@@ -1214,13 +1225,10 @@ windows_x86_64_gnu@0.53.1 X X
windows_x86_64_msvc@0.48.5 X X
windows_x86_64_msvc@0.52.6 X X
windows_x86_64_msvc@0.53.1 X X
-windows-collections@0.2.0 X X
windows-collections@0.3.2 X X
windows-core@0.58.0 X X
windows-core@0.59.0 X X
-windows-core@0.61.2 X X
windows-core@0.62.2 X X
-windows-future@0.2.1 X X
windows-future@0.3.2 X X
windows-implement@0.58.0 X X
windows-implement@0.59.0 X X
@@ -1230,7 +1238,6 @@ windows-interface@0.59.3 X X
windows-link@0.1.3 X X
windows-link@0.2.1 X X
windows-native-keyring-store@1.1.0 X X
-windows-numerics@0.2.0 X X
windows-numerics@0.3.1 X X
windows-registry@0.4.0 X X
windows-registry@0.6.1 X X
@@ -1239,7 +1246,6 @@ windows-result@0.3.4 X X
windows-result@0.4.1 X X
windows-strings@0.1.0 X X
windows-strings@0.3.1 X X
-windows-strings@0.4.2 X X
windows-strings@0.5.1 X X
windows-sys@0.48.0 X X
windows-sys@0.52.0 X X
@@ -1249,13 +1255,11 @@ windows-sys@0.61.2 X X
windows-targets@0.48.5 X X
windows-targets@0.52.6 X X
windows-targets@0.53.5 X X
-windows-threading@0.1.0 X X
windows-threading@0.2.1 X X
windows-version@0.1.7 X X
windows-win@3.0.0 X
windows@0.58.0 X X
windows@0.59.0 X X
-windows@0.61.3 X X
windows@0.62.2 X X
winnow@0.7.15 X
winnow@1.0.4 X
diff --git a/products/maka/cli/THIRD_PARTY_NOTICES.txt b/products/maka/cli/THIRD_PARTY_NOTICES.txt
index 76f31e8997..71fc533796 100644
--- a/products/maka/cli/THIRD_PARTY_NOTICES.txt
+++ b/products/maka/cli/THIRD_PARTY_NOTICES.txt
@@ -399,6 +399,7 @@ rustix@0.38.44 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT); authors de
rustix@1.1.4 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT); authors declared by package: Dan Gohman <de...@sunfishcode.online>, Jakub Konka <ku...@jakubkonka.com>
rustversion@1.0.23 (MIT OR Apache-2.0); authors declared by package: David Tolnay <dt...@gmail.com>
semver@1.0.28 (MIT OR Apache-2.0); authors declared by package: David Tolnay <dt...@gmail.com>
+serde-saphyr@1.3.0 (MIT OR Apache-2.0)
serde@1.0.229 (MIT OR Apache-2.0); authors declared by package: Erick Tryzelaar <er...@gmail.com>, David Tolnay <dt...@gmail.com>
serde_bytes@0.11.19 (MIT OR Apache-2.0); authors declared by package: David Tolnay <dt...@gmail.com>
serde_core@1.0.229 (MIT OR Apache-2.0); authors declared by package: Erick Tryzelaar <er...@gmail.com>, David Tolnay <dt...@gmail.com>
@@ -513,6 +514,7 @@ https://crates.io/api/v1/crates/rustix/0.38.44/download :: LICENSE-MIT
https://crates.io/api/v1/crates/rustix/1.1.4/download :: LICENSE-MIT
https://crates.io/api/v1/crates/rustversion/1.0.23/download :: LICENSE-MIT
https://crates.io/api/v1/crates/semver/1.0.28/download :: LICENSE-MIT
+https://crates.io/api/v1/crates/serde-saphyr/1.3.0/download :: LICENSE-MIT
https://crates.io/api/v1/crates/serde/1.0.229/download :: LICENSE-MIT
https://crates.io/api/v1/crates/serde_bytes/0.11.19/download :: LICENSE-MIT
https://crates.io/api/v1/crates/serde_core/1.0.229/download :: LICENSE-MIT
@@ -609,6 +611,7 @@ rustversion@1.0.23 (MIT OR Apache-2.0); authors declared by package: David Tolna
ryu-js@1.0.3 (Apache-2.0 OR BSL-1.0); authors declared by package: David Tolnay <dt...@gmail.com>, boa-dev
ryu@1.0.23 (Apache-2.0 OR BSL-1.0); authors declared by package: David Tolnay <dt...@gmail.com>
semver@1.0.28 (MIT OR Apache-2.0); authors declared by package: David Tolnay <dt...@gmail.com>
+serde-saphyr@1.3.0 (MIT OR Apache-2.0)
serde@1.0.229 (MIT OR Apache-2.0); authors declared by package: Erick Tryzelaar <er...@gmail.com>, David Tolnay <dt...@gmail.com>
serde_bytes@0.11.19 (MIT OR Apache-2.0); authors declared by package: David Tolnay <dt...@gmail.com>
serde_core@1.0.229 (MIT OR Apache-2.0); authors declared by package: Erick Tryzelaar <er...@gmail.com>, David Tolnay <dt...@gmail.com>
@@ -662,6 +665,7 @@ https://crates.io/api/v1/crates/rustversion/1.0.23/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/ryu-js/1.0.3/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/ryu/1.0.23/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/semver/1.0.28/download :: LICENSE-APACHE
+https://crates.io/api/v1/crates/serde-saphyr/1.3.0/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/serde/1.0.229/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/serde_bytes/0.11.19/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/serde_core/1.0.229/download :: LICENSE-APACHE
@@ -993,6 +997,7 @@ fs2@0.4.3 (MIT/Apache-2.0); authors declared by package: Dan Burkert <dan@danbur
futf@0.1.5 (MIT / Apache-2.0); authors declared by package: Keegan McAllister <km...@mozilla.com>
futures-lite@2.6.1 (Apache-2.0 OR MIT); authors declared by package: Stjepan Glavina <st...@gmail.com>, Contributors to futures-rs
gethostname@1.1.0 (Apache-2.0); authors declared by package: Sebastian Wiesner <se...@swsnr.de>
+gif@0.13.3 (MIT OR Apache-2.0); authors declared by package: The image-rs Developers
gif@0.14.2 (MIT OR Apache-2.0); authors declared by package: The image-rs Developers
gimli@0.31.1 (MIT OR Apache-2.0)
gimli@0.32.3 (MIT OR Apache-2.0)
@@ -1029,6 +1034,7 @@ io-extras@0.19.0 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT); authors
io-lifetimes@2.0.4 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT); authors declared by package: Dan Gohman <de...@sunfishcode.online>
io-lifetimes@3.0.1 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT); authors declared by package: Dan Gohman <de...@sunfishcode.online>
ipconfig@0.3.4 (MIT/Apache-2.0); authors declared by package: Liran Ringel <li...@gmail.com>
+itertools@0.11.0 (MIT OR Apache-2.0); authors declared by package: bluss
itertools@0.13.0 (MIT OR Apache-2.0); authors declared by package: bluss
itertools@0.14.0 (MIT OR Apache-2.0); authors declared by package: bluss
jobserver@0.1.35 (MIT OR Apache-2.0); authors declared by package: Alex Crichton <al...@alexcrichton.com>
@@ -1119,10 +1125,12 @@ security-framework@3.7.0 (MIT OR Apache-2.0); authors declared by package: Steve
serde_with@3.23.0 (MIT OR Apache-2.0); authors declared by package: Jonas Bushart, Marcin Kaźmierczak
serde_with_macros@3.23.0 (MIT OR Apache-2.0); authors declared by package: Jonas Bushart
servo_arc@0.4.3 (MIT OR Apache-2.0); authors declared by package: The Servo Project Developers
+shellexpand@3.1.2 (MIT/Apache-2.0); authors declared by package: Vladimir Matveev <vm...@citrine.cc>, Ian Jackson <iw...@torproject.org>
signal-hook-mio@0.2.5 (MIT OR Apache-2.0); authors declared by package: Michal 'vorner' Vaner <vo...@vorner.cz>, Thomas Himmelstoss <th...@posteo.de>
signal-hook-registry@1.4.8 (MIT OR Apache-2.0); authors declared by package: Michal 'vorner' Vaner <vo...@vorner.cz>, Masaki Hara <ac...@gmail.com>
signal-hook@0.3.18 (Apache-2.0/MIT); authors declared by package: Michal 'vorner' Vaner <vo...@vorner.cz>, Thomas Himmelstoss <th...@posteo.de>
signal-hook@0.4.4 (MIT OR Apache-2.0); authors declared by package: Michal 'vorner' Vaner <vo...@vorner.cz>, Thomas Himmelstoss <th...@posteo.de>
+similar@2.7.0 (Apache-2.0); authors declared by package: Armin Ronacher <ar...@active-4.com>, Pierre-Étienne Meunier <pe...@pijul.org>, Brandon Williams <bw...@gmail.com>
similar@3.2.0 (Apache-2.0); authors declared by package: Armin Ronacher <ar...@active-4.com>, Pierre-Étienne Meunier <pe...@pijul.org>, Brandon Williams <bw...@gmail.com>
simplecss@0.2.2 (Apache-2.0 OR MIT)
smallvec@1.16.0 (MIT OR Apache-2.0); authors declared by package: The Servo Project Developers
@@ -1135,6 +1143,7 @@ string_cache@0.8.9 (MIT OR Apache-2.0); authors declared by package: The Servo P
string_cache@0.9.0 (MIT OR Apache-2.0); authors declared by package: The Servo Project Developers
string_cache_codegen@0.5.4 (MIT OR Apache-2.0); authors declared by package: The Servo Project Developers
string_cache_codegen@0.6.1 (MIT OR Apache-2.0); authors declared by package: The Servo Project Developers
+svgtypes@0.15.3 (Apache-2.0 OR MIT)
svgtypes@0.16.1 (Apache-2.0 OR MIT)
swash@0.2.10 (Apache-2.0 OR MIT); authors declared by package: Chad Brokaw <cb...@gmail.com>
syn@1.0.109 (MIT OR Apache-2.0); authors declared by package: David Tolnay <dt...@gmail.com>
@@ -1271,6 +1280,7 @@ https://crates.io/api/v1/crates/fs2/0.4.3/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/futf/0.1.5/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/futures-lite/2.6.1/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/gethostname/1.1.0/download :: LICENSE
+https://crates.io/api/v1/crates/gif/0.13.3/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/gif/0.14.2/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/gimli/0.31.1/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/gimli/0.32.3/download :: LICENSE-APACHE
@@ -1307,6 +1317,7 @@ https://crates.io/api/v1/crates/io-extras/0.19.0/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/io-lifetimes/2.0.4/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/io-lifetimes/3.0.1/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/ipconfig/0.3.4/download :: LICENSE-APACHE
+https://crates.io/api/v1/crates/itertools/0.11.0/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/itertools/0.13.0/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/itertools/0.14.0/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/jobserver/0.1.35/download :: LICENSE-APACHE
@@ -1394,10 +1405,12 @@ https://crates.io/api/v1/crates/security-framework/3.7.0/download :: LICENSE-APA
https://crates.io/api/v1/crates/serde_with/3.23.0/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/serde_with_macros/3.23.0/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/servo_arc/0.4.3/download :: LICENSE-APACHE
+https://crates.io/api/v1/crates/shellexpand/3.1.2/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/signal-hook-mio/0.2.5/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/signal-hook-registry/1.4.8/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/signal-hook/0.3.18/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/signal-hook/0.4.4/download :: LICENSE-APACHE
+https://crates.io/api/v1/crates/similar/2.7.0/download :: LICENSE
https://crates.io/api/v1/crates/similar/3.2.0/download :: LICENSE
https://crates.io/api/v1/crates/simplecss/0.2.2/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/smallvec/1.16.0/download :: LICENSE-APACHE
@@ -1410,6 +1423,7 @@ https://crates.io/api/v1/crates/string_cache/0.8.9/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/string_cache/0.9.0/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/string_cache_codegen/0.5.4/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/string_cache_codegen/0.6.1/download :: LICENSE-APACHE
+https://crates.io/api/v1/crates/svgtypes/0.15.3/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/svgtypes/0.16.1/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/swash/0.2.10/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/syn/1.0.109/download :: LICENSE-APACHE
@@ -3518,6 +3532,7 @@ Software.
================================================================================
+annotate-snippets@0.12.16 (MIT OR Apache-2.0)
anstyle@1.0.14 (MIT OR Apache-2.0)
bit_field@0.10.3 (Apache-2.0/MIT); authors declared by package: Philipp Oppermann <de...@phil-opp.com>
bytecount@0.6.9 (Apache-2.0/MIT); authors declared by package: Andre Bogus <bo...@gmail.de>, Joshua Landau <jo...@landau.ws>
@@ -3565,6 +3580,7 @@ toml_writer@1.1.2+spec-1.1.0 (MIT OR Apache-2.0)
unicode-general-category@1.1.0 (Apache-2.0); authors declared by package: YesLogic Pty. Ltd. <in...@yeslogic.com>
winapi@0.3.9 (MIT/Apache-2.0); authors declared by package: Peter Atashian <re...@gmail.com>
+https://crates.io/api/v1/crates/annotate-snippets/0.12.16/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/anstyle/1.0.14/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/bit_field/0.10.3/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/bytecount/0.6.9/download :: LICENSE.Apache2
@@ -3816,6 +3832,7 @@ Apache License
================================================================================
+annotate-snippets@0.12.16 (MIT OR Apache-2.0)
anstyle@1.0.14 (MIT OR Apache-2.0)
clap@4.6.6 (MIT OR Apache-2.0)
clap_builder@4.6.6 (MIT OR Apache-2.0)
@@ -3835,6 +3852,7 @@ toml_parser@1.1.3+spec-1.1.0 (MIT OR Apache-2.0)
toml_write@0.1.2 (MIT OR Apache-2.0)
toml_writer@1.1.2+spec-1.1.0 (MIT OR Apache-2.0)
+https://crates.io/api/v1/crates/annotate-snippets/0.12.16/download :: LICENSE-MIT
https://crates.io/api/v1/crates/anstyle/1.0.14/download :: LICENSE-MIT
https://crates.io/api/v1/crates/clap/4.6.6/download :: LICENSE-MIT
https://crates.io/api/v1/crates/clap_builder/4.6.6/download :: LICENSE-MIT
@@ -3903,6 +3921,7 @@ flume@0.12.0 (Apache-2.0/MIT); authors declared by package: Joshua Barretto <jos
fontdue@0.9.4 (MIT OR Apache-2.0 OR Zlib); authors declared by package: Joe Cumbo <mo...@gmail.com>
halfbrown@0.4.0 (Apache-2.0/MIT); authors declared by package: Heinz N. Gies <he...@licenser.net>
imgref@1.12.3 (CC0-1.0 OR Apache-2.0); authors declared by package: Kornel Lesiński <ko...@geekhood.net>
+kurbo@0.11.3 (Apache-2.0 OR MIT); authors declared by package: Raph Levien <ra...@gmail.com>
kurbo@0.13.1 (Apache-2.0 OR MIT)
llrt_abort@0.9.0-beta (Apache-2.0)
llrt_async_hooks@0.9.0-beta (Apache-2.0)
@@ -3923,6 +3942,7 @@ llrt_url@0.9.0-beta (Apache-2.0)
llrt_utils@0.9.0-beta (Apache-2.0)
llrt_zlib@0.9.0-beta (Apache-2.0)
lru-slab@0.1.2 (MIT OR Apache-2.0 OR Zlib); authors declared by package: Benjamin Saunders <be...@gmail.com>
+nohash-hasher@0.2.0 (Apache-2.0 OR MIT); authors declared by package: Parity Technologies <ad...@parity.io>
ntapi@0.4.3 (Apache-2.0 OR MIT); authors declared by package: MSxDOS <me...@gmail.com>
opentelemetry-http@0.32.0 (Apache-2.0)
opentelemetry-otlp@0.32.0 (Apache-2.0)
@@ -3988,8 +4008,10 @@ https://crates.io/api/v1/crates/flume/0.12.0/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/fontdue/0.9.4/download :: licenses/xi-editor/xi-unicode
https://crates.io/api/v1/crates/halfbrown/0.4.0/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/imgref/1.12.3/download :: LICENSE-APACHE
+https://crates.io/api/v1/crates/kurbo/0.11.3/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/kurbo/0.13.1/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/lru-slab/0.1.2/download :: LICENSE-APACHE
+https://crates.io/api/v1/crates/nohash-hasher/0.2.0/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/ntapi/0.4.3/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/opentelemetry-proto/0.32.0/download :: src/proto/opentelemetry-proto/LICENSE
https://crates.io/api/v1/crates/polycool/0.4.0/download :: LICENSE-APACHE
@@ -4264,9 +4286,11 @@ SOFTWARE.
================================================================================
arbitrary@1.4.2 (MIT OR Apache-2.0); authors declared by package: The Rust-Fuzz Project Developers, Nick Fitzgerald <fi...@gmail.com>, Manish Goregaokar <ma...@gmail.com>, Simonas Kazlauskas <ar...@kazlauskas.me>, Brian L. Troutwine <br...@troutwine.us>, Corey Farwell <co...@rwell.org>
+triomphe@0.1.17 (MIT OR Apache-2.0); authors declared by package: Manish Goregaokar <ma...@gmail.com>, The Servo Project Developers
unicode-script@0.5.8 (MIT OR Apache-2.0); authors declared by package: Manish Goregaokar <ma...@gmail.com>
https://crates.io/api/v1/crates/arbitrary/1.4.2/download :: LICENSE-MIT
+https://crates.io/api/v1/crates/triomphe/0.1.17/download :: LICENSE-MIT
https://crates.io/api/v1/crates/unicode-script/0.5.8/download :: LICENSE-MIT
MIT License
@@ -6260,6 +6284,32 @@ DEALINGS IN THE SOFTWARE.
================================================================================
+base62@2.2.6 (MIT); authors declared by package: François Bernier <fr...@gmail.com>, Chai T. Rex <Ch...@users.noreply.github.com>
+
+https://crates.io/api/v1/crates/base62/2.2.6/download :: LICENSE
+
+Copyright (c) 2015 François Bernier
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+================================================================================
+
base64-simd@0.8.0 (MIT)
hex-simd@0.8.0 (MIT)
uuid-simd@0.8.0 (MIT)
@@ -6446,6 +6496,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================================
+bit-set@0.10.0 (Apache-2.0 OR MIT); authors declared by package: Alexis Beingessner <a....@gmail.com>
bit-set@0.8.0 (Apache-2.0 OR MIT); authors declared by package: Alexis Beingessner <a....@gmail.com>
bit-set@0.9.1 (Apache-2.0 OR MIT); authors declared by package: Alexis Beingessner <a....@gmail.com>
bit-vec@0.8.0 (Apache-2.0 OR MIT); authors declared by package: Alexis Beingessner <a....@gmail.com>
@@ -6460,6 +6511,7 @@ minimal-lexical@0.2.1 (MIT/Apache-2.0); authors declared by package: Alex Huszag
presser@0.3.1 (MIT OR Apache-2.0); authors declared by package: Embark <op...@embark-studios.com>, Gray Olson <gray@grayolson.com
qoi@0.4.1 (MIT/Apache-2.0); authors declared by package: Ivan Smirnov <ru...@ivan.smirnov.ie>
+https://crates.io/api/v1/crates/bit-set/0.10.0/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/bit-set/0.8.0/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/bit-set/0.9.1/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/bit-vec/0.8.0/download :: LICENSE-APACHE
@@ -6678,15 +6730,13 @@ limitations under the License.
================================================================================
-bit-set@0.8.0 (Apache-2.0 OR MIT); authors declared by package: Alexis Beingessner <a....@gmail.com>
-bit-vec@0.8.0 (Apache-2.0 OR MIT); authors declared by package: Alexis Beingessner <a....@gmail.com>
-bit-vec@0.9.1 (Apache-2.0 OR MIT); authors declared by package: Alexis Beingessner <a....@gmail.com>
+bit-set@0.10.0 (Apache-2.0 OR MIT); authors declared by package: Alexis Beingessner <a....@gmail.com>
+bit-set@0.9.1 (Apache-2.0 OR MIT); authors declared by package: Alexis Beingessner <a....@gmail.com>
-https://crates.io/api/v1/crates/bit-set/0.8.0/download :: LICENSE-MIT
-https://crates.io/api/v1/crates/bit-vec/0.8.0/download :: LICENSE-MIT
-https://crates.io/api/v1/crates/bit-vec/0.9.1/download :: LICENSE-MIT
+https://crates.io/api/v1/crates/bit-set/0.10.0/download :: LICENSE-MIT
+https://crates.io/api/v1/crates/bit-set/0.9.1/download :: LICENSE-MIT
-Copyright (c) 2023 The Rust Project Developers
+Copyright (c) 2026 The Rust Project Developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
@@ -6714,11 +6764,15 @@ DEALINGS IN THE SOFTWARE.
================================================================================
-bit-set@0.9.1 (Apache-2.0 OR MIT); authors declared by package: Alexis Beingessner <a....@gmail.com>
+bit-set@0.8.0 (Apache-2.0 OR MIT); authors declared by package: Alexis Beingessner <a....@gmail.com>
+bit-vec@0.8.0 (Apache-2.0 OR MIT); authors declared by package: Alexis Beingessner <a....@gmail.com>
+bit-vec@0.9.1 (Apache-2.0 OR MIT); authors declared by package: Alexis Beingessner <a....@gmail.com>
-https://crates.io/api/v1/crates/bit-set/0.9.1/download :: LICENSE-MIT
+https://crates.io/api/v1/crates/bit-set/0.8.0/download :: LICENSE-MIT
+https://crates.io/api/v1/crates/bit-vec/0.8.0/download :: LICENSE-MIT
+https://crates.io/api/v1/crates/bit-vec/0.9.1/download :: LICENSE-MIT
-Copyright (c) 2026 The Rust Project Developers
+Copyright (c) 2023 The Rust Project Developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
@@ -7126,6 +7180,7 @@ seahash@4.1.0 (MIT); authors declared by package: ticki <ticki@users.noreply.git
simd_helpers@0.1.0 (MIT); authors declared by package: Luca Barbato <lu...@gentoo.org>
sys_traits@0.1.28 (MIT)
sys_traits_macros@0.1.0 (MIT)
+zune-core@0.4.12 (MIT OR Apache-2.0 OR Zlib)
zune-inflate@0.2.54 (MIT OR Apache-2.0 OR Zlib)
https://crates.io/api/v1/crates/pathfinder_geometry/0.5.1/download
@@ -7140,6 +7195,7 @@ https://github.com/denoland/rustls-tokio-stream/blob/c8a9239c987dbfa7b1eaed78d27
https://github.com/dsherret/sys_traits/blob/709e02e198721f81b485c078ccdf1835fa3e6a40/Cargo.toml
https://github.com/dsherret/sys_traits/blob/75cbf3ea478624cc78275b0264e78e9ae13ce3f5/macros/Cargo.toml
https://github.com/etemesi254/zune-image/blob/69502ce83fdfecdd0beefd677e2abb3781b29d98/zune-inflate/Cargo.toml
+https://github.com/etemesi254/zune-image/blob/f8fbb123d5ed04441e8324a555bfcda0cb1bd28f/crates/zune-core/Cargo.toml
https://github.com/longbridge/gpui-kit/blob/c5f8eca4a0759f8c0c7ed624eddfd03f1c93a5f2/crates/shell/rquickjs-compat/Cargo.toml
https://github.com/lu-zero/simd_helpers/blob/ca1a2f84aa386d758e98f8a609d990263932fb85/Cargo.toml
https://github.com/mmastrac/derive-io/blob/69a023098378b66e8f8852173b65ca5762b201e2/crates/derive-io-macros/Cargo.toml
@@ -14323,6 +14379,7 @@ DEALINGS IN THE SOFTWARE.
================================================================================
either@1.18.0 (MIT OR Apache-2.0)
+itertools@0.11.0 (MIT OR Apache-2.0); authors declared by package: bluss
itertools@0.13.0 (MIT OR Apache-2.0); authors declared by package: bluss
itertools@0.14.0 (MIT OR Apache-2.0); authors declared by package: bluss
petgraph@0.8.3 (MIT OR Apache-2.0); authors declared by package: bluss, mitchmindtree
@@ -14330,6 +14387,7 @@ serde_with@3.23.0 (MIT OR Apache-2.0); authors declared by package: Jonas Bushar
serde_with_macros@3.23.0 (MIT OR Apache-2.0); authors declared by package: Jonas Bushart
https://crates.io/api/v1/crates/either/1.18.0/download :: LICENSE-MIT
+https://crates.io/api/v1/crates/itertools/0.11.0/download :: LICENSE-MIT
https://crates.io/api/v1/crates/itertools/0.13.0/download :: LICENSE-MIT
https://crates.io/api/v1/crates/itertools/0.14.0/download :: LICENSE-MIT
https://crates.io/api/v1/crates/petgraph/0.8.3/download :: LICENSE-MIT
@@ -14706,6 +14764,29 @@ https://crates.io/api/v1/crates/enum-as-inner/0.6.1/download :: copyright.txt
================================================================================
+enum-iterator-derive@1.5.0 (0BSD); authors declared by package: Stephane Raux <st...@gmail.com>
+enum-iterator@2.3.0 (0BSD); authors declared by package: Stephane Raux <st...@gmail.com>
+
+https://crates.io/api/v1/crates/enum-iterator-derive/1.5.0/download :: LICENSE
+https://crates.io/api/v1/crates/enum-iterator/2.3.0/download :: LICENSE
+
+BSD Zero Clause License
+
+Copyright (c) 2018 Stephane Raux
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
+
+================================================================================
+
enumflags2@0.7.12 (MIT OR Apache-2.0); authors declared by package: maik klein <ma...@googlemail.com>, Maja Kądziołka <ma...@compilercrim.es>
https://crates.io/api/v1/crates/enumflags2/0.7.12/download :: LICENSE-APACHE
@@ -15373,26 +15454,37 @@ image@0.25.10 (MIT OR Apache-2.0); authors declared by package: The image-rs Dev
linebender_resource_handle@0.1.1 (Apache-2.0 OR MIT)
miniz_oxide@0.8.9 (MIT OR Zlib OR Apache-2.0); authors declared by package: Frommi <da...@gmail.com>, oyvindln <oy...@users.noreply.github.com>, Rich Geldreich richgel99@gmail.com
miniz_oxide@0.9.1 (MIT OR Zlib OR Apache-2.0); authors declared by package: Frommi <da...@gmail.com>, oyvindln <oy...@users.noreply.github.com>, Rich Geldreich richgel99@gmail.com
+naga-types@30.0.1 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
naga@29.0.4 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
+naga@30.0.1 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
+normpath@1.5.2 (MIT OR Apache-2.0); authors declared by package: dylni
num-conv@0.2.2 (MIT OR Apache-2.0); authors declared by package: Jacob Pratt <ja...@jhpratt.dev>
pin-project-internal@1.1.13 (Apache-2.0 OR MIT)
pin-project-lite@0.2.17 (Apache-2.0 OR MIT)
pin-project@1.1.13 (Apache-2.0 OR MIT)
portable-atomic@1.15.0 (Apache-2.0 OR MIT)
raw-window-handle@0.6.2 (MIT OR Apache-2.0 OR Zlib); authors declared by package: Osspial <os...@gmail.com>
+resvg@0.45.1 (Apache-2.0 OR MIT)
resvg@0.46.0 (Apache-2.0 OR MIT)
simdutf8@0.1.5 (MIT OR Apache-2.0); authors declared by package: Hans Kratz <ha...@appfour.com>
sync_wrapper@1.0.2 (Apache-2.0); authors declared by package: Actyx AG <de...@actyx.io>
time-core@0.1.9 (MIT OR Apache-2.0); authors declared by package: Jacob Pratt <op...@jhpratt.dev>, Time contributors
time-macros@0.2.32 (MIT OR Apache-2.0); authors declared by package: Jacob Pratt <op...@jhpratt.dev>, Time contributors
time@0.3.55 (MIT OR Apache-2.0); authors declared by package: Jacob Pratt <op...@jhpratt.dev>, Time contributors
+usvg@0.45.1 (Apache-2.0 OR MIT)
usvg@0.46.0 (Apache-2.0 OR MIT)
wgpu-core-deps-windows-linux-android@29.0.4 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
+wgpu-core-deps-windows-linux-android@30.0.1 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
wgpu-core@29.0.4 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
+wgpu-core@30.0.1 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
wgpu-hal@29.0.4 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
+wgpu-hal@30.0.1 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
wgpu-naga-bridge@29.0.4 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
+wgpu-naga-bridge@30.0.1 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
wgpu-types@29.0.4 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
+wgpu-types@30.0.1 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
wgpu@29.0.4 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
+wgpu@30.0.1 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
widestring@1.2.1 (MIT OR Apache-2.0)
https://crates.io/api/v1/crates/fdeflate/0.3.7/download :: LICENSE-APACHE
@@ -15454,26 +15546,37 @@ https://crates.io/api/v1/crates/image/0.25.10/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/linebender_resource_handle/0.1.1/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/miniz_oxide/0.8.9/download :: LICENSE-APACHE.md
https://crates.io/api/v1/crates/miniz_oxide/0.9.1/download :: LICENSE-APACHE.md
+https://crates.io/api/v1/crates/naga-types/30.0.1/download :: LICENSE.APACHE
https://crates.io/api/v1/crates/naga/29.0.4/download :: LICENSE.APACHE
+https://crates.io/api/v1/crates/naga/30.0.1/download :: LICENSE.APACHE
+https://crates.io/api/v1/crates/normpath/1.5.2/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/num-conv/0.2.2/download :: LICENSE-Apache
https://crates.io/api/v1/crates/pin-project-internal/1.1.13/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/pin-project-lite/0.2.17/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/pin-project/1.1.13/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/portable-atomic/1.15.0/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/raw-window-handle/0.6.2/download :: LICENSE-APACHE.md
+https://crates.io/api/v1/crates/resvg/0.45.1/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/resvg/0.46.0/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/simdutf8/0.1.5/download :: LICENSE-Apache
https://crates.io/api/v1/crates/sync_wrapper/1.0.2/download :: LICENSE
https://crates.io/api/v1/crates/time-core/0.1.9/download :: LICENSE-Apache
https://crates.io/api/v1/crates/time-macros/0.2.32/download :: LICENSE-Apache
https://crates.io/api/v1/crates/time/0.3.55/download :: LICENSE-Apache
+https://crates.io/api/v1/crates/usvg/0.45.1/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/usvg/0.46.0/download :: LICENSE-APACHE
https://crates.io/api/v1/crates/wgpu-core-deps-windows-linux-android/29.0.4/download :: LICENSE.APACHE
+https://crates.io/api/v1/crates/wgpu-core-deps-windows-linux-android/30.0.1/download :: LICENSE.APACHE
https://crates.io/api/v1/crates/wgpu-core/29.0.4/download :: LICENSE.APACHE
+https://crates.io/api/v1/crates/wgpu-core/30.0.1/download :: LICENSE.APACHE
https://crates.io/api/v1/crates/wgpu-hal/29.0.4/download :: LICENSE.APACHE
+https://crates.io/api/v1/crates/wgpu-hal/30.0.1/download :: LICENSE.APACHE
https://crates.io/api/v1/crates/wgpu-naga-bridge/29.0.4/download :: LICENSE.APACHE
+https://crates.io/api/v1/crates/wgpu-naga-bridge/30.0.1/download :: LICENSE.APACHE
https://crates.io/api/v1/crates/wgpu-types/29.0.4/download :: LICENSE.APACHE
+https://crates.io/api/v1/crates/wgpu-types/30.0.1/download :: LICENSE.APACHE
https://crates.io/api/v1/crates/wgpu/29.0.4/download :: LICENSE.APACHE
+https://crates.io/api/v1/crates/wgpu/30.0.1/download :: LICENSE.APACHE
https://crates.io/api/v1/crates/widestring/1.2.1/download :: LICENSE-APACHE
https://github.com/image-rs/image-webp#08b57d473c90148dd4b37a075df365afc3dfcc2b (adapted; third-party/image-webp) :: LICENSE-APACHE
@@ -17650,8 +17753,10 @@ DEALINGS IN THE SOFTWARE.
================================================================================
+gif@0.13.3 (MIT OR Apache-2.0); authors declared by package: The image-rs Developers
gif@0.14.2 (MIT OR Apache-2.0); authors declared by package: The image-rs Developers
+https://crates.io/api/v1/crates/gif/0.13.3/download :: LICENSE-MIT
https://crates.io/api/v1/crates/gif/0.14.2/download :: LICENSE-MIT
The MIT License (MIT)
@@ -17831,6 +17936,36 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================================================
+globwalk@0.8.1 (MIT); authors declared by package: Gilad Naaman <gi...@naaman.io>
+memoffset@0.6.5 (MIT); authors declared by package: Gilad Naaman <gi...@gmail.com>
+memoffset@0.9.1 (MIT); authors declared by package: Gilad Naaman <gi...@gmail.com>
+
+https://crates.io/api/v1/crates/globwalk/0.8.1/download :: LICENSE
+https://crates.io/api/v1/crates/memoffset/0.6.5/download :: LICENSE
+https://crates.io/api/v1/crates/memoffset/0.9.1/download :: LICENSE
+
+Copyright (c) 2017 Gilad Naaman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================================
+
glow@0.17.0 (MIT OR Apache-2.0 OR Zlib); authors declared by package: Joshua Groves <jo...@joshgroves.com>, Dzmitry Malyshau <kv...@gmail.com>
https://crates.io/api/v1/crates/glow/0.17.0/download :: LICENSE-ZLIB
@@ -18174,12 +18309,16 @@ DEALINGS IN THE SOFTWARE.
================================================================================
-gpui-base@0.7.3 (Apache-2.0)
-gpui-fps@0.7.3 (Apache-2.0)
-gpui-shell@0.7.3 (Apache-2.0)
+gpui-base@0.8.0 (Apache-2.0)
+gpui-component-macros@0.8.0 (Apache-2.0)
+gpui-component@0.8.0 (Apache-2.0)
+gpui-fps@0.8.0 (Apache-2.0)
+gpui-kit@0.8.0 (Apache-2.0)
+gpui-shell@0.8.0 (Apache-2.0)
-https://github.com/longbridge/gpui-kit#c5f8eca4a0759f8c0c7ed624eddfd03f1c93a5f2 (adapted; third-party/gpui-kit/crates/base) :: LICENSE-APACHE
-https://raw.githubusercontent.com/longbridge/gpui-kit/c5f8eca4a0759f8c0c7ed624eddfd03f1c93a5f2/LICENSE-APACHE
+git+https://github.com/longbridge/gpui-kit?rev=77ff6f794dca3bcee2e90977cedf12a64aed17e4#77ff6f794dca3bcee2e90977cedf12a64aed17e4 :: LICENSE-APACHE
+https://github.com/longbridge/gpui-kit#77ff6f794dca3bcee2e90977cedf12a64aed17e4 (adapted; third-party/gpui-kit/crates/base) :: LICENSE-APACHE
+https://raw.githubusercontent.com/longbridge/gpui-kit/77ff6f794dca3bcee2e90977cedf12a64aed17e4/LICENSE-APACHE
Copyright 2024 - 2026 Longbridge <https://longbridge.com>
@@ -18375,243 +18514,12 @@ END OF TERMS AND CONDITIONS
================================================================================
-gpui-fast-apple@0.1.7 (Apache-2.0); authors declared by package: Longbridge
-gpui-fast-linux@0.1.7 (Apache-2.0)
-gpui-fast-macos@0.1.7 (Apache-2.0)
-gpui-fast-macros@0.1.7 (Apache-2.0); authors declared by package: Longbridge
-gpui-fast-platform@0.1.7 (Apache-2.0); authors declared by package: Longbridge
-gpui-fast-wgpu@0.1.7 (Apache-2.0); authors declared by package: Longbridge
-gpui-fast-windows@0.1.7 (Apache-2.0)
-gpui-fast@0.1.7 (Apache-2.0); authors declared by package: Nathan Sobo <na...@zed.dev>
-gpui-pre-scheduler@0.3.9 (Apache-2.0)
-
-https://crates.io/api/v1/crates/gpui-fast-apple/0.1.7/download :: LICENSE
-https://crates.io/api/v1/crates/gpui-fast-macros/0.1.7/download :: LICENSE
-https://crates.io/api/v1/crates/gpui-fast-platform/0.1.7/download :: LICENSE
-https://crates.io/api/v1/crates/gpui-fast-wgpu/0.1.7/download :: LICENSE
-https://github.com/longbridge/gpui-fast#371769f1ff378bc0bd886440e0f97fc94aaeae5d (adapted; third-party/gpui-fast/crates/gpui) :: ../../LICENSE-APACHE
-https://github.com/longbridge/gpui-fast#371769f1ff378bc0bd886440e0f97fc94aaeae5d (adapted; third-party/gpui-fast/crates/gpui) :: LICENSE-APACHE
-https://github.com/longbridge/gpui-fast#371769f1ff378bc0bd886440e0f97fc94aaeae5d (adapted; third-party/gpui-fast/crates/gpui_linux) :: ../../LICENSE-APACHE
-https://github.com/longbridge/gpui-fast#371769f1ff378bc0bd886440e0f97fc94aaeae5d (adapted; third-party/gpui-fast/crates/gpui_linux) :: LICENSE-APACHE
-https://github.com/longbridge/gpui-fast#371769f1ff378bc0bd886440e0f97fc94aaeae5d (adapted; third-party/gpui-fast/crates/gpui_macos) :: ../../LICENSE-APACHE
-https://github.com/longbridge/gpui-fast#371769f1ff378bc0bd886440e0f97fc94aaeae5d (adapted; third-party/gpui-fast/crates/gpui_macos) :: LICENSE-APACHE
-https://github.com/longbridge/gpui-fast#371769f1ff378bc0bd886440e0f97fc94aaeae5d (adapted; third-party/gpui-fast/crates/gpui_windows) :: ../../LICENSE-APACHE
-https://github.com/longbridge/gpui-fast#371769f1ff378bc0bd886440e0f97fc94aaeae5d (adapted; third-party/gpui-fast/crates/gpui_windows) :: LICENSE-APACHE
-https://github.com/longbridge/gpui-fast#371769f1ff378bc0bd886440e0f97fc94aaeae5d (adapted; third-party/gpui-fast/crates/scheduler) :: ../../LICENSE-APACHE
-
-Copyright 2022 - 2025 Zed Industries, Inc.
-Copyright 2026 Longbridge.
-
-
-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
-
-================================================================================
-
-gpui-fps@0.7.3 (Apache-2.0)
-gpui-shell@0.7.3 (Apache-2.0)
+gpui-component-macros@0.8.0 (Apache-2.0)
+gpui-fps@0.8.0 (Apache-2.0)
+gpui-kit@0.8.0 (Apache-2.0)
+gpui-shell@0.8.0 (Apache-2.0)
-https://raw.githubusercontent.com/longbridge/gpui-kit/c5f8eca4a0759f8c0c7ed624eddfd03f1c93a5f2/LICENSE-DOCS.md
+https://raw.githubusercontent.com/longbridge/gpui-kit/77ff6f794dca3bcee2e90977cedf12a64aed17e4/LICENSE-DOCS.md
# Documentation license
@@ -18623,60 +18531,364 @@ This documentation license does **not** apply to software source, website implem
================================================================================
-gpui-pre-apple@0.3.9 (Apache-2.0)
-gpui-pre-collections@0.3.9 (Apache-2.0)
-gpui-pre-derive-refineable@0.3.9 (Apache-2.0)
-gpui-pre-http-client@0.3.9 (Apache-2.0)
-gpui-pre-linux@0.3.9 (Apache-2.0)
-gpui-pre-macos@0.3.9 (Apache-2.0)
-gpui-pre-macros@0.3.9 (Apache-2.0)
-gpui-pre-platform@0.3.9 (Apache-2.0)
-gpui-pre-refineable@0.3.9 (Apache-2.0)
-gpui-pre-shared-string@0.3.9 (Apache-2.0)
-gpui-pre-sum-tree@0.3.9 (Apache-2.0)
-gpui-pre-util@0.3.9 (Apache-2.0)
-gpui-pre-wgpu@0.3.9 (Apache-2.0)
-gpui-pre-windows@0.3.9 (Apache-2.0)
-gpui-pre-ztracing-macro@0.3.9 (Apache-2.0)
-gpui-pre-ztracing@0.3.9 (Apache-2.0)
-gpui-pre@0.3.9 (Apache-2.0); authors declared by package: Nathan Sobo <na...@zed.dev>
+gpui-fast-apple@0.1.10 (Apache-2.0); authors declared by package: Longbridge
+gpui-fast-linux@0.1.10 (Apache-2.0)
+gpui-fast-macos@0.1.10 (Apache-2.0)
+gpui-fast-macros@0.1.10 (Apache-2.0); authors declared by package: Longbridge
+gpui-fast-platform@0.1.10 (Apache-2.0); authors declared by package: Longbridge
+gpui-fast-wgpu@0.1.10 (Apache-2.0); authors declared by package: Longbridge
+gpui-fast-windows@0.1.10 (Apache-2.0)
+gpui-fast@0.1.10 (Apache-2.0); authors declared by package: Nathan Sobo <na...@zed.dev>
+gpui-pre-scheduler@0.3.9 (Apache-2.0)
-https://crates.io/api/v1/crates/gpui-pre-apple/0.3.9/download :: LICENSE-APACHE
-https://crates.io/api/v1/crates/gpui-pre-collections/0.3.9/download :: LICENSE-APACHE
-https://crates.io/api/v1/crates/gpui-pre-derive-refineable/0.3.9/download :: LICENSE-APACHE
-https://crates.io/api/v1/crates/gpui-pre-http-client/0.3.9/download :: LICENSE-APACHE
-https://crates.io/api/v1/crates/gpui-pre-linux/0.3.9/download :: LICENSE-APACHE
-https://crates.io/api/v1/crates/gpui-pre-macos/0.3.9/download :: LICENSE-APACHE
-https://crates.io/api/v1/crates/gpui-pre-macros/0.3.9/download :: LICENSE-APACHE
-https://crates.io/api/v1/crates/gpui-pre-platform/0.3.9/download :: LICENSE-APACHE
-https://crates.io/api/v1/crates/gpui-pre-refineable/0.3.9/download :: LICENSE-APACHE
-https://crates.io/api/v1/crates/gpui-pre-shared-string/0.3.9/download :: LICENSE-APACHE
-https://crates.io/api/v1/crates/gpui-pre-sum-tree/0.3.9/download :: LICENSE-APACHE
-https://crates.io/api/v1/crates/gpui-pre-util/0.3.9/download :: LICENSE-APACHE
-https://crates.io/api/v1/crates/gpui-pre-wgpu/0.3.9/download :: LICENSE-APACHE
-https://crates.io/api/v1/crates/gpui-pre-windows/0.3.9/download :: LICENSE-APACHE
-https://crates.io/api/v1/crates/gpui-pre-ztracing-macro/0.3.9/download :: LICENSE-APACHE
-https://crates.io/api/v1/crates/gpui-pre-ztracing/0.3.9/download :: LICENSE-APACHE
-https://crates.io/api/v1/crates/gpui-pre/0.3.9/download :: LICENSE-APACHE
+https://crates.io/api/v1/crates/gpui-fast-apple/0.1.10/download :: LICENSE
+https://crates.io/api/v1/crates/gpui-fast-macros/0.1.10/download :: LICENSE
+https://crates.io/api/v1/crates/gpui-fast-platform/0.1.10/download :: LICENSE
+https://crates.io/api/v1/crates/gpui-fast-wgpu/0.1.10/download :: LICENSE
+https://github.com/longbridge/gpui-fast#b836dd970b26830f2124b7cf91b0e19018fd12da (adapted; third-party/gpui-fast/crates/gpui) :: ../../LICENSE-APACHE
+https://github.com/longbridge/gpui-fast#b836dd970b26830f2124b7cf91b0e19018fd12da (adapted; third-party/gpui-fast/crates/gpui) :: LICENSE-APACHE
+https://github.com/longbridge/gpui-fast#b836dd970b26830f2124b7cf91b0e19018fd12da (adapted; third-party/gpui-fast/crates/gpui_linux) :: ../../LICENSE-APACHE
+https://github.com/longbridge/gpui-fast#b836dd970b26830f2124b7cf91b0e19018fd12da (adapted; third-party/gpui-fast/crates/gpui_linux) :: LICENSE-APACHE
+https://github.com/longbridge/gpui-fast#b836dd970b26830f2124b7cf91b0e19018fd12da (adapted; third-party/gpui-fast/crates/gpui_macos) :: ../../LICENSE-APACHE
+https://github.com/longbridge/gpui-fast#b836dd970b26830f2124b7cf91b0e19018fd12da (adapted; third-party/gpui-fast/crates/gpui_macos) :: LICENSE-APACHE
+https://github.com/longbridge/gpui-fast#b836dd970b26830f2124b7cf91b0e19018fd12da (adapted; third-party/gpui-fast/crates/gpui_windows) :: ../../LICENSE-APACHE
+https://github.com/longbridge/gpui-fast#b836dd970b26830f2124b7cf91b0e19018fd12da (adapted; third-party/gpui-fast/crates/gpui_windows) :: LICENSE-APACHE
+https://github.com/longbridge/gpui-fast#b836dd970b26830f2124b7cf91b0e19018fd12da (adapted; third-party/gpui-fast/crates/scheduler) :: ../../LICENSE-APACHE
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.
-
-
+Copyright 2026 Longbridge.
+
+
+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
+
+================================================================================
+
+gpui-kit-assets@0.8.0 (Apache-2.0)
+
+git+https://github.com/longbridge/gpui-kit?rev=77ff6f794dca3bcee2e90977cedf12a64aed17e4#77ff6f794dca3bcee2e90977cedf12a64aed17e4 :: LICENSE-LUCIDE
+
+ISC License
+
+Copyright (c) 2026 Lucide Icons and Contributors
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+---
+
+The following Lucide icons are derived from the Feather project:
+
+airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right [...]
+
+The MIT License (MIT) (for the icons listed above)
+
+Copyright (c) 2013-present Cole Bemis
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================================
+
+gpui-kit-assets@0.8.0 (Apache-2.0)
+
+git+https://github.com/longbridge/gpui-kit?rev=77ff6f794dca3bcee2e90977cedf12a64aed17e4#77ff6f794dca3bcee2e90977cedf12a64aed17e4 :: assets/icons/copyright.svg
+
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ stroke-width="2"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+>
+ <circle cx="12" cy="12" r="10" />
+ <path d="M14.83 14.83a4 4 0 1 1 0-5.66" />
+</svg>
+
+================================================================================
+
+gpui-pre-apple@0.3.9 (Apache-2.0)
+gpui-pre-collections@0.3.9 (Apache-2.0)
+gpui-pre-derive-refineable@0.3.9 (Apache-2.0)
+gpui-pre-http-client@0.3.9 (Apache-2.0)
+gpui-pre-linux@0.3.9 (Apache-2.0)
+gpui-pre-macos@0.3.9 (Apache-2.0)
+gpui-pre-macros@0.3.9 (Apache-2.0)
+gpui-pre-platform@0.3.9 (Apache-2.0)
+gpui-pre-refineable@0.3.9 (Apache-2.0)
+gpui-pre-shared-string@0.3.9 (Apache-2.0)
+gpui-pre-sum-tree@0.3.9 (Apache-2.0)
+gpui-pre-util@0.3.9 (Apache-2.0)
+gpui-pre-wgpu@0.3.9 (Apache-2.0)
+gpui-pre-windows@0.3.9 (Apache-2.0)
+gpui-pre-ztracing-macro@0.3.9 (Apache-2.0)
+gpui-pre-ztracing@0.3.9 (Apache-2.0)
+gpui-pre@0.3.9 (Apache-2.0); authors declared by package: Nathan Sobo <na...@zed.dev>
+
+https://crates.io/api/v1/crates/gpui-pre-apple/0.3.9/download :: LICENSE-APACHE
+https://crates.io/api/v1/crates/gpui-pre-collections/0.3.9/download :: LICENSE-APACHE
+https://crates.io/api/v1/crates/gpui-pre-derive-refineable/0.3.9/download :: LICENSE-APACHE
+https://crates.io/api/v1/crates/gpui-pre-http-client/0.3.9/download :: LICENSE-APACHE
+https://crates.io/api/v1/crates/gpui-pre-linux/0.3.9/download :: LICENSE-APACHE
+https://crates.io/api/v1/crates/gpui-pre-macos/0.3.9/download :: LICENSE-APACHE
+https://crates.io/api/v1/crates/gpui-pre-macros/0.3.9/download :: LICENSE-APACHE
+https://crates.io/api/v1/crates/gpui-pre-platform/0.3.9/download :: LICENSE-APACHE
+https://crates.io/api/v1/crates/gpui-pre-refineable/0.3.9/download :: LICENSE-APACHE
+https://crates.io/api/v1/crates/gpui-pre-shared-string/0.3.9/download :: LICENSE-APACHE
+https://crates.io/api/v1/crates/gpui-pre-sum-tree/0.3.9/download :: LICENSE-APACHE
+https://crates.io/api/v1/crates/gpui-pre-util/0.3.9/download :: LICENSE-APACHE
+https://crates.io/api/v1/crates/gpui-pre-wgpu/0.3.9/download :: LICENSE-APACHE
+https://crates.io/api/v1/crates/gpui-pre-windows/0.3.9/download :: LICENSE-APACHE
+https://crates.io/api/v1/crates/gpui-pre-ztracing-macro/0.3.9/download :: LICENSE-APACHE
+https://crates.io/api/v1/crates/gpui-pre-ztracing/0.3.9/download :: LICENSE-APACHE
+https://crates.io/api/v1/crates/gpui-pre/0.3.9/download :: LICENSE-APACHE
+
+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
@@ -19120,6 +19332,28 @@ limitations under the License.
================================================================================
+granit-parser@1.3.0 (MIT OR Apache-2.0); authors declared by package: Ethiraric <et...@gmail.com>, David Aguilar <da...@gmail.com>, Yuheng Chen <yu...@sensetime.com>, Bourumir Wyngs <bo...@gmail.com>
+
+https://crates.io/api/v1/crates/granit-parser/1.3.0/download :: LICENSE
+
+Inherited license notices:
+
+Code up to and including commit `da52a68615f2ecdd6b7e4567019f280c433c1521` is licensed by Chen Yuheng under either of:
+ - [Apache License, Version 2.0](.licenses/ChenYuheng-Apache) (http://www.apache.org/licenses/LICENSE-2.0)
+ - [MIT License](.licenses/ChenYuheng-MIT) (http://opensource.org/licenses/MIT)
+
+Code modifications starting with commit `1d71a23b151dcc12b289d0f06d8207dd9c764216` (included) are licensed by Ethiraric under either of:
+ - [Apache License, Version 2.0](.licenses/Ethiraric-Apache) (http://www.apache.org/licenses/LICENSE-2.0)
+ - [MIT License](.licenses/Ethiraric-MIT) (http://opensource.org/licenses/MIT)
+
+granit-parser modification notice:
+
+Modifications after commit `825edf857426d034954523fed19599635cbb5043` are Copyright (c) 2026 Bourumir Wyngs and licensed under MIT OR Apache-2.0.
+
+Redistributions of this Work must include all inherited and new license notices, including the notices for Chen Yuheng, Ethiraric, and Bourumir Wyngs.
+
+================================================================================
+
group@0.13.0 (MIT/Apache-2.0); authors declared by package: Sean Bowe <ew...@gmail.com>, Jack Grigg <ja...@z.cash>
group@0.14.0 (MIT/Apache-2.0); authors declared by package: Sean Bowe <ew...@gmail.com>, Jack Grigg <ja...@z.cash>
@@ -21013,9 +21247,11 @@ DEALINGS IN THE SOFTWARE.
================================================================================
+imagesize@0.13.0 (MIT); authors declared by package: Maid Dog <ma...@gmail.com>
imagesize@0.14.0 (MIT); authors declared by package: Maid Dog <ma...@gmail.com>
imagesize@0.15.0 (MIT); authors declared by package: Maid Dog <ma...@gmail.com>
+https://crates.io/api/v1/crates/imagesize/0.13.0/download :: LICENSE
https://crates.io/api/v1/crates/imagesize/0.14.0/download :: LICENSE
https://crates.io/api/v1/crates/imagesize/0.15.0/download :: LICENSE
@@ -21977,9 +22213,11 @@ DEALINGS IN THE SOFTWARE.
================================================================================
+kurbo@0.11.3 (Apache-2.0 OR MIT); authors declared by package: Raph Levien <ra...@gmail.com>
kurbo@0.13.1 (Apache-2.0 OR MIT)
polycool@0.4.0 (MIT OR Apache-2.0)
+https://crates.io/api/v1/crates/kurbo/0.11.3/download :: LICENSE-MIT
https://crates.io/api/v1/crates/kurbo/0.13.1/download :: LICENSE-MIT
https://crates.io/api/v1/crates/polycool/0.4.0/download :: LICENSE-MIT
@@ -23718,34 +23956,6 @@ DEALINGS IN THE SOFTWARE.
================================================================================
-memoffset@0.6.5 (MIT); authors declared by package: Gilad Naaman <gi...@gmail.com>
-memoffset@0.9.1 (MIT); authors declared by package: Gilad Naaman <gi...@gmail.com>
-
-https://crates.io/api/v1/crates/memoffset/0.6.5/download :: LICENSE
-https://crates.io/api/v1/crates/memoffset/0.9.1/download :: LICENSE
-
-Copyright (c) 2017 Gilad Naaman
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
-================================================================================
-
meval@0.2.0 (Unlicense/MIT); authors declared by package: Norbert Pozar <be...@gmail.com>
https://crates.io/api/v1/crates/meval/0.2.0/download :: LICENSE-MIT
@@ -24550,10 +24760,12 @@ windows-strings@0.1.0 (MIT OR Apache-2.0); authors declared by package: Microsof
windows-strings@0.3.1 (MIT OR Apache-2.0); authors declared by package: Microsoft
windows-strings@0.4.2 (MIT OR Apache-2.0); authors declared by package: Microsoft
windows-strings@0.5.1 (MIT OR Apache-2.0)
+windows-sys@0.48.0 (MIT OR Apache-2.0); authors declared by package: Microsoft
windows-sys@0.52.0 (MIT OR Apache-2.0); authors declared by package: Microsoft
windows-sys@0.59.0 (MIT OR Apache-2.0); authors declared by package: Microsoft
windows-sys@0.60.2 (MIT OR Apache-2.0); authors declared by package: Microsoft
windows-sys@0.61.2 (MIT OR Apache-2.0)
+windows-targets@0.48.5 (MIT OR Apache-2.0); authors declared by package: Microsoft
windows-targets@0.52.6 (MIT OR Apache-2.0); authors declared by package: Microsoft
windows-targets@0.53.5 (MIT OR Apache-2.0)
windows-threading@0.1.0 (MIT OR Apache-2.0); authors declared by package: Microsoft
@@ -24563,6 +24775,7 @@ windows@0.58.0 (MIT OR Apache-2.0); authors declared by package: Microsoft
windows@0.59.0 (MIT OR Apache-2.0); authors declared by package: Microsoft
windows@0.61.3 (MIT OR Apache-2.0); authors declared by package: Microsoft
windows@0.62.2 (MIT OR Apache-2.0)
+windows_x86_64_msvc@0.48.5 (MIT OR Apache-2.0); authors declared by package: Microsoft
windows_x86_64_msvc@0.52.6 (MIT OR Apache-2.0); authors declared by package: Microsoft
windows_x86_64_msvc@0.53.1 (MIT OR Apache-2.0)
@@ -24598,10 +24811,12 @@ https://crates.io/api/v1/crates/windows-strings/0.1.0/download :: license-mit
https://crates.io/api/v1/crates/windows-strings/0.3.1/download :: license-mit
https://crates.io/api/v1/crates/windows-strings/0.4.2/download :: license-mit
https://crates.io/api/v1/crates/windows-strings/0.5.1/download :: license-mit
+https://crates.io/api/v1/crates/windows-sys/0.48.0/download :: license-mit
https://crates.io/api/v1/crates/windows-sys/0.52.0/download :: license-mit
https://crates.io/api/v1/crates/windows-sys/0.59.0/download :: license-mit
https://crates.io/api/v1/crates/windows-sys/0.60.2/download :: license-mit
https://crates.io/api/v1/crates/windows-sys/0.61.2/download :: license-mit
+https://crates.io/api/v1/crates/windows-targets/0.48.5/download :: license-mit
https://crates.io/api/v1/crates/windows-targets/0.52.6/download :: license-mit
https://crates.io/api/v1/crates/windows-targets/0.53.5/download :: license-mit
https://crates.io/api/v1/crates/windows-threading/0.1.0/download :: license-mit
@@ -24611,6 +24826,7 @@ https://crates.io/api/v1/crates/windows/0.58.0/download :: license-mit
https://crates.io/api/v1/crates/windows/0.59.0/download :: license-mit
https://crates.io/api/v1/crates/windows/0.61.3/download :: license-mit
https://crates.io/api/v1/crates/windows/0.62.2/download :: license-mit
+https://crates.io/api/v1/crates/windows_x86_64_msvc/0.48.5/download :: license-mit
https://crates.io/api/v1/crates/windows_x86_64_msvc/0.52.6/download :: license-mit
https://crates.io/api/v1/crates/windows_x86_64_msvc/0.53.1/download :: license-mit
@@ -24638,21 +24854,37 @@ MIT License
================================================================================
+naga-types@30.0.1 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
naga@29.0.4 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
+naga@30.0.1 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
wgpu-core-deps-windows-linux-android@29.0.4 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
+wgpu-core-deps-windows-linux-android@30.0.1 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
wgpu-core@29.0.4 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
+wgpu-core@30.0.1 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
wgpu-hal@29.0.4 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
+wgpu-hal@30.0.1 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
wgpu-naga-bridge@29.0.4 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
+wgpu-naga-bridge@30.0.1 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
wgpu-types@29.0.4 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
+wgpu-types@30.0.1 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
wgpu@29.0.4 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
+wgpu@30.0.1 (MIT OR Apache-2.0); authors declared by package: gfx-rs developers
+https://crates.io/api/v1/crates/naga-types/30.0.1/download :: LICENSE.MIT
https://crates.io/api/v1/crates/naga/29.0.4/download :: LICENSE.MIT
+https://crates.io/api/v1/crates/naga/30.0.1/download :: LICENSE.MIT
https://crates.io/api/v1/crates/wgpu-core-deps-windows-linux-android/29.0.4/download :: LICENSE.MIT
+https://crates.io/api/v1/crates/wgpu-core-deps-windows-linux-android/30.0.1/download :: LICENSE.MIT
https://crates.io/api/v1/crates/wgpu-core/29.0.4/download :: LICENSE.MIT
+https://crates.io/api/v1/crates/wgpu-core/30.0.1/download :: LICENSE.MIT
https://crates.io/api/v1/crates/wgpu-hal/29.0.4/download :: LICENSE.MIT
+https://crates.io/api/v1/crates/wgpu-hal/30.0.1/download :: LICENSE.MIT
https://crates.io/api/v1/crates/wgpu-naga-bridge/29.0.4/download :: LICENSE.MIT
+https://crates.io/api/v1/crates/wgpu-naga-bridge/30.0.1/download :: LICENSE.MIT
https://crates.io/api/v1/crates/wgpu-types/29.0.4/download :: LICENSE.MIT
+https://crates.io/api/v1/crates/wgpu-types/30.0.1/download :: LICENSE.MIT
https://crates.io/api/v1/crates/wgpu/29.0.4/download :: LICENSE.MIT
+https://crates.io/api/v1/crates/wgpu/30.0.1/download :: LICENSE.MIT
MIT License
@@ -24796,6 +25028,31 @@ SOFTWARE.
================================================================================
+nohash-hasher@0.2.0 (Apache-2.0 OR MIT); authors declared by package: Parity Technologies <ad...@parity.io>
+
+https://crates.io/api/v1/crates/nohash-hasher/0.2.0/download :: LICENSE-MIT
+
+Copyright 2018 Parity Technologies (UK) Ltd.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+================================================================================
+
nom@1.2.4 (MIT); authors declared by package: contact@geoffroycouprie.com
https://crates.io/api/v1/crates/nom/1.2.4/download :: LICENSE
@@ -24906,13 +25163,27 @@ SOFTWARE.
================================================================================
-notify-rust@4.18.1 (MIT OR Apache-2.0); authors declared by package: Hendrik Sollich <he...@hoodie.de>
+normpath@1.5.2 (MIT OR Apache-2.0); authors declared by package: dylni
-https://crates.io/api/v1/crates/notify-rust/4.18.1/download :: LICENSE-MIT
+https://crates.io/api/v1/crates/normpath/1.5.2/download :: COPYRIGHT
+
+Copyright (c) 2020 dylni (https://github.com/dylni)
+
+Some files also include explicit copyright notices.
+
+Licensed under the Apache License, Version 2.0 <LICENSE-APACHE> or the MIT
+license <LICENSE-MIT>, at your option. All files in this project may not be
+copied, modified, or distributed except according to those terms.
+
+================================================================================
+
+normpath@1.5.2 (MIT OR Apache-2.0); authors declared by package: dylni
+
+https://crates.io/api/v1/crates/normpath/1.5.2/download :: LICENSE-MIT
MIT License
-Copyright (c) 2017 Hendrik Sollich
+Copyright (c) 2020 dylni (https://github.com/dylni)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -24934,219 +25205,698 @@ SOFTWARE.
================================================================================
-notify-types@2.1.0 (MIT OR Apache-2.0); authors declared by package: Daniel Faust <he...@gmail.com>
-
-https://crates.io/api/v1/crates/notify-types/2.1.0/download :: LICENSE-APACHE
-
-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.
+normpath@1.5.2 (MIT OR Apache-2.0); authors declared by package: dylni
-END OF TERMS AND CONDITIONS
-
-APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
-Copyright 2023 Notify Contributors
-
-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.
+https://crates.io/api/v1/crates/normpath/1.5.2/download :: LICENSE-THIRD-PARTY
-================================================================================
-
-notify-types@2.1.0 (MIT OR Apache-2.0); authors declared by package: Daniel Faust <he...@gmail.com>
-
-https://crates.io/api/v1/crates/notify-types/2.1.0/download :: LICENSE-MIT
+===============================================================================
-Copyright (c) 2023 Notify Contributors
+The Rust Programming Language
+https://github.com/rust-lang/rust/blob/b1277d04db0dc8009037e872a1be7cdc2bd74a43/LICENSE-MIT
+
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+
+===============================================================================
+
+The Rust Programming Language
+https://github.com/rust-lang/rust/blob/b1277d04db0dc8009037e872a1be7cdc2bd74a43/LICENSE-APACHE
+
+ 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
+
+===============================================================================
+
+Fruity
+https://github.com/nvzqz/fruity/blob/320efcf715c2c5fbd2f3084f671f2be2e03a6f2b/LICENSE-MIT
+
+MIT License
+
+Copyright (c) 2020 Nikolai Vazquez
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+===============================================================================
+
+Fruity
+https://github.com/nvzqz/fruity/blob/320efcf715c2c5fbd2f3084f671f2be2e03a6f2b/LICENSE-APACHE
+
+ 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
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
+
+================================================================================
+
+notify-rust@4.18.1 (MIT OR Apache-2.0); authors declared by package: Hendrik Sollich <he...@hoodie.de>
+
+https://crates.io/api/v1/crates/notify-rust/4.18.1/download :: LICENSE-MIT
+
+MIT License
+
+Copyright (c) 2017 Hendrik Sollich
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================================
+
+notify-types@2.1.0 (MIT OR Apache-2.0); authors declared by package: Daniel Faust <he...@gmail.com>
+
+https://crates.io/api/v1/crates/notify-types/2.1.0/download :: LICENSE-APACHE
+
+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
+
+APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+Copyright 2023 Notify Contributors
+
+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.
+
+================================================================================
+
+notify-types@2.1.0 (MIT OR Apache-2.0); authors declared by package: Daniel Faust <he...@gmail.com>
+
+https://crates.io/api/v1/crates/notify-types/2.1.0/download :: LICENSE-MIT
+
+Copyright (c) 2023 Notify Contributors
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
@@ -29614,10 +30364,14 @@ SOFTWARE.
================================================================================
+resvg@0.45.1 (Apache-2.0 OR MIT)
resvg@0.46.0 (Apache-2.0 OR MIT)
+usvg@0.45.1 (Apache-2.0 OR MIT)
usvg@0.46.0 (Apache-2.0 OR MIT)
+https://crates.io/api/v1/crates/resvg/0.45.1/download :: LICENSE-MIT
https://crates.io/api/v1/crates/resvg/0.46.0/download :: LICENSE-MIT
+https://crates.io/api/v1/crates/usvg/0.45.1/download :: LICENSE-MIT
https://crates.io/api/v1/crates/usvg/0.46.0/download :: LICENSE-MIT
Copyright 2017 the Resvg Authors
@@ -30310,6 +31064,69 @@ SOFTWARE.
================================================================================
+rust-embed-impl@8.13.0 (MIT); authors declared by package: pyrossh
+rust-embed-utils@8.13.0 (MIT); authors declared by package: pyrossh
+rust-embed@8.13.0 (MIT); authors declared by package: pyrossh
+
+https://crates.io/api/v1/crates/rust-embed-impl/8.13.0/download :: LICENSE
+https://crates.io/api/v1/crates/rust-embed-utils/8.13.0/download :: LICENSE
+https://crates.io/api/v1/crates/rust-embed/8.13.0/download :: LICENSE
+
+The MIT License (MIT)
+
+Copyright (c) 2018 pyros2097
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================================
+
+rust-i18n-macro@4.2.4 (MIT)
+rust-i18n-support@4.2.4 (MIT)
+rust-i18n@4.2.4 (MIT); authors declared by package: Jason Lee <hu...@gmail.com>
+
+https://crates.io/api/v1/crates/rust-i18n/4.2.4/download :: LICENSE
+https://raw.githubusercontent.com/longbridge/rust-i18n/5f3ea5ba03395f2f2908e3587e01788cfdfd3555/LICENSE
+
+MIT License
+
+Copyright (c) 2021 Longbridge
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================================
+
rustix-linux-procfs@0.1.1 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT); authors declared by package: Dan Gohman <de...@sunfishcode.online>
https://crates.io/api/v1/crates/rustix-linux-procfs/0.1.1/download :: COPYRIGHT
@@ -32345,6 +33162,34 @@ DEALINGS IN THE SOFTWARE.
================================================================================
+shellexpand@3.1.2 (MIT/Apache-2.0); authors declared by package: Vladimir Matveev <vm...@citrine.cc>, Ian Jackson <iw...@torproject.org>
+
+https://crates.io/api/v1/crates/shellexpand/3.1.2/download :: LICENSE-MIT
+
+The MIT License (MIT)
+
+Copyright (c) 2016 Vladimir Matveev
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================================
+
shlex@1.3.0 (MIT OR Apache-2.0); authors declared by package: comex <co...@gmail.com>, Fenhl <fe...@fenhl.net>, Adrian Taylor <ad...@chromium.org>, Alex Touchet <al...@outlook.com>, Daniel Parks <dp...@oxidized.org>, Garrett Berg <go...@gmail.com>
shlex@2.0.1 (MIT OR Apache-2.0); authors declared by package: comex <co...@gmail.com>, Fenhl <fe...@fenhl.net>, Adrian Taylor <ad...@chromium.org>, Alex Touchet <al...@outlook.com>, Daniel Parks <dp...@oxidized.org>, Garrett Berg <go...@gmail.com>
@@ -33269,9 +34114,11 @@ SOFTWARE.
================================================================================
+svgtypes@0.15.3 (Apache-2.0 OR MIT)
svgtypes@0.16.1 (Apache-2.0 OR MIT)
ttf-parser@0.25.1 (MIT OR Apache-2.0); authors declared by package: Caleb Maclennan <ca...@alerque.com>, Laurenz Stampfl <la...@gmail.com>, Yevhenii Reizner <ra...@gmail.com>, خالد حسني (Khaled Hosny) <kh...@aliftype.com>
+https://crates.io/api/v1/crates/svgtypes/0.15.3/download :: LICENSE-MIT
https://crates.io/api/v1/crates/svgtypes/0.16.1/download :: LICENSE-MIT
https://crates.io/api/v1/crates/ttf-parser/0.25.1/download :: LICENSE-MIT
@@ -34569,19 +35416,444 @@ APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+Copyright 2017 quininer kel
+
+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.
+
+================================================================================
+
+tokio-rustls@0.26.5 (MIT OR Apache-2.0)
+
+https://crates.io/api/v1/crates/tokio-rustls/0.26.5/download :: LICENSE-MIT
+
+Copyright (c) 2017 quininer kel
+
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+
+================================================================================
+
+tokio-socks@0.5.3 (MIT); authors declared by package: Yilin Chen <st...@gmail.com>
+
+https://crates.io/api/v1/crates/tokio-socks/0.5.3/download :: LICENSE
+
+MIT License
+
+Copyright (c) 2018 Yilin Chen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================================
+
+tokio-stream@0.1.19 (MIT); authors declared by package: Tokio Contributors <te...@tokio.rs>
+tokio-util@0.7.19 (MIT); authors declared by package: Tokio Contributors <te...@tokio.rs>
+tokio@1.53.1 (MIT); authors declared by package: Tokio Contributors <te...@tokio.rs>
+
+https://crates.io/api/v1/crates/tokio-stream/0.1.19/download :: LICENSE
+https://crates.io/api/v1/crates/tokio-util/0.7.19/download :: LICENSE
+https://crates.io/api/v1/crates/tokio/1.53.1/download :: LICENSE
+
+MIT License
+
+Copyright (c) Tokio Contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================================
+
+tokio-tungstenite@0.24.0 (MIT); authors declared by package: Daniel Abramov <da...@snapview.de>, Alexey Galakhov <ag...@snapview.de>
+tokio-tungstenite@0.29.0 (MIT); authors declared by package: Daniel Abramov <da...@snapview.de>, Alexey Galakhov <ag...@snapview.de>
+
+https://crates.io/api/v1/crates/tokio-tungstenite/0.24.0/download :: LICENSE
+https://crates.io/api/v1/crates/tokio-tungstenite/0.29.0/download :: LICENSE
+
+Copyright (c) 2017 Daniel Abramov
+Copyright (c) 2017 Alexey Galakhov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+================================================================================
+
+tower-http@0.6.11 (MIT); authors declared by package: Tower Maintainers <te...@tower-rs.com>
+
+https://crates.io/api/v1/crates/tower-http/0.6.11/download :: LICENSE
+
+Copyright (c) 2019-2021 Tower Contributors
+
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+
+================================================================================
+
+tower-layer@0.3.3 (MIT); authors declared by package: Tower Maintainers <te...@tower-rs.com>
+tower-service@0.3.3 (MIT); authors declared by package: Tower Maintainers <te...@tower-rs.com>
+tower@0.5.3 (MIT); authors declared by package: Tower Maintainers <te...@tower-rs.com>
+
+https://crates.io/api/v1/crates/tower-layer/0.3.3/download :: LICENSE
+https://crates.io/api/v1/crates/tower-service/0.3.3/download :: LICENSE
+https://crates.io/api/v1/crates/tower/0.5.3/download :: LICENSE
+
+Copyright (c) 2019 Tower Contributors
+
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+
+================================================================================
+
+tree_magic_mini@3.2.2 (MIT); authors declared by package: Matt Brubeck <mb...@limpet.net>, Allison Hancock <aa...@umich.edu>
+
+https://crates.io/api/v1/crates/tree_magic_mini/3.2.2/download :: LICENSE
+
+MIT License
+
+Copyright (c) 2017 Aaron Hancock
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================================
+
+triomphe@0.1.17 (MIT OR Apache-2.0); authors declared by package: Manish Goregaokar <ma...@gmail.com>, The Servo Project Developers
+
+https://crates.io/api/v1/crates/triomphe/0.1.17/download :: LICENSE-APACHE
+
+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
+
+APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
-Copyright 2017 quininer kel
+Copyright [yyyy] [name of copyright owner]
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
+ 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,
@@ -34591,223 +35863,6 @@ limitations under the License.
================================================================================
-tokio-rustls@0.26.5 (MIT OR Apache-2.0)
-
-https://crates.io/api/v1/crates/tokio-rustls/0.26.5/download :: LICENSE-MIT
-
-Copyright (c) 2017 quininer kel
-
-Permission is hereby granted, free of charge, to any
-person obtaining a copy of this software and associated
-documentation files (the "Software"), to deal in the
-Software without restriction, including without
-limitation the rights to use, copy, modify, merge,
-publish, distribute, sublicense, and/or sell copies of
-the Software, and to permit persons to whom the Software
-is furnished to do so, subject to the following
-conditions:
-
-The above copyright notice and this permission notice
-shall be included in all copies or substantial portions
-of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
-ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
-TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
-PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
-SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
-CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
-IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-DEALINGS IN THE SOFTWARE.
-
-================================================================================
-
-tokio-socks@0.5.3 (MIT); authors declared by package: Yilin Chen <st...@gmail.com>
-
-https://crates.io/api/v1/crates/tokio-socks/0.5.3/download :: LICENSE
-
-MIT License
-
-Copyright (c) 2018 Yilin Chen
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
-================================================================================
-
-tokio-stream@0.1.19 (MIT); authors declared by package: Tokio Contributors <te...@tokio.rs>
-tokio-util@0.7.19 (MIT); authors declared by package: Tokio Contributors <te...@tokio.rs>
-tokio@1.53.1 (MIT); authors declared by package: Tokio Contributors <te...@tokio.rs>
-
-https://crates.io/api/v1/crates/tokio-stream/0.1.19/download :: LICENSE
-https://crates.io/api/v1/crates/tokio-util/0.7.19/download :: LICENSE
-https://crates.io/api/v1/crates/tokio/1.53.1/download :: LICENSE
-
-MIT License
-
-Copyright (c) Tokio Contributors
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
-================================================================================
-
-tokio-tungstenite@0.24.0 (MIT); authors declared by package: Daniel Abramov <da...@snapview.de>, Alexey Galakhov <ag...@snapview.de>
-tokio-tungstenite@0.29.0 (MIT); authors declared by package: Daniel Abramov <da...@snapview.de>, Alexey Galakhov <ag...@snapview.de>
-
-https://crates.io/api/v1/crates/tokio-tungstenite/0.24.0/download :: LICENSE
-https://crates.io/api/v1/crates/tokio-tungstenite/0.29.0/download :: LICENSE
-
-Copyright (c) 2017 Daniel Abramov
-Copyright (c) 2017 Alexey Galakhov
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-
-================================================================================
-
-tower-http@0.6.11 (MIT); authors declared by package: Tower Maintainers <te...@tower-rs.com>
-
-https://crates.io/api/v1/crates/tower-http/0.6.11/download :: LICENSE
-
-Copyright (c) 2019-2021 Tower Contributors
-
-Permission is hereby granted, free of charge, to any
-person obtaining a copy of this software and associated
-documentation files (the "Software"), to deal in the
-Software without restriction, including without
-limitation the rights to use, copy, modify, merge,
-publish, distribute, sublicense, and/or sell copies of
-the Software, and to permit persons to whom the Software
-is furnished to do so, subject to the following
-conditions:
-
-The above copyright notice and this permission notice
-shall be included in all copies or substantial portions
-of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
-ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
-TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
-PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
-SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
-CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
-IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-DEALINGS IN THE SOFTWARE.
-
-================================================================================
-
-tower-layer@0.3.3 (MIT); authors declared by package: Tower Maintainers <te...@tower-rs.com>
-tower-service@0.3.3 (MIT); authors declared by package: Tower Maintainers <te...@tower-rs.com>
-tower@0.5.3 (MIT); authors declared by package: Tower Maintainers <te...@tower-rs.com>
-
-https://crates.io/api/v1/crates/tower-layer/0.3.3/download :: LICENSE
-https://crates.io/api/v1/crates/tower-service/0.3.3/download :: LICENSE
-https://crates.io/api/v1/crates/tower/0.5.3/download :: LICENSE
-
-Copyright (c) 2019 Tower Contributors
-
-Permission is hereby granted, free of charge, to any
-person obtaining a copy of this software and associated
-documentation files (the "Software"), to deal in the
-Software without restriction, including without
-limitation the rights to use, copy, modify, merge,
-publish, distribute, sublicense, and/or sell copies of
-the Software, and to permit persons to whom the Software
-is furnished to do so, subject to the following
-conditions:
-
-The above copyright notice and this permission notice
-shall be included in all copies or substantial portions
-of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
-ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
-TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
-PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
-SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
-CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
-IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-DEALINGS IN THE SOFTWARE.
-
-================================================================================
-
-tree_magic_mini@3.2.2 (MIT); authors declared by package: Matt Brubeck <mb...@limpet.net>, Allison Hancock <aa...@umich.edu>
-
-https://crates.io/api/v1/crates/tree_magic_mini/3.2.2/download :: LICENSE
-
-MIT License
-
-Copyright (c) 2017 Aaron Hancock
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
-================================================================================
-
try-lock@0.2.5 (MIT); authors declared by package: Sean McArthur <se...@seanmonstar.com>
https://crates.io/api/v1/crates/try-lock/0.2.5/download :: LICENSE
@@ -38663,10 +39718,12 @@ windows-strings@0.1.0 (MIT OR Apache-2.0); authors declared by package: Microsof
windows-strings@0.3.1 (MIT OR Apache-2.0); authors declared by package: Microsoft
windows-strings@0.4.2 (MIT OR Apache-2.0); authors declared by package: Microsoft
windows-strings@0.5.1 (MIT OR Apache-2.0)
+windows-sys@0.48.0 (MIT OR Apache-2.0); authors declared by package: Microsoft
windows-sys@0.52.0 (MIT OR Apache-2.0); authors declared by package: Microsoft
windows-sys@0.59.0 (MIT OR Apache-2.0); authors declared by package: Microsoft
windows-sys@0.60.2 (MIT OR Apache-2.0); authors declared by package: Microsoft
windows-sys@0.61.2 (MIT OR Apache-2.0)
+windows-targets@0.48.5 (MIT OR Apache-2.0); authors declared by package: Microsoft
windows-targets@0.52.6 (MIT OR Apache-2.0); authors declared by package: Microsoft
windows-targets@0.53.5 (MIT OR Apache-2.0)
windows-threading@0.1.0 (MIT OR Apache-2.0); authors declared by package: Microsoft
@@ -38676,6 +39733,7 @@ windows@0.58.0 (MIT OR Apache-2.0); authors declared by package: Microsoft
windows@0.59.0 (MIT OR Apache-2.0); authors declared by package: Microsoft
windows@0.61.3 (MIT OR Apache-2.0); authors declared by package: Microsoft
windows@0.62.2 (MIT OR Apache-2.0)
+windows_x86_64_msvc@0.48.5 (MIT OR Apache-2.0); authors declared by package: Microsoft
windows_x86_64_msvc@0.52.6 (MIT OR Apache-2.0); authors declared by package: Microsoft
windows_x86_64_msvc@0.53.1 (MIT OR Apache-2.0)
@@ -38709,10 +39767,12 @@ https://crates.io/api/v1/crates/windows-strings/0.1.0/download :: license-apache
https://crates.io/api/v1/crates/windows-strings/0.3.1/download :: license-apache-2.0
https://crates.io/api/v1/crates/windows-strings/0.4.2/download :: license-apache-2.0
https://crates.io/api/v1/crates/windows-strings/0.5.1/download :: license-apache-2.0
+https://crates.io/api/v1/crates/windows-sys/0.48.0/download :: license-apache-2.0
https://crates.io/api/v1/crates/windows-sys/0.52.0/download :: license-apache-2.0
https://crates.io/api/v1/crates/windows-sys/0.59.0/download :: license-apache-2.0
https://crates.io/api/v1/crates/windows-sys/0.60.2/download :: license-apache-2.0
https://crates.io/api/v1/crates/windows-sys/0.61.2/download :: license-apache-2.0
+https://crates.io/api/v1/crates/windows-targets/0.48.5/download :: license-apache-2.0
https://crates.io/api/v1/crates/windows-targets/0.52.6/download :: license-apache-2.0
https://crates.io/api/v1/crates/windows-targets/0.53.5/download :: license-apache-2.0
https://crates.io/api/v1/crates/windows-threading/0.1.0/download :: license-apache-2.0
@@ -38722,6 +39782,7 @@ https://crates.io/api/v1/crates/windows/0.58.0/download :: license-apache-2.0
https://crates.io/api/v1/crates/windows/0.59.0/download :: license-apache-2.0
https://crates.io/api/v1/crates/windows/0.61.3/download :: license-apache-2.0
https://crates.io/api/v1/crates/windows/0.62.2/download :: license-apache-2.0
+https://crates.io/api/v1/crates/windows_x86_64_msvc/0.48.5/download :: license-apache-2.0
https://crates.io/api/v1/crates/windows_x86_64_msvc/0.52.6/download :: license-apache-2.0
https://crates.io/api/v1/crates/windows_x86_64_msvc/0.53.1/download :: license-apache-2.0
@@ -40426,6 +41487,28 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
================================================================================
+zune-core@0.4.12 (MIT OR Apache-2.0 OR Zlib)
+
+https://crates.io/api/v1/crates/zune-core/0.4.12/download :: src/lib.rs
+
+/*
+ * Copyright (c) 2023.
+ *
+ * This software is free software; You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
+ */
+
+Copyright (c) zune-image developers
+
+Licensed under the Apache License, Version 2.0
+<LICENSE-APACHE or
+http://www.apache.org/licenses/LICENSE-2.0> or the MIT
+license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
+at your option. All files in the project carrying such
+notice may not be copied, modified, or distributed except
+according to those terms
+
+================================================================================
+
zune-core@0.5.1 (MIT OR Apache-2.0 OR Zlib)
zune-jpeg@0.5.15 (MIT OR Apache-2.0 OR Zlib); authors declared by package: caleb <et...@gmail.com>
@@ -40481,3 +41564,41 @@ freely, subject to the following restrictions:
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
+
+================================================================================
+
+zune-jpeg@0.4.21 (MIT OR Apache-2.0 OR Zlib); authors declared by package: caleb <et...@gmail.com>
+
+https://raw.githubusercontent.com/etemesi254/zune-image/fa2c767a01d7d9373911d0bf63e0588553d67e0e/LICENSE-ZLIB
+
+This software is provided 'as-is', without any express or implied
+warranty. In no event will the authors be held liable for any damages
+arising from the use of this software.
+
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+
+1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+3. This notice may not be removed or altered from any source distribution.
+
+================================================================================
+
+zune-jpeg@0.4.21 (MIT OR Apache-2.0 OR Zlib); authors declared by package: caleb <et...@gmail.com>
+
+https://raw.githubusercontent.com/etemesi254/zune-image/fa2c767a01d7d9373911d0bf63e0588553d67e0e/LICENSE.md
+
+Copyright (c) zune-image developers
+
+Licensed under the Apache License, Version 2.0
+<LICENSE-APACHE or
+http://www.apache.org/licenses/LICENSE-2.0> or the MIT
+license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
+at your option. All files in the project carrying such
+notice may not be copied, modified, or distributed except
+according to those terms
diff --git a/products/maka/cli/notices.json b/products/maka/cli/notices.json
index b72d74e703..186e39ee5c 100644
--- a/products/maka/cli/notices.json
+++ b/products/maka/cli/notices.json
@@ -1,7 +1,7 @@
{
"inputs": {
- "Cargo.lock": "70551c8922c2ec2b2728d1a8d274cfc4a55094335c7a9a3bd29164828f710d23",
- "Cargo.toml": "a8de04fd6213b52cf187f9208c0f36bb15d8dab4d8ef8e7c482503d75c9b7e43",
+ "Cargo.lock": "fc2624a808be17374651f4e50d22b5260f402bbe01e1136f2846f6ffe6a7f99b",
+ "Cargo.toml": "45a2573a3700ebf6d1df5badde2ca0a02197f19f86eeeabb35e4c0ce874a2f28",
"package-lock.json": "a5af15aca21fabe0e1bdd48784167df4dc81cee17777d54861dcbd641168a4eb",
"package.json": "38baf3beeb5876849ca16e31868d59bfaee769f260c7555b4a8a1847b92e28a1",
"packages/plugin-sdk/src/configurations.ts": "e7bb3e7245adc43f27a3834c2522b2645540ae5a6a1300a706842abd4a4da852",
@@ -507,16 +507,16 @@
"third-party/gpui-fast/Cargo.toml": "4d549553fd2df7aba644dae5f978857c5ba22900d78d32bac905b8d75651b6c8",
"third-party/gpui-fast/LICENSE": "5f78a733701db49b73da93addad8cb74f816f3789ef30c3c0e959e943ecd99b4",
"third-party/gpui-fast/LICENSE-APACHE": "5f78a733701db49b73da93addad8cb74f816f3789ef30c3c0e959e943ecd99b4",
- "third-party/gpui-fast/SOURCE.md": "7baa2379f99f8d0968410eabc3d398cd28d456eb5a6e18af7516a775870d3bfb",
- "third-party/gpui-fast/SOURCE.zh-CN.md": "8137585669a8536cc21c59f2808b93d87aa38364884b99a02c95547353af1ced",
+ "third-party/gpui-fast/SOURCE.md": "9884f549c183f37b6c856f4745cce24416722bb6fa5bcff403f4b68e9bb8245a",
+ "third-party/gpui-fast/SOURCE.zh-CN.md": "fade3ef12bef8851943579d0532bec1ebfcd9497e2faf17e67b4bacf1931b078",
"third-party/gpui-fast/assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf": "975dcda37d80f038dcd143c22e33ca2d97a0cc5a929aace1c749153b0fe1afa5",
"third-party/gpui-fast/assets/fonts/ibm-plex-sans/license.txt": "91c25c350d3cac39da2736d74f7ba37ef648f5237a4e330a240615bc8d8c4360",
"third-party/gpui-fast/assets/fonts/lilex/Lilex-Regular.ttf": "8a467ad5a1941dc7c9cebaee34e3ed9364a73b48393034e363e4b528bd711d04",
"third-party/gpui-fast/assets/fonts/lilex/OFL.txt": "a356708a399e3f54b937ec7d9b722c26dabba21365b60f95c56eb7ebca0dee40",
- "third-party/gpui-fast/crates/gpui/Cargo.toml": "7960754ca49938acebcf85675cac18f26a9dfde9d8983bf675ca414aba6bfa33",
+ "third-party/gpui-fast/crates/gpui/Cargo.toml": "3219f6adbc58e1578753a6390fa737eb2551d734a06f864ac0b79970d2436a11",
"third-party/gpui-fast/crates/gpui/LICENSE-APACHE": "5f78a733701db49b73da93addad8cb74f816f3789ef30c3c0e959e943ecd99b4",
"third-party/gpui-fast/crates/gpui/README.md": "989b10e5914dcc8a1f2568303849aeab2d6aa26f2c5dfead96de0491330ada79",
- "third-party/gpui-fast/crates/gpui/build.rs": "faf9c1df29ca77d633e6e809cd47a0b4fd760466de4df161c676e9830b54a1b6",
+ "third-party/gpui-fast/crates/gpui/build.rs": "82245901888defc1371cf554ed505a141755ea412233ae242ff3ca6c62aa12ad",
"third-party/gpui-fast/crates/gpui/docs/contexts.md": "2688efb86472685cf2ea1af254c7c4bc70d53dd369c3d30118bf5e356fe86574",
"third-party/gpui-fast/crates/gpui/docs/key_dispatch.md": "56189467c43ca57c7cfa507f6ed7cdc31516144cbe648ef1f5fd97696862fe1a",
"third-party/gpui-fast/crates/gpui/examples/image/exif-orientation-rotate-180.jpg": "cb6c28e3e6065d62790ba38662d14f3f1510180375cacba18547720e8b48d9d5",
@@ -525,14 +525,14 @@
"third-party/gpui-fast/crates/gpui/src/_accessibility.rs": "8dbaf1f77751f57d5a59aeeb15d2d2aa0f868b45d1b80919afc7c4719627da3f",
"third-party/gpui-fast/crates/gpui/src/_ownership_and_data_flow.rs": "4edbde237dc322ceac8969028811fd30d87a8bd108475a642c4fb961b2770258",
"third-party/gpui-fast/crates/gpui/src/action.rs": "6566c34d4f4cf7f6a5280cbc4c07c747c5c6fc4c6554abd42c73fefe5716fc20",
- "third-party/gpui-fast/crates/gpui/src/app.rs": "38a571e3b7911a39af159f121ce9d800cbb943c7bb6c6d5f02d6a1a1fe21bb45",
+ "third-party/gpui-fast/crates/gpui/src/app.rs": "43810f579ddc5b1141562bc4fb039de3de09fa039385897c7d79c04b6379dc15",
"third-party/gpui-fast/crates/gpui/src/app/async_context.rs": "3a1948721bca22ccfae2926c6b40722ed3315a33e79c8d87885edc4bd610c115",
"third-party/gpui-fast/crates/gpui/src/app/bench_context.rs": "6bef320831a933502651903f85b849b6ea371b84c6822b7b88d3e064dc8c9818",
"third-party/gpui-fast/crates/gpui/src/app/context.rs": "b05a478fb75a5488887cb46e2ea151c5d85ea8543311aca9ac5e724200f03ecb",
"third-party/gpui-fast/crates/gpui/src/app/entity_map.rs": "bb2f2a5220461e820bb55c808369e8c1f2bc29f03a68d24bda3c23d74d74d9a2",
"third-party/gpui-fast/crates/gpui/src/app/headless_app_context.rs": "11f273fc2aeda076cb40b49b5a658d7ea2be50f72d3fe56524b2e78f7a04bc15",
"third-party/gpui-fast/crates/gpui/src/app/test_app.rs": "ea41a502634c8eb8011a0f499b8a3900939228e6f6260bf8857766c799633316",
- "third-party/gpui-fast/crates/gpui/src/app/test_context.rs": "f34cc5f584df6f3b8bd17feba637ad4a0d1608de45b2b37af34719f5d916dec0",
+ "third-party/gpui-fast/crates/gpui/src/app/test_context.rs": "9894b987b226cac04cf565c71db467198d811f393da6456b366662363280a9a8",
"third-party/gpui-fast/crates/gpui/src/app/visual_test_context.rs": "b6e0399349417b70cf067a90cf46a385a0b2efc999e30fd4a037d0ecdd87ff1c",
"third-party/gpui-fast/crates/gpui/src/arena.rs": "262e6403f8fa505592dddb674028f43c0e41beb1f51aa97e37c917215052580b",
"third-party/gpui-fast/crates/gpui/src/asset_cache.rs": "cc1eb306523094d2d9a0e15538ce9a4fe29408288bd73e27781ea9a92c1b059e",
@@ -547,7 +547,7 @@
"third-party/gpui-fast/crates/gpui/src/elements/canvas.rs": "3e09646e725d31c10a88be290c146d6a6e829e0be173e6bc82c138461668e646",
"third-party/gpui-fast/crates/gpui/src/elements/container_query.rs": "7e72aab18967e7e16ebbe38c535caf2ac6f881a62cf9f6045922fbeb8a06df07",
"third-party/gpui-fast/crates/gpui/src/elements/deferred.rs": "af8780d3197e8795f7920aa42920d676f1a8bc194af8d8cc35c508b8fc8af500",
- "third-party/gpui-fast/crates/gpui/src/elements/div.rs": "da566e57e3b7dedb7b8b06bfe489baf677eeb7cc2244f42a5e284308e2f330bc",
+ "third-party/gpui-fast/crates/gpui/src/elements/div.rs": "5f41ed8fe1c22d080fb39d6ef430ecaeb45ce51a5289c8e5c762f8aaf54d481e",
"third-party/gpui-fast/crates/gpui/src/elements/div/scroll.rs": "3751cc9754a3bc48136f816a10eed5a0d80dc7747e417f5e16a43440b6b304b0",
"third-party/gpui-fast/crates/gpui/src/elements/image_cache.rs": "e4540d7e92bc63bbaaf703df42338bbfabca764e34b7244880460d38962e14c4",
"third-party/gpui-fast/crates/gpui/src/elements/img.rs": "c196cb1ed3db30ab3141b036c84288a22e04de2b53b687e1d86fe732420374e3",
@@ -560,83 +560,87 @@
"third-party/gpui-fast/crates/gpui/src/executor.rs": "02e68407eb89156de31e05320a4d3565ea6f1c639dd378650b1703e8729b5640",
"third-party/gpui-fast/crates/gpui/src/fast/a11y.rs": "0030f7a33ec52693d3ecf833f268391643add9dd10eabe0ba566dd3dbaa2d866",
"third-party/gpui-fast/crates/gpui/src/fast/bounds_tree.rs": "c9986acf0367c287c51453a86770c5fc4ff08906b4cd2aad589ef125424aa19b",
- "third-party/gpui-fast/crates/gpui/src/fast/composition.rs": "e203b9b1c12e7c5ca2ea7bbf522e079a9d76a5812cb1de377e4a28b6cb49fa51",
+ "third-party/gpui-fast/crates/gpui/src/fast/composition.rs": "b171e521a7ab96f06bb6941690f498a67b086d51225c74f55dcf3ac9fd5cba41",
"third-party/gpui-fast/crates/gpui/src/fast/dependencies.rs": "9e6f4ccfdcb7255d0e6de75cd493ddac4ed5b0f2f2d4f44819ade104d9b4f1eb",
"third-party/gpui-fast/crates/gpui/src/fast/dispatch.rs": "e961ac65eae7a2abf556aed79314eabe769acc321f5dbeefed9fd875c1b8471b",
"third-party/gpui-fast/crates/gpui/src/fast/global_id.rs": "bf5c602aef9fbf6fcf8bbe4bb1038950730fa3e3911a0a3aa71f59f5b793b595",
"third-party/gpui-fast/crates/gpui/src/fast/glyphs.rs": "ef4a80e125b0d2a64cddf1083d57cbf3f31c6861a458f3190e2f197afd351649",
"third-party/gpui-fast/crates/gpui/src/fast/interactivity.rs": "e9d3ff06009898771ad1ac2941c807e96baf7202cf4590fe79ff5c64d6e8088f",
"third-party/gpui-fast/crates/gpui/src/fast/layers/background.rs": "217322f6cfde5d48eb4269299acc28ef9e034595f3836ddb87b08a471b40491d",
- "third-party/gpui-fast/crates/gpui/src/fast/layers/hover_freeze.rs": "249d6048dbaeae018e741358815b6b272c6868dee73d5decb553080cf4675981",
- "third-party/gpui-fast/crates/gpui/src/fast/layers/input.rs": "a88ec16517f5fd0b9bcf1d3158e9475b449e9cbb201a3c393d64fa1b448e4bb2",
+ "third-party/gpui-fast/crates/gpui/src/fast/layers/hover_freeze.rs": "3fa1948b45832bc7cd3ad87c1f0cff11a683c6cfc3b826b2718d2d06353ee0de",
+ "third-party/gpui-fast/crates/gpui/src/fast/layers/input.rs": "0e00cadf848ece8280d00e9e5875b58029718ba3872ccbfe20ca84e13f61ebfa",
"third-party/gpui-fast/crates/gpui/src/fast/layers/invalidate.rs": "cdca05c90d5acaaf99e27acb36e19d3f0ca03c30b26c92007d49cba20975dff0",
- "third-party/gpui-fast/crates/gpui/src/fast/layers/lists.rs": "46f52a92efadb3d05e18a075e73d0efbe970d16e74cb62d360641c44a4f1e5b5",
+ "third-party/gpui-fast/crates/gpui/src/fast/layers/lists.rs": "6f2f28330001174f6316a694f9755999c24530fb72fbcd64907fc7b7856d9b0e",
"third-party/gpui-fast/crates/gpui/src/fast/layers/mod.rs": "e4e72047559318b36beadff4514530463a767e6c8113231c32a0569121ba8f63",
- "third-party/gpui-fast/crates/gpui/src/fast/layers/overlay.rs": "63031bca6d31064880514b63c07607431a87fb38dedd5aba0468b4b9b7cd6b5c",
- "third-party/gpui-fast/crates/gpui/src/fast/layers/paint.rs": "1887ca85af79cb1b8d3e55e386666c2aa558b23d0ce8616f2b5504fe322ca5dd",
- "third-party/gpui-fast/crates/gpui/src/fast/layers/policy.rs": "e87a890bfe2a59e7d75496cc278f0c2d89e723f36d1308559f72c0e372726cce",
+ "third-party/gpui-fast/crates/gpui/src/fast/layers/overlay.rs": "455affd0e077ae44db460db65e8472facd112e531758060a09cce5d01b8aef44",
+ "third-party/gpui-fast/crates/gpui/src/fast/layers/paint.rs": "e7d3c4bc4f3d7419177c285757c15ea724546baea68a674410a3f2a43510f2c9",
+ "third-party/gpui-fast/crates/gpui/src/fast/layers/policy.rs": "de4bd84781e9c5dc3ced333cd1955c88999e627c18c556b5feb06b964cff8f5e",
"third-party/gpui-fast/crates/gpui/src/fast/layers/record.rs": "1ba15dd5cbfd3133e5368b7ce3281b3122947aa4f88bea5244f76a5cc8f2f695",
- "third-party/gpui-fast/crates/gpui/src/fast/layers/reuse.rs": "f748c2b41f17dd5dc0262b8457238f14799f2ae8af5ad90ac7bca0aac736fc7d",
- "third-party/gpui-fast/crates/gpui/src/fast/layers/scene.rs": "2bbed5b42868bb7d43012a20a8e283f5895f08a7b193df7d955cd3fa540a7636",
+ "third-party/gpui-fast/crates/gpui/src/fast/layers/reuse.rs": "7c5ec44e1e965dca350e06a6912a111d27f375679cee87a302b38bdca540094c",
+ "third-party/gpui-fast/crates/gpui/src/fast/layers/scene.rs": "0a36b8f1203dec2d14d8b5f389a8e55718d31262f4bb825a433c978c0f89f78e",
"third-party/gpui-fast/crates/gpui/src/fast/layers/scroll_to_bottom.rs": "59d4c375352f9c4c1a354438acfd431520ce8d89b01545fbf6f9f4ba6c26e631",
- "third-party/gpui-fast/crates/gpui/src/fast/layers/tiles.rs": "e9ecdcce39aebcd248ced9c2cf6f92bc2ede039da08135abd67932b215289486",
- "third-party/gpui-fast/crates/gpui/src/fast/layers/verify.rs": "ee186c02b95a622154846538c2dab41de6e6ce7bfae1056f4d155be2304a004e",
+ "third-party/gpui-fast/crates/gpui/src/fast/layers/tiles.rs": "6f19fda58ad071c7269b36eddca6619b3e5ca655714beb8ace1950ec570a38fa",
+ "third-party/gpui-fast/crates/gpui/src/fast/layers/verify.rs": "ea881a3d81acef65f2c05fbad777391570360cfee4e3212356fef0fb409aa940",
"third-party/gpui-fast/crates/gpui/src/fast/layers/wheel.rs": "ebfcaba6fb3c95f7d1660710c2a266a4a7c9af63f0ada4a0e6ef892c6c91c0ce",
"third-party/gpui-fast/crates/gpui/src/fast/layers/work.rs": "3854efa6e74d677897844d3b454612a25963d70f150deb8e2af943fe98e3f205",
- "third-party/gpui-fast/crates/gpui/src/fast/layout.rs": "5591f12968a9424b4ff1444dac95951e9ae1825e7f0f755f2a4a3e7b9de6b340",
+ "third-party/gpui-fast/crates/gpui/src/fast/layout.rs": "894e198f59ce27c93f74aaec6c61c7e2108526f6428e7aace91bc893da0d41a2",
"third-party/gpui-fast/crates/gpui/src/fast/layout_bounds.rs": "6a76a1cb904e315c4a280c7630a4e43a591fe3f2fdce55709915a5a6df70eda0",
"third-party/gpui-fast/crates/gpui/src/fast/layout_key.rs": "207bc5f474a48046e264eb0a88726d099e450cbd3694ec5e83dea642f114d048",
"third-party/gpui-fast/crates/gpui/src/fast/line_break.rs": "a136de66c81ebeb3304916b4ec664762f4ed18da114f7009f61617b4ec90a8aa",
"third-party/gpui-fast/crates/gpui/src/fast/misc/mod.rs": "c35c2b066dcca039e0013a7c4d8742eaa042352dd97f84ca4907406cc0a71808",
"third-party/gpui-fast/crates/gpui/src/fast/misc/touch_fling.rs": "37ed876f8fe36abcae802948683482f6cc57ec83115b3cbe99470f596667034c",
- "third-party/gpui-fast/crates/gpui/src/fast/mod.rs": "5dde3db3553fee6e83245de20e6ba6d835acfdbfeae77129a807d5d762a5e603",
+ "third-party/gpui-fast/crates/gpui/src/fast/mod.rs": "01483dfb5d6998043a72a4c7bf9ae6fdd6fd8fca5c8f1a799c3d2777b589f35f",
"third-party/gpui-fast/crates/gpui/src/fast/number_shaping.rs": "20282510326771e7bf5df622b49fa6b1a50d40d830485286fbd652db6a92413d",
"third-party/gpui-fast/crates/gpui/src/fast/path_cache.rs": "a987001a9ea91419d280b0d8af78e5ea167bbde1390a7a532bc00ba56fe08ffe",
- "third-party/gpui-fast/crates/gpui/src/fast/retained.rs": "27c4372b1ffa6f3ac9ff80b5a608cf77c672892942c98a614404fd21526057b9",
- "third-party/gpui-fast/crates/gpui/src/fast/scene.rs": "a0207c22d41a761dbb7430e25902f0d7cdbba10643a12853e646df4fdb1aa0e9",
+ "third-party/gpui-fast/crates/gpui/src/fast/retained.rs": "a3137347255dc59fc3e00efa9df97d4d4b62b6c21a4b11ff54660c768ece75a7",
+ "third-party/gpui-fast/crates/gpui/src/fast/rounded_clip.rs": "a53d95a31065245a82f4570cfed91fe6392a533da44b435e35ae4363fafa0bdc",
+ "third-party/gpui-fast/crates/gpui/src/fast/rounded_clip_fixtures.rs": "3311656ed7d47e7c785729bbc4ca888c783f0c87a232e117b0e90dd3734918e9",
+ "third-party/gpui-fast/crates/gpui/src/fast/rounded_clip_scene.rs": "5180ab7ca01d626ac187068f5542be5f768e39039d64db66a5c76f176ef360de",
+ "third-party/gpui-fast/crates/gpui/src/fast/scene.rs": "643cb067383d09a74241d3a4e989c6acb88e6cd7b7b5e70cc62fab1d6761d1b9",
"third-party/gpui-fast/crates/gpui/src/fast/splice.rs": "40b53008eed3e97abf07eb714c70639ee4fb0b65284b1015c3171174f7ab8771",
- "third-party/gpui-fast/crates/gpui/src/fast/stats.rs": "84781cd88451b8d4612a1745adb2bda4186c5302b90e39311ad53bab2df402ed",
+ "third-party/gpui-fast/crates/gpui/src/fast/stats.rs": "ef9f4d300f7f517af2b61b9ca372518917b98e0332b8e0941384181880346bc8",
"third-party/gpui-fast/crates/gpui/src/fast/tests/a11y.rs": "bedcb90fef1418867b8d68154223f86d669cd9e5061c8574c51dfd62f2122e03",
"third-party/gpui-fast/crates/gpui/src/fast/tests/cached_layout.rs": "9bf9240ce3e477bf9713211fde26412642d48dcf625ebf737e69e8312ada4216",
"third-party/gpui-fast/crates/gpui/src/fast/tests/dependencies.rs": "cea9e94ab131a884b0f0a217f86ebef97beff14dd53016d3e00314c06bb7ac2c",
"third-party/gpui-fast/crates/gpui/src/fast/tests/dispatch.rs": "80e60f28d65b094dc0a6ec14b86b2fa77022310382164c6553b6a861fa1c0529",
"third-party/gpui-fast/crates/gpui/src/fast/tests/global_id.rs": "e114b24de4a1a82c6aa16537e8783e2a493b1f6a4cf3d45e8df9c3d5e88608db",
- "third-party/gpui-fast/crates/gpui/src/fast/tests/layers.rs": "d6bb89977d26c053b2ab3af6497877284ab9718f7cd9c6e92288bc3e30b4319a",
- "third-party/gpui-fast/crates/gpui/src/fast/tests/layers_lists.rs": "9207a9e0d589ea09dcd2a8b683d03c38a5fac2721dbdf01870c8ebcf4da4d8ed",
+ "third-party/gpui-fast/crates/gpui/src/fast/tests/layers.rs": "413fff528792fe7337b21e094a648640024aec6abe50cf1b34b3df8340951b1c",
+ "third-party/gpui-fast/crates/gpui/src/fast/tests/layers_lists.rs": "351019cd9d9d99e1b19e0f2fffee1edc51f4997701f8741ca0a6367058199e89",
"third-party/gpui-fast/crates/gpui/src/fast/tests/layers_oracle.rs": "6c5883a4f26ff4238e9355d50e583938e213dd2508de64fb6508afbf9386c799",
- "third-party/gpui-fast/crates/gpui/src/fast/tests/layout.rs": "3dff17c6786f686c9c20610cdf8ce4e378dd262daa8dcbe9a1c78f3a8de04801",
+ "third-party/gpui-fast/crates/gpui/src/fast/tests/layout.rs": "adc89337813fe43afb09a55e2185d9f3a2a58bff167e700e769066a855ba73e6",
"third-party/gpui-fast/crates/gpui/src/fast/tests/misc/mod.rs": "d3edc13befd6096fd0735b791d1db2a24c079a1a1941bcec46a39a7e1d94cc12",
"third-party/gpui-fast/crates/gpui/src/fast/tests/misc/touch_fling.rs": "2908337c4797a1280f5f77d5d6a3031be21f57637e9e43f2b5fae0efc559f381",
"third-party/gpui-fast/crates/gpui/src/fast/tests/mod.rs": "f3a1ec5df739f3f2120acfbcb9c09303bdbcec72c4977d0c8ef0cf564e3c7ae1",
"third-party/gpui-fast/crates/gpui/src/fast/tests/number_shaping.rs": "90ca2782c11c124ef64e1f38f8413e266363753964a1faf2a84edccc72dd533d",
"third-party/gpui-fast/crates/gpui/src/fast/tests/oracle.rs": "46f74be40bc930b1cc369e92a918cdf4902c97b125018d9313b111ba4f27bbd3",
"third-party/gpui-fast/crates/gpui/src/fast/tests/path_cache.rs": "38d69662c00114e3e1c9db2029c0e63ba56110783a01a9c3cd37104e0524ef91",
- "third-party/gpui-fast/crates/gpui/src/fast/tests/retained.rs": "7ebb9b8082223a397b6e9d75e3351f27f43f6def679ab1a14a5ce6da74c6f91b",
+ "third-party/gpui-fast/crates/gpui/src/fast/tests/retained.rs": "a0804fb72f84f4c948cb73db53fe1575a28fa3dd4c7e9db1a7c92a20a14eaac2",
"third-party/gpui-fast/crates/gpui/src/fast/tests/retained_bench.rs": "a225a193b5ff4954c28cd435d77b9c2e5614e4e59b6cd1e6201a663f1c704311",
"third-party/gpui-fast/crates/gpui/src/fast/tests/scene_order.rs": "e0966c3c8f08eb388c0358672f6b1877a9f8606085a63d3a15ac3efc1b3f15dd",
"third-party/gpui-fast/crates/gpui/src/fast/tests/scroll_to_bottom.rs": "ce19afc434fe0dd7ec464073d09abeacd9fa9b927061d9a3b633e7105888065c",
"third-party/gpui-fast/crates/gpui/src/fast/tests/splice.rs": "8af04038af9454aaa6ecad9e72498d30444ab3f027c4c7b92242105f52e9716e",
- "third-party/gpui-fast/crates/gpui/src/fast/tests/support.rs": "b119ee26c1cda6ec424043078fca61b39d589ebfa97436f080cf93be8b0ff21b",
+ "third-party/gpui-fast/crates/gpui/src/fast/tests/support.rs": "499bdc891f21b3bdb82ca5883ce277afd190a42a1f24db3594de15fabe5de107",
"third-party/gpui-fast/crates/gpui/src/fast/tests/text.rs": "fc79c6c46c3db1386ba9450d10dbef6e705e4f7c3f1b5130b038599d2627a667",
"third-party/gpui-fast/crates/gpui/src/fast/tests/text_shaping.rs": "108f2e286a6a55f9ed89eacbb9d010beab386f4bb22ff44560c3a82b7ee337df",
- "third-party/gpui-fast/crates/gpui/src/fast/tests/visibility.rs": "02e099f026d0195df36778fd0b5196654098aa9810feba785363352a8c560bf1",
+ "third-party/gpui-fast/crates/gpui/src/fast/tests/visibility.rs": "296746ce1a9f7d48fa5c90f7d1cb184f4143a03e1ea881816c39c7e035ecaa1d",
"third-party/gpui-fast/crates/gpui/src/fast/tests/wheel_notifies.rs": "226e8aa39a0523a2b48882ad36dad94178fecc16ced43cf326cff125b0d2b534",
"third-party/gpui-fast/crates/gpui/src/fast/text.rs": "de7ecd08c71fe2bb8c4111a57ba862c84bab72d8b10d114414e20c12686d4c9a",
"third-party/gpui-fast/crates/gpui/src/fast/text_style.rs": "37bdddfd10919e8281d3a6ae236a8f869c4b6f60f2e8508cebaca523fea31138",
"third-party/gpui-fast/crates/gpui/src/fast/windows_manifest.rs": "2180789e5d545e4a77e4a6f56584de05908cf7f6a9bdc642f378c65a3723b9d6",
- "third-party/gpui-fast/crates/gpui/src/geometry.rs": "900fc506a964cda44ed0cf1f97c01ff28d7268c87c9eeadbf7a4f1bb50e33edd",
- "third-party/gpui-fast/crates/gpui/src/gestures.rs": "0a84d9cdc0521d2c57772956d12e4238fb14bd992a38cbb41595837f907316cc",
+ "third-party/gpui-fast/crates/gpui/src/geometry.rs": "fe4fdc556e471e228e882488bbc0afd9389bccde46c71b846ed7bef91ad96155",
+ "third-party/gpui-fast/crates/gpui/src/gestures.rs": "dd512f2cb2fe5b8427e7056c84b95f6416096ceac8b44a13176175920c85aa84",
"third-party/gpui-fast/crates/gpui/src/global.rs": "c52ddc647f712282a3a1f3629f864d0868966169fee7aff5f1f8d27e67f3f077",
- "third-party/gpui-fast/crates/gpui/src/gpui.rs": "3da43f4a892b0ed8fcf75e3bd46fee5212e57d0e722598ed86b4752f9bc20d31",
- "third-party/gpui-fast/crates/gpui/src/input.rs": "1e27952de8d447b268db7894f74fb41dad65a65c7d872466f589917b023a295d",
+ "third-party/gpui-fast/crates/gpui/src/gpui.rs": "2c8b75831e9cda1ad19b8d60466cac9a8316ec19e7e1f5f8da917f5e90f4ea50",
+ "third-party/gpui-fast/crates/gpui/src/hot_reload.rs": "9f756bb1bc5c6e5dc9fd5000ea5812cc5440006166d54c8982c3bdea70062de9",
+ "third-party/gpui-fast/crates/gpui/src/input.rs": "6128860a23946373d0c52c7eaabe980a165999640ec012c33a821fd11c9f7eda",
"third-party/gpui-fast/crates/gpui/src/inspector.rs": "39a2790b992b2c354eccb4d60a7070e41628a86c5463d339f9c7b8fa250c1ede",
"third-party/gpui-fast/crates/gpui/src/interactive.rs": "72d909c3a87eb1ad26cbb15a18b845a5d5dd1e1a3ceeb3b6214724ce917ef5d5",
- "third-party/gpui-fast/crates/gpui/src/key_dispatch.rs": "a8eb26f75b97992a03293e0a4e3797b60ea2d2cf22f64532a46a155e7101639a",
+ "third-party/gpui-fast/crates/gpui/src/key_dispatch.rs": "1cce7933e0196613c7f031d2e523a1a301b8746bee8711638ab813051f9d7f16",
"third-party/gpui-fast/crates/gpui/src/keymap.rs": "be60f705557a311e1a6ec7c6fbc9a1f5dc3f7a784630edeba98a8a14d7796c5b",
"third-party/gpui-fast/crates/gpui/src/keymap/binding.rs": "7306cd42de8bd0dbbf111c7b1c9d30b2a814251ad0f00b7f7cb3bf042925f895",
"third-party/gpui-fast/crates/gpui/src/keymap/context.rs": "dd3cf2e883caed26e201a8aed39fbc51de782e7f806ecc22fde617929ca223cf",
"third-party/gpui-fast/crates/gpui/src/path_builder.rs": "3e5a02f2896f20dd5aed61a423a5a6fbf9d4e886bb9004f0958374c16b43d268",
- "third-party/gpui-fast/crates/gpui/src/platform.rs": "cf72647c2a045c00127966965f8001141cf32fe44b719a1b79fd0f2b4f66f40c",
+ "third-party/gpui-fast/crates/gpui/src/platform.rs": "2f711c6aa0248578ae088ead3a0847cb2e0891fa476214bd8504b525ca7a9806",
"third-party/gpui-fast/crates/gpui/src/platform/app_menu.rs": "c751e70bf50cf2366dad9d6b2a66fc3a8e9cd1547128173dbfc59520feb454cc",
"third-party/gpui-fast/crates/gpui/src/platform/cleanup.rs": "7867d07d306c8fd95e9f50435efd2d8d8993958c4987bd55bd8293c68952dc7e",
"third-party/gpui-fast/crates/gpui/src/platform/keyboard.rs": "8de8d585f4238204fd14113abded703afa6bb0a532dacabdf400df8249390376",
@@ -647,7 +651,7 @@
"third-party/gpui-fast/crates/gpui/src/platform/test.rs": "52861874bfdd1433728d61bd7be3a40b1f8c27aae4e4cc9d31b311ef13bda994",
"third-party/gpui-fast/crates/gpui/src/platform/test/dispatcher.rs": "dea03a78a0c64d486f2c5b7bc669ba281638a0f8ee53316a52993a0e5b5cabd0",
"third-party/gpui-fast/crates/gpui/src/platform/test/display.rs": "f026713ec76569ea1402b6d1d4996636530d4f592245a0c97ac795d112f8debf",
- "third-party/gpui-fast/crates/gpui/src/platform/test/platform.rs": "296df7c83bc522af226752183c2bb5dcdf17d7a38ee7dcb2ef356b41ceecf968",
+ "third-party/gpui-fast/crates/gpui/src/platform/test/platform.rs": "3769b094a86b268114a40843aae731b747ce1eb4ca58e7c2a0c56b1e76efa037",
"third-party/gpui-fast/crates/gpui/src/platform/test/window.rs": "405de3bc06e3da64ca5b68b51c3982830d74198137e30388c3a30daf02c5137b",
"third-party/gpui-fast/crates/gpui/src/platform/threaded_dispatcher.rs": "52648bf446a49088245f817237fe282d89325912d9b8167988475799427e804e",
"third-party/gpui-fast/crates/gpui/src/platform/visual_test.rs": "50625b19b4607d8bf2c9d2308491e0efed9b1e1822ac27454c9d5250134bbbe6",
@@ -657,18 +661,18 @@
"third-party/gpui-fast/crates/gpui/src/profiler/actions.rs": "1907458a07d8370f19652fb60bb0bf1ef8ee8baf3e5fe4d04fc54f380e586d8f",
"third-party/gpui-fast/crates/gpui/src/profiler/hang.rs": "62a45a98e77e8ce55a5e27a497ff9f27f99256609fe45ec557f15dc4e6bd3588",
"third-party/gpui-fast/crates/gpui/src/profiler/hang/monitor.rs": "4000e36cea97480ee811dcf4c1839b0d8ca78fc61d885a38dd02f8c84030fa53",
- "third-party/gpui-fast/crates/gpui/src/profiler/journal.rs": "78240995b3727fb3cdcf4f8c06f38f81c49f9a3a0793b414450e19bb7da00c3e",
+ "third-party/gpui-fast/crates/gpui/src/profiler/journal.rs": "61b7565e1f243eaf6677998915635da54971e39828cbc27cbd1e6a4852a8799d",
"third-party/gpui-fast/crates/gpui/src/queue.rs": "9753459cd04fedae7f2323bd9b1b2e7379e0cf10858f4b82922d62a84d474710",
"third-party/gpui-fast/crates/gpui/src/randomized_element_tree.rs": "9ca829b94c8bf48ed007ef89a684d3909ff12fd9446c726ee6d2a1c6185fb615",
- "third-party/gpui-fast/crates/gpui/src/scene.rs": "e0461bb783d64bc81b4b8d23e2841dee70f676e39a40a8e8293975480a59544d",
+ "third-party/gpui-fast/crates/gpui/src/scene.rs": "9b191b9b392f9d4162fd898f9fe3e8ce50f98613a628d3c58dc5ee8299d7cb8a",
"third-party/gpui-fast/crates/gpui/src/seeds.rs": "5ee929b48bf3f5b0fa6ba0dd99cb87371ba4dfcf0c09814494b3af3b4fd5971c",
"third-party/gpui-fast/crates/gpui/src/shared_uri.rs": "7a0a4feb3e290bb3f2c15fd4c8ee4c8ca3a9a5974e04ec084c4f8f0f3fc89248",
"third-party/gpui-fast/crates/gpui/src/spring.rs": "1cdc32dc3fd2d2b8cbdc7809e51c276ced938592dd08083fe7cc063a02086663",
- "third-party/gpui-fast/crates/gpui/src/style.rs": "df15f328d8f9ef93bb3d5fedd42b388f90d7a08575a52230322e570bf5455791",
+ "third-party/gpui-fast/crates/gpui/src/style.rs": "edb706a56699485d7db53aaf0eedb53d0f324e7c559bf2bf28ae389fef3b5cc7",
"third-party/gpui-fast/crates/gpui/src/styled.rs": "c6e62fe136bedf887ee755c886df5466d5fe0b398a004b35d1a3530fef809c8d",
"third-party/gpui-fast/crates/gpui/src/subscription.rs": "cca3b5138cb23084932772c8e009cce8a08562c2c00b69f3e0d4b1676b88087a",
"third-party/gpui-fast/crates/gpui/src/svg_renderer.rs": "fb12dae87c0cd4c5f5a1fbbd2f9548d52fde9440c21e3d576dfeb0233eda88a6",
- "third-party/gpui-fast/crates/gpui/src/tab_stop.rs": "16fa183dfd46aab4db1eac34500ca779189eae65b94b97e24a750ce7105ae513",
+ "third-party/gpui-fast/crates/gpui/src/tab_stop.rs": "5edb4973ef44b51c05fb6f93be53b169585e4f9e21569b48fc7419895fe066c7",
"third-party/gpui-fast/crates/gpui/src/taffy.rs": "9095901a5e857b004a5331f7c04ae8b86a7b922548e9c4edff8ff14454d31dbd",
"third-party/gpui-fast/crates/gpui/src/test.rs": "205f9f481fe843470dca43972853f4d64b23ddd03ee9b26eebbf2a2508a9c65f",
"third-party/gpui-fast/crates/gpui/src/text_system.rs": "1061b423cdceccf3ea49c05dfff87e8ad39390cab387d4e6fe8a82bc7ae58334",
@@ -678,18 +682,19 @@
"third-party/gpui-fast/crates/gpui/src/text_system/line_layout.rs": "c72a0c6c7ffdc737d314b2b0306e478b0bfc92fb102042c7642009fe15f8e4f1",
"third-party/gpui-fast/crates/gpui/src/text_system/line_wrapper.rs": "e96b639b5399340b9c716f875deb5b35e2ca29562235336f1ae8489e83d6d376",
"third-party/gpui-fast/crates/gpui/src/util.rs": "04397f667bf5d890cce690b96441cd5b8f5eb0a5dd2942ce27b0314be38fa636",
- "third-party/gpui-fast/crates/gpui/src/view.rs": "eb93d6e4e2354be24d475862d66f97658f263bbc889788ce33bf5dac43576cac",
- "third-party/gpui-fast/crates/gpui/src/window.rs": "9e38e4d2df2864a6acdbb53facd206073709acc19aa471d33dd9e13162c4009a",
+ "third-party/gpui-fast/crates/gpui/src/view.rs": "86d53d5aca92397279a9a8eefb3f819610194d55199e65ed98ba55a08ac93675",
+ "third-party/gpui-fast/crates/gpui/src/window.rs": "0d0ac4408c38bc927e86f8b999dc055230dbd7008bc6c0a7f27d53749ca5305e",
"third-party/gpui-fast/crates/gpui/src/window/a11y.rs": "d6b698deb5b8ae3e07e31e8c6a8e0a76d37fa63c88655cc0ea10df71c65362cf",
"third-party/gpui-fast/crates/gpui/src/window/a11y/debug.rs": "83d0693effdc98e64a083b217dbb09623f03f8f2b55db4a10f567aee977985d8",
"third-party/gpui-fast/crates/gpui/src/window/input_scope.rs": "7eed12d39d100e2b74cab5092c9f8a5c827fe5ec9152c4827e09e9ca28934c78",
"third-party/gpui-fast/crates/gpui/src/window/prompts.rs": "0d58ec14e8139af53b50f4ad67690f01959ed59ffafc4adb2cf522f3f6197164",
"third-party/gpui-fast/crates/gpui/tests/action_macros.rs": "47b69024f403feb6cb63c9990a6993b8582856146f5a6ae16984714321956a84",
- "third-party/gpui-fast/crates/gpui_linux/Cargo.toml": "2d626bcade66f6070525b947afb2748c41db9795f5f24872e15fb810f78b80e1",
+ "third-party/gpui-fast/crates/gpui_linux/Cargo.toml": "a7497f54f05393ad7c082fb27b89833d2c0597b4f466cde41d8579c8feef6825",
"third-party/gpui-fast/crates/gpui_linux/LICENSE-APACHE": "5f78a733701db49b73da93addad8cb74f816f3789ef30c3c0e959e943ecd99b4",
- "third-party/gpui-fast/crates/gpui_linux/src/fast/composition/mod.rs": "442f1133ebeb57f594d3917cb22c0d7764fe023adf4518ab454927476ee38e78",
- "third-party/gpui-fast/crates/gpui_linux/src/fast/composition/wayland.rs": "d0198c278d843222b2a552dbd324f4fdac532319373f941a5870a5a9d758b164",
- "third-party/gpui-fast/crates/gpui_linux/src/fast/composition/x11.rs": "333bcabaf8a83f6de756df05dee193cccd33ff2af3533401e4b9e4db3895be69",
+ "third-party/gpui-fast/crates/gpui_linux/src/fast/composition/mod.rs": "1bd6259cf207ae951bb3f2ba9a0d704bfa44517497a09495b1a6f08e0e3f8710",
+ "third-party/gpui-fast/crates/gpui_linux/src/fast/composition/occluders.rs": "bad9fac0777025521b843926c8ad6dfa4ca8afe347ae974b92d00c133ac7dcec",
+ "third-party/gpui-fast/crates/gpui_linux/src/fast/composition/wayland.rs": "3c038621284e9344967c755197de56031a01789c33df9678a77726c5ab21c4a3",
+ "third-party/gpui-fast/crates/gpui_linux/src/fast/composition/x11.rs": "89c4b1404620329752c47bfce00a51ec344f96aa440689bdc489eed8d14d5114",
"third-party/gpui-fast/crates/gpui_linux/src/fast/mod.rs": "2f8a5991eff2c657b47cf1e5d2067fa72ac32ba4b43288f563ff6b841338f354",
"third-party/gpui-fast/crates/gpui_linux/src/fast/x11_scale.rs": "9806732b010bd4e72f88fc73760a4c5e049b745b0d1908c2a056856d4104b0aa",
"third-party/gpui-fast/crates/gpui_linux/src/fast/xim.rs": "dbd3c3d2f2214b2106cae74087f7d00723cb8661b0d801e9e74424d21fee3017",
@@ -704,7 +709,7 @@
"third-party/gpui-fast/crates/gpui_linux/src/linux/system_notifications.rs": "49030ee41edb842c996d32870c4d9846f12bd8a50f4a6db0bea9dc658329f1b5",
"third-party/gpui-fast/crates/gpui_linux/src/linux/text_system.rs": "afc6712696eed6749cc277b800cfb62690af9cbfe275961ad1df1c8d190d4028",
"third-party/gpui-fast/crates/gpui_linux/src/linux/wayland.rs": "d3b0faac9d726ecae306c52914bcec72253d7308f5b499235d51816397a4736a",
- "third-party/gpui-fast/crates/gpui_linux/src/linux/wayland/client.rs": "52c430a09f012a5b35776908f74ac3fa293c68e24d0bc2c7996fd8bbfc83de39",
+ "third-party/gpui-fast/crates/gpui_linux/src/linux/wayland/client.rs": "133568b88ad3c663f420ac4e6337b61ddbdee7d25568c4ac3deb49e0ed380607",
"third-party/gpui-fast/crates/gpui_linux/src/linux/wayland/clipboard.rs": "7dab6bf97638ed62a6f6ebb7d6f435c6663631ee759a48b69ca4037cbda54df1",
"third-party/gpui-fast/crates/gpui_linux/src/linux/wayland/cursor.rs": "1645dc1e70fe9bb766ee65ab78fa6590538a7a52125de4d86dde18a26004c821",
"third-party/gpui-fast/crates/gpui_linux/src/linux/wayland/display.rs": "79253d4d1860ea5e36e733e35f820600e51a288c7504c2e731d4b86866dc1113",
@@ -713,19 +718,19 @@
"third-party/gpui-fast/crates/gpui_linux/src/linux/wayland/serial.rs": "e45add908e937b0af6897e1a462357e0fc99dcc0c53c802d912c5659ffedc72c",
"third-party/gpui-fast/crates/gpui_linux/src/linux/wayland/window.rs": "3074c96477dd82e7bb564555a8ab4a690211b8496136bd83dbca37ce95f62fe1",
"third-party/gpui-fast/crates/gpui_linux/src/linux/x11.rs": "df1e4f889ecaccf9ea9d7cc9f539fa4cdaac463998bbbd8367ff919f49e4ddf3",
- "third-party/gpui-fast/crates/gpui_linux/src/linux/x11/client.rs": "a0ae8b7547a5ba075d68608bf94400d32695476a5488c58f34409e0664d867e3",
+ "third-party/gpui-fast/crates/gpui_linux/src/linux/x11/client.rs": "0456e08e6ebacedcaa9810a21ca3b5ff877da6efdc944572c4f9988da52650b3",
"third-party/gpui-fast/crates/gpui_linux/src/linux/x11/clipboard.rs": "580eb2519d8c38422b3e5baba24a11e6fa5e942392c9a9e74abedfa3fa84c0f3",
"third-party/gpui-fast/crates/gpui_linux/src/linux/x11/display.rs": "d625c6aa288b8d302cd86f704a507c947d4b09d3d746d3004ea917fd6dbaaa35",
"third-party/gpui-fast/crates/gpui_linux/src/linux/x11/event.rs": "6303b0fd823f7fd7bcdab70ec085b31705f1e82fc254c385a523df309462e5b5",
"third-party/gpui-fast/crates/gpui_linux/src/linux/x11/window.rs": "7fb62f94cab8b6e270876ca6d43826054c12400f78355f9835da313be14345a2",
"third-party/gpui-fast/crates/gpui_linux/src/linux/x11/xim_handler.rs": "5a1f5b1a6a436a82c6bbbf9222a7470d537c5fac6b06da4e898aca497ee2adf2",
"third-party/gpui-fast/crates/gpui_linux/src/linux/xdg_desktop_portal.rs": "2e3a8eb44718dbd31b21fc368ad2f5c2105b340ef04f7e002ee0029adb9f3cc1",
- "third-party/gpui-fast/crates/gpui_macos/Cargo.toml": "1b0832ca94e7b3f16b25e50dee3efe2eb12e00cec770e4612d00d39907c6c3cd",
+ "third-party/gpui-fast/crates/gpui_macos/Cargo.toml": "364641fbea2fbc955fea1723047b5ed4f74129e5ecd68b9f6d4873106c9bf191",
"third-party/gpui-fast/crates/gpui_macos/LICENSE-APACHE": "5f78a733701db49b73da93addad8cb74f816f3789ef30c3c0e959e943ecd99b4",
"third-party/gpui-fast/crates/gpui_macos/src/display.rs": "4be5fbeedecfc6d6f60afeb29704790bb67c78554c7ae8178a07d4a4303a9afa",
"third-party/gpui-fast/crates/gpui_macos/src/display_link.rs": "3f9de99c94f9390abcc1f1d69729f35304b233468668e471420793b0f4e02513",
"third-party/gpui-fast/crates/gpui_macos/src/events.rs": "a4027cd12e14953b0954c5c04981148125ff85e8460a7219200dbcc3e93d50f7",
- "third-party/gpui-fast/crates/gpui_macos/src/fast/composition.rs": "cf98578a12e962ec2ff5c1d6fc254fae80eb6a893905271a4c9d44b817f2da19",
+ "third-party/gpui-fast/crates/gpui_macos/src/fast/composition.rs": "d182d8d2a0f23c3094ec346d026f2d05b9c9f21a8db9ea89e1b9b8e942af9dc7",
"third-party/gpui-fast/crates/gpui_macos/src/fast/mod.rs": "751d08c82a29bb2a067b6a2bee94522e32686f935246c90f0c78c961a4c15467",
"third-party/gpui-fast/crates/gpui_macos/src/gpui_macos.rs": "81aef744ceb3ffa7d4c9f53fea2218186eab76394a7c47bacb4bc8bf6f84a121",
"third-party/gpui-fast/crates/gpui_macos/src/keyboard.rs": "d6cbb1b91f11a67836a094689989b1ea7d79df60d7fa7b1829e6e78e9a01ad2b",
@@ -733,11 +738,11 @@
"third-party/gpui-fast/crates/gpui_macos/src/platform.rs": "7d1eb313f78884ec84ee5ef25626fd4c53848df2e9340ce12e019457a9ec586f",
"third-party/gpui-fast/crates/gpui_macos/src/screen_capture.rs": "996c4308d93aea1c809e27baa2ac42b18d038a677cfdd599e481abf5ad200d4b",
"third-party/gpui-fast/crates/gpui_macos/src/system_notifications.rs": "df8374f075932996d58b99c043d97a82e1cefebab79e5b0f4648c8a3776ea5fe",
- "third-party/gpui-fast/crates/gpui_macos/src/window.rs": "2fb5ce3cb6d3d58478e018632caac2e59ac3b7c59fd309899036dc4cbb6b56c0",
+ "third-party/gpui-fast/crates/gpui_macos/src/window.rs": "755285d5e252e6f346e5d1580ae7c14ca50d96ea1941d0f9cb7afb0cbb668e9a",
"third-party/gpui-fast/crates/gpui_macos/src/window_appearance.rs": "b8a1950175c0184469606447fca9450ab39a194fde04b80da3f0ad468ce2bd03",
- "third-party/gpui-fast/crates/gpui_windows/Cargo.toml": "0bcda924cb7e7167f606798551426ec9a3953ab1f539b12ee6134cbf49d10e7c",
+ "third-party/gpui-fast/crates/gpui_windows/Cargo.toml": "143f4c346884e89fde946eb0ec8cca7ac8d3fec9c92ea8e03568d401b2ef6817",
"third-party/gpui-fast/crates/gpui_windows/LICENSE-APACHE": "5f78a733701db49b73da93addad8cb74f816f3789ef30c3c0e959e943ecd99b4",
- "third-party/gpui-fast/crates/gpui_windows/build.rs": "d01829e27c91591c6fc8ee12ea90f279debc7b378c42253739cd94c7eb37a1a4",
+ "third-party/gpui-fast/crates/gpui_windows/build.rs": "543e093723fe0f930d5cb30c8b60148adb08c448a115a650bed50dbc5159b7d0",
"third-party/gpui-fast/crates/gpui_windows/src/alpha_correction.hlsl": "d37b106f1266b40a0e17478455dfaac1205214f597e23fbd0a26455c49806bf4",
"third-party/gpui-fast/crates/gpui_windows/src/clipboard.rs": "eb68368598663c85916328d03582435d923e2b6eeae87a7a365a386848b87bf9",
"third-party/gpui-fast/crates/gpui_windows/src/color_text_raster.hlsl": "53bb7f737dcf5b7c7a2c474cf6e7f649cc4e7d49b5526c2ac5f75f75086ce2b8",
@@ -747,25 +752,27 @@
"third-party/gpui-fast/crates/gpui_windows/src/direct_write.rs": "27ff8aa902705794c5b9c0405bb7c3063e4b63e0d3ac44d92148d9ed18ee866c",
"third-party/gpui-fast/crates/gpui_windows/src/directx_atlas.rs": "eff897707e3b8b4e549cd247725a6cee9c14b6a8188e32d32ad6360d693ce455",
"third-party/gpui-fast/crates/gpui_windows/src/directx_devices.rs": "83372170f6225db45033473d118fc43708942ec9687467de09e2b5215c835cf1",
- "third-party/gpui-fast/crates/gpui_windows/src/directx_renderer.rs": "9781ee407151b3ef9b0be5436c518cd13e97f62131aed4145b74df1fc96d0390",
+ "third-party/gpui-fast/crates/gpui_windows/src/directx_renderer.rs": "8c1bbb79ddf92a82ba7bc20df7e998a96101d88ef4451d7f4d2ee4dee4f00902",
"third-party/gpui-fast/crates/gpui_windows/src/dispatcher.rs": "a24daa0105ceadc2a4d50f0d1afab5a4af441a6a1254412d9811a04095f8cdd4",
"third-party/gpui-fast/crates/gpui_windows/src/display.rs": "466c0c2a4e3e9b001f26681f9f992d7dcc4a930954749942b980f228c5c0b83f",
- "third-party/gpui-fast/crates/gpui_windows/src/events.rs": "723414174db7a345ba9a22f782bea7de1d0e667a09c1c319f6619979dab0d5cc",
+ "third-party/gpui-fast/crates/gpui_windows/src/events.rs": "c77bb07fee7200e61d7543e8515d8294ef9f2e3364e4deb5b6da10dfce504358",
"third-party/gpui-fast/crates/gpui_windows/src/fast/class_names.rs": "53c3a53d7afdcd7d14dbad534d07751ab23a8dce696b882fe504f050c00890e8",
- "third-party/gpui-fast/crates/gpui_windows/src/fast/composition.rs": "8bba63b262e77d5e47e598ef437926b1aa0f4f571f14e79cf516d2126024eba3",
+ "third-party/gpui-fast/crates/gpui_windows/src/fast/composition.rs": "dec26761f2e55f80b3cc201ee6c8cc3b19bd4061a783fdf16bcf0b1c52edef47",
"third-party/gpui-fast/crates/gpui_windows/src/fast/draw_state.rs": "aaf39581fbc365fc7832d65ead8c2ef2a7922e98a5552918ae78b105db657c61",
- "third-party/gpui-fast/crates/gpui_windows/src/fast/frame.rs": "5ef34a60a1d8b848b1724d85387e502469bf526e241287eaeffc6fe400cbf76c",
+ "third-party/gpui-fast/crates/gpui_windows/src/fast/frame.rs": "76d9724e33526d01fc47c3b9938852440e4bd5d54d31933340b94e3483348110",
"third-party/gpui-fast/crates/gpui_windows/src/fast/globals.rs": "00a46a726d3d3161728e89374fc29ba012235cf1dc4db18cf3e47036337bfd31",
- "third-party/gpui-fast/crates/gpui_windows/src/fast/layers/composite.rs": "5f4b6ee509646797df5fcf5b114abd7e6e968fc23bce43ba140a939cc80e77a1",
+ "third-party/gpui-fast/crates/gpui_windows/src/fast/layers/composite.rs": "6f5290c01e9796d297c75793dc71868f0f504503ea150c0e48d7b1c059c74bdc",
"third-party/gpui-fast/crates/gpui_windows/src/fast/layers/mod.rs": "92912cd1ffe70202a45998cdafceee4a69dfd64421f349a948b24df74b066e53",
- "third-party/gpui-fast/crates/gpui_windows/src/fast/layers/raster.rs": "82f788b7315fdb7a6e56426863884fb384f7dd7ee6a96e1236092dab0380ad1e",
- "third-party/gpui-fast/crates/gpui_windows/src/fast/layers/tests.rs": "b62f3cbcae112a02169e9c852da53efbdf3704c4d1dce54c0faa31fbd8453f76",
+ "third-party/gpui-fast/crates/gpui_windows/src/fast/layers/raster.rs": "44c7171efffe1076775671c9745122703f777e95262e513914827caf4c05c2d5",
+ "third-party/gpui-fast/crates/gpui_windows/src/fast/layers/tests.rs": "9f24e3f2c4afe035ca11e426ee785c1edcd22b14cbd1ae5504545b9afd8cc7f1",
"third-party/gpui-fast/crates/gpui_windows/src/fast/layers/tile_cache.rs": "f7d7e78bc48f632f53b18ea18b7bfad111a95eceed16ae6e4f399af397ac244e",
- "third-party/gpui-fast/crates/gpui_windows/src/fast/mod.rs": "2c2a6b7e1e9f167fed7d91be5cd70a5c45d0bd24acaddd8f3cae636541c53a5a",
+ "third-party/gpui-fast/crates/gpui_windows/src/fast/mod.rs": "bfba1b21ef505a9fa8f2edb727969641533d86f87d77f29917f979a83b3faf68",
+ "third-party/gpui-fast/crates/gpui_windows/src/fast/rounded_clip.rs": "08ece3ace4634b0c99e025a2afb41b6b990e2f18687ae47042cd429cbd630c55",
+ "third-party/gpui-fast/crates/gpui_windows/src/fast/rounded_clip_shader_build.rs": "a3903edc52ca9ccb1e589db317de234901fc908101a3955dfe33728b7eaca8ec",
"third-party/gpui-fast/crates/gpui_windows/src/gpui_windows.rs": "cc3fd2d5b1799e4535694255d598001c99120f7b67f58ebee7af177d4db39753",
"third-party/gpui-fast/crates/gpui_windows/src/keyboard.rs": "4e373ce1a3f23b6527b1abb3051b59c96d5cc2b0f777d4d4097dd27dd1673516",
- "third-party/gpui-fast/crates/gpui_windows/src/platform.rs": "30756b536207bbe4c1962c38232e9c1f1a99f2ab2973a4a2f58a4355ffaef97b",
- "third-party/gpui-fast/crates/gpui_windows/src/shaders.hlsl": "9f45956583cfef19558315789afe433309cb96896279035703a6b782faab632d",
+ "third-party/gpui-fast/crates/gpui_windows/src/platform.rs": "050a6d64e47a5ab7747c9b54df001c5708004ea36268756f54fc0de9dbeebc7e",
+ "third-party/gpui-fast/crates/gpui_windows/src/shaders.hlsl": "376b89937d9afeb86353c81d06e38c005976506f436290e5ccc9442d56cbe752",
"third-party/gpui-fast/crates/gpui_windows/src/system_notifications.rs": "d0159dbf6fd10622a5b106806ea2f57b3fe983eb6e2fc8635ab38bdf6529879d",
"third-party/gpui-fast/crates/gpui_windows/src/system_settings.rs": "a9d22571c2405f610d44b85d86fbcf0356f7cc6aeeffad269a534b42eeffbdd9",
"third-party/gpui-fast/crates/gpui_windows/src/util.rs": "e9bdd79c80149f7d60d2a35f4e86398343762a6c2549fd67de1de4de9ab65147",
@@ -778,14 +785,15 @@
"third-party/gpui-fast/crates/scheduler/src/scheduler.rs": "0ac23422e37efebc6c5256488cff320c58c3aa67e007e5bd2ab407e891717b71",
"third-party/gpui-fast/crates/scheduler/src/test_scheduler.rs": "0694ea64950398b3fa6710ce84284f63dfef8bd38bc3b29e21fe84e16ce8f329",
"third-party/gpui-fast/crates/scheduler/src/tests.rs": "fc268a8ecfe19e00833c060ccf7b8b246a692bdc3e04727a03b6d39ac011d5fa",
- "third-party/gpui-fast/upstream.json": "1d356a88dfd9c2cbab305e00bc18d5b8b3fd4aebdfc164559935f4a9084225c0",
+ "third-party/gpui-fast/upstream.json": "7b9c0d7019c4717857ff706efddb0931341d6d7dbc759f75c22b7247d4080c14",
"third-party/gpui-kit/LICENSE-APACHE": "d1b0449e5478c574ba4f686c2656df7fe77d66821a61f8b6ed3378a58ed9a811",
"third-party/gpui-kit/LICENSE-DOCS.md": "8a9ccf144b2d5bf5aae56abdc6e723e17e1ba4de1f5d515b3b644288fd4fc16f",
- "third-party/gpui-kit/README.md": "7ed4da0c4b942614fcef5de14337879155c815d6ae51c36af1364634fb165a6c",
- "third-party/gpui-kit/SOURCE.md": "4f8b2283013f30c1c64c4d4f8e279db0941b8d1c93cd1a02a02ceb4ad87a0ade",
- "third-party/gpui-kit/crates/base/Cargo.toml": "715deda4da1f9d8ce5de6ead801da7428747e1900535ba333e90db73181a966b",
+ "third-party/gpui-kit/README.md": "5954b9e237e78df5384a7768f3b2c0921bc07ceab10bf4b4cb33767c8968ec06",
+ "third-party/gpui-kit/SOURCE.md": "dd924cc1bbd32e4aab37925c00ad4daf9dcc35075286739126066ca8ffded1cf",
+ "third-party/gpui-kit/crates/base/Cargo.toml": "05e139a764f7a92240a852bd75d187e5dd244170c24bbf52d94d09d126dcb5a1",
"third-party/gpui-kit/crates/base/LICENSE-APACHE": "d1b0449e5478c574ba4f686c2656df7fe77d66821a61f8b6ed3378a58ed9a811",
"third-party/gpui-kit/crates/base/README.md": "8c1f0ce117fd98117f6733ab4f8a1c07b9f5723f8623b60b988754c00cb40d61",
+ "third-party/gpui-kit/crates/base/src/a11y_disabled.rs": "979c0103db88f31fad9425ca6fc16351bdb07a5b5b0fe30a85d2aaec573bcceb",
"third-party/gpui-kit/crates/base/src/accordion.rs": "3196365518cbcff7d688317defbe7443518d03415f1f8d894cafbd0bafda3f0e",
"third-party/gpui-kit/crates/base/src/actions.rs": "66f57a6e5c8eb1e4a903880225efb48abd8eb885c8dd37ef2dc77071571d7659",
"third-party/gpui-kit/crates/base/src/alert_dialog.rs": "ad1becbd68ec0fc05c718de6181135d3b09a7d91f4cf00a833a6f88f9cf660a5",
@@ -793,14 +801,14 @@
"third-party/gpui-kit/crates/base/src/async_util.rs": "c40cf6c230f9f22c613a2d50c790a1c7edacc47131742412e2b5c757de34f713",
"third-party/gpui-kit/crates/base/src/auto_scroll.rs": "cb61a0b4f81dc05f3602a966f66deca008946bfa59ff4124011ccee8fa323e48",
"third-party/gpui-kit/crates/base/src/avatar.rs": "aa13d83fa33a99c7e9045f8b10633648b3166569a20421b7a227963f895307f3",
- "third-party/gpui-kit/crates/base/src/button.rs": "ff3026d04f12afe3e6aeb3cc3d95b25c5ea6eb65884258fea1b973ad6d08255e",
+ "third-party/gpui-kit/crates/base/src/button.rs": "b000a1e05c22f63dc41cd027d89a0c78472424c9ff102503eb4faa31a7d4bb33",
"third-party/gpui-kit/crates/base/src/calendar.rs": "23f747d81667c1f68a54ca827863d556e167fe500a25eec4049d1c90a7d3cd6e",
- "third-party/gpui-kit/crates/base/src/checkbox.rs": "faa48acd7e1243ff661b542d753a6aa1327f76058837edb4cc00b3f469409883",
+ "third-party/gpui-kit/crates/base/src/checkbox.rs": "36bd84c9815c350aff5b108eb2126006c495459fc5aa8b2438e6b64beb9ca8c4",
"third-party/gpui-kit/crates/base/src/collapsible.rs": "cf04f69ad0f34ef2e6513bb7c66b949c04ef85e67e46bda2da60fd652d9ebca6",
"third-party/gpui-kit/crates/base/src/color_picker.rs": "b045024a8bea202115ddbfd6eeabaf357da9a1be9de1db8268f1e0d89809fd26",
"third-party/gpui-kit/crates/base/src/combobox.rs": "72d0e7e24d9433bd99665d613cdd637462fae11039c79f218bf2649ec19e5ddf",
"third-party/gpui-kit/crates/base/src/component_traits.rs": "0cba18cbcf88b94b32c7c18b9cabab03137c22d69a6b53dd143a32f86b5cbec9",
- "third-party/gpui-kit/crates/base/src/date_picker.rs": "995c7e32ad660e7f1e96d1c429e90374b6479bc9cd8364568683c8305bc8715b",
+ "third-party/gpui-kit/crates/base/src/date_picker.rs": "7d273eef6b1e5d189a84a9752e825d1bd27513e63d386bdef364c9697f08d4f0",
"third-party/gpui-kit/crates/base/src/dialog.rs": "93a5bbc13bed9d7ba8f5ba81b3a1b51f947b043d2fc1f57cc3d31c74c53fec01",
"third-party/gpui-kit/crates/base/src/dock/active.rs": "a191ccbd9bf6ed8304bdb05d957b8514f4df696e5c4e959a50e4384463b31411",
"third-party/gpui-kit/crates/base/src/dock/dock_area.rs": "e2cf018266a3fa577d6dac4baba1a933cc0a6faca095d689ada69992e5555681",
@@ -808,8 +816,10 @@
"third-party/gpui-kit/crates/base/src/dock/drag.rs": "8204979a0f2519bf94c17f48ebda4c49bddd3c090b82d5edf622257a8d607b5e",
"third-party/gpui-kit/crates/base/src/dock/fixtures/bare_tab_panel_root.json": "3e6a100a3870a5b2aeac1d14eb312432515cc9ae529253bf9be13e8c48179e04",
"third-party/gpui-kit/crates/base/src/dock/fixtures/layout.json": "dc90df21089188bb050b497d216999b6c92a484cc2b16d95daa4a98bcb4aed9d",
+ "third-party/gpui-kit/crates/base/src/dock/fixtures/legacy_empty_stack_panel.json": "cb73d04fe7e52d69d6cf807a75683783f912381700155eb5282c223de22dd87b",
"third-party/gpui-kit/crates/base/src/dock/fixtures/legacy_empty_tab_group.json": "681a074ca743f088db86a7c74ee7998315464c00da565215680411184cc2d398",
"third-party/gpui-kit/crates/base/src/dock/fixtures/nested_splits.json": "3845cc07bf4d4e16bbbb4f44d2fd2316a39da7cc72300a5e9e6b553e5d8c30a6",
+ "third-party/gpui-kit/crates/base/src/dock/fixtures/placeholder_wrapped_empty_stack.json": "92ae101fafe1b4657f671cff30dc5d7aaaa7fce3bfb1c5687dde3bb724b00abf",
"third-party/gpui-kit/crates/base/src/dock/fixtures/unregistered_panel.json": "209c37364a6506a9e332247bec8583e03fd5039fbf521f3a44e141652b8cfea9",
"third-party/gpui-kit/crates/base/src/dock/fixtures/zero_size_sentinel.json": "857fdae65b5d7a66226db9495f77b53c8903c5d541b791260696a47f212aabf6",
"third-party/gpui-kit/crates/base/src/dock/layout/builder.rs": "a75985cd04da4a50bcfad9faaaec26432d8fb71c9997a6c67c031701965dc171",
@@ -822,22 +832,22 @@
"third-party/gpui-kit/crates/base/src/dock/panel.rs": "b29050fcd827f649cf813cc717d115c24853008e99a08d83190af553c92610ce",
"third-party/gpui-kit/crates/base/src/dock/registry.rs": "4b53802c3add781065ac6af6cc14170529e9db440126dfaddcf9b3bf6ee0e668",
"third-party/gpui-kit/crates/base/src/dock/state.rs": "90cef9125c5f75a66066e07ce8c6f6c07504bbc77d08c004fe7d7e37acdae0a3",
- "third-party/gpui-kit/crates/base/src/dock/state_convert.rs": "7992cb0a32ac530ed782559342851eea21a439985ae642e7f2c060ccfc02f3e8",
+ "third-party/gpui-kit/crates/base/src/dock/state_convert.rs": "3f7fe6c91fc65164d22de610c0b4dd24c6f9e48e922169c7242858a5517f0854",
"third-party/gpui-kit/crates/base/src/dock/tab_group.rs": "d2073a2222fe3158736cea57821b97a5fa938a30ecab87ee8565e85c8afda0c2",
"third-party/gpui-kit/crates/base/src/dock/test_support.rs": "80aaa7a90973577efcde89e33e08d194c8932f11434e8d2e12179145dc98b943",
"third-party/gpui-kit/crates/base/src/element_ext.rs": "f5465fcbb872f0e2f7479a0129f96084e0439d362cbe069a877bedd4cd35e1e3",
"third-party/gpui-kit/crates/base/src/event.rs": "1485d874cf6e45f515ee8d2d497c0241af452f51ca262fbd9b68a15a52455fca",
- "third-party/gpui-kit/crates/base/src/focus_trap.rs": "ab9a02963d8562e73e85e09652e70fa7acc4c03bba67f89d814f52b03f9a5b97",
+ "third-party/gpui-kit/crates/base/src/focus_trap.rs": "10fe3e8516e80bc2948c11ba76f60e9661802dbd0f9baba8dfe1e06635120f08",
"third-party/gpui-kit/crates/base/src/geometry.rs": "ffe99efcf2dad27387e030ed2576a695031ea1dbecf30fb5f8f5ce315ae941e0",
"third-party/gpui-kit/crates/base/src/global_state.rs": "d9351126b89a53ebcc0c9e8a4346d5a47a1ac59e28145574318f07f121684f88",
"third-party/gpui-kit/crates/base/src/history.rs": "7312a757d19fe5d4e62c4b8b9db65421603224526ab2edc1545ca2b17f166abc",
"third-party/gpui-kit/crates/base/src/hover_card.rs": "94f35f91b6a518b8c80ff6383c1c0fa1ba7cf676c82c0bf705fee4431e6d9a68",
"third-party/gpui-kit/crates/base/src/index_path.rs": "7e28ff5c5ae88fe94311c19732aaaf420723ad7ed299d70a8ec94231a47164fc",
"third-party/gpui-kit/crates/base/src/input/README.md": "09a9aa264b3f1c19d3a45c7d8c26202fecdd3c1d3ad11f4cc7f217af2751996c",
- "third-party/gpui-kit/crates/base/src/input/base/blink_cursor.rs": "7abc8f62bc3d4823986a25155a13bc57ef57b12f96ae163938ef353a4c6f487a",
+ "third-party/gpui-kit/crates/base/src/input/base/blink_cursor.rs": "e2cd5e37849e683d596bf85528def80a84203938eb09bfa8498680786eadcb04",
"third-party/gpui-kit/crates/base/src/input/base/change.rs": "7555a8bd9067a7f19248c2140c9836ea0f68ad80640210b95e886b01b91b9599",
"third-party/gpui-kit/crates/base/src/input/base/cursor.rs": "05cca82894ddef125cfaaa24d92d72af62529a2255da26aaffc9b134bbc79413",
- "third-party/gpui-kit/crates/base/src/input/base/element.rs": "b9c2880a1f0f3d5d356c0686399d89844ae4995b133cb13ffa2d4fbf2a410ed3",
+ "third-party/gpui-kit/crates/base/src/input/base/element.rs": "483c56af00f4f6e564e8d63066bde7bcf1aca1cba5682329fa57aaba6bf63393",
"third-party/gpui-kit/crates/base/src/input/base/inline_tokens.rs": "5eda0fba2a965aa48e04155048fd4034f2741b846ae330632e4ea0886c254bc4",
"third-party/gpui-kit/crates/base/src/input/base/kind.rs": "eb518388f148a514e599ef4ae99f037ac64033fcffbeda7ca8541462689dbb15",
"third-party/gpui-kit/crates/base/src/input/base/layout.rs": "08cedc8853fec2f61be0d0a3af980cb8bac8f35c1fc97838fc96c1bb09926db9",
@@ -848,7 +858,7 @@
"third-party/gpui-kit/crates/base/src/input/base/native.rs": "8fcb188c4027f73756e69a7e885d18d150e8bbbe2d5d6cacf1bdf51cb87f904b",
"third-party/gpui-kit/crates/base/src/input/base/rope_ext.rs": "07c2ca02bb8413411f9c67c6f03033fd80e259838842ba539f4f12a285ff6d8a",
"third-party/gpui-kit/crates/base/src/input/base/selection.rs": "e27dcde643ed47e68e150f531ee37523f3b396a2056f94c7da60077f9fef40b1",
- "third-party/gpui-kit/crates/base/src/input/base/state.rs": "180c42e74612c035ebcf7421942c45091a3ae9c2d635bd4a76b922fca32381cd",
+ "third-party/gpui-kit/crates/base/src/input/base/state.rs": "1a55e9e3982a92fab7d47b86ff2df2e02263fad1c1b5e3f747b9f126a47d16e9",
"third-party/gpui-kit/crates/base/src/input/base/text_accessibility.rs": "56d064d33cadfdcd08a8797db4f1d2ebea065b5caece9b930091af719f46765f",
"third-party/gpui-kit/crates/base/src/input/base/token_presentation.rs": "38258e52bad19102873d932768ba4b00abff93feeae5d3566b04093756fd577d",
"third-party/gpui-kit/crates/base/src/input/base/touch.rs": "630fc8e95799067899f3de33ad43b7a96c71c940b1e62d9f941c3f2aae569df0",
@@ -857,7 +867,7 @@
"third-party/gpui-kit/crates/base/src/input/editor/decorations.rs": "8c2baa9faa161ad0bc1f35596d77d45ab25ad80df481392f4468c3cd19872809",
"third-party/gpui-kit/crates/base/src/input/editor/diagnostics.rs": "1132594168d966f66ea4a49c307ba49fa91c70f5a9176218bd4bb618c8dca5dc",
"third-party/gpui-kit/crates/base/src/input/editor/display_map/README.md": "6c75ec64ed126d747a343afd0d480805576bc19228adca021b7de013a5243085",
- "third-party/gpui-kit/crates/base/src/input/editor/display_map/display_map.rs": "30458f68964bb6a8d942ae8531cdd0d8fde1671dbf1914701d8be0aafea2f759",
+ "third-party/gpui-kit/crates/base/src/input/editor/display_map/display_map.rs": "28202732dde11f8978d3a27292e3bbfc2401dbe43ccd86e88a60c1dbf78fcee9",
"third-party/gpui-kit/crates/base/src/input/editor/display_map/fold_map.rs": "90700a951cbb1d42cdbe24ee96a26d2d71f18643bd6c72a7a16844538685751f",
"third-party/gpui-kit/crates/base/src/input/editor/display_map/folding.rs": "088ed38893a508cae48a7c504070fb982be01ecd4661ba7e19186bdb14c82b92",
"third-party/gpui-kit/crates/base/src/input/editor/display_map/inline_line.rs": "b277ea0878886b010de8dfe52162848468f2ebba844df397f4ab43c8a10437e6",
@@ -869,35 +879,36 @@
"third-party/gpui-kit/crates/base/src/input/editor/language.rs": "4f95415cbc0f2a2fe9e5ac7a571f23daa0b08336f7bf355709d63607ad4d94c9",
"third-party/gpui-kit/crates/base/src/input/editor/language_config.rs": "6fc15fe185b8a15d79e3dddf92f1bb3d9bd3fd482767a30c26ce0f5dd8c030f7",
"third-party/gpui-kit/crates/base/src/input/editor/lsp/code_actions.rs": "f7218b634f357e79514932980b2367f9f65b2c6f18e864f1d09097bd06d04d3e",
- "third-party/gpui-kit/crates/base/src/input/editor/lsp/completions.rs": "a53b58f9a81f51912425541a813a2812c5c80484c7e4d5027952ac5164d7632e",
+ "third-party/gpui-kit/crates/base/src/input/editor/lsp/completions.rs": "38dafa9ec0016aca685a09e5f903742919ebd0f8a146bdf5ee48594596e2e416",
"third-party/gpui-kit/crates/base/src/input/editor/lsp/definitions.rs": "4eb240024336cb90b888eb2404a4d95a38d6e563992e07b8fd904b3e1db7cf22",
"third-party/gpui-kit/crates/base/src/input/editor/lsp/document_colors.rs": "189a6d618d5f8ae88d82e5bf0dff85f0c95b7c03209230ec61091dd4e96229bc",
"third-party/gpui-kit/crates/base/src/input/editor/lsp/hover.rs": "c8e0ee17fc0b886099db65cf16497fc660edb04860e96e7f2ca8f37074c5aa63",
- "third-party/gpui-kit/crates/base/src/input/editor/lsp/mod.rs": "4eab79a6ddb3f1e929f754f852fa45f3dbf860b47114da5efedac7a018d0a61d",
+ "third-party/gpui-kit/crates/base/src/input/editor/lsp/mod.rs": "3f9ddbb6dda84392a84dd4dfb6ef527f4cd5bfea5240d6087615788c8948b56f",
"third-party/gpui-kit/crates/base/src/input/editor/lsp/overlay.rs": "deccb036968f152caa115b408bb5a3ad2bc49bedaf4001e39bb62a3a42c9b5e9",
"third-party/gpui-kit/crates/base/src/input/editor/lsp/semantic_tokens.rs": "c81764d47de5cda2dc1b8de670f0e0fbe78f2f984f2a67d436286b6da13b0ee0",
"third-party/gpui-kit/crates/base/src/input/editor/mod.rs": "a407e3492c701ba5fba3e648e4381357ef0767e64e065ccf149b648567c1af13",
- "third-party/gpui-kit/crates/base/src/input/editor/search.rs": "9e2a8495136ce81b8176a2f5121cbbc24d6d9427e6f49549a3db9fad88a95c61",
+ "third-party/gpui-kit/crates/base/src/input/editor/search.rs": "4e793f8ac73c2531eb0cb067d6d56cd8cc6c1c8b7f17a68ce4f4f2970bfaa92c",
"third-party/gpui-kit/crates/base/src/input/input/mod.rs": "6795120ffbd098bcfe1bfad635615eb0133551af31ae22d429fe6a2945e4680b",
- "third-party/gpui-kit/crates/base/src/input/mod.rs": "6be0b33073fc11938958e8d12c86f9837233d536103c6cc22409a14833a855bc",
+ "third-party/gpui-kit/crates/base/src/input/mod.rs": "ef37cd8b93d28e082205516302bf42a2e93d3d4baf14fd5d2b9a670c18e27b8d",
"third-party/gpui-kit/crates/base/src/input/textarea/mod.rs": "55e95879a0bd3a6d4f717e35b5cc93151ee03757f007d5fd7e28638acb2f69d1",
- "third-party/gpui-kit/crates/base/src/lib.rs": "d8fac4236dce7b137476e19c440243847bb949727aa3f0abd71386632ffc1e8f",
- "third-party/gpui-kit/crates/base/src/link.rs": "67828689cb912cc06f655ab4cf9ee96d500a222bb226080618ac90d5116fcf56",
+ "third-party/gpui-kit/crates/base/src/lib.rs": "eea70030cac5c7724d4626a5824e58099b13a006a427299a44500bdba8e21796",
+ "third-party/gpui-kit/crates/base/src/link.rs": "33644ecef3506038c4b0020c21b3c1906986bfbfc9493b20b4e6eafdb8d5f3ae",
"third-party/gpui-kit/crates/base/src/list_settings.rs": "72dff3e527aaf963d839babbbde99c78291c07ff5d01f86f24a39782cd9d6f1f",
"third-party/gpui-kit/crates/base/src/macos_accessibility.rs": "089d673a24a155d7bac2cec3129b535a680cea4970c5cfb32da0e0b8b228b6af",
"third-party/gpui-kit/crates/base/src/measure.rs": "ef0c897ee836610c618ce93ba6f288b8b53021c336c04161fc2cd81111ce7484",
- "third-party/gpui-kit/crates/base/src/motion.rs": "61003a5ff87a084171ae0efd476d7418924c3a26d562d4f915415b8abda00d3a",
+ "third-party/gpui-kit/crates/base/src/motion.rs": "1188835be06b414ecd3dfae4173cac62d56597cbbb3272631c96a435d452468a",
"third-party/gpui-kit/crates/base/src/motion/easing.rs": "156d4626e17592ca614b23597ddb976059cdebb7324e682dad83ea61205dbf73",
"third-party/gpui-kit/crates/base/src/motion/keyframes.rs": "49c5162ce6dd28904cf7d339677f30bc91b73a640d2291227e425a32623c9cee",
- "third-party/gpui-kit/crates/base/src/motion/presence.rs": "4cf8b79b1753abce7ccd0755af09698484c272fa27b861c8602c2c8f2e2aeffd",
+ "third-party/gpui-kit/crates/base/src/motion/presence.rs": "29f42bfce53fd7c2034b92e6700b0f68f2aea9633e2421f64042deb5272ff039",
"third-party/gpui-kit/crates/base/src/motion/reveal.rs": "20795e5eae575d1d778a9a3dc77930a28be35a9532fb9059a131a5616294457e",
"third-party/gpui-kit/crates/base/src/motion/sequence.rs": "d3284d45440e57ebcef176be8101baf76e35f1369b4d7f21c2b839b79d9e601b",
"third-party/gpui-kit/crates/base/src/motion/stagger.rs": "e5c877c3def8a34626d551e65f3b4291a7f5bcee92072ce0f619b7d0e4535221",
"third-party/gpui-kit/crates/base/src/motion/timing.rs": "81e652d374cfaeae2304a44e9d600a86ae2460fe54732577b6e9ecbe613bf047",
"third-party/gpui-kit/crates/base/src/nav_stack.rs": "1955873d50a84b8dd9be4d33db64c93fe1ffba332298117f958111eb223da27b",
- "third-party/gpui-kit/crates/base/src/number_input.rs": "b3883ee45450ad3168eade9c83d4c31662127e7171d26f36bad8cc7b43314bce",
+ "third-party/gpui-kit/crates/base/src/number_input.rs": "ef937e1764c170a12d19377907835087b16e83e2cad78b3967f6dc499d92dedd",
"third-party/gpui-kit/crates/base/src/observe.rs": "802689740fc842fb087e3f9502859ac847c90f6905eb3977e856c7bea2a37f8d",
"third-party/gpui-kit/crates/base/src/otp_input.rs": "39960e7b4315955cafa9f0808efb89f1d60ca34cc0d3a058828461639b60a7b6",
+ "third-party/gpui-kit/crates/base/src/overscroll.rs": "0efa958c3b0becba0755b88a838204a8bc620f79b9758299f863fffcad573272",
"third-party/gpui-kit/crates/base/src/pagination.rs": "23259096e34ee4becb61489e3ae62b65dd24dd1a3db2b69bd5cfe07a595cf23b",
"third-party/gpui-kit/crates/base/src/plot/appear.rs": "f1d17352bf5ae10b9f7980e54a0b9e6471bdb2c312e48e4aafbe013488b1fa25",
"third-party/gpui-kit/crates/base/src/plot/axis.rs": "218632b939f971712ba28560cce14328d77c105f58bbb3f7cabba172e30e7499",
@@ -922,7 +933,7 @@
"third-party/gpui-kit/crates/base/src/plot/shape/radial_line.rs": "0656a11e1921394e6d4895d4243cad4e908045bda8c9f63e9bcfefa9e8062097",
"third-party/gpui-kit/crates/base/src/plot/shape/sankey.rs": "a25ecd363d5c5ace86ed97977616634f4753f99e7d9ba1403704f63a1c3c9cf9",
"third-party/gpui-kit/crates/base/src/plot/shape/stack.rs": "372baaa75805bc6107cc2942e9f4b6611a1e6831597546800e90358373b93c16",
- "third-party/gpui-kit/crates/base/src/popover.rs": "bd104cb093ece9bb5e2c0f485dbae4ddf273f4fa502ff77da3f3038430de5809",
+ "third-party/gpui-kit/crates/base/src/popover.rs": "e145a6eabe5b82ecda3a65470c87a47068316472ef416414668bd2e810e474a7",
"third-party/gpui-kit/crates/base/src/popup.rs": "b112fa6df512dff23bbd0cdb373fc397743a1f7fa50fa42649f2d4cb35c9ea98",
"third-party/gpui-kit/crates/base/src/positioner.rs": "445518efa333ffa5a7d2caff69bc7a2620c3f48fbef1b7cdcbe6d04d0c2ee79b",
"third-party/gpui-kit/crates/base/src/progress.rs": "76c6410940ca91ee3c4909f901bd945e01a671af5eddb6d6f0cfdc8983a421cb",
@@ -931,69 +942,71 @@
"third-party/gpui-kit/crates/base/src/questionnaire/mod.rs": "0ffa8e8632d53a893a4ac25de142f86ba3bd2d3f52e04a27c10251e9746fefb7",
"third-party/gpui-kit/crates/base/src/questionnaire/state.rs": "a8c231c29622172662a4191e25aef82173f3bf18e028cc46703464a04779e99b",
"third-party/gpui-kit/crates/base/src/questionnaire/types.rs": "c0c41591fc90752031ca7268e3543e1404f8639825ebe6ae1d23e4b26d45c392",
- "third-party/gpui-kit/crates/base/src/radio.rs": "9116fbe206e2488f298e0baee0009f59f5a997c24cf1604089b040e9e2e04ace",
+ "third-party/gpui-kit/crates/base/src/radio.rs": "4ccace0c81b96a3e6c9b6806d91e8ef384a758638a844a625ea743cd3654fcb5",
"third-party/gpui-kit/crates/base/src/radio_group.rs": "975b427dab521ba1c5ea8cbb3046ca5c96da14f2d6897fdd0aa6e2efdb920672",
"third-party/gpui-kit/crates/base/src/reduce_motion.rs": "eb277973b7a8e75e56e83a79d184fa2e6484aebef41543f32f5cb821236554fa",
"third-party/gpui-kit/crates/base/src/resizable/mod.rs": "b8d4db1f561eaa71490d562ce331bc743d61f00960f54d1db437ccdcf3276726",
"third-party/gpui-kit/crates/base/src/resizable/panel.rs": "f49a4bf1c47c8c747236e41e5468d530618551373e9713f3a900cb7d7d692444",
"third-party/gpui-kit/crates/base/src/resizable/resize_handle.rs": "ed277ce01b1c8113c95f938ed1760f59c0cf74ed4421d682f95b5c01a3e955b8",
- "third-party/gpui-kit/crates/base/src/root.rs": "919e4693e3b94521a9c118161c6f809befc5536697a2202e8a3cff098c1070cf",
+ "third-party/gpui-kit/crates/base/src/root.rs": "93ad8ef26b262767b8a1922a7a229119bb1f854f22ea4f7ed6e0425ec10e763e",
"third-party/gpui-kit/crates/base/src/root/click_target.rs": "6bb620ba876701cdf5465d428faf8b684e20927fcea8a57194f46815d43391f3",
"third-party/gpui-kit/crates/base/src/root/focus_reveal.rs": "f81d580b7e10c8320df104b1330f7ff994eae05589a5882c8cbf701ccb12e16e",
- "third-party/gpui-kit/crates/base/src/scroll_bounce.rs": "b568b604054d4735ee652e52ba51cb80b518f6b934915dae350e6a62cbc5e424",
+ "third-party/gpui-kit/crates/base/src/scroll_bounce.rs": "c0d7858754319515ececaa1b3ffba0f741a213a394b26ebc0219de8817696f14",
"third-party/gpui-kit/crates/base/src/scroll_retention.rs": "3fb8fd96ad6b68211a268f5aa6ddc4ffe2e181a537e24a10e7670dc372e06bcf",
- "third-party/gpui-kit/crates/base/src/scrollable_mask.rs": "4e289b6f22563f43821fb52ea0bb09ef5a930c9f94f09d2529624c585e04dd2c",
- "third-party/gpui-kit/crates/base/src/scrollbar.rs": "62d38a07d0127a989cec8e083d02d084f662d32ad947c115f3b818e8add8ac09",
- "third-party/gpui-kit/crates/base/src/select.rs": "d7c9bc2a8c5e3b74fbd37ea3a9afadf276f317403bb4a2938ea4eb2b69cedb69",
+ "third-party/gpui-kit/crates/base/src/scrollable_mask.rs": "ebd75f948c3815d66b76aa96e6f5f5569b9d3a064381d7f5547976b5b332057f",
+ "third-party/gpui-kit/crates/base/src/scrollbar.rs": "7f38fc4e5a5020c30920f3a6e0e071f02689da038b0d2bd1627c3e212c2aa436",
+ "third-party/gpui-kit/crates/base/src/select.rs": "6022c14be71dee973962c104b7c7f37a8fe0c2df783f7cfdd963aa01880d047c",
"third-party/gpui-kit/crates/base/src/selectable_text.rs": "3f740daba291284e6a234380e9b7c22bafd6f66b60086755dac3640691cf265a",
"third-party/gpui-kit/crates/base/src/sheet.rs": "7a5f84d671a9aebb9a5f4afce688119ed05ea4b3648828ef79168aa806db6515",
- "third-party/gpui-kit/crates/base/src/slider.rs": "3835782f061d375d87644e668df301fd0b786275dc51259fddba1cbae4fb1d6e",
+ "third-party/gpui-kit/crates/base/src/slider.rs": "71b801a92588d12f7931a5a9e1745fb62f4283ae87d651d4004ae9b41ad72da3",
"third-party/gpui-kit/crates/base/src/state_style.rs": "f7e1f85c0215e3cd84df4b6fbce34938a33d275c9db392bdc9025670454760b1",
"third-party/gpui-kit/crates/base/src/styled.rs": "7c1e88964cc3d76968609e6ac24db0c3698c87f9b138ef975d3776eac81dbd87",
- "third-party/gpui-kit/crates/base/src/switch.rs": "d06394441975ac7eb4478d353762c527ca0e9d8034c7f5097902d9d976543567",
+ "third-party/gpui-kit/crates/base/src/switch.rs": "b4cbb590a304f3fb25a31d99f64fedf6ceadd51dde477e87ffe5e837b0659712",
"third-party/gpui-kit/crates/base/src/table.rs": "b9101b8ff59b1514955e20ddb111fce7d8cb81cd27c170eb1fd77bcdb9512ad6",
- "third-party/gpui-kit/crates/base/src/tabs.rs": "e3fcdaeaa72f6d4b34e30aa54bfb0a872e92e32bd3d093382bff0fc3bf1583b8",
- "third-party/gpui-kit/crates/base/src/test_support.rs": "6be2a2fac0504aad624fce074d61c227d95e664b88384e7ed6fc11b9cab6ab5a",
- "third-party/gpui-kit/crates/base/src/text/document.rs": "cad6529dbb35e892c117c748b86198cfab37082427a81205619b140ec4df3ec2",
- "third-party/gpui-kit/crates/base/src/text/format/html.rs": "541954ff4cde6062bc243dd93c30ff0ae12f76fe83d3d19eeb73226edab8c08c",
- "third-party/gpui-kit/crates/base/src/text/format/html5minify/mod.rs": "201963ab9c300088dda2c8db27f28d996fc670d34cb456c837ed4f3e603363e4",
- "third-party/gpui-kit/crates/base/src/text/format/markdown.rs": "55de3381b1e428d9f5ba5ee8bdbf8a7b496b9ba1a100d8cb741a4bc22587a68a",
+ "third-party/gpui-kit/crates/base/src/tabs.rs": "301dce5f2715fbb9fe33dce6e7a6e2b2fe40d4fbab45ba9defe3a0c7e2bef141",
+ "third-party/gpui-kit/crates/base/src/test_support.rs": "439c16c9f332cee4dbac2bbf588630543fb41bcbfce04f9c81823cefa017db24",
+ "third-party/gpui-kit/crates/base/src/text/document.rs": "d72db0b3f2c6a1bd42956ce8f346feb2237f6566fdf110834880fb7a82e1dd3a",
+ "third-party/gpui-kit/crates/base/src/text/editor_host.rs": "100e576fb839f635f258e576790d05bbea8b1dabb9e97148fa6469da3f6a6b08",
+ "third-party/gpui-kit/crates/base/src/text/editor_seam.rs": "d10d86c056d6792f5d1fe904efc410c97830d77d3dfb555867a371182fa1e101",
+ "third-party/gpui-kit/crates/base/src/text/format/html.rs": "0831c7ec41d74ca526cf40a5b667bf63eb15cf86bd1bf42351d98632425d6ee9",
+ "third-party/gpui-kit/crates/base/src/text/format/html5minify/mod.rs": "139ae3bdc3c5a1d35f2c7439183b1f2aafc5514ced0e1a2f89917098cdc2d714",
+ "third-party/gpui-kit/crates/base/src/text/format/markdown.rs": "a929a1f7788513ffbba6589654a99f9b578dcc102c9524679441593c1785baa2",
"third-party/gpui-kit/crates/base/src/text/format/mod.rs": "a9c352dd0d58f53f6dffe14568378bc481c8352985b2653b0ad7dcea67f72ba8",
- "third-party/gpui-kit/crates/base/src/text/inline.rs": "415628b573bbb0f39671a9557422d479bd36f8ac234da63f31a5d2ac508e1fa5",
+ "third-party/gpui-kit/crates/base/src/text/inline.rs": "26a0ce00e0ceabe531da7a97370e539bc615846a150e56c1fc345a1088f70268",
"third-party/gpui-kit/crates/base/src/text/inline_element.rs": "ee002a6860ae76829568f994ea7ac92caba9ced2129c513a43da51c45ce96e37",
- "third-party/gpui-kit/crates/base/src/text/inline_flow.rs": "0c2fcab83fd70ee2b56cbdbe1ee001032a2850fcec59d5422c231e2c5b2c18fa",
- "third-party/gpui-kit/crates/base/src/text/inline_object.rs": "65ae10d1ff4978723f29e5e58a2d0f7b8065f667ba81f950130cde7ae6070cbe",
+ "third-party/gpui-kit/crates/base/src/text/inline_flow.rs": "6c23caa6fded2cb6bea2a2320ad189564d6bfc74cac030beabdd26e4a9a3772f",
+ "third-party/gpui-kit/crates/base/src/text/inline_object.rs": "c671832619b1916b0b13e2c2a589168ed1d57a827b37cd6cd9875b02ac6b97e8",
"third-party/gpui-kit/crates/base/src/text/inline_virtual_tests.rs": "e86c28cc7c0d918e10669c241129756cece9ea52ffd8f73b8e06125a259c4356",
- "third-party/gpui-kit/crates/base/src/text/link.rs": "929525600564a7dbe5a01c387c8aca0041828d4808893cec602ea85eac8cbe31",
+ "third-party/gpui-kit/crates/base/src/text/link.rs": "73143e4958ac4ddc85de216d2db7fdd8fa3c1771c5ab89942b5a61b9616890d1",
"third-party/gpui-kit/crates/base/src/text/markdown_ext.rs": "44fa63ece976a0474f402a2a65d7d1f382f143c1a482dbb392b8f39f85e397b3",
- "third-party/gpui-kit/crates/base/src/text/mod.rs": "7c85f8f9cfaf9644845d6ed2683a3328a93d22f599c0707887ea5b04cc8f40b0",
- "third-party/gpui-kit/crates/base/src/text/node.rs": "22716f10b4867dceff25fed3c74f9105dfb80c9eb86f2559001246b3a82ec68f",
+ "third-party/gpui-kit/crates/base/src/text/mod.rs": "0fc6be2bd4e1f2b6cad341c48467b6e7b175b2c4468186b1730ba8ff8041e4cc",
+ "third-party/gpui-kit/crates/base/src/text/node.rs": "35e354ee6b587cd527994c0e74f8d673ea77ff63310e6ca7b71ec254f20d7803",
"third-party/gpui-kit/crates/base/src/text/range_highlight.rs": "fe9442d7dacc91ebada8343562f9cb8c3dca035d986f4d83905e85c706f295d9",
"third-party/gpui-kit/crates/base/src/text/selection.rs": "5a4923a2128cd6a3eea9f91b70dd9e7bd4369b020d1da4b209292c29e6ea870f",
- "third-party/gpui-kit/crates/base/src/text/selection_adapter.rs": "1289a8b56d3e5b71b4225208fce2869e61f21df978e029f63ac860a921cbfe46",
- "third-party/gpui-kit/crates/base/src/text/state.rs": "8020029050ae31c7e791002c3f75c35ed078a2ce6d9d4b87edbe7b11e19d8575",
+ "third-party/gpui-kit/crates/base/src/text/selection_adapter.rs": "97b82fce883a3022491f9f4a259bb5833aa9c6c1cefbe99d4ebb3fe080c8d5b9",
+ "third-party/gpui-kit/crates/base/src/text/state.rs": "1bfe27b7d62d3802fe8c93b5d2b97babab5ef4930ddfe8f74f4dd9cbe491d0cb",
"third-party/gpui-kit/crates/base/src/text/stream_fade.rs": "8070e549356951d29604a5e436afdf7606bfb6c0cd3fe284384acaf6195260b1",
- "third-party/gpui-kit/crates/base/src/text/style.rs": "ccd933ab473fc405b001c8deed1dcdd83a0e444cc7438b6ceb63e2865e7d5d94",
- "third-party/gpui-kit/crates/base/src/text/text_view.rs": "7063f44de4e04e31127a8ed3a757606c41fcc71aada77135f1666f8cb292e63e",
+ "third-party/gpui-kit/crates/base/src/text/style.rs": "d3de18242659840e963536ef0c265eede1c7803cf63cf709d764b05954c483f7",
+ "third-party/gpui-kit/crates/base/src/text/text_view.rs": "811029db41fd807a64adbd59b2fd01d03c3d06d8966a8453dd366b435c645126",
"third-party/gpui-kit/crates/base/src/text/utils.rs": "046254a406ccfc4ccb1e85cd0493e7b306d097b4ee872387cb8914356e723231",
"third-party/gpui-kit/crates/base/src/text_boundary.rs": "ca0ab74dc597c5ef6a8fa95259b75dcfc6320996be3cc9b35c79f0ca17862e6e",
- "third-party/gpui-kit/crates/base/src/text_selection.rs": "291e889c619b296b5444a598d0df2a7306d03b4f983bc436458572b5c6ce224b",
+ "third-party/gpui-kit/crates/base/src/text_selection.rs": "95b04aee5b4e3b747872be8ba4d96a671c64e63b10c2447aec420dee2e4d25af",
"third-party/gpui-kit/crates/base/src/theme.rs": "4376d3effde9e0f12f32bcf87a126e82771ce2cca17816eece40823f37ea65f4",
"third-party/gpui-kit/crates/base/src/theme_tokens.rs": "3e8fcec011d18b8d4f0b6beb738851e36134faa0ffccffa72069fdf04b3bc64f",
- "third-party/gpui-kit/crates/base/src/time_field.rs": "b79dcdf3c0d5e828d27388c9ca06116dc9fb408ddc0991e1656036ce3dc06ab9",
+ "third-party/gpui-kit/crates/base/src/time_field.rs": "3954d4cea036ad176a7898290a4bc1e70cf63b0a79988f21286ffbd0b4257e1c",
"third-party/gpui-kit/crates/base/src/toast.rs": "da19db55ad5955e35b687fa851405d629e971732f2d5083772bd2b3f747f346b",
- "third-party/gpui-kit/crates/base/src/toggle.rs": "884451c884b3f26cec33af18e644cc524c054b17c704f4d7dc13f155cb3de1e7",
+ "third-party/gpui-kit/crates/base/src/toggle.rs": "f534cdf953b1ed7d61e98f787e108d1698bfe482ad6ac28ebc314afea98ad802",
"third-party/gpui-kit/crates/base/src/toggle_group.rs": "a3859562050fe83bf5ec86c1c8cfcf19fc6e9871a049679bab975fc28408832d",
"third-party/gpui-kit/crates/base/src/toolbar.rs": "7059ec38f372f0b3c7721ca776b5df817bdbddcee370c3bf054855805492221e",
"third-party/gpui-kit/crates/base/src/tooltip.rs": "a304c3e0ed0e56e4f5e1ac66e746b3d219f2b3a5872e50817d0a7a9e141bd7a9",
"third-party/gpui-kit/crates/base/src/touch_selection.rs": "a4c0e56e99af0d268dd38e3d8f9d1cbc7fe4259fe9c52a6e4247e275ffe48141",
- "third-party/gpui-kit/crates/base/src/tree.rs": "e74e4fcdef6ccec174e3e6ced50f108433e726f3e3d46f3539b1f41d5944e5df",
+ "third-party/gpui-kit/crates/base/src/tree.rs": "6a8f108cafc850f2fc82f4259f617fb0724dd9f86e33ffc5e2b00bde722713fc",
"third-party/gpui-kit/crates/base/src/undo_history.rs": "ed5b8f83b9de3d749855d2c9bb06097693c6d9664fb201d7a692801eca4fc1ce",
"third-party/gpui-kit/crates/base/src/virtual_list.rs": "9a93638f5a2e15f1c4b3c87ca0426cee421341cf7a067da1809e12e67c1c9408",
- "third-party/gpui-kit/crates/base/src/window_border.rs": "fa50193a00edb19c4f8658339a9cb948ca1a32b2747411ae1da5578c16ba5b59",
- "third-party/gpui-kit/crates/shell/Cargo.toml": "c5b4b9413e7b674e66b499dc4ba78a0358f806656c7cc8447117d015efa46fe5",
- "third-party/gpui-kit/crates/shell/README.md": "3df51c049ab2ea10799eff9488690dcdcfaa41124c0ef0278a4a3e4566bde838",
- "third-party/gpui-kit/crates/shell/component-inventory.json": "0e3596c0f5df8bcff88d6031b7f07a94d60e4e28c81db5cc0ff414b60741dc0f",
+ "third-party/gpui-kit/crates/base/src/window_border.rs": "92e6e477d4ff1aea93af0d686ecafa6d6fa6bcc1b75f2f7a968f7775d09306c8",
+ "third-party/gpui-kit/crates/shell/Cargo.toml": "fb7d0222c6638f2d3052727fa6a4390ab14e45a4741bd6fdecf3b622a55aa74c",
+ "third-party/gpui-kit/crates/shell/README.md": "84c765d387a0f81e227ac58111024df4d2b11c3ff524bac48c0ad15bae50bf9b",
+ "third-party/gpui-kit/crates/shell/component-inventory.json": "eddcd1e9c87c1e03b18db981f36ccbed8cb01c082540589fa36e0cedb8d4c20b",
"third-party/gpui-kit/crates/shell/rquickjs-compat/Cargo.toml": "cd6a0e1b53913eb4f2559b79aae3b7877b41e9c559c56a0231501cbd0fe2ad3a",
"third-party/gpui-kit/crates/shell/rquickjs-compat/src/lib.rs": "bb742dbe37d7e8494e7279d0f3f3ecd0a7df30ebc66696829aa3eaa507fb9e42",
"third-party/gpui-kit/crates/shell/rquickjs-compat/tests/identity.rs": "f78d4fbdc0bd78efb5199c57d6e5f8d76c71bd757e7cd15c2638f2e378de5f65",
@@ -1001,16 +1014,16 @@
"third-party/gpui-kit/crates/shell/src/action.rs": "221fd1b69094e5e27f9081c62f54d5a79096d9650f77e2de95bc44ead064cc7c",
"third-party/gpui-kit/crates/shell/src/assets.rs": "9a2adb1725b5453bb7e76a3d648bf72dc384e0a6ea2fc1d05f89f7a5a72ac8cc",
"third-party/gpui-kit/crates/shell/src/bin/gpui-shell.rs": "780f2a21e91ef67958870327eeba904d11ddee14a170711badc54470b82f900a",
- "third-party/gpui-kit/crates/shell/src/capability.rs": "93db9f31b18accfe8f665d70f40f0025acc575bcefab5003d45c8359e619bbf7",
+ "third-party/gpui-kit/crates/shell/src/capability.rs": "fb96f017db33fec76e03180d47694e15f2b8cfc3e87609f45a4ebcdd2345327a",
"third-party/gpui-kit/crates/shell/src/component.rs": "b68f529033bd78e669385c2a8b6e13c2f34657227bed2e61e2d5fabb83f00b58",
- "third-party/gpui-kit/crates/shell/src/component_registry.rs": "bc48632fea79ca62e2e4bceda40b4caf41082823b9e59303f58ec4950d363933",
- "third-party/gpui-kit/crates/shell/src/components/mod.rs": "be55c5061c92998b97c38f1e92f42159e8379fb4f1b42f795d99fbd3058eaecd",
+ "third-party/gpui-kit/crates/shell/src/component_registry.rs": "3c5ebd98381546e78dd0d16c70e05b48c5c452ec642e9962fd0d9a97a09387c8",
+ "third-party/gpui-kit/crates/shell/src/components/mod.rs": "bcf3d3e9ca18f989d927b6f27870e4f440f234237a168263473995709a643f8c",
"third-party/gpui-kit/crates/shell/src/components/shell/basic/dropdown_button.rs": "89cbc1fdf0f0039f9dc208e859a689295070712bc534e5a24b52cfd8f293bea0",
"third-party/gpui-kit/crates/shell/src/components/shell/basic/mod.rs": "b3fb0dd8ffde717f42ea2e8f4da2ca42b60849312470316fb9469ff123cadce4",
"third-party/gpui-kit/crates/shell/src/components/shell/basic/text.rs": "5ca10f8ba55ebc2e6df1fc58842d29af575727366cfa2108a30d77bd412b8109",
"third-party/gpui-kit/crates/shell/src/components/shell/carousel.rs": "134281a37b0f404bfafb7e73bfe6e4a492d93dfd9bfc2ddf43cd6b1b2a39d888",
"third-party/gpui-kit/crates/shell/src/components/shell/chart/mod.rs": "a2376fdbe44ed2b1400066ece4f026c2d0fe88aa9efe8dd188d346082caefcaf",
- "third-party/gpui-kit/crates/shell/src/components/shell/chat.rs": "afa0e535f1dd62000d429858b30bb59bb009945d10324c3fcc1e8524e8325326",
+ "third-party/gpui-kit/crates/shell/src/components/shell/chat.rs": "2e300b21b34ce03f137b9008fd9351808639fe77ab8601dac33c58d5930b23ca",
"third-party/gpui-kit/crates/shell/src/components/shell/collections/mod.rs": "25646a24666c1fbabb5a6bf01f5afb848760d7706fe42ee565e7a6000ab5cdcf",
"third-party/gpui-kit/crates/shell/src/components/shell/collections/tree.rs": "be6c66c27ce1926aa3084fc35d1ea6affaee94c757749cb18b3ada26df85e116",
"third-party/gpui-kit/crates/shell/src/components/shell/command/command.rs": "e194c7e13ba682e4fbc336c41facfdb7f42d2b71108102a6c30c0d20bc3dece7",
@@ -1050,14 +1063,15 @@
"third-party/gpui-kit/crates/shell/src/components/shell/input_tokens.rs": "0f6a09423570e17e923847e88f47434a1fff724025c2017d5a75aa693ecba1b5",
"third-party/gpui-kit/crates/shell/src/components/shell/layout/mod.rs": "5899b80b6fd3f44153e58a64f828340b678bdf963b65e94b3a34f5da72392666",
"third-party/gpui-kit/crates/shell/src/components/shell/layout/resizable.rs": "07e6f3a16a3644d35d431ea47fb562cb8dc7470ffd73d21cef3239b8a1481cd0",
- "third-party/gpui-kit/crates/shell/src/components/shell/layout/textarea.rs": "83a12bfc4a4e6a4e44821e7da1b50e9744a48f476a8c8af979d414477e6df467",
+ "third-party/gpui-kit/crates/shell/src/components/shell/layout/textarea.rs": "b220b17083e8d5f69b3f1bfc86197e5230096037d3db17f1b50418832f098d72",
"third-party/gpui-kit/crates/shell/src/components/shell/lifecycle/menu.rs": "a8a6cfdc701d2c2f5a038285855a0e0a46306a7c45e87198f7e12672b185c360",
"third-party/gpui-kit/crates/shell/src/components/shell/lifecycle/mod.rs": "6d1754ec6977ad9f421c9c78afadb17c68a5d399ef0e8ad1b957e509552a3a12",
"third-party/gpui-kit/crates/shell/src/components/shell/lifecycle/tooltip.rs": "0812019de4e2161f2ebfb0f94d148905a963fc9f7bad460de08ad82304a6b04a",
"third-party/gpui-kit/crates/shell/src/components/shell/media/diff.rs": "c6e1df8dc5afbde5564567a110431e6461724d0dcf1cb80cfc090bc25fa60b86",
"third-party/gpui-kit/crates/shell/src/components/shell/media/editor.rs": "4ee6c0552453515c3803ed7211464bff39ff469ab7d68877ac55d66b09681b88",
"third-party/gpui-kit/crates/shell/src/components/shell/media/image.rs": "9f526fd9215ad828ca1f307e2b11818d6343e8b1f9147bbb6483ab1147009ffc",
- "third-party/gpui-kit/crates/shell/src/components/shell/media/mod.rs": "4d94bbd8c51681d6ece85a0245d6d67f5f139548102f6f46a33ae6e1968d84c8",
+ "third-party/gpui-kit/crates/shell/src/components/shell/media/mod.rs": "25ce42690cb664359f7128ee966035f5245cfa75cd7bb358294d0d3fe6c07749",
+ "third-party/gpui-kit/crates/shell/src/components/shell/media/webview.rs": "b10ed4e6fdf1ae89ddaec5f7c7363b920e52680e0eb1989c13cac4a646068d99",
"third-party/gpui-kit/crates/shell/src/components/shell/mod.rs": "be8342aaf3b9f5cebe06f309ecfbd3a250e60df03ba98d9f6f73089b8f4db606",
"third-party/gpui-kit/crates/shell/src/components/shell/navigation/icon.rs": "1d627644b7475a2c0adc8bdf0c1c4a88e31b96b9bd5a0f36611172a33fbb422b",
"third-party/gpui-kit/crates/shell/src/components/shell/navigation/mod.rs": "4e6cbf3d962d2344d54600440c13695184ee31154323bf751541604cfa66dc9d",
@@ -1067,9 +1081,9 @@
"third-party/gpui-kit/crates/shell/src/components/shell/overlays/mod.rs": "e817c99bc5bc802712a5aa5959b287d5c57adcda462b5842ef969f002ac86555",
"third-party/gpui-kit/crates/shell/src/components/shell/overlays/popover.rs": "8345dd410b27772256d39e70f98d285552a4711d21817e2714ea14545721ff7a",
"third-party/gpui-kit/crates/shell/src/components/shell/questionnaire/mod.rs": "f0c07b188175ded1e3bb2a4923449a781b6b0a48770f428e9d08712a586ffab6",
- "third-party/gpui-kit/crates/shell/src/components/shell/retained_forms/mod.rs": "5f7779ed8f1ba7fc45ff5804bab4c4a198e12e645004c023d4417dc9b8d18c94",
+ "third-party/gpui-kit/crates/shell/src/components/shell/retained_forms/mod.rs": "a7c3e0c0f90ee2b3dbaefd331b20b5abb787136b9e9175c021a2c0535f98dfc6",
"third-party/gpui-kit/crates/shell/src/components/shell/scroll/mod.rs": "583b810340f2d97eea438db2a9fd615448558fb66018d24975959a8a5e82c4d9",
- "third-party/gpui-kit/crates/shell/src/components/shell/scroll/scroll.rs": "52af54f3b73deb227ab971d19705e7a357b897df51caf8cedf5c81e420cb4ee9",
+ "third-party/gpui-kit/crates/shell/src/components/shell/scroll/scroll.rs": "2ee1320728fbabf357ee43f1a9cd6842fe5f172d5a45c6a977db7dd1f6f197c5",
"third-party/gpui-kit/crates/shell/src/components/shell/separator.rs": "937ad1715b241ecc44c8f899799c3f4b9924a9a46ae0d895ed9319c388f0da84",
"third-party/gpui-kit/crates/shell/src/components/shell/settings/mod.rs": "06339b0193f8cd95723d14c8b59c27c9d3f3c6e8d7e34e60c1d28ff33d68f1c4",
"third-party/gpui-kit/crates/shell/src/components/shell/skeleton.rs": "21f652cc7a5c3c786903e36cf85bf018f5e0cad9b4b3babdc97aa17a5d5cff85",
@@ -1083,46 +1097,49 @@
"third-party/gpui-kit/crates/shell/src/components/shell/typed_child.rs": "4284bb1be58cfc2dc5c3b1bd39672f8b3fa7784a603ad7c4b89cbdc9e7a9a01d",
"third-party/gpui-kit/crates/shell/src/components/shell/typed_compound/mod.rs": "c2027159972a3f575cefd5fbf3140cc9ba94fbe80fa00fe7378091a0b1d3a69f",
"third-party/gpui-kit/crates/shell/src/components/shell/window_effects/mod.rs": "fbf6a4ed214c5b6e4445c8920a244a04cee264944d54fe111892f18ad2cd66ce",
+ "third-party/gpui-kit/crates/shell/src/default-env": "8e42e5baeda3377ebc23040e071df2f5481e66fa0f852daa8b7ec274bfbc8e96",
"third-party/gpui-kit/crates/shell/src/default-tokens.json": "0968d8329bc4498c28cd9c1e022ae161caebeedc0343c74b0a59cd30b7c94308",
"third-party/gpui-kit/crates/shell/src/dependencies.rs": "d64c218bd9fe31111b140e6f41b1c6ba7268f9e1d05bde9e5f036e4c3fa396bb",
"third-party/gpui-kit/crates/shell/src/dock.rs": "feb63823d83357a3e969c3bf06cd9afb7e2e6071b0ee17acbf1108e0b9305aeb",
"third-party/gpui-kit/crates/shell/src/engine/mod.rs": "89e4ce95713b4a2824dd834eb2eb94bac22cd50ae89c58469885a024b2b25bc0",
- "third-party/gpui-kit/crates/shell/src/engine/quickjs/dock_api.rs": "aeecd707b956ec05455aa4a19f85ab6ac4fd859abf116b33fde4b99a6968ae3c",
- "third-party/gpui-kit/crates/shell/src/engine/quickjs/entity_api.rs": "83df83a43a118676beeb5e73535d68ac13dfefad21f694a481f3aad3aebccac8",
- "third-party/gpui-kit/crates/shell/src/engine/quickjs/host.rs": "d85adceef8c4a65174d90ce52ca3ebc3bf69ea227388a4f33b239d06a78522f2",
- "third-party/gpui-kit/crates/shell/src/engine/quickjs/host_modules.rs": "8a5d139baffb97518a463ca8695ceb4b607716b5033598597fe4e5e811ce83ec",
- "third-party/gpui-kit/crates/shell/src/engine/quickjs/mod.rs": "19d4115e3b1e93787406ac7583322d6e0c0b3fd8e5f93f671b25236ac174f440",
- "third-party/gpui-kit/crates/shell/src/engine/quickjs/overlay.rs": "b2ed2a3bf5fd3b0cceb59b47acc10ed8c01ee4150979b7f0ab3a372fb9aa2d09",
- "third-party/gpui-kit/crates/shell/src/engine/quickjs/sandbox.rs": "0f2356d5038f6c9b867aa4477d60a1ef839bc750b621daebf3b57b13266826c1",
- "third-party/gpui-kit/crates/shell/src/engine/quickjs/scheduler.rs": "f6d4802ccf8a495cc96c51319d1464cdc397bbdb073a75379126c3ed60a7b973",
+ "third-party/gpui-kit/crates/shell/src/engine/quickjs/dock_api.rs": "00b0a4a8e8f9721133d47d930858531642f6f19775a7b768677f7c6fe37eea45",
+ "third-party/gpui-kit/crates/shell/src/engine/quickjs/entity_api.rs": "04e6862c19d97c67cba4ea13028746116976e16d2dc379a70bddeee95e8a21bf",
+ "third-party/gpui-kit/crates/shell/src/engine/quickjs/host.rs": "dc132a1cb1cc2dbcfcfa6c2aee70eb3a566f74c05b0207d7b853de9321ef95f0",
+ "third-party/gpui-kit/crates/shell/src/engine/quickjs/host_modules.rs": "46731dc49ac81e26161ac7db8abf47edf15367a8aa92d001be52e4a7543699e6",
+ "third-party/gpui-kit/crates/shell/src/engine/quickjs/js_error.rs": "1211c85c2b07f76f22839d4b25a2598074860b2f5a4d69ea55e5ac81bdc1f03d",
+ "third-party/gpui-kit/crates/shell/src/engine/quickjs/mod.rs": "bba0be1f405fd7d37c5b341e1d51a229e0e2909bb691667516e1ea01e9b5a3b3",
+ "third-party/gpui-kit/crates/shell/src/engine/quickjs/overlay.rs": "d65a470f0ded47b5def85c084b42fb320dca9b7d265df32f2a4512f02242b307",
+ "third-party/gpui-kit/crates/shell/src/engine/quickjs/sandbox.rs": "0c8bdd5ad8a79774fd387a3f196ad344cea611566d06c2b66782800999a3946f",
+ "third-party/gpui-kit/crates/shell/src/engine/quickjs/scheduler.rs": "196e00a7627e2f444fc22f285cfe3f376034639090486fe98720139e294c4c29",
"third-party/gpui-kit/crates/shell/src/engine/quickjs/standard/connect.rs": "a1706d46dcc1b2cab62b4d65b409193e625013c9d50aaae07f496dfd4dd73ece",
"third-party/gpui-kit/crates/shell/src/engine/quickjs/standard/console.rs": "108085f14389e704b2d5a3b37b7139f103e9b7d5933bc8f810dad9607bc9f4b3",
- "third-party/gpui-kit/crates/shell/src/engine/quickjs/standard/fetch.rs": "93d33dd9e5fcfd24cd373c8ec588ee3f00971a1eddd966b2435743a0d395f31f",
- "third-party/gpui-kit/crates/shell/src/engine/quickjs/standard/fs.rs": "4ac0ca532f558788bfc7e4558223ca6476e65382b59026b10d02303c41d45ee9",
- "third-party/gpui-kit/crates/shell/src/engine/quickjs/standard/mod.rs": "f406c16294c5ee30cac9d9c6b7d30efcfde57a2f42dcec900b221dc0a376343e",
- "third-party/gpui-kit/crates/shell/src/engine/quickjs/standard/net.rs": "dc73c8ad5fda62d11d2c86768cdf5c235fbb62a64c8bc0ca379fdd29cbdd0177",
- "third-party/gpui-kit/crates/shell/src/engine/quickjs/standard/os.rs": "bf698d43b79b5260ec5228677bf6c41537cdfa342193613f6579c911cc9361e9",
- "third-party/gpui-kit/crates/shell/src/engine/quickjs/standard/process.rs": "e27b38b9713c6402ccacb511db8a8d4d410907d92ef1ab3bb5ba9a6229d489f2",
- "third-party/gpui-kit/crates/shell/src/engine/quickjs/standard/websocket.rs": "49f870b9265459237c30b6020eecb08009ed94b6aa245785b2cbba7d42531694",
- "third-party/gpui-kit/crates/shell/src/engine/quickjs/template.rs": "88ecf9a07fc630a053f7e250fb0ccd7aacba7edd2569c02ec42a406d6a192441",
- "third-party/gpui-kit/crates/shell/src/engine/quickjs/theme_api.rs": "28411db96a117e285cc1d905c6f62656f44a0b635adfc29c02f9367d8897f14a",
- "third-party/gpui-kit/crates/shell/src/engine/quickjs/window_api.rs": "efeee042703a97ac8fa2a5e4ba1f31c4b6e001b8be489f86a365a6a4c58b434c",
- "third-party/gpui-kit/crates/shell/src/entities.rs": "1c9689bc012a183dad715112b99c6805f4ab31fcdb26c297b1ed84fd0a0a4bef",
+ "third-party/gpui-kit/crates/shell/src/engine/quickjs/standard/encoding.rs": "0f58464cb7220325168766cef97956e0210c25afbd72e3a8c2fada693bf2cd56",
+ "third-party/gpui-kit/crates/shell/src/engine/quickjs/standard/fetch.rs": "85dcea9e2ff70e6dbdfc9f6af25eed7c3a17073466f0c18e494bdd8d5c09bc47",
+ "third-party/gpui-kit/crates/shell/src/engine/quickjs/standard/fs.rs": "1c1c177d3262be7ecd20da485e8717e9f5096ff65c5b4be8d09aac8c77867110",
+ "third-party/gpui-kit/crates/shell/src/engine/quickjs/standard/mod.rs": "41644315c8bd623ec8a02e1668486a87c5ef59be48ae86716c0e256821a6b619",
+ "third-party/gpui-kit/crates/shell/src/engine/quickjs/standard/net.rs": "318514752438f22def165d8fb37a002f2dd2504fbf2f51d0b09f31860580d649",
+ "third-party/gpui-kit/crates/shell/src/engine/quickjs/standard/os.rs": "bc08295252a4c49b238e0e6d8de75f84c818e91a96eb4274990940f6308f2cf4",
+ "third-party/gpui-kit/crates/shell/src/engine/quickjs/standard/process.rs": "ae0c3ebb0801e12cd8ad27caa54331dcfb7406a89a8a7684949610063f4b5b7b",
+ "third-party/gpui-kit/crates/shell/src/engine/quickjs/standard/websocket.rs": "7123175ed0400b712223e3e1dd7095050bdcc01f2a35e5ca99e92a5b17d2aae4",
+ "third-party/gpui-kit/crates/shell/src/engine/quickjs/template.rs": "c9a4954ae99d19486ff956caa3f31011a90745962aa522567ae9f9b4ee478695",
+ "third-party/gpui-kit/crates/shell/src/engine/quickjs/theme_api.rs": "630f54b6cd68956224862771a49a5c265edbb1400a09a886fe57110c0f74efd6",
+ "third-party/gpui-kit/crates/shell/src/engine/quickjs/window_api.rs": "79551ff07066a0f63e05c57c471e146440bbbc69f05558214f0d32abf3080b75",
+ "third-party/gpui-kit/crates/shell/src/entities.rs": "3d04a29046d56f8a57c249e45289ff00e7a63a67653f83197e2ab3a7c683e100",
"third-party/gpui-kit/crates/shell/src/error.rs": "39474b8d408632c2ee8950ec95a37fb453614116eadc39357ce7eeef544ad3a0",
- "third-party/gpui-kit/crates/shell/src/host.rs": "ee323667c12d20963410859e03e82884970143001d83be1099fb14bf9b22f4f4",
- "third-party/gpui-kit/crates/shell/src/host/agents.md": "d426f8cf92a89e9c23cde9377537b507b1908dc9535f7767db20b26547f8d8c4",
+ "third-party/gpui-kit/crates/shell/src/host.rs": "f42039e9e095a841e9448166edbd6a83a5a035a873c155b800893713300860ba",
+ "third-party/gpui-kit/crates/shell/src/host/agents.md": "aa9f31c6a7d704e48adf080d3713c66a9d594c33b8c884dd30cb830ab8184b5e",
"third-party/gpui-kit/crates/shell/src/host/agents.rs": "c38d6f0b65647860588d364a34f021dc0fb58e614de0eca6e38c9a40d63301e1",
"third-party/gpui-kit/crates/shell/src/host/bundle.rs": "17a0e2203f24fe0ff3bf023f39d37e1bc626de38865c973c82dbc233eb771751",
"third-party/gpui-kit/crates/shell/src/host/bundle/linux-install.sh": "596fd248fc5a840437b889ee1675d061a699a79235079467ae4c4a800fcffd18",
"third-party/gpui-kit/crates/shell/src/host/bundle/linux-launcher.sh": "efe6df9255465be99c6c50b76c3fce84d9d967a4d070498e38990dbfff289667",
"third-party/gpui-kit/crates/shell/src/host/bundle/macos-launcher.sh": "987be9822a100d0b012dca2be47590cfb39c39d530244dcd185177ccc570bd3a",
"third-party/gpui-kit/crates/shell/src/host/bundle/runtime.sh": "33ace3cd775cf09d6c1beaeffc907bef86ea0f7fc3ddc6f64572d7f8dee6665a",
- "third-party/gpui-kit/crates/shell/src/host/installed.rs": "4b77ae17721191eab0044881bbcf98968b206525c9f5564866dd2ecb4a2d57c0",
- "third-party/gpui-kit/crates/shell/src/host/scaffold.rs": "36f502b980fefd4809447fd78c191749b8c43795005dc0ce50b0f65f737b885e",
+ "third-party/gpui-kit/crates/shell/src/host/installed.rs": "c95dced4295b9a95c1f6f71439f42bf1399c3e39529c7520f3851cbdbe60f146",
+ "third-party/gpui-kit/crates/shell/src/host/scaffold.rs": "c6bab98bf667c424e80e8e511d7e81114464bce76f9bc9f91f15fc352d2c8cfb",
"third-party/gpui-kit/crates/shell/src/host_modules.rs": "ef28eb06ec8722f89556424a9b5de7fbd2e773a85d403f1b7a51d8b6bbf09b9c",
"third-party/gpui-kit/crates/shell/src/input_tokens.rs": "e1b6634b1456a461ab1c242c74427270895770fc76097c9cf3d9dd53928025ab",
- "third-party/gpui-kit/crates/shell/src/lib.rs": "9b327fe3da2f44bc0091e103c2bc0e975eaa4722cf5dc724e82470998057d30b",
- "third-party/gpui-kit/crates/shell/src/materialize.rs": "3da799b9671f07abccaae3a153b4dc8bf6cb916b8362111ab59ad958f21cbb5d",
+ "third-party/gpui-kit/crates/shell/src/lib.rs": "3212d86c61bd17928347576dc07cfb64631a7f103292aa8f7b0e841dfeec6b64",
+ "third-party/gpui-kit/crates/shell/src/materialize.rs": "63bf402cca0a506651b6bfed845235dc73d8d32b5af451f69d4e4da799faf521",
"third-party/gpui-kit/crates/shell/src/materialize/components.rs": "b957e35b5ccc1e764fbb3e8ee7e240705940e5c5847febbf5f0865ca8a25200b",
"third-party/gpui-kit/crates/shell/src/materialize/components/accordion.rs": "36263db263d1619c255f86efc21db829c410a2de2bcc463e7c1edc3845757e44",
"third-party/gpui-kit/crates/shell/src/materialize/components/avatar.rs": "556a48dfea94c1e111f054b39b8676cde709d1332e4a876714d0da114d70e1f2",
@@ -1130,7 +1147,7 @@
"third-party/gpui-kit/crates/shell/src/materialize/components/dock.rs": "4c861739f09e0b82e56f77e1ef4b9a3345aa761e50a859f02125e57cb892ac54",
"third-party/gpui-kit/crates/shell/src/materialize/components/fps.rs": "31ea62c5ec56d82f569816fcfa53e527aa89a843af0d20cb325a46b9165f9a48",
"third-party/gpui-kit/crates/shell/src/materialize/components/group.rs": "998120af2816a4a91db6af53225f0463d9a039d6c976c0290834d1f7ce4948f3",
- "third-party/gpui-kit/crates/shell/src/materialize/components/list.rs": "8ef59efec02b3de11ba70469907af62ccecc921a4d24aea25a3b5982d7c3569d",
+ "third-party/gpui-kit/crates/shell/src/materialize/components/list.rs": "cdca8363820e5c43360b8da845378392dc769bd9bfba1e0dbf4d06ca7b0c3099",
"third-party/gpui-kit/crates/shell/src/materialize/components/number_input.rs": "6aa29f1b674e5b3e73036bfd66a899ad3e3cc23378d23737cfc0e35ab7622426",
"third-party/gpui-kit/crates/shell/src/materialize/components/otp_input.rs": "dc712b9edd35c6cdbd439346ac6c0a9dd5e0c3ae62f17bb468656f278a1fadb9",
"third-party/gpui-kit/crates/shell/src/materialize/components/pagination.rs": "8a0505628f482c26a8b58ef70ca63e7be10a868ab099b689d56001d294bd4798",
@@ -1146,46 +1163,46 @@
"third-party/gpui-kit/crates/shell/src/materialize/components/tabs.rs": "5a62a3caa1990447ea68c1377108ced9d496055f65d637eac319786bea7cb05e",
"third-party/gpui-kit/crates/shell/src/materialize/components/textarea.rs": "fe25ff7123be9e30b2531b85271810b80f892f081f6866530cc36bef6f538ae4",
"third-party/gpui-kit/crates/shell/src/materialize/components/toggle.rs": "68c794784ab442467bf7070dd0d2c44a33b5292fdd1c4984223629c69544a539",
- "third-party/gpui-kit/crates/shell/src/materialize/components/tooltip.rs": "3b682b6edddf8a7e0e83afc7588a458cee637cc504431b2ee7d6a13030d914bf",
- "third-party/gpui-kit/crates/shell/src/materialize/components/virtual_list.rs": "819c76c2c72e53d36f8312316f13f0390aa383ce59fd7f2e06cfb2871f3f10b5",
+ "third-party/gpui-kit/crates/shell/src/materialize/components/tooltip.rs": "aeb64dc0905e3f1633fd9935c11dd49b8f2605e9c05abf5ead46b8afe9a765a4",
+ "third-party/gpui-kit/crates/shell/src/materialize/components/virtual_list.rs": "f0629a2b99964ec7c07cfa113ea15f68fbfa79cb435081a3c945c8daeffb6783",
"third-party/gpui-kit/crates/shell/src/materialize/text_view.rs": "8f548d46ee78318b650dafa6829c9db4038b535752ddf274ede6837b618e82e5",
"third-party/gpui-kit/crates/shell/src/materialize/text_view/style.rs": "cda550412397bc9a62e807dd77d70beabcdd1ec1d6a32faa3ec42944e1e36156",
"third-party/gpui-kit/crates/shell/src/materialize/text_view/tests.rs": "03d3293e1ef52ebb4eac5de1f3894f40f17d0cc3736efa15c359e86d55655a0a",
"third-party/gpui-kit/crates/shell/src/metrics.rs": "058b00d9903d6983825552c55ce23a3e9cbde5767023564d4e4395a23ad97273",
- "third-party/gpui-kit/crates/shell/src/path.rs": "9defe2a871fa085b755fd15cebc9467c37a3787f27a2e7d1f9cf68a830d9e089",
- "third-party/gpui-kit/crates/shell/src/plugin.rs": "f36c3bba39c68d7af882449e1a8a521a38fea386e7cd7a81e025fa341bdbee17",
- "third-party/gpui-kit/crates/shell/src/policy.rs": "3009c6412800f4b507e208145dba49570144a46c31695a64b5d95755490414b4",
- "third-party/gpui-kit/crates/shell/src/process.rs": "9ef7fc0b42d1b4cc7e1a55e45bc59a1ea94c6832870294eabe7493bbe826b718",
- "third-party/gpui-kit/crates/shell/src/root.rs": "271bacefb58be70d6cf2565b58cf540cf159c2a023473dd3fe7d9fd3afb1d4f5",
- "third-party/gpui-kit/crates/shell/src/runtime.rs": "c0abac2450b2973c22a21da34a38daeb2b2eaeb0dc16715aaea073d6f18a43e2",
+ "third-party/gpui-kit/crates/shell/src/path.rs": "2237f67ea8a57bd40d2562bfd68e876b98678d13cac8d8bb6f649ee748f2d226",
+ "third-party/gpui-kit/crates/shell/src/plugin.rs": "8e69913dd6a8b70cf3db5de0a486fa8d98ec152563b62a38c15fd2c5c3b6968e",
+ "third-party/gpui-kit/crates/shell/src/policy.rs": "2267f3e971fd32b81ff75b38addd56f82af3365d08e60aaac283ac614549f6f9",
+ "third-party/gpui-kit/crates/shell/src/process.rs": "c320cdf1f8eadf776950a6745eaea4e912736002d45295dd680d6a8acd572004",
+ "third-party/gpui-kit/crates/shell/src/root.rs": "9b93dd11dfb2f3806763d29ffb383de9825994fa8b5a57df9c8d9f8cb1aec385",
+ "third-party/gpui-kit/crates/shell/src/runtime.rs": "6668bf74896be3252366288dca8eeab2305fe4ce3b6afa31c7e926779e544279",
"third-party/gpui-kit/crates/shell/src/scope.rs": "412e074e3253eca9a394f6347242bff5b61bf9126f86dc01321cd35ecd75df1c",
"third-party/gpui-kit/crates/shell/src/script_callback.rs": "a47805ff8c935958f740ea9c264eab66a64b7a37a21a6ff40c031e1fd31d4c49",
"third-party/gpui-kit/crates/shell/src/scroll.rs": "a42a263621f51fe8de7d16d292f8f8ddb2067c763b591aa0fc4ea5e9d30da6cc",
"third-party/gpui-kit/crates/shell/src/snapshot.rs": "b714170f2e128b86d365e3681a2e1bb22667c8a06da949cdfeb49acd4e80fd41",
- "third-party/gpui-kit/crates/shell/src/spec.rs": "98bdc5a5ab81114e34f989f8ed943bd1c4b03301c57e52c2b5d989e047e8d020",
+ "third-party/gpui-kit/crates/shell/src/spec.rs": "8b7cb386649579a89b989541cc90751112d811015cdcb61eae657c1ca763d1e0",
"third-party/gpui-kit/crates/shell/src/storage.rs": "1d03aa5767e27b62a88741ab644e35243f07206699f5a448b050d371e9e88a83",
- "third-party/gpui-kit/crates/shell/src/style.rs": "ece8ba3e9eeaeb0e28c5424afc44140b0cfd2f00dad892e7419bfe34871c12de",
+ "third-party/gpui-kit/crates/shell/src/style.rs": "670d9861b0c6dfa20bdea72e5dda7106ded5ebb8586a11007626b98edc42efbf",
"third-party/gpui-kit/crates/shell/src/tests/benchmark.rs": "43efcf67d1415ef499daa1d5d1bdf64c74a9b5a49878359ae55a598fdf1972c6",
"third-party/gpui-kit/crates/shell/src/tests/dock.rs": "0e2b8ae7d006499187e46f45156810695780d3f6b41ea5f56ae6a25dfbe7ee35",
- "third-party/gpui-kit/crates/shell/src/tests/fs.rs": "2394f1163bcf38af80f2f5483ba164cb643ad6e9d44d9534654a52a2ce69fe24",
+ "third-party/gpui-kit/crates/shell/src/tests/fs.rs": "6e298a8d05fcac11a353158a61ca0d19d78f6d844b58c1392b7a2e4a1ca4a9ec",
"third-party/gpui-kit/crates/shell/src/tests/host_api.rs": "d8ee38eebef9e280251f31099f85abbfd67edf89699f208b2a59c123b2e61099",
"third-party/gpui-kit/crates/shell/src/tests/http_request.rs": "da609339b11c6a99debea0e366b4e4ee72c95dc66517850b0149c0758b8c2ec6",
"third-party/gpui-kit/crates/shell/src/tests/mod.rs": "575279cdfe4c45162fd7088514e074936d9df6f1a631a4dc1cfbf26dc439dda9",
- "third-party/gpui-kit/crates/shell/src/tests/network.rs": "28527841c89b2781c0480a39a43d774b0a2d56a555a7afaa221d3ad8038d3ef3",
- "third-party/gpui-kit/crates/shell/src/tests/process.rs": "f2c2fb6a202ac76b93514e8f13ad88e655254d556949e604c055cb5578edbdc4",
- "third-party/gpui-kit/crates/shell/src/tests/render.rs": "8954f6ff5421e0ddebd3a1364806e76b6863bc79c61d7a25ddf644558e515541",
- "third-party/gpui-kit/crates/shell/src/tests/snapshot.rs": "16b8b5a6a7337ae303addc999eb12e9f38e30b028c7d44560e78480f23025865",
+ "third-party/gpui-kit/crates/shell/src/tests/network.rs": "10d2919c957ff02fc224697c4a2dd0db51d46793325c6d4d93e2ea9ed54765b0",
+ "third-party/gpui-kit/crates/shell/src/tests/process.rs": "29b951a89cc906dab05bbcd3471b15139fc62b0dab9236c2e2147c8f08cb4a86",
+ "third-party/gpui-kit/crates/shell/src/tests/render.rs": "3ca223110774b778e7715dbb100e397bcbcabd122edabf60eee9fa2928168f37",
+ "third-party/gpui-kit/crates/shell/src/tests/snapshot.rs": "3811ad14936b9128f6e2935a30db8bc6a1ded0e07eb3ac71243d1b618297562d",
"third-party/gpui-kit/crates/shell/src/tests/spin.rs": "18dda2798a669d077281378e2f0f19af2a1a2418e6e718486932725a2f928cd3",
- "third-party/gpui-kit/crates/shell/src/tests/standard_runtime.rs": "06fb0d1921924f4b4f9e966476b9a15015eec51f1fb982cbbdad14aec76b485d",
+ "third-party/gpui-kit/crates/shell/src/tests/standard_runtime.rs": "f803f45c4be537e0471a9b36e38d4e587aa991f0579257bf1ee1ed875164fac4",
"third-party/gpui-kit/crates/shell/src/tests/structure.rs": "3c7d68eedf2f67d3914614a0c140a0cadf0fb180194b4c704c09e724c25d8c47",
- "third-party/gpui-kit/crates/shell/src/tests/template.rs": "45ce118ba7665228bd29cf367527489ce59f1656578862cbc5ff2e93e24c70bc",
+ "third-party/gpui-kit/crates/shell/src/tests/template.rs": "8e6c52fec35d909b90c96b7e525944dedd20d245d95384bcb75b504c918ae221",
"third-party/gpui-kit/crates/shell/src/tests/text_style.rs": "b02cb951fa2884bc05aadff8496d69c0fb974b44460fb7e64f92097df0c14f45",
"third-party/gpui-kit/crates/shell/src/theme_tokens.rs": "c7397ebdf686950f569030ae08195d558cc6c6d833d6b47784a5bfff7bda1825",
- "third-party/gpui-kit/crates/shell/src/typings.rs": "e770fd195c8617bb60ffee28b62580342a7bc644aa7f49eb3fd142b3ac708c20",
- "third-party/gpui-kit/crates/shell/src/value.rs": "263d0c298f005891de2e8a50945c852b020c6c9a6af0c2b1dafc35760a47c215",
- "third-party/gpui-kit/crates/shell/src/view.rs": "a9165bad6b34363f73874e4771531225769414ff041ae3a38552868f91751b47",
- "third-party/gpui-kit/crates/shell/src/watch.rs": "fcd626acfe003cf6c3d9b236d81fb503d43912813d1f1650825dfd404a34f521",
- "third-party/gpui-kit/upstream.json": "5c953a6c6a1eb34a1f0068dfdf26b0ac82e82e2e842642f5942a0970c6051826",
+ "third-party/gpui-kit/crates/shell/src/typings.rs": "5a900a892da9c9653c58a1e071b9bdee0b67585096eee828d874e8a5ec95153d",
+ "third-party/gpui-kit/crates/shell/src/value.rs": "6a27cd66f0a38a7ba7b6b07843f9afcff6f1f246da8ab04dc556aa14f9c4b1ca",
+ "third-party/gpui-kit/crates/shell/src/view.rs": "00c251e93afe7884feceeadc77cb4257b8ca60ff6892fdabbb3a6a1e3ee2c4dc",
+ "third-party/gpui-kit/crates/shell/src/watch.rs": "6bab4748374e743afd10958302e5a97c6d0b62e7639d7ba62c0749053f3a9d25",
+ "third-party/gpui-kit/upstream.json": "ac08a3964926053c92e069aa6da408b481e8b6754cc081b1a73f5c2b61677778",
"third-party/image-webp/Cargo.toml": "c05cc68c768c57e79c581b2ad6c112759556df9689b83313e35d5943f2951262",
"third-party/image-webp/LICENSE-APACHE": "0d542e0c8804e39aa7f37eb00da5a762149dc682d7829451287e11b938e94594",
"third-party/image-webp/LICENSE-MIT": "c77a4cf9da729987d0fe7ccd811e3bd27393914ddf3d23467c18cc22954513b3",
@@ -1264,5 +1281,5 @@
"third-party/sqlx-sqlite/upstream.json": "0d9b56e81ace351d771e27f0e247e346fa6605f5e160176ea531d9f7720a10db",
"tools/schema/Cargo.toml": "28f0d16abedac58f4e3e899081ae8652633fc3619474418ec954d214672abbe8"
},
- "sha256": "620175982d6f9aabfc25b7915679af068c394f426be998c057ac8ebbb41c05a7"
+ "sha256": "03c3826d6010a486a3755d1e7d3de40efbb77305a0f52b0889f03ef83f4262e7"
}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
(maka) 08/11: test(gpui): use native Fast keystroke simulation
Posted by wy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch rust-preview
in repository https://gitbox.apache.org/repos/asf/maka.git
commit 7b60f527778058ab710f5f34d4a9809c082d2e04
Author: M4n5ter <m4...@gmail.com>
AuthorDate: Tue Oct 20 15:58:01 2026 +0800
test(gpui): use native Fast keystroke simulation
Run the imported focus-trap regressions through the existing VisualTestContext native key path.
Generated-by: OpenAI Codex
---
third-party/gpui-kit/crates/base/src/root.rs | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/third-party/gpui-kit/crates/base/src/root.rs b/third-party/gpui-kit/crates/base/src/root.rs
index 35d190c38b..d85acab52a 100644
--- a/third-party/gpui-kit/crates/base/src/root.rs
+++ b/third-party/gpui-kit/crates/base/src/root.rs
@@ -449,9 +449,9 @@ mod tests {
window.draw(cx).clear(cx);
});
- cx.dispatch_keystrokes("tab");
+ cx.simulate_keystrokes("tab");
assert_trapped_focus(cx, &container, &controls[0]);
- cx.dispatch_keystrokes("tab");
+ cx.simulate_keystrokes("tab");
assert_trapped_focus(cx, &container, &controls[1]);
assert_eq!(background_focuses.get(), 0);
}
@@ -464,9 +464,9 @@ mod tests {
window.draw(cx).clear(cx);
});
- cx.dispatch_keystrokes("shift-tab");
+ cx.simulate_keystrokes("shift-tab");
assert_trapped_focus(cx, &container, &controls[1]);
- cx.dispatch_keystrokes("shift-tab");
+ cx.simulate_keystrokes("shift-tab");
assert_trapped_focus(cx, &container, &controls[0]);
assert_eq!(background_focuses.get(), 0);
}
@@ -479,9 +479,9 @@ mod tests {
window.draw(cx).clear(cx);
});
- cx.dispatch_keystrokes("tab");
+ cx.simulate_keystrokes("tab");
assert_trapped_focus(cx, &container, &container);
- cx.dispatch_keystrokes("shift-tab");
+ cx.simulate_keystrokes("shift-tab");
assert_trapped_focus(cx, &container, &container);
assert_eq!(background_focuses.get(), 0);
}
@@ -497,12 +497,12 @@ mod tests {
// receive native focus notifications when a background control is used.
assert_eq!(background_focuses.get(), 1);
- cx.dispatch_keystrokes("tab");
+ cx.simulate_keystrokes("tab");
cx.update(|window, cx| {
window.draw(cx).clear(cx);
assert_eq!(window.focused(cx), Some(background[1].clone()));
});
- cx.dispatch_keystrokes("shift-tab");
+ cx.simulate_keystrokes("shift-tab");
cx.update(|window, cx| {
window.draw(cx).clear(cx);
assert_eq!(window.focused(cx), Some(background[0].clone()));
@@ -793,9 +793,9 @@ mod tests {
controls[index][1].focus(window, cx);
window.draw(cx).clear(cx);
});
- cx.dispatch_keystrokes("tab");
+ cx.simulate_keystrokes("tab");
assert_trapped_focus(cx, &containers[index], &controls[index][0]);
- cx.dispatch_keystrokes("shift-tab");
+ cx.simulate_keystrokes("shift-tab");
assert_trapped_focus(cx, &containers[index], &controls[index][1]);
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
(maka) 04/11: fix(desktop): satisfy current GPUI strict lint checks
Posted by wy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch rust-preview
in repository https://gitbox.apache.org/repos/asf/maka.git
commit 4b13afde3db6e723c57efa54f12d74f31649ddab
Author: M4n5ter <m4...@gmail.com>
AuthorDate: Tue Oct 20 14:35:39 2026 +0800
fix(desktop): satisfy current GPUI strict lint checks
Use the standard golden ratio constant, stable since Rust 1.94, with the same f32 bits as the existing value. Keep the replay chord macro explanation as an ordinary comment.
Generated-by: OpenAI Codex
---
third-party/gpui-fast/crates/gpui/src/elements/div.rs | 4 ++--
third-party/gpui-fast/crates/gpui/src/geometry.rs | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/third-party/gpui-fast/crates/gpui/src/elements/div.rs b/third-party/gpui-fast/crates/gpui/src/elements/div.rs
index 14ab003d6c..a386e75055 100644
--- a/third-party/gpui-fast/crates/gpui/src/elements/div.rs
+++ b/third-party/gpui-fast/crates/gpui/src/elements/div.rs
@@ -6084,8 +6084,8 @@ mod tests {
assert_eq!(node.numeric_value_step(), Some(1.0));
}
- /// Two focusable, clickable elements ("a" and "b") used to exercise the
- /// Enter/Space -> synthesized click press/release pairing.
+ // Two focusable, clickable elements ("a" and "b") used to exercise the
+ // Enter/Space -> synthesized click press/release pairing.
crate::actions!(native_click_replay_test, [ReplayChord]);
struct KeyboardActivationTest {
diff --git a/third-party/gpui-fast/crates/gpui/src/geometry.rs b/third-party/gpui-fast/crates/gpui/src/geometry.rs
index 1c87031c7a..c6f0d21f0d 100644
--- a/third-party/gpui-fast/crates/gpui/src/geometry.rs
+++ b/third-party/gpui-fast/crates/gpui/src/geometry.rs
@@ -3720,7 +3720,7 @@ pub const fn relative(fraction: f32) -> DefiniteLength {
/// Returns the Golden Ratio, i.e. `~(1.0 + sqrt(5.0)) / 2.0`.
pub const fn phi() -> DefiniteLength {
- relative(1.618_034)
+ relative(std::f32::consts::GOLDEN_RATIO)
}
/// Constructs a `Rems` value representing a length in rems.
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
(maka) 09/11: test(gpui): deliver animation frames in bounce regressions
Posted by wy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch rust-preview
in repository https://gitbox.apache.org/repos/asf/maka.git
commit fb00881401f5e939270a6d38a3d959fe09a4a902
Author: M4n5ter <m4...@gmail.com>
AuthorDate: Tue Oct 20 16:03:12 2026 +0800
test(gpui): deliver animation frames in bounce regressions
Drain the existing next-frame callbacks before each draw so retained inputs receive the same invalidation as the native frame loop. Keep the original clock intervals and return/geometry assertions.
Generated-by: OpenAI Codex
---
third-party/gpui-kit/crates/base/src/input/base/state.rs | 2 ++
1 file changed, 2 insertions(+)
diff --git a/third-party/gpui-kit/crates/base/src/input/base/state.rs b/third-party/gpui-kit/crates/base/src/input/base/state.rs
index cc17e6ae68..abc25e9c52 100644
--- a/third-party/gpui-kit/crates/base/src/input/base/state.rs
+++ b/third-party/gpui-kit/crates/base/src/input/base/state.rs
@@ -6477,6 +6477,7 @@ mod tests {
for _ in 0..120 {
cx.executor()
.advance_clock(std::time::Duration::from_millis(16));
+ cx.update(|window, cx| window.simulate_next_frame(cx));
cx.update(|window, cx| window.draw(cx).clear(cx));
}
view.input.read_with(&cx, |state, _| {
@@ -6589,6 +6590,7 @@ mod tests {
for _ in 0..120 {
cx.executor()
.advance_clock(std::time::Duration::from_millis(16));
+ cx.update(|window, cx| window.simulate_next_frame(cx));
cx.update(|window, cx| window.draw(cx).clear(cx));
}
assert_eq!(
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
(maka) 07/11: fix(gpui): share caret affinity across scroll projection
Posted by wy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch rust-preview
in repository https://gitbox.apache.org/repos/asf/maka.git
commit d99e031c3a3c8f4a9f165e8f7b9a72e0e8c9681e
Author: M4n5ter <m4...@gmail.com>
AuthorDate: Tue Oct 20 15:56:29 2026 +0800
fix(gpui): share caret affinity across scroll projection
Reuse the active caret affinity for both wrapped-row resolution and horizontal caret positioning after the Kit import.
Generated-by: OpenAI Codex
---
third-party/gpui-kit/crates/base/src/input/base/state.rs | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/third-party/gpui-kit/crates/base/src/input/base/state.rs b/third-party/gpui-kit/crates/base/src/input/base/state.rs
index 5dab8fc5f4..cc17e6ae68 100644
--- a/third-party/gpui-kit/crates/base/src/input/base/state.rs
+++ b/third-party/gpui-kit/crates/base/src/input/base/state.rs
@@ -2976,9 +2976,8 @@ impl<M: InputModeKind> InputBaseState<M> {
let row = point.row;
// Resolve the wrapped row even when the target is outside the last layout.
- let display_row = self
- .display_map
- .offset_to_display_row(offset, self.line_end_affinity_at(offset));
+ let affinity = self.line_end_affinity_at(offset);
+ let display_row = self.display_map.offset_to_display_row(offset, affinity);
let row_offset_y = line_height * display_row;
// For Right alignment use 0 margin: the cursor indicator is clamped inside bounds
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org