Merge remote-tracking branch 'origin/main' into polish-codegen

Antonio Scandurra created

Change summary

.gitignore                                                   |   1 
Cargo.lock                                                   |  87 
Cargo.toml                                                   |   6 
README.md                                                    |  32 
assets/icons/Icons/exit.svg                                  |   3 
assets/icons/select-all.svg                                  |   2 
assets/icons/stop_sharing.svg                                |   2 
assets/keymaps/default.json                                  |  11 
assets/keymaps/vim.json                                      |  35 
crates/collab_ui/src/collab_panel.rs                         |   8 
crates/collab_ui/src/collab_titlebar_item.rs                 |  13 
crates/collab_ui/src/face_pile.rs                            |  14 
crates/command_palette/src/command_palette.rs                |  45 
crates/diagnostics/src/items.rs                              |   3 
crates/editor/src/display_map.rs                             |  89 
crates/editor/src/editor.rs                                  | 145 +
crates/editor/src/editor_tests.rs                            |   2 
crates/editor/src/element.rs                                 | 453 +--
crates/editor/src/hover_popover.rs                           |   8 
crates/editor/src/items.rs                                   |  24 
crates/feedback/src/feedback_editor.rs                       |   9 
crates/file_finder/src/file_finder.rs                        |   9 
crates/gpui/Cargo.toml                                       |   3 
crates/gpui/examples/corner_radii.rs                         |  27 
crates/gpui/examples/text.rs                                 |   6 
crates/gpui/playground/Cargo.toml                            |  26 
crates/gpui/playground/src/div.rs                            | 108 -
crates/gpui/playground/src/element.rs                        | 158 -
crates/gpui/playground/src/hoverable.rs                      |  76 
crates/gpui/playground/src/interactive.rs                    |  34 
crates/gpui/playground/src/layout_context.rs                 |  54 
crates/gpui/playground/src/paint_context.rs                  |  71 
crates/gpui/playground/src/playground.rs                     |  83 
crates/gpui/playground/src/style.rs                          | 286 --
crates/gpui/playground/src/text.rs                           | 151 -
crates/gpui/playground/src/themes.rs                         |  84 
crates/gpui/playground/src/themes/rose_pine.rs               | 133 -
crates/gpui/playground_macros/src/styleable_helpers.rs       | 147 -
crates/gpui/playground_macros/src/tailwind_lengths.rs        |  99 
crates/gpui/src/app.rs                                       | 356 --
crates/gpui/src/app/test_app_context.rs                      |   1 
crates/gpui/src/app/window.rs                                | 202 +
crates/gpui/src/elements.rs                                  |  87 
crates/gpui/src/elements/align.rs                            |   9 
crates/gpui/src/elements/canvas.rs                           |  13 
crates/gpui/src/elements/clipped.rs                          |  18 
crates/gpui/src/elements/component.rs                        |  13 
crates/gpui/src/elements/constrained_box.rs                  |  22 
crates/gpui/src/elements/container.rs                        | 196 +
crates/gpui/src/elements/empty.rs                            |   7 
crates/gpui/src/elements/expanded.rs                         |  11 
crates/gpui/src/elements/flex.rs                             |  34 
crates/gpui/src/elements/hook.rs                             |  10 
crates/gpui/src/elements/image.rs                            |  14 
crates/gpui/src/elements/keystroke_label.rs                  |   7 
crates/gpui/src/elements/label.rs                            |  15 
crates/gpui/src/elements/list.rs                             |  78 
crates/gpui/src/elements/mouse_event_handler.rs              |  37 
crates/gpui/src/elements/overlay.rs                          |  42 
crates/gpui/src/elements/resizable.rs                        | 120 
crates/gpui/src/elements/stack.rs                            |  13 
crates/gpui/src/elements/svg.rs                              |  10 
crates/gpui/src/elements/text.rs                             |  33 
crates/gpui/src/elements/tooltip.rs                          |  13 
crates/gpui/src/elements/uniform_list.rs                     |  15 
crates/gpui/src/fonts.rs                                     |  85 
crates/gpui/src/geometry.rs                                  | 126 +
crates/gpui/src/gpui.rs                                      |   6 
crates/gpui/src/image_cache.rs                               |  99 
crates/gpui/src/platform.rs                                  |   1 
crates/gpui/src/platform/mac/platform.rs                     |   1 
crates/gpui/src/platform/mac/renderer.rs                     |  18 
crates/gpui/src/platform/mac/status_item.rs                  |   4 
crates/gpui/src/platform/mac/window.rs                       |  53 
crates/gpui/src/platform/test.rs                             |   4 
crates/gpui/src/scene.rs                                     | 215 -
crates/gpui/src/text_layout.rs                               |  21 
crates/gpui2/Cargo.toml                                      |  32 
crates/gpui2/src/adapter.rs                                  |  46 
crates/gpui2/src/color.rs                                    |  52 
crates/gpui2/src/element.rs                                  | 186 +
crates/gpui2/src/elements.rs                                 |  10 
crates/gpui2/src/elements/div.rs                             | 320 +++
crates/gpui2/src/elements/hoverable.rs                       | 105 +
crates/gpui2/src/elements/img.rs                             | 110 +
crates/gpui2/src/elements/pressable.rs                       | 108 +
crates/gpui2/src/elements/svg.rs                             |  84 
crates/gpui2/src/elements/text.rs                            | 109 +
crates/gpui2/src/gpui2.rs                                    |  22 
crates/gpui2/src/interactive.rs                              | 165 +
crates/gpui2/src/style.rs                                    | 604 ++++++
crates/gpui2/src/view.rs                                     |   0 
crates/gpui2/src/view_context.rs                             |  79 
crates/gpui2_macros/Cargo.toml                               |   4 
crates/gpui2_macros/src/derive_element.rs                    |  22 
crates/gpui2_macros/src/derive_into_element.rs               |   2 
crates/gpui2_macros/src/gpui2_macros.rs                      |   6 
crates/gpui2_macros/src/styleable_helpers.rs                 | 326 +++
crates/gpui_macros/src/gpui_macros.rs                        |   7 
crates/language/src/language.rs                              |  79 
crates/language_selector/src/active_buffer_language.rs       |   2 
crates/language_tools/src/lsp_log.rs                         |  16 
crates/live_kit_client/LiveKitBridge/Package.resolved        |   4 
crates/project/src/search.rs                                 |  37 
crates/refineable/derive_refineable/src/derive_refineable.rs |  56 
crates/refineable/src/refineable.rs                          |  46 
crates/search/src/buffer_search.rs                           | 330 ++
crates/search/src/project_search.rs                          |  45 
crates/search/src/search.rs                                  |  38 
crates/semantic_index/src/embedding.rs                       |  70 
crates/semantic_index/src/semantic_index.rs                  |  11 
crates/semantic_index/src/semantic_index_tests.rs            |   6 
crates/storybook/Cargo.lock                                  |   2 
crates/storybook/Cargo.toml                                  |  23 
crates/storybook/docs/thoughts.md                            |   0 
crates/storybook/src/collab_panel.rs                         | 177 +
crates/storybook/src/components.rs                           |  65 
crates/storybook/src/components/icon_button.rs               |  50 
crates/storybook/src/components/tab.rs                       |  55 
crates/storybook/src/element_ext.rs                          |  22 
crates/storybook/src/modules.rs                              |   3 
crates/storybook/src/modules/tab_bar.rs                      |  82 
crates/storybook/src/storybook.rs                            | 110 +
crates/storybook/src/theme.rs                                | 192 +
crates/storybook/src/workspace.rs                            | 448 ++++
crates/terminal_view/README.md                               |   8 
crates/terminal_view/src/terminal_element.rs                 |  49 
crates/terminal_view/src/terminal_view.rs                    |  24 
crates/theme/src/theme.rs                                    |  25 
crates/util/src/arc_cow.rs                                   |  74 
crates/util/src/http.rs                                      |   2 
crates/util/src/util.rs                                      |  17 
crates/vim/Cargo.toml                                        |   2 
crates/vim/src/editor_events.rs                              |   3 
crates/vim/src/insert.rs                                     | 118 +
crates/vim/src/motion.rs                                     | 136 
crates/vim/src/normal.rs                                     |  85 
crates/vim/src/normal/case.rs                                |  16 
crates/vim/src/normal/change.rs                              | 170 
crates/vim/src/normal/delete.rs                              |  94 
crates/vim/src/normal/paste.rs                               |   1 
crates/vim/src/normal/repeat.rs                              | 528 +++++
crates/vim/src/normal/scroll.rs                              |   2 
crates/vim/src/normal/search.rs                              |   4 
crates/vim/src/normal/substitute.rs                          |   6 
crates/vim/src/state.rs                                      |  76 
crates/vim/src/test.rs                                       | 100 
crates/vim/src/test/neovim_backed_test_context.rs            |  26 
crates/vim/src/test/vim_test_context.rs                      |  15 
crates/vim/src/vim.rs                                        | 193 +
crates/vim/src/visual.rs                                     |   2 
crates/vim/test_data/test_change_case.json                   |   5 
crates/vim/test_data/test_clear_counts.json                  |   7 
crates/vim/test_data/test_delete_with_counts.json            |  16 
crates/vim/test_data/test_dot_repeat.json                    |  38 
crates/vim/test_data/test_insert_with_counts.json            |  36 
crates/vim/test_data/test_insert_with_repeat.json            |  23 
crates/vim/test_data/test_join_lines.json                    |  13 
crates/vim/test_data/test_repeat_motion_counts.json          |  13 
crates/vim/test_data/test_repeat_visual.json                 |  51 
crates/vim/test_data/test_wrapped_lines.json                 |   6 
crates/vim/test_data/test_zero.json                          |   7 
crates/workspace/src/item.rs                                 |  10 
crates/workspace/src/pane.rs                                 | 222 +
crates/workspace/src/pane/dragged_item_receiver.rs           |  17 
crates/workspace/src/pane_group.rs                           |  27 
crates/workspace/src/searchable.rs                           |  18 
crates/workspace/src/shared_screen.rs                        |   4 
crates/workspace/src/status_bar.rs                           |  15 
crates/workspace/src/workspace.rs                            |  35 
crates/zed/Cargo.toml                                        |   1 
crates/zed/src/languages.rs                                  |   1 
crates/zed/src/languages/nu/brackets.scm                     |   4 
crates/zed/src/languages/nu/config.toml                      |   9 
crates/zed/src/languages/nu/highlights.scm                   | 302 +++
crates/zed/src/languages/nu/indents.scm                      |   3 
crates/zed/src/languages/python.rs                           |  38 
crates/zed/src/languages/rust.rs                             |   1 
crates/zed/src/menus.rs                                      |   7 
crates/zed/src/zed.rs                                        |  52 
styles/src/build_themes.ts                                   |   3 
styles/src/style_tree/search.ts                              |  87 
test.rs                                                      | 416 +--
183 files changed, 8,324 insertions(+), 3,863 deletions(-)

Detailed changes

.gitignore πŸ”—

@@ -1,4 +1,5 @@
 **/target
+**/cargo-target
 /zed.xcworkspace
 .DS_Store
 /plugins/bin

Cargo.lock πŸ”—

@@ -3164,6 +3164,7 @@ dependencies = [
  "sqlez",
  "sum_tree",
  "taffy",
+ "thiserror",
  "time 0.3.27",
  "tiny-skia",
  "usvg",
@@ -3172,6 +3173,36 @@ dependencies = [
  "waker-fn",
 ]
 
+[[package]]
+name = "gpui2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "derive_more",
+ "futures 0.3.28",
+ "gpui",
+ "gpui2_macros",
+ "log",
+ "parking_lot 0.11.2",
+ "refineable",
+ "rust-embed",
+ "serde",
+ "settings",
+ "simplelog",
+ "smallvec",
+ "theme",
+ "util",
+]
+
+[[package]]
+name = "gpui2_macros"
+version = "0.1.0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "gpui_macros"
 version = "0.1.0"
@@ -5232,33 +5263,6 @@ version = "0.3.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
 
-[[package]]
-name = "playground"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "derive_more",
- "gpui",
- "log",
- "parking_lot 0.11.2",
- "playground_macros",
- "refineable",
- "serde",
- "simplelog",
- "smallvec",
- "taffy",
- "util",
-]
-
-[[package]]
-name = "playground_macros"
-version = "0.1.0"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
 [[package]]
 name = "plist"
 version = "1.5.0"
@@ -7368,6 +7372,21 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
 
+[[package]]
+name = "storybook"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "gpui2",
+ "log",
+ "rust-embed",
+ "serde",
+ "settings",
+ "simplelog",
+ "theme",
+ "util",
+]
+
 [[package]]
 name = "stringprep"
 version = "0.1.3"
@@ -7567,7 +7586,7 @@ dependencies = [
 [[package]]
 name = "taffy"
 version = "0.3.11"
-source = "git+https://github.com/DioxusLabs/taffy?rev=dab541d6104d58e2e10ce90c4a1dad0b703160cd#dab541d6104d58e2e10ce90c4a1dad0b703160cd"
+source = "git+https://github.com/DioxusLabs/taffy?rev=4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e#4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e"
 dependencies = [
  "arrayvec 0.7.4",
  "grid",
@@ -8407,6 +8426,15 @@ dependencies = [
  "tree-sitter",
 ]
 
+[[package]]
+name = "tree-sitter-nu"
+version = "0.0.1"
+source = "git+https://github.com/nushell/tree-sitter-nu?rev=786689b0562b9799ce53e824cb45a1a2a04dc673#786689b0562b9799ce53e824cb45a1a2a04dc673"
+dependencies = [
+ "cc",
+ "tree-sitter",
+]
+
 [[package]]
 name = "tree-sitter-php"
 version = "0.19.1"
@@ -8830,12 +8858,14 @@ dependencies = [
  "collections",
  "command_palette",
  "editor",
+ "futures 0.3.28",
  "gpui",
  "indoc",
  "itertools",
  "language",
  "language_selector",
  "log",
+ "lsp",
  "nvim-rs",
  "parking_lot 0.11.2",
  "project",
@@ -9867,6 +9897,7 @@ dependencies = [
  "tree-sitter-lua",
  "tree-sitter-markdown",
  "tree-sitter-nix",
+ "tree-sitter-nu",
  "tree-sitter-php",
  "tree-sitter-python",
  "tree-sitter-racket",

Cargo.toml πŸ”—

@@ -32,9 +32,9 @@ members = [
     "crates/git",
     "crates/go_to_line",
     "crates/gpui",
-    "crates/gpui/playground",
-    "crates/gpui/playground_macros",
     "crates/gpui_macros",
+    "crates/gpui2",
+    "crates/gpui2_macros",
     "crates/install_cli",
     "crates/journal",
     "crates/language",
@@ -63,6 +63,7 @@ members = [
     "crates/sqlez",
     "crates/sqlez_macros",
     "crates/feature_flags",
+    "crates/storybook",
     "crates/sum_tree",
     "crates/terminal",
     "crates/text",
@@ -141,6 +142,7 @@ tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-rack
 tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "f545a41f57502e1b5ddf2a6668896c1b0620f930"}
 tree-sitter-lua = "0.0.14"
 tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
+tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"}
 
 [patch.crates-io]
 tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" }

README.md πŸ”—

@@ -8,7 +8,26 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
 
 ### Dependencies
 
-* Install [Postgres.app](https://postgresapp.com) and start it.
+* Install Xcode from https://apps.apple.com/us/app/xcode/id497799835?mt=12, and accept the license:
+  ```
+  sudo xcodebuild -license
+  ```
+  
+* Install homebrew, node and rustup-init (rutup, rust, cargo, etc.)
+  ```
+  /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
+  brew install node rustup-init
+  rustup-init # follow the installation steps
+  ```
+  
+* Install postgres and configure the database
+  ```
+  brew install postgresql@15
+  brew services start postgresql@15
+  psql -c "CREATE ROLE postgres SUPERUSER LOGIN" postgres
+  psql -U postgres -c "CREATE DATABASE zed"
+  ```
+  
 * Install the `LiveKit` server and the `foreman` process supervisor:
 
     ```
@@ -41,6 +60,17 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
     GITHUB_TOKEN=<$token> script/bootstrap
     ```
 
+* Now try running zed with collaboration disabled:
+  ```
+  cargo run
+  ```
+
+### Common errors
+
+* `xcrun: error: unable to find utility "metal", not a developer tool or in PATH`
+  * You need to install Xcode and then run: `xcode-select --switch /Applications/Xcode.app/Contents/Developer`
+  * (see https://github.com/gfx-rs/gfx/issues/2309)
+
 ### Testing against locally-running servers
 
 Start the web and collab servers:

assets/icons/Icons/exit.svg πŸ”—

@@ -0,0 +1,3 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M3 1C2.44771 1 2 1.44772 2 2V13C2 13.5523 2.44772 14 3 14H10.5C10.7761 14 11 13.7761 11 13.5C11 13.2239 10.7761 13 10.5 13H3V2L10.5 2C10.7761 2 11 1.77614 11 1.5C11 1.22386 10.7761 1 10.5 1H3ZM12.6036 4.89645C12.4083 4.70118 12.0917 4.70118 11.8964 4.89645C11.7012 5.09171 11.7012 5.40829 11.8964 5.60355L13.2929 7H6.5C6.22386 7 6 7.22386 6 7.5C6 7.77614 6.22386 8 6.5 8H13.2929L11.8964 9.39645C11.7012 9.59171 11.7012 9.90829 11.8964 10.1036C12.0917 10.2988 12.4083 10.2988 12.6036 10.1036L14.8536 7.85355C15.0488 7.65829 15.0488 7.34171 14.8536 7.14645L12.6036 4.89645Z" fill="black"/>
+</svg>

assets/icons/select-all.svg πŸ”—

@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.5 7V9.5M9.5 12V9.5M12 9.5H9.5M7 9.5H9.5M9.5 9.5L11.1667 7.83333M9.5 9.5L7.83333 11.1667M9.5 9.5L11.1667 11.1667M9.5 9.5L7.83333 7.83333" stroke="#11181C" stroke-width="1.25" stroke-linecap="round"/>

assets/icons/stop_sharing.svg πŸ”—

@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7.70312 4L7.26046 2.97338C7.10239 2.60678 6.74141 2.36933 6.34219 2.36933H2.5C2.22386 2.36933 2 2.59319 2 2.86933V4.375V8" stroke="#11181C" stroke-width="1.25" stroke-linecap="round"/>

assets/keymaps/default.json πŸ”—

@@ -231,7 +231,14 @@
     }
   },
   {
-    "context": "BufferSearchBar > Editor",
+    "context": "BufferSearchBar && in_replace",
+    "bindings": {
+      "enter": "search::ReplaceNext",
+      "cmd-enter": "search::ReplaceAll"
+    }
+  },
+  {
+    "context": "BufferSearchBar && !in_replace > Editor",
     "bindings": {
       "up": "search::PreviousHistoryQuery",
       "down": "search::NextHistoryQuery"
@@ -533,7 +540,7 @@
       // TODO: Move this to a dock open action
       "cmd-shift-c": "collab_panel::ToggleFocus",
       "cmd-alt-i": "zed::DebugElements",
-      "ctrl-:": "editor::ToggleInlayHints",
+      "ctrl-:": "editor::ToggleInlayHints"
     }
   },
   {

assets/keymaps/vim.json πŸ”—

@@ -32,6 +32,8 @@
       "right": "vim::Right",
       "$": "vim::EndOfLine",
       "^": "vim::FirstNonWhitespace",
+      "_": "vim::StartOfLineDownward",
+      "g _": "vim::EndOfLineDownward",
       "shift-g": "vim::EndOfDocument",
       "w": "vim::NextWordStart",
       "{": "vim::StartOfParagraph",
@@ -198,6 +200,18 @@
       "z c": "editor::Fold",
       "z o": "editor::UnfoldLines",
       "z f": "editor::FoldSelectedRanges",
+      "shift-z shift-q": [
+        "pane::CloseActiveItem",
+        {
+          "saveBehavior": "dontSave"
+        }
+      ],
+      "shift-z shift-z": [
+        "pane::CloseActiveItem",
+        {
+          "saveBehavior": "promptOnConflict"
+        }
+      ],
       // Count support
       "1": [
         "vim::Number",
@@ -314,8 +328,9 @@
     }
   },
   {
-    "context": "Editor && vim_mode == normal && (vim_operator == none || vim_operator == n) && !VimWaiting",
+    "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
     "bindings": {
+      ".": "vim::Repeat",
       "c": [
         "vim::PushOperator",
         "Change"
@@ -326,15 +341,12 @@
         "Delete"
       ],
       "shift-d": "vim::DeleteToEndOfLine",
-      "shift-j": "editor::JoinLines",
+      "shift-j": "vim::JoinLines",
       "y": [
         "vim::PushOperator",
         "Yank"
       ],
-      "i": [
-        "vim::SwitchMode",
-        "Insert"
-      ],
+      "i": "vim::InsertBefore",
       "shift-i": "vim::InsertFirstNonWhitespace",
       "a": "vim::InsertAfter",
       "shift-a": "vim::InsertEndOfLine",
@@ -379,7 +391,7 @@
     }
   },
   {
-    "context": "Editor && vim_operator == n",
+    "context": "Editor && VimCount",
     "bindings": {
       "0": [
         "vim::Number",
@@ -448,13 +460,12 @@
       ],
       "s": "vim::Substitute",
       "shift-s": "vim::SubstituteLine",
+      "shift-r": "vim::SubstituteLine",
       "c": "vim::Substitute",
       "~": "vim::ChangeCase",
-      "shift-i": [
-        "vim::SwitchMode",
-        "Insert"
-      ],
+      "shift-i": "vim::InsertBefore",
       "shift-a": "vim::InsertAfter",
+      "shift-j": "vim::JoinLines",
       "r": [
         "vim::PushOperator",
         "Replace"
@@ -488,7 +499,7 @@
             "around": true
           }
         }
-      ],
+      ]
     }
   },
   {

crates/collab_ui/src/collab_panel.rs πŸ”—

@@ -2434,14 +2434,14 @@ fn render_tree_branch(
     let cap_height = row_style.cap_height(font_cache);
     let baseline_offset = row_style.baseline_offset(font_cache) + (size.y() - line_height) / 2.;
 
-    Canvas::new(move |scene, bounds, _, _, _| {
-        scene.paint_layer(None, |scene| {
+    Canvas::new(move |bounds, _, _, cx| {
+        cx.paint_layer(None, |cx| {
             let start_x = bounds.min_x() + (bounds.width() / 2.) - (branch_style.width / 2.);
             let end_x = bounds.max_x();
             let start_y = bounds.min_y();
             let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
 
-            scene.push_quad(gpui::Quad {
+            cx.scene().push_quad(gpui::Quad {
                 bounds: RectF::from_points(
                     vec2f(start_x, start_y),
                     vec2f(
@@ -2453,7 +2453,7 @@ fn render_tree_branch(
                 border: gpui::Border::default(),
                 corner_radii: (0.).into(),
             });
-            scene.push_quad(gpui::Quad {
+            cx.scene().push_quad(gpui::Quad {
                 bounds: RectF::from_points(
                     vec2f(start_x, end_y),
                     vec2f(end_x, end_y + branch_style.width),

crates/collab_ui/src/collab_titlebar_item.rs πŸ”—

@@ -13,8 +13,8 @@ use gpui::{
     geometry::{rect::RectF, vector::vec2f, PathBuilder},
     json::{self, ToJson},
     platform::{CursorStyle, MouseButton},
-    AppContext, Entity, ImageData, LayoutContext, ModelHandle, PaintContext, SceneBuilder,
-    Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
+    AppContext, Entity, ImageData, ModelHandle, Subscription, View, ViewContext, ViewHandle,
+    WeakViewHandle,
 };
 use picker::PickerEvent;
 use project::{Project, RepositoryEntry};
@@ -771,7 +771,7 @@ impl CollabTitlebarItem {
                 })
                 .with_tooltip::<ToggleUserMenu>(
                     0,
-                    "Toggle user menu".to_owned(),
+                    "Toggle User Menu".to_owned(),
                     Some(Box::new(ToggleUserMenu)),
                     tooltip,
                     cx,
@@ -1165,19 +1165,18 @@ impl Element<CollabTitlebarItem> for AvatarRibbon {
         &mut self,
         constraint: gpui::SizeConstraint,
         _: &mut CollabTitlebarItem,
-        _: &mut LayoutContext<CollabTitlebarItem>,
+        _: &mut ViewContext<CollabTitlebarItem>,
     ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
         (constraint.max, ())
     }
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         _: RectF,
         _: &mut Self::LayoutState,
         _: &mut CollabTitlebarItem,
-        _: &mut PaintContext<CollabTitlebarItem>,
+        cx: &mut ViewContext<CollabTitlebarItem>,
     ) -> Self::PaintState {
         let mut path = PathBuilder::new();
         path.reset(bounds.lower_left());
@@ -1188,7 +1187,7 @@ impl Element<CollabTitlebarItem> for AvatarRibbon {
         path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
         path.curve_to(bounds.lower_right(), bounds.upper_right());
         path.line_to(bounds.lower_left());
-        scene.push_path(path.build(self.color, None));
+        cx.scene().push_path(path.build(self.color, None));
     }
 
     fn rect_for_text_range(

crates/collab_ui/src/face_pile.rs πŸ”—

@@ -7,7 +7,7 @@ use gpui::{
     },
     json::ToJson,
     serde_json::{self, json},
-    AnyElement, Axis, Element, LayoutContext, PaintContext, SceneBuilder, View, ViewContext,
+    AnyElement, Axis, Element, View, ViewContext,
 };
 
 pub(crate) struct FacePile<V: View> {
@@ -32,7 +32,7 @@ impl<V: View> Element<V> for FacePile<V> {
         &mut self,
         constraint: gpui::SizeConstraint,
         view: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
 
@@ -53,12 +53,11 @@ impl<V: View> Element<V> for FacePile<V> {
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         _layout: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> Self::PaintState {
         let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
 
@@ -69,9 +68,10 @@ impl<V: View> Element<V> for FacePile<V> {
             let size = face.size();
             origin_x -= size.x();
             let origin_y = origin_y + (bounds.height() - size.y()) / 2.0;
-            scene.paint_layer(None, |scene| {
-                face.paint(scene, vec2f(origin_x, origin_y), visible_bounds, view, cx);
-            });
+
+            cx.scene().push_layer(None);
+            face.paint(vec2f(origin_x, origin_y), visible_bounds, view, cx);
+            cx.scene().pop_layer();
             origin_x += self.overlap;
         }
 

crates/command_palette/src/command_palette.rs πŸ”—

@@ -1,11 +1,11 @@
-use collections::CommandPaletteFilter;
+use collections::{CommandPaletteFilter, HashMap};
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
     actions, anyhow::anyhow, elements::*, keymap_matcher::Keystroke, Action, AnyWindowHandle,
     AppContext, Element, MouseState, ViewContext,
 };
 use picker::{Picker, PickerDelegate, PickerEvent};
-use std::cmp;
+use std::cmp::{self, Reverse};
 use util::ResultExt;
 use workspace::Workspace;
 
@@ -33,13 +33,18 @@ pub enum Event {
         action: Box<dyn Action>,
     },
 }
-
 struct Command {
     name: String,
     action: Box<dyn Action>,
     keystrokes: Vec<Keystroke>,
 }
 
+/// Hit count for each command in the palette.
+/// We only account for commands triggered directly via command palette and not by e.g. keystrokes because
+/// if an user already knows a keystroke for a command, they are unlikely to use a command palette to look for it.
+#[derive(Default)]
+struct HitCounts(HashMap<String, usize>);
+
 fn toggle_command_palette(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
     let focused_view_id = cx.focused_view_id().unwrap_or_else(|| cx.view_id());
     workspace.toggle_modal(cx, |_, cx| {
@@ -83,7 +88,7 @@ impl PickerDelegate for CommandPaletteDelegate {
         let view_id = self.focused_view_id;
         let window = cx.window();
         cx.spawn(move |picker, mut cx| async move {
-            let actions = window
+            let mut actions = window
                 .available_actions(view_id, &cx)
                 .into_iter()
                 .flatten()
@@ -112,6 +117,16 @@ impl PickerDelegate for CommandPaletteDelegate {
                     }
                 })
                 .collect::<Vec<_>>();
+            let actions = cx.read(move |cx| {
+                let hit_counts = cx.optional_global::<HitCounts>();
+                actions.sort_by_key(|action| {
+                    (
+                        Reverse(hit_counts.and_then(|map| map.0.get(&action.name)).cloned()),
+                        action.name.clone(),
+                    )
+                });
+                actions
+            });
             let candidates = actions
                 .iter()
                 .enumerate()
@@ -166,7 +181,12 @@ impl PickerDelegate for CommandPaletteDelegate {
             let window = cx.window();
             let focused_view_id = self.focused_view_id;
             let action_ix = self.matches[self.selected_ix].candidate_id;
-            let action = self.actions.remove(action_ix).action;
+            let command = self.actions.remove(action_ix);
+            cx.update_default_global(|hit_counts: &mut HitCounts, _| {
+                *hit_counts.0.entry(command.name).or_default() += 1;
+            });
+            let action = command.action;
+
             cx.app_context()
                 .spawn(move |mut cx| async move {
                     window
@@ -319,6 +339,21 @@ mod tests {
             workspace.modal::<CommandPalette>().unwrap()
         });
 
+        palette
+            .update(cx, |palette, cx| {
+                // Fill up palette's command list by running an empty query;
+                // we only need it to subsequently assert that the palette is initially
+                // sorted by command's name.
+                palette.delegate_mut().update_matches("".to_string(), cx)
+            })
+            .await;
+
+        palette.update(cx, |palette, _| {
+            let is_sorted =
+                |actions: &[Command]| actions.windows(2).all(|pair| pair[0].name <= pair[1].name);
+            assert!(is_sorted(&palette.delegate().actions));
+        });
+
         palette
             .update(cx, |palette, cx| {
                 palette

crates/diagnostics/src/items.rs πŸ”—

@@ -32,7 +32,8 @@ impl DiagnosticIndicator {
                 this.in_progress_checks.insert(*language_server_id);
                 cx.notify();
             }
-            project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
+            project::Event::DiskBasedDiagnosticsFinished { language_server_id }
+            | project::Event::LanguageServerRemoved(language_server_id) => {
                 this.summary = project.read(cx).diagnostic_summary(cx);
                 this.in_progress_checks.remove(language_server_id);
                 cx.notify();

crates/editor/src/display_map.rs πŸ”—

@@ -555,67 +555,6 @@ impl DisplaySnapshot {
             })
     }
 
-    /// Returns an iterator of the start positions of the occurrences of `target` in the `self` after `from`
-    /// Stops if `condition` returns false for any of the character position pairs observed.
-    pub fn find_while<'a>(
-        &'a self,
-        from: DisplayPoint,
-        target: &str,
-        condition: impl FnMut(char, DisplayPoint) -> bool + 'a,
-    ) -> impl Iterator<Item = DisplayPoint> + 'a {
-        Self::find_internal(self.chars_at(from), target.chars().collect(), condition)
-    }
-
-    /// Returns an iterator of the end positions of the occurrences of `target` in the `self` before `from`
-    /// Stops if `condition` returns false for any of the character position pairs observed.
-    pub fn reverse_find_while<'a>(
-        &'a self,
-        from: DisplayPoint,
-        target: &str,
-        condition: impl FnMut(char, DisplayPoint) -> bool + 'a,
-    ) -> impl Iterator<Item = DisplayPoint> + 'a {
-        Self::find_internal(
-            self.reverse_chars_at(from),
-            target.chars().rev().collect(),
-            condition,
-        )
-    }
-
-    fn find_internal<'a>(
-        iterator: impl Iterator<Item = (char, DisplayPoint)> + 'a,
-        target: Vec<char>,
-        mut condition: impl FnMut(char, DisplayPoint) -> bool + 'a,
-    ) -> impl Iterator<Item = DisplayPoint> + 'a {
-        // List of partial matches with the index of the last seen character in target and the starting point of the match
-        let mut partial_matches: Vec<(usize, DisplayPoint)> = Vec::new();
-        iterator
-            .take_while(move |(ch, point)| condition(*ch, *point))
-            .filter_map(move |(ch, point)| {
-                if Some(&ch) == target.get(0) {
-                    partial_matches.push((0, point));
-                }
-
-                let mut found = None;
-                // Keep partial matches that have the correct next character
-                partial_matches.retain_mut(|(match_position, match_start)| {
-                    if target.get(*match_position) == Some(&ch) {
-                        *match_position += 1;
-                        if *match_position == target.len() {
-                            found = Some(match_start.clone());
-                            // This match is completed. No need to keep tracking it
-                            false
-                        } else {
-                            true
-                        }
-                    } else {
-                        false
-                    }
-                });
-
-                found
-            })
-    }
-
     pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
         let mut count = 0;
         let mut column = 0;
@@ -933,7 +872,7 @@ pub mod tests {
     use smol::stream::StreamExt;
     use std::{env, sync::Arc};
     use theme::SyntaxTheme;
-    use util::test::{marked_text_offsets, marked_text_ranges, sample_text};
+    use util::test::{marked_text_ranges, sample_text};
     use Bias::*;
 
     #[gpui::test(iterations = 100)]
@@ -1744,32 +1683,6 @@ pub mod tests {
         )
     }
 
-    #[test]
-    fn test_find_internal() {
-        assert("This is a Λ‡test of find internal", "test");
-        assert("Some text ˇaˇaˇaa with repeated characters", "aa");
-
-        fn assert(marked_text: &str, target: &str) {
-            let (text, expected_offsets) = marked_text_offsets(marked_text);
-
-            let chars = text
-                .chars()
-                .enumerate()
-                .map(|(index, ch)| (ch, DisplayPoint::new(0, index as u32)));
-            let target = target.chars();
-
-            assert_eq!(
-                expected_offsets
-                    .into_iter()
-                    .map(|offset| offset as u32)
-                    .collect::<Vec<_>>(),
-                DisplaySnapshot::find_internal(chars, target.collect(), |_, _| true)
-                    .map(|point| point.column())
-                    .collect::<Vec<_>>()
-            )
-        }
-    }
-
     fn syntax_chunks<'a>(
         rows: Range<u32>,
         map: &ModelHandle<DisplayMap>,

crates/editor/src/editor.rs πŸ”—

@@ -572,7 +572,7 @@ pub struct Editor {
     project: Option<ModelHandle<Project>>,
     focused: bool,
     blink_manager: ModelHandle<BlinkManager>,
-    show_local_selections: bool,
+    pub show_local_selections: bool,
     mode: EditorMode,
     replica_id_mapping: Option<HashMap<ReplicaId, ReplicaId>>,
     show_gutter: bool,
@@ -2273,10 +2273,6 @@ impl Editor {
         if self.read_only {
             return;
         }
-        if !self.input_enabled {
-            cx.emit(Event::InputIgnored { text });
-            return;
-        }
 
         let selections = self.selections.all_adjusted(cx);
         let mut brace_inserted = false;
@@ -3211,17 +3207,30 @@ impl Editor {
             .count();
 
         let snapshot = self.buffer.read(cx).snapshot(cx);
+        let mut range_to_replace: Option<Range<isize>> = None;
         let mut ranges = Vec::new();
         for selection in &selections {
             if snapshot.contains_str_at(selection.start.saturating_sub(lookbehind), &old_text) {
                 let start = selection.start.saturating_sub(lookbehind);
                 let end = selection.end + lookahead;
+                if selection.id == newest_selection.id {
+                    range_to_replace = Some(
+                        ((start + common_prefix_len) as isize - selection.start as isize)
+                            ..(end as isize - selection.start as isize),
+                    );
+                }
                 ranges.push(start + common_prefix_len..end);
             } else {
                 common_prefix_len = 0;
                 ranges.clear();
                 ranges.extend(selections.iter().map(|s| {
                     if s.id == newest_selection.id {
+                        range_to_replace = Some(
+                            old_range.start.to_offset_utf16(&snapshot).0 as isize
+                                - selection.start as isize
+                                ..old_range.end.to_offset_utf16(&snapshot).0 as isize
+                                    - selection.start as isize,
+                        );
                         old_range.clone()
                     } else {
                         s.start..s.end
@@ -3232,6 +3241,11 @@ impl Editor {
         }
         let text = &text[common_prefix_len..];
 
+        cx.emit(Event::InputHandled {
+            utf16_range_to_replace: range_to_replace,
+            text: text.into(),
+        });
+
         self.transact(cx, |this, cx| {
             if let Some(mut snippet) = snippet {
                 snippet.text = text.to_string();
@@ -3689,6 +3703,10 @@ impl Editor {
 
                 self.report_copilot_event(Some(completion.uuid.clone()), true, cx)
             }
+            cx.emit(Event::InputHandled {
+                utf16_range_to_replace: None,
+                text: suggestion.text.to_string().into(),
+            });
             self.insert_with_autoindent_mode(&suggestion.text.to_string(), None, cx);
             cx.notify();
             true
@@ -8437,6 +8455,41 @@ impl Editor {
     pub fn inlay_hint_cache(&self) -> &InlayHintCache {
         &self.inlay_hint_cache
     }
+
+    pub fn replay_insert_event(
+        &mut self,
+        text: &str,
+        relative_utf16_range: Option<Range<isize>>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if !self.input_enabled {
+            cx.emit(Event::InputIgnored { text: text.into() });
+            return;
+        }
+        if let Some(relative_utf16_range) = relative_utf16_range {
+            let selections = self.selections.all::<OffsetUtf16>(cx);
+            self.change_selections(None, cx, |s| {
+                let new_ranges = selections.into_iter().map(|range| {
+                    let start = OffsetUtf16(
+                        range
+                            .head()
+                            .0
+                            .saturating_add_signed(relative_utf16_range.start),
+                    );
+                    let end = OffsetUtf16(
+                        range
+                            .head()
+                            .0
+                            .saturating_add_signed(relative_utf16_range.end),
+                    );
+                    start..end
+                });
+                s.select_ranges(new_ranges);
+            });
+        }
+
+        self.handle_input(text, cx);
+    }
 }
 
 fn document_to_inlay_range(
@@ -8525,6 +8578,10 @@ pub enum Event {
     InputIgnored {
         text: Arc<str>,
     },
+    InputHandled {
+        utf16_range_to_replace: Option<Range<isize>>,
+        text: Arc<str>,
+    },
     ExcerptsAdded {
         buffer: ModelHandle<Buffer>,
         predecessor: ExcerptId,
@@ -8742,29 +8799,51 @@ impl View for Editor {
         text: &str,
         cx: &mut ViewContext<Self>,
     ) {
+        if !self.input_enabled {
+            cx.emit(Event::InputIgnored { text: text.into() });
+            return;
+        }
+
         self.transact(cx, |this, cx| {
-            if this.input_enabled {
-                let new_selected_ranges = if let Some(range_utf16) = range_utf16 {
-                    let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end);
-                    Some(this.selection_replacement_ranges(range_utf16, cx))
-                } else {
-                    this.marked_text_ranges(cx)
-                };
+            let new_selected_ranges = if let Some(range_utf16) = range_utf16 {
+                let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end);
+                Some(this.selection_replacement_ranges(range_utf16, cx))
+            } else {
+                this.marked_text_ranges(cx)
+            };
 
-                if let Some(new_selected_ranges) = new_selected_ranges {
-                    this.change_selections(None, cx, |selections| {
-                        selections.select_ranges(new_selected_ranges)
-                    });
-                }
+            let range_to_replace = new_selected_ranges.as_ref().and_then(|ranges_to_replace| {
+                let newest_selection_id = this.selections.newest_anchor().id;
+                this.selections
+                    .all::<OffsetUtf16>(cx)
+                    .iter()
+                    .zip(ranges_to_replace.iter())
+                    .find_map(|(selection, range)| {
+                        if selection.id == newest_selection_id {
+                            Some(
+                                (range.start.0 as isize - selection.head().0 as isize)
+                                    ..(range.end.0 as isize - selection.head().0 as isize),
+                            )
+                        } else {
+                            None
+                        }
+                    })
+            });
+
+            cx.emit(Event::InputHandled {
+                utf16_range_to_replace: range_to_replace,
+                text: text.into(),
+            });
+
+            if let Some(new_selected_ranges) = new_selected_ranges {
+                this.change_selections(None, cx, |selections| {
+                    selections.select_ranges(new_selected_ranges)
+                });
             }
 
             this.handle_input(text, cx);
         });
 
-        if !self.input_enabled {
-            return;
-        }
-
         if let Some(transaction) = self.ime_transaction {
             self.buffer.update(cx, |buffer, cx| {
                 buffer.group_until_transaction(transaction, cx);
@@ -8782,6 +8861,7 @@ impl View for Editor {
         cx: &mut ViewContext<Self>,
     ) {
         if !self.input_enabled {
+            cx.emit(Event::InputIgnored { text: text.into() });
             return;
         }
 
@@ -8806,6 +8886,29 @@ impl View for Editor {
                 None
             };
 
+            let range_to_replace = ranges_to_replace.as_ref().and_then(|ranges_to_replace| {
+                let newest_selection_id = this.selections.newest_anchor().id;
+                this.selections
+                    .all::<OffsetUtf16>(cx)
+                    .iter()
+                    .zip(ranges_to_replace.iter())
+                    .find_map(|(selection, range)| {
+                        if selection.id == newest_selection_id {
+                            Some(
+                                (range.start.0 as isize - selection.head().0 as isize)
+                                    ..(range.end.0 as isize - selection.head().0 as isize),
+                            )
+                        } else {
+                            None
+                        }
+                    })
+            });
+
+            cx.emit(Event::InputHandled {
+                utf16_range_to_replace: range_to_replace,
+                text: text.into(),
+            });
+
             if let Some(ranges) = ranges_to_replace {
                 this.change_selections(None, cx, |s| s.select_ranges(ranges));
             }

crates/editor/src/editor_tests.rs πŸ”—

@@ -7807,7 +7807,7 @@ fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewCo
 /// Handle completion request passing a marked string specifying where the completion
 /// should be triggered from using '|' character, what range should be replaced, and what completions
 /// should be returned using '<' and '>' to delimit the range
-fn handle_completion_request<'a>(
+pub fn handle_completion_request<'a>(
     cx: &mut EditorLspTestContext<'a>,
     marked_string: &str,
     completions: Vec<&'static str>,

crates/editor/src/element.rs πŸ”—

@@ -32,8 +32,8 @@ use gpui::{
     json::{self, ToJson},
     platform::{CursorStyle, Modifiers, MouseButton, MouseButtonEvent, MouseMovedEvent},
     text_layout::{self, Line, RunStyle, TextLayoutCache},
-    AnyElement, Axis, Border, CursorRegion, Element, EventContext, FontCache, LayoutContext,
-    MouseRegion, PaintContext, Quad, SceneBuilder, SizeConstraint, ViewContext, WindowContext,
+    AnyElement, Axis, CursorRegion, Element, EventContext, FontCache, MouseRegion, Quad,
+    SizeConstraint, ViewContext, WindowContext,
 };
 use itertools::Itertools;
 use json::json;
@@ -131,7 +131,6 @@ impl EditorElement {
     }
 
     fn attach_mouse_handlers(
-        scene: &mut SceneBuilder,
         position_map: &Arc<PositionMap>,
         has_popovers: bool,
         visible_bounds: RectF,
@@ -141,124 +140,124 @@ impl EditorElement {
         cx: &mut ViewContext<Editor>,
     ) {
         enum EditorElementMouseHandlers {}
-        scene.push_mouse_region(
-            MouseRegion::new::<EditorElementMouseHandlers>(
-                cx.view_id(),
-                cx.view_id(),
-                visible_bounds,
-            )
-            .on_down(MouseButton::Left, {
-                let position_map = position_map.clone();
-                move |event, editor, cx| {
-                    if !Self::mouse_down(
-                        editor,
-                        event.platform_event,
-                        position_map.as_ref(),
-                        text_bounds,
-                        gutter_bounds,
-                        cx,
-                    ) {
-                        cx.propagate_event();
-                    }
-                }
-            })
-            .on_down(MouseButton::Right, {
-                let position_map = position_map.clone();
-                move |event, editor, cx| {
-                    if !Self::mouse_right_down(
-                        editor,
-                        event.position,
-                        position_map.as_ref(),
-                        text_bounds,
-                        cx,
-                    ) {
-                        cx.propagate_event();
+        let view_id = cx.view_id();
+        cx.scene().push_mouse_region(
+            MouseRegion::new::<EditorElementMouseHandlers>(view_id, view_id, visible_bounds)
+                .on_down(MouseButton::Left, {
+                    let position_map = position_map.clone();
+                    move |event, editor, cx| {
+                        if !Self::mouse_down(
+                            editor,
+                            event.platform_event,
+                            position_map.as_ref(),
+                            text_bounds,
+                            gutter_bounds,
+                            cx,
+                        ) {
+                            cx.propagate_event();
+                        }
                     }
-                }
-            })
-            .on_up(MouseButton::Left, {
-                let position_map = position_map.clone();
-                move |event, editor, cx| {
-                    if !Self::mouse_up(
-                        editor,
-                        event.position,
-                        event.cmd,
-                        event.shift,
-                        event.alt,
-                        position_map.as_ref(),
-                        text_bounds,
-                        cx,
-                    ) {
-                        cx.propagate_event()
+                })
+                .on_down(MouseButton::Right, {
+                    let position_map = position_map.clone();
+                    move |event, editor, cx| {
+                        if !Self::mouse_right_down(
+                            editor,
+                            event.position,
+                            position_map.as_ref(),
+                            text_bounds,
+                            cx,
+                        ) {
+                            cx.propagate_event();
+                        }
                     }
-                }
-            })
-            .on_drag(MouseButton::Left, {
-                let position_map = position_map.clone();
-                move |event, editor, cx| {
-                    if event.end {
-                        return;
+                })
+                .on_up(MouseButton::Left, {
+                    let position_map = position_map.clone();
+                    move |event, editor, cx| {
+                        if !Self::mouse_up(
+                            editor,
+                            event.position,
+                            event.cmd,
+                            event.shift,
+                            event.alt,
+                            position_map.as_ref(),
+                            text_bounds,
+                            cx,
+                        ) {
+                            cx.propagate_event()
+                        }
                     }
+                })
+                .on_drag(MouseButton::Left, {
+                    let position_map = position_map.clone();
+                    move |event, editor, cx| {
+                        if event.end {
+                            return;
+                        }
 
-                    if !Self::mouse_dragged(
-                        editor,
-                        event.platform_event,
-                        position_map.as_ref(),
-                        text_bounds,
-                        cx,
-                    ) {
-                        cx.propagate_event()
+                        if !Self::mouse_dragged(
+                            editor,
+                            event.platform_event,
+                            position_map.as_ref(),
+                            text_bounds,
+                            cx,
+                        ) {
+                            cx.propagate_event()
+                        }
                     }
-                }
-            })
-            .on_move({
-                let position_map = position_map.clone();
-                move |event, editor, cx| {
-                    if !Self::mouse_moved(
-                        editor,
-                        event.platform_event,
-                        &position_map,
-                        text_bounds,
-                        cx,
-                    ) {
-                        cx.propagate_event()
+                })
+                .on_move({
+                    let position_map = position_map.clone();
+                    move |event, editor, cx| {
+                        if !Self::mouse_moved(
+                            editor,
+                            event.platform_event,
+                            &position_map,
+                            text_bounds,
+                            cx,
+                        ) {
+                            cx.propagate_event()
+                        }
                     }
-                }
-            })
-            .on_move_out(move |_, editor: &mut Editor, cx| {
-                if has_popovers {
-                    hide_hover(editor, cx);
-                }
-            })
-            .on_scroll({
-                let position_map = position_map.clone();
-                move |event, editor, cx| {
-                    if !Self::scroll(
-                        editor,
-                        event.position,
-                        *event.delta.raw(),
-                        event.delta.precise(),
-                        &position_map,
-                        bounds,
-                        cx,
-                    ) {
-                        cx.propagate_event()
+                })
+                .on_move_out(move |_, editor: &mut Editor, cx| {
+                    if has_popovers {
+                        hide_hover(editor, cx);
                     }
-                }
-            }),
+                })
+                .on_scroll({
+                    let position_map = position_map.clone();
+                    move |event, editor, cx| {
+                        if !Self::scroll(
+                            editor,
+                            event.position,
+                            *event.delta.raw(),
+                            event.delta.precise(),
+                            &position_map,
+                            bounds,
+                            cx,
+                        ) {
+                            cx.propagate_event()
+                        }
+                    }
+                }),
         );
 
         enum GutterHandlers {}
-        scene.push_mouse_region(
-            MouseRegion::new::<GutterHandlers>(cx.view_id(), cx.view_id() + 1, gutter_bounds)
-                .on_hover(|hover, editor: &mut Editor, cx| {
+        let view_id = cx.view_id();
+        let region_id = cx.view_id() + 1;
+        cx.scene().push_mouse_region(
+            MouseRegion::new::<GutterHandlers>(view_id, region_id, gutter_bounds).on_hover(
+                |hover, editor: &mut Editor, cx| {
                     editor.gutter_hover(
                         &GutterHover {
                             hovered: hover.started,
                         },
                         cx,
                     );
-                }),
+                },
+            ),
         )
     }
 
@@ -528,24 +527,24 @@ impl EditorElement {
 
     fn paint_background(
         &self,
-        scene: &mut SceneBuilder,
         gutter_bounds: RectF,
         text_bounds: RectF,
         layout: &LayoutState,
+        cx: &mut ViewContext<Editor>,
     ) {
         let bounds = gutter_bounds.union_rect(text_bounds);
         let scroll_top =
             layout.position_map.snapshot.scroll_position().y() * layout.position_map.line_height;
-        scene.push_quad(Quad {
+        cx.scene().push_quad(Quad {
             bounds: gutter_bounds,
             background: Some(self.style.gutter_background),
-            border: Border::new(0., Color::transparent_black()),
+            border: Border::new(0., Color::transparent_black()).into(),
             corner_radii: Default::default(),
         });
-        scene.push_quad(Quad {
+        cx.scene().push_quad(Quad {
             bounds: text_bounds,
             background: Some(self.style.background),
-            border: Border::new(0., Color::transparent_black()),
+            border: Border::new(0., Color::transparent_black()).into(),
             corner_radii: Default::default(),
         });
 
@@ -570,10 +569,10 @@ impl EditorElement {
                         bounds.width(),
                         layout.position_map.line_height * (end_row - start_row + 1) as f32,
                     );
-                    scene.push_quad(Quad {
+                    cx.scene().push_quad(Quad {
                         bounds: RectF::new(origin, size),
                         background: Some(self.style.active_line_background),
-                        border: Border::default(),
+                        border: Border::default().into(),
                         corner_radii: Default::default(),
                     });
                 }
@@ -590,10 +589,10 @@ impl EditorElement {
                     bounds.width(),
                     layout.position_map.line_height * highlighted_rows.len() as f32,
                 );
-                scene.push_quad(Quad {
+                cx.scene().push_quad(Quad {
                     bounds: RectF::new(origin, size),
                     background: Some(self.style.highlighted_line_background),
-                    border: Border::default(),
+                    border: Border::default().into(),
                     corner_radii: Default::default(),
                 });
             }
@@ -617,13 +616,13 @@ impl EditorElement {
                 } else {
                     self.style.wrap_guide
                 };
-                scene.push_quad(Quad {
+                cx.scene().push_quad(Quad {
                     bounds: RectF::new(
                         vec2f(x, text_bounds.origin_y()),
                         vec2f(1., text_bounds.height()),
                     ),
                     background: Some(color),
-                    border: Border::new(0., Color::transparent_black()),
+                    border: Border::new(0., Color::transparent_black()).into(),
                     corner_radii: Default::default(),
                 });
             }
@@ -632,12 +631,11 @@ impl EditorElement {
 
     fn paint_gutter(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         layout: &mut LayoutState,
         editor: &mut Editor,
-        cx: &mut PaintContext<Editor>,
+        cx: &mut ViewContext<Editor>,
     ) {
         let line_height = layout.position_map.line_height;
 
@@ -650,7 +648,7 @@ impl EditorElement {
         );
 
         if show_gutter {
-            Self::paint_diff_hunks(scene, bounds, layout, cx);
+            Self::paint_diff_hunks(bounds, layout, cx);
         }
 
         for (ix, line) in layout.line_number_layouts.iter().enumerate() {
@@ -661,7 +659,7 @@ impl EditorElement {
                         ix as f32 * line_height - (scroll_top % line_height),
                     );
 
-                line.paint(scene, line_origin, visible_bounds, line_height, cx);
+                line.paint(line_origin, visible_bounds, line_height, cx);
             }
         }
 
@@ -678,7 +676,7 @@ impl EditorElement {
 
                 let indicator_origin = bounds.origin() + position + centering_offset;
 
-                indicator.paint(scene, indicator_origin, visible_bounds, editor, cx);
+                indicator.paint(indicator_origin, visible_bounds, editor, cx);
             }
         }
 
@@ -687,22 +685,11 @@ impl EditorElement {
             let mut y = *row as f32 * line_height - scroll_top;
             x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.;
             y += (line_height - indicator.size().y()) / 2.;
-            indicator.paint(
-                scene,
-                bounds.origin() + vec2f(x, y),
-                visible_bounds,
-                editor,
-                cx,
-            );
+            indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, editor, cx);
         }
     }
 
-    fn paint_diff_hunks(
-        scene: &mut SceneBuilder,
-        bounds: RectF,
-        layout: &mut LayoutState,
-        cx: &mut ViewContext<Editor>,
-    ) {
+    fn paint_diff_hunks(bounds: RectF, layout: &mut LayoutState, cx: &mut ViewContext<Editor>) {
         let diff_style = &theme::current(cx).editor.diff.clone();
         let line_height = layout.position_map.line_height;
 
@@ -721,10 +708,10 @@ impl EditorElement {
                     let highlight_size = vec2f(width * 2., end_y - start_y);
                     let highlight_bounds = RectF::new(highlight_origin, highlight_size);
 
-                    scene.push_quad(Quad {
+                    cx.scene().push_quad(Quad {
                         bounds: highlight_bounds,
                         background: Some(diff_style.modified),
-                        border: Border::new(0., Color::transparent_black()),
+                        border: Border::new(0., Color::transparent_black()).into(),
                         corner_radii: (1. * line_height).into(),
                     });
 
@@ -754,10 +741,10 @@ impl EditorElement {
                     let highlight_size = vec2f(width * 2., end_y - start_y);
                     let highlight_bounds = RectF::new(highlight_origin, highlight_size);
 
-                    scene.push_quad(Quad {
+                    cx.scene().push_quad(Quad {
                         bounds: highlight_bounds,
                         background: Some(diff_style.deleted),
-                        border: Border::new(0., Color::transparent_black()),
+                        border: Border::new(0., Color::transparent_black()).into(),
                         corner_radii: (1. * line_height).into(),
                     });
 
@@ -776,10 +763,10 @@ impl EditorElement {
             let highlight_size = vec2f(width * 2., end_y - start_y);
             let highlight_bounds = RectF::new(highlight_origin, highlight_size);
 
-            scene.push_quad(Quad {
+            cx.scene().push_quad(Quad {
                 bounds: highlight_bounds,
                 background: Some(color),
-                border: Border::new(0., Color::transparent_black()),
+                border: Border::new(0., Color::transparent_black()).into(),
                 corner_radii: (diff_style.corner_radius * line_height).into(),
             });
         }
@@ -787,12 +774,11 @@ impl EditorElement {
 
     fn paint_text(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         layout: &mut LayoutState,
         editor: &mut Editor,
-        cx: &mut PaintContext<Editor>,
+        cx: &mut ViewContext<Editor>,
     ) {
         let style = &self.style;
         let scroll_position = layout.position_map.snapshot.scroll_position();
@@ -804,9 +790,9 @@ impl EditorElement {
         let line_end_overshoot = 0.15 * layout.position_map.line_height;
         let whitespace_setting = editor.buffer.read(cx).settings_at(0, cx).show_whitespaces;
 
-        scene.push_layer(Some(bounds));
+        cx.scene().push_layer(Some(bounds));
 
-        scene.push_cursor_region(CursorRegion {
+        cx.scene().push_cursor_region(CursorRegion {
             bounds,
             style: if !editor.link_go_to_definition_state.definitions.is_empty() {
                 CursorStyle::PointingHand
@@ -819,7 +805,6 @@ impl EditorElement {
             self.style.folds.ellipses.corner_radius_factor * layout.position_map.line_height;
         for (id, range, color) in layout.fold_ranges.iter() {
             self.paint_highlighted_range(
-                scene,
                 range.clone(),
                 *color,
                 fold_corner_radius,
@@ -829,6 +814,7 @@ impl EditorElement {
                 scroll_top,
                 scroll_left,
                 bounds,
+                cx,
             );
 
             for bound in range_to_bounds(
@@ -840,7 +826,7 @@ impl EditorElement {
                 line_end_overshoot,
                 &layout.position_map,
             ) {
-                scene.push_cursor_region(CursorRegion {
+                cx.scene().push_cursor_region(CursorRegion {
                     bounds: bound,
                     style: CursorStyle::PointingHand,
                 });
@@ -851,8 +837,9 @@ impl EditorElement {
                     .to_point(&layout.position_map.snapshot.display_snapshot)
                     .row;
 
-                scene.push_mouse_region(
-                    MouseRegion::new::<FoldMarkers>(cx.view_id(), *id as usize, bound)
+                let view_id = cx.view_id();
+                cx.scene().push_mouse_region(
+                    MouseRegion::new::<FoldMarkers>(view_id, *id as usize, bound)
                         .on_click(MouseButton::Left, move |_, editor: &mut Editor, cx| {
                             editor.unfold_at(&UnfoldAt { buffer_row }, cx)
                         })
@@ -864,7 +851,6 @@ impl EditorElement {
 
         for (range, color) in &layout.highlighted_ranges {
             self.paint_highlighted_range(
-                scene,
                 range.clone(),
                 *color,
                 0.,
@@ -874,6 +860,7 @@ impl EditorElement {
                 scroll_top,
                 scroll_left,
                 bounds,
+                cx,
             );
         }
 
@@ -891,7 +878,6 @@ impl EditorElement {
 
             for selection in selections {
                 self.paint_highlighted_range(
-                    scene,
                     selection.range.clone(),
                     selection_style.selection,
                     corner_radius,
@@ -901,6 +887,7 @@ impl EditorElement {
                     scroll_top,
                     scroll_left,
                     bounds,
+                    cx,
                 );
 
                 if selection.is_local && !selection.range.is_empty() {
@@ -980,7 +967,6 @@ impl EditorElement {
                     layout,
                     row,
                     scroll_top,
-                    scene,
                     content_origin,
                     scroll_left,
                     visible_text_bounds,
@@ -992,14 +978,14 @@ impl EditorElement {
             }
         }
 
-        scene.paint_layer(Some(bounds), |scene| {
-            for cursor in cursors {
-                cursor.paint(scene, content_origin, cx);
-            }
-        });
+        cx.scene().push_layer(Some(bounds));
+        for cursor in cursors {
+            cursor.paint(content_origin, cx);
+        }
+        cx.scene().pop_layer();
 
         if let Some((position, context_menu)) = layout.context_menu.as_mut() {
-            scene.push_stacking_context(None, None);
+            cx.scene().push_stacking_context(None, None);
             let cursor_row_layout =
                 &layout.position_map.line_layouts[(position.row() - start_row) as usize].line;
             let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left;
@@ -1019,18 +1005,17 @@ impl EditorElement {
             }
 
             context_menu.paint(
-                scene,
                 list_origin,
                 RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor
                 editor,
                 cx,
             );
 
-            scene.pop_stacking_context();
+            cx.scene().pop_stacking_context();
         }
 
         if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() {
-            scene.push_stacking_context(None, None);
+            cx.scene().push_stacking_context(None, None);
 
             // This is safe because we check on layout whether the required row is available
             let hovered_row_layout =
@@ -1061,7 +1046,6 @@ impl EditorElement {
                     }
 
                     hover_popover.paint(
-                        scene,
                         popover_origin,
                         RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor
                         editor,
@@ -1083,7 +1067,6 @@ impl EditorElement {
                     }
 
                     hover_popover.paint(
-                        scene,
                         popover_origin,
                         RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor
                         editor,
@@ -1094,10 +1077,10 @@ impl EditorElement {
                 }
             }
 
-            scene.pop_stacking_context();
+            cx.scene().pop_stacking_context();
         }
 
-        scene.pop_layer();
+        cx.scene().pop_layer();
     }
 
     fn scrollbar_left(&self, bounds: &RectF) -> f32 {
@@ -1106,11 +1089,10 @@ impl EditorElement {
 
     fn paint_scrollbar(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         layout: &mut LayoutState,
-        cx: &mut ViewContext<Editor>,
         editor: &Editor,
+        cx: &mut ViewContext<Editor>,
     ) {
         enum ScrollbarMouseHandlers {}
         if layout.mode != EditorMode::Full {
@@ -1147,9 +1129,9 @@ impl EditorElement {
         let thumb_bounds = RectF::from_points(vec2f(left, thumb_top), vec2f(right, thumb_bottom));
 
         if layout.show_scrollbars {
-            scene.push_quad(Quad {
+            cx.scene().push_quad(Quad {
                 bounds: track_bounds,
-                border: style.track.border,
+                border: style.track.border.into(),
                 background: style.track.background_color,
                 ..Default::default()
             });
@@ -1177,10 +1159,10 @@ impl EditorElement {
                     }
                     let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y));
 
-                    scene.push_quad(Quad {
+                    cx.scene().push_quad(Quad {
                         bounds,
                         background: Some(color),
-                        border,
+                        border: border.into(),
                         corner_radii: style.thumb.corner_radii.into(),
                     })
                 };
@@ -1237,29 +1219,30 @@ impl EditorElement {
                         left: true,
                     };
 
-                    scene.push_quad(Quad {
+                    cx.scene().push_quad(Quad {
                         bounds,
                         background: Some(color),
-                        border,
+                        border: border.into(),
                         corner_radii: style.thumb.corner_radii.into(),
                     })
                 }
             }
 
-            scene.push_quad(Quad {
+            cx.scene().push_quad(Quad {
                 bounds: thumb_bounds,
-                border: style.thumb.border,
+                border: style.thumb.border.into(),
                 background: style.thumb.background_color,
                 corner_radii: style.thumb.corner_radii.into(),
             });
         }
 
-        scene.push_cursor_region(CursorRegion {
+        cx.scene().push_cursor_region(CursorRegion {
             bounds: track_bounds,
             style: CursorStyle::Arrow,
         });
-        scene.push_mouse_region(
-            MouseRegion::new::<ScrollbarMouseHandlers>(cx.view_id(), cx.view_id(), track_bounds)
+        let region_id = cx.view_id();
+        cx.scene().push_mouse_region(
+            MouseRegion::new::<ScrollbarMouseHandlers>(region_id, region_id, track_bounds)
                 .on_move(move |event, editor: &mut Editor, cx| {
                     if event.pressed_button.is_none() {
                         editor.scroll_manager.show_scrollbar(cx);
@@ -1305,7 +1288,6 @@ impl EditorElement {
     #[allow(clippy::too_many_arguments)]
     fn paint_highlighted_range(
         &self,
-        scene: &mut SceneBuilder,
         range: Range<DisplayPoint>,
         color: Color,
         corner_radius: f32,
@@ -1315,6 +1297,7 @@ impl EditorElement {
         scroll_top: f32,
         scroll_left: f32,
         bounds: RectF,
+        cx: &mut ViewContext<Editor>,
     ) {
         let start_row = layout.visible_display_row_range.start;
         let end_row = layout.visible_display_row_range.end;
@@ -1358,18 +1341,17 @@ impl EditorElement {
                     .collect(),
             };
 
-            highlighted_range.paint(bounds, scene);
+            highlighted_range.paint(bounds, cx);
         }
     }
 
     fn paint_blocks(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         layout: &mut LayoutState,
         editor: &mut Editor,
-        cx: &mut PaintContext<Editor>,
+        cx: &mut ViewContext<Editor>,
     ) {
         let scroll_position = layout.position_map.snapshot.scroll_position();
         let scroll_left = scroll_position.x() * layout.position_map.em_width;
@@ -1384,9 +1366,7 @@ impl EditorElement {
             if !matches!(block.style, BlockStyle::Sticky) {
                 origin += vec2f(-scroll_left, 0.);
             }
-            block
-                .element
-                .paint(scene, origin, visible_bounds, editor, cx);
+            block.element.paint(origin, visible_bounds, editor, cx);
         }
     }
 
@@ -1690,7 +1670,7 @@ impl EditorElement {
         style: &EditorStyle,
         line_layouts: &[LineWithInvisibles],
         editor: &mut Editor,
-        cx: &mut LayoutContext<Editor>,
+        cx: &mut ViewContext<Editor>,
     ) -> (f32, Vec<BlockLayout>) {
         let mut block_id = 0;
         let scroll_x = snapshot.scroll_anchor.offset.x();
@@ -2022,7 +2002,6 @@ impl LineWithInvisibles {
         layout: &LayoutState,
         row: u32,
         scroll_top: f32,
-        scene: &mut SceneBuilder,
         content_origin: Vector2F,
         scroll_left: f32,
         visible_text_bounds: RectF,
@@ -2035,7 +2014,6 @@ impl LineWithInvisibles {
         let line_y = row as f32 * line_height - scroll_top;
 
         self.line.paint(
-            scene,
             content_origin + vec2f(-scroll_left, line_y),
             visible_text_bounds,
             line_height,
@@ -2049,7 +2027,6 @@ impl LineWithInvisibles {
             scroll_left,
             line_y,
             row,
-            scene,
             visible_bounds,
             line_height,
             whitespace_setting,
@@ -2065,7 +2042,6 @@ impl LineWithInvisibles {
         scroll_left: f32,
         line_y: f32,
         row: u32,
-        scene: &mut SceneBuilder,
         visible_bounds: RectF,
         line_height: f32,
         whitespace_setting: ShowWhitespaceSetting,
@@ -2097,7 +2073,7 @@ impl LineWithInvisibles {
                     continue;
                 }
             }
-            invisible_symbol.paint(scene, origin, visible_bounds, line_height, cx);
+            invisible_symbol.paint(origin, visible_bounds, line_height, cx);
         }
     }
 }
@@ -2116,7 +2092,7 @@ impl Element<Editor> for EditorElement {
         &mut self,
         constraint: SizeConstraint,
         editor: &mut Editor,
-        cx: &mut LayoutContext<Editor>,
+        cx: &mut ViewContext<Editor>,
     ) -> (Vector2F, Self::LayoutState) {
         let mut size = constraint.max;
         if size.x().is_infinite() {
@@ -2590,15 +2566,14 @@ impl Element<Editor> for EditorElement {
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         layout: &mut Self::LayoutState,
         editor: &mut Editor,
-        cx: &mut PaintContext<Editor>,
+        cx: &mut ViewContext<Editor>,
     ) -> Self::PaintState {
         let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
-        scene.push_layer(Some(visible_bounds));
+        cx.scene().push_layer(Some(visible_bounds));
 
         let gutter_bounds = RectF::new(bounds.origin(), layout.gutter_size);
         let text_bounds = RectF::new(
@@ -2607,7 +2582,6 @@ impl Element<Editor> for EditorElement {
         );
 
         Self::attach_mouse_handlers(
-            scene,
             &layout.position_map,
             layout.hover_popovers.is_some(),
             visible_bounds,
@@ -2617,20 +2591,19 @@ impl Element<Editor> for EditorElement {
             cx,
         );
 
-        self.paint_background(scene, gutter_bounds, text_bounds, layout);
+        self.paint_background(gutter_bounds, text_bounds, layout, cx);
         if layout.gutter_size.x() > 0. {
-            self.paint_gutter(scene, gutter_bounds, visible_bounds, layout, editor, cx);
+            self.paint_gutter(gutter_bounds, visible_bounds, layout, editor, cx);
         }
-        self.paint_text(scene, text_bounds, visible_bounds, layout, editor, cx);
+        self.paint_text(text_bounds, visible_bounds, layout, editor, cx);
 
-        scene.push_layer(Some(bounds));
+        cx.scene().push_layer(Some(bounds));
         if !layout.blocks.is_empty() {
-            self.paint_blocks(scene, bounds, visible_bounds, layout, editor, cx);
+            self.paint_blocks(bounds, visible_bounds, layout, editor, cx);
         }
-        self.paint_scrollbar(scene, bounds, layout, cx, &editor);
-        scene.pop_layer();
-
-        scene.pop_layer();
+        self.paint_scrollbar(bounds, layout, &editor, cx);
+        cx.scene().pop_layer();
+        cx.scene().pop_layer();
     }
 
     fn rect_for_text_range(
@@ -2873,7 +2846,7 @@ impl Cursor {
         )
     }
 
-    pub fn paint(&self, scene: &mut SceneBuilder, origin: Vector2F, cx: &mut WindowContext) {
+    pub fn paint(&self, origin: Vector2F, cx: &mut WindowContext) {
         let bounds = match self.shape {
             CursorShape::Bar => RectF::new(self.origin + origin, vec2f(2.0, self.line_height)),
             CursorShape::Block | CursorShape::Hollow => RectF::new(
@@ -2888,14 +2861,14 @@ impl Cursor {
 
         //Draw background or border quad
         if matches!(self.shape, CursorShape::Hollow) {
-            scene.push_quad(Quad {
+            cx.scene().push_quad(Quad {
                 bounds,
                 background: None,
-                border: Border::all(1., self.color),
+                border: Border::all(1., self.color).into(),
                 corner_radii: Default::default(),
             });
         } else {
-            scene.push_quad(Quad {
+            cx.scene().push_quad(Quad {
                 bounds,
                 background: Some(self.color),
                 border: Default::default(),
@@ -2904,7 +2877,7 @@ impl Cursor {
         }
 
         if let Some(block_text) = &self.block_text {
-            block_text.paint(scene, self.origin + origin, bounds, self.line_height, cx);
+            block_text.paint(self.origin + origin, bounds, self.line_height, cx);
         }
     }
 
@@ -2929,17 +2902,17 @@ pub struct HighlightedRangeLine {
 }
 
 impl HighlightedRange {
-    pub fn paint(&self, bounds: RectF, scene: &mut SceneBuilder) {
+    pub fn paint(&self, bounds: RectF, cx: &mut WindowContext) {
         if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x {
-            self.paint_lines(self.start_y, &self.lines[0..1], bounds, scene);
+            self.paint_lines(self.start_y, &self.lines[0..1], bounds, cx);
             self.paint_lines(
                 self.start_y + self.line_height,
                 &self.lines[1..],
                 bounds,
-                scene,
+                cx,
             );
         } else {
-            self.paint_lines(self.start_y, &self.lines, bounds, scene);
+            self.paint_lines(self.start_y, &self.lines, bounds, cx);
         }
     }
 
@@ -2948,7 +2921,7 @@ impl HighlightedRange {
         start_y: f32,
         lines: &[HighlightedRangeLine],
         bounds: RectF,
-        scene: &mut SceneBuilder,
+        cx: &mut WindowContext,
     ) {
         if lines.is_empty() {
             return;
@@ -3046,7 +3019,7 @@ impl HighlightedRange {
         }
         path.line_to(first_top_right - top_curve_width);
 
-        scene.push_path(path.build(self.color, Some(bounds)));
+        cx.scene().push_path(path.build(self.color, Some(bounds)));
     }
 }
 
@@ -3204,18 +3177,10 @@ mod tests {
                     Point::new(5, 6)..Point::new(6, 0),
                 ]);
             });
-            let mut new_parents = Default::default();
-            let mut notify_views_if_parents_change = Default::default();
-            let mut layout_cx = LayoutContext::new(
-                cx,
-                &mut new_parents,
-                &mut notify_views_if_parents_change,
-                false,
-            );
             element.layout(
                 SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)),
                 editor,
-                &mut layout_cx,
+                cx,
             )
         });
         assert_eq!(state.selections.len(), 1);
@@ -3296,18 +3261,10 @@ mod tests {
                     DisplayPoint::new(10, 0)..DisplayPoint::new(13, 0),
                 ]);
             });
-            let mut new_parents = Default::default();
-            let mut notify_views_if_parents_change = Default::default();
-            let mut layout_cx = LayoutContext::new(
-                cx,
-                &mut new_parents,
-                &mut notify_views_if_parents_change,
-                false,
-            );
             element.layout(
                 SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)),
                 editor,
-                &mut layout_cx,
+                cx,
             )
         });
 
@@ -3363,18 +3320,10 @@ mod tests {
 
         let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
         let (size, mut state) = editor.update(cx, |editor, cx| {
-            let mut new_parents = Default::default();
-            let mut notify_views_if_parents_change = Default::default();
-            let mut layout_cx = LayoutContext::new(
-                cx,
-                &mut new_parents,
-                &mut notify_views_if_parents_change,
-                false,
-            );
             element.layout(
                 SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)),
                 editor,
-                &mut layout_cx,
+                cx,
             )
         });
 
@@ -3389,17 +3338,9 @@ mod tests {
         );
 
         // Don't panic.
-        let mut scene = SceneBuilder::new(1.0);
         let bounds = RectF::new(Default::default(), size);
         editor.update(cx, |editor, cx| {
-            element.paint(
-                &mut scene,
-                bounds,
-                bounds,
-                &mut state,
-                editor,
-                &mut PaintContext::new(cx),
-            );
+            element.paint(bounds, bounds, &mut state, editor, cx);
         });
     }
 
@@ -3567,18 +3508,10 @@ mod tests {
             editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
             editor.set_wrap_width(Some(editor_width), cx);
 
-            let mut new_parents = Default::default();
-            let mut notify_views_if_parents_change = Default::default();
-            let mut layout_cx = LayoutContext::new(
-                cx,
-                &mut new_parents,
-                &mut notify_views_if_parents_change,
-                false,
-            );
             element.layout(
                 SizeConstraint::new(vec2f(editor_width, 500.), vec2f(editor_width, 500.)),
                 editor,
-                &mut layout_cx,
+                cx,
             )
         });
 

crates/editor/src/hover_popover.rs πŸ”—

@@ -691,15 +691,15 @@ impl InfoPopover {
                         .with_highlights(rendered_content.highlights.clone())
                         .with_custom_runs(
                             rendered_content.region_ranges.clone(),
-                            move |ix, bounds, scene, _| {
+                            move |ix, bounds, cx| {
                                 region_id += 1;
                                 let region = regions[ix].clone();
                                 if let Some(url) = region.link_url {
-                                    scene.push_cursor_region(CursorRegion {
+                                    cx.scene().push_cursor_region(CursorRegion {
                                         bounds,
                                         style: CursorStyle::PointingHand,
                                     });
-                                    scene.push_mouse_region(
+                                    cx.scene().push_mouse_region(
                                         MouseRegion::new::<Self>(view_id, region_id, bounds)
                                             .on_click::<Editor, _>(
                                                 MouseButton::Left,
@@ -708,7 +708,7 @@ impl InfoPopover {
                                     );
                                 }
                                 if region.code {
-                                    scene.push_quad(gpui::Quad {
+                                    cx.scene().push_quad(gpui::Quad {
                                         bounds,
                                         background: Some(code_span_background_color),
                                         border: Default::default(),

crates/editor/src/items.rs πŸ”—

@@ -16,7 +16,7 @@ use language::{
     proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point,
     SelectionGoal,
 };
-use project::{FormatTrigger, Item as _, Project, ProjectPath};
+use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
 use rpc::proto::{self, update_view};
 use smallvec::SmallVec;
 use std::{
@@ -26,6 +26,7 @@ use std::{
     iter,
     ops::Range,
     path::{Path, PathBuf},
+    sync::Arc,
 };
 use text::Selection;
 use util::{
@@ -978,7 +979,26 @@ impl SearchableItem for Editor {
         }
         self.change_selections(None, cx, |s| s.select_ranges(ranges));
     }
+    fn replace(
+        &mut self,
+        identifier: &Self::Match,
+        query: &SearchQuery,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let text = self.buffer.read(cx);
+        let text = text.snapshot(cx);
+        let text = text.text_for_range(identifier.clone()).collect::<Vec<_>>();
+        let text: Cow<_> = if text.len() == 1 {
+            text.first().cloned().unwrap().into()
+        } else {
+            let joined_chunks = text.join("");
+            joined_chunks.into()
+        };
 
+        if let Some(replacement) = query.replacement(&text) {
+            self.edit([(identifier.clone(), Arc::from(&*replacement))], cx);
+        }
+    }
     fn match_index_for_direction(
         &mut self,
         matches: &Vec<Range<Anchor>>,
@@ -1030,7 +1050,7 @@ impl SearchableItem for Editor {
 
     fn find_matches(
         &mut self,
-        query: project::search::SearchQuery,
+        query: Arc<project::search::SearchQuery>,
         cx: &mut ViewContext<Self>,
     ) -> Task<Vec<Range<Anchor>>> {
         let buffer = self.buffer().read(cx).snapshot(cx);

crates/feedback/src/feedback_editor.rs πŸ”—

@@ -13,7 +13,7 @@ use gpui::{
 use isahc::Request;
 use language::Buffer;
 use postage::prelude::Stream;
-use project::Project;
+use project::{search::SearchQuery, Project};
 use regex::Regex;
 use serde::Serialize;
 use smallvec::SmallVec;
@@ -418,10 +418,13 @@ impl SearchableItem for FeedbackEditor {
         self.editor
             .update(cx, |e, cx| e.select_matches(matches, cx))
     }
-
+    fn replace(&mut self, matches: &Self::Match, query: &SearchQuery, cx: &mut ViewContext<Self>) {
+        self.editor
+            .update(cx, |e, cx| e.replace(matches, query, cx));
+    }
     fn find_matches(
         &mut self,
-        query: project::search::SearchQuery,
+        query: Arc<project::search::SearchQuery>,
         cx: &mut ViewContext<Self>,
     ) -> Task<Vec<Self::Match>> {
         self.editor

crates/file_finder/src/file_finder.rs πŸ”—

@@ -1528,8 +1528,13 @@ mod tests {
         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
         active_pane
             .update(cx, |pane, cx| {
-                pane.close_active_item(&workspace::CloseActiveItem, cx)
-                    .unwrap()
+                pane.close_active_item(
+                    &workspace::CloseActiveItem {
+                        save_behavior: None,
+                    },
+                    cx,
+                )
+                .unwrap()
             })
             .await
             .unwrap();

crates/gpui/Cargo.toml πŸ”—

@@ -48,7 +48,8 @@ serde_derive.workspace = true
 serde_json.workspace = true
 smallvec.workspace = true
 smol.workspace = true
-taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "dab541d6104d58e2e10ce90c4a1dad0b703160cd", features = ["flexbox"] }
+taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e" }
+thiserror.workspace = true
 time.workspace = true
 tiny-skia = "0.5"
 usvg = { version = "0.14", features = [] }

crates/gpui/examples/corner_radii.rs πŸ”—

@@ -42,29 +42,28 @@ impl<V: View> gpui::Element<V> for CornersElement {
         &mut self,
         constraint: gpui::SizeConstraint,
         _: &mut V,
-        _: &mut gpui::LayoutContext<V>,
+        _: &mut gpui::ViewContext<V>,
     ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
         (constraint.max, ())
     }
 
     fn paint(
         &mut self,
-        scene: &mut gpui::SceneBuilder,
         bounds: pathfinder_geometry::rect::RectF,
         _: pathfinder_geometry::rect::RectF,
         _: &mut Self::LayoutState,
         _: &mut V,
-        _: &mut gpui::PaintContext<V>,
+        cx: &mut gpui::ViewContext<V>,
     ) -> Self::PaintState {
-        scene.push_quad(Quad {
+        cx.scene().push_quad(Quad {
             bounds,
             background: Some(Color::white()),
             ..Default::default()
         });
 
-        scene.push_layer(None);
+        cx.scene().push_layer(None);
 
-        scene.push_quad(Quad {
+        cx.scene().push_quad(Quad {
             bounds: RectF::new(vec2f(100., 100.), vec2f(100., 100.)),
             background: Some(Color::red()),
             border: Default::default(),
@@ -74,7 +73,7 @@ impl<V: View> gpui::Element<V> for CornersElement {
             },
         });
 
-        scene.push_quad(Quad {
+        cx.scene().push_quad(Quad {
             bounds: RectF::new(vec2f(200., 100.), vec2f(100., 100.)),
             background: Some(Color::green()),
             border: Default::default(),
@@ -84,7 +83,7 @@ impl<V: View> gpui::Element<V> for CornersElement {
             },
         });
 
-        scene.push_quad(Quad {
+        cx.scene().push_quad(Quad {
             bounds: RectF::new(vec2f(100., 200.), vec2f(100., 100.)),
             background: Some(Color::blue()),
             border: Default::default(),
@@ -94,7 +93,7 @@ impl<V: View> gpui::Element<V> for CornersElement {
             },
         });
 
-        scene.push_quad(Quad {
+        cx.scene().push_quad(Quad {
             bounds: RectF::new(vec2f(200., 200.), vec2f(100., 100.)),
             background: Some(Color::yellow()),
             border: Default::default(),
@@ -104,7 +103,7 @@ impl<V: View> gpui::Element<V> for CornersElement {
             },
         });
 
-        scene.push_shadow(Shadow {
+        cx.scene().push_shadow(Shadow {
             bounds: RectF::new(vec2f(400., 100.), vec2f(100., 100.)),
             corner_radii: gpui::scene::CornerRadii {
                 bottom_right: 20.,
@@ -114,8 +113,8 @@ impl<V: View> gpui::Element<V> for CornersElement {
             color: Color::black(),
         });
 
-        scene.push_layer(None);
-        scene.push_quad(Quad {
+        cx.scene().push_layer(None);
+        cx.scene().push_quad(Quad {
             bounds: RectF::new(vec2f(400., 100.), vec2f(100., 100.)),
             background: Some(Color::red()),
             border: Default::default(),
@@ -125,8 +124,8 @@ impl<V: View> gpui::Element<V> for CornersElement {
             },
         });
 
-        scene.pop_layer();
-        scene.pop_layer();
+        cx.scene().pop_layer();
+        cx.scene().pop_layer();
     }
 
     fn rect_for_text_range(

crates/gpui/examples/text.rs πŸ”—

@@ -62,12 +62,12 @@ impl gpui::View for TextView {
             },
         )
         .with_highlights(vec![(17..26, underline), (34..40, underline)])
-        .with_custom_runs(vec![(17..26), (34..40)], move |ix, bounds, scene, _| {
-            scene.push_cursor_region(CursorRegion {
+        .with_custom_runs(vec![(17..26), (34..40)], move |ix, bounds, cx| {
+            cx.scene().push_cursor_region(CursorRegion {
                 bounds,
                 style: CursorStyle::PointingHand,
             });
-            scene.push_mouse_region(
+            cx.scene().push_mouse_region(
                 MouseRegion::new::<Self>(view_id, ix, bounds).on_click::<Self, _>(
                     MouseButton::Left,
                     move |_, _, _| {

crates/gpui/playground/Cargo.toml πŸ”—

@@ -1,26 +0,0 @@
-[package]
-name = "playground"
-version = "0.1.0"
-edition = "2021"
-publish = false
-
-[[bin]]
-name = "playground"
-path = "src/playground.rs"
-
-[dependencies]
-anyhow.workspace = true
-derive_more.workspace = true
-gpui = { path = ".." }
-log.workspace = true
-playground_macros = { path = "../playground_macros" }
-parking_lot.workspace = true
-refineable.workspace = true
-serde.workspace = true
-simplelog = "0.9"
-smallvec.workspace = true
-taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "dab541d6104d58e2e10ce90c4a1dad0b703160cd", features = ["flexbox"] }
-util = { path = "../../util" }
-
-[dev-dependencies]
-gpui = { path = "..", features = ["test-support"] }

crates/gpui/playground/src/div.rs πŸ”—

@@ -1,108 +0,0 @@
-use crate::{
-    element::{AnyElement, Element, Layout, ParentElement},
-    interactive::{InteractionHandlers, Interactive},
-    layout_context::LayoutContext,
-    paint_context::PaintContext,
-    style::{Style, StyleHelpers, StyleRefinement, Styleable},
-};
-use anyhow::Result;
-use gpui::LayoutId;
-use smallvec::SmallVec;
-
-pub struct Div<V: 'static> {
-    style: StyleRefinement,
-    handlers: InteractionHandlers<V>,
-    children: SmallVec<[AnyElement<V>; 2]>,
-}
-
-pub fn div<V>() -> Div<V> {
-    Div {
-        style: Default::default(),
-        handlers: Default::default(),
-        children: Default::default(),
-    }
-}
-
-impl<V: 'static> Element<V> for Div<V> {
-    type Layout = ();
-
-    fn layout(&mut self, view: &mut V, cx: &mut LayoutContext<V>) -> Result<Layout<V, ()>>
-    where
-        Self: Sized,
-    {
-        let children = self
-            .children
-            .iter_mut()
-            .map(|child| child.layout(view, cx))
-            .collect::<Result<Vec<LayoutId>>>()?;
-
-        cx.add_layout_node(self.style(), (), children)
-    }
-
-    fn paint(&mut self, view: &mut V, layout: &mut Layout<V, ()>, cx: &mut PaintContext<V>)
-    where
-        Self: Sized,
-    {
-        let style = self.style();
-
-        style.paint_background::<V, Self>(layout, cx);
-        for child in &mut self.children {
-            child.paint(view, cx);
-        }
-    }
-}
-
-impl<V> Styleable for Div<V> {
-    type Style = Style;
-
-    fn declared_style(&mut self) -> &mut StyleRefinement {
-        &mut self.style
-    }
-}
-
-impl<V> StyleHelpers for Div<V> {}
-
-impl<V> Interactive<V> for Div<V> {
-    fn interaction_handlers(&mut self) -> &mut InteractionHandlers<V> {
-        &mut self.handlers
-    }
-}
-
-impl<V: 'static> ParentElement<V> for Div<V> {
-    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
-        &mut self.children
-    }
-}
-
-#[test]
-fn test() {
-    // let elt = div().w_auto();
-}
-
-// trait Element<V: 'static> {
-//     type Style;
-
-//     fn layout()
-// }
-
-// trait Stylable<V: 'static>: Element<V> {
-//     type Style;
-
-//     fn with_style(self, style: Self::Style) -> Self;
-// }
-
-// pub struct HoverStyle<S> {
-//     default: S,
-//     hovered: S,
-// }
-
-// struct Hover<V: 'static, C: Stylable<V>> {
-//     child: C,
-//     style: HoverStyle<C::Style>,
-// }
-
-// impl<V: 'static, C: Stylable<V>> Hover<V, C> {
-//     fn new(child: C, style: HoverStyle<C::Style>) -> Self {
-//         Self { child, style }
-//     }
-// }

crates/gpui/playground/src/element.rs πŸ”—

@@ -1,158 +0,0 @@
-use anyhow::Result;
-use derive_more::{Deref, DerefMut};
-use gpui::{geometry::rect::RectF, EngineLayout};
-use smallvec::SmallVec;
-use std::marker::PhantomData;
-use util::ResultExt;
-
-pub use crate::layout_context::LayoutContext;
-pub use crate::paint_context::PaintContext;
-
-type LayoutId = gpui::LayoutId;
-
-pub trait Element<V: 'static>: 'static {
-    type Layout;
-
-    fn layout(
-        &mut self,
-        view: &mut V,
-        cx: &mut LayoutContext<V>,
-    ) -> Result<Layout<V, Self::Layout>>
-    where
-        Self: Sized;
-
-    fn paint(
-        &mut self,
-        view: &mut V,
-        layout: &mut Layout<V, Self::Layout>,
-        cx: &mut PaintContext<V>,
-    ) where
-        Self: Sized;
-
-    fn into_any(self) -> AnyElement<V>
-    where
-        Self: 'static + Sized,
-    {
-        AnyElement(Box::new(ElementState {
-            element: self,
-            layout: None,
-        }))
-    }
-}
-
-/// Used to make ElementState<V, E> into a trait object, so we can wrap it in AnyElement<V>.
-trait ElementStateObject<V> {
-    fn layout(&mut self, view: &mut V, cx: &mut LayoutContext<V>) -> Result<LayoutId>;
-    fn paint(&mut self, view: &mut V, cx: &mut PaintContext<V>);
-}
-
-/// A wrapper around an element that stores its layout state.
-struct ElementState<V: 'static, E: Element<V>> {
-    element: E,
-    layout: Option<Layout<V, E::Layout>>,
-}
-
-/// We blanket-implement the object-safe ElementStateObject interface to make ElementStates into trait objects
-impl<V, E: Element<V>> ElementStateObject<V> for ElementState<V, E> {
-    fn layout(&mut self, view: &mut V, cx: &mut LayoutContext<V>) -> Result<LayoutId> {
-        let layout = self.element.layout(view, cx)?;
-        let layout_id = layout.id;
-        self.layout = Some(layout);
-        Ok(layout_id)
-    }
-
-    fn paint(&mut self, view: &mut V, cx: &mut PaintContext<V>) {
-        let layout = self.layout.as_mut().expect("paint called before layout");
-        if layout.engine_layout.is_none() {
-            layout.engine_layout = cx.computed_layout(layout.id).log_err()
-        }
-        self.element.paint(view, layout, cx)
-    }
-}
-
-/// A dynamic element.
-pub struct AnyElement<V>(Box<dyn ElementStateObject<V>>);
-
-impl<V> AnyElement<V> {
-    pub fn layout(&mut self, view: &mut V, cx: &mut LayoutContext<V>) -> Result<LayoutId> {
-        self.0.layout(view, cx)
-    }
-
-    pub fn paint(&mut self, view: &mut V, cx: &mut PaintContext<V>) {
-        self.0.paint(view, cx)
-    }
-}
-
-#[derive(Deref, DerefMut)]
-pub struct Layout<V, D> {
-    id: LayoutId,
-    engine_layout: Option<EngineLayout>,
-    #[deref]
-    #[deref_mut]
-    element_data: D,
-    view_type: PhantomData<V>,
-}
-
-impl<V: 'static, D> Layout<V, D> {
-    pub fn new(id: LayoutId, element_data: D) -> Self {
-        Self {
-            id,
-            engine_layout: None,
-            element_data: element_data,
-            view_type: PhantomData,
-        }
-    }
-
-    pub fn bounds(&mut self, cx: &mut PaintContext<V>) -> RectF {
-        self.engine_layout(cx).bounds
-    }
-
-    pub fn order(&mut self, cx: &mut PaintContext<V>) -> u32 {
-        self.engine_layout(cx).order
-    }
-
-    fn engine_layout(&mut self, cx: &mut PaintContext<'_, '_, '_, '_, V>) -> &mut EngineLayout {
-        self.engine_layout
-            .get_or_insert_with(|| cx.computed_layout(self.id).log_err().unwrap_or_default())
-    }
-}
-
-impl<V: 'static> Layout<V, Option<AnyElement<V>>> {
-    pub fn paint(&mut self, view: &mut V, cx: &mut PaintContext<V>) {
-        let mut element = self.element_data.take().unwrap();
-        element.paint(view, cx);
-        self.element_data = Some(element);
-    }
-}
-
-pub trait ParentElement<V: 'static> {
-    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]>;
-
-    fn child(mut self, child: impl IntoElement<V>) -> Self
-    where
-        Self: Sized,
-    {
-        self.children_mut().push(child.into_element().into_any());
-        self
-    }
-
-    fn children<I, E>(mut self, children: I) -> Self
-    where
-        I: IntoIterator<Item = E>,
-        E: IntoElement<V>,
-        Self: Sized,
-    {
-        self.children_mut().extend(
-            children
-                .into_iter()
-                .map(|child| child.into_element().into_any()),
-        );
-        self
-    }
-}
-
-pub trait IntoElement<V: 'static> {
-    type Element: Element<V>;
-
-    fn into_element(self) -> Self::Element;
-}

crates/gpui/playground/src/hoverable.rs πŸ”—

@@ -1,76 +0,0 @@
-use crate::{
-    element::{Element, Layout},
-    layout_context::LayoutContext,
-    paint_context::PaintContext,
-    style::{StyleRefinement, Styleable},
-};
-use anyhow::Result;
-use gpui::platform::MouseMovedEvent;
-use refineable::Refineable;
-use std::{cell::Cell, marker::PhantomData};
-
-pub struct Hoverable<V: 'static, E: Element<V> + Styleable> {
-    hovered: Cell<bool>,
-    child_style: StyleRefinement,
-    hovered_style: StyleRefinement,
-    child: E,
-    view_type: PhantomData<V>,
-}
-
-pub fn hoverable<V, E: Element<V> + Styleable>(mut child: E) -> Hoverable<V, E> {
-    Hoverable {
-        hovered: Cell::new(false),
-        child_style: child.declared_style().clone(),
-        hovered_style: Default::default(),
-        child,
-        view_type: PhantomData,
-    }
-}
-
-impl<V, E: Element<V> + Styleable> Styleable for Hoverable<V, E> {
-    type Style = E::Style;
-
-    fn declared_style(&mut self) -> &mut crate::style::StyleRefinement {
-        self.child.declared_style()
-    }
-}
-
-impl<V: 'static, E: Element<V> + Styleable> Element<V> for Hoverable<V, E> {
-    type Layout = E::Layout;
-
-    fn layout(&mut self, view: &mut V, cx: &mut LayoutContext<V>) -> Result<Layout<V, Self::Layout>>
-    where
-        Self: Sized,
-    {
-        self.child.layout(view, cx)
-    }
-
-    fn paint(
-        &mut self,
-        view: &mut V,
-        layout: &mut Layout<V, Self::Layout>,
-        cx: &mut PaintContext<V>,
-    ) where
-        Self: Sized,
-    {
-        if self.hovered.get() {
-            // If hovered, refine the child's style with this element's style.
-            self.child.declared_style().refine(&self.hovered_style);
-        } else {
-            // Otherwise, set the child's style back to its original style.
-            *self.child.declared_style() = self.child_style.clone();
-        }
-
-        let bounds = layout.bounds(cx);
-        let order = layout.order(cx);
-        self.hovered.set(bounds.contains_point(cx.mouse_position()));
-        let was_hovered = self.hovered.clone();
-        cx.on_event(order, move |view, event: &MouseMovedEvent, cx| {
-            let is_hovered = bounds.contains_point(event.position);
-            if is_hovered != was_hovered.get() {
-                was_hovered.set(is_hovered);
-                cx.repaint();
-            }
-        });
-    }
-}

crates/gpui/playground/src/interactive.rs πŸ”—

@@ -1,34 +0,0 @@
-use gpui::{platform::MouseMovedEvent, EventContext};
-use smallvec::SmallVec;
-use std::rc::Rc;
-
-pub trait Interactive<V: 'static> {
-    fn interaction_handlers(&mut self) -> &mut InteractionHandlers<V>;
-
-    fn on_mouse_move<H>(mut self, handler: H) -> Self
-    where
-        H: 'static + Fn(&mut V, &MouseMovedEvent, bool, &mut EventContext<V>),
-        Self: Sized,
-    {
-        self.interaction_handlers()
-            .mouse_moved
-            .push(Rc::new(move |view, event, hit_test, cx| {
-                handler(view, event, hit_test, cx);
-                cx.bubble
-            }));
-        self
-    }
-}
-
-pub struct InteractionHandlers<V: 'static> {
-    mouse_moved:
-        SmallVec<[Rc<dyn Fn(&mut V, &MouseMovedEvent, bool, &mut EventContext<V>) -> bool>; 2]>,
-}
-
-impl<V> Default for InteractionHandlers<V> {
-    fn default() -> Self {
-        Self {
-            mouse_moved: Default::default(),
-        }
-    }
-}

crates/gpui/playground/src/layout_context.rs πŸ”—

@@ -1,54 +0,0 @@
-use anyhow::{anyhow, Result};
-use derive_more::{Deref, DerefMut};
-pub use gpui::LayoutContext as LegacyLayoutContext;
-use gpui::{RenderContext, ViewContext};
-pub use taffy::tree::NodeId;
-
-use crate::{element::Layout, style::Style};
-
-#[derive(Deref, DerefMut)]
-pub struct LayoutContext<'a, 'b, 'c, 'd, V> {
-    #[deref]
-    #[deref_mut]
-    pub(crate) legacy_cx: &'d mut LegacyLayoutContext<'a, 'b, 'c, V>,
-}
-
-impl<'a, 'b, V> RenderContext<'a, 'b, V> for LayoutContext<'a, 'b, '_, '_, V> {
-    fn text_style(&self) -> gpui::fonts::TextStyle {
-        self.legacy_cx.text_style()
-    }
-
-    fn push_text_style(&mut self, style: gpui::fonts::TextStyle) {
-        self.legacy_cx.push_text_style(style)
-    }
-
-    fn pop_text_style(&mut self) {
-        self.legacy_cx.pop_text_style()
-    }
-
-    fn as_view_context(&mut self) -> &mut ViewContext<'a, 'b, V> {
-        &mut self.view_context
-    }
-}
-
-impl<'a, 'b, 'c, 'd, V: 'static> LayoutContext<'a, 'b, 'c, 'd, V> {
-    pub fn new(legacy_cx: &'d mut LegacyLayoutContext<'a, 'b, 'c, V>) -> Self {
-        Self { legacy_cx }
-    }
-
-    pub fn add_layout_node<D>(
-        &mut self,
-        style: Style,
-        element_data: D,
-        children: impl IntoIterator<Item = NodeId>,
-    ) -> Result<Layout<V, D>> {
-        let rem_size = self.rem_pixels();
-        let id = self
-            .legacy_cx
-            .layout_engine()
-            .ok_or_else(|| anyhow!("no layout engine"))?
-            .add_node(style.to_taffy(rem_size), children)?;
-
-        Ok(Layout::new(id, element_data))
-    }
-}

crates/gpui/playground/src/paint_context.rs πŸ”—

@@ -1,71 +0,0 @@
-use anyhow::{anyhow, Result};
-use derive_more::{Deref, DerefMut};
-use gpui::{scene::EventHandler, EngineLayout, EventContext, LayoutId, RenderContext, ViewContext};
-pub use gpui::{LayoutContext, PaintContext as LegacyPaintContext};
-use std::{any::TypeId, rc::Rc};
-pub use taffy::tree::NodeId;
-
-#[derive(Deref, DerefMut)]
-pub struct PaintContext<'a, 'b, 'c, 'd, V> {
-    #[deref]
-    #[deref_mut]
-    pub(crate) legacy_cx: &'d mut LegacyPaintContext<'a, 'b, 'c, V>,
-    pub(crate) scene: &'d mut gpui::SceneBuilder,
-}
-
-impl<'a, 'b, V> RenderContext<'a, 'b, V> for PaintContext<'a, 'b, '_, '_, V> {
-    fn text_style(&self) -> gpui::fonts::TextStyle {
-        self.legacy_cx.text_style()
-    }
-
-    fn push_text_style(&mut self, style: gpui::fonts::TextStyle) {
-        self.legacy_cx.push_text_style(style)
-    }
-
-    fn pop_text_style(&mut self) {
-        self.legacy_cx.pop_text_style()
-    }
-
-    fn as_view_context(&mut self) -> &mut ViewContext<'a, 'b, V> {
-        &mut self.view_context
-    }
-}
-
-impl<'a, 'b, 'c, 'd, V: 'static> PaintContext<'a, 'b, 'c, 'd, V> {
-    pub fn new(
-        legacy_cx: &'d mut LegacyPaintContext<'a, 'b, 'c, V>,
-        scene: &'d mut gpui::SceneBuilder,
-    ) -> Self {
-        Self { legacy_cx, scene }
-    }
-
-    pub fn on_event<E: 'static>(
-        &mut self,
-        order: u32,
-        handler: impl Fn(&mut V, &E, &mut ViewContext<V>) + 'static,
-    ) {
-        let view = self.weak_handle();
-
-        self.scene.event_handlers.push(EventHandler {
-            order,
-            handler: Rc::new(move |event, window_cx| {
-                if let Some(view) = view.upgrade(window_cx) {
-                    view.update(window_cx, |view, view_cx| {
-                        let mut event_cx = EventContext::new(view_cx);
-                        handler(view, event.downcast_ref().unwrap(), &mut event_cx);
-                        event_cx.bubble
-                    })
-                } else {
-                    true
-                }
-            }),
-            event_type: TypeId::of::<E>(),
-        })
-    }
-
-    pub(crate) fn computed_layout(&mut self, layout_id: LayoutId) -> Result<EngineLayout> {
-        self.layout_engine()
-            .ok_or_else(|| anyhow!("no layout engine present"))?
-            .computed_layout(layout_id)
-    }
-}

crates/gpui/playground/src/playground.rs πŸ”—

@@ -1,83 +0,0 @@
-#![allow(dead_code, unused_variables)]
-use crate::{color::black, style::StyleHelpers};
-use element::Element;
-use gpui::{
-    geometry::{rect::RectF, vector::vec2f},
-    platform::WindowOptions,
-};
-use log::LevelFilter;
-use simplelog::SimpleLogger;
-use themes::{rose_pine, ThemeColors};
-use view::view;
-
-mod adapter;
-mod color;
-mod components;
-mod div;
-mod element;
-mod hoverable;
-mod interactive;
-mod layout_context;
-mod paint_context;
-mod style;
-mod text;
-mod themes;
-mod view;
-
-fn main() {
-    SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
-
-    gpui::App::new(()).unwrap().run(|cx| {
-        cx.add_window(
-            WindowOptions {
-                bounds: gpui::platform::WindowBounds::Fixed(RectF::new(
-                    vec2f(0., 0.),
-                    vec2f(400., 300.),
-                )),
-                center: true,
-                ..Default::default()
-            },
-            |_| view(|_| playground(&rose_pine::moon())),
-        );
-        cx.platform().activate(true);
-    });
-}
-
-fn playground<V: 'static>(theme: &ThemeColors) -> impl Element<V> {
-    use div::div;
-
-    div()
-        .text_color(black())
-        .h_full()
-        .w_1_2()
-        .fill(theme.success(0.5))
-    // .hover()
-    // .fill(theme.error(0.5))
-    // .child(button().label("Hello").click(|_, _, _| println!("click!")))
-}
-
-//     todo!()
-//     // column()
-//     // .size(auto())
-//     // .fill(theme.base(0.5))
-//     // .text_color(theme.text(0.5))
-//     // .child(title_bar(theme))
-//     // .child(stage(theme))
-//     // .child(status_bar(theme))
-// }
-
-// fn title_bar<V: 'static>(theme: &ThemeColors) -> impl Element<V> {
-//     row()
-//         .fill(theme.base(0.2))
-//         .justify(0.)
-//         .width(auto())
-//         .child(text("Zed Playground"))
-// }
-
-// fn stage<V: 'static>(theme: &ThemeColors) -> impl Element<V> {
-//     row().fill(theme.surface(0.9))
-// }
-
-// fn status_bar<V: 'static>(theme: &ThemeColors) -> impl Element<V> {
-//     row().fill(theme.surface(0.1))
-// }

crates/gpui/playground/src/style.rs πŸ”—

@@ -1,286 +0,0 @@
-use crate::{
-    color::Hsla,
-    element::{Element, Layout},
-    paint_context::PaintContext,
-};
-use gpui::{
-    fonts::TextStyleRefinement,
-    geometry::{
-        AbsoluteLength, DefiniteLength, Edges, EdgesRefinement, Length, Point, PointRefinement,
-        Size, SizeRefinement,
-    },
-};
-use playground_macros::styleable_helpers;
-use refineable::Refineable;
-pub use taffy::style::{
-    AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap, JustifyContent,
-    Overflow, Position,
-};
-
-#[derive(Clone, Refineable)]
-pub struct Style {
-    /// What layout strategy should be used?
-    pub display: Display,
-
-    // Overflow properties
-    /// How children overflowing their container should affect layout
-    #[refineable]
-    pub overflow: Point<Overflow>,
-    /// How much space (in points) should be reserved for the scrollbars of `Overflow::Scroll` and `Overflow::Auto` nodes.
-    pub scrollbar_width: f32,
-
-    // Position properties
-    /// What should the `position` value of this struct use as a base offset?
-    pub position: Position,
-    /// How should the position of this element be tweaked relative to the layout defined?
-    #[refineable]
-    pub inset: Edges<Length>,
-
-    // Size properies
-    /// Sets the initial size of the item
-    #[refineable]
-    pub size: Size<Length>,
-    /// Controls the minimum size of the item
-    #[refineable]
-    pub min_size: Size<Length>,
-    /// Controls the maximum size of the item
-    #[refineable]
-    pub max_size: Size<Length>,
-    /// Sets the preferred aspect ratio for the item. The ratio is calculated as width divided by height.
-    pub aspect_ratio: Option<f32>,
-
-    // Spacing Properties
-    /// How large should the margin be on each side?
-    #[refineable]
-    pub margin: Edges<Length>,
-    /// How large should the padding be on each side?
-    #[refineable]
-    pub padding: Edges<DefiniteLength>,
-    /// How large should the border be on each side?
-    #[refineable]
-    pub border: Edges<DefiniteLength>,
-
-    // Alignment properties
-    /// How this node's children aligned in the cross/block axis?
-    pub align_items: Option<AlignItems>,
-    /// How this node should be aligned in the cross/block axis. Falls back to the parents [`AlignItems`] if not set
-    pub align_self: Option<AlignSelf>,
-    /// How should content contained within this item be aligned in the cross/block axis
-    pub align_content: Option<AlignContent>,
-    /// How should contained within this item be aligned in the main/inline axis
-    pub justify_content: Option<JustifyContent>,
-    /// How large should the gaps between items in a flex container be?
-    #[refineable]
-    pub gap: Size<DefiniteLength>,
-
-    // Flexbox properies
-    /// Which direction does the main axis flow in?
-    pub flex_direction: FlexDirection,
-    /// Should elements wrap, or stay in a single line?
-    pub flex_wrap: FlexWrap,
-    /// Sets the initial main axis size of the item
-    pub flex_basis: Length,
-    /// The relative rate at which this item grows when it is expanding to fill space, 0.0 is the default value, and this value must be positive.
-    pub flex_grow: f32,
-    /// The relative rate at which this item shrinks when it is contracting to fit into space, 1.0 is the default value, and this value must be positive.
-    pub flex_shrink: f32,
-
-    /// The fill color of this element
-    pub fill: Option<Fill>,
-    /// The radius of the corners of this element
-    #[refineable]
-    pub corner_radii: CornerRadii,
-    /// The color of text within this element. Cascades to children unless overridden.
-    pub text_color: Option<Hsla>,
-}
-
-impl Style {
-    pub fn to_taffy(&self, rem_size: f32) -> taffy::style::Style {
-        taffy::style::Style {
-            display: self.display,
-            overflow: self.overflow.clone().into(),
-            scrollbar_width: self.scrollbar_width,
-            position: self.position,
-            inset: self.inset.to_taffy(rem_size),
-            size: self.size.to_taffy(rem_size),
-            min_size: self.min_size.to_taffy(rem_size),
-            max_size: self.max_size.to_taffy(rem_size),
-            aspect_ratio: self.aspect_ratio,
-            margin: self.margin.to_taffy(rem_size),
-            padding: self.padding.to_taffy(rem_size),
-            border: self.border.to_taffy(rem_size),
-            align_items: self.align_items,
-            align_self: self.align_self,
-            align_content: self.align_content,
-            justify_content: self.justify_content,
-            gap: self.gap.to_taffy(rem_size),
-            flex_direction: self.flex_direction,
-            flex_wrap: self.flex_wrap,
-            flex_basis: self.flex_basis.to_taffy(rem_size).into(),
-            flex_grow: self.flex_grow,
-            flex_shrink: self.flex_shrink,
-            ..Default::default() // Ignore grid properties for now
-        }
-    }
-
-    /// Paints the background of an element styled with this style.
-    /// Return the bounds in which to paint the content.
-    pub fn paint_background<V: 'static, E: Element<V>>(
-        &self,
-        layout: &mut Layout<V, E::Layout>,
-        cx: &mut PaintContext<V>,
-    ) {
-        let bounds = layout.bounds(cx);
-        let rem_size = cx.rem_pixels();
-        if let Some(color) = self.fill.as_ref().and_then(Fill::color) {
-            cx.scene.push_quad(gpui::Quad {
-                bounds,
-                background: Some(color.into()),
-                corner_radii: self.corner_radii.to_gpui(rem_size),
-                border: Default::default(),
-            });
-        }
-    }
-}
-
-impl Default for Style {
-    fn default() -> Self {
-        Style {
-            display: Display::DEFAULT,
-            overflow: Point {
-                x: Overflow::Visible,
-                y: Overflow::Visible,
-            },
-            scrollbar_width: 0.0,
-            position: Position::Relative,
-            inset: Edges::auto(),
-            margin: Edges::<Length>::zero(),
-            padding: Edges::<DefiniteLength>::zero(),
-            border: Edges::<DefiniteLength>::zero(),
-            size: Size::auto(),
-            min_size: Size::auto(),
-            max_size: Size::auto(),
-            aspect_ratio: None,
-            gap: Size::zero(),
-            // Aligment
-            align_items: None,
-            align_self: None,
-            align_content: None,
-            justify_content: None,
-            // Flexbox
-            flex_direction: FlexDirection::Row,
-            flex_wrap: FlexWrap::NoWrap,
-            flex_grow: 0.0,
-            flex_shrink: 1.0,
-            flex_basis: Length::Auto,
-            fill: None,
-            text_color: None,
-            corner_radii: CornerRadii::default(),
-        }
-    }
-}
-
-impl StyleRefinement {
-    pub fn text_style(&self) -> Option<TextStyleRefinement> {
-        self.text_color.map(|color| TextStyleRefinement {
-            color: Some(color.into()),
-            ..Default::default()
-        })
-    }
-}
-
-pub struct OptionalTextStyle {
-    color: Option<Hsla>,
-}
-
-impl OptionalTextStyle {
-    pub fn apply(&self, style: &mut gpui::fonts::TextStyle) {
-        if let Some(color) = self.color {
-            style.color = color.into();
-        }
-    }
-}
-
-#[derive(Clone)]
-pub enum Fill {
-    Color(Hsla),
-}
-
-impl Fill {
-    pub fn color(&self) -> Option<Hsla> {
-        match self {
-            Fill::Color(color) => Some(*color),
-        }
-    }
-}
-
-impl Default for Fill {
-    fn default() -> Self {
-        Self::Color(Hsla::default())
-    }
-}
-
-impl From<Hsla> for Fill {
-    fn from(color: Hsla) -> Self {
-        Self::Color(color)
-    }
-}
-
-#[derive(Clone, Refineable, Default)]
-pub struct CornerRadii {
-    top_left: AbsoluteLength,
-    top_right: AbsoluteLength,
-    bottom_left: AbsoluteLength,
-    bottom_right: AbsoluteLength,
-}
-
-impl CornerRadii {
-    pub fn to_gpui(&self, rem_size: f32) -> gpui::scene::CornerRadii {
-        gpui::scene::CornerRadii {
-            top_left: self.top_left.to_pixels(rem_size),
-            top_right: self.top_right.to_pixels(rem_size),
-            bottom_left: self.bottom_left.to_pixels(rem_size),
-            bottom_right: self.bottom_right.to_pixels(rem_size),
-        }
-    }
-}
-
-pub trait Styleable {
-    type Style: refineable::Refineable;
-
-    fn declared_style(&mut self) -> &mut playground::style::StyleRefinement;
-
-    fn style(&mut self) -> playground::style::Style {
-        let mut style = playground::style::Style::default();
-        style.refine(self.declared_style());
-        style
-    }
-}
-
-// Helpers methods that take and return mut self. This includes tailwind style methods for standard sizes etc.
-//
-// Example:
-// // Sets the padding to 0.5rem, just like class="p-2" in Tailwind.
-// fn p_2(mut self) -> Self where Self: Sized;
-use crate as playground; // Macro invocation references this crate as playground.
-pub trait StyleHelpers: Styleable<Style = Style> {
-    styleable_helpers!();
-
-    fn fill<F>(mut self, fill: F) -> Self
-    where
-        F: Into<Fill>,
-        Self: Sized,
-    {
-        self.declared_style().fill = Some(fill.into());
-        self
-    }
-
-    fn text_color<C>(mut self, color: C) -> Self
-    where
-        C: Into<Hsla>,
-        Self: Sized,
-    {
-        self.declared_style().text_color = Some(color.into());
-        self
-    }
-}

crates/gpui/playground/src/text.rs πŸ”—

@@ -1,151 +0,0 @@
-use crate::{
-    element::{Element, IntoElement, Layout},
-    layout_context::LayoutContext,
-    paint_context::PaintContext,
-};
-use anyhow::Result;
-use gpui::text_layout::LineLayout;
-use parking_lot::Mutex;
-use std::sync::Arc;
-
-impl<V: 'static, S: Into<ArcCow<'static, str>>> IntoElement<V> for S {
-    type Element = Text;
-
-    fn into_element(self) -> Self::Element {
-        Text { text: self.into() }
-    }
-}
-
-pub struct Text {
-    text: ArcCow<'static, str>,
-}
-
-impl<V: 'static> Element<V> for Text {
-    type Layout = Arc<Mutex<Option<TextLayout>>>;
-
-    fn layout(
-        &mut self,
-        view: &mut V,
-        cx: &mut LayoutContext<V>,
-    ) -> Result<Layout<V, Self::Layout>> {
-        // let rem_size = cx.rem_pixels();
-        // let fonts = cx.platform().fonts();
-        // let text_style = cx.text_style();
-        // let line_height = cx.font_cache().line_height(text_style.font_size);
-        // let layout_engine = cx.layout_engine().expect("no layout engine present");
-        // let text = self.text.clone();
-        // let layout = Arc::new(Mutex::new(None));
-
-        // let style: Style = Style::default().refined(&self.metadata.style);
-        // let node_id = layout_engine.add_measured_node(style.to_taffy(rem_size), {
-        //     let layout = layout.clone();
-        //     move |params| {
-        //         let line_layout = fonts.layout_line(
-        //             text.as_ref(),
-        //             text_style.font_size,
-        //             &[(text.len(), text_style.to_run())],
-        //         );
-
-        //         let size = Size {
-        //             width: line_layout.width,
-        //             height: line_height,
-        //         };
-
-        //         layout.lock().replace(TextLayout {
-        //             line_layout: Arc::new(line_layout),
-        //             line_height,
-        //         });
-
-        //         size
-        //     }
-        // })?;
-
-        // Ok((node_id, layout))
-        todo!()
-    }
-
-    fn paint<'a>(
-        &mut self,
-        view: &mut V,
-        layout: &mut Layout<V, Self::Layout>,
-        cx: &mut PaintContext<V>,
-    ) {
-        // ) {
-        //     let element_layout_lock = layout.from_element.lock();
-        //     let element_layout = element_layout_lock
-        //         .as_ref()
-        //         .expect("layout has not been performed");
-        //     let line_layout = element_layout.line_layout.clone();
-        //     let line_height = element_layout.line_height;
-        //     drop(element_layout_lock);
-
-        //     let text_style = cx.text_style();
-        //     let line =
-        //         gpui::text_layout::Line::new(line_layout, &[(self.text.len(), text_style.to_run())]);
-        //     line.paint(
-        //         cx.scene,
-        //         layout.from_engine.bounds.origin(),
-        //         layout.from_engine.bounds,
-        //         line_height,
-        //         cx.legacy_cx,
-        //     );
-        todo!()
-    }
-}
-
-pub struct TextLayout {
-    line_layout: Arc<LineLayout>,
-    line_height: f32,
-}
-
-pub enum ArcCow<'a, T: ?Sized> {
-    Borrowed(&'a T),
-    Owned(Arc<T>),
-}
-
-impl<'a, T: ?Sized> Clone for ArcCow<'a, T> {
-    fn clone(&self) -> Self {
-        match self {
-            Self::Borrowed(borrowed) => Self::Borrowed(borrowed),
-            Self::Owned(owned) => Self::Owned(owned.clone()),
-        }
-    }
-}
-
-impl<'a, T: ?Sized> From<&'a T> for ArcCow<'a, T> {
-    fn from(s: &'a T) -> Self {
-        Self::Borrowed(s)
-    }
-}
-
-impl<T> From<Arc<T>> for ArcCow<'_, T> {
-    fn from(s: Arc<T>) -> Self {
-        Self::Owned(s)
-    }
-}
-
-impl From<String> for ArcCow<'_, str> {
-    fn from(value: String) -> Self {
-        Self::Owned(value.into())
-    }
-}
-
-impl<T: ?Sized> std::ops::Deref for ArcCow<'_, T> {
-    type Target = T;
-
-    fn deref(&self) -> &Self::Target {
-        match self {
-            ArcCow::Borrowed(s) => s,
-            ArcCow::Owned(s) => s.as_ref(),
-        }
-    }
-}
-
-impl<T: ?Sized> AsRef<T> for ArcCow<'_, T> {
-    fn as_ref(&self) -> &T {
-        match self {
-            ArcCow::Borrowed(borrowed) => borrowed,
-            ArcCow::Owned(owned) => owned.as_ref(),
-        }
-    }
-}

crates/gpui/playground/src/themes.rs πŸ”—

@@ -1,84 +0,0 @@
-use crate::color::{Hsla, Lerp};
-use std::ops::Range;
-
-pub mod rose_pine;
-
-pub struct ThemeColors {
-    pub base: Range<Hsla>,
-    pub surface: Range<Hsla>,
-    pub overlay: Range<Hsla>,
-    pub muted: Range<Hsla>,
-    pub subtle: Range<Hsla>,
-    pub text: Range<Hsla>,
-    pub highlight_low: Range<Hsla>,
-    pub highlight_med: Range<Hsla>,
-    pub highlight_high: Range<Hsla>,
-    pub success: Range<Hsla>,
-    pub warning: Range<Hsla>,
-    pub error: Range<Hsla>,
-    pub inserted: Range<Hsla>,
-    pub deleted: Range<Hsla>,
-    pub modified: Range<Hsla>,
-}
-
-impl ThemeColors {
-    pub fn base(&self, level: f32) -> Hsla {
-        self.base.lerp(level)
-    }
-
-    pub fn surface(&self, level: f32) -> Hsla {
-        self.surface.lerp(level)
-    }
-
-    pub fn overlay(&self, level: f32) -> Hsla {
-        self.overlay.lerp(level)
-    }
-
-    pub fn muted(&self, level: f32) -> Hsla {
-        self.muted.lerp(level)
-    }
-
-    pub fn subtle(&self, level: f32) -> Hsla {
-        self.subtle.lerp(level)
-    }
-
-    pub fn text(&self, level: f32) -> Hsla {
-        self.text.lerp(level)
-    }
-
-    pub fn highlight_low(&self, level: f32) -> Hsla {
-        self.highlight_low.lerp(level)
-    }
-
-    pub fn highlight_med(&self, level: f32) -> Hsla {
-        self.highlight_med.lerp(level)
-    }
-
-    pub fn highlight_high(&self, level: f32) -> Hsla {
-        self.highlight_high.lerp(level)
-    }
-
-    pub fn success(&self, level: f32) -> Hsla {
-        self.success.lerp(level)
-    }
-
-    pub fn warning(&self, level: f32) -> Hsla {
-        self.warning.lerp(level)
-    }
-
-    pub fn error(&self, level: f32) -> Hsla {
-        self.error.lerp(level)
-    }
-
-    pub fn inserted(&self, level: f32) -> Hsla {
-        self.inserted.lerp(level)
-    }
-
-    pub fn deleted(&self, level: f32) -> Hsla {
-        self.deleted.lerp(level)
-    }
-
-    pub fn modified(&self, level: f32) -> Hsla {
-        self.modified.lerp(level)
-    }
-}

crates/gpui/playground/src/themes/rose_pine.rs πŸ”—

@@ -1,133 +0,0 @@
-use std::ops::Range;
-
-use crate::{
-    color::{hsla, rgb, Hsla},
-    ThemeColors,
-};
-
-pub struct RosePineThemes {
-    pub default: RosePinePalette,
-    pub dawn: RosePinePalette,
-    pub moon: RosePinePalette,
-}
-
-#[derive(Clone, Copy, Debug)]
-pub struct RosePinePalette {
-    pub base: Hsla,
-    pub surface: Hsla,
-    pub overlay: Hsla,
-    pub muted: Hsla,
-    pub subtle: Hsla,
-    pub text: Hsla,
-    pub love: Hsla,
-    pub gold: Hsla,
-    pub rose: Hsla,
-    pub pine: Hsla,
-    pub foam: Hsla,
-    pub iris: Hsla,
-    pub highlight_low: Hsla,
-    pub highlight_med: Hsla,
-    pub highlight_high: Hsla,
-}
-
-impl RosePinePalette {
-    pub fn default() -> RosePinePalette {
-        RosePinePalette {
-            base: rgb(0x191724),
-            surface: rgb(0x1f1d2e),
-            overlay: rgb(0x26233a),
-            muted: rgb(0x6e6a86),
-            subtle: rgb(0x908caa),
-            text: rgb(0xe0def4),
-            love: rgb(0xeb6f92),
-            gold: rgb(0xf6c177),
-            rose: rgb(0xebbcba),
-            pine: rgb(0x31748f),
-            foam: rgb(0x9ccfd8),
-            iris: rgb(0xc4a7e7),
-            highlight_low: rgb(0x21202e),
-            highlight_med: rgb(0x403d52),
-            highlight_high: rgb(0x524f67),
-        }
-    }
-
-    pub fn moon() -> RosePinePalette {
-        RosePinePalette {
-            base: rgb(0x232136),
-            surface: rgb(0x2a273f),
-            overlay: rgb(0x393552),
-            muted: rgb(0x6e6a86),
-            subtle: rgb(0x908caa),
-            text: rgb(0xe0def4),
-            love: rgb(0xeb6f92),
-            gold: rgb(0xf6c177),
-            rose: rgb(0xea9a97),
-            pine: rgb(0x3e8fb0),
-            foam: rgb(0x9ccfd8),
-            iris: rgb(0xc4a7e7),
-            highlight_low: rgb(0x2a283e),
-            highlight_med: rgb(0x44415a),
-            highlight_high: rgb(0x56526e),
-        }
-    }
-
-    pub fn dawn() -> RosePinePalette {
-        RosePinePalette {
-            base: rgb(0xfaf4ed),
-            surface: rgb(0xfffaf3),
-            overlay: rgb(0xf2e9e1),
-            muted: rgb(0x9893a5),
-            subtle: rgb(0x797593),
-            text: rgb(0x575279),
-            love: rgb(0xb4637a),
-            gold: rgb(0xea9d34),
-            rose: rgb(0xd7827e),
-            pine: rgb(0x286983),
-            foam: rgb(0x56949f),
-            iris: rgb(0x907aa9),
-            highlight_low: rgb(0xf4ede8),
-            highlight_med: rgb(0xdfdad9),
-            highlight_high: rgb(0xcecacd),
-        }
-    }
-}
-
-pub fn default() -> ThemeColors {
-    theme_colors(&RosePinePalette::default())
-}
-
-pub fn moon() -> ThemeColors {
-    theme_colors(&RosePinePalette::moon())
-}
-
-pub fn dawn() -> ThemeColors {
-    theme_colors(&RosePinePalette::dawn())
-}
-
-fn theme_colors(p: &RosePinePalette) -> ThemeColors {
-    ThemeColors {
-        base: scale_sl(p.base, (0.8, 0.8), (1.2, 1.2)),
-        surface: scale_sl(p.surface, (0.8, 0.8), (1.2, 1.2)),
-        overlay: scale_sl(p.overlay, (0.8, 0.8), (1.2, 1.2)),
-        muted: scale_sl(p.muted, (0.8, 0.8), (1.2, 1.2)),
-        subtle: scale_sl(p.subtle, (0.8, 0.8), (1.2, 1.2)),
-        text: scale_sl(p.text, (0.8, 0.8), (1.2, 1.2)),
-        highlight_low: scale_sl(p.highlight_low, (0.8, 0.8), (1.2, 1.2)),
-        highlight_med: scale_sl(p.highlight_med, (0.8, 0.8), (1.2, 1.2)),
-        highlight_high: scale_sl(p.highlight_high, (0.8, 0.8), (1.2, 1.2)),
-        success: scale_sl(p.foam, (0.8, 0.8), (1.2, 1.2)),
-        warning: scale_sl(p.gold, (0.8, 0.8), (1.2, 1.2)),
-        error: scale_sl(p.love, (0.8, 0.8), (1.2, 1.2)),
-        inserted: scale_sl(p.foam, (0.8, 0.8), (1.2, 1.2)),
-        deleted: scale_sl(p.love, (0.8, 0.8), (1.2, 1.2)),
-        modified: scale_sl(p.rose, (0.8, 0.8), (1.2, 1.2)),
-    }
-}
-
-/// Produces a range by multiplying the saturation and lightness of the base color by the given
-/// start and end factors.
-fn scale_sl(base: Hsla, (start_s, start_l): (f32, f32), (end_s, end_l): (f32, f32)) -> Range<Hsla> {
-    let start = hsla(base.h, base.s * start_s, base.l * start_l, base.a);
-    let end = hsla(base.h, base.s * end_s, base.l * end_l, base.a);
-    Range { start, end }
-}

crates/gpui/playground_macros/src/styleable_helpers.rs πŸ”—

@@ -1,147 +0,0 @@
-use proc_macro::TokenStream;
-use proc_macro2::TokenStream as TokenStream2;
-use quote::{format_ident, quote};
-use syn::{
-    parse::{Parse, ParseStream, Result},
-    parse_macro_input,
-};
-
-struct StyleableMacroInput;
-
-impl Parse for StyleableMacroInput {
-    fn parse(_input: ParseStream) -> Result<Self> {
-        Ok(StyleableMacroInput)
-    }
-}
-
-pub fn styleable_helpers(input: TokenStream) -> TokenStream {
-    let _ = parse_macro_input!(input as StyleableMacroInput);
-    let methods = generate_methods();
-    let output = quote! {
-        #(#methods)*
-    };
-    output.into()
-}
-
-fn generate_methods() -> Vec<TokenStream2> {
-    let mut methods = Vec::new();
-
-    for (prefix, auto_allowed, fields) in tailwind_prefixes() {
-        for (suffix, length_tokens) in tailwind_lengths() {
-            if !auto_allowed && suffix == "auto" {
-                // Conditional to skip "auto"
-                continue;
-            }
-
-            let method_name = format_ident!("{}_{}", prefix, suffix);
-            let field_assignments = fields
-                .iter()
-                .map(|field_tokens| {
-                    quote! {
-                        style.#field_tokens = Some(gpui::geometry::#length_tokens);
-                    }
-                })
-                .collect::<Vec<_>>();
-
-            let method = quote! {
-                fn #method_name(mut self) -> Self where Self: std::marker::Sized {
-                    let mut style = self.declared_style();
-                    #(#field_assignments)*
-                    self
-                }
-            };
-
-            methods.push(method);
-        }
-    }
-
-    methods
-}
-
-fn tailwind_lengths() -> Vec<(&'static str, TokenStream2)> {
-    vec![
-        ("0", quote! { pixels(0.) }),
-        ("1", quote! { rems(0.25) }),
-        ("2", quote! { rems(0.5) }),
-        ("3", quote! { rems(0.75) }),
-        ("4", quote! { rems(1.) }),
-        ("5", quote! { rems(1.25) }),
-        ("6", quote! { rems(1.5) }),
-        ("8", quote! { rems(2.0) }),
-        ("10", quote! { rems(2.5) }),
-        ("12", quote! { rems(3.) }),
-        ("16", quote! { rems(4.) }),
-        ("20", quote! { rems(5.) }),
-        ("24", quote! { rems(6.) }),
-        ("32", quote! { rems(8.) }),
-        ("40", quote! { rems(10.) }),
-        ("48", quote! { rems(12.) }),
-        ("56", quote! { rems(14.) }),
-        ("64", quote! { rems(16.) }),
-        ("72", quote! { rems(18.) }),
-        ("80", quote! { rems(20.) }),
-        ("96", quote! { rems(24.) }),
-        ("auto", quote! { auto() }),
-        ("px", quote! { pixels(1.) }),
-        ("full", quote! { relative(1.) }),
-        ("1_2", quote! { relative(0.5) }),
-        ("1_3", quote! { relative(1./3.) }),
-        ("2_3", quote! { relative(2./3.) }),
-        ("1_4", quote! { relative(0.25) }),
-        ("2_4", quote! { relative(0.5) }),
-        ("3_4", quote! { relative(0.75) }),
-        ("1_5", quote! { relative(0.2) }),
-        ("2_5", quote! { relative(0.4) }),
-        ("3_5", quote! { relative(0.6) }),
-        ("4_5", quote! { relative(0.8) }),
-        ("1_6", quote! { relative(1./6.) }),
-        ("5_6", quote! { relative(5./6.) }),
-        ("1_12", quote! { relative(1./12.) }),
-        // ("screen_50", quote! { DefiniteLength::Vh(50.0) }),
-        // ("screen_75", quote! { DefiniteLength::Vh(75.0) }),
-        // ("screen", quote! { DefiniteLength::Vh(100.0) }),
-    ]
-}
-
-fn tailwind_prefixes() -> Vec<(&'static str, bool, Vec<TokenStream2>)> {
-    vec![
-        ("w", true, vec![quote! { size.width }]),
-        ("h", true, vec![quote! { size.height }]),
-        ("min_w", false, vec![quote! { min_size.width }]),
-        ("min_h", false, vec![quote! { min_size.height }]),
-        ("max_w", false, vec![quote! { max_size.width }]),
-        ("max_h", false, vec![quote! { max_size.height }]),
-        (
-            "m",
-            true,
-            vec![quote! { margin.top }, quote! { margin.bottom }],
-        ),
-        ("mt", true, vec![quote! { margin.top }]),
-        ("mb", true, vec![quote! { margin.bottom }]),
-        (
-            "mx",
-            true,
-            vec![quote! { margin.left }, quote! { margin.right }],
-        ),
-        ("ml", true, vec![quote! { margin.left }]),
-        ("mr", true, vec![quote! { margin.right }]),
-        (
-            "p",
-            false,
-            vec![quote! { padding.top }, quote! { padding.bottom }],
-        ),
-        ("pt", false, vec![quote! { padding.top }]),
-        ("pb", false, vec![quote! { padding.bottom }]),
-        (
-            "px",
-            false,
-            vec![quote! { padding.left }, quote! { padding.right }],
-        ),
-        ("pl", false, vec![quote! { padding.left }]),
-        ("pr", false, vec![quote! { padding.right }]),
-        ("top", true, vec![quote! { inset.top }]),
-        ("bottom", true, vec![quote! { inset.bottom }]),
-        ("left", true, vec![quote! { inset.left }]),
-        ("right", true, vec![quote! { inset.right }]),
-    ]
-}

crates/gpui/playground_macros/src/tailwind_lengths.rs πŸ”—

@@ -1,99 +0,0 @@
-use proc_macro::TokenStream;
-use proc_macro2::TokenStream as TokenStream2;
-use quote::{format_ident, quote};
-use syn::{parse_macro_input, FnArg, ItemFn, PatType};
-
-pub fn tailwind_lengths(_attr: TokenStream, item: TokenStream) -> TokenStream {
-    let input_function = parse_macro_input!(item as ItemFn);
-
-    let visibility = &input_function.vis;
-    let function_signature = input_function.sig.clone();
-    let function_body = input_function.block;
-    let where_clause = &function_signature.generics.where_clause;
-
-    let argument_name = match function_signature.inputs.iter().nth(1) {
-        Some(FnArg::Typed(PatType { pat, .. })) => pat,
-        _ => panic!("Couldn't find the second argument in the function signature"),
-    };
-
-    let mut output_functions = TokenStream2::new();
-
-    for (length, value) in fixed_lengths() {
-        let function_name = format_ident!("{}{}", function_signature.ident, length);
-        output_functions.extend(quote! {
-            #visibility fn #function_name(mut self) -> Self #where_clause {
-                let #argument_name = #value.into();
-                #function_body
-            }
-        });
-    }
-
-    output_functions.into()
-}
-
-fn fixed_lengths() -> Vec<(&'static str, TokenStream2)> {
-    vec![
-        ("0", quote! { DefinedLength::Pixels(0.) }),
-        ("px", quote! { DefinedLength::Pixels(1.) }),
-        ("0_5", quote! { DefinedLength::Rems(0.125) }),
-        ("1", quote! { DefinedLength::Rems(0.25) }),
-        ("1_5", quote! { DefinedLength::Rems(0.375) }),
-        ("2", quote! { DefinedLength::Rems(0.5) }),
-        ("2_5", quote! { DefinedLength::Rems(0.625) }),
-        ("3", quote! { DefinedLength::Rems(0.75) }),
-        ("3_5", quote! { DefinedLength::Rems(0.875) }),
-        ("4", quote! { DefinedLength::Rems(1.) }),
-        ("5", quote! { DefinedLength::Rems(1.25) }),
-        ("6", quote! { DefinedLength::Rems(1.5) }),
-        ("7", quote! { DefinedLength::Rems(1.75) }),
-        ("8", quote! { DefinedLength::Rems(2.) }),
-        ("9", quote! { DefinedLength::Rems(2.25) }),
-        ("10", quote! { DefinedLength::Rems(2.5) }),
-        ("11", quote! { DefinedLength::Rems(2.75) }),
-        ("12", quote! { DefinedLength::Rems(3.) }),
-        ("14", quote! { DefinedLength::Rems(3.5) }),
-        ("16", quote! { DefinedLength::Rems(4.) }),
-        ("20", quote! { DefinedLength::Rems(5.) }),
-        ("24", quote! { DefinedLength::Rems(6.) }),
-        ("28", quote! { DefinedLength::Rems(7.) }),
-        ("32", quote! { DefinedLength::Rems(8.) }),
-        ("36", quote! { DefinedLength::Rems(9.) }),
-        ("40", quote! { DefinedLength::Rems(10.) }),
-        ("44", quote! { DefinedLength::Rems(11.) }),
-        ("48", quote! { DefinedLength::Rems(12.) }),
-        ("52", quote! { DefinedLength::Rems(13.) }),
-        ("56", quote! { DefinedLength::Rems(14.) }),
-        ("60", quote! { DefinedLength::Rems(15.) }),
-        ("64", quote! { DefinedLength::Rems(16.) }),
-        ("72", quote! { DefinedLength::Rems(18.) }),
-        ("80", quote! { DefinedLength::Rems(20.) }),
-        ("96", quote! { DefinedLength::Rems(24.) }),
-        ("half", quote! { DefinedLength::Percent(50.) }),
-        ("1_3rd", quote! { DefinedLength::Percent(33.333333) }),
-        ("2_3rd", quote! { DefinedLength::Percent(66.666667) }),
-        ("1_4th", quote! { DefinedLength::Percent(25.) }),
-        ("2_4th", quote! { DefinedLength::Percent(50.) }),
-        ("3_4th", quote! { DefinedLength::Percent(75.) }),
-        ("1_5th", quote! { DefinedLength::Percent(20.) }),
-        ("2_5th", quote! { DefinedLength::Percent(40.) }),
-        ("3_5th", quote! { DefinedLength::Percent(60.) }),
-        ("4_5th", quote! { DefinedLength::Percent(80.) }),
-        ("1_6th", quote! { DefinedLength::Percent(16.666667) }),
-        ("2_6th", quote! { DefinedLength::Percent(33.333333) }),
-        ("3_6th", quote! { DefinedLength::Percent(50.) }),
-        ("4_6th", quote! { DefinedLength::Percent(66.666667) }),
-        ("5_6th", quote! { DefinedLength::Percent(83.333333) }),
-        ("1_12th", quote! { DefinedLength::Percent(8.333333) }),
-        ("2_12th", quote! { DefinedLength::Percent(16.666667) }),
-        ("3_12th", quote! { DefinedLength::Percent(25.) }),
-        ("4_12th", quote! { DefinedLength::Percent(33.333333) }),
-        ("5_12th", quote! { DefinedLength::Percent(41.666667) }),
-        ("6_12th", quote! { DefinedLength::Percent(50.) }),
-        ("7_12th", quote! { DefinedLength::Percent(58.333333) }),
-        ("8_12th", quote! { DefinedLength::Percent(66.666667) }),
-        ("9_12th", quote! { DefinedLength::Percent(75.) }),
-        ("10_12th", quote! { DefinedLength::Percent(83.333333) }),
-        ("11_12th", quote! { DefinedLength::Percent(91.666667) }),
-        ("full", quote! { DefinedLength::Percent(100.) }),
-    ]
-}

crates/gpui/src/app.rs πŸ”—

@@ -10,7 +10,7 @@ mod window_input_handler;
 use crate::{
     elements::{AnyElement, AnyRootElement, RootElement},
     executor::{self, Task},
-    fonts::TextStyle,
+    image_cache::ImageCache,
     json,
     keymap_matcher::{self, Binding, KeymapContext, KeymapMatcher, Keystroke, MatchResult},
     platform::{
@@ -28,6 +28,7 @@ use collections::{hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque};
 use derive_more::Deref;
 pub use menu::*;
 use parking_lot::Mutex;
+use pathfinder_geometry::rect::RectF;
 use platform::Event;
 use postage::oneshot;
 #[cfg(any(test, feature = "test-support"))]
@@ -51,8 +52,12 @@ use std::{
 };
 #[cfg(any(test, feature = "test-support"))]
 pub use test_app_context::{ContextHandle, TestAppContext};
-use util::ResultExt;
+use util::{
+    http::{self, HttpClient},
+    ResultExt,
+};
 use uuid::Uuid;
+pub use window::MeasureParams;
 use window_input_handler::WindowInputHandler;
 
 pub trait Entity: 'static {
@@ -154,12 +159,14 @@ impl App {
         let platform = platform::current::platform();
         let foreground = Rc::new(executor::Foreground::platform(platform.dispatcher())?);
         let foreground_platform = platform::current::foreground_platform(foreground.clone());
+        let http_client = http::client();
         let app = Self(Rc::new(RefCell::new(AppContext::new(
             foreground,
             Arc::new(executor::Background::new()),
             platform.clone(),
             foreground_platform.clone(),
             Arc::new(FontCache::new(platform.fonts())),
+            http_client,
             Default::default(),
             asset_source,
         ))));
@@ -456,6 +463,7 @@ pub struct AppContext {
     pub asset_cache: Arc<AssetCache>,
     font_system: Arc<dyn FontSystem>,
     pub font_cache: Arc<FontCache>,
+    pub image_cache: Arc<ImageCache>,
     action_deserializers: HashMap<&'static str, (TypeId, DeserializeActionCallback)>,
     capture_actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
     // Entity Types -> { Action Types -> Action Handlers }
@@ -499,6 +507,7 @@ impl AppContext {
         platform: Arc<dyn platform::Platform>,
         foreground_platform: Rc<dyn platform::ForegroundPlatform>,
         font_cache: Arc<FontCache>,
+        http_client: Arc<dyn HttpClient>,
         ref_counts: RefCounts,
         asset_source: impl AssetSource,
     ) -> Self {
@@ -517,6 +526,7 @@ impl AppContext {
             platform,
             foreground_platform,
             font_cache,
+            image_cache: Arc::new(ImageCache::new(http_client)),
             asset_cache: Arc::new(AssetCache::new(asset_source)),
             action_deserializers: Default::default(),
             capture_actions: Default::default(),
@@ -1898,7 +1908,6 @@ impl AppContext {
 
     fn handle_repaint_window_effect(&mut self, window: AnyWindowHandle) {
         self.update_window(window, |cx| {
-            cx.layout(false).log_err();
             if let Some(scene) = cx.paint().log_err() {
                 cx.window.platform_window.present_scene(scene);
             }
@@ -3345,10 +3354,6 @@ impl<'a, 'b, V: 'static> ViewContext<'a, 'b, V> {
         self.element_state::<Tag, T>(element_id, T::default())
     }
 
-    pub fn rem_pixels(&self) -> f32 {
-        16.
-    }
-
     pub fn default_element_state_dynamic<T: 'static + Default>(
         &mut self,
         tag: TypeTag,
@@ -3356,136 +3361,6 @@ impl<'a, 'b, V: 'static> ViewContext<'a, 'b, V> {
     ) -> ElementStateHandle<T> {
         self.element_state_dynamic::<T>(tag, element_id, T::default())
     }
-}
-
-impl<V: View> ViewContext<'_, '_, V> {
-    pub fn emit(&mut self, event: V::Event) {
-        self.window_context
-            .pending_effects
-            .push_back(Effect::Event {
-                entity_id: self.view_id,
-                payload: Box::new(event),
-            });
-    }
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub struct TypeTag {
-    tag: TypeId,
-    composed: Option<TypeId>,
-    #[cfg(debug_assertions)]
-    tag_type_name: &'static str,
-}
-
-impl TypeTag {
-    pub fn new<Tag: 'static>() -> Self {
-        Self {
-            tag: TypeId::of::<Tag>(),
-            composed: None,
-            #[cfg(debug_assertions)]
-            tag_type_name: std::any::type_name::<Tag>(),
-        }
-    }
-
-    pub fn dynamic(tag: TypeId, #[cfg(debug_assertions)] type_name: &'static str) -> Self {
-        Self {
-            tag,
-            composed: None,
-            #[cfg(debug_assertions)]
-            tag_type_name: type_name,
-        }
-    }
-
-    pub fn compose(mut self, other: TypeTag) -> Self {
-        self.composed = Some(other.tag);
-        self
-    }
-
-    #[cfg(debug_assertions)]
-    pub(crate) fn type_name(&self) -> &'static str {
-        self.tag_type_name
-    }
-}
-
-impl<V> BorrowAppContext for ViewContext<'_, '_, V> {
-    fn read_with<T, F: FnOnce(&AppContext) -> T>(&self, f: F) -> T {
-        BorrowAppContext::read_with(&*self.window_context, f)
-    }
-
-    fn update<T, F: FnOnce(&mut AppContext) -> T>(&mut self, f: F) -> T {
-        BorrowAppContext::update(&mut *self.window_context, f)
-    }
-}
-
-impl<V> BorrowWindowContext for ViewContext<'_, '_, V> {
-    type Result<T> = T;
-
-    fn read_window<T, F: FnOnce(&WindowContext) -> T>(&self, window: AnyWindowHandle, f: F) -> T {
-        BorrowWindowContext::read_window(&*self.window_context, window, f)
-    }
-
-    fn read_window_optional<T, F>(&self, window: AnyWindowHandle, f: F) -> Option<T>
-    where
-        F: FnOnce(&WindowContext) -> Option<T>,
-    {
-        BorrowWindowContext::read_window_optional(&*self.window_context, window, f)
-    }
-
-    fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
-        &mut self,
-        window: AnyWindowHandle,
-        f: F,
-    ) -> T {
-        BorrowWindowContext::update_window(&mut *self.window_context, window, f)
-    }
-
-    fn update_window_optional<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Option<T>
-    where
-        F: FnOnce(&mut WindowContext) -> Option<T>,
-    {
-        BorrowWindowContext::update_window_optional(&mut *self.window_context, window, f)
-    }
-}
-
-/// Methods shared by both LayoutContext and PaintContext
-///
-/// It's that PaintContext should be implemented in terms of layout context and
-/// deref to it, in which case we wouldn't need this.
-pub trait RenderContext<'a, 'b, V> {
-    fn text_style(&self) -> TextStyle;
-    fn push_text_style(&mut self, style: TextStyle);
-    fn pop_text_style(&mut self);
-    fn as_view_context(&mut self) -> &mut ViewContext<'a, 'b, V>;
-}
-
-pub struct LayoutContext<'a, 'b, 'c, V> {
-    // Nathan: Making this is public while I work on playground.
-    pub view_context: &'c mut ViewContext<'a, 'b, V>,
-    new_parents: &'c mut HashMap<usize, usize>,
-    views_to_notify_if_ancestors_change: &'c mut HashMap<usize, SmallVec<[usize; 2]>>,
-    text_style_stack: Vec<TextStyle>,
-    pub refreshing: bool,
-}
-
-impl<'a, 'b, 'c, V> LayoutContext<'a, 'b, 'c, V> {
-    pub fn new(
-        view_context: &'c mut ViewContext<'a, 'b, V>,
-        new_parents: &'c mut HashMap<usize, usize>,
-        views_to_notify_if_ancestors_change: &'c mut HashMap<usize, SmallVec<[usize; 2]>>,
-        refreshing: bool,
-    ) -> Self {
-        Self {
-            view_context,
-            new_parents,
-            views_to_notify_if_ancestors_change,
-            text_style_stack: Vec::new(),
-            refreshing,
-        }
-    }
-
-    pub fn view_context(&mut self) -> &mut ViewContext<'a, 'b, V> {
-        self.view_context
-    }
 
     /// Return keystrokes that would dispatch the given action on the given view.
     pub(crate) fn keystrokes_for_action(
@@ -3523,80 +3398,95 @@ impl<'a, 'b, 'c, V> LayoutContext<'a, 'b, 'c, V> {
 
     fn notify_if_view_ancestors_change(&mut self, view_id: usize) {
         let self_view_id = self.view_id;
-        self.views_to_notify_if_ancestors_change
+        self.window
+            .views_to_notify_if_ancestors_change
             .entry(view_id)
             .or_default()
             .push(self_view_id);
     }
 
-    pub fn with_text_style<F, T>(&mut self, style: TextStyle, f: F) -> T
+    pub fn paint_layer<F, R>(&mut self, clip_bounds: Option<RectF>, f: F) -> R
     where
-        F: FnOnce(&mut Self) -> T,
+        F: FnOnce(&mut Self) -> R,
     {
-        self.push_text_style(style);
+        self.scene().push_layer(clip_bounds);
         let result = f(self);
-        self.pop_text_style();
+        self.scene().pop_layer();
         result
     }
 }
 
-impl<'a, 'b, 'c, V> RenderContext<'a, 'b, V> for LayoutContext<'a, 'b, 'c, V> {
-    fn text_style(&self) -> TextStyle {
-        self.text_style_stack
-            .last()
-            .cloned()
-            .unwrap_or(TextStyle::default(&self.font_cache))
+impl<V: View> ViewContext<'_, '_, V> {
+    pub fn emit(&mut self, event: V::Event) {
+        self.window_context
+            .pending_effects
+            .push_back(Effect::Event {
+                entity_id: self.view_id,
+                payload: Box::new(event),
+            });
     }
+}
 
-    fn push_text_style(&mut self, style: TextStyle) {
-        self.text_style_stack.push(style);
-    }
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct TypeTag {
+    tag: TypeId,
+    composed: Option<TypeId>,
+    #[cfg(debug_assertions)]
+    tag_type_name: &'static str,
+}
 
-    fn pop_text_style(&mut self) {
-        self.text_style_stack.pop();
+impl TypeTag {
+    pub fn new<Tag: 'static>() -> Self {
+        Self {
+            tag: TypeId::of::<Tag>(),
+            composed: None,
+            #[cfg(debug_assertions)]
+            tag_type_name: std::any::type_name::<Tag>(),
+        }
     }
 
-    fn as_view_context(&mut self) -> &mut ViewContext<'a, 'b, V> {
-        &mut self.view_context
+    pub fn dynamic(tag: TypeId, #[cfg(debug_assertions)] type_name: &'static str) -> Self {
+        Self {
+            tag,
+            composed: None,
+            #[cfg(debug_assertions)]
+            tag_type_name: type_name,
+        }
     }
-}
-
-impl<'a, 'b, 'c, V> Deref for LayoutContext<'a, 'b, 'c, V> {
-    type Target = ViewContext<'a, 'b, V>;
 
-    fn deref(&self) -> &Self::Target {
-        &self.view_context
+    pub fn compose(mut self, other: TypeTag) -> Self {
+        self.composed = Some(other.tag);
+        self
     }
-}
 
-impl<V> DerefMut for LayoutContext<'_, '_, '_, V> {
-    fn deref_mut(&mut self) -> &mut Self::Target {
-        &mut self.view_context
+    #[cfg(debug_assertions)]
+    pub(crate) fn type_name(&self) -> &'static str {
+        self.tag_type_name
     }
 }
 
-impl<V> BorrowAppContext for LayoutContext<'_, '_, '_, V> {
+impl<V> BorrowAppContext for ViewContext<'_, '_, V> {
     fn read_with<T, F: FnOnce(&AppContext) -> T>(&self, f: F) -> T {
-        BorrowAppContext::read_with(&*self.view_context, f)
+        BorrowAppContext::read_with(&*self.window_context, f)
     }
 
     fn update<T, F: FnOnce(&mut AppContext) -> T>(&mut self, f: F) -> T {
-        BorrowAppContext::update(&mut *self.view_context, f)
+        BorrowAppContext::update(&mut *self.window_context, f)
     }
 }
 
-impl<V> BorrowWindowContext for LayoutContext<'_, '_, '_, V> {
+impl<V> BorrowWindowContext for ViewContext<'_, '_, V> {
     type Result<T> = T;
 
     fn read_window<T, F: FnOnce(&WindowContext) -> T>(&self, window: AnyWindowHandle, f: F) -> T {
-        BorrowWindowContext::read_window(&*self.view_context, window, f)
+        BorrowWindowContext::read_window(&*self.window_context, window, f)
     }
 
     fn read_window_optional<T, F>(&self, window: AnyWindowHandle, f: F) -> Option<T>
     where
         F: FnOnce(&WindowContext) -> Option<T>,
     {
-        BorrowWindowContext::read_window_optional(&*self.view_context, window, f)
+        BorrowWindowContext::read_window_optional(&*self.window_context, window, f)
     }
 
     fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
@@ -3604,105 +3494,14 @@ impl<V> BorrowWindowContext for LayoutContext<'_, '_, '_, V> {
         window: AnyWindowHandle,
         f: F,
     ) -> T {
-        BorrowWindowContext::update_window(&mut *self.view_context, window, f)
-    }
-
-    fn update_window_optional<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Option<T>
-    where
-        F: FnOnce(&mut WindowContext) -> Option<T>,
-    {
-        BorrowWindowContext::update_window_optional(&mut *self.view_context, window, f)
-    }
-}
-
-pub struct PaintContext<'a, 'b, 'c, V> {
-    pub view_context: &'c mut ViewContext<'a, 'b, V>,
-    text_style_stack: Vec<TextStyle>,
-}
-
-impl<'a, 'b, 'c, V> PaintContext<'a, 'b, 'c, V> {
-    pub fn new(view_context: &'c mut ViewContext<'a, 'b, V>) -> Self {
-        Self {
-            view_context,
-            text_style_stack: Vec::new(),
-        }
-    }
-}
-
-impl<'a, 'b, 'c, V> RenderContext<'a, 'b, V> for PaintContext<'a, 'b, 'c, V> {
-    fn text_style(&self) -> TextStyle {
-        self.text_style_stack
-            .last()
-            .cloned()
-            .unwrap_or(TextStyle::default(&self.font_cache))
-    }
-
-    fn push_text_style(&mut self, style: TextStyle) {
-        self.text_style_stack.push(style);
-    }
-
-    fn pop_text_style(&mut self) {
-        self.text_style_stack.pop();
-    }
-
-    fn as_view_context(&mut self) -> &mut ViewContext<'a, 'b, V> {
-        &mut self.view_context
-    }
-}
-
-impl<'a, 'b, 'c, V> Deref for PaintContext<'a, 'b, 'c, V> {
-    type Target = ViewContext<'a, 'b, V>;
-
-    fn deref(&self) -> &Self::Target {
-        &self.view_context
-    }
-}
-
-impl<V> DerefMut for PaintContext<'_, '_, '_, V> {
-    fn deref_mut(&mut self) -> &mut Self::Target {
-        &mut self.view_context
-    }
-}
-
-impl<V> BorrowAppContext for PaintContext<'_, '_, '_, V> {
-    fn read_with<T, F: FnOnce(&AppContext) -> T>(&self, f: F) -> T {
-        BorrowAppContext::read_with(&*self.view_context, f)
-    }
-
-    fn update<T, F: FnOnce(&mut AppContext) -> T>(&mut self, f: F) -> T {
-        BorrowAppContext::update(&mut *self.view_context, f)
-    }
-}
-
-impl<V> BorrowWindowContext for PaintContext<'_, '_, '_, V> {
-    type Result<T> = T;
-
-    fn read_window<T, F>(&self, window: AnyWindowHandle, f: F) -> Self::Result<T>
-    where
-        F: FnOnce(&WindowContext) -> T,
-    {
-        BorrowWindowContext::read_window(self.view_context, window, f)
-    }
-
-    fn read_window_optional<T, F>(&self, window: AnyWindowHandle, f: F) -> Option<T>
-    where
-        F: FnOnce(&WindowContext) -> Option<T>,
-    {
-        BorrowWindowContext::read_window_optional(self.view_context, window, f)
-    }
-
-    fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Self::Result<T>
-    where
-        F: FnOnce(&mut WindowContext) -> T,
-    {
-        BorrowWindowContext::update_window(self.view_context, window, f)
+        BorrowWindowContext::update_window(&mut *self.window_context, window, f)
     }
 
     fn update_window_optional<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Option<T>
     where
         F: FnOnce(&mut WindowContext) -> Option<T>,
     {
-        BorrowWindowContext::update_window_optional(self.view_context, window, f)
+        BorrowWindowContext::update_window_optional(&mut *self.window_context, window, f)
     }
 }
 
@@ -6555,32 +6354,21 @@ mod tests {
         view_1.update(cx, |_, cx| {
             view_2.update(cx, |_, cx| {
                 // Sanity check
-                let mut new_parents = Default::default();
-                let mut notify_views_if_parents_change = Default::default();
-                let mut layout_cx = LayoutContext::new(
-                    cx,
-                    &mut new_parents,
-                    &mut notify_views_if_parents_change,
-                    false,
-                );
                 assert_eq!(
-                    layout_cx
-                        .keystrokes_for_action(view_1_id, &Action1)
+                    cx.keystrokes_for_action(view_1_id, &Action1)
                         .unwrap()
                         .as_slice(),
                     &[Keystroke::parse("a").unwrap()]
                 );
                 assert_eq!(
-                    layout_cx
-                        .keystrokes_for_action(view_2.id(), &Action2)
+                    cx.keystrokes_for_action(view_2.id(), &Action2)
                         .unwrap()
                         .as_slice(),
                     &[Keystroke::parse("b").unwrap()]
                 );
-                assert_eq!(layout_cx.keystrokes_for_action(view_1.id(), &Action3), None);
+                assert_eq!(cx.keystrokes_for_action(view_1.id(), &Action3), None);
                 assert_eq!(
-                    layout_cx
-                        .keystrokes_for_action(view_2.id(), &Action3)
+                    cx.keystrokes_for_action(view_2.id(), &Action3)
                         .unwrap()
                         .as_slice(),
                     &[Keystroke::parse("c").unwrap()]
@@ -6589,21 +6377,17 @@ mod tests {
                 // The 'a' keystroke propagates up the view tree from view_2
                 // to view_1. The action, Action1, is handled by view_1.
                 assert_eq!(
-                    layout_cx
-                        .keystrokes_for_action(view_2.id(), &Action1)
+                    cx.keystrokes_for_action(view_2.id(), &Action1)
                         .unwrap()
                         .as_slice(),
                     &[Keystroke::parse("a").unwrap()]
                 );
 
                 // Actions that are handled below the current view don't have bindings
-                assert_eq!(layout_cx.keystrokes_for_action(view_1_id, &Action2), None);
+                assert_eq!(cx.keystrokes_for_action(view_1_id, &Action2), None);
 
                 // Actions that are handled in other branches of the tree should not have a binding
-                assert_eq!(
-                    layout_cx.keystrokes_for_action(view_2.id(), &GlobalAction),
-                    None
-                );
+                assert_eq!(cx.keystrokes_for_action(view_2.id(), &GlobalAction), None);
             });
         });
 

crates/gpui/src/app/window.rs πŸ”—

@@ -1,5 +1,6 @@
 use crate::{
     elements::AnyRootElement,
+    fonts::{TextStyle, TextStyleRefinement},
     geometry::{rect::RectF, Size},
     json::ToJson,
     keymap_matcher::{Binding, KeymapContext, Keystroke, MatchResult},
@@ -15,9 +16,8 @@ use crate::{
     text_layout::TextLayoutCache,
     util::post_inc,
     Action, AnyView, AnyViewHandle, AnyWindowHandle, AppContext, BorrowAppContext,
-    BorrowWindowContext, Effect, Element, Entity, Handle, LayoutContext, MouseRegion,
-    MouseRegionId, PaintContext, SceneBuilder, Subscription, View, ViewContext, ViewHandle,
-    WindowInvalidation,
+    BorrowWindowContext, Effect, Element, Entity, Handle, MouseRegion, MouseRegionId, SceneBuilder,
+    Subscription, View, ViewContext, ViewHandle, WindowInvalidation,
 };
 use anyhow::{anyhow, bail, Result};
 use collections::{HashMap, HashSet};
@@ -30,7 +30,7 @@ use sqlez::{
     statement::Statement,
 };
 use std::{
-    any::TypeId,
+    any::{type_name, Any, TypeId},
     mem,
     ops::{Deref, DerefMut, Range, Sub},
 };
@@ -50,20 +50,28 @@ pub struct Window {
     pub(crate) parents: HashMap<usize, usize>,
     pub(crate) is_active: bool,
     pub(crate) is_fullscreen: bool,
+    inspector_enabled: bool,
     pub(crate) invalidation: Option<WindowInvalidation>,
     pub(crate) platform_window: Box<dyn platform::Window>,
     pub(crate) rendered_views: HashMap<usize, Box<dyn AnyRootElement>>,
+    scene: SceneBuilder,
+    pub(crate) text_style_stack: Vec<TextStyle>,
+    pub(crate) theme_stack: Vec<Box<dyn Any>>,
+    pub(crate) new_parents: HashMap<usize, usize>,
+    pub(crate) views_to_notify_if_ancestors_change: HashMap<usize, SmallVec<[usize; 2]>>,
     titlebar_height: f32,
     appearance: Appearance,
     cursor_regions: Vec<CursorRegion>,
     mouse_regions: Vec<(MouseRegion, usize)>,
     event_handlers: Vec<EventHandler>,
     last_mouse_moved_event: Option<Event>,
+    last_mouse_position: Vector2F,
+    pressed_buttons: HashSet<MouseButton>,
     pub(crate) hovered_region_ids: Vec<MouseRegionId>,
     pub(crate) clicked_region_ids: Vec<MouseRegionId>,
     pub(crate) clicked_region: Option<(MouseRegionId, MouseButton)>,
-    mouse_position: Vector2F,
     text_layout_cache: TextLayoutCache,
+    refreshing: bool,
 }
 
 impl Window {
@@ -87,19 +95,27 @@ impl Window {
             is_active: false,
             invalidation: None,
             is_fullscreen: false,
+            inspector_enabled: false,
             platform_window,
             rendered_views: Default::default(),
+            scene: SceneBuilder::new(),
+            text_style_stack: Vec::new(),
+            theme_stack: Vec::new(),
+            new_parents: HashMap::default(),
+            views_to_notify_if_ancestors_change: HashMap::default(),
             cursor_regions: Default::default(),
             mouse_regions: Default::default(),
             event_handlers: Default::default(),
             text_layout_cache: TextLayoutCache::new(cx.font_system.clone()),
             last_mouse_moved_event: None,
+            last_mouse_position: Vector2F::zero(),
+            pressed_buttons: Default::default(),
             hovered_region_ids: Default::default(),
             clicked_region_ids: Default::default(),
             clicked_region: None,
-            mouse_position: vec2f(0., 0.),
             titlebar_height,
             appearance,
+            refreshing: false,
         };
 
         let mut window_context = WindowContext::mutable(cx, &mut window, handle);
@@ -226,6 +242,26 @@ impl<'a> WindowContext<'a> {
             .push_back(Effect::RepaintWindow { window });
     }
 
+    pub fn scene(&mut self) -> &mut SceneBuilder {
+        &mut self.window.scene
+    }
+
+    pub fn enable_inspector(&mut self) {
+        self.window.inspector_enabled = true;
+    }
+
+    pub fn is_inspector_enabled(&self) -> bool {
+        self.window.inspector_enabled
+    }
+
+    pub fn is_mouse_down(&self, button: MouseButton) -> bool {
+        self.window.pressed_buttons.contains(&button)
+    }
+
+    pub fn rem_size(&self) -> f32 {
+        16.
+    }
+
     pub fn layout_engine(&mut self) -> Option<&mut LayoutEngine> {
         self.window.layout_engines.last_mut()
     }
@@ -259,7 +295,11 @@ impl<'a> WindowContext<'a> {
     }
 
     pub fn mouse_position(&self) -> Vector2F {
-        self.window.mouse_position
+        self.window.platform_window.mouse_position()
+    }
+
+    pub fn refreshing(&self) -> bool {
+        self.window.refreshing
     }
 
     pub fn text_layout_cache(&self) -> &TextLayoutCache {
@@ -507,7 +547,9 @@ impl<'a> WindowContext<'a> {
     }
 
     pub(crate) fn dispatch_event(&mut self, event: Event, event_reused: bool) -> bool {
-        self.dispatch_to_new_event_handlers(&event);
+        if !event_reused {
+            self.dispatch_event_2(&event);
+        }
 
         let mut mouse_events = SmallVec::<[_; 2]>::new();
         let mut notified_views: HashSet<usize> = Default::default();
@@ -576,7 +618,7 @@ impl<'a> WindowContext<'a> {
                 // Synthesize one last drag event to end the drag
                 mouse_events.push(MouseEvent::Drag(MouseDrag {
                     region: Default::default(),
-                    prev_mouse_position: self.window.mouse_position,
+                    prev_mouse_position: self.window.last_mouse_position,
                     platform_event: MouseMovedEvent {
                         position: e.position,
                         pressed_button: Some(e.button),
@@ -630,14 +672,14 @@ impl<'a> WindowContext<'a> {
                     if pressed_button.is_some() {
                         mouse_events.push(MouseEvent::Drag(MouseDrag {
                             region: Default::default(),
-                            prev_mouse_position: self.window.mouse_position,
+                            prev_mouse_position: self.window.last_mouse_position,
                             platform_event: e.clone(),
                             end: false,
                         }));
                     } else if let Some((_, clicked_button)) = self.window.clicked_region {
                         mouse_events.push(MouseEvent::Drag(MouseDrag {
                             region: Default::default(),
-                            prev_mouse_position: self.window.mouse_position,
+                            prev_mouse_position: self.window.last_mouse_position,
                             platform_event: e.clone(),
                             end: true,
                         }));
@@ -697,7 +739,7 @@ impl<'a> WindowContext<'a> {
         }
 
         if let Some(position) = event.position() {
-            self.window.mouse_position = position;
+            self.window.last_mouse_position = position;
         }
 
         // 2. Dispatch mouse events on regions
@@ -711,7 +753,7 @@ impl<'a> WindowContext<'a> {
             match &mouse_event {
                 MouseEvent::Hover(_) => {
                     let mut highest_z_index = None;
-                    let mouse_position = self.window.mouse_position.clone();
+                    let mouse_position = self.mouse_position();
                     let window = &mut *self.window;
                     let prev_hovered_regions = mem::take(&mut window.hovered_region_ids);
                     for (region, z_index) in window.mouse_regions.iter().rev() {
@@ -756,7 +798,7 @@ impl<'a> WindowContext<'a> {
 
                 MouseEvent::Down(_) | MouseEvent::Up(_) => {
                     for (region, _) in self.window.mouse_regions.iter().rev() {
-                        if region.bounds.contains_point(self.window.mouse_position) {
+                        if region.bounds.contains_point(self.mouse_position()) {
                             valid_regions.push(region.clone());
                             if region.notify_on_click {
                                 notified_views.insert(region.id().view_id());
@@ -783,10 +825,7 @@ impl<'a> WindowContext<'a> {
                         // Find regions which still overlap with the mouse since the last MouseDown happened
                         for (mouse_region, _) in self.window.mouse_regions.iter().rev() {
                             if clicked_region_ids.contains(&mouse_region.id()) {
-                                if mouse_region
-                                    .bounds
-                                    .contains_point(self.window.mouse_position)
-                                {
+                                if mouse_region.bounds.contains_point(self.mouse_position()) {
                                     valid_regions.push(mouse_region.clone());
                                 } else {
                                     // Let the view know that it hasn't been clicked anymore
@@ -813,10 +852,7 @@ impl<'a> WindowContext<'a> {
                 | MouseEvent::ClickOut(_) => {
                     for (mouse_region, _) in self.window.mouse_regions.iter().rev() {
                         // NOT contains
-                        if !mouse_region
-                            .bounds
-                            .contains_point(self.window.mouse_position)
-                        {
+                        if !mouse_region.bounds.contains_point(self.mouse_position()) {
                             valid_regions.push(mouse_region.clone());
                         }
                     }
@@ -825,10 +861,7 @@ impl<'a> WindowContext<'a> {
                 _ => {
                     for (mouse_region, _) in self.window.mouse_regions.iter().rev() {
                         // Contains
-                        if mouse_region
-                            .bounds
-                            .contains_point(self.window.mouse_position)
-                        {
+                        if mouse_region.bounds.contains_point(self.mouse_position()) {
                             valid_regions.push(mouse_region.clone());
                         }
                     }
@@ -892,12 +925,24 @@ impl<'a> WindowContext<'a> {
         any_event_handled
     }
 
-    fn dispatch_to_new_event_handlers(&mut self, event: &Event) {
+    fn dispatch_event_2(&mut self, event: &Event) {
+        match event {
+            Event::MouseDown(event) => {
+                self.window.pressed_buttons.insert(event.button);
+            }
+            Event::MouseUp(event) => {
+                self.window.pressed_buttons.remove(&event.button);
+            }
+            _ => {}
+        }
+
         if let Some(mouse_event) = event.mouse_event() {
             let event_handlers = self.window.take_event_handlers();
             for event_handler in event_handlers.iter().rev() {
                 if event_handler.event_type == mouse_event.type_id() {
-                    (event_handler.handler)(mouse_event, self);
+                    if !(event_handler.handler)(mouse_event, self) {
+                        break;
+                    }
                 }
             }
             self.window.event_handlers = event_handlers;
@@ -1000,21 +1045,17 @@ impl<'a> WindowContext<'a> {
 
         let mut rendered_root = self.window.rendered_views.remove(&root_view_id).unwrap();
 
-        let mut new_parents = HashMap::default();
-        let mut views_to_notify_if_ancestors_change = HashMap::default();
-        rendered_root.layout(
-            SizeConstraint::new(window_size, window_size),
-            &mut new_parents,
-            &mut views_to_notify_if_ancestors_change,
-            refreshing,
-            self,
-        )?;
+        self.window.refreshing = refreshing;
+        rendered_root.layout(SizeConstraint::strict(window_size), self)?;
+        self.window.refreshing = false;
 
+        let views_to_notify_if_ancestors_change =
+            mem::take(&mut self.window.views_to_notify_if_ancestors_change);
         for (view_id, view_ids_to_notify) in views_to_notify_if_ancestors_change {
             let mut current_view_id = view_id;
             loop {
                 let old_parent_id = self.window.parents.get(&current_view_id);
-                let new_parent_id = new_parents.get(&current_view_id);
+                let new_parent_id = self.window.new_parents.get(&current_view_id);
                 if old_parent_id.is_none() && new_parent_id.is_none() {
                     break;
                 } else if old_parent_id == new_parent_id {
@@ -1029,6 +1070,7 @@ impl<'a> WindowContext<'a> {
             }
         }
 
+        let new_parents = mem::take(&mut self.window.new_parents);
         let old_parents = mem::replace(&mut self.window.parents, new_parents);
         self.window
             .rendered_views
@@ -1043,9 +1085,7 @@ impl<'a> WindowContext<'a> {
         let root_view_id = self.window.root_view().id();
         let mut rendered_root = self.window.rendered_views.remove(&root_view_id).unwrap();
 
-        let mut scene_builder = SceneBuilder::new(scale_factor);
         rendered_root.paint(
-            &mut scene_builder,
             Vector2F::zero(),
             RectF::from_points(Vector2F::zero(), window_size),
             self,
@@ -1055,7 +1095,7 @@ impl<'a> WindowContext<'a> {
             .insert(root_view_id, rendered_root);
 
         self.window.text_layout_cache.finish_frame();
-        let mut scene = scene_builder.build();
+        let mut scene = self.window.scene.build(scale_factor);
         self.window.cursor_regions = scene.cursor_regions();
         self.window.mouse_regions = scene.mouse_regions();
         self.window.event_handlers = scene.take_event_handlers();
@@ -1110,7 +1150,7 @@ impl<'a> WindowContext<'a> {
         self.window.is_fullscreen
     }
 
-    pub(crate) fn dispatch_action(&mut self, view_id: Option<usize>, action: &dyn Action) -> bool {
+    pub fn dispatch_action(&mut self, view_id: Option<usize>, action: &dyn Action) -> bool {
         if let Some(view_id) = view_id {
             self.halt_action_dispatch = false;
             self.visit_dispatch_path(view_id, |view_id, capture_phase, cx| {
@@ -1203,6 +1243,10 @@ impl<'a> WindowContext<'a> {
         self.window.platform_window.bounds()
     }
 
+    pub fn titlebar_height(&self) -> f32 {
+        self.window.titlebar_height
+    }
+
     pub fn window_appearance(&self) -> Appearance {
         self.window.appearance
     }
@@ -1274,6 +1318,43 @@ impl<'a> WindowContext<'a> {
         };
         handle
     }
+
+    pub fn text_style(&self) -> TextStyle {
+        self.window
+            .text_style_stack
+            .last()
+            .cloned()
+            .unwrap_or(TextStyle::default(&self.font_cache))
+    }
+
+    pub fn push_text_style(&mut self, refinement: &TextStyleRefinement) -> Result<()> {
+        let mut style = self.text_style();
+        style.refine(refinement, self.font_cache())?;
+        self.window.text_style_stack.push(style);
+        Ok(())
+    }
+
+    pub fn pop_text_style(&mut self) {
+        self.window.text_style_stack.pop();
+    }
+
+    pub fn theme<T: 'static>(&self) -> &T {
+        self.window
+            .theme_stack
+            .iter()
+            .rev()
+            .find_map(|theme| theme.downcast_ref())
+            .ok_or_else(|| anyhow!("no theme provided of type {}", type_name::<T>()))
+            .unwrap()
+    }
+
+    pub fn push_theme<T: 'static>(&mut self, theme: T) {
+        self.window.theme_stack.push(Box::new(theme));
+    }
+
+    pub fn pop_theme(&mut self) {
+        self.window.theme_stack.pop();
+    }
 }
 
 #[derive(Default)]
@@ -1289,9 +1370,12 @@ impl LayoutEngine {
     where
         C: IntoIterator<Item = LayoutId>,
     {
-        Ok(self
-            .0
-            .new_with_children(style, &children.into_iter().collect::<Vec<_>>())?)
+        let children = children.into_iter().collect::<Vec<_>>();
+        if children.is_empty() {
+            Ok(self.0.new_leaf(style)?)
+        } else {
+            Ok(self.0.new_with_children(style, &children)?)
+        }
     }
 
     pub fn add_measured_node<F>(&mut self, style: LayoutStyle, measure: F) -> Result<LayoutId>
@@ -1314,8 +1398,8 @@ impl LayoutEngine {
         Ok(())
     }
 
-    pub fn computed_layout(&mut self, node: LayoutId) -> Result<EngineLayout> {
-        Ok(self.0.layout(node)?.into())
+    pub fn computed_layout(&mut self, node: LayoutId) -> Result<Layout> {
+        Ok(Layout::from(self.0.layout(node)?))
     }
 }
 
@@ -1339,7 +1423,7 @@ where
 }
 
 #[derive(Debug, Clone, Default)]
-pub struct EngineLayout {
+pub struct Layout {
     pub bounds: RectF,
     pub order: u32,
 }
@@ -1349,7 +1433,7 @@ pub struct MeasureParams {
     pub available_space: Size<AvailableSpace>,
 }
 
-#[derive(Clone)]
+#[derive(Clone, Debug)]
 pub enum AvailableSpace {
     /// The amount of space available is the specified number of pixels
     Pixels(f32),
@@ -1375,7 +1459,7 @@ impl From<taffy::prelude::AvailableSpace> for AvailableSpace {
     }
 }
 
-impl From<&taffy::tree::Layout> for EngineLayout {
+impl From<&taffy::tree::Layout> for Layout {
     fn from(value: &taffy::tree::Layout) -> Self {
         Self {
             bounds: RectF::new(
@@ -1592,18 +1676,13 @@ impl<V: 'static> Element<V> for ChildView {
         &mut self,
         constraint: SizeConstraint,
         _: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         if let Some(mut rendered_view) = cx.window.rendered_views.remove(&self.view_id) {
-            cx.new_parents.insert(self.view_id, cx.view_id());
+            let parent_id = cx.view_id();
+            cx.window.new_parents.insert(self.view_id, parent_id);
             let size = rendered_view
-                .layout(
-                    constraint,
-                    cx.new_parents,
-                    cx.views_to_notify_if_ancestors_change,
-                    cx.refreshing,
-                    cx.view_context,
-                )
+                .layout(constraint, cx)
                 .log_err()
                 .unwrap_or(Vector2F::zero());
             cx.window.rendered_views.insert(self.view_id, rendered_view);
@@ -1620,16 +1699,15 @@ impl<V: 'static> Element<V> for ChildView {
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         _: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) {
         if let Some(mut rendered_view) = cx.window.rendered_views.remove(&self.view_id) {
             rendered_view
-                .paint(scene, bounds.origin(), visible_bounds, cx)
+                .paint(bounds.origin(), visible_bounds, cx)
                 .log_err();
             cx.window.rendered_views.insert(self.view_id, rendered_view);
         } else {

crates/gpui/src/elements.rs πŸ”—

@@ -34,14 +34,12 @@ use crate::{
         rect::RectF,
         vector::{vec2f, Vector2F},
     },
-    json, Action, Entity, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, TypeTag, View,
-    ViewContext, WeakViewHandle, WindowContext,
+    json, Action, Entity, SizeConstraint, TypeTag, View, ViewContext, WeakViewHandle,
+    WindowContext,
 };
 use anyhow::{anyhow, Result};
-use collections::HashMap;
 use core::panic;
 use json::ToJson;
-use smallvec::SmallVec;
 use std::{
     any::{type_name, Any},
     borrow::Cow,
@@ -61,17 +59,16 @@ pub trait Element<V: 'static>: 'static {
         &mut self,
         constraint: SizeConstraint,
         view: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> (Vector2F, Self::LayoutState);
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         layout: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> Self::PaintState;
 
     fn rect_for_text_range(
@@ -262,16 +259,15 @@ trait AnyElementState<V> {
         &mut self,
         constraint: SizeConstraint,
         view: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> Vector2F;
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         origin: Vector2F,
         visible_bounds: RectF,
         view: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     );
 
     fn rect_for_text_range(
@@ -314,7 +310,7 @@ impl<V, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
         &mut self,
         constraint: SizeConstraint,
         view: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> Vector2F {
         let result;
         *self = match mem::take(self) {
@@ -348,11 +344,10 @@ impl<V, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         origin: Vector2F,
         visible_bounds: RectF,
         view: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) {
         *self = match mem::take(self) {
             ElementState::PostLayout {
@@ -362,14 +357,7 @@ impl<V, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
                 mut layout,
             } => {
                 let bounds = RectF::new(origin, size);
-                let paint = element.paint(
-                    scene,
-                    bounds,
-                    visible_bounds,
-                    &mut layout,
-                    view,
-                    &mut PaintContext::new(cx),
-                );
+                let paint = element.paint(bounds, visible_bounds, &mut layout, view, cx);
                 ElementState::PostPaint {
                     element,
                     constraint,
@@ -387,14 +375,7 @@ impl<V, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
                 ..
             } => {
                 let bounds = RectF::new(origin, bounds.size());
-                let paint = element.paint(
-                    scene,
-                    bounds,
-                    visible_bounds,
-                    &mut layout,
-                    view,
-                    &mut PaintContext::new(cx),
-                );
+                let paint = element.paint(bounds, visible_bounds, &mut layout, view, cx);
                 ElementState::PostPaint {
                     element,
                     constraint,
@@ -517,20 +498,19 @@ impl<V> AnyElement<V> {
         &mut self,
         constraint: SizeConstraint,
         view: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> Vector2F {
         self.state.layout(constraint, view, cx)
     }
 
     pub fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         origin: Vector2F,
         visible_bounds: RectF,
         view: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) {
-        self.state.paint(scene, origin, visible_bounds, view, cx);
+        self.state.paint(origin, visible_bounds, view, cx);
     }
 
     pub fn rect_for_text_range(
@@ -578,7 +558,7 @@ impl<V: 'static> Element<V> for AnyElement<V> {
         &mut self,
         constraint: SizeConstraint,
         view: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         let size = self.layout(constraint, view, cx);
         (size, ())
@@ -586,14 +566,13 @@ impl<V: 'static> Element<V> for AnyElement<V> {
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> Self::PaintState {
-        self.paint(scene, bounds.origin(), visible_bounds, view, cx);
+        self.paint(bounds.origin(), visible_bounds, view, cx);
     }
 
     fn rect_for_text_range(
@@ -646,17 +625,9 @@ impl<V> RootElement<V> {
 }
 
 pub trait AnyRootElement {
-    fn layout(
-        &mut self,
-        constraint: SizeConstraint,
-        new_parents: &mut HashMap<usize, usize>,
-        views_to_notify_if_ancestors_change: &mut HashMap<usize, SmallVec<[usize; 2]>>,
-        refreshing: bool,
-        cx: &mut WindowContext,
-    ) -> Result<Vector2F>;
+    fn layout(&mut self, constraint: SizeConstraint, cx: &mut WindowContext) -> Result<Vector2F>;
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         origin: Vector2F,
         visible_bounds: RectF,
         cx: &mut WindowContext,
@@ -671,32 +642,16 @@ pub trait AnyRootElement {
 }
 
 impl<V: View> AnyRootElement for RootElement<V> {
-    fn layout(
-        &mut self,
-        constraint: SizeConstraint,
-        new_parents: &mut HashMap<usize, usize>,
-        views_to_notify_if_ancestors_change: &mut HashMap<usize, SmallVec<[usize; 2]>>,
-        refreshing: bool,
-        cx: &mut WindowContext,
-    ) -> Result<Vector2F> {
+    fn layout(&mut self, constraint: SizeConstraint, cx: &mut WindowContext) -> Result<Vector2F> {
         let view = self
             .view
             .upgrade(cx)
             .ok_or_else(|| anyhow!("layout called on a root element for a dropped view"))?;
-        view.update(cx, |view, cx| {
-            let mut cx = LayoutContext::new(
-                cx,
-                new_parents,
-                views_to_notify_if_ancestors_change,
-                refreshing,
-            );
-            Ok(self.element.layout(constraint, view, &mut cx))
-        })
+        view.update(cx, |view, cx| Ok(self.element.layout(constraint, view, cx)))
     }
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         origin: Vector2F,
         visible_bounds: RectF,
         cx: &mut WindowContext,
@@ -707,9 +662,7 @@ impl<V: View> AnyRootElement for RootElement<V> {
             .ok_or_else(|| anyhow!("paint called on a root element for a dropped view"))?;
 
         view.update(cx, |view, cx| {
-            let mut cx = PaintContext::new(cx);
-            self.element
-                .paint(scene, origin, visible_bounds, view, &mut cx);
+            self.element.paint(origin, visible_bounds, view, cx);
             Ok(())
         })
     }

crates/gpui/src/elements/align.rs πŸ”—

@@ -1,7 +1,6 @@
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
-    json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
-    ViewContext,
+    json, AnyElement, Element, SizeConstraint, ViewContext,
 };
 use json::ToJson;
 
@@ -49,7 +48,7 @@ impl<V: 'static> Element<V> for Align<V> {
         &mut self,
         mut constraint: SizeConstraint,
         view: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         let mut size = constraint.max;
         constraint.min = Vector2F::zero();
@@ -65,12 +64,11 @@ impl<V: 'static> Element<V> for Align<V> {
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> Self::PaintState {
         let my_center = bounds.size() / 2.;
         let my_target = my_center + my_center * self.alignment;
@@ -79,7 +77,6 @@ impl<V: 'static> Element<V> for Align<V> {
         let child_target = child_center + child_center * self.alignment;
 
         self.child.paint(
-            scene,
             bounds.origin() - (child_target - my_target),
             visible_bounds,
             view,

crates/gpui/src/elements/canvas.rs πŸ”—

@@ -3,7 +3,7 @@ use std::marker::PhantomData;
 use super::Element;
 use crate::{
     json::{self, json},
-    PaintContext, SceneBuilder, ViewContext,
+    ViewContext,
 };
 use json::ToJson;
 use pathfinder_geometry::{
@@ -15,7 +15,7 @@ pub struct Canvas<V, F>(F, PhantomData<V>);
 
 impl<V, F> Canvas<V, F>
 where
-    F: FnMut(&mut SceneBuilder, RectF, RectF, &mut V, &mut ViewContext<V>),
+    F: FnMut(RectF, RectF, &mut V, &mut ViewContext<V>),
 {
     pub fn new(f: F) -> Self {
         Self(f, PhantomData)
@@ -24,7 +24,7 @@ where
 
 impl<V: 'static, F> Element<V> for Canvas<V, F>
 where
-    F: 'static + FnMut(&mut SceneBuilder, RectF, RectF, &mut V, &mut ViewContext<V>),
+    F: 'static + FnMut(RectF, RectF, &mut V, &mut ViewContext<V>),
 {
     type LayoutState = ();
     type PaintState = ();
@@ -33,7 +33,7 @@ where
         &mut self,
         constraint: crate::SizeConstraint,
         _: &mut V,
-        _: &mut crate::LayoutContext<V>,
+        _: &mut crate::ViewContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         let x = if constraint.max.x().is_finite() {
             constraint.max.x()
@@ -50,14 +50,13 @@ where
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> Self::PaintState {
-        self.0(scene, bounds, visible_bounds, view, cx)
+        self.0(bounds, visible_bounds, view, cx)
     }
 
     fn rect_for_text_range(

crates/gpui/src/elements/clipped.rs πŸ”—

@@ -3,10 +3,7 @@ use std::ops::Range;
 use pathfinder_geometry::{rect::RectF, vector::Vector2F};
 use serde_json::json;
 
-use crate::{
-    json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
-    ViewContext,
-};
+use crate::{json, AnyElement, Element, SizeConstraint, ViewContext};
 
 pub struct Clipped<V> {
     child: AnyElement<V>,
@@ -26,24 +23,23 @@ impl<V: 'static> Element<V> for Clipped<V> {
         &mut self,
         constraint: SizeConstraint,
         view: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         (self.child.layout(constraint, view, cx), ())
     }
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> Self::PaintState {
-        scene.paint_layer(Some(bounds), |scene| {
-            self.child
-                .paint(scene, bounds.origin(), visible_bounds, view, cx)
-        })
+        cx.scene().push_layer(Some(bounds));
+        let state = self.child.paint(bounds.origin(), visible_bounds, view, cx);
+        cx.scene().pop_layer();
+        state
     }
 
     fn rect_for_text_range(

crates/gpui/src/elements/component.rs πŸ”—

@@ -2,9 +2,7 @@ use std::{any::Any, marker::PhantomData};
 
 use pathfinder_geometry::{rect::RectF, vector::Vector2F};
 
-use crate::{
-    AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, ViewContext,
-};
+use crate::{AnyElement, Element, SizeConstraint, ViewContext};
 
 use super::Empty;
 
@@ -284,14 +282,14 @@ impl<V: 'static, C: StatefulComponent<V> + 'static> Element<V> for ComponentAdap
         &mut self,
         constraint: SizeConstraint,
         view: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         if self.element.is_none() {
             let element = self
                 .component
                 .take()
                 .expect("Component can only be rendered once")
-                .render(view, cx.view_context());
+                .render(view, cx);
             self.element = Some(element);
         }
         let constraint = self.element.as_mut().unwrap().layout(constraint, view, cx);
@@ -300,17 +298,16 @@ impl<V: 'static, C: StatefulComponent<V> + 'static> Element<V> for ComponentAdap
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> Self::PaintState {
         self.element
             .as_mut()
             .expect("Layout should always be called before paint")
-            .paint(scene, bounds.origin(), visible_bounds, view, cx)
+            .paint(bounds.origin(), visible_bounds, view, cx)
     }
 
     fn rect_for_text_range(

crates/gpui/src/elements/constrained_box.rs πŸ”—

@@ -5,8 +5,7 @@ use serde_json::json;
 
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
-    json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
-    ViewContext,
+    json, AnyElement, Element, SizeConstraint, ViewContext,
 };
 
 pub struct ConstrainedBox<V> {
@@ -16,7 +15,7 @@ pub struct ConstrainedBox<V> {
 
 pub enum Constraint<V> {
     Static(SizeConstraint),
-    Dynamic(Box<dyn FnMut(SizeConstraint, &mut V, &mut LayoutContext<V>) -> SizeConstraint>),
+    Dynamic(Box<dyn FnMut(SizeConstraint, &mut V, &mut ViewContext<V>) -> SizeConstraint>),
 }
 
 impl<V> ToJson for Constraint<V> {
@@ -38,8 +37,7 @@ impl<V: 'static> ConstrainedBox<V> {
 
     pub fn dynamically(
         mut self,
-        constraint: impl 'static
-            + FnMut(SizeConstraint, &mut V, &mut LayoutContext<V>) -> SizeConstraint,
+        constraint: impl 'static + FnMut(SizeConstraint, &mut V, &mut ViewContext<V>) -> SizeConstraint,
     ) -> Self {
         self.constraint = Constraint::Dynamic(Box::new(constraint));
         self
@@ -121,7 +119,7 @@ impl<V: 'static> ConstrainedBox<V> {
         &mut self,
         input_constraint: SizeConstraint,
         view: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> SizeConstraint {
         match &mut self.constraint {
             Constraint::Static(constraint) => *constraint,
@@ -140,7 +138,7 @@ impl<V: 'static> Element<V> for ConstrainedBox<V> {
         &mut self,
         mut parent_constraint: SizeConstraint,
         view: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         let constraint = self.constraint(parent_constraint, view, cx);
         parent_constraint.min = parent_constraint.min.max(constraint.min);
@@ -152,17 +150,15 @@ impl<V: 'static> Element<V> for ConstrainedBox<V> {
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> Self::PaintState {
-        scene.paint_layer(Some(visible_bounds), |scene| {
-            self.child
-                .paint(scene, bounds.origin(), visible_bounds, view, cx);
-        })
+        cx.scene().push_layer(Some(visible_bounds));
+        self.child.paint(bounds.origin(), visible_bounds, view, cx);
+        cx.scene().pop_layer();
     }
 
     fn rect_for_text_range(

crates/gpui/src/elements/container.rs πŸ”—

@@ -9,8 +9,8 @@ use crate::{
     },
     json::ToJson,
     platform::CursorStyle,
-    scene::{self, Border, CornerRadii, CursorRegion, Quad},
-    AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, ViewContext,
+    scene::{self, CornerRadii, CursorRegion, Quad},
+    AnyElement, Element, SizeConstraint, ViewContext,
 };
 use schemars::JsonSchema;
 use serde::Deserialize;
@@ -206,6 +206,163 @@ impl<V> Container<V> {
     }
 }
 
+#[derive(Copy, Clone, Debug, Default, JsonSchema)]
+pub struct Border {
+    pub color: Color,
+    pub width: f32,
+    pub overlay: bool,
+    pub top: bool,
+    pub bottom: bool,
+    pub left: bool,
+    pub right: bool,
+}
+
+impl Into<scene::Border> for Border {
+    fn into(self) -> scene::Border {
+        scene::Border {
+            color: self.color,
+            left: if self.left { self.width } else { 0.0 },
+            right: if self.right { self.width } else { 0.0 },
+            top: if self.top { self.width } else { 0.0 },
+            bottom: if self.bottom { self.width } else { 0.0 },
+        }
+    }
+}
+
+impl Border {
+    pub fn new(width: f32, color: Color) -> Self {
+        Self {
+            width,
+            color,
+            overlay: false,
+            top: false,
+            left: false,
+            bottom: false,
+            right: false,
+        }
+    }
+
+    pub fn all(width: f32, color: Color) -> Self {
+        Self {
+            width,
+            color,
+            overlay: false,
+            top: true,
+            left: true,
+            bottom: true,
+            right: true,
+        }
+    }
+
+    pub fn top(width: f32, color: Color) -> Self {
+        let mut border = Self::new(width, color);
+        border.top = true;
+        border
+    }
+
+    pub fn left(width: f32, color: Color) -> Self {
+        let mut border = Self::new(width, color);
+        border.left = true;
+        border
+    }
+
+    pub fn bottom(width: f32, color: Color) -> Self {
+        let mut border = Self::new(width, color);
+        border.bottom = true;
+        border
+    }
+
+    pub fn right(width: f32, color: Color) -> Self {
+        let mut border = Self::new(width, color);
+        border.right = true;
+        border
+    }
+
+    pub fn with_sides(mut self, top: bool, left: bool, bottom: bool, right: bool) -> Self {
+        self.top = top;
+        self.left = left;
+        self.bottom = bottom;
+        self.right = right;
+        self
+    }
+
+    pub fn top_width(&self) -> f32 {
+        if self.top {
+            self.width
+        } else {
+            0.0
+        }
+    }
+
+    pub fn left_width(&self) -> f32 {
+        if self.left {
+            self.width
+        } else {
+            0.0
+        }
+    }
+}
+
+impl<'de> Deserialize<'de> for Border {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        #[derive(Deserialize)]
+        struct BorderData {
+            pub width: f32,
+            pub color: Color,
+            #[serde(default)]
+            pub overlay: bool,
+            #[serde(default)]
+            pub top: bool,
+            #[serde(default)]
+            pub right: bool,
+            #[serde(default)]
+            pub bottom: bool,
+            #[serde(default)]
+            pub left: bool,
+        }
+
+        let data = BorderData::deserialize(deserializer)?;
+        let mut border = Border {
+            width: data.width,
+            color: data.color,
+            overlay: data.overlay,
+            top: data.top,
+            bottom: data.bottom,
+            left: data.left,
+            right: data.right,
+        };
+        if !border.top && !border.bottom && !border.left && !border.right {
+            border.top = true;
+            border.bottom = true;
+            border.left = true;
+            border.right = true;
+        }
+        Ok(border)
+    }
+}
+
+impl ToJson for Border {
+    fn to_json(&self) -> serde_json::Value {
+        let mut value = json!({});
+        if self.top {
+            value["top"] = json!(self.width);
+        }
+        if self.right {
+            value["right"] = json!(self.width);
+        }
+        if self.bottom {
+            value["bottom"] = json!(self.width);
+        }
+        if self.left {
+            value["left"] = json!(self.width);
+        }
+        value
+    }
+}
+
 impl<V: 'static> Element<V> for Container<V> {
     type LayoutState = ();
     type PaintState = ();
@@ -214,7 +371,7 @@ impl<V: 'static> Element<V> for Container<V> {
         &mut self,
         constraint: SizeConstraint,
         view: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         let mut size_buffer = self.margin_size() + self.padding_size();
         if !self.style.border.overlay {
@@ -230,12 +387,11 @@ impl<V: 'static> Element<V> for Container<V> {
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> Self::PaintState {
         let quad_bounds = RectF::from_points(
             bounds.origin() + vec2f(self.style.margin.left, self.style.margin.top),
@@ -243,7 +399,7 @@ impl<V: 'static> Element<V> for Container<V> {
         );
 
         if let Some(shadow) = self.style.shadow.as_ref() {
-            scene.push_shadow(scene::Shadow {
+            cx.scene().push_shadow(scene::Shadow {
                 bounds: quad_bounds + shadow.offset,
                 corner_radii: self.style.corner_radii,
                 sigma: shadow.blur,
@@ -253,7 +409,7 @@ impl<V: 'static> Element<V> for Container<V> {
 
         if let Some(hit_bounds) = quad_bounds.intersection(visible_bounds) {
             if let Some(style) = self.style.cursor {
-                scene.push_cursor_region(CursorRegion {
+                cx.scene().push_cursor_region(CursorRegion {
                     bounds: hit_bounds,
                     style,
                 });
@@ -264,29 +420,28 @@ impl<V: 'static> Element<V> for Container<V> {
             quad_bounds.origin() + vec2f(self.style.padding.left, self.style.padding.top);
 
         if self.style.border.overlay {
-            scene.push_quad(Quad {
+            cx.scene().push_quad(Quad {
                 bounds: quad_bounds,
                 background: self.style.background_color,
                 border: Default::default(),
                 corner_radii: self.style.corner_radii.into(),
             });
 
-            self.child
-                .paint(scene, child_origin, visible_bounds, view, cx);
+            self.child.paint(child_origin, visible_bounds, view, cx);
 
-            scene.push_layer(None);
-            scene.push_quad(Quad {
+            cx.scene().push_layer(None);
+            cx.scene().push_quad(Quad {
                 bounds: quad_bounds,
                 background: self.style.overlay_color,
-                border: self.style.border,
+                border: self.style.border.into(),
                 corner_radii: self.style.corner_radii.into(),
             });
-            scene.pop_layer();
+            cx.scene().pop_layer();
         } else {
-            scene.push_quad(Quad {
+            cx.scene().push_quad(Quad {
                 bounds: quad_bounds,
                 background: self.style.background_color,
-                border: self.style.border,
+                border: self.style.border.into(),
                 corner_radii: self.style.corner_radii.into(),
             });
 
@@ -295,18 +450,17 @@ impl<V: 'static> Element<V> for Container<V> {
                     self.style.border.left_width(),
                     self.style.border.top_width(),
                 );
-            self.child
-                .paint(scene, child_origin, visible_bounds, view, cx);
+            self.child.paint(child_origin, visible_bounds, view, cx);
 
             if self.style.overlay_color.is_some() {
-                scene.push_layer(None);
-                scene.push_quad(Quad {
+                cx.scene().push_layer(None);
+                cx.scene().push_quad(Quad {
                     bounds: quad_bounds,
                     background: self.style.overlay_color,
                     border: Default::default(),
                     corner_radii: self.style.corner_radii.into(),
                 });
-                scene.pop_layer();
+                cx.scene().pop_layer();
             }
         }
     }

crates/gpui/src/elements/empty.rs πŸ”—

@@ -6,7 +6,7 @@ use crate::{
         vector::{vec2f, Vector2F},
     },
     json::{json, ToJson},
-    LayoutContext, PaintContext, SceneBuilder, ViewContext,
+    ViewContext,
 };
 use crate::{Element, SizeConstraint};
 
@@ -34,7 +34,7 @@ impl<V: 'static> Element<V> for Empty {
         &mut self,
         constraint: SizeConstraint,
         _: &mut V,
-        _: &mut LayoutContext<V>,
+        _: &mut ViewContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         let x = if constraint.max.x().is_finite() && !self.collapsed {
             constraint.max.x()
@@ -52,12 +52,11 @@ impl<V: 'static> Element<V> for Empty {
 
     fn paint(
         &mut self,
-        _: &mut SceneBuilder,
         _: RectF,
         _: RectF,
         _: &mut Self::LayoutState,
         _: &mut V,
-        _: &mut PaintContext<V>,
+        _: &mut ViewContext<V>,
     ) -> Self::PaintState {
     }
 

crates/gpui/src/elements/expanded.rs πŸ”—

@@ -2,8 +2,7 @@ use std::ops::Range;
 
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
-    json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
-    ViewContext,
+    json, AnyElement, Element, SizeConstraint, ViewContext,
 };
 use serde_json::json;
 
@@ -43,7 +42,7 @@ impl<V: 'static> Element<V> for Expanded<V> {
         &mut self,
         mut constraint: SizeConstraint,
         view: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         if self.full_width {
             constraint.min.set_x(constraint.max.x());
@@ -57,15 +56,13 @@ impl<V: 'static> Element<V> for Expanded<V> {
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> Self::PaintState {
-        self.child
-            .paint(scene, bounds.origin(), visible_bounds, view, cx);
+        self.child.paint(bounds.origin(), visible_bounds, view, cx);
     }
 
     fn rect_for_text_range(

crates/gpui/src/elements/flex.rs πŸ”—

@@ -2,8 +2,7 @@ use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc};
 
 use crate::{
     json::{self, ToJson, Value},
-    AnyElement, Axis, Element, ElementStateHandle, LayoutContext, PaintContext, SceneBuilder,
-    SizeConstraint, Vector2FExt, ViewContext,
+    AnyElement, Axis, Element, ElementStateHandle, SizeConstraint, Vector2FExt, ViewContext,
 };
 use pathfinder_geometry::{
     rect::RectF,
@@ -85,7 +84,7 @@ impl<V: 'static> Flex<V> {
         remaining_flex: &mut f32,
         cross_axis_max: &mut f32,
         view: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) {
         let cross_axis = self.axis.invert();
         for child in self.children.iter_mut() {
@@ -136,7 +135,7 @@ impl<V: 'static> Element<V> for Flex<V> {
         &mut self,
         constraint: SizeConstraint,
         view: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         let mut total_flex = None;
         let mut fixed_space = self.children.len().saturating_sub(1) as f32 * self.spacing;
@@ -225,7 +224,7 @@ impl<V: 'static> Element<V> for Flex<V> {
         }
 
         if let Some(scroll_state) = self.scroll_state.as_ref() {
-            scroll_state.0.update(cx.view_context(), |scroll_state, _| {
+            scroll_state.0.update(cx, |scroll_state, _| {
                 if let Some(scroll_to) = scroll_state.scroll_to.take() {
                     let visible_start = scroll_state.scroll_position.get();
                     let visible_end = visible_start + size.along(self.axis);
@@ -260,26 +259,25 @@ impl<V: 'static> Element<V> for Flex<V> {
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         remaining_space: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> Self::PaintState {
         let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
 
         let mut remaining_space = *remaining_space;
         let overflowing = remaining_space < 0.;
         if overflowing {
-            scene.push_layer(Some(visible_bounds));
+            cx.scene().push_layer(Some(visible_bounds));
         }
 
-        if let Some(scroll_state) = &self.scroll_state {
-            scene.push_mouse_region(
-                crate::MouseRegion::new::<Self>(scroll_state.1, 0, bounds)
+        if let Some((scroll_state, id)) = &self.scroll_state {
+            let scroll_state = scroll_state.read(cx).clone();
+            cx.scene().push_mouse_region(
+                crate::MouseRegion::new::<Self>(*id, 0, bounds)
                     .on_scroll({
-                        let scroll_state = scroll_state.0.read(cx).clone();
                         let axis = self.axis;
                         move |e, _: &mut V, cx| {
                             if remaining_space < 0. {
@@ -358,7 +356,7 @@ impl<V: 'static> Element<V> for Flex<V> {
                 aligned_child_origin
             };
 
-            child.paint(scene, aligned_child_origin, visible_bounds, view, cx);
+            child.paint(aligned_child_origin, visible_bounds, view, cx);
 
             match self.axis {
                 Axis::Horizontal => child_origin += vec2f(child.size().x() + self.spacing, 0.0),
@@ -367,7 +365,7 @@ impl<V: 'static> Element<V> for Flex<V> {
         }
 
         if overflowing {
-            scene.pop_layer();
+            cx.scene().pop_layer();
         }
     }
 
@@ -443,7 +441,7 @@ impl<V: 'static> Element<V> for FlexItem<V> {
         &mut self,
         constraint: SizeConstraint,
         view: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         let size = self.child.layout(constraint, view, cx);
         (size, ())
@@ -451,15 +449,13 @@ impl<V: 'static> Element<V> for FlexItem<V> {
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> Self::PaintState {
-        self.child
-            .paint(scene, bounds.origin(), visible_bounds, view, cx)
+        self.child.paint(bounds.origin(), visible_bounds, view, cx)
     }
 
     fn rect_for_text_range(

crates/gpui/src/elements/hook.rs πŸ”—

@@ -3,7 +3,7 @@ use std::ops::Range;
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
     json::json,
-    AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, ViewContext,
+    AnyElement, Element, SizeConstraint, ViewContext,
 };
 
 pub struct Hook<V> {
@@ -36,7 +36,7 @@ impl<V: 'static> Element<V> for Hook<V> {
         &mut self,
         constraint: SizeConstraint,
         view: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         let size = self.child.layout(constraint, view, cx);
         if let Some(handler) = self.after_layout.as_mut() {
@@ -47,15 +47,13 @@ impl<V: 'static> Element<V> for Hook<V> {
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) {
-        self.child
-            .paint(scene, bounds.origin(), visible_bounds, view, cx);
+        self.child.paint(bounds.origin(), visible_bounds, view, cx);
     }
 
     fn rect_for_text_range(

crates/gpui/src/elements/image.rs πŸ”—

@@ -1,12 +1,11 @@
-use super::constrain_size_preserving_aspect_ratio;
+use super::{constrain_size_preserving_aspect_ratio, Border};
 use crate::{
     geometry::{
         rect::RectF,
         vector::{vec2f, Vector2F},
     },
     json::{json, ToJson},
-    scene, Border, Element, ImageData, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
-    ViewContext,
+    scene, Element, ImageData, SizeConstraint, ViewContext,
 };
 use schemars::JsonSchema;
 use serde::Deserialize;
@@ -65,7 +64,7 @@ impl<V: 'static> Element<V> for Image {
         &mut self,
         constraint: SizeConstraint,
         _: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         let data = match &self.source {
             ImageSource::Path(path) => match cx.asset_cache.png(path) {
@@ -92,17 +91,16 @@ impl<V: 'static> Element<V> for Image {
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         _: RectF,
         layout: &mut Self::LayoutState,
         _: &mut V,
-        _: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> Self::PaintState {
         if let Some(data) = layout {
-            scene.push_image(scene::Image {
+            cx.scene().push_image(scene::Image {
                 bounds,
-                border: self.style.border,
+                border: self.style.border.into(),
                 corner_radii: self.style.corner_radius.into(),
                 grayscale: self.style.grayscale,
                 data: data.clone(),

crates/gpui/src/elements/keystroke_label.rs πŸ”—

@@ -39,7 +39,7 @@ impl<V: 'static> Element<V> for KeystrokeLabel {
         &mut self,
         constraint: SizeConstraint,
         view: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> (Vector2F, AnyElement<V>) {
         let mut element = if let Some(keystrokes) =
             cx.keystrokes_for_action(self.view_id, self.action.as_ref())
@@ -61,14 +61,13 @@ impl<V: 'static> Element<V> for KeystrokeLabel {
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         element: &mut AnyElement<V>,
         view: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) {
-        element.paint(scene, bounds.origin(), visible_bounds, view, cx);
+        element.paint(bounds.origin(), visible_bounds, view, cx);
     }
 
     fn rect_for_text_range(

crates/gpui/src/elements/label.rs πŸ”—

@@ -8,7 +8,7 @@ use crate::{
     },
     json::{ToJson, Value},
     text_layout::{Line, RunStyle},
-    Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, ViewContext,
+    Element, SizeConstraint, ViewContext,
 };
 use schemars::JsonSchema;
 use serde::Deserialize;
@@ -136,7 +136,7 @@ impl<V: 'static> Element<V> for Label {
         &mut self,
         constraint: SizeConstraint,
         _: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         let runs = self.compute_runs();
         let line = cx.text_layout_cache().layout_str(
@@ -158,21 +158,14 @@ impl<V: 'static> Element<V> for Label {
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         line: &mut Self::LayoutState,
         _: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> Self::PaintState {
         let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
-        line.paint(
-            scene,
-            bounds.origin(),
-            visible_bounds,
-            bounds.size().y(),
-            cx,
-        )
+        line.paint(bounds.origin(), visible_bounds, bounds.size().y(), cx)
     }
 
     fn rect_for_text_range(

crates/gpui/src/elements/list.rs πŸ”—

@@ -4,8 +4,7 @@ use crate::{
         vector::{vec2f, Vector2F},
     },
     json::json,
-    AnyElement, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder, SizeConstraint,
-    ViewContext,
+    AnyElement, Element, MouseRegion, SizeConstraint, ViewContext,
 };
 use std::{cell::RefCell, collections::VecDeque, fmt::Debug, ops::Range, rc::Rc};
 use sum_tree::{Bias, SumTree};
@@ -100,7 +99,7 @@ impl<V: 'static> Element<V> for List<V> {
         &mut self,
         constraint: SizeConstraint,
         view: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         let state = &mut *self.state.0.borrow_mut();
         let size = constraint.max;
@@ -108,7 +107,7 @@ impl<V: 'static> Element<V> for List<V> {
         item_constraint.min.set_y(0.);
         item_constraint.max.set_y(f32::INFINITY);
 
-        if cx.refreshing || state.last_layout_width != Some(size.x()) {
+        if cx.refreshing() || state.last_layout_width != Some(size.x()) {
             state.rendered_range = 0..0;
             state.items = SumTree::from_iter(
                 (0..state.items.summary().count).map(|_| ListItem::Unrendered),
@@ -250,17 +249,17 @@ impl<V: 'static> Element<V> for List<V> {
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         scroll_top: &mut ListOffset,
         view: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) {
         let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
-        scene.push_layer(Some(visible_bounds));
-        scene.push_mouse_region(
-            MouseRegion::new::<Self>(cx.view_id(), 0, bounds).on_scroll({
+        cx.scene().push_layer(Some(visible_bounds));
+        let view_id = cx.view_id();
+        cx.scene()
+            .push_mouse_region(MouseRegion::new::<Self>(view_id, 0, bounds).on_scroll({
                 let state = self.state.clone();
                 let height = bounds.height();
                 let scroll_top = scroll_top.clone();
@@ -274,17 +273,14 @@ impl<V: 'static> Element<V> for List<V> {
                         cx,
                     )
                 }
-            }),
-        );
+            }));
 
         let state = &mut *self.state.0.borrow_mut();
         for (element, origin) in state.visible_elements(bounds, scroll_top) {
-            element
-                .borrow_mut()
-                .paint(scene, origin, visible_bounds, view, cx);
+            element.borrow_mut().paint(origin, visible_bounds, view, cx);
         }
 
-        scene.pop_layer();
+        cx.scene().pop_layer();
     }
 
     fn rect_for_text_range(
@@ -453,7 +449,7 @@ impl<V: 'static> StateInner<V> {
         existing_element: Option<&ListItem<V>>,
         constraint: SizeConstraint,
         view: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> Option<Rc<RefCell<AnyElement<V>>>> {
         if let Some(ListItem::Rendered(element)) = existing_element {
             Some(element.clone())
@@ -647,7 +643,7 @@ impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{elements::Empty, geometry::vector::vec2f, Entity, PaintContext};
+    use crate::{elements::Empty, geometry::vector::vec2f, Entity};
     use rand::prelude::*;
     use std::env;
 
@@ -666,15 +662,7 @@ mod tests {
             });
 
             let mut list = List::new(state.clone());
-            let mut new_parents = Default::default();
-            let mut notify_views_if_parents_change = Default::default();
-            let mut layout_cx = LayoutContext::new(
-                cx,
-                &mut new_parents,
-                &mut notify_views_if_parents_change,
-                false,
-            );
-            let (size, _) = list.layout(constraint, &mut view, &mut layout_cx);
+            let (size, _) = list.layout(constraint, &mut view, cx);
             assert_eq!(size, vec2f(100., 40.));
             assert_eq!(
                 state.0.borrow().items.summary().clone(),
@@ -698,13 +686,7 @@ mod tests {
                 cx,
             );
 
-            let mut layout_cx = LayoutContext::new(
-                cx,
-                &mut new_parents,
-                &mut notify_views_if_parents_change,
-                false,
-            );
-            let (_, logical_scroll_top) = list.layout(constraint, &mut view, &mut layout_cx);
+            let (_, logical_scroll_top) = list.layout(constraint, &mut view, cx);
             assert_eq!(
                 logical_scroll_top,
                 ListOffset {
@@ -728,13 +710,7 @@ mod tests {
                 }
             );
 
-            let mut layout_cx = LayoutContext::new(
-                cx,
-                &mut new_parents,
-                &mut notify_views_if_parents_change,
-                false,
-            );
-            let (size, logical_scroll_top) = list.layout(constraint, &mut view, &mut layout_cx);
+            let (size, logical_scroll_top) = list.layout(constraint, &mut view, cx);
             assert_eq!(size, vec2f(100., 40.));
             assert_eq!(
                 state.0.borrow().items.summary().clone(),
@@ -852,18 +828,10 @@ mod tests {
 
                 let mut list = List::new(state.clone());
                 let window_size = vec2f(width, height);
-                let mut new_parents = Default::default();
-                let mut notify_views_if_parents_change = Default::default();
-                let mut layout_cx = LayoutContext::new(
-                    cx,
-                    &mut new_parents,
-                    &mut notify_views_if_parents_change,
-                    false,
-                );
                 let (size, logical_scroll_top) = list.layout(
                     SizeConstraint::new(vec2f(0., 0.), window_size),
                     &mut view,
-                    &mut layout_cx,
+                    cx,
                 );
                 assert_eq!(size, window_size);
                 last_logical_scroll_top = Some(logical_scroll_top);
@@ -976,20 +944,12 @@ mod tests {
             &mut self,
             _: SizeConstraint,
             _: &mut V,
-            _: &mut LayoutContext<V>,
+            _: &mut ViewContext<V>,
         ) -> (Vector2F, ()) {
             (self.size, ())
         }
 
-        fn paint(
-            &mut self,
-            _: &mut SceneBuilder,
-            _: RectF,
-            _: RectF,
-            _: &mut (),
-            _: &mut V,
-            _: &mut PaintContext<V>,
-        ) {
+        fn paint(&mut self, _: RectF, _: RectF, _: &mut (), _: &mut V, _: &mut ViewContext<V>) {
             unimplemented!()
         }
 

crates/gpui/src/elements/mouse_event_handler.rs πŸ”—

@@ -10,8 +10,8 @@ use crate::{
         CursorRegion, HandlerSet, MouseClick, MouseClickOut, MouseDown, MouseDownOut, MouseDrag,
         MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut,
     },
-    AnyElement, Element, EventContext, LayoutContext, MouseRegion, MouseState, PaintContext,
-    SceneBuilder, SizeConstraint, TypeTag, ViewContext,
+    AnyElement, Element, EventContext, MouseRegion, MouseState, SizeConstraint, TypeTag,
+    ViewContext,
 };
 use serde_json::json;
 use std::ops::Range;
@@ -236,26 +236,21 @@ impl<V: 'static> MouseEventHandler<V> {
         .round_out()
     }
 
-    fn paint_regions(
-        &self,
-        scene: &mut SceneBuilder,
-        bounds: RectF,
-        visible_bounds: RectF,
-        cx: &mut ViewContext<V>,
-    ) {
+    fn paint_regions(&self, bounds: RectF, visible_bounds: RectF, cx: &mut ViewContext<V>) {
         let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
         let hit_bounds = self.hit_bounds(visible_bounds);
 
         if let Some(style) = self.cursor_style {
-            scene.push_cursor_region(CursorRegion {
+            cx.scene().push_cursor_region(CursorRegion {
                 bounds: hit_bounds,
                 style,
             });
         }
-        scene.push_mouse_region(
+        let view_id = cx.view_id();
+        cx.scene().push_mouse_region(
             MouseRegion::from_handlers(
                 self.tag,
-                cx.view_id(),
+                view_id,
                 self.region_id,
                 hit_bounds,
                 self.handlers.clone(),
@@ -275,31 +270,27 @@ impl<V: 'static> Element<V> for MouseEventHandler<V> {
         &mut self,
         constraint: SizeConstraint,
         view: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         (self.child.layout(constraint, view, cx), ())
     }
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> Self::PaintState {
         if self.above {
-            self.child
-                .paint(scene, bounds.origin(), visible_bounds, view, cx);
-
-            scene.paint_layer(None, |scene| {
-                self.paint_regions(scene, bounds, visible_bounds, cx);
+            self.child.paint(bounds.origin(), visible_bounds, view, cx);
+            cx.paint_layer(None, |cx| {
+                self.paint_regions(bounds, visible_bounds, cx);
             });
         } else {
-            self.paint_regions(scene, bounds, visible_bounds, cx);
-            self.child
-                .paint(scene, bounds.origin(), visible_bounds, view, cx);
+            self.paint_regions(bounds, visible_bounds, cx);
+            self.child.paint(bounds.origin(), visible_bounds, view, cx);
         }
     }
 

crates/gpui/src/elements/overlay.rs πŸ”—

@@ -3,8 +3,7 @@ use std::ops::Range;
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
     json::ToJson,
-    AnyElement, Axis, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder,
-    SizeConstraint, ViewContext,
+    AnyElement, Axis, Element, MouseRegion, SizeConstraint, ViewContext,
 };
 use serde_json::json;
 
@@ -125,7 +124,7 @@ impl<V: 'static> Element<V> for Overlay<V> {
         &mut self,
         constraint: SizeConstraint,
         view: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         let constraint = if self.anchor_position.is_some() {
             SizeConstraint::new(Vector2F::zero(), cx.window_size())
@@ -138,12 +137,11 @@ impl<V: 'static> Element<V> for Overlay<V> {
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         _: RectF,
         size: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) {
         let (anchor_position, mut bounds) = match self.position_mode {
             OverlayPositionMode::Window => {
@@ -213,25 +211,23 @@ impl<V: 'static> Element<V> for Overlay<V> {
             OverlayFitMode::None => {}
         }
 
-        scene.paint_stacking_context(None, self.z_index, |scene| {
-            if self.hoverable {
-                enum OverlayHoverCapture {}
-                // Block hovers in lower stacking contexts
-                scene.push_mouse_region(MouseRegion::new::<OverlayHoverCapture>(
-                    cx.view_id(),
-                    cx.view_id(),
-                    bounds,
+        cx.scene().push_stacking_context(None, self.z_index);
+        if self.hoverable {
+            enum OverlayHoverCapture {}
+            // Block hovers in lower stacking contexts
+            let view_id = cx.view_id();
+            cx.scene()
+                .push_mouse_region(MouseRegion::new::<OverlayHoverCapture>(
+                    view_id, view_id, bounds,
                 ));
-            }
-
-            self.child.paint(
-                scene,
-                bounds.origin(),
-                RectF::new(Vector2F::zero(), cx.window_size()),
-                view,
-                cx,
-            );
-        });
+        }
+        self.child.paint(
+            bounds.origin(),
+            RectF::new(Vector2F::zero(), cx.window_size()),
+            view,
+            cx,
+        );
+        cx.scene().pop_stacking_context();
     }
 
     fn rect_for_text_range(

crates/gpui/src/elements/resizable.rs πŸ”—

@@ -7,8 +7,7 @@ use serde_json::json;
 use crate::{
     geometry::rect::RectF,
     platform::{CursorStyle, MouseButton},
-    AnyElement, AppContext, Axis, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder,
-    SizeConstraint, TypeTag, View, ViewContext,
+    AnyElement, AppContext, Axis, Element, MouseRegion, SizeConstraint, TypeTag, View, ViewContext,
 };
 
 #[derive(Copy, Clone, Debug)]
@@ -105,77 +104,77 @@ impl<V: 'static> Element<V> for Resizable<V> {
         &mut self,
         constraint: crate::SizeConstraint,
         view: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         (self.child.layout(constraint, view, cx), constraint)
     }
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: pathfinder_geometry::rect::RectF,
         visible_bounds: pathfinder_geometry::rect::RectF,
         constraint: &mut SizeConstraint,
         view: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> Self::PaintState {
-        scene.push_stacking_context(None, None);
+        cx.scene().push_stacking_context(None, None);
 
         let handle_region = self.handle_side.of_rect(bounds, self.handle_size);
 
         enum ResizeHandle {}
-        scene.push_mouse_region(
-            MouseRegion::new::<ResizeHandle>(
-                cx.view_id(),
-                self.handle_side as usize,
-                handle_region,
-            )
-            .on_down(MouseButton::Left, |_, _: &mut V, _| {}) // This prevents the mouse down event from being propagated elsewhere
-            .on_click(MouseButton::Left, {
-                let on_resize = self.on_resize.clone();
-                move |click, v, cx| {
-                    if click.click_count == 2 {
-                        on_resize.borrow_mut()(v, None, cx);
+        let view_id = cx.view_id();
+        cx.scene().push_mouse_region(
+            MouseRegion::new::<ResizeHandle>(view_id, self.handle_side as usize, handle_region)
+                .on_down(MouseButton::Left, |_, _: &mut V, _| {}) // This prevents the mouse down event from being propagated elsewhere
+                .on_click(MouseButton::Left, {
+                    let on_resize = self.on_resize.clone();
+                    move |click, v, cx| {
+                        if click.click_count == 2 {
+                            on_resize.borrow_mut()(v, None, cx);
+                        }
                     }
-                }
-            })
-            .on_drag(MouseButton::Left, {
-                let bounds = bounds.clone();
-                let side = self.handle_side;
-                let prev_size = side.relevant_component(bounds.size());
-                let min_size = side.relevant_component(constraint.min);
-                let max_size = side.relevant_component(constraint.max);
-                let on_resize = self.on_resize.clone();
-                let tag = self.tag;
-                move |event, view: &mut V, cx| {
-                    if event.end {
-                        return;
+                })
+                .on_drag(MouseButton::Left, {
+                    let bounds = bounds.clone();
+                    let side = self.handle_side;
+                    let prev_size = side.relevant_component(bounds.size());
+                    let min_size = side.relevant_component(constraint.min);
+                    let max_size = side.relevant_component(constraint.max);
+                    let on_resize = self.on_resize.clone();
+                    let tag = self.tag;
+                    move |event, view: &mut V, cx| {
+                        if event.end {
+                            return;
+                        }
+
+                        let Some((bounds, _)) = get_bounds(tag, cx) else {
+                            return;
+                        };
+
+                        let new_size_raw = match side {
+                            // Handle on top side of element => Element is on bottom
+                            HandleSide::Top => {
+                                bounds.height() + bounds.origin_y() - event.position.y()
+                            }
+                            // Handle on right side of element => Element is on left
+                            HandleSide::Right => event.position.x() - bounds.lower_left().x(),
+                            // Handle on left side of element => Element is on the right
+                            HandleSide::Left => {
+                                bounds.width() + bounds.origin_x() - event.position.x()
+                            }
+                            // Handle on bottom side of element => Element is on the top
+                            HandleSide::Bottom => event.position.y() - bounds.lower_left().y(),
+                        };
+
+                        let new_size = min_size.max(new_size_raw).min(max_size).round();
+                        if new_size != prev_size {
+                            on_resize.borrow_mut()(view, Some(new_size), cx);
+                        }
                     }
-
-                    let Some((bounds, _)) = get_bounds(tag, cx) else {
-                        return;
-                    };
-
-                    let new_size_raw = match side {
-                        // Handle on top side of element => Element is on bottom
-                        HandleSide::Top => bounds.height() + bounds.origin_y() - event.position.y(),
-                        // Handle on right side of element => Element is on left
-                        HandleSide::Right => event.position.x() - bounds.lower_left().x(),
-                        // Handle on left side of element => Element is on the right
-                        HandleSide::Left => bounds.width() + bounds.origin_x() - event.position.x(),
-                        // Handle on bottom side of element => Element is on the top
-                        HandleSide::Bottom => event.position.y() - bounds.lower_left().y(),
-                    };
-
-                    let new_size = min_size.max(new_size_raw).min(max_size).round();
-                    if new_size != prev_size {
-                        on_resize.borrow_mut()(view, Some(new_size), cx);
-                    }
-                }
-            }),
+                }),
         );
 
-        scene.push_cursor_region(crate::CursorRegion {
+        cx.scene().push_cursor_region(crate::CursorRegion {
             bounds: handle_region,
             style: match self.handle_side.axis() {
                 Axis::Horizontal => CursorStyle::ResizeLeftRight,
@@ -183,10 +182,9 @@ impl<V: 'static> Element<V> for Resizable<V> {
             },
         });
 
-        scene.pop_stacking_context();
+        cx.scene().pop_stacking_context();
 
-        self.child
-            .paint(scene, bounds.origin(), visible_bounds, view, cx);
+        self.child.paint(bounds.origin(), visible_bounds, view, cx);
     }
 
     fn rect_for_text_range(
@@ -242,26 +240,24 @@ impl<V: View, P: 'static> Element<V> for BoundsProvider<V, P> {
         &mut self,
         constraint: crate::SizeConstraint,
         view: &mut V,
-        cx: &mut crate::LayoutContext<V>,
+        cx: &mut crate::ViewContext<V>,
     ) -> (pathfinder_geometry::vector::Vector2F, Self::LayoutState) {
         (self.child.layout(constraint, view, cx), ())
     }
 
     fn paint(
         &mut self,
-        scene: &mut crate::SceneBuilder,
         bounds: pathfinder_geometry::rect::RectF,
         visible_bounds: pathfinder_geometry::rect::RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut crate::PaintContext<V>,
+        cx: &mut crate::ViewContext<V>,
     ) -> Self::PaintState {
         cx.update_default_global::<ProviderMap, _, _>(|map, _| {
             map.0.insert(TypeTag::new::<P>(), (bounds, visible_bounds));
         });
 
-        self.child
-            .paint(scene, bounds.origin(), visible_bounds, view, cx)
+        self.child.paint(bounds.origin(), visible_bounds, view, cx)
     }
 
     fn rect_for_text_range(

crates/gpui/src/elements/stack.rs πŸ”—

@@ -3,7 +3,7 @@ use std::ops::Range;
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
     json::{self, json, ToJson},
-    AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, ViewContext,
+    AnyElement, Element, SizeConstraint, ViewContext,
 };
 
 /// Element which renders it's children in a stack on top of each other.
@@ -34,7 +34,7 @@ impl<V: 'static> Element<V> for Stack<V> {
         &mut self,
         mut constraint: SizeConstraint,
         view: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         let mut size = constraint.min;
         let mut children = self.children.iter_mut();
@@ -52,17 +52,16 @@ impl<V: 'static> Element<V> for Stack<V> {
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> Self::PaintState {
         for child in &mut self.children {
-            scene.paint_layer(None, |scene| {
-                child.paint(scene, bounds.origin(), visible_bounds, view, cx);
-            });
+            cx.scene().push_layer(None);
+            child.paint(bounds.origin(), visible_bounds, view, cx);
+            cx.scene().pop_layer();
         }
     }
 

crates/gpui/src/elements/svg.rs πŸ”—

@@ -1,13 +1,12 @@
 use super::constrain_size_preserving_aspect_ratio;
 use crate::json::ToJson;
-use crate::PaintContext;
 use crate::{
     color::Color,
     geometry::{
         rect::RectF,
         vector::{vec2f, Vector2F},
     },
-    scene, Element, LayoutContext, SceneBuilder, SizeConstraint, ViewContext,
+    scene, Element, SizeConstraint, ViewContext,
 };
 use schemars::JsonSchema;
 use serde_derive::Deserialize;
@@ -49,7 +48,7 @@ impl<V: 'static> Element<V> for Svg {
         &mut self,
         constraint: SizeConstraint,
         _: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         match cx.asset_cache.svg(&self.path) {
             Ok(tree) => {
@@ -69,15 +68,14 @@ impl<V: 'static> Element<V> for Svg {
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         _visible_bounds: RectF,
         svg: &mut Self::LayoutState,
         _: &mut V,
-        _: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) {
         if let Some(svg) = svg.clone() {
-            scene.push_icon(scene::Icon {
+            cx.scene().push_icon(scene::Icon {
                 bounds,
                 svg,
                 path: self.path.clone(),

crates/gpui/src/elements/text.rs πŸ”—

@@ -7,8 +7,7 @@ use crate::{
     },
     json::{ToJson, Value},
     text_layout::{Line, RunStyle, ShapedBoundary},
-    AppContext, Element, FontCache, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
-    TextLayoutCache, ViewContext,
+    Element, FontCache, SizeConstraint, TextLayoutCache, ViewContext, WindowContext,
 };
 use log::warn;
 use serde_json::json;
@@ -21,7 +20,7 @@ pub struct Text {
     highlights: Option<Box<[(Range<usize>, HighlightStyle)]>>,
     custom_runs: Option<(
         Box<[Range<usize>]>,
-        Box<dyn FnMut(usize, RectF, &mut SceneBuilder, &mut AppContext)>,
+        Box<dyn FnMut(usize, RectF, &mut WindowContext)>,
     )>,
 }
 
@@ -58,7 +57,7 @@ impl Text {
     pub fn with_custom_runs(
         mut self,
         runs: impl Into<Box<[Range<usize>]>>,
-        callback: impl 'static + FnMut(usize, RectF, &mut SceneBuilder, &mut AppContext),
+        callback: impl 'static + FnMut(usize, RectF, &mut WindowContext),
     ) -> Self {
         self.custom_runs = Some((runs.into(), Box::new(callback)));
         self
@@ -78,7 +77,7 @@ impl<V: 'static> Element<V> for Text {
         &mut self,
         constraint: SizeConstraint,
         _: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         // Convert the string and highlight ranges into an iterator of highlighted chunks.
 
@@ -166,16 +165,15 @@ impl<V: 'static> Element<V> for Text {
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         layout: &mut Self::LayoutState,
         _: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> Self::PaintState {
         let mut origin = bounds.origin();
         let empty = Vec::new();
-        let mut callback = |_, _, _: &mut SceneBuilder, _: &mut AppContext| {};
+        let mut callback = |_, _, _: &mut WindowContext| {};
 
         let mouse_runs;
         let custom_run_callback;
@@ -202,7 +200,6 @@ impl<V: 'static> Element<V> for Text {
             if boundaries.intersects(visible_bounds) {
                 if self.soft_wrap {
                     line.paint_wrapped(
-                        scene,
                         origin,
                         visible_bounds,
                         layout.line_height,
@@ -210,7 +207,7 @@ impl<V: 'static> Element<V> for Text {
                         cx,
                     );
                 } else {
-                    line.paint(scene, origin, visible_bounds, layout.line_height, cx);
+                    line.paint(origin, visible_bounds, layout.line_height, cx);
                 }
             }
 
@@ -248,7 +245,7 @@ impl<V: 'static> Element<V> for Text {
                                     *run_origin,
                                     glyph_origin + vec2f(0., layout.line_height),
                                 );
-                                custom_run_callback(*run_ix, bounds, scene, cx);
+                                custom_run_callback(*run_ix, bounds, cx);
                                 *run_origin =
                                     vec2f(origin.x(), glyph_origin.y() + layout.line_height);
                             }
@@ -264,7 +261,7 @@ impl<V: 'static> Element<V> for Text {
                                     run_origin,
                                     glyph_origin + vec2f(0., layout.line_height),
                                 );
-                                custom_run_callback(run_ix, bounds, scene, cx);
+                                custom_run_callback(run_ix, bounds, cx);
                                 custom_runs.next();
                             }
 
@@ -294,7 +291,7 @@ impl<V: 'static> Element<V> for Text {
                             run_origin,
                             line_end + vec2f(0., layout.line_height),
                         );
-                        custom_run_callback(run_ix, bounds, scene, cx);
+                        custom_run_callback(run_ix, bounds, cx);
                         if end_offset == run_end_offset {
                             custom_runs.next();
                         }
@@ -411,18 +408,10 @@ mod tests {
             let mut view = TestView;
             fonts::with_font_cache(cx.font_cache().clone(), || {
                 let mut text = Text::new("Hello\r\n", Default::default()).with_soft_wrap(true);
-                let mut new_parents = Default::default();
-                let mut notify_views_if_parents_change = Default::default();
-                let mut layout_cx = LayoutContext::new(
-                    cx,
-                    &mut new_parents,
-                    &mut notify_views_if_parents_change,
-                    false,
-                );
                 let (_, state) = text.layout(
                     SizeConstraint::new(Default::default(), vec2f(f32::INFINITY, f32::INFINITY)),
                     &mut view,
-                    &mut layout_cx,
+                    cx,
                 );
                 assert_eq!(state.shaped_lines.len(), 2);
                 assert_eq!(state.wrap_boundaries.len(), 2);

crates/gpui/src/elements/tooltip.rs πŸ”—

@@ -6,8 +6,7 @@ use crate::{
     fonts::TextStyle,
     geometry::{rect::RectF, vector::Vector2F},
     json::json,
-    Action, Axis, ElementStateHandle, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
-    Task, TypeTag, ViewContext,
+    Action, Axis, ElementStateHandle, SizeConstraint, Task, TypeTag, ViewContext,
 };
 use schemars::JsonSchema;
 use serde::Deserialize;
@@ -189,7 +188,7 @@ impl<V: 'static> Element<V> for Tooltip<V> {
         &mut self,
         constraint: SizeConstraint,
         view: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         let size = self.child.layout(constraint, view, cx);
         if let Some(tooltip) = self.tooltip.as_mut() {
@@ -204,17 +203,15 @@ impl<V: 'static> Element<V> for Tooltip<V> {
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) {
-        self.child
-            .paint(scene, bounds.origin(), visible_bounds, view, cx);
+        self.child.paint(bounds.origin(), visible_bounds, view, cx);
         if let Some(tooltip) = self.tooltip.as_mut() {
-            tooltip.paint(scene, bounds.origin(), visible_bounds, view, cx);
+            tooltip.paint(bounds.origin(), visible_bounds, view, cx);
         }
     }
 

crates/gpui/src/elements/uniform_list.rs πŸ”—

@@ -6,7 +6,7 @@ use crate::{
     },
     json::{self, json},
     platform::ScrollWheelEvent,
-    AnyElement, LayoutContext, MouseRegion, PaintContext, SceneBuilder, ViewContext,
+    AnyElement, MouseRegion, ViewContext,
 };
 use json::ToJson;
 use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
@@ -158,7 +158,7 @@ impl<V: 'static> Element<V> for UniformList<V> {
         &mut self,
         constraint: SizeConstraint,
         view: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         if constraint.max.y().is_infinite() {
             unimplemented!(
@@ -272,18 +272,17 @@ impl<V: 'static> Element<V> for UniformList<V> {
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         layout: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> Self::PaintState {
         let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
 
-        scene.push_layer(Some(visible_bounds));
+        cx.scene().push_layer(Some(visible_bounds));
 
-        scene.push_mouse_region(
+        cx.scene().push_mouse_region(
             MouseRegion::new::<Self>(self.view_id, 0, visible_bounds).on_scroll({
                 let scroll_max = layout.scroll_max;
                 let state = self.state.clone();
@@ -312,11 +311,11 @@ impl<V: 'static> Element<V> for UniformList<V> {
             );
 
         for item in &mut layout.items {
-            item.paint(scene, item_origin, visible_bounds, view, cx);
+            item.paint(item_origin, visible_bounds, view, cx);
             item_origin += vec2f(0.0, layout.item_height);
         }
 
-        scene.pop_layer();
+        cx.scene().pop_layer();
     }
 
     fn rect_for_text_range(

crates/gpui/src/fonts.rs πŸ”—

@@ -60,7 +60,7 @@ pub struct Features {
     pub zero: Option<bool>,
 }
 
-#[derive(Clone, Debug, JsonSchema, Refineable)]
+#[derive(Clone, Debug, JsonSchema)]
 pub struct TextStyle {
     pub color: Color,
     pub font_family_name: Arc<str>,
@@ -80,19 +80,78 @@ impl TextStyle {
             ..Default::default()
         }
     }
+}
+
+impl TextStyle {
+    pub fn refine(
+        &mut self,
+        refinement: &TextStyleRefinement,
+        font_cache: &FontCache,
+    ) -> Result<()> {
+        if let Some(font_size) = refinement.font_size {
+            self.font_size = font_size;
+        }
+        if let Some(color) = refinement.color {
+            self.color = color;
+        }
+        if let Some(underline) = refinement.underline {
+            self.underline = underline;
+        }
+
+        let mut update_font_id = false;
+        if let Some(font_family) = refinement.font_family.clone() {
+            self.font_family_id = font_cache.load_family(&[&font_family], &Default::default())?;
+            self.font_family_name = font_family;
+            update_font_id = true;
+        }
+        if let Some(font_weight) = refinement.font_weight {
+            self.font_properties.weight = font_weight;
+            update_font_id = true;
+        }
+        if let Some(font_style) = refinement.font_style {
+            self.font_properties.style = font_style;
+            update_font_id = true;
+        }
+
+        if update_font_id {
+            self.font_id = font_cache.select_font(self.font_family_id, &self.font_properties)?;
+        }
 
-    pub fn refine(self, refinement: TextStyleRefinement) -> TextStyle {
-        TextStyle {
-            color: refinement.color.unwrap_or(self.color),
-            font_family_name: refinement
-                .font_family_name
-                .unwrap_or_else(|| self.font_family_name.clone()),
-            font_family_id: refinement.font_family_id.unwrap_or(self.font_family_id),
-            font_id: refinement.font_id.unwrap_or(self.font_id),
-            font_size: refinement.font_size.unwrap_or(self.font_size),
-            font_properties: refinement.font_properties.unwrap_or(self.font_properties),
-            underline: refinement.underline.unwrap_or(self.underline),
-            soft_wrap: refinement.soft_wrap.unwrap_or(self.soft_wrap),
+        Ok(())
+    }
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct TextStyleRefinement {
+    pub color: Option<Color>,
+    pub font_family: Option<Arc<str>>,
+    pub font_size: Option<f32>,
+    pub font_weight: Option<Weight>,
+    pub font_style: Option<Style>,
+    pub underline: Option<Underline>,
+}
+
+impl Refineable for TextStyleRefinement {
+    type Refinement = Self;
+
+    fn refine(&mut self, refinement: &Self::Refinement) {
+        if refinement.color.is_some() {
+            self.color = refinement.color;
+        }
+        if refinement.font_family.is_some() {
+            self.font_family = refinement.font_family.clone();
+        }
+        if refinement.font_size.is_some() {
+            self.font_size = refinement.font_size;
+        }
+        if refinement.font_weight.is_some() {
+            self.font_weight = refinement.font_weight;
+        }
+        if refinement.font_style.is_some() {
+            self.font_style = refinement.font_style;
+        }
+        if refinement.underline.is_some() {
+            self.underline = refinement.underline;
         }
     }
 }

crates/gpui/src/geometry.rs πŸ”—

@@ -1,3 +1,5 @@
+use std::fmt::Debug;
+
 use super::scene::{Path, PathVertex};
 use crate::{color::Color, json::ToJson};
 pub use pathfinder_geometry::*;
@@ -133,13 +135,14 @@ impl ToJson for RectF {
     }
 }
 
-#[derive(Refineable)]
-pub struct Point<T: Clone + Default> {
+#[derive(Refineable, Debug)]
+#[refineable(debug)]
+pub struct Point<T: Clone + Default + Debug> {
     pub x: T,
     pub y: T,
 }
 
-impl<T: Clone + Default> Clone for Point<T> {
+impl<T: Clone + Default + Debug> Clone for Point<T> {
     fn clone(&self) -> Self {
         Self {
             x: self.x.clone(),
@@ -148,7 +151,7 @@ impl<T: Clone + Default> Clone for Point<T> {
     }
 }
 
-impl<T: Clone + Default> Into<taffy::geometry::Point<T>> for Point<T> {
+impl<T: Clone + Default + Debug> Into<taffy::geometry::Point<T>> for Point<T> {
     fn into(self) -> taffy::geometry::Point<T> {
         taffy::geometry::Point {
             x: self.x,
@@ -157,13 +160,14 @@ impl<T: Clone + Default> Into<taffy::geometry::Point<T>> for Point<T> {
     }
 }
 
-#[derive(Clone, Refineable)]
-pub struct Size<T: Clone + Default> {
+#[derive(Refineable, Clone, Debug)]
+#[refineable(debug)]
+pub struct Size<T: Clone + Default + Debug> {
     pub width: T,
     pub height: T,
 }
 
-impl<S, T: Clone + Default> From<taffy::geometry::Size<S>> for Size<T>
+impl<S, T: Clone + Default + Debug> From<taffy::geometry::Size<S>> for Size<T>
 where
     S: Into<T>,
 {
@@ -175,7 +179,7 @@ where
     }
 }
 
-impl<S, T: Clone + Default> Into<taffy::geometry::Size<S>> for Size<T>
+impl<S, T: Clone + Default + Debug> Into<taffy::geometry::Size<S>> for Size<T>
 where
     T: Into<S>,
 {
@@ -222,15 +226,25 @@ impl Size<Length> {
     }
 }
 
-#[derive(Clone, Default, Refineable)]
-pub struct Edges<T: Clone + Default> {
+#[derive(Clone, Default, Refineable, Debug)]
+#[refineable(debug)]
+pub struct Edges<T: Clone + Default + Debug> {
     pub top: T,
     pub right: T,
     pub bottom: T,
     pub left: T,
 }
 
-impl Edges<DefiniteLength> {
+impl Edges<Length> {
+    pub fn auto() -> Self {
+        Self {
+            top: Length::Auto,
+            right: Length::Auto,
+            bottom: Length::Auto,
+            left: Length::Auto,
+        }
+    }
+
     pub fn zero() -> Self {
         Self {
             top: pixels(0.),
@@ -240,7 +254,10 @@ impl Edges<DefiniteLength> {
         }
     }
 
-    pub fn to_taffy(&self, rem_size: f32) -> taffy::geometry::Rect<taffy::style::LengthPercentage> {
+    pub fn to_taffy(
+        &self,
+        rem_size: f32,
+    ) -> taffy::geometry::Rect<taffy::style::LengthPercentageAuto> {
         taffy::geometry::Rect {
             top: self.top.to_taffy(rem_size),
             right: self.right.to_taffy(rem_size),
@@ -250,16 +267,27 @@ impl Edges<DefiniteLength> {
     }
 }
 
-impl Edges<Length> {
-    pub fn auto() -> Self {
+impl Edges<DefiniteLength> {
+    pub fn zero() -> Self {
         Self {
-            top: Length::Auto,
-            right: Length::Auto,
-            bottom: Length::Auto,
-            left: Length::Auto,
+            top: pixels(0.),
+            right: pixels(0.),
+            bottom: pixels(0.),
+            left: pixels(0.),
         }
     }
 
+    pub fn to_taffy(&self, rem_size: f32) -> taffy::geometry::Rect<taffy::style::LengthPercentage> {
+        taffy::geometry::Rect {
+            top: self.top.to_taffy(rem_size),
+            right: self.right.to_taffy(rem_size),
+            bottom: self.bottom.to_taffy(rem_size),
+            left: self.left.to_taffy(rem_size),
+        }
+    }
+}
+
+impl Edges<AbsoluteLength> {
     pub fn zero() -> Self {
         Self {
             top: pixels(0.),
@@ -269,10 +297,7 @@ impl Edges<Length> {
         }
     }
 
-    pub fn to_taffy(
-        &self,
-        rem_size: f32,
-    ) -> taffy::geometry::Rect<taffy::style::LengthPercentageAuto> {
+    pub fn to_taffy(&self, rem_size: f32) -> taffy::geometry::Rect<taffy::style::LengthPercentage> {
         taffy::geometry::Rect {
             top: self.top.to_taffy(rem_size),
             right: self.right.to_taffy(rem_size),
@@ -280,6 +305,21 @@ impl Edges<Length> {
             left: self.left.to_taffy(rem_size),
         }
     }
+
+    pub fn to_pixels(&self, rem_size: f32) -> Edges<f32> {
+        Edges {
+            top: self.top.to_pixels(rem_size),
+            right: self.right.to_pixels(rem_size),
+            bottom: self.bottom.to_pixels(rem_size),
+            left: self.left.to_pixels(rem_size),
+        }
+    }
+}
+
+impl Edges<f32> {
+    pub fn is_empty(&self) -> bool {
+        self.top == 0.0 && self.right == 0.0 && self.bottom == 0.0 && self.left == 0.0
+    }
 }
 
 #[derive(Clone, Copy)]
@@ -288,6 +328,15 @@ pub enum AbsoluteLength {
     Rems(f32),
 }
 
+impl std::fmt::Debug for AbsoluteLength {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            AbsoluteLength::Pixels(pixels) => write!(f, "{}px", pixels),
+            AbsoluteLength::Rems(rems) => write!(f, "{}rems", rems),
+        }
+    }
+}
+
 impl AbsoluteLength {
     pub fn to_pixels(&self, rem_size: f32) -> f32 {
         match self {
@@ -295,6 +344,13 @@ impl AbsoluteLength {
             AbsoluteLength::Rems(rems) => rems * rem_size,
         }
     }
+
+    pub fn to_taffy(&self, rem_size: f32) -> taffy::style::LengthPercentage {
+        match self {
+            AbsoluteLength::Pixels(pixels) => taffy::style::LengthPercentage::Length(*pixels),
+            AbsoluteLength::Rems(rems) => taffy::style::LengthPercentage::Length(rems * rem_size),
+        }
+    }
 }
 
 impl Default for AbsoluteLength {
@@ -307,7 +363,7 @@ impl Default for AbsoluteLength {
 #[derive(Clone, Copy)]
 pub enum DefiniteLength {
     Absolute(AbsoluteLength),
-    Relative(f32), // Percent, from 0 to 100.
+    Relative(f32), // 0. to 1.
 }
 
 impl DefiniteLength {
@@ -326,6 +382,15 @@ impl DefiniteLength {
     }
 }
 
+impl std::fmt::Debug for DefiniteLength {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            DefiniteLength::Absolute(length) => std::fmt::Debug::fmt(length, f),
+            DefiniteLength::Relative(fract) => write!(f, "{}%", (fract * 100.0) as i32),
+        }
+    }
+}
+
 impl From<AbsoluteLength> for DefiniteLength {
     fn from(length: AbsoluteLength) -> Self {
         Self::Absolute(length)
@@ -345,6 +410,15 @@ pub enum Length {
     Auto,
 }
 
+impl std::fmt::Debug for Length {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Length::Definite(definite_length) => write!(f, "{:?}", definite_length),
+            Length::Auto => write!(f, "auto"),
+        }
+    }
+}
+
 pub fn relative<T: From<DefiniteLength>>(fraction: f32) -> T {
     DefiniteLength::Relative(fraction).into()
 }
@@ -387,3 +461,9 @@ impl Default for Length {
         Self::Definite(DefiniteLength::default())
     }
 }
+
+impl From<()> for Length {
+    fn from(_: ()) -> Self {
+        Self::Definite(DefiniteLength::default())
+    }
+}

crates/gpui/src/gpui.rs πŸ”—

@@ -1,4 +1,5 @@
 mod app;
+mod image_cache;
 pub use app::*;
 mod assets;
 #[cfg(any(test, feature = "test-support"))]
@@ -8,6 +9,7 @@ pub mod elements;
 pub mod font_cache;
 mod image_data;
 pub use crate::image_data::ImageData;
+pub use taffy;
 pub mod views;
 pub use font_cache::FontCache;
 mod clipboard;
@@ -27,9 +29,9 @@ pub mod json;
 pub mod keymap_matcher;
 pub mod platform;
 pub use gpui_macros::{test, Element};
+pub use usvg;
 pub use window::{
-    Axis, EngineLayout, LayoutEngine, LayoutId, RectFExt, SizeConstraint, Vector2FExt,
-    WindowContext,
+    Axis, Layout, LayoutEngine, LayoutId, RectFExt, SizeConstraint, Vector2FExt, WindowContext,
 };
 
 pub use anyhow;

crates/gpui/src/image_cache.rs πŸ”—

@@ -0,0 +1,99 @@
+use std::sync::Arc;
+
+use crate::ImageData;
+use collections::HashMap;
+use futures::{
+    future::{BoxFuture, Shared},
+    AsyncReadExt, FutureExt,
+};
+use image::ImageError;
+use parking_lot::Mutex;
+use thiserror::Error;
+use util::{
+    arc_cow::ArcCow,
+    http::{self, HttpClient},
+};
+
+#[derive(Debug, Error, Clone)]
+pub enum Error {
+    #[error("http error: {0}")]
+    Client(#[from] http::Error),
+    #[error("IO error: {0}")]
+    Io(Arc<std::io::Error>),
+    #[error("unexpected http status: {status}, body: {body}")]
+    BadStatus {
+        status: http::StatusCode,
+        body: String,
+    },
+    #[error("image error: {0}")]
+    Image(Arc<ImageError>),
+}
+
+impl From<std::io::Error> for Error {
+    fn from(error: std::io::Error) -> Self {
+        Error::Io(Arc::new(error))
+    }
+}
+
+impl From<ImageError> for Error {
+    fn from(error: ImageError) -> Self {
+        Error::Image(Arc::new(error))
+    }
+}
+
+pub struct ImageCache {
+    client: Arc<dyn HttpClient>,
+    images: Arc<Mutex<HashMap<ArcCow<'static, str>, FetchImageFuture>>>,
+}
+
+type FetchImageFuture = Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>>;
+
+impl ImageCache {
+    pub fn new(client: Arc<dyn HttpClient>) -> Self {
+        ImageCache {
+            client,
+            images: Default::default(),
+        }
+    }
+
+    pub fn get(
+        &self,
+        uri: impl Into<ArcCow<'static, str>>,
+    ) -> Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>> {
+        let uri = uri.into();
+        let mut images = self.images.lock();
+
+        match images.get(uri.as_ref()) {
+            Some(future) => future.clone(),
+            None => {
+                let client = self.client.clone();
+                let future = {
+                    let uri = uri.clone();
+                    async move {
+                        let mut response = client.get(uri.as_ref(), ().into(), true).await?;
+                        let mut body = Vec::new();
+                        response.body_mut().read_to_end(&mut body).await?;
+
+                        if !response.status().is_success() {
+                            return Err(Error::BadStatus {
+                                status: response.status(),
+                                body: String::from_utf8_lossy(&body).into_owned(),
+                            });
+                        }
+
+                        let format = image::guess_format(&body)?;
+                        let image =
+                            image::load_from_memory_with_format(&body, format)?.into_bgra8();
+
+                        Ok(ImageData::new(image))
+                    }
+                }
+                .boxed()
+                .shared();
+
+                images.insert(uri, future.clone());
+                future
+            }
+        }
+    }
+}

crates/gpui/src/platform.rs πŸ”—

@@ -146,6 +146,7 @@ pub trait Window {
     fn titlebar_height(&self) -> f32;
     fn appearance(&self) -> Appearance;
     fn screen(&self) -> Rc<dyn Screen>;
+    fn mouse_position(&self) -> Vector2F;
 
     fn as_any_mut(&mut self) -> &mut dyn Any;
     fn set_input_handler(&mut self, input_handler: Box<dyn InputHandler>);

crates/gpui/src/platform/mac/renderer.rs πŸ”—

@@ -577,7 +577,6 @@ impl Renderer {
         };
         for (ix, quad) in quads.iter().enumerate() {
             let bounds = quad.bounds * scale_factor;
-            let border_width = quad.border.width * scale_factor;
             let shader_quad = shaders::GPUIQuad {
                 origin: bounds.origin().round().to_float2(),
                 size: bounds.size().round().to_float2(),
@@ -585,10 +584,10 @@ impl Renderer {
                     .background
                     .unwrap_or_else(Color::transparent_black)
                     .to_uchar4(),
-                border_top: border_width * (quad.border.top as usize as f32),
-                border_right: border_width * (quad.border.right as usize as f32),
-                border_bottom: border_width * (quad.border.bottom as usize as f32),
-                border_left: border_width * (quad.border.left as usize as f32),
+                border_top: quad.border.top * scale_factor,
+                border_right: quad.border.right * scale_factor,
+                border_bottom: quad.border.bottom * scale_factor,
+                border_left: quad.border.left * scale_factor,
                 border_color: quad.border.color.to_uchar4(),
                 corner_radius_top_left: quad.corner_radii.top_left * scale_factor,
                 corner_radius_top_right: quad.corner_radii.top_right * scale_factor,
@@ -746,7 +745,6 @@ impl Renderer {
             let origin = image.bounds.origin() * scale_factor;
             let target_size = image.bounds.size() * scale_factor;
             let corner_radii = image.corner_radii * scale_factor;
-            let border_width = image.border.width * scale_factor;
             let (alloc_id, atlas_bounds) = self.image_cache.render(&image.data);
             images_by_atlas
                 .entry(alloc_id.atlas_id)
@@ -756,10 +754,10 @@ impl Renderer {
                     target_size: target_size.to_float2(),
                     source_size: atlas_bounds.size().to_float2(),
                     atlas_origin: atlas_bounds.origin().to_float2(),
-                    border_top: border_width * (image.border.top as usize as f32),
-                    border_right: border_width * (image.border.right as usize as f32),
-                    border_bottom: border_width * (image.border.bottom as usize as f32),
-                    border_left: border_width * (image.border.left as usize as f32),
+                    border_top: image.border.top * scale_factor,
+                    border_right: image.border.right * scale_factor,
+                    border_bottom: image.border.bottom * scale_factor,
+                    border_left: image.border.left * scale_factor,
                     border_color: image.border.color.to_uchar4(),
                     corner_radius_top_left: corner_radii.top_left,
                     corner_radius_top_right: corner_radii.top_right,

crates/gpui/src/platform/mac/window.rs πŸ”—

@@ -221,6 +221,14 @@ unsafe fn build_classes() {
     };
 }
 
+pub fn convert_mouse_position(position: NSPoint, window_height: f32) -> Vector2F {
+    vec2f(
+        position.x as f32,
+        // MacOS screen coordinates are relative to bottom left
+        window_height - position.y as f32,
+    )
+}
+
 unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const Class {
     let mut decl = ClassDecl::new(name, superclass).unwrap();
     decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
@@ -661,6 +669,16 @@ impl platform::Window for MacWindow {
         }
     }
 
+    fn mouse_position(&self) -> Vector2F {
+        let position = unsafe {
+            self.0
+                .borrow()
+                .native_window
+                .mouseLocationOutsideOfEventStream()
+        };
+        convert_mouse_position(position, self.content_size().y())
+    }
+
     fn as_any_mut(&mut self) -> &mut dyn Any {
         self
     }
@@ -988,7 +1006,40 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
                         .flatten()
                         .is_some();
                 if !is_composing {
-                    handled = callback(Event::KeyDown(event));
+                    // if the IME has changed the key, we'll first emit an event with the character
+                    // generated by the IME system; then fallback to the keystroke if that is not
+                    // handled.
+                    // cases that we have working:
+                    // - " on a brazillian layout by typing <quote><space>
+                    // - ctrl-` on a brazillian layout by typing <ctrl-`>
+                    // - $ on a czech QWERTY layout by typing <alt-4>
+                    // - 4 on a czech QWERTY layout by typing <shift-4>
+                    // - ctrl-4 on a czech QWERTY layout by typing <ctrl-alt-4> (or <ctrl-shift-4>)
+                    if ime_text.is_some() && ime_text.as_ref() != Some(&event.keystroke.key) {
+                        let event_with_ime_text = KeyDownEvent {
+                            is_held: false,
+                            keystroke: Keystroke {
+                                // we match ctrl because some use-cases need it.
+                                // we don't match alt because it's often used to generate the optional character
+                                // we don't match shift because we're not here with letters (usually)
+                                // we don't match cmd/fn because they don't seem to use IME
+                                ctrl: event.keystroke.ctrl,
+                                alt: false,
+                                shift: false,
+                                cmd: false,
+                                function: false,
+                                key: ime_text.clone().unwrap(),
+                            },
+                        };
+                        handled = callback(Event::KeyDown(event_with_ime_text));
+                    }
+                    if !handled {
+                        // empty key happens when you type a deadkey in input composition.
+                        // (e.g. on a brazillian keyboard typing quote is a deadkey)
+                        if !event.keystroke.key.is_empty() {
+                            handled = callback(Event::KeyDown(event));
+                        }
+                    }
                 }
 
                 if !handled {

crates/gpui/src/platform/test.rs πŸ”—

@@ -332,6 +332,10 @@ impl super::Window for Window {
         Rc::new(Screen)
     }
 
+    fn mouse_position(&self) -> Vector2F {
+        Vector2F::zero()
+    }
+
     fn as_any_mut(&mut self) -> &mut dyn Any {
         self
     }

crates/gpui/src/scene.rs πŸ”—

@@ -8,7 +8,6 @@ use derive_more::Mul;
 use schemars::JsonSchema;
 use serde::Deserialize;
 use serde_derive::Serialize;
-use serde_json::json;
 use std::{
     any::{Any, TypeId},
     borrow::Cow,
@@ -20,7 +19,6 @@ use crate::{
     color::Color,
     fonts::{FontId, GlyphId},
     geometry::{rect::RectF, vector::Vector2F},
-    json::ToJson,
     platform::{current::Surface, CursorStyle},
     ImageData, WindowContext,
 };
@@ -28,10 +26,9 @@ pub use mouse_event::*;
 pub use mouse_region::*;
 
 pub struct SceneBuilder {
-    scale_factor: f32,
     stacking_contexts: Vec<StackingContext>,
     active_stacking_context_stack: Vec<usize>,
-    /// Used by the playground crate.
+    /// Used by the gpui2 crate.
     pub event_handlers: Vec<EventHandler>,
     #[cfg(debug_assertions)]
     mouse_region_ids: HashSet<MouseRegionId>,
@@ -171,15 +168,13 @@ pub struct Icon {
     pub color: Color,
 }
 
-#[derive(Clone, Copy, Default, Debug, JsonSchema)]
+#[derive(Clone, Copy, Default, Debug)]
 pub struct Border {
-    pub width: f32,
     pub color: Color,
-    pub overlay: bool,
-    pub top: bool,
-    pub right: bool,
-    pub bottom: bool,
-    pub left: bool,
+    pub top: f32,
+    pub right: f32,
+    pub bottom: f32,
+    pub left: f32,
 }
 
 #[derive(Clone, Copy, Default, Debug)]
@@ -191,47 +186,6 @@ pub struct Underline {
     pub squiggly: bool,
 }
 
-impl<'de> Deserialize<'de> for Border {
-    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-    where
-        D: serde::Deserializer<'de>,
-    {
-        #[derive(Deserialize)]
-        struct BorderData {
-            pub width: f32,
-            pub color: Color,
-            #[serde(default)]
-            pub overlay: bool,
-            #[serde(default)]
-            pub top: bool,
-            #[serde(default)]
-            pub right: bool,
-            #[serde(default)]
-            pub bottom: bool,
-            #[serde(default)]
-            pub left: bool,
-        }
-
-        let data = BorderData::deserialize(deserializer)?;
-        let mut border = Border {
-            width: data.width,
-            color: data.color,
-            overlay: data.overlay,
-            top: data.top,
-            bottom: data.bottom,
-            left: data.left,
-            right: data.right,
-        };
-        if !border.top && !border.bottom && !border.left && !border.right {
-            border.top = true;
-            border.bottom = true;
-            border.left = true;
-            border.right = true;
-        }
-        Ok(border)
-    }
-}
-
 #[derive(Debug)]
 pub struct Path {
     pub bounds: RectF,
@@ -290,43 +244,38 @@ impl Scene {
 }
 
 impl SceneBuilder {
-    pub fn new(scale_factor: f32) -> Self {
-        let stacking_context = StackingContext::new(None, 0);
-        SceneBuilder {
-            scale_factor,
-            stacking_contexts: vec![stacking_context],
-            active_stacking_context_stack: vec![0],
+    pub fn new() -> Self {
+        let mut this = SceneBuilder {
+            stacking_contexts: Vec::new(),
+            active_stacking_context_stack: Vec::new(),
             #[cfg(debug_assertions)]
-            mouse_region_ids: Default::default(),
+            mouse_region_ids: HashSet::default(),
             event_handlers: Vec::new(),
-        }
+        };
+        this.clear();
+        this
     }
 
-    pub fn build(mut self) -> Scene {
-        self.stacking_contexts
-            .sort_by_key(|context| context.z_index);
-        Scene {
-            scale_factor: self.scale_factor,
-            stacking_contexts: self.stacking_contexts,
-            event_handlers: self.event_handlers,
-        }
+    pub fn clear(&mut self) {
+        self.stacking_contexts.clear();
+        self.stacking_contexts.push(StackingContext::new(None, 0));
+        self.active_stacking_context_stack.clear();
+        self.active_stacking_context_stack.push(0);
+        #[cfg(debug_assertions)]
+        self.mouse_region_ids.clear();
     }
 
-    pub fn scale_factor(&self) -> f32 {
-        self.scale_factor
-    }
+    pub fn build(&mut self, scale_factor: f32) -> Scene {
+        let mut stacking_contexts = std::mem::take(&mut self.stacking_contexts);
+        stacking_contexts.sort_by_key(|context| context.z_index);
+        let event_handlers = std::mem::take(&mut self.event_handlers);
+        self.clear();
 
-    pub fn paint_stacking_context<F>(
-        &mut self,
-        clip_bounds: Option<RectF>,
-        z_index: Option<usize>,
-        f: F,
-    ) where
-        F: FnOnce(&mut Self),
-    {
-        self.push_stacking_context(clip_bounds, z_index);
-        f(self);
-        self.pop_stacking_context();
+        Scene {
+            scale_factor,
+            stacking_contexts,
+            event_handlers,
+        }
     }
 
     pub fn push_stacking_context(&mut self, clip_bounds: Option<RectF>, z_index: Option<usize>) {
@@ -342,15 +291,6 @@ impl SceneBuilder {
         assert!(!self.active_stacking_context_stack.is_empty());
     }
 
-    pub fn paint_layer<F>(&mut self, clip_bounds: Option<RectF>, f: F)
-    where
-        F: FnOnce(&mut Self),
-    {
-        self.push_layer(clip_bounds);
-        f(self);
-        self.pop_layer();
-    }
-
     pub fn push_layer(&mut self, clip_bounds: Option<RectF>) {
         self.active_stacking_context().push_layer(clip_bounds);
     }
@@ -606,99 +546,6 @@ impl Layer {
     }
 }
 
-impl Border {
-    pub fn new(width: f32, color: Color) -> Self {
-        Self {
-            width,
-            color,
-            overlay: false,
-            top: false,
-            left: false,
-            bottom: false,
-            right: false,
-        }
-    }
-
-    pub fn all(width: f32, color: Color) -> Self {
-        Self {
-            width,
-            color,
-            overlay: false,
-            top: true,
-            left: true,
-            bottom: true,
-            right: true,
-        }
-    }
-
-    pub fn top(width: f32, color: Color) -> Self {
-        let mut border = Self::new(width, color);
-        border.top = true;
-        border
-    }
-
-    pub fn left(width: f32, color: Color) -> Self {
-        let mut border = Self::new(width, color);
-        border.left = true;
-        border
-    }
-
-    pub fn bottom(width: f32, color: Color) -> Self {
-        let mut border = Self::new(width, color);
-        border.bottom = true;
-        border
-    }
-
-    pub fn right(width: f32, color: Color) -> Self {
-        let mut border = Self::new(width, color);
-        border.right = true;
-        border
-    }
-
-    pub fn with_sides(mut self, top: bool, left: bool, bottom: bool, right: bool) -> Self {
-        self.top = top;
-        self.left = left;
-        self.bottom = bottom;
-        self.right = right;
-        self
-    }
-
-    pub fn top_width(&self) -> f32 {
-        if self.top {
-            self.width
-        } else {
-            0.0
-        }
-    }
-
-    pub fn left_width(&self) -> f32 {
-        if self.left {
-            self.width
-        } else {
-            0.0
-        }
-    }
-}
-
-impl ToJson for Border {
-    fn to_json(&self) -> serde_json::Value {
-        let mut value = json!({});
-        if self.top {
-            value["top"] = json!(self.width);
-        }
-        if self.right {
-            value["right"] = json!(self.width);
-        }
-        if self.bottom {
-            value["bottom"] = json!(self.width);
-        }
-        if self.left {
-            value["left"] = json!(self.width);
-        }
-        value
-    }
-}
-
 impl MouseRegion {
     pub fn id(&self) -> MouseRegionId {
         self.id

crates/gpui/src/text_layout.rs πŸ”—

@@ -9,7 +9,6 @@ use crate::{
     platform::FontSystem,
     scene,
     window::WindowContext,
-    SceneBuilder,
 };
 use ordered_float::OrderedFloat;
 use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
@@ -284,7 +283,6 @@ impl Line {
 
     pub fn paint(
         &self,
-        scene: &mut SceneBuilder,
         origin: Vector2F,
         visible_bounds: RectF,
         line_height: f32,
@@ -347,7 +345,7 @@ impl Line {
                 }
 
                 if let Some((underline_origin, underline_style)) = finished_underline {
-                    scene.push_underline(scene::Underline {
+                    cx.scene().push_underline(scene::Underline {
                         origin: underline_origin,
                         width: glyph_origin.x() - underline_origin.x(),
                         thickness: underline_style.thickness.into(),
@@ -357,14 +355,14 @@ impl Line {
                 }
 
                 if glyph.is_emoji {
-                    scene.push_image_glyph(scene::ImageGlyph {
+                    cx.scene().push_image_glyph(scene::ImageGlyph {
                         font_id: run.font_id,
                         font_size: self.layout.font_size,
                         id: glyph.id,
                         origin: glyph_origin,
                     });
                 } else {
-                    scene.push_glyph(scene::Glyph {
+                    cx.scene().push_glyph(scene::Glyph {
                         font_id: run.font_id,
                         font_size: self.layout.font_size,
                         id: glyph.id,
@@ -377,7 +375,7 @@ impl Line {
 
         if let Some((underline_start, underline_style)) = underline.take() {
             let line_end_x = origin.x() + self.layout.width;
-            scene.push_underline(scene::Underline {
+            cx.scene().push_underline(scene::Underline {
                 origin: underline_start,
                 width: line_end_x - underline_start.x(),
                 color: underline_style.color.unwrap(),
@@ -389,7 +387,6 @@ impl Line {
 
     pub fn paint_wrapped(
         &self,
-        scene: &mut SceneBuilder,
         origin: Vector2F,
         visible_bounds: RectF,
         line_height: f32,
@@ -417,7 +414,7 @@ impl Line {
                 {
                     boundaries.next();
                     if let Some((underline_origin, underline_style)) = underline {
-                        scene.push_underline(scene::Underline {
+                        cx.scene().push_underline(scene::Underline {
                             origin: underline_origin,
                             width: glyph_origin.x() - underline_origin.x(),
                             thickness: underline_style.thickness.into(),
@@ -461,7 +458,7 @@ impl Line {
                 }
 
                 if let Some((underline_origin, underline_style)) = finished_underline {
-                    scene.push_underline(scene::Underline {
+                    cx.scene().push_underline(scene::Underline {
                         origin: underline_origin,
                         width: glyph_origin.x() - underline_origin.x(),
                         thickness: underline_style.thickness.into(),
@@ -477,14 +474,14 @@ impl Line {
                 );
                 if glyph_bounds.intersects(visible_bounds) {
                     if glyph.is_emoji {
-                        scene.push_image_glyph(scene::ImageGlyph {
+                        cx.scene().push_image_glyph(scene::ImageGlyph {
                             font_id: run.font_id,
                             font_size: self.layout.font_size,
                             id: glyph.id,
                             origin: glyph_bounds.origin() + baseline_offset,
                         });
                     } else {
-                        scene.push_glyph(scene::Glyph {
+                        cx.scene().push_glyph(scene::Glyph {
                             font_id: run.font_id,
                             font_size: self.layout.font_size,
                             id: glyph.id,
@@ -498,7 +495,7 @@ impl Line {
 
         if let Some((underline_origin, underline_style)) = underline.take() {
             let line_end_x = glyph_origin.x() + self.layout.width - prev_position;
-            scene.push_underline(scene::Underline {
+            cx.scene().push_underline(scene::Underline {
                 origin: underline_origin,
                 width: line_end_x - underline_origin.x(),
                 thickness: underline_style.thickness.into(),

crates/gpui2/Cargo.toml πŸ”—

@@ -0,0 +1,32 @@
+[package]
+name = "gpui2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+name = "gpui2"
+path = "src/gpui2.rs"
+
+[features]
+test-support = ["gpui/test-support"]
+
+[dependencies]
+anyhow.workspace = true
+derive_more.workspace = true
+gpui = { path = "../gpui" }
+log.workspace = true
+futures.workspace = true
+gpui2_macros = { path = "../gpui2_macros" }
+parking_lot.workspace = true
+refineable.workspace = true
+rust-embed.workspace = true
+serde.workspace = true
+settings = { path = "../settings" }
+simplelog = "0.9"
+smallvec.workspace = true
+theme = { path = "../theme" }
+util = { path = "../util" }
+
+[dev-dependencies]
+gpui = { path = "../gpui", features = ["test-support"] }

crates/gpui/playground/src/adapter.rs β†’ crates/gpui2/src/adapter.rs πŸ”—

@@ -1,8 +1,8 @@
-use crate::{layout_context::LayoutContext, paint_context::PaintContext};
+use crate::ViewContext;
 use gpui::{geometry::rect::RectF, LayoutEngine, LayoutId};
 use util::ResultExt;
 
-/// Makes a new, playground-style element into a legacy element.
+/// Makes a new, gpui2-style element into a legacy element.
 pub struct AdapterElement<V>(pub(crate) crate::element::AnyElement<V>);
 
 impl<V: 'static> gpui::Element<V> for AdapterElement<V> {
@@ -13,12 +13,11 @@ impl<V: 'static> gpui::Element<V> for AdapterElement<V> {
         &mut self,
         constraint: gpui::SizeConstraint,
         view: &mut V,
-        cx: &mut gpui::LayoutContext<V>,
+        cx: &mut gpui::ViewContext<V>,
     ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
         cx.push_layout_engine(LayoutEngine::new());
 
-        let size = constraint.max;
-        let mut cx = LayoutContext::new(cx);
+        let mut cx = ViewContext::new(cx);
         let layout_id = self.0.layout(view, &mut cx).log_err();
         if let Some(layout_id) = layout_id {
             cx.layout_engine()
@@ -37,41 +36,40 @@ impl<V: 'static> gpui::Element<V> for AdapterElement<V> {
 
     fn paint(
         &mut self,
-        scene: &mut gpui::SceneBuilder,
         bounds: RectF,
-        visible_bounds: RectF,
+        _visible_bounds: RectF,
         layout_data: &mut Option<(LayoutEngine, LayoutId)>,
         view: &mut V,
-        legacy_cx: &mut gpui::PaintContext<V>,
+        cx: &mut gpui::ViewContext<V>,
     ) -> Self::PaintState {
         let (layout_engine, layout_id) = layout_data.take().unwrap();
-        legacy_cx.push_layout_engine(layout_engine);
-        let mut cx = PaintContext::new(legacy_cx, scene);
-        self.0.paint(view, &mut cx);
-        *layout_data = legacy_cx.pop_layout_engine().zip(Some(layout_id));
+        cx.push_layout_engine(layout_engine);
+        self.0
+            .paint(view, bounds.origin(), &mut ViewContext::new(cx));
+        *layout_data = cx.pop_layout_engine().zip(Some(layout_id));
         debug_assert!(layout_data.is_some());
     }
 
     fn rect_for_text_range(
         &self,
-        range_utf16: std::ops::Range<usize>,
-        bounds: RectF,
-        visible_bounds: RectF,
-        layout: &Self::LayoutState,
-        paint: &Self::PaintState,
-        view: &V,
-        cx: &gpui::ViewContext<V>,
+        _range_utf16: std::ops::Range<usize>,
+        _bounds: RectF,
+        _visible_bounds: RectF,
+        _layout: &Self::LayoutState,
+        _paint: &Self::PaintState,
+        _view: &V,
+        _cx: &gpui::ViewContext<V>,
     ) -> Option<RectF> {
         todo!("implement before merging to main")
     }
 
     fn debug(
         &self,
-        bounds: RectF,
-        layout: &Self::LayoutState,
-        paint: &Self::PaintState,
-        view: &V,
-        cx: &gpui::ViewContext<V>,
+        _bounds: RectF,
+        _layout: &Self::LayoutState,
+        _paint: &Self::PaintState,
+        _view: &V,
+        _cx: &gpui::ViewContext<V>,
     ) -> gpui::serde_json::Value {
         todo!("implement before merging to main")
     }

crates/gpui/playground/src/color.rs β†’ crates/gpui2/src/color.rs πŸ”—

@@ -1,8 +1,9 @@
 #![allow(dead_code)]
 
-use std::{num::ParseIntError, ops::Range};
-
+use serde::de::{self, Deserialize, Deserializer, Visitor};
 use smallvec::SmallVec;
+use std::fmt;
+use std::{num::ParseIntError, ops::Range};
 
 pub fn rgb<C: From<Rgba>>(hex: u32) -> C {
     let r = ((hex >> 16) & 0xFF) as f32 / 255.0;
@@ -19,6 +20,40 @@ pub struct Rgba {
     pub a: f32,
 }
 
+struct RgbaVisitor;
+
+impl<'de> Visitor<'de> for RgbaVisitor {
+    type Value = Rgba;
+
+    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+        formatter.write_str("a string in the format #rrggbb or #rrggbbaa")
+    }
+
+    fn visit_str<E: de::Error>(self, value: &str) -> Result<Rgba, E> {
+        if value.len() == 7 || value.len() == 9 {
+            let r = u8::from_str_radix(&value[1..3], 16).unwrap() as f32 / 255.0;
+            let g = u8::from_str_radix(&value[3..5], 16).unwrap() as f32 / 255.0;
+            let b = u8::from_str_radix(&value[5..7], 16).unwrap() as f32 / 255.0;
+            let a = if value.len() == 9 {
+                u8::from_str_radix(&value[7..9], 16).unwrap() as f32 / 255.0
+            } else {
+                1.0
+            };
+            Ok(Rgba { r, g, b, a })
+        } else {
+            Err(E::custom(
+                "Bad format for RGBA. Expected #rrggbb or #rrggbbaa.",
+            ))
+        }
+    }
+}
+
+impl<'de> Deserialize<'de> for Rgba {
+    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
+        deserializer.deserialize_str(RgbaVisitor)
+    }
+}
+
 pub trait Lerp {
     fn lerp(&self, level: f32) -> Hsla;
 }
@@ -219,6 +254,19 @@ impl Into<gpui::color::Color> for Hsla {
     }
 }
 
+impl<'de> Deserialize<'de> for Hsla {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        // First, deserialize it into Rgba
+        let rgba = Rgba::deserialize(deserializer)?;
+
+        // Then, use the From<Rgba> for Hsla implementation to convert it
+        Ok(Hsla::from(rgba))
+    }
+}
+
 pub struct ColorScale {
     colors: SmallVec<[Hsla; 2]>,
     positions: SmallVec<[f32; 2]>,

crates/gpui2/src/element.rs πŸ”—

@@ -0,0 +1,186 @@
+pub use crate::ViewContext;
+use anyhow::Result;
+use gpui::geometry::vector::Vector2F;
+pub use gpui::{Layout, LayoutId};
+use smallvec::SmallVec;
+
+pub trait Element<V: 'static>: 'static + IntoElement<V> {
+    type PaintState;
+
+    fn layout(
+        &mut self,
+        view: &mut V,
+        cx: &mut ViewContext<V>,
+    ) -> Result<(LayoutId, Self::PaintState)>
+    where
+        Self: Sized;
+
+    fn paint(
+        &mut self,
+        view: &mut V,
+        parent_origin: Vector2F,
+        layout: &Layout,
+        state: &mut Self::PaintState,
+        cx: &mut ViewContext<V>,
+    ) where
+        Self: Sized;
+
+    fn into_any(self) -> AnyElement<V>
+    where
+        Self: 'static + Sized,
+    {
+        AnyElement(Box::new(StatefulElement {
+            element: self,
+            phase: ElementPhase::Init,
+        }))
+    }
+}
+
+/// Used to make ElementState<V, E> into a trait object, so we can wrap it in AnyElement<V>.
+trait AnyStatefulElement<V> {
+    fn layout(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> Result<LayoutId>;
+    fn paint(&mut self, view: &mut V, parent_origin: Vector2F, cx: &mut ViewContext<V>);
+}
+
+/// A wrapper around an element that stores its layout state.
+struct StatefulElement<V: 'static, E: Element<V>> {
+    element: E,
+    phase: ElementPhase<V, E>,
+}
+
+enum ElementPhase<V: 'static, E: Element<V>> {
+    Init,
+    PostLayout {
+        layout_id: LayoutId,
+        paint_state: E::PaintState,
+    },
+    #[allow(dead_code)]
+    PostPaint {
+        layout: Layout,
+        paint_state: E::PaintState,
+    },
+    Error(String),
+}
+
+impl<V: 'static, E: Element<V>> std::fmt::Debug for ElementPhase<V, E> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            ElementPhase::Init => write!(f, "Init"),
+            ElementPhase::PostLayout { layout_id, .. } => {
+                write!(f, "PostLayout with layout id: {:?}", layout_id)
+            }
+            ElementPhase::PostPaint { layout, .. } => {
+                write!(f, "PostPaint with layout: {:?}", layout)
+            }
+            ElementPhase::Error(err) => write!(f, "Error: {}", err),
+        }
+    }
+}
+
+impl<V: 'static, E: Element<V>> Default for ElementPhase<V, E> {
+    fn default() -> Self {
+        Self::Init
+    }
+}
+
+/// We blanket-implement the object-safe ElementStateObject interface to make ElementStates into trait objects
+impl<V, E: Element<V>> AnyStatefulElement<V> for StatefulElement<V, E> {
+    fn layout(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> Result<LayoutId> {
+        let result;
+        self.phase = match self.element.layout(view, cx) {
+            Ok((layout_id, paint_state)) => {
+                result = Ok(layout_id);
+                ElementPhase::PostLayout {
+                    layout_id,
+                    paint_state,
+                }
+            }
+            Err(error) => {
+                let message = error.to_string();
+                result = Err(error);
+                ElementPhase::Error(message)
+            }
+        };
+        result
+    }
+
+    fn paint(&mut self, view: &mut V, parent_origin: Vector2F, cx: &mut ViewContext<V>) {
+        self.phase = match std::mem::take(&mut self.phase) {
+            ElementPhase::PostLayout {
+                layout_id,
+                mut paint_state,
+            } => match cx.computed_layout(layout_id) {
+                Ok(layout) => {
+                    self.element
+                        .paint(view, parent_origin, &layout, &mut paint_state, cx);
+                    ElementPhase::PostPaint {
+                        layout,
+                        paint_state,
+                    }
+                }
+                Err(error) => ElementPhase::Error(error.to_string()),
+            },
+            ElementPhase::PostPaint {
+                layout,
+                mut paint_state,
+            } => {
+                self.element
+                    .paint(view, parent_origin, &layout, &mut paint_state, cx);
+                ElementPhase::PostPaint {
+                    layout,
+                    paint_state,
+                }
+            }
+            phase @ ElementPhase::Error(_) => phase,
+
+            phase @ _ => {
+                panic!("invalid element phase to call paint: {:?}", phase);
+            }
+        };
+    }
+}
+
+/// A dynamic element.
+pub struct AnyElement<V>(Box<dyn AnyStatefulElement<V>>);
+
+impl<V> AnyElement<V> {
+    pub fn layout(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> Result<LayoutId> {
+        self.0.layout(view, cx)
+    }
+
+    pub fn paint(&mut self, view: &mut V, parent_origin: Vector2F, cx: &mut ViewContext<V>) {
+        self.0.paint(view, parent_origin, cx)
+    }
+}
+
+pub trait ParentElement<V: 'static> {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]>;
+
+    fn child(mut self, child: impl IntoElement<V>) -> Self
+    where
+        Self: Sized,
+    {
+        self.children_mut().push(child.into_element().into_any());
+        self
+    }
+
+    fn children<I, E>(mut self, children: I) -> Self
+    where
+        I: IntoIterator<Item = E>,
+        E: IntoElement<V>,
+        Self: Sized,
+    {
+        self.children_mut().extend(
+            children
+                .into_iter()
+                .map(|child| child.into_element().into_any()),
+        );
+        self
+    }
+}
+
+pub trait IntoElement<V: 'static> {
+    type Element: Element<V>;
+
+    fn into_element(self) -> Self::Element;
+}

crates/gpui2/src/elements.rs πŸ”—

@@ -0,0 +1,10 @@
+pub mod div;
+pub mod hoverable;
+mod img;
+pub mod pressable;
+pub mod svg;
+pub mod text;
+
+pub use div::div;
+pub use img::img;
+pub use svg::svg;

crates/gpui2/src/elements/div.rs πŸ”—

@@ -0,0 +1,320 @@
+use std::{cell::Cell, rc::Rc};
+
+use crate::{
+    element::{AnyElement, Element, IntoElement, Layout, ParentElement},
+    hsla,
+    style::{CornerRadii, Overflow, Style, StyleHelpers, Styleable},
+    InteractionHandlers, Interactive, ViewContext,
+};
+use anyhow::Result;
+use gpui::{
+    geometry::{rect::RectF, vector::Vector2F, Point},
+    platform::{MouseButton, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent},
+    scene::{self},
+    LayoutId,
+};
+use refineable::{Refineable, RefinementCascade};
+use smallvec::SmallVec;
+use util::ResultExt;
+
+pub struct Div<V: 'static> {
+    styles: RefinementCascade<Style>,
+    handlers: InteractionHandlers<V>,
+    children: SmallVec<[AnyElement<V>; 2]>,
+    scroll_state: Option<ScrollState>,
+}
+
+pub fn div<V>() -> Div<V> {
+    Div {
+        styles: Default::default(),
+        handlers: Default::default(),
+        children: Default::default(),
+        scroll_state: None,
+    }
+}
+
+impl<V: 'static> Element<V> for Div<V> {
+    type PaintState = Vec<LayoutId>;
+
+    fn layout(
+        &mut self,
+        view: &mut V,
+        cx: &mut ViewContext<V>,
+    ) -> Result<(LayoutId, Self::PaintState)>
+    where
+        Self: Sized,
+    {
+        let style = self.computed_style();
+        let pop_text_style = style.text_style(cx).map_or(false, |style| {
+            cx.push_text_style(&style).log_err().is_some()
+        });
+
+        let children = self
+            .children
+            .iter_mut()
+            .map(|child| child.layout(view, cx))
+            .collect::<Result<Vec<LayoutId>>>()?;
+
+        if pop_text_style {
+            cx.pop_text_style();
+        }
+
+        Ok((cx.add_layout_node(style, children.clone())?, children))
+    }
+
+    fn paint(
+        &mut self,
+        view: &mut V,
+        parent_origin: Vector2F,
+        layout: &Layout,
+        child_layouts: &mut Vec<LayoutId>,
+        cx: &mut ViewContext<V>,
+    ) where
+        Self: Sized,
+    {
+        let order = layout.order;
+        let bounds = layout.bounds + parent_origin;
+
+        let style = self.computed_style();
+        let pop_text_style = style.text_style(cx).map_or(false, |style| {
+            cx.push_text_style(&style).log_err().is_some()
+        });
+        style.paint_background(bounds, cx);
+        self.interaction_handlers().paint(order, bounds, cx);
+
+        let scrolled_origin = bounds.origin() - self.scroll_offset(&style.overflow);
+
+        // TODO: Support only one dimension being hidden
+        let mut pop_layer = false;
+        if style.overflow.y != Overflow::Visible || style.overflow.x != Overflow::Visible {
+            cx.scene().push_layer(Some(bounds));
+            pop_layer = true;
+        }
+
+        for child in &mut self.children {
+            child.paint(view, scrolled_origin, cx);
+        }
+
+        if pop_layer {
+            cx.scene().pop_layer();
+        }
+
+        style.paint_foreground(bounds, cx);
+        if pop_text_style {
+            cx.pop_text_style();
+        }
+
+        self.handle_scroll(order, bounds, style.overflow.clone(), child_layouts, cx);
+
+        if cx.is_inspector_enabled() {
+            self.paint_inspector(parent_origin, layout, cx);
+        }
+    }
+}
+
+impl<V: 'static> Div<V> {
+    pub fn overflow_hidden(mut self) -> Self {
+        self.declared_style().overflow.x = Some(Overflow::Hidden);
+        self.declared_style().overflow.y = Some(Overflow::Hidden);
+        self
+    }
+
+    pub fn overflow_hidden_x(mut self) -> Self {
+        self.declared_style().overflow.x = Some(Overflow::Hidden);
+        self
+    }
+
+    pub fn overflow_hidden_y(mut self) -> Self {
+        self.declared_style().overflow.y = Some(Overflow::Hidden);
+        self
+    }
+
+    pub fn overflow_scroll(mut self, scroll_state: ScrollState) -> Self {
+        self.scroll_state = Some(scroll_state);
+        self.declared_style().overflow.x = Some(Overflow::Scroll);
+        self.declared_style().overflow.y = Some(Overflow::Scroll);
+        self
+    }
+
+    pub fn overflow_x_scroll(mut self, scroll_state: ScrollState) -> Self {
+        self.scroll_state = Some(scroll_state);
+        self.declared_style().overflow.x = Some(Overflow::Scroll);
+        self
+    }
+
+    pub fn overflow_y_scroll(mut self, scroll_state: ScrollState) -> Self {
+        self.scroll_state = Some(scroll_state);
+        self.declared_style().overflow.y = Some(Overflow::Scroll);
+        self
+    }
+
+    fn scroll_offset(&self, overflow: &Point<Overflow>) -> Vector2F {
+        let mut offset = Vector2F::zero();
+        if overflow.y == Overflow::Scroll {
+            offset.set_y(self.scroll_state.as_ref().unwrap().y());
+        }
+        if overflow.x == Overflow::Scroll {
+            offset.set_x(self.scroll_state.as_ref().unwrap().x());
+        }
+
+        offset
+    }
+
+    fn handle_scroll(
+        &mut self,
+        order: u32,
+        bounds: RectF,
+        overflow: Point<Overflow>,
+        child_layout_ids: &[LayoutId],
+        cx: &mut ViewContext<V>,
+    ) {
+        if overflow.y == Overflow::Scroll || overflow.x == Overflow::Scroll {
+            let mut scroll_max = Vector2F::zero();
+            for child_layout_id in child_layout_ids {
+                if let Some(child_layout) = cx
+                    .layout_engine()
+                    .unwrap()
+                    .computed_layout(*child_layout_id)
+                    .log_err()
+                {
+                    scroll_max = scroll_max.max(child_layout.bounds.lower_right());
+                }
+            }
+            scroll_max -= bounds.size();
+
+            let scroll_state = self.scroll_state.as_ref().unwrap().clone();
+            cx.on_event(order, move |_, event: &ScrollWheelEvent, cx| {
+                if bounds.contains_point(event.position) {
+                    let scroll_delta = match event.delta {
+                        gpui::platform::ScrollDelta::Pixels(delta) => delta,
+                        gpui::platform::ScrollDelta::Lines(delta) => {
+                            delta * cx.text_style().font_size
+                        }
+                    };
+                    if overflow.x == Overflow::Scroll {
+                        scroll_state.set_x(
+                            (scroll_state.x() - scroll_delta.x())
+                                .max(0.)
+                                .min(scroll_max.x()),
+                        );
+                    }
+                    if overflow.y == Overflow::Scroll {
+                        scroll_state.set_y(
+                            (scroll_state.y() - scroll_delta.y())
+                                .max(0.)
+                                .min(scroll_max.y()),
+                        );
+                    }
+                    cx.repaint();
+                } else {
+                    cx.bubble_event();
+                }
+            })
+        }
+    }
+
+    fn paint_inspector(&self, parent_origin: Vector2F, layout: &Layout, cx: &mut ViewContext<V>) {
+        let style = self.styles.merged();
+        let bounds = layout.bounds + parent_origin;
+
+        let hovered = bounds.contains_point(cx.mouse_position());
+        if hovered {
+            let rem_size = cx.rem_size();
+            cx.scene().push_quad(scene::Quad {
+                bounds,
+                background: Some(hsla(0., 0., 1., 0.05).into()),
+                border: gpui::Border {
+                    color: hsla(0., 0., 1., 0.2).into(),
+                    top: 1.,
+                    right: 1.,
+                    bottom: 1.,
+                    left: 1.,
+                },
+                corner_radii: CornerRadii::default()
+                    .refined(&style.corner_radii)
+                    .to_gpui(bounds.size(), rem_size),
+            })
+        }
+
+        let pressed = Cell::new(hovered && cx.is_mouse_down(MouseButton::Left));
+        cx.on_event(layout.order, move |_, event: &MouseButtonEvent, _| {
+            if bounds.contains_point(event.position) {
+                if event.is_down {
+                    pressed.set(true);
+                } else if pressed.get() {
+                    pressed.set(false);
+                    eprintln!("clicked div {:?} {:#?}", bounds, style);
+                }
+            }
+        });
+
+        let hovered = Cell::new(hovered);
+        cx.on_event(layout.order, move |_, event: &MouseMovedEvent, cx| {
+            cx.bubble_event();
+            let hovered_now = bounds.contains_point(event.position);
+            if hovered.get() != hovered_now {
+                hovered.set(hovered_now);
+                cx.repaint();
+            }
+        });
+    }
+}
+
+impl<V> Styleable for Div<V> {
+    type Style = Style;
+
+    fn style_cascade(&mut self) -> &mut RefinementCascade<Self::Style> {
+        &mut self.styles
+    }
+
+    fn declared_style(&mut self) -> &mut <Self::Style as Refineable>::Refinement {
+        self.styles.base()
+    }
+}
+
+impl<V> StyleHelpers for Div<V> {}
+
+impl<V> Interactive<V> for Div<V> {
+    fn interaction_handlers(&mut self) -> &mut InteractionHandlers<V> {
+        &mut self.handlers
+    }
+}
+
+impl<V: 'static> ParentElement<V> for Div<V> {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
+        &mut self.children
+    }
+}
+
+impl<V: 'static> IntoElement<V> for Div<V> {
+    type Element = Self;
+
+    fn into_element(self) -> Self::Element {
+        self
+    }
+}
+
+#[derive(Default, Clone)]
+pub struct ScrollState(Rc<Cell<Vector2F>>);
+
+impl ScrollState {
+    pub fn x(&self) -> f32 {
+        self.0.get().x()
+    }
+
+    pub fn set_x(&self, value: f32) {
+        let mut current_value = self.0.get();
+        current_value.set_x(value);
+        self.0.set(current_value);
+    }
+
+    pub fn y(&self) -> f32 {
+        self.0.get().y()
+    }
+
+    pub fn set_y(&self, value: f32) {
+        let mut current_value = self.0.get();
+        current_value.set_y(value);
+        self.0.set(current_value);
+    }
+}

crates/gpui2/src/elements/hoverable.rs πŸ”—

@@ -0,0 +1,105 @@
+use crate::{
+    element::{AnyElement, Element, IntoElement, Layout, ParentElement},
+    interactive::{InteractionHandlers, Interactive},
+    style::{Style, StyleHelpers, Styleable},
+    ViewContext,
+};
+use anyhow::Result;
+use gpui::{geometry::vector::Vector2F, platform::MouseMovedEvent, LayoutId};
+use refineable::{CascadeSlot, Refineable, RefinementCascade};
+use smallvec::SmallVec;
+use std::{cell::Cell, rc::Rc};
+
+pub struct Hoverable<E: Styleable> {
+    hovered: Rc<Cell<bool>>,
+    cascade_slot: CascadeSlot,
+    hovered_style: <E::Style as Refineable>::Refinement,
+    child: E,
+}
+
+pub fn hoverable<E: Styleable>(mut child: E) -> Hoverable<E> {
+    Hoverable {
+        hovered: Rc::new(Cell::new(false)),
+        cascade_slot: child.style_cascade().reserve(),
+        hovered_style: Default::default(),
+        child,
+    }
+}
+
+impl<E: Styleable> Styleable for Hoverable<E> {
+    type Style = E::Style;
+
+    fn style_cascade(&mut self) -> &mut RefinementCascade<Self::Style> {
+        self.child.style_cascade()
+    }
+
+    fn declared_style(&mut self) -> &mut <Self::Style as Refineable>::Refinement {
+        &mut self.hovered_style
+    }
+}
+
+impl<V: 'static, E: Element<V> + Styleable> Element<V> for Hoverable<E> {
+    type PaintState = E::PaintState;
+
+    fn layout(
+        &mut self,
+        view: &mut V,
+        cx: &mut ViewContext<V>,
+    ) -> Result<(LayoutId, Self::PaintState)>
+    where
+        Self: Sized,
+    {
+        Ok(self.child.layout(view, cx)?)
+    }
+
+    fn paint(
+        &mut self,
+        view: &mut V,
+        parent_origin: Vector2F,
+        layout: &Layout,
+        paint_state: &mut Self::PaintState,
+        cx: &mut ViewContext<V>,
+    ) where
+        Self: Sized,
+    {
+        let bounds = layout.bounds + parent_origin;
+        self.hovered.set(bounds.contains_point(cx.mouse_position()));
+
+        let slot = self.cascade_slot;
+        let style = self.hovered.get().then_some(self.hovered_style.clone());
+        self.style_cascade().set(slot, style);
+
+        let hovered = self.hovered.clone();
+        cx.on_event(layout.order, move |_view, _: &MouseMovedEvent, cx| {
+            cx.bubble_event();
+            if bounds.contains_point(cx.mouse_position()) != hovered.get() {
+                cx.repaint();
+            }
+        });
+
+        self.child
+            .paint(view, parent_origin, layout, paint_state, cx);
+    }
+}
+
+impl<E: Styleable<Style = Style>> StyleHelpers for Hoverable<E> {}
+
+impl<V: 'static, E: Interactive<V> + Styleable> Interactive<V> for Hoverable<E> {
+    fn interaction_handlers(&mut self) -> &mut InteractionHandlers<V> {
+        self.child.interaction_handlers()
+    }
+}
+
+impl<V: 'static, E: ParentElement<V> + Styleable> ParentElement<V> for Hoverable<E> {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
+        self.child.children_mut()
+    }
+}
+
+impl<V: 'static, E: Element<V> + Styleable> IntoElement<V> for Hoverable<E> {
+    type Element = Self;
+
+    fn into_element(self) -> Self::Element {
+        self
+    }
+}

crates/gpui2/src/elements/img.rs πŸ”—

@@ -0,0 +1,110 @@
+use crate as gpui2;
+use crate::{
+    style::{Style, StyleHelpers, Styleable},
+    Element,
+};
+use futures::FutureExt;
+use gpui::geometry::vector::Vector2F;
+use gpui::scene;
+use gpui2_macros::IntoElement;
+use refineable::RefinementCascade;
+use util::arc_cow::ArcCow;
+use util::ResultExt;
+
+#[derive(IntoElement)]
+pub struct Img {
+    style: RefinementCascade<Style>,
+    uri: Option<ArcCow<'static, str>>,
+}
+
+pub fn img() -> Img {
+    Img {
+        style: RefinementCascade::default(),
+        uri: None,
+    }
+}
+
+impl Img {
+    pub fn uri(mut self, uri: impl Into<ArcCow<'static, str>>) -> Self {
+        self.uri = Some(uri.into());
+        self
+    }
+}
+
+impl<V: 'static> Element<V> for Img {
+    type PaintState = ();
+
+    fn layout(
+        &mut self,
+        _: &mut V,
+        cx: &mut crate::ViewContext<V>,
+    ) -> anyhow::Result<(gpui::LayoutId, Self::PaintState)>
+    where
+        Self: Sized,
+    {
+        let style = self.computed_style();
+        let layout_id = cx.add_layout_node(style, [])?;
+        Ok((layout_id, ()))
+    }
+
+    fn paint(
+        &mut self,
+        _: &mut V,
+        parent_origin: Vector2F,
+        layout: &gpui::Layout,
+        _: &mut Self::PaintState,
+        cx: &mut crate::ViewContext<V>,
+    ) where
+        Self: Sized,
+    {
+        let style = self.computed_style();
+        let bounds = layout.bounds + parent_origin;
+
+        style.paint_background(bounds, cx);
+
+        if let Some(uri) = &self.uri {
+            let image_future = cx.image_cache.get(uri.clone());
+            if let Some(data) = image_future
+                .clone()
+                .now_or_never()
+                .and_then(ResultExt::log_err)
+            {
+                let rem_size = cx.rem_size();
+                cx.scene().push_image(scene::Image {
+                    bounds,
+                    border: gpui::Border {
+                        color: style.border_color.unwrap_or_default().into(),
+                        top: style.border_widths.top.to_pixels(rem_size),
+                        right: style.border_widths.right.to_pixels(rem_size),
+                        bottom: style.border_widths.bottom.to_pixels(rem_size),
+                        left: style.border_widths.left.to_pixels(rem_size),
+                    },
+                    corner_radii: style.corner_radii.to_gpui(bounds.size(), rem_size),
+                    grayscale: false,
+                    data,
+                })
+            } else {
+                cx.spawn(|this, mut cx| async move {
+                    if image_future.await.log_err().is_some() {
+                        this.update(&mut cx, |_, cx| cx.notify()).ok();
+                    }
+                })
+                .detach();
+            }
+        }
+    }
+}
+
+impl Styleable for Img {
+    type Style = Style;
+
+    fn style_cascade(&mut self) -> &mut RefinementCascade<Self::Style> {
+        &mut self.style
+    }
+
+    fn declared_style(&mut self) -> &mut <Self::Style as refineable::Refineable>::Refinement {
+        self.style.base()
+    }
+}
+
+impl StyleHelpers for Img {}

crates/gpui2/src/elements/pressable.rs πŸ”—

@@ -0,0 +1,108 @@
+use crate::{
+    element::{AnyElement, Element, IntoElement, Layout, ParentElement},
+    interactive::{InteractionHandlers, Interactive},
+    style::{Style, StyleHelpers, Styleable},
+    ViewContext,
+};
+use anyhow::Result;
+use gpui::{geometry::vector::Vector2F, platform::MouseButtonEvent, LayoutId};
+use refineable::{CascadeSlot, Refineable, RefinementCascade};
+use smallvec::SmallVec;
+use std::{cell::Cell, rc::Rc};
+
+pub struct Pressable<E: Styleable> {
+    pressed: Rc<Cell<bool>>,
+    pressed_style: <E::Style as Refineable>::Refinement,
+    cascade_slot: CascadeSlot,
+    child: E,
+}
+
+pub fn pressable<E: Styleable>(mut child: E) -> Pressable<E> {
+    Pressable {
+        pressed: Rc::new(Cell::new(false)),
+        pressed_style: Default::default(),
+        cascade_slot: child.style_cascade().reserve(),
+        child,
+    }
+}
+
+impl<E: Styleable> Styleable for Pressable<E> {
+    type Style = E::Style;
+
+    fn declared_style(&mut self) -> &mut <Self::Style as Refineable>::Refinement {
+        &mut self.pressed_style
+    }
+
+    fn style_cascade(&mut self) -> &mut RefinementCascade<E::Style> {
+        self.child.style_cascade()
+    }
+}
+
+impl<V: 'static, E: Element<V> + Styleable> Element<V> for Pressable<E> {
+    type PaintState = E::PaintState;
+
+    fn layout(
+        &mut self,
+        view: &mut V,
+        cx: &mut ViewContext<V>,
+    ) -> Result<(LayoutId, Self::PaintState)>
+    where
+        Self: Sized,
+    {
+        self.child.layout(view, cx)
+    }
+
+    fn paint(
+        &mut self,
+        view: &mut V,
+        parent_origin: Vector2F,
+        layout: &Layout,
+        paint_state: &mut Self::PaintState,
+        cx: &mut ViewContext<V>,
+    ) where
+        Self: Sized,
+    {
+        let slot = self.cascade_slot;
+        let style = self.pressed.get().then_some(self.pressed_style.clone());
+        self.style_cascade().set(slot, style);
+
+        let pressed = self.pressed.clone();
+        let bounds = layout.bounds + parent_origin;
+        cx.on_event(layout.order, move |_view, event: &MouseButtonEvent, cx| {
+            if event.is_down {
+                if bounds.contains_point(event.position) {
+                    pressed.set(true);
+                    cx.repaint();
+                }
+            } else if pressed.get() {
+                pressed.set(false);
+                cx.repaint();
+            }
+        });
+
+        self.child
+            .paint(view, parent_origin, layout, paint_state, cx);
+    }
+}
+
+impl<E: Styleable<Style = Style>> StyleHelpers for Pressable<E> {}
+
+impl<V: 'static, E: Interactive<V> + Styleable> Interactive<V> for Pressable<E> {
+    fn interaction_handlers(&mut self) -> &mut InteractionHandlers<V> {
+        self.child.interaction_handlers()
+    }
+}
+
+impl<V: 'static, E: ParentElement<V> + Styleable> ParentElement<V> for Pressable<E> {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
+        self.child.children_mut()
+    }
+}
+
+impl<V: 'static, E: Element<V> + Styleable> IntoElement<V> for Pressable<E> {
+    type Element = Self;
+
+    fn into_element(self) -> Self::Element {
+        self
+    }
+}

crates/gpui2/src/elements/svg.rs πŸ”—

@@ -0,0 +1,84 @@
+use crate::{
+    self as gpui2, scene,
+    style::{Style, StyleHelpers, Styleable},
+    Element, IntoElement, Layout, LayoutId, Rgba,
+};
+use gpui::geometry::vector::Vector2F;
+use refineable::RefinementCascade;
+use std::borrow::Cow;
+use util::ResultExt;
+
+#[derive(IntoElement)]
+pub struct Svg {
+    path: Option<Cow<'static, str>>,
+    style: RefinementCascade<Style>,
+}
+
+pub fn svg() -> Svg {
+    Svg {
+        path: None,
+        style: RefinementCascade::<Style>::default(),
+    }
+}
+
+impl Svg {
+    pub fn path(mut self, path: impl Into<Cow<'static, str>>) -> Self {
+        self.path = Some(path.into());
+        self
+    }
+}
+
+impl<V: 'static> Element<V> for Svg {
+    type PaintState = ();
+
+    fn layout(
+        &mut self,
+        _: &mut V,
+        cx: &mut crate::ViewContext<V>,
+    ) -> anyhow::Result<(LayoutId, Self::PaintState)>
+    where
+        Self: Sized,
+    {
+        let style = self.computed_style();
+        Ok((cx.add_layout_node(style, [])?, ()))
+    }
+
+    fn paint(
+        &mut self,
+        _: &mut V,
+        parent_origin: Vector2F,
+        layout: &Layout,
+        _: &mut Self::PaintState,
+        cx: &mut crate::ViewContext<V>,
+    ) where
+        Self: Sized,
+    {
+        let fill_color = self.computed_style().fill.and_then(|fill| fill.color());
+        if let Some((path, fill_color)) = self.path.as_ref().zip(fill_color) {
+            if let Some(svg_tree) = cx.asset_cache.svg(path).log_err() {
+                let icon = scene::Icon {
+                    bounds: layout.bounds + parent_origin,
+                    svg: svg_tree,
+                    path: path.clone(),
+                    color: Rgba::from(fill_color).into(),
+                };
+
+                cx.scene().push_icon(icon);
+            }
+        }
+    }
+}
+
+impl Styleable for Svg {
+    type Style = Style;
+
+    fn style_cascade(&mut self) -> &mut refineable::RefinementCascade<Self::Style> {
+        &mut self.style
+    }
+
+    fn declared_style(&mut self) -> &mut <Self::Style as refineable::Refineable>::Refinement {
+        self.style.base()
+    }
+}
+
+impl StyleHelpers for Svg {}

crates/gpui2/src/elements/text.rs πŸ”—

@@ -0,0 +1,109 @@
+use crate::{
+    element::{Element, IntoElement, Layout},
+    ViewContext,
+};
+use anyhow::Result;
+use gpui::{
+    geometry::{vector::Vector2F, Size},
+    text_layout::LineLayout,
+    LayoutId,
+};
+use parking_lot::Mutex;
+use std::sync::Arc;
+use util::arc_cow::ArcCow;
+
+impl<V: 'static, S: Into<ArcCow<'static, str>>> IntoElement<V> for S {
+    type Element = Text;
+
+    fn into_element(self) -> Self::Element {
+        Text { text: self.into() }
+    }
+}
+
+pub struct Text {
+    text: ArcCow<'static, str>,
+}
+
+impl<V: 'static> Element<V> for Text {
+    type PaintState = Arc<Mutex<Option<TextLayout>>>;
+
+    fn layout(
+        &mut self,
+        _view: &mut V,
+        cx: &mut ViewContext<V>,
+    ) -> Result<(LayoutId, Self::PaintState)> {
+        let fonts = cx.platform().fonts();
+        let text_style = cx.text_style();
+        let line_height = cx.font_cache().line_height(text_style.font_size);
+        let text = self.text.clone();
+        let paint_state = Arc::new(Mutex::new(None));
+
+        let layout_id = cx.add_measured_layout_node(Default::default(), {
+            let paint_state = paint_state.clone();
+            move |_params| {
+                let line_layout = fonts.layout_line(
+                    text.as_ref(),
+                    text_style.font_size,
+                    &[(text.len(), text_style.to_run())],
+                );
+
+                let size = Size {
+                    width: line_layout.width,
+                    height: line_height,
+                };
+
+                paint_state.lock().replace(TextLayout {
+                    line_layout: Arc::new(line_layout),
+                    line_height,
+                });
+
+                size
+            }
+        });
+
+        Ok((layout_id?, paint_state))
+    }
+
+    fn paint<'a>(
+        &mut self,
+        _view: &mut V,
+        parent_origin: Vector2F,
+        layout: &Layout,
+        paint_state: &mut Self::PaintState,
+        cx: &mut ViewContext<V>,
+    ) {
+        let bounds = layout.bounds + parent_origin;
+
+        let line_layout;
+        let line_height;
+        {
+            let paint_state = paint_state.lock();
+            let paint_state = paint_state
+                .as_ref()
+                .expect("measurement has not been performed");
+            line_layout = paint_state.line_layout.clone();
+            line_height = paint_state.line_height;
+        }
+
+        let text_style = cx.text_style();
+        let line =
+            gpui::text_layout::Line::new(line_layout, &[(self.text.len(), text_style.to_run())]);
+
+        // TODO: We haven't added visible bounds to the new element system yet, so this is a placeholder.
+        let visible_bounds = bounds;
+        line.paint(bounds.origin(), visible_bounds, line_height, cx.legacy_cx);
+    }
+}
+
+impl<V: 'static> IntoElement<V> for Text {
+    type Element = Self;
+
+    fn into_element(self) -> Self::Element {
+        self
+    }
+}
+
+pub struct TextLayout {
+    line_layout: Arc<LineLayout>,
+    line_height: f32,
+}

crates/gpui2/src/gpui2.rs πŸ”—

@@ -0,0 +1,22 @@
+pub mod adapter;
+pub mod color;
+pub mod element;
+pub mod elements;
+pub mod interactive;
+pub mod style;
+pub mod view;
+pub mod view_context;
+
+pub use color::*;
+pub use element::{AnyElement, Element, IntoElement, Layout, ParentElement};
+pub use geometry::{
+    rect::RectF,
+    vector::{vec2f, Vector2F},
+};
+pub use gpui::*;
+pub use gpui2_macros::{Element, *};
+pub use interactive::*;
+pub use platform::{Platform, WindowBounds, WindowOptions};
+pub use util::arc_cow::ArcCow;
+pub use view::*;
+pub use view_context::ViewContext;

crates/gpui2/src/interactive.rs πŸ”—

@@ -0,0 +1,165 @@
+use gpui::{
+    geometry::rect::RectF,
+    platform::{MouseButton, MouseButtonEvent},
+    EventContext,
+};
+use smallvec::SmallVec;
+use std::{cell::Cell, rc::Rc};
+
+use crate::ViewContext;
+
+pub trait Interactive<V: 'static> {
+    fn interaction_handlers(&mut self) -> &mut InteractionHandlers<V>;
+
+    fn on_mouse_down(
+        mut self,
+        button: MouseButton,
+        handler: impl Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interaction_handlers()
+            .mouse_down
+            .push(Rc::new(move |view, event, cx| {
+                if event.button == button {
+                    handler(view, event, cx)
+                }
+            }));
+        self
+    }
+
+    fn on_mouse_up(
+        mut self,
+        button: MouseButton,
+        handler: impl Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interaction_handlers()
+            .mouse_up
+            .push(Rc::new(move |view, event, cx| {
+                if event.button == button {
+                    handler(view, event, cx)
+                }
+            }));
+        self
+    }
+
+    fn on_mouse_down_out(
+        mut self,
+        button: MouseButton,
+        handler: impl Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interaction_handlers()
+            .mouse_down_out
+            .push(Rc::new(move |view, event, cx| {
+                if event.button == button {
+                    handler(view, event, cx)
+                }
+            }));
+        self
+    }
+
+    fn on_mouse_up_out(
+        mut self,
+        button: MouseButton,
+        handler: impl Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interaction_handlers()
+            .mouse_up_out
+            .push(Rc::new(move |view, event, cx| {
+                if event.button == button {
+                    handler(view, event, cx)
+                }
+            }));
+        self
+    }
+
+    fn on_click(
+        self,
+        button: MouseButton,
+        handler: impl Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        let pressed = Rc::new(Cell::new(false));
+        self.on_mouse_down(button, {
+            let pressed = pressed.clone();
+            move |_, _, _| {
+                pressed.set(true);
+            }
+        })
+        .on_mouse_up_out(button, {
+            let pressed = pressed.clone();
+            move |_, _, _| {
+                pressed.set(false);
+            }
+        })
+        .on_mouse_up(button, move |view, event, cx| {
+            if pressed.get() {
+                pressed.set(false);
+                handler(view, event, cx);
+            }
+        })
+    }
+}
+
+pub struct InteractionHandlers<V: 'static> {
+    mouse_down: SmallVec<[Rc<dyn Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>)>; 2]>,
+    mouse_down_out: SmallVec<[Rc<dyn Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>)>; 2]>,
+    mouse_up: SmallVec<[Rc<dyn Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>)>; 2]>,
+    mouse_up_out: SmallVec<[Rc<dyn Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>)>; 2]>,
+}
+
+impl<V: 'static> InteractionHandlers<V> {
+    pub fn paint(&self, order: u32, bounds: RectF, cx: &mut ViewContext<V>) {
+        for handler in self.mouse_down.iter().cloned() {
+            cx.on_event(order, move |view, event: &MouseButtonEvent, cx| {
+                if event.is_down && bounds.contains_point(event.position) {
+                    handler(view, event, cx);
+                }
+            })
+        }
+        for handler in self.mouse_up.iter().cloned() {
+            cx.on_event(order, move |view, event: &MouseButtonEvent, cx| {
+                if !event.is_down && bounds.contains_point(event.position) {
+                    handler(view, event, cx);
+                }
+            })
+        }
+        for handler in self.mouse_down_out.iter().cloned() {
+            cx.on_event(order, move |view, event: &MouseButtonEvent, cx| {
+                if event.is_down && !bounds.contains_point(event.position) {
+                    handler(view, event, cx);
+                }
+            })
+        }
+        for handler in self.mouse_up_out.iter().cloned() {
+            cx.on_event(order, move |view, event: &MouseButtonEvent, cx| {
+                if !event.is_down && !bounds.contains_point(event.position) {
+                    handler(view, event, cx);
+                }
+            })
+        }
+    }
+}
+
+impl<V> Default for InteractionHandlers<V> {
+    fn default() -> Self {
+        Self {
+            mouse_down: Default::default(),
+            mouse_up: Default::default(),
+            mouse_down_out: Default::default(),
+            mouse_up_out: Default::default(),
+        }
+    }
+}

crates/gpui2/src/style.rs πŸ”—

@@ -0,0 +1,604 @@
+use crate::{
+    color::Hsla,
+    elements::hoverable::{hoverable, Hoverable},
+    elements::pressable::{pressable, Pressable},
+    ViewContext,
+};
+pub use fonts::Style as FontStyle;
+pub use fonts::Weight as FontWeight;
+pub use gpui::taffy::style::{
+    AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap, JustifyContent,
+    Overflow, Position,
+};
+use gpui::{
+    fonts::{self, TextStyleRefinement},
+    geometry::{
+        rect::RectF, relative, vector::Vector2F, AbsoluteLength, DefiniteLength, Edges,
+        EdgesRefinement, Length, Point, PointRefinement, Size, SizeRefinement,
+    },
+    scene, taffy, WindowContext,
+};
+use gpui2_macros::styleable_helpers;
+use refineable::{Refineable, RefinementCascade};
+use std::sync::Arc;
+
+#[derive(Clone, Refineable, Debug)]
+#[refineable(debug)]
+pub struct Style {
+    /// What layout strategy should be used?
+    pub display: Display,
+
+    // Overflow properties
+    /// How children overflowing their container should affect layout
+    #[refineable]
+    pub overflow: Point<Overflow>,
+    /// How much space (in points) should be reserved for the scrollbars of `Overflow::Scroll` and `Overflow::Auto` nodes.
+    pub scrollbar_width: f32,
+
+    // Position properties
+    /// What should the `position` value of this struct use as a base offset?
+    pub position: Position,
+    /// How should the position of this element be tweaked relative to the layout defined?
+    #[refineable]
+    pub inset: Edges<Length>,
+
+    // Size properies
+    /// Sets the initial size of the item
+    #[refineable]
+    pub size: Size<Length>,
+    /// Controls the minimum size of the item
+    #[refineable]
+    pub min_size: Size<Length>,
+    /// Controls the maximum size of the item
+    #[refineable]
+    pub max_size: Size<Length>,
+    /// Sets the preferred aspect ratio for the item. The ratio is calculated as width divided by height.
+    pub aspect_ratio: Option<f32>,
+
+    // Spacing Properties
+    /// How large should the margin be on each side?
+    #[refineable]
+    pub margin: Edges<Length>,
+    /// How large should the padding be on each side?
+    #[refineable]
+    pub padding: Edges<DefiniteLength>,
+    /// How large should the border be on each side?
+    #[refineable]
+    pub border_widths: Edges<AbsoluteLength>,
+
+    // Alignment properties
+    /// How this node's children aligned in the cross/block axis?
+    pub align_items: Option<AlignItems>,
+    /// How this node should be aligned in the cross/block axis. Falls back to the parents [`AlignItems`] if not set
+    pub align_self: Option<AlignSelf>,
+    /// How should content contained within this item be aligned in the cross/block axis
+    pub align_content: Option<AlignContent>,
+    /// How should contained within this item be aligned in the main/inline axis
+    pub justify_content: Option<JustifyContent>,
+    /// How large should the gaps between items in a flex container be?
+    #[refineable]
+    pub gap: Size<DefiniteLength>,
+
+    // Flexbox properies
+    /// Which direction does the main axis flow in?
+    pub flex_direction: FlexDirection,
+    /// Should elements wrap, or stay in a single line?
+    pub flex_wrap: FlexWrap,
+    /// Sets the initial main axis size of the item
+    pub flex_basis: Length,
+    /// The relative rate at which this item grows when it is expanding to fill space, 0.0 is the default value, and this value must be positive.
+    pub flex_grow: f32,
+    /// The relative rate at which this item shrinks when it is contracting to fit into space, 1.0 is the default value, and this value must be positive.
+    pub flex_shrink: f32,
+
+    /// The fill color of this element
+    pub fill: Option<Fill>,
+
+    /// The border color of this element
+    pub border_color: Option<Hsla>,
+
+    /// The radius of the corners of this element
+    #[refineable]
+    pub corner_radii: CornerRadii,
+
+    /// The color of text within this element. Cascades to children unless overridden.
+    pub text_color: Option<Hsla>,
+
+    /// The font size in rems.
+    pub font_size: Option<f32>,
+
+    pub font_family: Option<Arc<str>>,
+
+    pub font_weight: Option<FontWeight>,
+
+    pub font_style: Option<FontStyle>,
+}
+
+impl Style {
+    pub fn text_style(&self, cx: &WindowContext) -> Option<TextStyleRefinement> {
+        if self.text_color.is_none()
+            && self.font_size.is_none()
+            && self.font_family.is_none()
+            && self.font_weight.is_none()
+            && self.font_style.is_none()
+        {
+            return None;
+        }
+
+        Some(TextStyleRefinement {
+            color: self.text_color.map(Into::into),
+            font_family: self.font_family.clone(),
+            font_size: self.font_size.map(|size| size * cx.rem_size()),
+            font_weight: self.font_weight,
+            font_style: self.font_style,
+            underline: None,
+        })
+    }
+
+    pub fn to_taffy(&self, rem_size: f32) -> taffy::style::Style {
+        taffy::style::Style {
+            display: self.display,
+            overflow: self.overflow.clone().into(),
+            scrollbar_width: self.scrollbar_width,
+            position: self.position,
+            inset: self.inset.to_taffy(rem_size),
+            size: self.size.to_taffy(rem_size),
+            min_size: self.min_size.to_taffy(rem_size),
+            max_size: self.max_size.to_taffy(rem_size),
+            aspect_ratio: self.aspect_ratio,
+            margin: self.margin.to_taffy(rem_size),
+            padding: self.padding.to_taffy(rem_size),
+            border: self.border_widths.to_taffy(rem_size),
+            align_items: self.align_items,
+            align_self: self.align_self,
+            align_content: self.align_content,
+            justify_content: self.justify_content,
+            gap: self.gap.to_taffy(rem_size),
+            flex_direction: self.flex_direction,
+            flex_wrap: self.flex_wrap,
+            flex_basis: self.flex_basis.to_taffy(rem_size).into(),
+            flex_grow: self.flex_grow,
+            flex_shrink: self.flex_shrink,
+            ..Default::default() // Ignore grid properties for now
+        }
+    }
+
+    /// Paints the background of an element styled with this style.
+    pub fn paint_background<V: 'static>(&self, bounds: RectF, cx: &mut ViewContext<V>) {
+        let rem_size = cx.rem_size();
+        if let Some(color) = self.fill.as_ref().and_then(Fill::color) {
+            cx.scene().push_quad(gpui::Quad {
+                bounds,
+                background: Some(color.into()),
+                corner_radii: self.corner_radii.to_gpui(bounds.size(), rem_size),
+                border: Default::default(),
+            });
+        }
+    }
+
+    /// Paints the foreground of an element styled with this style.
+    pub fn paint_foreground<V: 'static>(&self, bounds: RectF, cx: &mut ViewContext<V>) {
+        let rem_size = cx.rem_size();
+
+        if let Some(color) = self.border_color {
+            let border = self.border_widths.to_pixels(rem_size);
+            if !border.is_empty() {
+                cx.scene().push_quad(gpui::Quad {
+                    bounds,
+                    background: None,
+                    corner_radii: self.corner_radii.to_gpui(bounds.size(), rem_size),
+                    border: scene::Border {
+                        color: color.into(),
+                        top: border.top,
+                        right: border.right,
+                        bottom: border.bottom,
+                        left: border.left,
+                    },
+                });
+            }
+        }
+    }
+}
+
+impl Default for Style {
+    fn default() -> Self {
+        Style {
+            display: Display::Block,
+            overflow: Point {
+                x: Overflow::Visible,
+                y: Overflow::Visible,
+            },
+            scrollbar_width: 0.0,
+            position: Position::Relative,
+            inset: Edges::auto(),
+            margin: Edges::<Length>::zero(),
+            padding: Edges::<DefiniteLength>::zero(),
+            border_widths: Edges::<AbsoluteLength>::zero(),
+            size: Size::auto(),
+            min_size: Size::auto(),
+            max_size: Size::auto(),
+            aspect_ratio: None,
+            gap: Size::zero(),
+            // Aligment
+            align_items: None,
+            align_self: None,
+            align_content: None,
+            justify_content: None,
+            // Flexbox
+            flex_direction: FlexDirection::Row,
+            flex_wrap: FlexWrap::NoWrap,
+            flex_grow: 0.0,
+            flex_shrink: 1.0,
+            flex_basis: Length::Auto,
+            fill: None,
+            border_color: None,
+            corner_radii: CornerRadii::default(),
+            text_color: None,
+            font_size: Some(1.),
+            font_family: None,
+            font_weight: None,
+            font_style: None,
+        }
+    }
+}
+
+#[derive(Clone, Debug)]
+pub enum Fill {
+    Color(Hsla),
+}
+
+impl Fill {
+    pub fn color(&self) -> Option<Hsla> {
+        match self {
+            Fill::Color(color) => Some(*color),
+        }
+    }
+}
+
+impl Default for Fill {
+    fn default() -> Self {
+        Self::Color(Hsla::default())
+    }
+}
+
+impl From<Hsla> for Fill {
+    fn from(color: Hsla) -> Self {
+        Self::Color(color)
+    }
+}
+
+#[derive(Clone, Refineable, Default, Debug)]
+#[refineable(debug)]
+pub struct CornerRadii {
+    top_left: AbsoluteLength,
+    top_right: AbsoluteLength,
+    bottom_left: AbsoluteLength,
+    bottom_right: AbsoluteLength,
+}
+
+impl CornerRadii {
+    pub fn to_gpui(&self, box_size: Vector2F, rem_size: f32) -> gpui::scene::CornerRadii {
+        let max_radius = box_size.x().min(box_size.y()) / 2.;
+
+        gpui::scene::CornerRadii {
+            top_left: self.top_left.to_pixels(rem_size).min(max_radius),
+            top_right: self.top_right.to_pixels(rem_size).min(max_radius),
+            bottom_left: self.bottom_left.to_pixels(rem_size).min(max_radius),
+            bottom_right: self.bottom_right.to_pixels(rem_size).min(max_radius),
+        }
+    }
+}
+
+pub trait Styleable {
+    type Style: Refineable + Default;
+
+    fn style_cascade(&mut self) -> &mut RefinementCascade<Self::Style>;
+    fn declared_style(&mut self) -> &mut <Self::Style as Refineable>::Refinement;
+
+    fn computed_style(&mut self) -> Self::Style {
+        Self::Style::from_refinement(&self.style_cascade().merged())
+    }
+
+    fn hover(self) -> Hoverable<Self>
+    where
+        Self: Sized,
+    {
+        hoverable(self)
+    }
+
+    fn active(self) -> Pressable<Self>
+    where
+        Self: Sized,
+    {
+        pressable(self)
+    }
+}
+
+// Helpers methods that take and return mut self. This includes tailwind style methods for standard sizes etc.
+//
+// Example:
+// // Sets the padding to 0.5rem, just like class="p-2" in Tailwind.
+// fn p_2(mut self) -> Self where Self: Sized;
+pub trait StyleHelpers: Styleable<Style = Style> {
+    styleable_helpers!();
+
+    fn h(mut self, height: Length) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().size.height = Some(height);
+        self
+    }
+
+    /// size_{n}: Sets width & height to {n}
+    ///
+    /// Example:
+    /// size_1: Sets width & height to 1
+    fn size(mut self, size: Length) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().size.height = Some(size);
+        self.declared_style().size.width = Some(size);
+        self
+    }
+
+    fn full(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().size.width = Some(relative(1.));
+        self.declared_style().size.height = Some(relative(1.));
+        self
+    }
+
+    fn relative(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().position = Some(Position::Relative);
+        self
+    }
+
+    fn absolute(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().position = Some(Position::Absolute);
+        self
+    }
+
+    fn block(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().display = Some(Display::Block);
+        self
+    }
+
+    fn flex(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().display = Some(Display::Flex);
+        self
+    }
+
+    fn flex_col(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().flex_direction = Some(FlexDirection::Column);
+        self
+    }
+
+    fn flex_row(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().flex_direction = Some(FlexDirection::Row);
+        self
+    }
+
+    fn flex_1(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().flex_grow = Some(1.);
+        self.declared_style().flex_shrink = Some(1.);
+        self.declared_style().flex_basis = Some(relative(0.));
+        self
+    }
+
+    fn flex_auto(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().flex_grow = Some(1.);
+        self.declared_style().flex_shrink = Some(1.);
+        self.declared_style().flex_basis = Some(Length::Auto);
+        self
+    }
+
+    fn flex_initial(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().flex_grow = Some(0.);
+        self.declared_style().flex_shrink = Some(1.);
+        self.declared_style().flex_basis = Some(Length::Auto);
+        self
+    }
+
+    fn flex_none(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().flex_grow = Some(0.);
+        self.declared_style().flex_shrink = Some(0.);
+        self
+    }
+
+    fn grow(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().flex_grow = Some(1.);
+        self
+    }
+
+    fn items_start(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().align_items = Some(AlignItems::FlexStart);
+        self
+    }
+
+    fn items_end(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().align_items = Some(AlignItems::FlexEnd);
+        self
+    }
+
+    fn items_center(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().align_items = Some(AlignItems::Center);
+        self
+    }
+
+    fn justify_between(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().justify_content = Some(JustifyContent::SpaceBetween);
+        self
+    }
+
+    fn justify_center(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().justify_content = Some(JustifyContent::Center);
+        self
+    }
+
+    fn justify_start(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().justify_content = Some(JustifyContent::Start);
+        self
+    }
+
+    fn justify_end(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().justify_content = Some(JustifyContent::End);
+        self
+    }
+
+    fn justify_around(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().justify_content = Some(JustifyContent::SpaceAround);
+        self
+    }
+
+    fn fill<F>(mut self, fill: F) -> Self
+    where
+        F: Into<Fill>,
+        Self: Sized,
+    {
+        self.declared_style().fill = Some(fill.into());
+        self
+    }
+
+    fn border_color<C>(mut self, border_color: C) -> Self
+    where
+        C: Into<Hsla>,
+        Self: Sized,
+    {
+        self.declared_style().border_color = Some(border_color.into());
+        self
+    }
+
+    fn text_color<C>(mut self, color: C) -> Self
+    where
+        C: Into<Hsla>,
+        Self: Sized,
+    {
+        self.declared_style().text_color = Some(color.into());
+        self
+    }
+
+    fn text_xs(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().font_size = Some(0.75);
+        self
+    }
+
+    fn text_sm(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().font_size = Some(0.875);
+        self
+    }
+
+    fn text_base(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().font_size = Some(1.0);
+        self
+    }
+
+    fn text_lg(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().font_size = Some(1.125);
+        self
+    }
+
+    fn text_xl(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().font_size = Some(1.25);
+        self
+    }
+
+    fn text_2xl(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().font_size = Some(1.5);
+        self
+    }
+
+    fn text_3xl(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().font_size = Some(1.875);
+        self
+    }
+
+    fn font(mut self, family_name: impl Into<Arc<str>>) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().font_family = Some(family_name.into());
+        self
+    }
+}

crates/gpui2/src/view_context.rs πŸ”—

@@ -0,0 +1,79 @@
+use std::{any::TypeId, rc::Rc};
+
+use crate::{element::LayoutId, style::Style};
+use anyhow::{anyhow, Result};
+use derive_more::{Deref, DerefMut};
+use gpui::{geometry::Size, scene::EventHandler, EventContext, Layout, MeasureParams};
+pub use gpui::{taffy::tree::NodeId, ViewContext as LegacyViewContext};
+
+#[derive(Deref, DerefMut)]
+pub struct ViewContext<'a, 'b, 'c, V> {
+    #[deref]
+    #[deref_mut]
+    pub(crate) legacy_cx: &'c mut LegacyViewContext<'a, 'b, V>,
+}
+
+impl<'a, 'b, 'c, V: 'static> ViewContext<'a, 'b, 'c, V> {
+    pub fn new(legacy_cx: &'c mut LegacyViewContext<'a, 'b, V>) -> Self {
+        Self { legacy_cx }
+    }
+
+    pub fn add_layout_node(
+        &mut self,
+        style: Style,
+        children: impl IntoIterator<Item = NodeId>,
+    ) -> Result<LayoutId> {
+        let rem_size = self.rem_size();
+        let style = style.to_taffy(rem_size);
+        let id = self
+            .legacy_cx
+            .layout_engine()
+            .ok_or_else(|| anyhow!("no layout engine"))?
+            .add_node(style, children)?;
+
+        Ok(id)
+    }
+
+    pub fn add_measured_layout_node<F>(&mut self, style: Style, measure: F) -> Result<LayoutId>
+    where
+        F: Fn(MeasureParams) -> Size<f32> + Sync + Send + 'static,
+    {
+        let rem_size = self.rem_size();
+        let layout_id = self
+            .layout_engine()
+            .ok_or_else(|| anyhow!("no layout engine"))?
+            .add_measured_node(style.to_taffy(rem_size), measure)?;
+
+        Ok(layout_id)
+    }
+
+    pub fn on_event<E: 'static>(
+        &mut self,
+        order: u32,
+        handler: impl Fn(&mut V, &E, &mut EventContext<V>) + 'static,
+    ) {
+        let view = self.weak_handle();
+
+        self.scene().event_handlers.push(EventHandler {
+            order,
+            handler: Rc::new(move |event, window_cx| {
+                if let Some(view) = view.upgrade(window_cx) {
+                    view.update(window_cx, |view, view_cx| {
+                        let mut event_cx = EventContext::new(view_cx);
+                        handler(view, event.downcast_ref().unwrap(), &mut event_cx);
+                        event_cx.bubble
+                    })
+                } else {
+                    true
+                }
+            }),
+            event_type: TypeId::of::<E>(),
+        })
+    }
+
+    pub(crate) fn computed_layout(&mut self, layout_id: LayoutId) -> Result<Layout> {
+        self.layout_engine()
+            .ok_or_else(|| anyhow!("no layout engine present"))?
+            .computed_layout(layout_id)
+    }
+}

crates/gpui/playground_macros/Cargo.toml β†’ crates/gpui2_macros/Cargo.toml πŸ”—

@@ -1,11 +1,11 @@
 [package]
-name = "playground_macros"
+name = "gpui2_macros"
 version = "0.1.0"
 edition = "2021"
 publish = false
 
 [lib]
-path = "src/playground_macros.rs"
+path = "src/gpui2_macros.rs"
 proc-macro = true
 
 [dependencies]

crates/gpui/playground_macros/src/derive_element.rs β†’ crates/gpui2_macros/src/derive_element.rs πŸ”—

@@ -59,28 +59,30 @@ pub fn derive_element(input: TokenStream) -> TokenStream {
     );
 
     let gen = quote! {
-        impl #impl_generics playground::element::Element<#view_type_name> for #type_name #type_generics
+        impl #impl_generics gpui2::element::Element<#view_type_name> for #type_name #type_generics
         #where_clause
         {
-            type Layout = Option<playground::element::AnyElement<#view_type_name #lifetimes>>;
+            type PaintState = gpui2::element::AnyElement<#view_type_name #lifetimes>;
 
             fn layout(
                 &mut self,
                 view: &mut V,
-                cx: &mut playground::element::LayoutContext<V>,
-            ) -> anyhow::Result<playground::element::Layout<V, Self::Layout>> {
-                let mut element = self.render(view, cx).into_any();
-                let layout_id = element.layout(view, cx)?;
-                Ok(playground::element::Layout::new(layout_id, Some(element)))
+                cx: &mut gpui2::ViewContext<V>,
+            ) -> anyhow::Result<(gpui2::element::LayoutId, Self::PaintState)> {
+                let mut rendered_element = self.render(view, cx).into_element().into_any();
+                let layout_id = rendered_element.layout(view, cx)?;
+                Ok((layout_id, rendered_element))
             }
 
             fn paint(
                 &mut self,
                 view: &mut V,
-                layout: &mut playground::element::Layout<V, Self::Layout>,
-                cx: &mut playground::element::PaintContext<V>,
+                parent_origin: gpui2::Vector2F,
+                _: &gpui2::element::Layout,
+                rendered_element: &mut Self::PaintState,
+                cx: &mut gpui2::ViewContext<V>,
             ) {
-                layout.paint(view, cx);
+                rendered_element.paint(view, parent_origin, cx);
             }
         }
 

crates/gpui/playground_macros/src/derive_into_element.rs β†’ crates/gpui2_macros/src/derive_into_element.rs πŸ”—

@@ -56,7 +56,7 @@ pub fn impl_into_element(
     where_clause: &Option<&WhereClause>,
 ) -> proc_macro2::TokenStream {
     quote! {
-        impl #impl_generics playground::element::IntoElement<#view_type_name> for #type_name #type_generics
+        impl #impl_generics gpui2::element::IntoElement<#view_type_name> for #type_name #type_generics
         #where_clause
         {
             type Element = Self;

crates/gpui/playground_macros/src/playground_macros.rs β†’ crates/gpui2_macros/src/gpui2_macros.rs πŸ”—

@@ -3,7 +3,6 @@ use proc_macro::TokenStream;
 mod derive_element;
 mod derive_into_element;
 mod styleable_helpers;
-mod tailwind_lengths;
 
 #[proc_macro]
 pub fn styleable_helpers(args: TokenStream) -> TokenStream {
@@ -19,8 +18,3 @@ pub fn derive_element(input: TokenStream) -> TokenStream {
 pub fn derive_into_element(input: TokenStream) -> TokenStream {
     derive_into_element::derive_into_element(input)
 }
-
-#[proc_macro_attribute]
-pub fn tailwind_lengths(attr: TokenStream, item: TokenStream) -> TokenStream {
-    tailwind_lengths::tailwind_lengths(attr, item)
-}

crates/gpui2_macros/src/styleable_helpers.rs πŸ”—

@@ -0,0 +1,326 @@
+use proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::{format_ident, quote};
+use syn::{
+    parse::{Parse, ParseStream, Result},
+    parse_macro_input,
+};
+
+struct StyleableMacroInput;
+
+impl Parse for StyleableMacroInput {
+    fn parse(_input: ParseStream) -> Result<Self> {
+        Ok(StyleableMacroInput)
+    }
+}
+
+pub fn styleable_helpers(input: TokenStream) -> TokenStream {
+    let _ = parse_macro_input!(input as StyleableMacroInput);
+    let methods = generate_methods();
+    let output = quote! {
+        #(#methods)*
+    };
+
+    output.into()
+}
+
+fn generate_methods() -> Vec<TokenStream2> {
+    let mut methods = Vec::new();
+
+    for (prefix, auto_allowed, fields) in box_prefixes() {
+        for (suffix, length_tokens, doc_string) in box_suffixes() {
+            if auto_allowed || suffix != "auto" {
+                let method = generate_method(prefix, suffix, &fields, length_tokens, doc_string);
+                methods.push(method);
+            }
+        }
+    }
+
+    for (prefix, fields) in corner_prefixes() {
+        for (suffix, radius_tokens, doc_string) in corner_suffixes() {
+            let method = generate_method(prefix, suffix, &fields, radius_tokens, doc_string);
+            methods.push(method);
+        }
+    }
+
+    for (prefix, fields) in border_prefixes() {
+        for (suffix, width_tokens, doc_string) in border_suffixes() {
+            let method = generate_method(prefix, suffix, &fields, width_tokens, doc_string);
+            methods.push(method);
+        }
+    }
+
+    methods
+}
+
+fn generate_method(
+    prefix: &'static str,
+    suffix: &'static str,
+    fields: &Vec<TokenStream2>,
+    length_tokens: TokenStream2,
+    doc_string: &'static str,
+) -> TokenStream2 {
+    let method_name = if suffix.is_empty() {
+        format_ident!("{}", prefix)
+    } else {
+        format_ident!("{}_{}", prefix, suffix)
+    };
+
+    let field_assignments = fields
+        .iter()
+        .map(|field_tokens| {
+            quote! {
+                style.#field_tokens = Some(gpui::geometry::#length_tokens);
+            }
+        })
+        .collect::<Vec<_>>();
+
+    let method = quote! {
+        #[doc = #doc_string]
+        fn #method_name(mut self) -> Self where Self: std::marker::Sized {
+            let mut style = self.declared_style();
+            #(#field_assignments)*
+            self
+        }
+    };
+
+    method
+}
+
+fn box_prefixes() -> Vec<(&'static str, bool, Vec<TokenStream2>)> {
+    vec![
+        ("w", true, vec![quote! { size.width }]),
+        ("h", true, vec![quote! { size.height }]),
+        (
+            "size",
+            true,
+            vec![quote! {size.width}, quote! {size.height}],
+        ),
+        ("min_w", false, vec![quote! { min_size.width }]),
+        ("min_h", false, vec![quote! { min_size.height }]),
+        ("max_w", false, vec![quote! { max_size.width }]),
+        ("max_h", false, vec![quote! { max_size.height }]),
+        (
+            "m",
+            true,
+            vec![
+                quote! { margin.top },
+                quote! { margin.bottom },
+                quote! { margin.left },
+                quote! { margin.right },
+            ],
+        ),
+        ("mt", true, vec![quote! { margin.top }]),
+        ("mb", true, vec![quote! { margin.bottom }]),
+        (
+            "my",
+            true,
+            vec![quote! { margin.top }, quote! { margin.bottom }],
+        ),
+        (
+            "mx",
+            true,
+            vec![quote! { margin.left }, quote! { margin.right }],
+        ),
+        ("ml", true, vec![quote! { margin.left }]),
+        ("mr", true, vec![quote! { margin.right }]),
+        (
+            "p",
+            false,
+            vec![
+                quote! { padding.top },
+                quote! { padding.bottom },
+                quote! { padding.left },
+                quote! { padding.right },
+            ],
+        ),
+        ("pt", false, vec![quote! { padding.top }]),
+        ("pb", false, vec![quote! { padding.bottom }]),
+        (
+            "px",
+            false,
+            vec![quote! { padding.left }, quote! { padding.right }],
+        ),
+        (
+            "py",
+            false,
+            vec![quote! { padding.top }, quote! { padding.bottom }],
+        ),
+        ("pl", false, vec![quote! { padding.left }]),
+        ("pr", false, vec![quote! { padding.right }]),
+        ("top", true, vec![quote! { inset.top }]),
+        ("bottom", true, vec![quote! { inset.bottom }]),
+        ("left", true, vec![quote! { inset.left }]),
+        ("right", true, vec![quote! { inset.right }]),
+        (
+            "gap",
+            false,
+            vec![quote! { gap.width }, quote! { gap.height }],
+        ),
+        ("gap_x", false, vec![quote! { gap.width }]),
+        ("gap_y", false, vec![quote! { gap.height }]),
+    ]
+}
+
+fn box_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> {
+    vec![
+        ("0", quote! { pixels(0.) }, "0px"),
+        ("0p5", quote! { rems(0.125) }, "2px (0.125rem)"),
+        ("1", quote! { rems(0.25) }, "4px (0.25rem)"),
+        ("1p5", quote! { rems(0.375) }, "6px (0.375rem)"),
+        ("2", quote! { rems(0.5) }, "8px (0.5rem)"),
+        ("2p5", quote! { rems(0.625) }, "10px (0.625rem)"),
+        ("3", quote! { rems(0.75) }, "12px (0.75rem)"),
+        ("3p5", quote! { rems(0.875) }, "14px (0.875rem)"),
+        ("4", quote! { rems(1.) }, "16px (1rem)"),
+        ("5", quote! { rems(1.25) }, "20px (1.25rem)"),
+        ("6", quote! { rems(1.5) }, "24px (1.5rem)"),
+        ("7", quote! { rems(1.75) }, "28px (1.75rem)"),
+        ("8", quote! { rems(2.0) }, "32px (2rem)"),
+        ("9", quote! { rems(2.25) }, "36px (2.25rem)"),
+        ("10", quote! { rems(2.5) }, "40px (2.5rem)"),
+        ("11", quote! { rems(2.75) }, "44px (2.75rem)"),
+        ("12", quote! { rems(3.) }, "48px (3rem)"),
+        ("16", quote! { rems(4.) }, "64px (4rem)"),
+        ("20", quote! { rems(5.) }, "80px (5rem)"),
+        ("24", quote! { rems(6.) }, "96px (6rem)"),
+        ("32", quote! { rems(8.) }, "128px (8rem)"),
+        ("40", quote! { rems(10.) }, "160px (10rem)"),
+        ("48", quote! { rems(12.) }, "192px (12rem)"),
+        ("56", quote! { rems(14.) }, "224px (14rem)"),
+        ("64", quote! { rems(16.) }, "256px (16rem)"),
+        ("72", quote! { rems(18.) }, "288px (18rem)"),
+        ("80", quote! { rems(20.) }, "320px (20rem)"),
+        ("96", quote! { rems(24.) }, "384px (24rem)"),
+        ("auto", quote! { auto() }, "Auto"),
+        ("px", quote! { pixels(1.) }, "1px"),
+        ("full", quote! { relative(1.) }, "100%"),
+        ("1_2", quote! { relative(0.5) }, "50% (1/2)"),
+        ("1_3", quote! { relative(1./3.) }, "33% (1/3)"),
+        ("2_3", quote! { relative(2./3.) }, "66% (2/3)"),
+        ("1_4", quote! { relative(0.25) }, "25% (1/4)"),
+        ("2_4", quote! { relative(0.5) }, "50% (2/4)"),
+        ("3_4", quote! { relative(0.75) }, "75% (3/4)"),
+        ("1_5", quote! { relative(0.2) }, "20% (1/5)"),
+        ("2_5", quote! { relative(0.4) }, "40% (2/5)"),
+        ("3_5", quote! { relative(0.6) }, "60% (3/5)"),
+        ("4_5", quote! { relative(0.8) }, "80% (4/5)"),
+        ("1_6", quote! { relative(1./6.) }, "16% (1/6)"),
+        ("5_6", quote! { relative(5./6.) }, "80% (5/6)"),
+        ("1_12", quote! { relative(1./12.) }, "8% (1/12)"),
+    ]
+}
+
+fn corner_prefixes() -> Vec<(&'static str, Vec<TokenStream2>)> {
+    vec![
+        (
+            "rounded",
+            vec![
+                quote! { corner_radii.top_left },
+                quote! { corner_radii.top_right },
+                quote! { corner_radii.bottom_right },
+                quote! { corner_radii.bottom_left },
+            ],
+        ),
+        (
+            "rounded_t",
+            vec![
+                quote! { corner_radii.top_left },
+                quote! { corner_radii.top_right },
+            ],
+        ),
+        (
+            "rounded_b",
+            vec![
+                quote! { corner_radii.bottom_left },
+                quote! { corner_radii.bottom_right },
+            ],
+        ),
+        (
+            "rounded_r",
+            vec![
+                quote! { corner_radii.top_right },
+                quote! { corner_radii.bottom_right },
+            ],
+        ),
+        (
+            "rounded_l",
+            vec![
+                quote! { corner_radii.top_left },
+                quote! { corner_radii.bottom_left },
+            ],
+        ),
+        ("rounded_tl", vec![quote! { corner_radii.top_left }]),
+        ("rounded_tr", vec![quote! { corner_radii.top_right }]),
+        ("rounded_bl", vec![quote! { corner_radii.bottom_left }]),
+        ("rounded_br", vec![quote! { corner_radii.bottom_right }]),
+    ]
+}
+
+fn corner_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> {
+    vec![
+        ("none", quote! { pixels(0.) }, "0px"),
+        ("sm", quote! { rems(0.125) }, "2px (0.125rem)"),
+        ("md", quote! { rems(0.25) }, "4px (0.25rem)"),
+        ("lg", quote! { rems(0.5) }, "8px (0.5rem)"),
+        ("xl", quote! { rems(0.75) }, "12px (0.75rem)"),
+        ("2xl", quote! { rems(1.) }, "16px (1rem)"),
+        ("3xl", quote! { rems(1.5) }, "24px (1.5rem)"),
+        ("full", quote! {  pixels(9999.) }, "9999px"),
+    ]
+}
+
+fn border_prefixes() -> Vec<(&'static str, Vec<TokenStream2>)> {
+    vec![
+        (
+            "border",
+            vec![
+                quote! { border_widths.top },
+                quote! { border_widths.right },
+                quote! { border_widths.bottom },
+                quote! { border_widths.left },
+            ],
+        ),
+        ("border_t", vec![quote! { border_widths.top }]),
+        ("border_b", vec![quote! { border_widths.bottom }]),
+        ("border_r", vec![quote! { border_widths.right }]),
+        ("border_l", vec![quote! { border_widths.left }]),
+        (
+            "border_x",
+            vec![
+                quote! { border_widths.left },
+                quote! { border_widths.right },
+            ],
+        ),
+        (
+            "border_y",
+            vec![
+                quote! { border_widths.top },
+                quote! { border_widths.bottom },
+            ],
+        ),
+    ]
+}
+
+fn border_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> {
+    vec![
+        ("", quote! { pixels(1.)}, "1px"),
+        ("0", quote! { pixels(0.)}, "0px"),
+        ("1", quote! { pixels(1.) }, "1px"),
+        ("2", quote! { pixels(2.) }, "2px"),
+        ("3", quote! { pixels(3.) }, "3px"),
+        ("4", quote! { pixels(4.) }, "4px"),
+        ("5", quote! { pixels(5.) }, "5px"),
+        ("6", quote! { pixels(6.) }, "6px"),
+        ("7", quote! { pixels(7.) }, "7px"),
+        ("8", quote! { pixels(8.) }, "8px"),
+        ("9", quote! { pixels(9.) }, "9px"),
+        ("10", quote! { pixels(10.) }, "10px"),
+        ("11", quote! { pixels(11.) }, "11px"),
+        ("12", quote! { pixels(12.) }, "12px"),
+        ("16", quote! { pixels(16.) }, "16px"),
+        ("20", quote! { pixels(20.) }, "20px"),
+        ("24", quote! { pixels(24.) }, "24px"),
+        ("32", quote! { pixels(32.) }, "32px"),
+    ]
+}

crates/gpui_macros/src/gpui_macros.rs πŸ”—

@@ -329,7 +329,7 @@ pub fn element_derive(input: TokenStream) -> TokenStream {
                 &mut self,
                 constraint: gpui::SizeConstraint,
                 view: &mut V,
-                cx: &mut gpui::LayoutContext<V>,
+                cx: &mut gpui::ViewContext<V>,
             ) -> (gpui::geometry::vector::Vector2F, gpui::elements::AnyElement<V>) {
                 let mut element = self.render(view, cx).into_any();
                 let size = element.layout(constraint, view, cx);
@@ -338,14 +338,13 @@ pub fn element_derive(input: TokenStream) -> TokenStream {
 
             fn paint(
                 &mut self,
-                scene: &mut gpui::SceneBuilder,
                 bounds: gpui::geometry::rect::RectF,
                 visible_bounds: gpui::geometry::rect::RectF,
                 element: &mut gpui::elements::AnyElement<V>,
                 view: &mut V,
-                cx: &mut gpui::PaintContext<V>,
+                cx: &mut gpui::ViewContext<V>,
             ) {
-                element.paint(scene, bounds.origin(), visible_bounds, view, cx);
+                element.paint(bounds.origin(), visible_bounds, view, cx);
             }
 
             fn rect_for_text_range(

crates/language/src/language.rs πŸ”—

@@ -13,7 +13,7 @@ use anyhow::{anyhow, Context, Result};
 use async_trait::async_trait;
 use collections::{HashMap, HashSet};
 use futures::{
-    channel::oneshot,
+    channel::{mpsc, oneshot},
     future::{BoxFuture, Shared},
     FutureExt, TryFutureExt as _,
 };
@@ -48,9 +48,6 @@ use unicase::UniCase;
 use util::{http::HttpClient, paths::PathExt};
 use util::{post_inc, ResultExt, TryFutureExt as _, UnwrapFuture};
 
-#[cfg(any(test, feature = "test-support"))]
-use futures::channel::mpsc;
-
 pub use buffer::Operation;
 pub use buffer::*;
 pub use diagnostic_set::DiagnosticEntry;
@@ -64,6 +61,27 @@ pub fn init(cx: &mut AppContext) {
     language_settings::init(cx);
 }
 
+#[derive(Clone, Default)]
+struct LspBinaryStatusSender {
+    txs: Arc<Mutex<Vec<mpsc::UnboundedSender<(Arc<Language>, LanguageServerBinaryStatus)>>>>,
+}
+
+impl LspBinaryStatusSender {
+    fn subscribe(&self) -> mpsc::UnboundedReceiver<(Arc<Language>, LanguageServerBinaryStatus)> {
+        let (tx, rx) = mpsc::unbounded();
+        self.txs.lock().push(tx);
+        rx
+    }
+
+    fn send(&self, language: Arc<Language>, status: LanguageServerBinaryStatus) {
+        let mut txs = self.txs.lock();
+        txs.retain(|tx| {
+            tx.unbounded_send((language.clone(), status.clone()))
+                .is_ok()
+        });
+    }
+}
+
 thread_local! {
     static PARSER: RefCell<Parser> = RefCell::new(Parser::new());
 }
@@ -594,14 +612,13 @@ struct AvailableLanguage {
 pub struct LanguageRegistry {
     state: RwLock<LanguageRegistryState>,
     language_server_download_dir: Option<Arc<Path>>,
-    lsp_binary_statuses_tx: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
-    lsp_binary_statuses_rx: async_broadcast::Receiver<(Arc<Language>, LanguageServerBinaryStatus)>,
     login_shell_env_loaded: Shared<Task<()>>,
     #[allow(clippy::type_complexity)]
     lsp_binary_paths: Mutex<
         HashMap<LanguageServerName, Shared<Task<Result<LanguageServerBinary, Arc<anyhow::Error>>>>>,
     >,
     executor: Option<Arc<Background>>,
+    lsp_binary_status_tx: LspBinaryStatusSender,
 }
 
 struct LanguageRegistryState {
@@ -624,7 +641,6 @@ pub struct PendingLanguageServer {
 
 impl LanguageRegistry {
     pub fn new(login_shell_env_loaded: Task<()>) -> Self {
-        let (lsp_binary_statuses_tx, lsp_binary_statuses_rx) = async_broadcast::broadcast(16);
         Self {
             state: RwLock::new(LanguageRegistryState {
                 next_language_server_id: 0,
@@ -638,11 +654,10 @@ impl LanguageRegistry {
                 reload_count: 0,
             }),
             language_server_download_dir: None,
-            lsp_binary_statuses_tx,
-            lsp_binary_statuses_rx,
             login_shell_env_loaded: login_shell_env_loaded.shared(),
             lsp_binary_paths: Default::default(),
             executor: None,
+            lsp_binary_status_tx: Default::default(),
         }
     }
 
@@ -918,8 +933,8 @@ impl LanguageRegistry {
         let container_dir: Arc<Path> = Arc::from(download_dir.join(adapter.name.0.as_ref()));
         let root_path = root_path.clone();
         let adapter = adapter.clone();
-        let lsp_binary_statuses = self.lsp_binary_statuses_tx.clone();
         let login_shell_env_loaded = self.login_shell_env_loaded.clone();
+        let lsp_binary_statuses = self.lsp_binary_status_tx.clone();
 
         let task = {
             let container_dir = container_dir.clone();
@@ -976,8 +991,8 @@ impl LanguageRegistry {
 
     pub fn language_server_binary_statuses(
         &self,
-    ) -> async_broadcast::Receiver<(Arc<Language>, LanguageServerBinaryStatus)> {
-        self.lsp_binary_statuses_rx.clone()
+    ) -> mpsc::UnboundedReceiver<(Arc<Language>, LanguageServerBinaryStatus)> {
+        self.lsp_binary_status_tx.subscribe()
     }
 
     pub fn delete_server_container(
@@ -1054,7 +1069,7 @@ async fn get_binary(
     language: Arc<Language>,
     delegate: Arc<dyn LspAdapterDelegate>,
     container_dir: Arc<Path>,
-    statuses: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
+    statuses: LspBinaryStatusSender,
     mut cx: AsyncAppContext,
 ) -> Result<LanguageServerBinary> {
     if !container_dir.exists() {
@@ -1081,19 +1096,15 @@ async fn get_binary(
             .cached_server_binary(container_dir.to_path_buf(), delegate.as_ref())
             .await
         {
-            statuses
-                .broadcast((language.clone(), LanguageServerBinaryStatus::Cached))
-                .await?;
+            statuses.send(language.clone(), LanguageServerBinaryStatus::Cached);
             return Ok(binary);
         } else {
-            statuses
-                .broadcast((
-                    language.clone(),
-                    LanguageServerBinaryStatus::Failed {
-                        error: format!("{:?}", error),
-                    },
-                ))
-                .await?;
+            statuses.send(
+                language.clone(),
+                LanguageServerBinaryStatus::Failed {
+                    error: format!("{:?}", error),
+                },
+            );
         }
     }
 
@@ -1105,27 +1116,21 @@ async fn fetch_latest_binary(
     language: Arc<Language>,
     delegate: &dyn LspAdapterDelegate,
     container_dir: &Path,
-    lsp_binary_statuses_tx: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
+    lsp_binary_statuses_tx: LspBinaryStatusSender,
 ) -> Result<LanguageServerBinary> {
     let container_dir: Arc<Path> = container_dir.into();
-    lsp_binary_statuses_tx
-        .broadcast((
-            language.clone(),
-            LanguageServerBinaryStatus::CheckingForUpdate,
-        ))
-        .await?;
+    lsp_binary_statuses_tx.send(
+        language.clone(),
+        LanguageServerBinaryStatus::CheckingForUpdate,
+    );
 
     let version_info = adapter.fetch_latest_server_version(delegate).await?;
-    lsp_binary_statuses_tx
-        .broadcast((language.clone(), LanguageServerBinaryStatus::Downloading))
-        .await?;
+    lsp_binary_statuses_tx.send(language.clone(), LanguageServerBinaryStatus::Downloading);
 
     let binary = adapter
         .fetch_server_binary(version_info, container_dir.to_path_buf(), delegate)
         .await?;
-    lsp_binary_statuses_tx
-        .broadcast((language.clone(), LanguageServerBinaryStatus::Downloaded))
-        .await?;
+    lsp_binary_statuses_tx.send(language.clone(), LanguageServerBinaryStatus::Downloaded);
 
     Ok(binary)
 }

crates/language_selector/src/active_buffer_language.rs πŸ”—

@@ -52,6 +52,7 @@ impl View for ActiveBufferLanguage {
             } else {
                 "Unknown".to_string()
             };
+            let theme = theme::current(cx).clone();
 
             MouseEventHandler::new::<Self, _>(0, cx, |state, cx| {
                 let theme = &theme::current(cx).workspace.status_bar;
@@ -68,6 +69,7 @@ impl View for ActiveBufferLanguage {
                     });
                 }
             })
+            .with_tooltip::<Self>(0, "Select Language", None, theme.tooltip.clone(), cx)
             .into_any()
         } else {
             Empty::new().into_any()

crates/language_tools/src/lsp_log.rs πŸ”—

@@ -13,7 +13,7 @@ use gpui::{
 };
 use language::{Buffer, LanguageServerId, LanguageServerName};
 use lsp::IoKind;
-use project::{Project, Worktree};
+use project::{search::SearchQuery, Project, Worktree};
 use std::{borrow::Cow, sync::Arc};
 use theme::{ui, Theme};
 use workspace::{
@@ -524,12 +524,24 @@ impl SearchableItem for LspLogView {
 
     fn find_matches(
         &mut self,
-        query: project::search::SearchQuery,
+        query: Arc<project::search::SearchQuery>,
         cx: &mut ViewContext<Self>,
     ) -> gpui::Task<Vec<Self::Match>> {
         self.editor.update(cx, |e, cx| e.find_matches(query, cx))
     }
 
+    fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext<Self>) {
+        // Since LSP Log is read-only, it doesn't make sense to support replace operation.
+    }
+    fn supported_options() -> workspace::searchable::SearchOptions {
+        workspace::searchable::SearchOptions {
+            case: true,
+            word: true,
+            regex: true,
+            // LSP log is read-only.
+            replacement: false,
+        }
+    }
     fn active_match_index(
         &mut self,
         matches: Vec<Self::Match>,

crates/live_kit_client/LiveKitBridge/Package.resolved πŸ”—

@@ -42,8 +42,8 @@
         "repositoryURL": "https://github.com/apple/swift-protobuf.git",
         "state": {
           "branch": null,
-          "revision": "0af9125c4eae12a4973fb66574c53a54962a9e1e",
-          "version": "1.21.0"
+          "revision": "ce20dc083ee485524b802669890291c0d8090170",
+          "version": "1.22.1"
         }
       }
     ]

crates/project/src/search.rs πŸ”—

@@ -7,6 +7,7 @@ use language::{char_kind, BufferSnapshot};
 use regex::{Regex, RegexBuilder};
 use smol::future::yield_now;
 use std::{
+    borrow::Cow,
     io::{BufRead, BufReader, Read},
     ops::Range,
     path::{Path, PathBuf},
@@ -35,6 +36,7 @@ impl SearchInputs {
 pub enum SearchQuery {
     Text {
         search: Arc<AhoCorasick<usize>>,
+        replacement: Option<String>,
         whole_word: bool,
         case_sensitive: bool,
         inner: SearchInputs,
@@ -42,7 +44,7 @@ pub enum SearchQuery {
 
     Regex {
         regex: Regex,
-
+        replacement: Option<String>,
         multiline: bool,
         whole_word: bool,
         case_sensitive: bool,
@@ -95,6 +97,7 @@ impl SearchQuery {
         };
         Self::Text {
             search: Arc::new(search),
+            replacement: None,
             whole_word,
             case_sensitive,
             inner,
@@ -130,6 +133,7 @@ impl SearchQuery {
         };
         Ok(Self::Regex {
             regex,
+            replacement: None,
             multiline,
             whole_word,
             case_sensitive,
@@ -156,7 +160,21 @@ impl SearchQuery {
             ))
         }
     }
-
+    pub fn with_replacement(mut self, new_replacement: Option<String>) -> Self {
+        match self {
+            Self::Text {
+                ref mut replacement,
+                ..
+            }
+            | Self::Regex {
+                ref mut replacement,
+                ..
+            } => {
+                *replacement = new_replacement;
+                self
+            }
+        }
+    }
     pub fn to_proto(&self, project_id: u64) -> proto::SearchProject {
         proto::SearchProject {
             project_id,
@@ -214,7 +232,20 @@ impl SearchQuery {
             }
         }
     }
-
+    pub fn replacement<'a>(&self, text: &'a str) -> Option<Cow<'a, str>> {
+        match self {
+            SearchQuery::Text { replacement, .. } => replacement.clone().map(Cow::from),
+            SearchQuery::Regex {
+                regex, replacement, ..
+            } => {
+                if let Some(replacement) = replacement {
+                    Some(regex.replace(text, replacement))
+                } else {
+                    None
+                }
+            }
+        }
+    }
     pub async fn search(
         &self,
         buffer: &BufferSnapshot,

crates/refineable/derive_refineable/src/derive_refineable.rs πŸ”—

@@ -12,9 +12,14 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
         ident,
         data,
         generics,
+        attrs,
         ..
     } = parse_macro_input!(input);
 
+    let impl_debug_on_refinement = attrs
+        .iter()
+        .any(|attr| attr.path.is_ident("refineable") && attr.tokens.to_string().contains("debug"));
+
     let refinement_ident = format_ident!("{}Refinement", ident);
     let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
 
@@ -120,6 +125,41 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
         })
         .collect();
 
+    let debug_impl = if impl_debug_on_refinement {
+        let refinement_field_debugs: Vec<TokenStream2> = fields
+            .iter()
+            .map(|field| {
+                let name = &field.ident;
+                quote! {
+                    if self.#name.is_some() {
+                        debug_struct.field(stringify!(#name), &self.#name);
+                    } else {
+                        all_some = false;
+                    }
+                }
+            })
+            .collect();
+
+        quote! {
+            impl #impl_generics std::fmt::Debug for #refinement_ident #ty_generics
+                #where_clause
+            {
+                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+                    let mut debug_struct = f.debug_struct(stringify!(#refinement_ident));
+                    let mut all_some = true;
+                    #( #refinement_field_debugs )*
+                    if all_some {
+                        debug_struct.finish()
+                    } else {
+                        debug_struct.finish_non_exhaustive()
+                    }
+                }
+            }
+        }
+    } else {
+        quote! {}
+    };
+
     let gen = quote! {
         #[derive(Default, Clone)]
         pub struct #refinement_ident #impl_generics {
@@ -145,8 +185,22 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
                 #( #refinement_field_assignments )*
             }
         }
-    };
 
+        impl #impl_generics #refinement_ident #ty_generics
+            #where_clause
+        {
+            pub fn is_some(&self) -> bool {
+                #(
+                    if self.#field_names.is_some() {
+                        return true;
+                    }
+                )*
+                false
+            }
+        }
+
+        #debug_impl
+    };
     gen.into()
 }
 

crates/refineable/src/refineable.rs πŸ”—

@@ -1,7 +1,7 @@
 pub use derive_refineable::Refineable;
 
-pub trait Refineable {
-    type Refinement: Default;
+pub trait Refineable: Clone {
+    type Refinement: Refineable<Refinement = Self::Refinement> + Default;
 
     fn refine(&mut self, refinement: &Self::Refinement);
     fn refined(mut self, refinement: &Self::Refinement) -> Self
@@ -11,4 +11,46 @@ pub trait Refineable {
         self.refine(refinement);
         self
     }
+    fn from_refinement(refinement: &Self::Refinement) -> Self
+    where
+        Self: Default + Sized,
+    {
+        Self::default().refined(refinement)
+    }
+}
+
+pub struct RefinementCascade<S: Refineable>(Vec<Option<S::Refinement>>);
+
+impl<S: Refineable + Default> Default for RefinementCascade<S> {
+    fn default() -> Self {
+        Self(vec![Some(Default::default())])
+    }
+}
+
+#[derive(Copy, Clone)]
+pub struct CascadeSlot(usize);
+
+impl<S: Refineable + Default> RefinementCascade<S> {
+    pub fn reserve(&mut self) -> CascadeSlot {
+        self.0.push(None);
+        return CascadeSlot(self.0.len() - 1);
+    }
+
+    pub fn base(&mut self) -> &mut S::Refinement {
+        self.0[0].as_mut().unwrap()
+    }
+
+    pub fn set(&mut self, slot: CascadeSlot, refinement: Option<S::Refinement>) {
+        self.0[slot.0] = refinement
+    }
+
+    pub fn merged(&self) -> S::Refinement {
+        let mut merged = self.0[0].clone().unwrap();
+        for refinement in self.0.iter().skip(1) {
+            if let Some(refinement) = refinement {
+                merged.refine(refinement);
+            }
+        }
+        merged
+    }
 }

crates/search/src/buffer_search.rs πŸ”—

@@ -2,19 +2,16 @@ use crate::{
     history::SearchHistory,
     mode::{next_mode, SearchMode, Side},
     search_bar::{render_nav_button, render_search_mode_button},
-    CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectAllMatches,
-    SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord,
+    CycleMode, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions,
+    SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace,
+    ToggleWholeWord,
 };
 use collections::HashMap;
 use editor::Editor;
 use futures::channel::oneshot;
 use gpui::{
-    actions,
-    elements::*,
-    impl_actions,
-    platform::{CursorStyle, MouseButton},
-    Action, AnyViewHandle, AppContext, Entity, Subscription, Task, View, ViewContext, ViewHandle,
-    WindowContext,
+    actions, elements::*, impl_actions, Action, AnyViewHandle, AppContext, Entity, Subscription,
+    Task, View, ViewContext, ViewHandle, WindowContext,
 };
 use project::search::SearchQuery;
 use serde::Deserialize;
@@ -54,6 +51,11 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(BufferSearchBar::previous_history_query);
     cx.add_action(BufferSearchBar::cycle_mode);
     cx.add_action(BufferSearchBar::cycle_mode_on_pane);
+    cx.add_action(BufferSearchBar::replace_all);
+    cx.add_action(BufferSearchBar::replace_next);
+    cx.add_action(BufferSearchBar::replace_all_on_pane);
+    cx.add_action(BufferSearchBar::replace_next_on_pane);
+    cx.add_action(BufferSearchBar::toggle_replace);
     add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
     add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
 }
@@ -73,9 +75,11 @@ fn add_toggle_option_action<A: Action>(option: SearchOptions, cx: &mut AppContex
 
 pub struct BufferSearchBar {
     query_editor: ViewHandle<Editor>,
+    replacement_editor: ViewHandle<Editor>,
     active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
     active_match_index: Option<usize>,
     active_searchable_item_subscription: Option<Subscription>,
+    active_search: Option<Arc<SearchQuery>>,
     searchable_items_with_matches:
         HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
     pending_search: Option<Task<()>>,
@@ -85,6 +89,7 @@ pub struct BufferSearchBar {
     dismissed: bool,
     search_history: SearchHistory,
     current_mode: SearchMode,
+    replace_is_active: bool,
 }
 
 impl Entity for BufferSearchBar {
@@ -96,6 +101,21 @@ impl View for BufferSearchBar {
         "BufferSearchBar"
     }
 
+    fn update_keymap_context(
+        &self,
+        keymap: &mut gpui::keymap_matcher::KeymapContext,
+        cx: &AppContext,
+    ) {
+        Self::reset_to_default_keymap_context(keymap);
+        let in_replace = self
+            .replacement_editor
+            .read_with(cx, |_, cx| cx.is_self_focused())
+            .unwrap_or(false);
+        if in_replace {
+            keymap.add_identifier("in_replace");
+        }
+    }
+
     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
         if cx.is_self_focused() {
             cx.focus(&self.query_editor);
@@ -156,6 +176,9 @@ impl View for BufferSearchBar {
         self.query_editor.update(cx, |editor, cx| {
             editor.set_placeholder_text(new_placeholder_text, cx);
         });
+        self.replacement_editor.update(cx, |editor, cx| {
+            editor.set_placeholder_text("Replace with...", cx);
+        });
         let search_button_for_mode = |mode, side, cx: &mut ViewContext<BufferSearchBar>| {
             let is_active = self.current_mode == mode;
 
@@ -212,7 +235,6 @@ impl View for BufferSearchBar {
                 cx,
             )
         };
-
         let query_column = Flex::row()
             .with_child(
                 Svg::for_style(theme.search.editor_icon.clone().icon)
@@ -243,7 +265,57 @@ impl View for BufferSearchBar {
             .with_max_width(theme.search.editor.max_width)
             .with_height(theme.search.search_bar_row_height)
             .flex(1., false);
+        let should_show_replace_input = self.replace_is_active && supported_options.replacement;
 
+        let replacement = should_show_replace_input.then(|| {
+            Flex::row()
+                .with_child(
+                    Svg::for_style(theme.search.replace_icon.clone().icon)
+                        .contained()
+                        .with_style(theme.search.replace_icon.clone().container),
+                )
+                .with_child(ChildView::new(&self.replacement_editor, cx).flex(1., true))
+                .align_children_center()
+                .flex(1., true)
+                .contained()
+                .with_style(query_container_style)
+                .constrained()
+                .with_min_width(theme.search.editor.min_width)
+                .with_max_width(theme.search.editor.max_width)
+                .with_height(theme.search.search_bar_row_height)
+                .flex(1., false)
+        });
+        let replace_all = should_show_replace_input.then(|| {
+            super::replace_action(
+                ReplaceAll,
+                "Replace all",
+                "icons/replace_all.svg",
+                theme.tooltip.clone(),
+                theme.search.action_button.clone(),
+            )
+        });
+        let replace_next = should_show_replace_input.then(|| {
+            super::replace_action(
+                ReplaceNext,
+                "Replace next",
+                "icons/replace_next.svg",
+                theme.tooltip.clone(),
+                theme.search.action_button.clone(),
+            )
+        });
+        let switches_column = supported_options.replacement.then(|| {
+            Flex::row()
+                .align_children_center()
+                .with_child(super::toggle_replace_button(
+                    self.replace_is_active,
+                    theme.tooltip.clone(),
+                    theme.search.option_button_component.clone(),
+                ))
+                .constrained()
+                .with_height(theme.search.search_bar_row_height)
+                .contained()
+                .with_style(theme.search.option_button_group)
+        });
         let mode_column = Flex::row()
             .with_child(search_button_for_mode(
                 SearchMode::Text,
@@ -261,7 +333,10 @@ impl View for BufferSearchBar {
             .with_height(theme.search.search_bar_row_height);
 
         let nav_column = Flex::row()
-            .with_child(self.render_action_button("all", cx))
+            .align_children_center()
+            .with_children(replace_next)
+            .with_children(replace_all)
+            .with_child(self.render_action_button("icons/select-all.svg", cx))
             .with_child(Flex::row().with_children(match_count))
             .with_child(nav_button_for_direction("<", Direction::Prev, cx))
             .with_child(nav_button_for_direction(">", Direction::Next, cx))
@@ -271,6 +346,8 @@ impl View for BufferSearchBar {
 
         Flex::row()
             .with_child(query_column)
+            .with_children(switches_column)
+            .with_children(replacement)
             .with_child(mode_column)
             .with_child(nav_column)
             .contained()
@@ -345,9 +422,18 @@ impl BufferSearchBar {
         });
         cx.subscribe(&query_editor, Self::on_query_editor_event)
             .detach();
-
+        let replacement_editor = cx.add_view(|cx| {
+            Editor::auto_height(
+                2,
+                Some(Arc::new(|theme| theme.search.editor.input.clone())),
+                cx,
+            )
+        });
+        // cx.subscribe(&replacement_editor, Self::on_query_editor_event)
+        //     .detach();
         Self {
             query_editor,
+            replacement_editor,
             active_searchable_item: None,
             active_searchable_item_subscription: None,
             active_match_index: None,
@@ -359,6 +445,8 @@ impl BufferSearchBar {
             dismissed: true,
             search_history: SearchHistory::default(),
             current_mode: SearchMode::default(),
+            active_search: None,
+            replace_is_active: false,
         }
     }
 
@@ -441,7 +529,9 @@ impl BufferSearchBar {
     pub fn query(&self, cx: &WindowContext) -> String {
         self.query_editor.read(cx).text(cx)
     }
-
+    pub fn replacement(&self, cx: &WindowContext) -> String {
+        self.replacement_editor.read(cx).text(cx)
+    }
     pub fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
         self.active_searchable_item
             .as_ref()
@@ -477,37 +567,16 @@ impl BufferSearchBar {
     ) -> AnyElement<Self> {
         let tooltip = "Select All Matches";
         let tooltip_style = theme::current(cx).tooltip.clone();
-        let action_type_id = 0_usize;
-        let has_matches = self.active_match_index.is_some();
-        let cursor_style = if has_matches {
-            CursorStyle::PointingHand
-        } else {
-            CursorStyle::default()
-        };
-        enum ActionButton {}
-        MouseEventHandler::new::<ActionButton, _>(action_type_id, cx, |state, cx| {
-            let theme = theme::current(cx);
-            let style = theme
-                .search
-                .action_button
-                .in_state(has_matches)
-                .style_for(state);
-            Label::new(icon, style.text.clone())
-                .aligned()
-                .contained()
-                .with_style(style.container)
-        })
-        .on_click(MouseButton::Left, move |_, this, cx| {
-            this.select_all_matches(&SelectAllMatches, cx)
-        })
-        .with_cursor_style(cursor_style)
-        .with_tooltip::<ActionButton>(
-            action_type_id,
-            tooltip.to_string(),
-            Some(Box::new(SelectAllMatches)),
-            tooltip_style,
-            cx,
-        )
+
+        let theme = theme::current(cx);
+        let style = theme.search.action_button.clone();
+
+        gpui::elements::Component::element(SafeStylable::with_style(
+            theme::components::action_button::Button::action(SelectAllMatches)
+                .with_tooltip(tooltip, tooltip_style)
+                .with_contents(theme::components::svg::Svg::new(icon)),
+            style,
+        ))
         .into_any()
     }
 
@@ -688,6 +757,7 @@ impl BufferSearchBar {
         let (done_tx, done_rx) = oneshot::channel();
         let query = self.query(cx);
         self.pending_search.take();
+
         if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
             if query.is_empty() {
                 self.active_match_index.take();
@@ -695,7 +765,7 @@ impl BufferSearchBar {
                 let _ = done_tx.send(());
                 cx.notify();
             } else {
-                let query = if self.current_mode == SearchMode::Regex {
+                let query: Arc<_> = if self.current_mode == SearchMode::Regex {
                     match SearchQuery::regex(
                         query,
                         self.search_options.contains(SearchOptions::WHOLE_WORD),
@@ -703,7 +773,8 @@ impl BufferSearchBar {
                         Vec::new(),
                         Vec::new(),
                     ) {
-                        Ok(query) => query,
+                        Ok(query) => query
+                            .with_replacement(Some(self.replacement(cx)).filter(|s| !s.is_empty())),
                         Err(_) => {
                             self.query_contains_error = true;
                             cx.notify();
@@ -718,8 +789,10 @@ impl BufferSearchBar {
                         Vec::new(),
                         Vec::new(),
                     )
-                };
-
+                    .with_replacement(Some(self.replacement(cx)).filter(|s| !s.is_empty()))
+                }
+                .into();
+                self.active_search = Some(query.clone());
                 let query_text = query.as_str().to_string();
                 let matches = active_searchable_item.find_matches(query, cx);
 
@@ -810,6 +883,64 @@ impl BufferSearchBar {
             cx.propagate_action();
         }
     }
+    fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
+        if let Some(_) = &self.active_searchable_item {
+            self.replace_is_active = !self.replace_is_active;
+            cx.notify();
+        }
+    }
+    fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
+        if !self.dismissed && self.active_search.is_some() {
+            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
+                if let Some(query) = self.active_search.as_ref() {
+                    if let Some(matches) = self
+                        .searchable_items_with_matches
+                        .get(&searchable_item.downgrade())
+                    {
+                        if let Some(active_index) = self.active_match_index {
+                            let query = query.as_ref().clone().with_replacement(
+                                Some(self.replacement(cx)).filter(|rep| !rep.is_empty()),
+                            );
+                            searchable_item.replace(&matches[active_index], &query, cx);
+                        }
+
+                        self.focus_editor(&FocusEditor, cx);
+                    }
+                }
+            }
+        }
+    }
+    fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
+        if !self.dismissed && self.active_search.is_some() {
+            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
+                if let Some(query) = self.active_search.as_ref() {
+                    if let Some(matches) = self
+                        .searchable_items_with_matches
+                        .get(&searchable_item.downgrade())
+                    {
+                        let query = query.as_ref().clone().with_replacement(
+                            Some(self.replacement(cx)).filter(|rep| !rep.is_empty()),
+                        );
+                        for m in matches {
+                            searchable_item.replace(m, &query, cx);
+                        }
+
+                        self.focus_editor(&FocusEditor, cx);
+                    }
+                }
+            }
+        }
+    }
+    fn replace_next_on_pane(pane: &mut Pane, action: &ReplaceNext, cx: &mut ViewContext<Pane>) {
+        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
+            search_bar.update(cx, |bar, cx| bar.replace_next(action, cx));
+        }
+    }
+    fn replace_all_on_pane(pane: &mut Pane, action: &ReplaceAll, cx: &mut ViewContext<Pane>) {
+        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
+            search_bar.update(cx, |bar, cx| bar.replace_all(action, cx));
+        }
+    }
 }
 
 #[cfg(test)]
@@ -1539,4 +1670,109 @@ mod tests {
             assert_eq!(search_bar.search_options, SearchOptions::NONE);
         });
     }
+    #[gpui::test]
+    async fn test_replace_simple(cx: &mut TestAppContext) {
+        let (editor, search_bar) = init_test(cx);
+
+        search_bar
+            .update(cx, |search_bar, cx| {
+                search_bar.search("expression", None, cx)
+            })
+            .await
+            .unwrap();
+
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.replacement_editor.update(cx, |editor, cx| {
+                // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
+                editor.set_text("expr$1", cx);
+            });
+            search_bar.replace_all(&ReplaceAll, cx)
+        });
+        assert_eq!(
+            editor.read_with(cx, |this, cx| { this.text(cx) }),
+            r#"
+        A regular expr$1 (shortened as regex or regexp;[1] also referred to as
+        rational expr$1[2][3]) is a sequence of characters that specifies a search
+        pattern in text. Usually such patterns are used by string-searching algorithms
+        for "find" or "find and replace" operations on strings, or for input validation.
+        "#
+            .unindent()
+        );
+
+        // Search for word boundaries and replace just a single one.
+        search_bar
+            .update(cx, |search_bar, cx| {
+                search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx)
+            })
+            .await
+            .unwrap();
+
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.replacement_editor.update(cx, |editor, cx| {
+                editor.set_text("banana", cx);
+            });
+            search_bar.replace_next(&ReplaceNext, cx)
+        });
+        // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
+        assert_eq!(
+            editor.read_with(cx, |this, cx| { this.text(cx) }),
+            r#"
+        A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
+        rational expr$1[2][3]) is a sequence of characters that specifies a search
+        pattern in text. Usually such patterns are used by string-searching algorithms
+        for "find" or "find and replace" operations on strings, or for input validation.
+        "#
+            .unindent()
+        );
+        // Let's turn on regex mode.
+        search_bar
+            .update(cx, |search_bar, cx| {
+                search_bar.activate_search_mode(SearchMode::Regex, cx);
+                search_bar.search("\\[([^\\]]+)\\]", None, cx)
+            })
+            .await
+            .unwrap();
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.replacement_editor.update(cx, |editor, cx| {
+                editor.set_text("${1}number", cx);
+            });
+            search_bar.replace_all(&ReplaceAll, cx)
+        });
+        assert_eq!(
+            editor.read_with(cx, |this, cx| { this.text(cx) }),
+            r#"
+        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
+        rational expr$12number3number) is a sequence of characters that specifies a search
+        pattern in text. Usually such patterns are used by string-searching algorithms
+        for "find" or "find and replace" operations on strings, or for input validation.
+        "#
+            .unindent()
+        );
+        // Now with a whole-word twist.
+        search_bar
+            .update(cx, |search_bar, cx| {
+                search_bar.activate_search_mode(SearchMode::Regex, cx);
+                search_bar.search("a\\w+s", Some(SearchOptions::WHOLE_WORD), cx)
+            })
+            .await
+            .unwrap();
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.replacement_editor.update(cx, |editor, cx| {
+                editor.set_text("things", cx);
+            });
+            search_bar.replace_all(&ReplaceAll, cx)
+        });
+        // The only word affected by this edit should be `algorithms`, even though there's a bunch
+        // of words in this text that would match this regex if not for WHOLE_WORD.
+        assert_eq!(
+            editor.read_with(cx, |this, cx| { this.text(cx) }),
+            r#"
+        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
+        rational expr$12number3number) is a sequence of characters that specifies a search
+        pattern in text. Usually such patterns are used by string-searching things
+        for "find" or "find and replace" operations on strings, or for input validation.
+        "#
+            .unindent()
+        );
+    }
 }

crates/search/src/project_search.rs πŸ”—

@@ -34,6 +34,7 @@ use std::{
     ops::{Not, Range},
     path::PathBuf,
     sync::Arc,
+    time::{Duration, Instant},
 };
 use util::ResultExt as _;
 use workspace::{
@@ -130,6 +131,7 @@ pub struct ProjectSearchView {
 
 struct SemanticState {
     index_status: SemanticIndexStatus,
+    maintain_rate_limit: Option<Task<()>>,
     _subscription: Subscription,
 }
 
@@ -319,11 +321,28 @@ impl View for ProjectSearchView {
                 let status = semantic.index_status;
                 match status {
                     SemanticIndexStatus::Indexed => Some("Indexing complete".to_string()),
-                    SemanticIndexStatus::Indexing { remaining_files } => {
+                    SemanticIndexStatus::Indexing {
+                        remaining_files,
+                        rate_limit_expiry,
+                    } => {
                         if remaining_files == 0 {
                             Some(format!("Indexing..."))
                         } else {
-                            Some(format!("Remaining files to index: {}", remaining_files))
+                            if let Some(rate_limit_expiry) = rate_limit_expiry {
+                                let remaining_seconds =
+                                    rate_limit_expiry.duration_since(Instant::now());
+                                if remaining_seconds > Duration::from_secs(0) {
+                                    Some(format!(
+                                        "Remaining files to index (rate limit resets in {}s): {}",
+                                        remaining_seconds.as_secs(),
+                                        remaining_files
+                                    ))
+                                } else {
+                                    Some(format!("Remaining files to index: {}", remaining_files))
+                                }
+                            } else {
+                                Some(format!("Remaining files to index: {}", remaining_files))
+                            }
                         }
                     }
                     SemanticIndexStatus::NotIndexed => None,
@@ -651,9 +670,10 @@ impl ProjectSearchView {
 
             self.semantic_state = Some(SemanticState {
                 index_status: semantic_index.read(cx).status(&project),
+                maintain_rate_limit: None,
                 _subscription: cx.observe(&semantic_index, Self::semantic_index_changed),
             });
-            cx.notify();
+            self.semantic_index_changed(semantic_index, cx);
         }
     }
 
@@ -664,8 +684,25 @@ impl ProjectSearchView {
     ) {
         let project = self.model.read(cx).project.clone();
         if let Some(semantic_state) = self.semantic_state.as_mut() {
-            semantic_state.index_status = semantic_index.read(cx).status(&project);
             cx.notify();
+            semantic_state.index_status = semantic_index.read(cx).status(&project);
+            if let SemanticIndexStatus::Indexing {
+                rate_limit_expiry: Some(_),
+                ..
+            } = &semantic_state.index_status
+            {
+                if semantic_state.maintain_rate_limit.is_none() {
+                    semantic_state.maintain_rate_limit =
+                        Some(cx.spawn(|this, mut cx| async move {
+                            loop {
+                                cx.background().timer(Duration::from_secs(1)).await;
+                                this.update(&mut cx, |_, cx| cx.notify()).log_err();
+                            }
+                        }));
+                    return;
+                }
+            }
+            semantic_state.maintain_rate_limit = None;
         }
     }
 

crates/search/src/search.rs πŸ”—

@@ -8,7 +8,9 @@ use gpui::{
 pub use mode::SearchMode;
 use project::search::SearchQuery;
 pub use project_search::{ProjectSearchBar, ProjectSearchView};
-use theme::components::{action_button::Button, svg::Svg, ComponentExt, ToggleIconButtonStyle};
+use theme::components::{
+    action_button::Button, svg::Svg, ComponentExt, IconButtonStyle, ToggleIconButtonStyle,
+};
 
 pub mod buffer_search;
 mod history;
@@ -27,6 +29,7 @@ actions!(
         CycleMode,
         ToggleWholeWord,
         ToggleCaseSensitive,
+        ToggleReplace,
         SelectNextMatch,
         SelectPrevMatch,
         SelectAllMatches,
@@ -34,7 +37,9 @@ actions!(
         PreviousHistoryQuery,
         ActivateTextMode,
         ActivateSemanticMode,
-        ActivateRegexMode
+        ActivateRegexMode,
+        ReplaceAll,
+        ReplaceNext
     ]
 );
 
@@ -98,3 +103,32 @@ impl SearchOptions {
             .into_any()
     }
 }
+
+fn toggle_replace_button<V: View>(
+    active: bool,
+    tooltip_style: TooltipStyle,
+    button_style: ToggleIconButtonStyle,
+) -> AnyElement<V> {
+    Button::dynamic_action(Box::new(ToggleReplace))
+        .with_tooltip("Toggle replace", tooltip_style)
+        .with_contents(theme::components::svg::Svg::new("icons/replace.svg"))
+        .toggleable(active)
+        .with_style(button_style)
+        .element()
+        .into_any()
+}
+
+fn replace_action<V: View>(
+    action: impl Action,
+    name: &'static str,
+    icon_path: &'static str,
+    tooltip_style: TooltipStyle,
+    button_style: IconButtonStyle,
+) -> AnyElement<V> {
+    Button::dynamic_action(Box::new(action))
+        .with_tooltip(name, tooltip_style)
+        .with_contents(theme::components::svg::Svg::new(icon_path))
+        .with_style(button_style)
+        .element()
+        .into_any()
+}

crates/semantic_index/src/embedding.rs πŸ”—

@@ -7,13 +7,16 @@ use isahc::http::StatusCode;
 use isahc::prelude::Configurable;
 use isahc::{AsyncBody, Response};
 use lazy_static::lazy_static;
+use parking_lot::Mutex;
 use parse_duration::parse;
+use postage::watch;
 use rusqlite::types::{FromSql, FromSqlResult, ToSqlOutput, ValueRef};
 use rusqlite::ToSql;
 use serde::{Deserialize, Serialize};
 use std::env;
+use std::ops::Add;
 use std::sync::Arc;
-use std::time::Duration;
+use std::time::{Duration, Instant};
 use tiktoken_rs::{cl100k_base, CoreBPE};
 use util::http::{HttpClient, Request};
 
@@ -82,6 +85,8 @@ impl ToSql for Embedding {
 pub struct OpenAIEmbeddings {
     pub client: Arc<dyn HttpClient>,
     pub executor: Arc<Background>,
+    rate_limit_count_rx: watch::Receiver<Option<Instant>>,
+    rate_limit_count_tx: Arc<Mutex<watch::Sender<Option<Instant>>>>,
 }
 
 #[derive(Serialize)]
@@ -114,12 +119,16 @@ pub trait EmbeddingProvider: Sync + Send {
     async fn embed_batch(&self, spans: Vec<String>) -> Result<Vec<Embedding>>;
     fn max_tokens_per_batch(&self) -> usize;
     fn truncate(&self, span: &str) -> (String, usize);
+    fn rate_limit_expiration(&self) -> Option<Instant>;
 }
 
 pub struct DummyEmbeddings {}
 
 #[async_trait]
 impl EmbeddingProvider for DummyEmbeddings {
+    fn rate_limit_expiration(&self) -> Option<Instant> {
+        None
+    }
     async fn embed_batch(&self, spans: Vec<String>) -> Result<Vec<Embedding>> {
         // 1024 is the OpenAI Embeddings size for ada models.
         // the model we will likely be starting with.
@@ -149,6 +158,50 @@ impl EmbeddingProvider for DummyEmbeddings {
 const OPENAI_INPUT_LIMIT: usize = 8190;
 
 impl OpenAIEmbeddings {
+    pub fn new(client: Arc<dyn HttpClient>, executor: Arc<Background>) -> Self {
+        let (rate_limit_count_tx, rate_limit_count_rx) = watch::channel_with(None);
+        let rate_limit_count_tx = Arc::new(Mutex::new(rate_limit_count_tx));
+
+        OpenAIEmbeddings {
+            client,
+            executor,
+            rate_limit_count_rx,
+            rate_limit_count_tx,
+        }
+    }
+
+    fn resolve_rate_limit(&self) {
+        let reset_time = *self.rate_limit_count_tx.lock().borrow();
+
+        if let Some(reset_time) = reset_time {
+            if Instant::now() >= reset_time {
+                *self.rate_limit_count_tx.lock().borrow_mut() = None
+            }
+        }
+
+        log::trace!(
+            "resolving reset time: {:?}",
+            *self.rate_limit_count_tx.lock().borrow()
+        );
+    }
+
+    fn update_reset_time(&self, reset_time: Instant) {
+        let original_time = *self.rate_limit_count_tx.lock().borrow();
+
+        let updated_time = if let Some(original_time) = original_time {
+            if reset_time < original_time {
+                Some(reset_time)
+            } else {
+                Some(original_time)
+            }
+        } else {
+            Some(reset_time)
+        };
+
+        log::trace!("updating rate limit time: {:?}", updated_time);
+
+        *self.rate_limit_count_tx.lock().borrow_mut() = updated_time;
+    }
     async fn send_request(
         &self,
         api_key: &str,
@@ -179,6 +232,9 @@ impl EmbeddingProvider for OpenAIEmbeddings {
         50000
     }
 
+    fn rate_limit_expiration(&self) -> Option<Instant> {
+        *self.rate_limit_count_rx.borrow()
+    }
     fn truncate(&self, span: &str) -> (String, usize) {
         let mut tokens = OPENAI_BPE_TOKENIZER.encode_with_special_tokens(span);
         let output = if tokens.len() > OPENAI_INPUT_LIMIT {
@@ -203,6 +259,7 @@ impl EmbeddingProvider for OpenAIEmbeddings {
             .ok_or_else(|| anyhow!("no api key"))?;
 
         let mut request_number = 0;
+        let mut rate_limiting = false;
         let mut request_timeout: u64 = 15;
         let mut response: Response<AsyncBody>;
         while request_number < MAX_RETRIES {
@@ -229,6 +286,12 @@ impl EmbeddingProvider for OpenAIEmbeddings {
                         response.usage.total_tokens
                     );
 
+                    // If we complete a request successfully that was previously rate_limited
+                    // resolve the rate limit
+                    if rate_limiting {
+                        self.resolve_rate_limit()
+                    }
+
                     return Ok(response
                         .data
                         .into_iter()
@@ -236,6 +299,7 @@ impl EmbeddingProvider for OpenAIEmbeddings {
                         .collect());
                 }
                 StatusCode::TOO_MANY_REQUESTS => {
+                    rate_limiting = true;
                     let mut body = String::new();
                     response.body_mut().read_to_string(&mut body).await?;
 
@@ -254,6 +318,10 @@ impl EmbeddingProvider for OpenAIEmbeddings {
                         }
                     };
 
+                    // If we've previously rate limited, increment the duration but not the count
+                    let reset_time = Instant::now().add(delay_duration);
+                    self.update_reset_time(reset_time);
+
                     log::trace!(
                         "openai rate limiting: waiting {:?} until lifted",
                         &delay_duration

crates/semantic_index/src/semantic_index.rs πŸ”—

@@ -91,10 +91,7 @@ pub fn init(
         let semantic_index = SemanticIndex::new(
             fs,
             db_file_path,
-            Arc::new(OpenAIEmbeddings {
-                client: http_client,
-                executor: cx.background(),
-            }),
+            Arc::new(OpenAIEmbeddings::new(http_client, cx.background())),
             language_registry,
             cx.clone(),
         )
@@ -113,7 +110,10 @@ pub fn init(
 pub enum SemanticIndexStatus {
     NotIndexed,
     Indexed,
-    Indexing { remaining_files: usize },
+    Indexing {
+        remaining_files: usize,
+        rate_limit_expiry: Option<Instant>,
+    },
 }
 
 pub struct SemanticIndex {
@@ -293,6 +293,7 @@ impl SemanticIndex {
             } else {
                 SemanticIndexStatus::Indexing {
                     remaining_files: project_state.pending_file_count_rx.borrow().clone(),
+                    rate_limit_expiry: self.embedding_provider.rate_limit_expiration(),
                 }
             }
         } else {

crates/semantic_index/src/semantic_index_tests.rs πŸ”—

@@ -21,7 +21,7 @@ use std::{
         atomic::{self, AtomicUsize},
         Arc,
     },
-    time::SystemTime,
+    time::{Instant, SystemTime},
 };
 use unindent::Unindent;
 use util::RandomCharIter;
@@ -1275,6 +1275,10 @@ impl EmbeddingProvider for FakeEmbeddingProvider {
         200
     }
 
+    fn rate_limit_expiration(&self) -> Option<Instant> {
+        None
+    }
+
     async fn embed_batch(&self, spans: Vec<String>) -> Result<Vec<Embedding>> {
         self.embedding_count
             .fetch_add(spans.len(), atomic::Ordering::SeqCst);

crates/gpui/playground/Cargo.lock β†’ crates/storybook/Cargo.lock πŸ”—

@@ -1686,7 +1686,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
 
 [[package]]
-name = "playground"
+name = "storybook"
 version = "0.1.0"
 dependencies = [
  "gpui",

crates/storybook/Cargo.toml πŸ”—

@@ -0,0 +1,23 @@
+[package]
+name = "storybook"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[[bin]]
+name = "storybook"
+path = "src/storybook.rs"
+
+[dependencies]
+gpui2 = { path = "../gpui2" }
+anyhow.workspace = true
+log.workspace = true
+rust-embed.workspace = true
+serde.workspace = true
+settings = { path = "../settings" }
+simplelog = "0.9"
+theme = { path = "../theme" }
+util = { path = "../util" }
+
+[dev-dependencies]
+gpui2 = { path = "../gpui2", features = ["test-support"] }

crates/storybook/src/collab_panel.rs πŸ”—

@@ -0,0 +1,177 @@
+use crate::theme::{theme, Theme};
+use gpui2::{
+    elements::{div, div::ScrollState, img, svg},
+    style::{StyleHelpers, Styleable},
+    ArcCow, Element, IntoElement, ParentElement, ViewContext,
+};
+use std::marker::PhantomData;
+
+#[derive(Element)]
+pub struct CollabPanelElement<V: 'static> {
+    view_type: PhantomData<V>,
+    scroll_state: ScrollState,
+}
+
+// When I improve child view rendering, I'd like to have V implement a trait  that
+// provides the scroll state, among other things.
+pub fn collab_panel<V: 'static>(scroll_state: ScrollState) -> CollabPanelElement<V> {
+    CollabPanelElement {
+        view_type: PhantomData,
+        scroll_state,
+    }
+}
+
+impl<V: 'static> CollabPanelElement<V> {
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        // Panel
+        div()
+            .w_64()
+            .h_full()
+            .flex()
+            .flex_col()
+            .font("Zed Sans Extended")
+            .text_color(theme.middle.base.default.foreground)
+            .border_color(theme.middle.base.default.border)
+            .border()
+            .fill(theme.middle.base.default.background)
+            .child(
+                div()
+                    .w_full()
+                    .flex()
+                    .flex_col()
+                    .overflow_y_scroll(self.scroll_state.clone())
+                    // List Container
+                    .child(
+                        div()
+                            .fill(theme.lowest.base.default.background)
+                            .pb_1()
+                            .border_color(theme.lowest.base.default.border)
+                            .border_b()
+                            //:: https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state
+                            // .group()
+                            // List Section Header
+                            .child(self.list_section_header("#CRDB", true, theme))
+                            // List Item Large
+                            .child(self.list_item(
+                                "http://github.com/maxbrunsfeld.png?s=50",
+                                "maxbrunsfeld",
+                                theme,
+                            )),
+                    )
+                    .child(
+                        div()
+                            .py_2()
+                            .flex()
+                            .flex_col()
+                            .child(self.list_section_header("CHANNELS", true, theme)),
+                    )
+                    .child(
+                        div()
+                            .py_2()
+                            .flex()
+                            .flex_col()
+                            .child(self.list_section_header("CONTACTS", true, theme))
+                            .children(
+                                std::iter::repeat_with(|| {
+                                    vec![
+                                        self.list_item(
+                                            "http://github.com/as-cii.png?s=50",
+                                            "as-cii",
+                                            theme,
+                                        ),
+                                        self.list_item(
+                                            "http://github.com/nathansobo.png?s=50",
+                                            "nathansobo",
+                                            theme,
+                                        ),
+                                        self.list_item(
+                                            "http://github.com/maxbrunsfeld.png?s=50",
+                                            "maxbrunsfeld",
+                                            theme,
+                                        ),
+                                    ]
+                                })
+                                .take(10)
+                                .flatten(),
+                            ),
+                    ),
+            )
+            .child(
+                div()
+                    .h_7()
+                    .px_2()
+                    .border_t()
+                    .border_color(theme.middle.variant.default.border)
+                    .flex()
+                    .items_center()
+                    .child(
+                        div()
+                            .text_sm()
+                            .text_color(theme.middle.variant.default.foreground)
+                            .child("Find..."),
+                    ),
+            )
+    }
+
+    fn list_section_header(
+        &self,
+        label: impl Into<ArcCow<'static, str>>,
+        expanded: bool,
+        theme: &Theme,
+    ) -> impl Element<V> {
+        div()
+            .h_7()
+            .px_2()
+            .flex()
+            .justify_between()
+            .items_center()
+            .child(div().flex().gap_1().text_sm().child(label))
+            .child(
+                div().flex().h_full().gap_1().items_center().child(
+                    svg()
+                        .path(if expanded {
+                            "icons/radix/caret-down.svg"
+                        } else {
+                            "icons/radix/caret-up.svg"
+                        })
+                        .w_3p5()
+                        .h_3p5()
+                        .fill(theme.middle.variant.default.foreground),
+                ),
+            )
+    }
+
+    fn list_item(
+        &self,
+        avatar_uri: impl Into<ArcCow<'static, str>>,
+        label: impl Into<ArcCow<'static, str>>,
+        theme: &Theme,
+    ) -> impl Element<V> {
+        div()
+            .h_7()
+            .px_2()
+            .flex()
+            .items_center()
+            .hover()
+            .fill(theme.lowest.variant.hovered.background)
+            .active()
+            .fill(theme.lowest.variant.pressed.background)
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .gap_1()
+                    .text_sm()
+                    .child(
+                        img()
+                            .uri(avatar_uri)
+                            .size_3p5()
+                            .rounded_full()
+                            .fill(theme.middle.positive.default.foreground),
+                    )
+                    .child(label),
+            )
+    }
+}

crates/gpui/playground/src/components.rs β†’ crates/storybook/src/components.rs πŸ”—

@@ -1,16 +1,17 @@
-use crate::{
-    div::div,
-    element::{Element, ParentElement},
-    style::StyleHelpers,
-    text::ArcCow,
-    themes::rose_pine,
+use gpui2::{
+    elements::div, interactive::Interactive, platform::MouseButton, style::StyleHelpers, ArcCow,
+    Element, EventContext, IntoElement, ParentElement, ViewContext,
 };
-use gpui::ViewContext;
-use playground_macros::Element;
 use std::{marker::PhantomData, rc::Rc};
 
+mod icon_button;
+mod tab;
+
+pub(crate) use icon_button::{icon_button, ButtonVariant};
+pub(crate) use tab::tab;
+
 struct ButtonHandlers<V, D> {
-    click: Option<Rc<dyn Fn(&mut V, &D, &mut ViewContext<V>)>>,
+    click: Option<Rc<dyn Fn(&mut V, &D, &mut EventContext<V>)>>,
 }
 
 impl<V, D> Default for ButtonHandlers<V, D> {
@@ -19,7 +20,6 @@ impl<V, D> Default for ButtonHandlers<V, D> {
     }
 }
 
-use crate as playground;
 #[derive(Element)]
 pub struct Button<V: 'static, D: 'static> {
     handlers: ButtonHandlers<V, D>,
@@ -53,7 +53,7 @@ impl<V: 'static> Button<V, ()> {
     }
 }
 
-// Impl block for *any* button.
+// Impl block for button regardless of its data type.
 impl<V: 'static, D: 'static> Button<V, D> {
     pub fn label(mut self, label: impl Into<ArcCow<'static, str>>) -> Self {
         self.label = Some(label.into());
@@ -65,12 +65,13 @@ impl<V: 'static, D: 'static> Button<V, D> {
         self
     }
 
-    // pub fn click(self, handler: impl Fn(&mut V, &D, &mut ViewContext<V>) + 'static) -> Self {
-    //     let data = self.data.clone();
-    //     Self::click(self, MouseButton::Left, move |view, _, cx| {
-    //         handler(view, data.as_ref(), cx);
-    //     })
-    // }
+    pub fn on_click(
+        mut self,
+        handler: impl Fn(&mut V, &D, &mut EventContext<V>) + 'static,
+    ) -> Self {
+        self.handlers.click = Some(Rc::new(handler));
+        self
+    }
 }
 
 pub fn button<V>() -> Button<V, ()> {
@@ -78,23 +79,25 @@ pub fn button<V>() -> Button<V, ()> {
 }
 
 impl<V: 'static, D: 'static> Button<V, D> {
-    fn render(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
-        // TODO: Drive theme from the context
+    fn render(
+        &mut self,
+        view: &mut V,
+        cx: &mut ViewContext<V>,
+    ) -> impl IntoElement<V> + Interactive<V> {
+        // let colors = &cx.theme::<Theme>().colors;
+
         let button = div()
-            .fill(rose_pine::dawn().error(0.5))
+            // .fill(colors.error(0.5))
             .h_4()
             .children(self.label.clone());
 
-        button
-
-        // TODO: Event handling
-        // if let Some(handler) = self.handlers.click.clone() {
-        //     let data = self.data.clone();
-        //     // button.mouse_down(MouseButton::Left, move |view, event, cx| {
-        //     //     handler(view, data.as_ref(), cx)
-        //     // })
-        // } else {
-        //     button
-        // }
+        if let Some(handler) = self.handlers.click.clone() {
+            let data = self.data.clone();
+            button.on_mouse_down(MouseButton::Left, move |view, event, cx| {
+                handler(view, data.as_ref(), cx)
+            })
+        } else {
+            button
+        }
     }
 }

crates/storybook/src/components/icon_button.rs πŸ”—

@@ -0,0 +1,50 @@
+use crate::theme::theme;
+use gpui2::elements::svg;
+use gpui2::style::{StyleHelpers, Styleable};
+use gpui2::{elements::div, IntoElement};
+use gpui2::{Element, ParentElement, ViewContext};
+
+#[derive(Element)]
+pub(crate) struct IconButton {
+    path: &'static str,
+    variant: ButtonVariant,
+}
+
+#[derive(PartialEq)]
+pub enum ButtonVariant {
+    Ghost,
+    Filled,
+}
+
+pub fn icon_button<V: 'static>(path: &'static str, variant: ButtonVariant) -> impl Element<V> {
+    IconButton { path, variant }
+}
+
+impl IconButton {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        let mut div = div();
+        if self.variant == ButtonVariant::Filled {
+            div = div.fill(theme.highest.on.default.background);
+        }
+
+        div.w_7()
+            .h_6()
+            .flex()
+            .items_center()
+            .justify_center()
+            .rounded_md()
+            .hover()
+            .fill(theme.highest.base.hovered.background)
+            .active()
+            .fill(theme.highest.base.pressed.background)
+            .child(
+                svg()
+                    .path(self.path)
+                    .w_4()
+                    .h_4()
+                    .fill(theme.highest.variant.default.foreground),
+            )
+    }
+}

crates/storybook/src/components/tab.rs πŸ”—

@@ -0,0 +1,55 @@
+use crate::theme::theme;
+use gpui2::style::{StyleHelpers, Styleable};
+use gpui2::{elements::div, IntoElement};
+use gpui2::{Element, ParentElement, ViewContext};
+
+#[derive(Element)]
+pub(crate) struct Tab {
+    title: &'static str,
+    enabled: bool,
+}
+
+pub fn tab<V: 'static>(title: &'static str, enabled: bool) -> impl Element<V> {
+    Tab { title, enabled }
+}
+
+impl Tab {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            .px_2()
+            .py_0p5()
+            .flex()
+            .items_center()
+            .justify_center()
+            .rounded_lg()
+            .fill(if self.enabled {
+                theme.highest.on.default.background
+            } else {
+                theme.highest.base.default.background
+            })
+            .hover()
+            .fill(if self.enabled {
+                theme.highest.on.hovered.background
+            } else {
+                theme.highest.base.hovered.background
+            })
+            .active()
+            .fill(if self.enabled {
+                theme.highest.on.pressed.background
+            } else {
+                theme.highest.base.pressed.background
+            })
+            .child(
+                div()
+                    .text_sm()
+                    .text_color(if self.enabled {
+                        theme.highest.base.default.foreground
+                    } else {
+                        theme.highest.variant.default.foreground
+                    })
+                    .child(self.title),
+            )
+    }
+}

crates/storybook/src/element_ext.rs πŸ”—

@@ -0,0 +1,22 @@
+use crate::theme::{Theme, Themed};
+use gpui2::Element;
+use std::marker::PhantomData;
+
+pub trait ElementExt<V: 'static>: Element<V> {
+    fn themed(self, theme: Theme) -> Themed<V, Self>
+    where
+        Self: Sized;
+}
+
+impl<V: 'static, E: Element<V>> ElementExt<V> for E {
+    fn themed(self, theme: Theme) -> Themed<V, Self>
+    where
+        Self: Sized,
+    {
+        Themed {
+            child: self,
+            theme,
+            view_type: PhantomData,
+        }
+    }
+}

crates/storybook/src/modules/tab_bar.rs πŸ”—

@@ -0,0 +1,82 @@
+use std::marker::PhantomData;
+
+use crate::components::{icon_button, tab, ButtonVariant};
+use crate::theme::theme;
+use gpui2::elements::div::ScrollState;
+use gpui2::style::StyleHelpers;
+use gpui2::{elements::div, IntoElement};
+use gpui2::{Element, ParentElement, ViewContext};
+
+#[derive(Element)]
+pub struct TabBar<V: 'static> {
+    view_type: PhantomData<V>,
+    scroll_state: ScrollState,
+}
+
+pub fn tab_bar<V: 'static>(scroll_state: ScrollState) -> TabBar<V> {
+    TabBar {
+        view_type: PhantomData,
+        scroll_state,
+    }
+}
+
+impl<V: 'static> TabBar<V> {
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            .w_full()
+            .flex()
+            // Left Side
+            .child(
+                div()
+                    .px_1()
+                    .flex()
+                    .flex_none()
+                    .gap_2()
+                    // Nav Buttons
+                    .child(
+                        div()
+                            .flex()
+                            .items_center()
+                            .gap_px()
+                            .child(icon_button("icons/arrow_left.svg", ButtonVariant::Filled))
+                            .child(icon_button("icons/arrow_right.svg", ButtonVariant::Ghost)),
+                    ),
+            )
+            .child(
+                div().w_0().flex_1().h_full().child(
+                    div()
+                        .flex()
+                        .gap_px()
+                        .overflow_x_scroll(self.scroll_state.clone())
+                        .child(tab("Cargo.toml", false))
+                        .child(tab("Channels Panel", true))
+                        .child(tab("channels_panel.rs", false))
+                        .child(tab("workspace.rs", false))
+                        .child(tab("icon_button.rs", false))
+                        .child(tab("storybook.rs", false))
+                        .child(tab("theme.rs", false))
+                        .child(tab("theme_registry.rs", false))
+                        .child(tab("styleable_helpers.rs", false)),
+                ),
+            )
+            // Right Side
+            .child(
+                div()
+                    .px_1()
+                    .flex()
+                    .flex_none()
+                    .gap_2()
+                    // Nav Buttons
+                    .child(
+                        div()
+                            .flex()
+                            .items_center()
+                            .gap_px()
+                            .child(icon_button("icons/plus.svg", ButtonVariant::Ghost))
+                            .child(icon_button("icons/split.svg", ButtonVariant::Ghost)),
+                    ),
+            )
+    }
+}

crates/storybook/src/storybook.rs πŸ”—

@@ -0,0 +1,110 @@
+#![allow(dead_code, unused_variables)]
+
+use crate::theme::Theme;
+use ::theme as legacy_theme;
+use element_ext::ElementExt;
+use gpui2::{serde_json, vec2f, view, Element, RectF, ViewContext, WindowBounds};
+use legacy_theme::ThemeSettings;
+use log::LevelFilter;
+use settings::{default_settings, SettingsStore};
+use simplelog::SimpleLogger;
+
+mod collab_panel;
+mod components;
+mod element_ext;
+mod modules;
+mod theme;
+mod workspace;
+
+gpui2::actions! {
+    storybook,
+    [ToggleInspector]
+}
+
+fn main() {
+    SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
+
+    gpui2::App::new(Assets).unwrap().run(|cx| {
+        let mut store = SettingsStore::default();
+        store
+            .set_default_settings(default_settings().as_ref(), cx)
+            .unwrap();
+        cx.set_global(store);
+        legacy_theme::init(Assets, cx);
+        // load_embedded_fonts(cx.platform().as_ref());
+
+        cx.add_window(
+            gpui2::WindowOptions {
+                bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), vec2f(1600., 900.))),
+                center: true,
+                ..Default::default()
+            },
+            |cx| {
+                view(|cx| {
+                    // cx.enable_inspector();
+                    storybook(&mut ViewContext::new(cx))
+                })
+            },
+        );
+        cx.platform().activate(true);
+    });
+}
+
+fn storybook<V: 'static>(cx: &mut ViewContext<V>) -> impl Element<V> {
+    workspace().themed(current_theme(cx))
+}
+
+// Nathan: During the transition to gpui2, we will include the base theme on the legacy Theme struct.
+fn current_theme<V: 'static>(cx: &mut ViewContext<V>) -> Theme {
+    settings::get::<ThemeSettings>(cx)
+        .theme
+        .deserialized_base_theme
+        .lock()
+        .get_or_insert_with(|| {
+            let theme: Theme =
+                serde_json::from_value(settings::get::<ThemeSettings>(cx).theme.base_theme.clone())
+                    .unwrap();
+            Box::new(theme)
+        })
+        .downcast_ref::<Theme>()
+        .unwrap()
+        .clone()
+}
+
+use anyhow::{anyhow, Result};
+use gpui2::AssetSource;
+use rust_embed::RustEmbed;
+use workspace::workspace;
+
+#[derive(RustEmbed)]
+#[folder = "../../assets"]
+#[include = "themes/**/*"]
+#[include = "fonts/**/*"]
+#[include = "icons/**/*"]
+#[exclude = "*.DS_Store"]
+pub struct Assets;
+
+impl AssetSource for Assets {
+    fn load(&self, path: &str) -> Result<std::borrow::Cow<[u8]>> {
+        Self::get(path)
+            .map(|f| f.data)
+            .ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
+    }
+
+    fn list(&self, path: &str) -> Vec<std::borrow::Cow<'static, str>> {
+        Self::iter().filter(|p| p.starts_with(path)).collect()
+    }
+}
+
+// fn load_embedded_fonts(platform: &dyn gpui2::Platform) {
+//     let font_paths = Assets.list("fonts");
+//     let mut embedded_fonts = Vec::new();
+//     for font_path in &font_paths {
+//         if font_path.ends_with(".ttf") {
+//             let font_path = &*font_path;
+//             let font_bytes = Assets.load(font_path).unwrap().to_vec();
+//             embedded_fonts.push(Arc::from(font_bytes));
+//         }
+//     }
+//     platform.fonts().add_fonts(&embedded_fonts).unwrap();
+// }

crates/storybook/src/theme.rs πŸ”—

@@ -0,0 +1,192 @@
+use gpui2::{
+    color::Hsla, element::Element, serde_json, AppContext, IntoElement, Vector2F, ViewContext,
+    WindowContext,
+};
+use serde::{de::Visitor, Deserialize, Deserializer};
+use std::{collections::HashMap, fmt, marker::PhantomData};
+use theme::ThemeSettings;
+
+#[derive(Deserialize, Clone, Default, Debug)]
+pub struct Theme {
+    pub name: String,
+    pub is_light: bool,
+    pub lowest: Layer,
+    pub middle: Layer,
+    pub highest: Layer,
+    pub popover_shadow: Shadow,
+    pub modal_shadow: Shadow,
+    #[serde(deserialize_with = "deserialize_player_colors")]
+    pub players: Vec<PlayerColors>,
+    #[serde(deserialize_with = "deserialize_syntax_colors")]
+    pub syntax: HashMap<String, Hsla>,
+}
+
+#[derive(Deserialize, Clone, Default, Debug)]
+pub struct Layer {
+    pub base: StyleSet,
+    pub variant: StyleSet,
+    pub on: StyleSet,
+    pub accent: StyleSet,
+    pub positive: StyleSet,
+    pub warning: StyleSet,
+    pub negative: StyleSet,
+}
+
+#[derive(Deserialize, Clone, Default, Debug)]
+pub struct StyleSet {
+    #[serde(rename = "default")]
+    pub default: ContainerColors,
+    pub hovered: ContainerColors,
+    pub pressed: ContainerColors,
+    pub active: ContainerColors,
+    pub disabled: ContainerColors,
+    pub inverted: ContainerColors,
+}
+
+#[derive(Deserialize, Clone, Default, Debug)]
+pub struct ContainerColors {
+    pub background: Hsla,
+    pub foreground: Hsla,
+    pub border: Hsla,
+}
+
+#[derive(Deserialize, Clone, Default, Debug)]
+pub struct PlayerColors {
+    pub selection: Hsla,
+    pub cursor: Hsla,
+}
+
+#[derive(Deserialize, Clone, Default, Debug)]
+pub struct Shadow {
+    pub blur: u8,
+    pub color: Hsla,
+    pub offset: Vec<u8>,
+}
+
+fn deserialize_player_colors<'de, D>(deserializer: D) -> Result<Vec<PlayerColors>, D::Error>
+where
+    D: Deserializer<'de>,
+{
+    struct PlayerArrayVisitor;
+
+    impl<'de> Visitor<'de> for PlayerArrayVisitor {
+        type Value = Vec<PlayerColors>;
+
+        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+            formatter.write_str("an object with integer keys")
+        }
+
+        fn visit_map<A: serde::de::MapAccess<'de>>(
+            self,
+            mut map: A,
+        ) -> Result<Self::Value, A::Error> {
+            let mut players = Vec::with_capacity(8);
+            while let Some((key, value)) = map.next_entry::<usize, PlayerColors>()? {
+                if key < 8 {
+                    players.push(value);
+                } else {
+                    return Err(serde::de::Error::invalid_value(
+                        serde::de::Unexpected::Unsigned(key as u64),
+                        &"a key in range 0..7",
+                    ));
+                }
+            }
+            Ok(players)
+        }
+    }
+
+    deserializer.deserialize_map(PlayerArrayVisitor)
+}
+
+fn deserialize_syntax_colors<'de, D>(deserializer: D) -> Result<HashMap<String, Hsla>, D::Error>
+where
+    D: serde::Deserializer<'de>,
+{
+    #[derive(Deserialize)]
+    struct ColorWrapper {
+        color: Hsla,
+    }
+
+    struct SyntaxVisitor;
+
+    impl<'de> Visitor<'de> for SyntaxVisitor {
+        type Value = HashMap<String, Hsla>;
+
+        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+            formatter.write_str("a map with keys and objects with a single color field as values")
+        }
+
+        fn visit_map<M>(self, mut map: M) -> Result<HashMap<String, Hsla>, M::Error>
+        where
+            M: serde::de::MapAccess<'de>,
+        {
+            let mut result = HashMap::new();
+            while let Some(key) = map.next_key()? {
+                let wrapper: ColorWrapper = map.next_value()?; // Deserialize values as Hsla
+                result.insert(key, wrapper.color);
+            }
+            Ok(result)
+        }
+    }
+    deserializer.deserialize_map(SyntaxVisitor)
+}
+
+#[derive(IntoElement)]
+pub struct Themed<V: 'static, E: Element<V>> {
+    pub(crate) theme: Theme,
+    pub(crate) child: E,
+    pub(crate) view_type: PhantomData<V>,
+}
+
+impl<V: 'static, E: Element<V>> Element<V> for Themed<V, E> {
+    type PaintState = E::PaintState;
+
+    fn layout(
+        &mut self,
+        view: &mut V,
+        cx: &mut ViewContext<V>,
+    ) -> anyhow::Result<(gpui2::LayoutId, Self::PaintState)>
+    where
+        Self: Sized,
+    {
+        cx.push_theme(self.theme.clone());
+        let result = self.child.layout(view, cx);
+        cx.pop_theme();
+        result
+    }
+
+    fn paint(
+        &mut self,
+        view: &mut V,
+        parent_origin: Vector2F,
+        layout: &gpui2::Layout,
+        state: &mut Self::PaintState,
+        cx: &mut ViewContext<V>,
+    ) where
+        Self: Sized,
+    {
+        cx.push_theme(self.theme.clone());
+        self.child.paint(view, parent_origin, layout, state, cx);
+        cx.pop_theme();
+    }
+}
+
+fn preferred_theme<V: 'static>(cx: &AppContext) -> Theme {
+    settings::get::<ThemeSettings>(cx)
+        .theme
+        .deserialized_base_theme
+        .lock()
+        .get_or_insert_with(|| {
+            let theme: Theme =
+                serde_json::from_value(settings::get::<ThemeSettings>(cx).theme.base_theme.clone())
+                    .unwrap();
+            Box::new(theme)
+        })
+        .downcast_ref::<Theme>()
+        .unwrap()
+        .clone()
+}
+
+pub fn theme<'a>(cx: &'a WindowContext) -> &'a Theme {
+    cx.theme::<Theme>()
+}

crates/storybook/src/workspace.rs πŸ”—

@@ -0,0 +1,448 @@
+use crate::{collab_panel::collab_panel, modules::tab_bar, theme::theme};
+use gpui2::{
+    elements::{div, div::ScrollState, img, svg},
+    style::{StyleHelpers, Styleable},
+    Element, IntoElement, ParentElement, ViewContext,
+};
+
+#[derive(Element, Default)]
+struct WorkspaceElement {
+    left_scroll_state: ScrollState,
+    right_scroll_state: ScrollState,
+    tab_bar_scroll_state: ScrollState,
+}
+
+pub fn workspace<V: 'static>() -> impl Element<V> {
+    WorkspaceElement::default()
+}
+
+impl WorkspaceElement {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            .size_full()
+            .flex()
+            .flex_col()
+            .font("Zed Sans Extended")
+            .gap_0()
+            .justify_start()
+            .items_start()
+            .text_color(theme.lowest.base.default.foreground)
+            .fill(theme.middle.base.default.background)
+            .child(titlebar())
+            .child(
+                div()
+                    .flex_1()
+                    .w_full()
+                    .flex()
+                    .flex_row()
+                    .overflow_hidden()
+                    .child(collab_panel(self.left_scroll_state.clone()))
+                    .child(
+                        div()
+                            .h_full()
+                            .flex_1()
+                            .fill(theme.highest.base.default.background)
+                            .child(
+                                div()
+                                    .flex()
+                                    .flex_col()
+                                    .flex_1()
+                                    .child(tab_bar(self.tab_bar_scroll_state.clone())),
+                            ),
+                    )
+                    .child(collab_panel(self.right_scroll_state.clone())),
+            )
+            .child(statusbar())
+    }
+}
+
+#[derive(Element)]
+struct TitleBar;
+
+pub fn titlebar<V: 'static>() -> impl Element<V> {
+    TitleBar
+}
+
+impl TitleBar {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        div()
+            .flex()
+            .items_center()
+            .justify_between()
+            .w_full()
+            .h_8()
+            .fill(theme.lowest.base.default.background)
+            .child(self.left_group(cx))
+            .child(self.right_group(cx))
+    }
+
+    fn left_group<V: 'static>(&mut self, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        div()
+            .flex()
+            .items_center()
+            .h_full()
+            .gap_4()
+            .px_2()
+            // === Traffic Lights === //
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .gap_2()
+                    .child(
+                        div()
+                            .w_3()
+                            .h_3()
+                            .rounded_full()
+                            .fill(theme.lowest.positive.default.foreground),
+                    )
+                    .child(
+                        div()
+                            .w_3()
+                            .h_3()
+                            .rounded_full()
+                            .fill(theme.lowest.warning.default.foreground),
+                    )
+                    .child(
+                        div()
+                            .w_3()
+                            .h_3()
+                            .rounded_full()
+                            .fill(theme.lowest.negative.default.foreground),
+                    ),
+            )
+            // === Project Info === //
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .gap_1()
+                    .child(
+                        div()
+                            .h_full()
+                            .flex()
+                            .items_center()
+                            .justify_center()
+                            .px_2()
+                            .rounded_md()
+                            .hover()
+                            .fill(theme.lowest.base.hovered.background)
+                            .active()
+                            .fill(theme.lowest.base.pressed.background)
+                            .child(div().text_sm().child("project")),
+                    )
+                    .child(
+                        div()
+                            .h_full()
+                            .flex()
+                            .items_center()
+                            .justify_center()
+                            .px_2()
+                            .rounded_md()
+                            .text_color(theme.lowest.variant.default.foreground)
+                            .hover()
+                            .fill(theme.lowest.base.hovered.background)
+                            .active()
+                            .fill(theme.lowest.base.pressed.background)
+                            .child(div().text_sm().child("branch")),
+                    ),
+            )
+    }
+
+    fn right_group<V: 'static>(&mut self, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        div()
+            .flex()
+            .items_center()
+            .h_full()
+            .gap_3()
+            .px_2()
+            // === Actions === //
+            .child(
+                div().child(
+                    div().flex().items_center().gap_1().child(
+                        div().size_4().flex().items_center().justify_center().child(
+                            svg()
+                                .path("icons/exit.svg")
+                                .size_4()
+                                .fill(theme.lowest.base.default.foreground),
+                        ),
+                    ),
+                ),
+            )
+            .child(div().w_px().h_3().fill(theme.lowest.base.default.border))
+            // === Comms === //
+            .child(
+                div().child(
+                    div()
+                        .flex()
+                        .items_center()
+                        .gap_px()
+                        .child(
+                            div()
+                                .px_2()
+                                .py_1()
+                                .rounded_md()
+                                .h_full()
+                                .flex()
+                                .items_center()
+                                .justify_center()
+                                .hover()
+                                .fill(theme.lowest.base.hovered.background)
+                                .active()
+                                .fill(theme.lowest.base.pressed.background)
+                                .child(
+                                    svg()
+                                        .path("icons/microphone.svg")
+                                        .size_3p5()
+                                        .fill(theme.lowest.base.default.foreground),
+                                ),
+                        )
+                        .child(
+                            div()
+                                .px_2()
+                                .py_1()
+                                .rounded_md()
+                                .h_full()
+                                .flex()
+                                .items_center()
+                                .justify_center()
+                                .hover()
+                                .fill(theme.lowest.base.hovered.background)
+                                .active()
+                                .fill(theme.lowest.base.pressed.background)
+                                .child(
+                                    svg()
+                                        .path("icons/radix/speaker-loud.svg")
+                                        .size_3p5()
+                                        .fill(theme.lowest.base.default.foreground),
+                                ),
+                        )
+                        .child(
+                            div()
+                                .px_2()
+                                .py_1()
+                                .rounded_md()
+                                .h_full()
+                                .flex()
+                                .items_center()
+                                .justify_center()
+                                .hover()
+                                .fill(theme.lowest.base.hovered.background)
+                                .active()
+                                .fill(theme.lowest.base.pressed.background)
+                                .child(
+                                    svg()
+                                        .path("icons/radix/desktop.svg")
+                                        .size_3p5()
+                                        .fill(theme.lowest.base.default.foreground),
+                                ),
+                        ),
+                ),
+            )
+            .child(div().w_px().h_3().fill(theme.lowest.base.default.border))
+            // User Group
+            .child(
+                div().child(
+                    div()
+                        .px_1()
+                        .py_1()
+                        .flex()
+                        .items_center()
+                        .justify_center()
+                        .rounded_md()
+                        .gap_0p5()
+                        .hover()
+                        .fill(theme.lowest.base.hovered.background)
+                        .active()
+                        .fill(theme.lowest.base.pressed.background)
+                        .child(
+                            img()
+                                .uri("https://avatars.githubusercontent.com/u/1714999?v=4")
+                                .size_4()
+                                .rounded_md()
+                                .fill(theme.middle.on.default.foreground),
+                        )
+                        .child(
+                            svg()
+                                .path("icons/caret_down_8.svg")
+                                .w_2()
+                                .h_2()
+                                .fill(theme.lowest.variant.default.foreground),
+                        ),
+                ),
+            )
+    }
+}
+
+// ================================================================================ //
+
+#[derive(Element)]
+struct StatusBar;
+
+pub fn statusbar<V: 'static>() -> impl Element<V> {
+    StatusBar
+}
+
+impl StatusBar {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        div()
+            .flex()
+            .items_center()
+            .justify_between()
+            .w_full()
+            .h_8()
+            .fill(theme.lowest.base.default.background)
+            .child(self.left_group(cx))
+            .child(self.right_group(cx))
+    }
+
+    fn left_group<V: 'static>(&mut self, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        div()
+            .flex()
+            .items_center()
+            .h_full()
+            .gap_4()
+            .px_2()
+            // === Tools === //
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .gap_1()
+                    .child(
+                        div()
+                            .w_6()
+                            .h_full()
+                            .flex()
+                            .items_center()
+                            .justify_center()
+                            .child(
+                                svg()
+                                    .path("icons/project.svg")
+                                    .w_4()
+                                    .h_4()
+                                    .fill(theme.lowest.base.default.foreground),
+                            ),
+                    )
+                    .child(
+                        div()
+                            .w_6()
+                            .h_full()
+                            .flex()
+                            .items_center()
+                            .justify_center()
+                            .child(
+                                svg()
+                                    .path("icons/conversations.svg")
+                                    .w_4()
+                                    .h_4()
+                                    .fill(theme.lowest.base.default.foreground),
+                            ),
+                    )
+                    .child(
+                        div()
+                            .w_6()
+                            .h_full()
+                            .flex()
+                            .items_center()
+                            .justify_center()
+                            .child(
+                                svg()
+                                    .path("icons/file_icons/notebook.svg")
+                                    .w_4()
+                                    .h_4()
+                                    .fill(theme.lowest.accent.default.foreground),
+                            ),
+                    ),
+            )
+            // === Diagnostics === //
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .gap_2()
+                    .child(
+                        div()
+                            .h_full()
+                            .flex()
+                            .items_center()
+                            .justify_center()
+                            .gap_0p5()
+                            .px_1()
+                            .text_color(theme.lowest.variant.default.foreground)
+                            .hover()
+                            .fill(theme.lowest.base.hovered.background)
+                            .active()
+                            .fill(theme.lowest.base.pressed.background)
+                            .child(
+                                svg()
+                                    .path("icons/error.svg")
+                                    .w_4()
+                                    .h_4()
+                                    .fill(theme.lowest.negative.default.foreground),
+                            )
+                            .child(div().text_sm().child("2")),
+                    )
+                    .child(
+                        div()
+                            .text_sm()
+                            .text_color(theme.lowest.variant.default.foreground)
+                            .child("Something is wrong"),
+                    ),
+            )
+    }
+
+    fn right_group<V: 'static>(&mut self, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        div()
+            .flex()
+            .items_center()
+            .h_full()
+            .gap_4()
+            .px_2()
+            // === Tools === //
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .gap_1()
+                    .child(
+                        div()
+                            .w_6()
+                            .h_full()
+                            .flex()
+                            .items_center()
+                            .justify_center()
+                            .child(
+                                svg()
+                                    .path("icons/check_circle.svg")
+                                    .w_4()
+                                    .h_4()
+                                    .fill(theme.lowest.base.default.foreground),
+                            ),
+                    )
+                    .child(
+                        div()
+                            .w_6()
+                            .h_full()
+                            .flex()
+                            .items_center()
+                            .justify_center()
+                            .child(
+                                svg()
+                                    .path("icons/copilot.svg")
+                                    .w_4()
+                                    .h_4()
+                                    .fill(theme.lowest.accent.default.foreground),
+                            ),
+                    ),
+            )
+    }
+}

crates/terminal_view/README.md πŸ”—

@@ -2,13 +2,13 @@ Design notes:
 
 This crate is split into two conceptual halves:
 - The terminal.rs file and the src/mappings/ folder, these contain the code for interacting with Alacritty and maintaining the pty event loop. Some behavior in this file is constrained by terminal protocols and standards. The Zed init function is also placed here.
-- Everything else. These other files integrate the `Terminal` struct created in terminal.rs into the rest of GPUI. The main entry point for GPUI is the terminal_view.rs file and the modal.rs file. 
+- Everything else. These other files integrate the `Terminal` struct created in terminal.rs into the rest of GPUI. The main entry point for GPUI is the terminal_view.rs file and the modal.rs file.
 
 ttys are created externally, and so can fail in unexpected ways. However, GPUI currently does not have an API for models than can fail to instantiate. `TerminalBuilder` solves this by using Rust's type system to split tty instantiation into a 2 step process: first attempt to create the file handles with `TerminalBuilder::new()`, check the result, then call `TerminalBuilder::subscribe(cx)` from within a model context.
 
 The TerminalView struct abstracts over failed and successful terminals, passing focus through to the associated view and allowing clients to build a terminal without worrying about errors.
 
-#Input 
+#Input
 
 There are currently many distinct paths for getting keystrokes to the terminal:
 
@@ -18,6 +18,6 @@ There are currently many distinct paths for getting keystrokes to the terminal:
 
 3. IME text. When the special character mappings fail, we pass the keystroke back to GPUI to hand it to the IME system. This comes back to us in the `View::replace_text_in_range()` method, and we then send that to the terminal directly, bypassing `try_keystroke()`.
 
-4. Pasted text has a separate pathway. 
+4. Pasted text has a separate pathway.
 
-Generally, there's a distinction between 'keystrokes that need to be mapped' and 'strings which need to be written'. I've attempted to unify these under the '.try_keystroke()' API and the `.input()` API (which try_keystroke uses) so we have consistent input handling across the terminal
+Generally, there's a distinction between 'keystrokes that need to be mapped' and 'strings which need to be written'. I've attempted to unify these under the '.try_keystroke()' API and the `.input()` API (which try_keystroke uses) so we have consistent input handling across the terminal

crates/terminal_view/src/terminal_element.rs πŸ”—

@@ -10,9 +10,8 @@ use gpui::{
     platform::{CursorStyle, MouseButton},
     serde_json::json,
     text_layout::{Line, RunStyle},
-    AnyElement, Element, EventContext, FontCache, LayoutContext, ModelContext, MouseRegion,
-    PaintContext, Quad, SceneBuilder, SizeConstraint, TextLayoutCache, ViewContext,
-    WeakModelHandle,
+    AnyElement, Element, EventContext, FontCache, ModelContext, MouseRegion, Quad, SizeConstraint,
+    TextLayoutCache, ViewContext, WeakModelHandle, WindowContext,
 };
 use itertools::Itertools;
 use language::CursorShape;
@@ -86,12 +85,11 @@ impl LayoutCell {
 
     fn paint(
         &self,
-        scene: &mut SceneBuilder,
         origin: Vector2F,
         layout: &LayoutState,
         visible_bounds: RectF,
         _view: &mut TerminalView,
-        cx: &mut ViewContext<TerminalView>,
+        cx: &mut WindowContext,
     ) {
         let pos = {
             let point = self.point;
@@ -102,7 +100,7 @@ impl LayoutCell {
         };
 
         self.text
-            .paint(scene, pos, visible_bounds, layout.size.line_height, cx);
+            .paint(pos, visible_bounds, layout.size.line_height, cx);
     }
 }
 
@@ -132,11 +130,10 @@ impl LayoutRect {
 
     fn paint(
         &self,
-        scene: &mut SceneBuilder,
         origin: Vector2F,
         layout: &LayoutState,
         _view: &mut TerminalView,
-        _cx: &mut ViewContext<TerminalView>,
+        cx: &mut ViewContext<TerminalView>,
     ) {
         let position = {
             let point = self.point;
@@ -150,7 +147,7 @@ impl LayoutRect {
             layout.size.line_height,
         );
 
-        scene.push_quad(Quad {
+        cx.scene().push_quad(Quad {
             bounds: RectF::new(position, size),
             background: Some(self.color),
             border: Default::default(),
@@ -387,7 +384,6 @@ impl TerminalElement {
 
     fn attach_mouse_handlers(
         &self,
-        scene: &mut SceneBuilder,
         origin: Vector2F,
         visible_bounds: RectF,
         mode: TermMode,
@@ -518,7 +514,7 @@ impl TerminalElement {
                 )
         }
 
-        scene.push_mouse_region(region);
+        cx.scene().push_mouse_region(region);
     }
 }
 
@@ -530,7 +526,7 @@ impl Element<TerminalView> for TerminalElement {
         &mut self,
         constraint: gpui::SizeConstraint,
         view: &mut TerminalView,
-        cx: &mut LayoutContext<TerminalView>,
+        cx: &mut ViewContext<TerminalView>,
     ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
         let settings = settings::get::<ThemeSettings>(cx);
         let terminal_settings = settings::get::<TerminalSettings>(cx);
@@ -733,25 +729,24 @@ impl Element<TerminalView> for TerminalElement {
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         layout: &mut Self::LayoutState,
         view: &mut TerminalView,
-        cx: &mut PaintContext<TerminalView>,
+        cx: &mut ViewContext<TerminalView>,
     ) -> Self::PaintState {
         let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
 
         //Setup element stuff
         let clip_bounds = Some(visible_bounds);
 
-        scene.paint_layer(clip_bounds, |scene| {
+        cx.paint_layer(clip_bounds, |cx| {
             let origin = bounds.origin() + vec2f(layout.gutter, 0.);
 
             // Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
-            self.attach_mouse_handlers(scene, origin, visible_bounds, layout.mode, cx);
+            self.attach_mouse_handlers(origin, visible_bounds, layout.mode, cx);
 
-            scene.push_cursor_region(gpui::CursorRegion {
+            cx.scene().push_cursor_region(gpui::CursorRegion {
                 bounds,
                 style: if layout.hyperlink_tooltip.is_some() {
                     CursorStyle::PointingHand
@@ -760,9 +755,9 @@ impl Element<TerminalView> for TerminalElement {
                 },
             });
 
-            scene.paint_layer(clip_bounds, |scene| {
+            cx.paint_layer(clip_bounds, |cx| {
                 //Start with a background color
-                scene.push_quad(Quad {
+                cx.scene().push_quad(Quad {
                     bounds: RectF::new(bounds.origin(), bounds.size()),
                     background: Some(layout.background_color),
                     border: Default::default(),
@@ -770,12 +765,12 @@ impl Element<TerminalView> for TerminalElement {
                 });
 
                 for rect in &layout.rects {
-                    rect.paint(scene, origin, layout, view, cx)
+                    rect.paint(origin, layout, view, cx);
                 }
             });
 
             //Draw Highlighted Backgrounds
-            scene.paint_layer(clip_bounds, |scene| {
+            cx.paint_layer(clip_bounds, |cx| {
                 for (relative_highlighted_range, color) in layout.relative_highlighted_ranges.iter()
                 {
                     if let Some((start_y, highlighted_range_lines)) =
@@ -789,29 +784,29 @@ impl Element<TerminalView> for TerminalElement {
                             //Copied from editor. TODO: move to theme or something
                             corner_radius: 0.15 * layout.size.line_height,
                         };
-                        hr.paint(bounds, scene);
+                        hr.paint(bounds, cx);
                     }
                 }
             });
 
             //Draw the text cells
-            scene.paint_layer(clip_bounds, |scene| {
+            cx.paint_layer(clip_bounds, |cx| {
                 for cell in &layout.cells {
-                    cell.paint(scene, origin, layout, visible_bounds, view, cx);
+                    cell.paint(origin, layout, visible_bounds, view, cx);
                 }
             });
 
             //Draw cursor
             if self.cursor_visible {
                 if let Some(cursor) = &layout.cursor {
-                    scene.paint_layer(clip_bounds, |scene| {
-                        cursor.paint(scene, origin, cx);
+                    cx.paint_layer(clip_bounds, |cx| {
+                        cursor.paint(origin, cx);
                     })
                 }
             }
 
             if let Some(element) = &mut layout.hyperlink_tooltip {
-                element.paint(scene, origin, visible_bounds, view, cx)
+                element.paint(origin, visible_bounds, view, cx)
             }
         });
     }

crates/terminal_view/src/terminal_view.rs πŸ”—

@@ -18,7 +18,7 @@ use gpui::{
     ViewHandle, WeakViewHandle,
 };
 use language::Bias;
-use project::{LocalWorktree, Project};
+use project::{search::SearchQuery, LocalWorktree, Project};
 use serde::Deserialize;
 use smallvec::{smallvec, SmallVec};
 use smol::Timer;
@@ -26,6 +26,7 @@ use std::{
     borrow::Cow,
     ops::RangeInclusive,
     path::{Path, PathBuf},
+    sync::Arc,
     time::Duration,
 };
 use terminal::{
@@ -283,7 +284,12 @@ impl TerminalView {
     pub fn deploy_context_menu(&mut self, position: Vector2F, cx: &mut ViewContext<Self>) {
         let menu_entries = vec![
             ContextMenuItem::action("Clear", Clear),
-            ContextMenuItem::action("Close", pane::CloseActiveItem),
+            ContextMenuItem::action(
+                "Close",
+                pane::CloseActiveItem {
+                    save_behavior: None,
+                },
+            ),
         ];
 
         self.context_menu.update(cx, |menu, cx| {
@@ -375,10 +381,10 @@ impl TerminalView {
 
     pub fn find_matches(
         &mut self,
-        query: project::search::SearchQuery,
+        query: Arc<project::search::SearchQuery>,
         cx: &mut ViewContext<Self>,
     ) -> Task<Vec<RangeInclusive<Point>>> {
-        let searcher = regex_search_for_query(query);
+        let searcher = regex_search_for_query(&query);
 
         if let Some(searcher) = searcher {
             self.terminal
@@ -481,7 +487,7 @@ fn possible_open_targets(
         .collect()
 }
 
-pub fn regex_search_for_query(query: project::search::SearchQuery) -> Option<RegexSearch> {
+pub fn regex_search_for_query(query: &project::search::SearchQuery) -> Option<RegexSearch> {
     let query = query.as_str();
     let searcher = RegexSearch::new(&query);
     searcher.ok()
@@ -793,6 +799,7 @@ impl SearchableItem for TerminalView {
             case: false,
             word: false,
             regex: false,
+            replacement: false,
         }
     }
 
@@ -846,10 +853,10 @@ impl SearchableItem for TerminalView {
     /// Get all of the matches for this query, should be done on the background
     fn find_matches(
         &mut self,
-        query: project::search::SearchQuery,
+        query: Arc<project::search::SearchQuery>,
         cx: &mut ViewContext<Self>,
     ) -> Task<Vec<Self::Match>> {
-        if let Some(searcher) = regex_search_for_query(query) {
+        if let Some(searcher) = regex_search_for_query(&query) {
             self.terminal()
                 .update(cx, |term, cx| term.find_matches(searcher, cx))
         } else {
@@ -893,6 +900,9 @@ impl SearchableItem for TerminalView {
 
         res
     }
+    fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext<Self>) {
+        // Replacement is not supported in terminal view, so this is a no-op.
+    }
 }
 
 ///Get's the working directory for the given workspace, respecting the user's settings.

crates/theme/src/theme.rs πŸ”—

@@ -3,18 +3,21 @@ mod theme_registry;
 mod theme_settings;
 pub mod ui;
 
-use components::{action_button::ButtonStyle, disclosure::DisclosureStyle, ToggleIconButtonStyle};
+use components::{
+    action_button::ButtonStyle, disclosure::DisclosureStyle, IconButtonStyle, ToggleIconButtonStyle,
+};
 use gpui::{
     color::Color,
-    elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle},
+    elements::{Border, ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle},
     fonts::{HighlightStyle, TextStyle},
-    platform, AppContext, AssetSource, Border, MouseState,
+    platform, AppContext, AssetSource, MouseState,
 };
+use parking_lot::Mutex;
 use schemars::JsonSchema;
 use serde::{de::DeserializeOwned, Deserialize};
 use serde_json::Value;
 use settings::SettingsStore;
-use std::{collections::HashMap, ops::Deref, sync::Arc};
+use std::{any::Any, collections::HashMap, ops::Deref, sync::Arc};
 use ui::{CheckboxStyle, CopilotCTAButton, IconStyle, ModalStyle};
 
 pub use theme_registry::*;
@@ -67,6 +70,14 @@ pub struct Theme {
     pub welcome: WelcomeStyle,
     pub titlebar: Titlebar,
     pub component_test: ComponentTest,
+    // Nathan: New elements are styled in Rust, directly from the base theme.
+    // We store it on the legacy theme so we can mix both kinds of elements during the transition.
+    #[schemars(skip)]
+    pub base_theme: serde_json::Value,
+    // A place to cache deserialized base theme.
+    #[serde(skip_deserializing)]
+    #[schemars(skip)]
+    pub deserialized_base_theme: Mutex<Option<Box<dyn Any + Send + Sync>>>,
 }
 
 #[derive(Deserialize, Default, Clone, JsonSchema)]
@@ -430,9 +441,7 @@ pub struct Search {
     pub include_exclude_editor: FindEditor,
     pub invalid_include_exclude_editor: ContainerStyle,
     pub include_exclude_inputs: ContainedText,
-    pub option_button: Toggleable<Interactive<IconButton>>,
     pub option_button_component: ToggleIconButtonStyle,
-    pub action_button: Toggleable<Interactive<ContainedText>>,
     pub match_background: Color,
     pub match_index: ContainedText,
     pub major_results_status: TextStyle,
@@ -444,6 +453,10 @@ pub struct Search {
     pub search_row_spacing: f32,
     pub option_button_height: f32,
     pub modes_container: ContainerStyle,
+    pub replace_icon: IconStyle,
+    // Used for filters and replace
+    pub option_button: Toggleable<Interactive<IconButton>>,
+    pub action_button: IconButtonStyle,
 }
 
 #[derive(Clone, Deserialize, Default, JsonSchema)]

crates/util/src/arc_cow.rs πŸ”—

@@ -0,0 +1,74 @@
+use std::sync::Arc;
+
+#[derive(PartialEq, Eq)]
+pub enum ArcCow<'a, T: ?Sized> {
+    Borrowed(&'a T),
+    Owned(Arc<T>),
+}
+
+use std::hash::{Hash, Hasher};
+
+impl<'a, T: ?Sized + Hash> Hash for ArcCow<'a, T> {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        match self {
+            Self::Borrowed(borrowed) => Hash::hash(borrowed, state),
+            Self::Owned(owned) => Hash::hash(&**owned, state),
+        }
+    }
+}
+
+impl<'a, T: ?Sized> Clone for ArcCow<'a, T> {
+    fn clone(&self) -> Self {
+        match self {
+            Self::Borrowed(borrowed) => Self::Borrowed(borrowed),
+            Self::Owned(owned) => Self::Owned(owned.clone()),
+        }
+    }
+}
+
+impl<'a, T: ?Sized> From<&'a T> for ArcCow<'a, T> {
+    fn from(s: &'a T) -> Self {
+        Self::Borrowed(s)
+    }
+}
+
+impl<T> From<Arc<T>> for ArcCow<'_, T> {
+    fn from(s: Arc<T>) -> Self {
+        Self::Owned(s)
+    }
+}
+
+impl From<String> for ArcCow<'_, str> {
+    fn from(value: String) -> Self {
+        Self::Owned(value.into())
+    }
+}
+
+impl<'a, T: ?Sized + ToOwned> std::borrow::Borrow<T> for ArcCow<'a, T> {
+    fn borrow(&self) -> &T {
+        match self {
+            ArcCow::Borrowed(borrowed) => borrowed,
+            ArcCow::Owned(owned) => owned.as_ref(),
+        }
+    }
+}
+
+impl<T: ?Sized> std::ops::Deref for ArcCow<'_, T> {
+    type Target = T;
+
+    fn deref(&self) -> &Self::Target {
+        match self {
+            ArcCow::Borrowed(s) => s,
+            ArcCow::Owned(s) => s.as_ref(),
+        }
+    }
+}
+
+impl<T: ?Sized> AsRef<T> for ArcCow<'_, T> {
+    fn as_ref(&self) -> &T {
+        match self {
+            ArcCow::Borrowed(borrowed) => borrowed,
+            ArcCow::Owned(owned) => owned.as_ref(),
+        }
+    }
+}

crates/util/src/http.rs πŸ”—

@@ -2,7 +2,7 @@ pub use anyhow::{anyhow, Result};
 use futures::future::BoxFuture;
 use isahc::config::{Configurable, RedirectPolicy};
 pub use isahc::{
-    http::{Method, Uri},
+    http::{Method, StatusCode, Uri},
     Error,
 };
 pub use isahc::{AsyncBody, Request, Response};

crates/util/src/util.rs πŸ”—

@@ -1,3 +1,4 @@
+pub mod arc_cow;
 pub mod channel;
 pub mod fs;
 pub mod github;
@@ -246,9 +247,16 @@ where
     }
 }
 
-struct Defer<F: FnOnce()>(Option<F>);
+pub struct Deferred<F: FnOnce()>(Option<F>);
 
-impl<F: FnOnce()> Drop for Defer<F> {
+impl<F: FnOnce()> Deferred<F> {
+    /// Drop without running the deferred function.
+    pub fn cancel(mut self) {
+        self.0.take();
+    }
+}
+
+impl<F: FnOnce()> Drop for Deferred<F> {
     fn drop(&mut self) {
         if let Some(f) = self.0.take() {
             f()
@@ -256,8 +264,9 @@ impl<F: FnOnce()> Drop for Defer<F> {
     }
 }
 
-pub fn defer<F: FnOnce()>(f: F) -> impl Drop {
-    Defer(Some(f))
+/// Run the given function when the returned value is dropped (unless it's cancelled).
+pub fn defer<F: FnOnce()>(f: F) -> Deferred<F> {
+    Deferred(Some(f))
 }
 
 pub struct RandomCharIter<T: Rng> {

crates/vim/Cargo.toml πŸ”—

@@ -38,6 +38,7 @@ language_selector = { path = "../language_selector"}
 [dev-dependencies]
 indoc.workspace = true
 parking_lot.workspace = true
+futures.workspace = true
 
 editor = { path = "../editor", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
@@ -47,3 +48,4 @@ util = { path = "../util", features = ["test-support"] }
 settings = { path = "../settings" }
 workspace = { path = "../workspace", features = ["test-support"] }
 theme = { path = "../theme", features = ["test-support"] }
+lsp = { path = "../lsp", features = ["test-support"] }

crates/vim/src/editor_events.rs πŸ”—

@@ -34,6 +34,9 @@ fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) {
 fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut AppContext) {
     editor.window().update(cx, |cx| {
         Vim::update(cx, |vim, cx| {
+            vim.clear_operator(cx);
+            vim.workspace_state.recording = false;
+            vim.workspace_state.recorded_actions.clear();
             if let Some(previous_editor) = vim.active_editor.clone() {
                 if previous_editor == editor.clone() {
                     vim.active_editor = None;

crates/vim/src/insert.rs πŸ”—

@@ -1,6 +1,6 @@
-use crate::{state::Mode, Vim};
+use crate::{normal::repeat, state::Mode, Vim};
 use editor::{scroll::autoscroll::Autoscroll, Bias};
-use gpui::{actions, AppContext, ViewContext};
+use gpui::{actions, Action, AppContext, ViewContext};
 use language::SelectionGoal;
 use workspace::Workspace;
 
@@ -10,23 +10,41 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(normal_before);
 }
 
-fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Workspace>) {
-    Vim::update(cx, |state, cx| {
-        state.update_active_editor(cx, |editor, cx| {
-            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-                s.move_cursors_with(|map, mut cursor, _| {
-                    *cursor.column_mut() = cursor.column().saturating_sub(1);
-                    (map.clip_point(cursor, Bias::Left), SelectionGoal::None)
+fn normal_before(_: &mut Workspace, action: &NormalBefore, cx: &mut ViewContext<Workspace>) {
+    let should_repeat = Vim::update(cx, |vim, cx| {
+        let count = vim.take_count(cx).unwrap_or(1);
+        vim.stop_recording_immediately(action.boxed_clone());
+        if count <= 1 || vim.workspace_state.replaying {
+            vim.update_active_editor(cx, |editor, cx| {
+                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                    s.move_cursors_with(|map, mut cursor, _| {
+                        *cursor.column_mut() = cursor.column().saturating_sub(1);
+                        (map.clip_point(cursor, Bias::Left), SelectionGoal::None)
+                    });
                 });
             });
-        });
-        state.switch_mode(Mode::Normal, false, cx);
-    })
+            vim.switch_mode(Mode::Normal, false, cx);
+            false
+        } else {
+            true
+        }
+    });
+
+    if should_repeat {
+        repeat::repeat(cx, true)
+    }
 }
 
 #[cfg(test)]
 mod test {
-    use crate::{state::Mode, test::VimTestContext};
+    use std::sync::Arc;
+
+    use gpui::executor::Deterministic;
+
+    use crate::{
+        state::Mode,
+        test::{NeovimBackedTestContext, VimTestContext},
+    };
 
     #[gpui::test]
     async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) {
@@ -39,4 +57,78 @@ mod test {
         assert_eq!(cx.mode(), Mode::Normal);
         cx.assert_editor_state("Tesˇt");
     }
+
+    #[gpui::test]
+    async fn test_insert_with_counts(
+        deterministic: Arc<Deterministic>,
+        cx: &mut gpui::TestAppContext,
+    ) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state("Λ‡hello\n").await;
+        cx.simulate_shared_keystrokes(["5", "i", "-", "escape"])
+            .await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("----Λ‡-hello\n").await;
+
+        cx.set_shared_state("Λ‡hello\n").await;
+        cx.simulate_shared_keystrokes(["5", "a", "-", "escape"])
+            .await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("h----Λ‡-ello\n").await;
+
+        cx.simulate_shared_keystrokes(["4", "shift-i", "-", "escape"])
+            .await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("---Λ‡-h-----ello\n").await;
+
+        cx.simulate_shared_keystrokes(["3", "shift-a", "-", "escape"])
+            .await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("----h-----ello--Λ‡-\n").await;
+
+        cx.set_shared_state("Λ‡hello\n").await;
+        cx.simulate_shared_keystrokes(["3", "o", "o", "i", "escape"])
+            .await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("hello\noi\noi\noˇi\n").await;
+
+        cx.set_shared_state("Λ‡hello\n").await;
+        cx.simulate_shared_keystrokes(["3", "shift-o", "o", "i", "escape"])
+            .await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("oi\noi\noˇi\nhello\n").await;
+    }
+
+    #[gpui::test]
+    async fn test_insert_with_repeat(
+        deterministic: Arc<Deterministic>,
+        cx: &mut gpui::TestAppContext,
+    ) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state("Λ‡hello\n").await;
+        cx.simulate_shared_keystrokes(["3", "i", "-", "escape"])
+            .await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("--Λ‡-hello\n").await;
+        cx.simulate_shared_keystrokes(["."]).await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("----Λ‡--hello\n").await;
+        cx.simulate_shared_keystrokes(["2", "."]).await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("-----Λ‡---hello\n").await;
+
+        cx.set_shared_state("Λ‡hello\n").await;
+        cx.simulate_shared_keystrokes(["2", "o", "k", "k", "escape"])
+            .await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("hello\nkk\nkˇk\n").await;
+        cx.simulate_shared_keystrokes(["."]).await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("hello\nkk\nkk\nkk\nkˇk\n").await;
+        cx.simulate_shared_keystrokes(["1", "."]).await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("hello\nkk\nkk\nkk\nkk\nkˇk\n").await;
+    }
 }

crates/vim/src/motion.rs πŸ”—

@@ -1,9 +1,9 @@
-use std::{cmp, sync::Arc};
+use std::cmp;
 
 use editor::{
     char_kind,
     display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint},
-    movement::{self, FindRange},
+    movement::{self, find_boundary, find_preceding_boundary, FindRange},
     Bias, CharKind, DisplayPoint, ToOffset,
 };
 use gpui::{actions, impl_actions, AppContext, WindowContext};
@@ -37,9 +37,11 @@ pub enum Motion {
     StartOfDocument,
     EndOfDocument,
     Matching,
-    FindForward { before: bool, text: Arc<str> },
-    FindBackward { after: bool, text: Arc<str> },
+    FindForward { before: bool, char: char },
+    FindBackward { after: bool, char: char },
     NextLineStart,
+    StartOfLineDownward,
+    EndOfLineDownward,
 }
 
 #[derive(Clone, Deserialize, PartialEq)]
@@ -65,9 +67,9 @@ struct PreviousWordStart {
 
 #[derive(Clone, Deserialize, PartialEq)]
 #[serde(rename_all = "camelCase")]
-struct Up {
+pub(crate) struct Up {
     #[serde(default)]
-    display_lines: bool,
+    pub(crate) display_lines: bool,
 }
 
 #[derive(Clone, Deserialize, PartialEq)]
@@ -93,9 +95,9 @@ struct EndOfLine {
 
 #[derive(Clone, Deserialize, PartialEq)]
 #[serde(rename_all = "camelCase")]
-struct StartOfLine {
+pub struct StartOfLine {
     #[serde(default)]
-    display_lines: bool,
+    pub(crate) display_lines: bool,
 }
 
 #[derive(Clone, Deserialize, PartialEq)]
@@ -117,6 +119,8 @@ actions!(
         EndOfDocument,
         Matching,
         NextLineStart,
+        StartOfLineDownward,
+        EndOfLineDownward,
     ]
 );
 impl_actions!(
@@ -207,6 +211,12 @@ pub fn init(cx: &mut AppContext) {
          cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
     );
     cx.add_action(|_: &mut Workspace, &NextLineStart, cx: _| motion(Motion::NextLineStart, cx));
+    cx.add_action(|_: &mut Workspace, &StartOfLineDownward, cx: _| {
+        motion(Motion::StartOfLineDownward, cx)
+    });
+    cx.add_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| {
+        motion(Motion::EndOfLineDownward, cx)
+    });
     cx.add_action(|_: &mut Workspace, action: &RepeatFind, cx: _| {
         repeat_motion(action.backwards, cx)
     })
@@ -219,11 +229,11 @@ pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
         Vim::update(cx, |vim, cx| vim.pop_operator(cx));
     }
 
-    let times = Vim::update(cx, |vim, cx| vim.pop_number_operator(cx));
+    let count = Vim::update(cx, |vim, cx| vim.take_count(cx));
     let operator = Vim::read(cx).active_operator();
     match Vim::read(cx).state().mode {
-        Mode::Normal => normal_motion(motion, operator, times, cx),
-        Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, times, cx),
+        Mode::Normal => normal_motion(motion, operator, count, cx),
+        Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, count, cx),
         Mode::Insert => {
             // Shouldn't execute a motion in insert mode. Ignoring
         }
@@ -233,25 +243,25 @@ pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
 
 fn repeat_motion(backwards: bool, cx: &mut WindowContext) {
     let find = match Vim::read(cx).workspace_state.last_find.clone() {
-        Some(Motion::FindForward { before, text }) => {
+        Some(Motion::FindForward { before, char }) => {
             if backwards {
                 Motion::FindBackward {
                     after: before,
-                    text,
+                    char,
                 }
             } else {
-                Motion::FindForward { before, text }
+                Motion::FindForward { before, char }
             }
         }
 
-        Some(Motion::FindBackward { after, text }) => {
+        Some(Motion::FindBackward { after, char }) => {
             if backwards {
                 Motion::FindForward {
                     before: after,
-                    text,
+                    char,
                 }
             } else {
-                Motion::FindBackward { after, text }
+                Motion::FindBackward { after, char }
             }
         }
         _ => return,
@@ -272,6 +282,7 @@ impl Motion {
             | EndOfDocument
             | CurrentLine
             | NextLineStart
+            | StartOfLineDownward
             | StartOfParagraph
             | EndOfParagraph => true,
             EndOfLine { .. }
@@ -282,6 +293,7 @@ impl Motion {
             | Backspace
             | Right
             | StartOfLine { .. }
+            | EndOfLineDownward
             | NextWordStart { .. }
             | PreviousWordStart { .. }
             | FirstNonWhitespace { .. }
@@ -305,6 +317,8 @@ impl Motion {
             | StartOfLine { .. }
             | StartOfParagraph
             | EndOfParagraph
+            | StartOfLineDownward
+            | EndOfLineDownward
             | NextWordStart { .. }
             | PreviousWordStart { .. }
             | FirstNonWhitespace { .. }
@@ -322,6 +336,7 @@ impl Motion {
             | EndOfDocument
             | CurrentLine
             | EndOfLine { .. }
+            | EndOfLineDownward
             | NextWordEnd { .. }
             | Matching
             | FindForward { .. }
@@ -330,6 +345,7 @@ impl Motion {
             | Backspace
             | Right
             | StartOfLine { .. }
+            | StartOfLineDownward
             | StartOfParagraph
             | EndOfParagraph
             | NextWordStart { .. }
@@ -396,22 +412,24 @@ impl Motion {
                 map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
                 SelectionGoal::None,
             ),
-            CurrentLine => (end_of_line(map, false, point), SelectionGoal::None),
+            CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
             StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
             EndOfDocument => (
                 end_of_document(map, point, maybe_times),
                 SelectionGoal::None,
             ),
             Matching => (matching(map, point), SelectionGoal::None),
-            FindForward { before, text } => (
-                find_forward(map, point, *before, text.clone(), times),
+            FindForward { before, char } => (
+                find_forward(map, point, *before, *char, times),
                 SelectionGoal::None,
             ),
-            FindBackward { after, text } => (
-                find_backward(map, point, *after, text.clone(), times),
+            FindBackward { after, char } => (
+                find_backward(map, point, *after, *char, times),
                 SelectionGoal::None,
             ),
             NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
+            StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
+            EndOfLineDownward => (next_line_end(map, point, times), SelectionGoal::None),
         };
 
         (new_point != point || infallible).then_some((new_point, goal))
@@ -793,44 +811,55 @@ fn find_forward(
     map: &DisplaySnapshot,
     from: DisplayPoint,
     before: bool,
-    target: Arc<str>,
+    target: char,
     times: usize,
 ) -> DisplayPoint {
-    map.find_while(from, target.as_ref(), |ch, _| ch != '\n')
-        .skip_while(|found_at| found_at == &from)
-        .nth(times - 1)
-        .map(|mut found| {
-            if before {
-                *found.column_mut() -= 1;
-                found = map.clip_point(found, Bias::Right);
-                found
-            } else {
-                found
-            }
-        })
-        .unwrap_or(from)
+    let mut to = from;
+    let mut found = false;
+
+    for _ in 0..times {
+        found = false;
+        to = find_boundary(map, to, FindRange::SingleLine, |_, right| {
+            found = right == target;
+            found
+        });
+    }
+
+    if found {
+        if before && to.column() > 0 {
+            *to.column_mut() -= 1;
+            map.clip_point(to, Bias::Left)
+        } else {
+            to
+        }
+    } else {
+        from
+    }
 }
 
 fn find_backward(
     map: &DisplaySnapshot,
     from: DisplayPoint,
     after: bool,
-    target: Arc<str>,
+    target: char,
     times: usize,
 ) -> DisplayPoint {
-    map.reverse_find_while(from, target.as_ref(), |ch, _| ch != '\n')
-        .skip_while(|found_at| found_at == &from)
-        .nth(times - 1)
-        .map(|mut found| {
-            if after {
-                *found.column_mut() += 1;
-                found = map.clip_point(found, Bias::Left);
-                found
-            } else {
-                found
-            }
-        })
-        .unwrap_or(from)
+    let mut to = from;
+
+    for _ in 0..times {
+        to = find_preceding_boundary(map, to, FindRange::SingleLine, |_, right| right == target);
+    }
+
+    if map.buffer_snapshot.chars_at(to.to_point(map)).next() == Some(target) {
+        if after {
+            *to.column_mut() += 1;
+            map.clip_point(to, Bias::Right)
+        } else {
+            to
+        }
+    } else {
+        from
+    }
 }
 
 fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
@@ -838,6 +867,13 @@ fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) ->
     first_non_whitespace(map, false, correct_line)
 }
 
+fn next_line_end(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
+    if times > 1 {
+        point = down(map, point, SelectionGoal::None, times - 1).0;
+    }
+    end_of_line(map, false, point)
+}
+
 #[cfg(test)]
 
 mod test {

crates/vim/src/normal.rs πŸ”—

@@ -2,6 +2,7 @@ mod case;
 mod change;
 mod delete;
 mod paste;
+pub(crate) mod repeat;
 mod scroll;
 mod search;
 pub mod substitute;
@@ -34,6 +35,7 @@ actions!(
     vim,
     [
         InsertAfter,
+        InsertBefore,
         InsertFirstNonWhitespace,
         InsertEndOfLine,
         InsertLineAbove,
@@ -44,33 +46,43 @@ actions!(
         DeleteToEndOfLine,
         Yank,
         ChangeCase,
+        JoinLines,
     ]
 );
 
 pub fn init(cx: &mut AppContext) {
+    paste::init(cx);
+    repeat::init(cx);
+    scroll::init(cx);
+    search::init(cx);
+    substitute::init(cx);
+
     cx.add_action(insert_after);
+    cx.add_action(insert_before);
     cx.add_action(insert_first_non_whitespace);
     cx.add_action(insert_end_of_line);
     cx.add_action(insert_line_above);
     cx.add_action(insert_line_below);
     cx.add_action(change_case);
-    substitute::init(cx);
-    search::init(cx);
+
     cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
         Vim::update(cx, |vim, cx| {
-            let times = vim.pop_number_operator(cx);
+            vim.record_current_action(cx);
+            let times = vim.take_count(cx);
             delete_motion(vim, Motion::Left, times, cx);
         })
     });
     cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| {
         Vim::update(cx, |vim, cx| {
-            let times = vim.pop_number_operator(cx);
+            vim.record_current_action(cx);
+            let times = vim.take_count(cx);
             delete_motion(vim, Motion::Right, times, cx);
         })
     });
     cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
         Vim::update(cx, |vim, cx| {
-            let times = vim.pop_number_operator(cx);
+            vim.start_recording(cx);
+            let times = vim.take_count(cx);
             change_motion(
                 vim,
                 Motion::EndOfLine {
@@ -83,7 +95,8 @@ pub fn init(cx: &mut AppContext) {
     });
     cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
         Vim::update(cx, |vim, cx| {
-            let times = vim.pop_number_operator(cx);
+            vim.record_current_action(cx);
+            let times = vim.take_count(cx);
             delete_motion(
                 vim,
                 Motion::EndOfLine {
@@ -94,8 +107,26 @@ pub fn init(cx: &mut AppContext) {
             );
         })
     });
-    scroll::init(cx);
-    paste::init(cx);
+    cx.add_action(|_: &mut Workspace, _: &JoinLines, cx| {
+        Vim::update(cx, |vim, cx| {
+            vim.record_current_action(cx);
+            let mut times = vim.take_count(cx).unwrap_or(1);
+            if vim.state().mode.is_visual() {
+                times = 1;
+            } else if times > 1 {
+                // 2J joins two lines together (same as J or 1J)
+                times -= 1;
+            }
+
+            vim.update_active_editor(cx, |editor, cx| {
+                editor.transact(cx, |editor, cx| {
+                    for _ in 0..times {
+                        editor.join_lines(&Default::default(), cx)
+                    }
+                })
+            })
+        })
+    })
 }
 
 pub fn normal_motion(
@@ -151,6 +182,7 @@ fn move_cursor(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut Win
 
 fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspace>) {
     Vim::update(cx, |vim, cx| {
+        vim.start_recording(cx);
         vim.switch_mode(Mode::Insert, false, cx);
         vim.update_active_editor(cx, |editor, cx| {
             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
@@ -162,12 +194,20 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspa
     });
 }
 
+fn insert_before(_: &mut Workspace, _: &InsertBefore, cx: &mut ViewContext<Workspace>) {
+    Vim::update(cx, |vim, cx| {
+        vim.start_recording(cx);
+        vim.switch_mode(Mode::Insert, false, cx);
+    });
+}
+
 fn insert_first_non_whitespace(
     _: &mut Workspace,
     _: &InsertFirstNonWhitespace,
     cx: &mut ViewContext<Workspace>,
 ) {
     Vim::update(cx, |vim, cx| {
+        vim.start_recording(cx);
         vim.switch_mode(Mode::Insert, false, cx);
         vim.update_active_editor(cx, |editor, cx| {
             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
@@ -184,6 +224,7 @@ fn insert_first_non_whitespace(
 
 fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewContext<Workspace>) {
     Vim::update(cx, |vim, cx| {
+        vim.start_recording(cx);
         vim.switch_mode(Mode::Insert, false, cx);
         vim.update_active_editor(cx, |editor, cx| {
             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
@@ -197,6 +238,7 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
 
 fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContext<Workspace>) {
     Vim::update(cx, |vim, cx| {
+        vim.start_recording(cx);
         vim.switch_mode(Mode::Insert, false, cx);
         vim.update_active_editor(cx, |editor, cx| {
             editor.transact(cx, |editor, cx| {
@@ -229,6 +271,7 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
 
 fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContext<Workspace>) {
     Vim::update(cx, |vim, cx| {
+        vim.start_recording(cx);
         vim.switch_mode(Mode::Insert, false, cx);
         vim.update_active_editor(cx, |editor, cx| {
             editor.transact(cx, |editor, cx| {
@@ -260,6 +303,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
 
 pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| {
+        vim.stop_recording();
         vim.update_active_editor(cx, |editor, cx| {
             editor.transact(cx, |editor, cx| {
                 editor.set_clip_at_line_ends(false, cx);
@@ -312,7 +356,7 @@ mod test {
 
     use crate::{
         state::Mode::{self},
-        test::{ExemptionFeatures, NeovimBackedTestContext},
+        test::NeovimBackedTestContext,
     };
 
     #[gpui::test]
@@ -718,20 +762,22 @@ mod test {
 
     #[gpui::test]
     async fn test_dd(cx: &mut gpui::TestAppContext) {
-        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "d"]);
-        cx.assert("Λ‡").await;
-        cx.assert("The Λ‡quick").await;
-        cx.assert_all(indoc! {"
-                The qˇuick
-                brown Λ‡fox
-                jumps Λ‡over"})
-            .await;
-        cx.assert_exempted(
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        cx.assert_neovim_compatible("Λ‡", ["d", "d"]).await;
+        cx.assert_neovim_compatible("The Λ‡quick", ["d", "d"]).await;
+        for marked_text in cx.each_marked_position(indoc! {"
+            The qˇuick
+            brown Λ‡fox
+            jumps Λ‡over"})
+        {
+            cx.assert_neovim_compatible(&marked_text, ["d", "d"]).await;
+        }
+        cx.assert_neovim_compatible(
             indoc! {"
                 The quick
                 Λ‡
                 brown fox"},
-            ExemptionFeatures::DeletionOnEmptyLine,
+            ["d", "d"],
         )
         .await;
     }
@@ -780,6 +826,7 @@ mod test {
     #[gpui::test]
     async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;
+
         for count in 1..=3 {
             let test_case = indoc! {"
                 ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa

crates/vim/src/normal/case.rs πŸ”—

@@ -7,7 +7,8 @@ use crate::{normal::ChangeCase, state::Mode, Vim};
 
 pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Workspace>) {
     Vim::update(cx, |vim, cx| {
-        let count = vim.pop_number_operator(cx).unwrap_or(1) as u32;
+        vim.record_current_action(cx);
+        let count = vim.take_count(cx).unwrap_or(1) as u32;
         vim.update_active_editor(cx, |editor, cx| {
             let mut ranges = Vec::new();
             let mut cursor_positions = Vec::new();
@@ -21,10 +22,16 @@ pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Works
                         ranges.push(start..end);
                         cursor_positions.push(start..start);
                     }
-                    Mode::Visual | Mode::VisualBlock => {
+                    Mode::Visual => {
                         ranges.push(selection.start..selection.end);
                         cursor_positions.push(selection.start..selection.start);
                     }
+                    Mode::VisualBlock => {
+                        ranges.push(selection.start..selection.end);
+                        if cursor_positions.len() == 0 {
+                            cursor_positions.push(selection.start..selection.start);
+                        }
+                    }
                     Mode::Insert | Mode::Normal => {
                         let start = selection.start;
                         let mut end = start;
@@ -96,6 +103,11 @@ mod test {
         cx.simulate_shared_keystrokes(["shift-v", "~"]).await;
         cx.assert_shared_state("Λ‡ABc\n").await;
 
+        // works in visual block mode
+        cx.set_shared_state("Λ‡aa\nbb\ncc").await;
+        cx.simulate_shared_keystrokes(["ctrl-v", "j", "~"]).await;
+        cx.assert_shared_state("Λ‡Aa\nBb\ncc").await;
+
         // works with multiple cursors (zed only)
         cx.set_state("aΛ‡ΓŸcdΛ‡e\n", Mode::Normal);
         cx.simulate_keystroke("~");

crates/vim/src/normal/change.rs πŸ”—

@@ -121,7 +121,7 @@ fn expand_changed_word_selection(
 mod test {
     use indoc::indoc;
 
-    use crate::test::{ExemptionFeatures, NeovimBackedTestContext};
+    use crate::test::NeovimBackedTestContext;
 
     #[gpui::test]
     async fn test_change_h(cx: &mut gpui::TestAppContext) {
@@ -239,150 +239,178 @@ mod test {
 
     #[gpui::test]
     async fn test_change_0(cx: &mut gpui::TestAppContext) {
-        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "0"]);
-        cx.assert(indoc! {"
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.assert_neovim_compatible(
+            indoc! {"
             The qˇuick
-            brown fox"})
-            .await;
-        cx.assert(indoc! {"
+            brown fox"},
+            ["c", "0"],
+        )
+        .await;
+        cx.assert_neovim_compatible(
+            indoc! {"
             The quick
             Λ‡
-            brown fox"})
-            .await;
+            brown fox"},
+            ["c", "0"],
+        )
+        .await;
     }
 
     #[gpui::test]
     async fn test_change_k(cx: &mut gpui::TestAppContext) {
-        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "k"]);
-        cx.assert(indoc! {"
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.assert_neovim_compatible(
+            indoc! {"
             The quick
             brown Λ‡fox
-            jumps over"})
-            .await;
-        cx.assert(indoc! {"
+            jumps over"},
+            ["c", "k"],
+        )
+        .await;
+        cx.assert_neovim_compatible(
+            indoc! {"
             The quick
             brown fox
-            jumps Λ‡over"})
-            .await;
-        cx.assert_exempted(
+            jumps Λ‡over"},
+            ["c", "k"],
+        )
+        .await;
+        cx.assert_neovim_compatible(
             indoc! {"
             The qˇuick
             brown fox
             jumps over"},
-            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+            ["c", "k"],
         )
         .await;
-        cx.assert_exempted(
+        cx.assert_neovim_compatible(
             indoc! {"
             Λ‡
             brown fox
             jumps over"},
-            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+            ["c", "k"],
         )
         .await;
     }
 
     #[gpui::test]
     async fn test_change_j(cx: &mut gpui::TestAppContext) {
-        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "j"]);
-        cx.assert(indoc! {"
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        cx.assert_neovim_compatible(
+            indoc! {"
             The quick
             brown Λ‡fox
-            jumps over"})
-            .await;
-        cx.assert_exempted(
+            jumps over"},
+            ["c", "j"],
+        )
+        .await;
+        cx.assert_neovim_compatible(
             indoc! {"
             The quick
             brown fox
             jumps Λ‡over"},
-            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+            ["c", "j"],
         )
         .await;
-        cx.assert(indoc! {"
+        cx.assert_neovim_compatible(
+            indoc! {"
             The qˇuick
             brown fox
-            jumps over"})
-            .await;
-        cx.assert_exempted(
+            jumps over"},
+            ["c", "j"],
+        )
+        .await;
+        cx.assert_neovim_compatible(
             indoc! {"
             The quick
             brown fox
             Λ‡"},
-            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+            ["c", "j"],
         )
         .await;
     }
 
     #[gpui::test]
     async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) {
-        let mut cx = NeovimBackedTestContext::new(cx)
-            .await
-            .binding(["c", "shift-g"]);
-        cx.assert(indoc! {"
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        cx.assert_neovim_compatible(
+            indoc! {"
             The quick
             brownˇ fox
             jumps over
-            the lazy"})
-            .await;
-        cx.assert(indoc! {"
+            the lazy"},
+            ["c", "shift-g"],
+        )
+        .await;
+        cx.assert_neovim_compatible(
+            indoc! {"
             The quick
             brownˇ fox
             jumps over
-            the lazy"})
-            .await;
-        cx.assert_exempted(
+            the lazy"},
+            ["c", "shift-g"],
+        )
+        .await;
+        cx.assert_neovim_compatible(
             indoc! {"
             The quick
             brown fox
             jumps over
             the lˇazy"},
-            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+            ["c", "shift-g"],
         )
         .await;
-        cx.assert_exempted(
+        cx.assert_neovim_compatible(
             indoc! {"
             The quick
             brown fox
             jumps over
             Λ‡"},
-            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+            ["c", "shift-g"],
         )
         .await;
     }
 
     #[gpui::test]
     async fn test_change_gg(cx: &mut gpui::TestAppContext) {
-        let mut cx = NeovimBackedTestContext::new(cx)
-            .await
-            .binding(["c", "g", "g"]);
-        cx.assert(indoc! {"
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        cx.assert_neovim_compatible(
+            indoc! {"
             The quick
             brownˇ fox
             jumps over
-            the lazy"})
-            .await;
-        cx.assert(indoc! {"
+            the lazy"},
+            ["c", "g", "g"],
+        )
+        .await;
+        cx.assert_neovim_compatible(
+            indoc! {"
             The quick
             brown fox
             jumps over
-            the lˇazy"})
-            .await;
-        cx.assert_exempted(
+            the lˇazy"},
+            ["c", "g", "g"],
+        )
+        .await;
+        cx.assert_neovim_compatible(
             indoc! {"
             The qˇuick
             brown fox
             jumps over
             the lazy"},
-            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+            ["c", "g", "g"],
         )
         .await;
-        cx.assert_exempted(
+        cx.assert_neovim_compatible(
             indoc! {"
             Λ‡
             brown fox
             jumps over
             the lazy"},
-            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+            ["c", "g", "g"],
         )
         .await;
     }
@@ -427,27 +455,17 @@ mod test {
     async fn test_repeated_cb(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;
 
-        cx.add_initial_state_exemptions(
-            indoc! {"
-            Λ‡The quick brown
-
-            fox jumps-over
-            the lazy dog
-            "},
-            ExemptionFeatures::OperatorAbortsOnFailedMotion,
-        );
-
         for count in 1..=5 {
-            cx.assert_binding_matches_all(
-                ["c", &count.to_string(), "b"],
-                indoc! {"
-                    ˇThe quˇickˇ browˇn
-                    Λ‡
-                    ˇfox ˇjumpsˇ-ˇoˇver
-                    Λ‡the lazy dog
-                    "},
-            )
-            .await;
+            for marked_text in cx.each_marked_position(indoc! {"
+                ˇThe quˇickˇ browˇn
+                Λ‡
+                ˇfox ˇjumpsˇ-ˇoˇver
+                Λ‡the lazy dog
+                "})
+            {
+                cx.assert_neovim_compatible(&marked_text, ["c", &count.to_string(), "b"])
+                    .await;
+            }
         }
     }
 

crates/vim/src/normal/delete.rs πŸ”—

@@ -4,6 +4,7 @@ use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Bias};
 use gpui::WindowContext;
 
 pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
+    vim.stop_recording();
     vim.update_active_editor(cx, |editor, cx| {
         editor.transact(cx, |editor, cx| {
             editor.set_clip_at_line_ends(false, cx);
@@ -37,6 +38,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
 }
 
 pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) {
+    vim.stop_recording();
     vim.update_active_editor(cx, |editor, cx| {
         editor.transact(cx, |editor, cx| {
             editor.set_clip_at_line_ends(false, cx);
@@ -276,37 +278,41 @@ mod test {
 
     #[gpui::test]
     async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) {
-        let mut cx = NeovimBackedTestContext::new(cx)
-            .await
-            .binding(["d", "shift-g"]);
-        cx.assert(indoc! {"
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        cx.assert_neovim_compatible(
+            indoc! {"
             The quick
             brownˇ fox
             jumps over
-            the lazy"})
-            .await;
-        cx.assert(indoc! {"
+            the lazy"},
+            ["d", "shift-g"],
+        )
+        .await;
+        cx.assert_neovim_compatible(
+            indoc! {"
             The quick
             brownˇ fox
             jumps over
-            the lazy"})
-            .await;
-        cx.assert_exempted(
+            the lazy"},
+            ["d", "shift-g"],
+        )
+        .await;
+        cx.assert_neovim_compatible(
             indoc! {"
             The quick
             brown fox
             jumps over
             the lˇazy"},
-            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+            ["d", "shift-g"],
         )
         .await;
-        cx.assert_exempted(
+        cx.assert_neovim_compatible(
             indoc! {"
             The quick
             brown fox
             jumps over
             Λ‡"},
-            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+            ["d", "shift-g"],
         )
         .await;
     }
@@ -316,34 +322,40 @@ mod test {
         let mut cx = NeovimBackedTestContext::new(cx)
             .await
             .binding(["d", "g", "g"]);
-        cx.assert(indoc! {"
+        cx.assert_neovim_compatible(
+            indoc! {"
             The quick
             brownˇ fox
             jumps over
-            the lazy"})
-            .await;
-        cx.assert(indoc! {"
+            the lazy"},
+            ["d", "g", "g"],
+        )
+        .await;
+        cx.assert_neovim_compatible(
+            indoc! {"
             The quick
             brown fox
             jumps over
-            the lˇazy"})
-            .await;
-        cx.assert_exempted(
+            the lˇazy"},
+            ["d", "g", "g"],
+        )
+        .await;
+        cx.assert_neovim_compatible(
             indoc! {"
             The qˇuick
             brown fox
             jumps over
             the lazy"},
-            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+            ["d", "g", "g"],
         )
         .await;
-        cx.assert_exempted(
+        cx.assert_neovim_compatible(
             indoc! {"
             Λ‡
             brown fox
             jumps over
             the lazy"},
-            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+            ["d", "g", "g"],
         )
         .await;
     }
@@ -385,4 +397,40 @@ mod test {
         assert_eq!(cx.active_operator(), None);
         assert_eq!(cx.mode(), Mode::Normal);
     }
+
+    #[gpui::test]
+    async fn test_delete_with_counts(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        cx.set_shared_state(indoc! {"
+                The Λ‡quick brown
+                fox jumps over
+                the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes(["d", "2", "d"]).await;
+        cx.assert_shared_state(indoc! {"
+        the Λ‡lazy dog"})
+            .await;
+
+        cx.set_shared_state(indoc! {"
+                The Λ‡quick brown
+                fox jumps over
+                the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes(["2", "d", "d"]).await;
+        cx.assert_shared_state(indoc! {"
+        the Λ‡lazy dog"})
+            .await;
+
+        cx.set_shared_state(indoc! {"
+                The Λ‡quick brown
+                fox jumps over
+                the moon,
+                a star, and
+                the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes(["2", "d", "2", "d"]).await;
+        cx.assert_shared_state(indoc! {"
+        the Λ‡lazy dog"})
+            .await;
+    }
 }

crates/vim/src/normal/paste.rs πŸ”—

@@ -28,6 +28,7 @@ pub(crate) fn init(cx: &mut AppContext) {
 
 fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
     Vim::update(cx, |vim, cx| {
+        vim.record_current_action(cx);
         vim.update_active_editor(cx, |editor, cx| {
             editor.transact(cx, |editor, cx| {
                 editor.set_clip_at_line_ends(false, cx);

crates/vim/src/normal/repeat.rs πŸ”—

@@ -0,0 +1,528 @@
+use crate::{
+    insert::NormalBefore,
+    motion::Motion,
+    state::{Mode, RecordedSelection, ReplayableAction},
+    visual::visual_motion,
+    Vim,
+};
+use gpui::{actions, Action, AppContext, WindowContext};
+use workspace::Workspace;
+
+actions!(vim, [Repeat, EndRepeat,]);
+
+fn should_replay(action: &Box<dyn Action>) -> bool {
+    // skip so that we don't leave the character palette open
+    if editor::ShowCharacterPalette.id() == action.id() {
+        return false;
+    }
+    true
+}
+
+fn repeatable_insert(action: &ReplayableAction) -> Option<Box<dyn Action>> {
+    match action {
+        ReplayableAction::Action(action) => {
+            if super::InsertBefore.id() == action.id()
+                || super::InsertAfter.id() == action.id()
+                || super::InsertFirstNonWhitespace.id() == action.id()
+                || super::InsertEndOfLine.id() == action.id()
+            {
+                Some(super::InsertBefore.boxed_clone())
+            } else if super::InsertLineAbove.id() == action.id()
+                || super::InsertLineBelow.id() == action.id()
+            {
+                Some(super::InsertLineBelow.boxed_clone())
+            } else {
+                None
+            }
+        }
+        ReplayableAction::Insertion { .. } => None,
+    }
+}
+
+pub(crate) fn init(cx: &mut AppContext) {
+    cx.add_action(|_: &mut Workspace, _: &EndRepeat, cx| {
+        Vim::update(cx, |vim, cx| {
+            vim.workspace_state.replaying = false;
+            vim.switch_mode(Mode::Normal, false, cx)
+        });
+    });
+
+    cx.add_action(|_: &mut Workspace, _: &Repeat, cx| repeat(cx, false));
+}
+
+pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) {
+    let Some((mut actions, editor, selection)) = Vim::update(cx, |vim, cx| {
+        let actions = vim.workspace_state.recorded_actions.clone();
+        if actions.is_empty() {
+            return None;
+        }
+
+        let Some(editor) = vim.active_editor.clone() else {
+            return None;
+        };
+        let count = vim.take_count(cx);
+
+        let selection = vim.workspace_state.recorded_selection.clone();
+        match selection {
+            RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
+                vim.workspace_state.recorded_count = None;
+                vim.switch_mode(Mode::Visual, false, cx)
+            }
+            RecordedSelection::VisualLine { .. } => {
+                vim.workspace_state.recorded_count = None;
+                vim.switch_mode(Mode::VisualLine, false, cx)
+            }
+            RecordedSelection::VisualBlock { .. } => {
+                vim.workspace_state.recorded_count = None;
+                vim.switch_mode(Mode::VisualBlock, false, cx)
+            }
+            RecordedSelection::None => {
+                if let Some(count) = count {
+                    vim.workspace_state.recorded_count = Some(count);
+                }
+            }
+        }
+
+        Some((actions, editor, selection))
+    }) else {
+        return;
+    };
+
+    match selection {
+        RecordedSelection::SingleLine { cols } => {
+            if cols > 1 {
+                visual_motion(Motion::Right, Some(cols as usize - 1), cx)
+            }
+        }
+        RecordedSelection::Visual { rows, cols } => {
+            visual_motion(
+                Motion::Down {
+                    display_lines: false,
+                },
+                Some(rows as usize),
+                cx,
+            );
+            visual_motion(
+                Motion::StartOfLine {
+                    display_lines: false,
+                },
+                None,
+                cx,
+            );
+            if cols > 1 {
+                visual_motion(Motion::Right, Some(cols as usize - 1), cx)
+            }
+        }
+        RecordedSelection::VisualBlock { rows, cols } => {
+            visual_motion(
+                Motion::Down {
+                    display_lines: false,
+                },
+                Some(rows as usize),
+                cx,
+            );
+            if cols > 1 {
+                visual_motion(Motion::Right, Some(cols as usize - 1), cx);
+            }
+        }
+        RecordedSelection::VisualLine { rows } => {
+            visual_motion(
+                Motion::Down {
+                    display_lines: false,
+                },
+                Some(rows as usize),
+                cx,
+            );
+        }
+        RecordedSelection::None => {}
+    }
+
+    // insert internally uses repeat to handle counts
+    // vim doesn't treat 3a1 as though you literally repeated a1
+    // 3 times, instead it inserts the content thrice at the insert position.
+    if let Some(to_repeat) = repeatable_insert(&actions[0]) {
+        if let Some(ReplayableAction::Action(action)) = actions.last() {
+            if action.id() == NormalBefore.id() {
+                actions.pop();
+            }
+        }
+
+        let mut new_actions = actions.clone();
+        actions[0] = ReplayableAction::Action(to_repeat.boxed_clone());
+
+        let mut count = Vim::read(cx).workspace_state.recorded_count.unwrap_or(1);
+
+        // if we came from insert mode we're just doing repititions 2 onwards.
+        if from_insert_mode {
+            count -= 1;
+            new_actions[0] = actions[0].clone();
+        }
+
+        for _ in 1..count {
+            new_actions.append(actions.clone().as_mut());
+        }
+        new_actions.push(ReplayableAction::Action(NormalBefore.boxed_clone()));
+        actions = new_actions;
+    }
+
+    Vim::update(cx, |vim, _| vim.workspace_state.replaying = true);
+    let window = cx.window();
+    cx.app_context()
+        .spawn(move |mut cx| async move {
+            editor.update(&mut cx, |editor, _| {
+                editor.show_local_selections = false;
+            })?;
+            for action in actions {
+                match action {
+                    ReplayableAction::Action(action) => {
+                        if should_replay(&action) {
+                            window
+                                .dispatch_action(editor.id(), action.as_ref(), &mut cx)
+                                .ok_or_else(|| anyhow::anyhow!("window was closed"))
+                        } else {
+                            Ok(())
+                        }
+                    }
+                    ReplayableAction::Insertion {
+                        text,
+                        utf16_range_to_replace,
+                    } => editor.update(&mut cx, |editor, cx| {
+                        editor.replay_insert_event(&text, utf16_range_to_replace.clone(), cx)
+                    }),
+                }?
+            }
+            editor.update(&mut cx, |editor, _| {
+                editor.show_local_selections = true;
+            })?;
+            window
+                .dispatch_action(editor.id(), &EndRepeat, &mut cx)
+                .ok_or_else(|| anyhow::anyhow!("window was closed"))
+        })
+        .detach_and_log_err(cx);
+}
+
+#[cfg(test)]
+mod test {
+    use std::sync::Arc;
+
+    use editor::test::editor_lsp_test_context::EditorLspTestContext;
+    use futures::StreamExt;
+    use indoc::indoc;
+
+    use gpui::{executor::Deterministic, View};
+
+    use crate::{
+        state::Mode,
+        test::{NeovimBackedTestContext, VimTestContext},
+    };
+
+    #[gpui::test]
+    async fn test_dot_repeat(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        // "o"
+        cx.set_shared_state("Λ‡hello").await;
+        cx.simulate_shared_keystrokes(["o", "w", "o", "r", "l", "d", "escape"])
+            .await;
+        cx.assert_shared_state("hello\nworlˇd").await;
+        cx.simulate_shared_keystrokes(["."]).await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("hello\nworld\nworlˇd").await;
+
+        // "d"
+        cx.simulate_shared_keystrokes(["^", "d", "f", "o"]).await;
+        cx.simulate_shared_keystrokes(["g", "g", "."]).await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("Λ‡\nworld\nrld").await;
+
+        // "p" (note that it pastes the current clipboard)
+        cx.simulate_shared_keystrokes(["j", "y", "y", "p"]).await;
+        cx.simulate_shared_keystrokes(["shift-g", "y", "y", "."])
+            .await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("\nworld\nworld\nrld\nˇrld").await;
+
+        // "~" (note that counts apply to the action taken, not . itself)
+        cx.set_shared_state("Λ‡the quick brown fox").await;
+        cx.simulate_shared_keystrokes(["2", "~", "."]).await;
+        deterministic.run_until_parked();
+        cx.set_shared_state("THE Λ‡quick brown fox").await;
+        cx.simulate_shared_keystrokes(["3", "."]).await;
+        deterministic.run_until_parked();
+        cx.set_shared_state("THE QUIˇck brown fox").await;
+        deterministic.run_until_parked();
+        cx.simulate_shared_keystrokes(["."]).await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("THE QUICK Λ‡brown fox").await;
+    }
+
+    #[gpui::test]
+    async fn test_repeat_ime(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+
+        cx.set_state("hˇllo", Mode::Normal);
+        cx.simulate_keystrokes(["i"]);
+
+        // simulate brazilian input for Γ€.
+        cx.update_editor(|editor, cx| {
+            editor.replace_and_mark_text_in_range(None, "\"", Some(1..1), cx);
+            editor.replace_text_in_range(None, "Γ€", cx);
+        });
+        cx.simulate_keystrokes(["escape"]);
+        cx.assert_state("hˇÀllo", Mode::Normal);
+        cx.simulate_keystrokes(["."]);
+        deterministic.run_until_parked();
+        cx.assert_state("hˇÀÀllo", Mode::Normal);
+    }
+
+    #[gpui::test]
+    async fn test_repeat_completion(
+        deterministic: Arc<Deterministic>,
+        cx: &mut gpui::TestAppContext,
+    ) {
+        let cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                completion_provider: Some(lsp::CompletionOptions {
+                    trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
+                    resolve_provider: Some(true),
+                    ..Default::default()
+                }),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+        let mut cx = VimTestContext::new_with_lsp(cx, true);
+
+        cx.set_state(
+            indoc! {"
+            onˇe
+            two
+            three
+        "},
+            Mode::Normal,
+        );
+
+        let mut request =
+            cx.handle_request::<lsp::request::Completion, _, _>(move |_, params, _| async move {
+                let position = params.text_document_position.position;
+                Ok(Some(lsp::CompletionResponse::Array(vec![
+                    lsp::CompletionItem {
+                        label: "first".to_string(),
+                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+                            range: lsp::Range::new(position.clone(), position.clone()),
+                            new_text: "first".to_string(),
+                        })),
+                        ..Default::default()
+                    },
+                    lsp::CompletionItem {
+                        label: "second".to_string(),
+                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+                            range: lsp::Range::new(position.clone(), position.clone()),
+                            new_text: "second".to_string(),
+                        })),
+                        ..Default::default()
+                    },
+                ])))
+            });
+        cx.simulate_keystrokes(["a", "."]);
+        request.next().await;
+        cx.condition(|editor, _| editor.context_menu_visible())
+            .await;
+        cx.simulate_keystrokes(["down", "enter", "!", "escape"]);
+
+        cx.assert_state(
+            indoc! {"
+                one.secondˇ!
+                two
+                three
+            "},
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes(["j", "."]);
+        deterministic.run_until_parked();
+        cx.assert_state(
+            indoc! {"
+                one.second!
+                two.secondˇ!
+                three
+            "},
+            Mode::Normal,
+        );
+    }
+
+    #[gpui::test]
+    async fn test_repeat_visual(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        // single-line (3 columns)
+        cx.set_shared_state(indoc! {
+            "Λ‡the quick brown
+            fox jumps over
+            the lazy dog"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["v", "i", "w", "s", "o", "escape"])
+            .await;
+        cx.assert_shared_state(indoc! {
+            "Λ‡o quick brown
+            fox jumps over
+            the lazy dog"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["j", "w", "."]).await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state(indoc! {
+            "o quick brown
+            fox Λ‡ops over
+            the lazy dog"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["f", "r", "."]).await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state(indoc! {
+            "o quick brown
+            fox ops oveˇothe lazy dog"
+        })
+        .await;
+
+        // visual
+        cx.set_shared_state(indoc! {
+            "the Λ‡quick brown
+            fox jumps over
+            fox jumps over
+            fox jumps over
+            the lazy dog"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["v", "j", "x"]).await;
+        cx.assert_shared_state(indoc! {
+            "the Λ‡umps over
+            fox jumps over
+            fox jumps over
+            the lazy dog"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["."]).await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state(indoc! {
+            "the Λ‡umps over
+            fox jumps over
+            the lazy dog"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["w", "."]).await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state(indoc! {
+            "the umps Λ‡umps over
+            the lazy dog"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["j", "."]).await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state(indoc! {
+            "the umps umps over
+            the Λ‡og"
+        })
+        .await;
+
+        // block mode (3 rows)
+        cx.set_shared_state(indoc! {
+            "Λ‡the quick brown
+            fox jumps over
+            the lazy dog"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["ctrl-v", "j", "j", "shift-i", "o", "escape"])
+            .await;
+        cx.assert_shared_state(indoc! {
+            "Λ‡othe quick brown
+            ofox jumps over
+            othe lazy dog"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["j", "4", "l", "."]).await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state(indoc! {
+            "othe quick brown
+            ofoxˇo jumps over
+            otheo lazy dog"
+        })
+        .await;
+
+        // line mode
+        cx.set_shared_state(indoc! {
+            "Λ‡the quick brown
+            fox jumps over
+            the lazy dog"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["shift-v", "shift-r", "o", "escape"])
+            .await;
+        cx.assert_shared_state(indoc! {
+            "Λ‡o
+            fox jumps over
+            the lazy dog"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["j", "."]).await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state(indoc! {
+            "o
+            Λ‡o
+            the lazy dog"
+        })
+        .await;
+    }
+
+    #[gpui::test]
+    async fn test_repeat_motion_counts(
+        deterministic: Arc<Deterministic>,
+        cx: &mut gpui::TestAppContext,
+    ) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {
+            "Λ‡the quick brown
+            fox jumps over
+            the lazy dog"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["3", "d", "3", "l"]).await;
+        cx.assert_shared_state(indoc! {
+            "Λ‡ brown
+            fox jumps over
+            the lazy dog"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["j", "."]).await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state(indoc! {
+            " brown
+            Λ‡ over
+            the lazy dog"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["j", "2", "."]).await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state(indoc! {
+            " brown
+             over
+            Λ‡e lazy dog"
+        })
+        .await;
+    }
+
+    #[gpui::test]
+    async fn test_record_interrupted(
+        deterministic: Arc<Deterministic>,
+        cx: &mut gpui::TestAppContext,
+    ) {
+        let mut cx = VimTestContext::new(cx, true).await;
+
+        cx.set_state("Λ‡hello\n", Mode::Normal);
+        cx.simulate_keystrokes(["4", "i", "j", "cmd-shift-p", "escape", "escape"]);
+        deterministic.run_until_parked();
+        cx.assert_state("Λ‡jhello\n", Mode::Normal);
+    }
+}

crates/vim/src/normal/scroll.rs πŸ”—

@@ -48,7 +48,7 @@ pub fn init(cx: &mut AppContext) {
 
 fn scroll(cx: &mut ViewContext<Workspace>, by: fn(c: Option<f32>) -> ScrollAmount) {
     Vim::update(cx, |vim, cx| {
-        let amount = by(vim.pop_number_operator(cx).map(|c| c as f32));
+        let amount = by(vim.take_count(cx).map(|c| c as f32));
         vim.update_active_editor(cx, |editor, cx| scroll_editor(editor, &amount, cx));
     })
 }

crates/vim/src/normal/search.rs πŸ”—

@@ -52,7 +52,7 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Works
         Direction::Next
     };
     Vim::update(cx, |vim, cx| {
-        let count = vim.pop_number_operator(cx).unwrap_or(1);
+        let count = vim.take_count(cx).unwrap_or(1);
         pane.update(cx, |pane, cx| {
             if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
                 search_bar.update(cx, |search_bar, cx| {
@@ -119,7 +119,7 @@ pub fn move_to_internal(
 ) {
     Vim::update(cx, |vim, cx| {
         let pane = workspace.active_pane().clone();
-        let count = vim.pop_number_operator(cx).unwrap_or(1);
+        let count = vim.take_count(cx).unwrap_or(1);
         pane.update(cx, |pane, cx| {
             if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
                 let search = search_bar.update(cx, |search_bar, cx| {

crates/vim/src/normal/substitute.rs πŸ”—

@@ -10,17 +10,19 @@ actions!(vim, [Substitute, SubstituteLine]);
 pub(crate) fn init(cx: &mut AppContext) {
     cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
         Vim::update(cx, |vim, cx| {
-            let count = vim.pop_number_operator(cx);
+            vim.start_recording(cx);
+            let count = vim.take_count(cx);
             substitute(vim, count, vim.state().mode == Mode::VisualLine, cx);
         })
     });
 
     cx.add_action(|_: &mut Workspace, _: &SubstituteLine, cx| {
         Vim::update(cx, |vim, cx| {
+            vim.start_recording(cx);
             if matches!(vim.state().mode, Mode::VisualBlock | Mode::Visual) {
                 vim.switch_mode(Mode::VisualLine, false, cx)
             }
-            let count = vim.pop_number_operator(cx);
+            let count = vim.take_count(cx);
             substitute(vim, count, true, cx)
         })
     });

crates/vim/src/state.rs πŸ”—

@@ -1,4 +1,6 @@
-use gpui::keymap_matcher::KeymapContext;
+use std::{ops::Range, sync::Arc};
+
+use gpui::{keymap_matcher::KeymapContext, Action};
 use language::CursorShape;
 use serde::{Deserialize, Serialize};
 use workspace::searchable::Direction;
@@ -31,7 +33,6 @@ impl Default for Mode {
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
 pub enum Operator {
-    Number(usize),
     Change,
     Delete,
     Yank,
@@ -45,13 +46,70 @@ pub enum Operator {
 pub struct EditorState {
     pub mode: Mode,
     pub last_mode: Mode,
+
+    /// pre_count is the number before an operator is specified (3 in 3d2d)
+    pub pre_count: Option<usize>,
+    /// post_count is the number after an operator is specified (2 in 3d2d)
+    pub post_count: Option<usize>,
+
     pub operator_stack: Vec<Operator>,
 }
 
+#[derive(Default, Clone, Debug)]
+pub enum RecordedSelection {
+    #[default]
+    None,
+    Visual {
+        rows: u32,
+        cols: u32,
+    },
+    SingleLine {
+        cols: u32,
+    },
+    VisualBlock {
+        rows: u32,
+        cols: u32,
+    },
+    VisualLine {
+        rows: u32,
+    },
+}
+
 #[derive(Default, Clone)]
 pub struct WorkspaceState {
     pub search: SearchState,
     pub last_find: Option<Motion>,
+
+    pub recording: bool,
+    pub stop_recording_after_next_action: bool,
+    pub replaying: bool,
+    pub recorded_count: Option<usize>,
+    pub recorded_actions: Vec<ReplayableAction>,
+    pub recorded_selection: RecordedSelection,
+}
+
+#[derive(Debug)]
+pub enum ReplayableAction {
+    Action(Box<dyn Action>),
+    Insertion {
+        text: Arc<str>,
+        utf16_range_to_replace: Option<Range<isize>>,
+    },
+}
+
+impl Clone for ReplayableAction {
+    fn clone(&self) -> Self {
+        match self {
+            Self::Action(action) => Self::Action(action.boxed_clone()),
+            Self::Insertion {
+                text,
+                utf16_range_to_replace,
+            } => Self::Insertion {
+                text: text.clone(),
+                utf16_range_to_replace: utf16_range_to_replace.clone(),
+            },
+        }
+    }
 }
 
 #[derive(Clone)]
@@ -105,6 +163,10 @@ impl EditorState {
         }
     }
 
+    pub fn active_operator(&self) -> Option<Operator> {
+        self.operator_stack.last().copied()
+    }
+
     pub fn keymap_context_layer(&self) -> KeymapContext {
         let mut context = KeymapContext::default();
         context.add_identifier("VimEnabled");
@@ -121,7 +183,14 @@ impl EditorState {
             context.add_identifier("VimControl");
         }
 
-        let active_operator = self.operator_stack.last();
+        if self.active_operator().is_none() && self.pre_count.is_some()
+            || self.active_operator().is_some() && self.post_count.is_some()
+        {
+            dbg!("VimCount");
+            context.add_identifier("VimCount");
+        }
+
+        let active_operator = self.active_operator();
 
         if let Some(active_operator) = active_operator {
             for context_flag in active_operator.context_flags().into_iter() {
@@ -141,7 +210,6 @@ impl EditorState {
 impl Operator {
     pub fn id(&self) -> &'static str {
         match self {
-            Operator::Number(_) => "n",
             Operator::Object { around: false } => "i",
             Operator::Object { around: true } => "a",
             Operator::Change => "c",

crates/vim/src/test.rs πŸ”—

@@ -286,6 +286,55 @@ async fn test_word_characters(cx: &mut gpui::TestAppContext) {
     )
 }
 
+#[gpui::test]
+async fn test_join_lines(cx: &mut gpui::TestAppContext) {
+    let mut cx = NeovimBackedTestContext::new(cx).await;
+
+    cx.set_shared_state(indoc! {"
+      Λ‡one
+      two
+      three
+      four
+      five
+      six
+      "})
+        .await;
+    cx.simulate_shared_keystrokes(["shift-j"]).await;
+    cx.assert_shared_state(indoc! {"
+          oneˇ two
+          three
+          four
+          five
+          six
+          "})
+        .await;
+    cx.simulate_shared_keystrokes(["3", "shift-j"]).await;
+    cx.assert_shared_state(indoc! {"
+          one two threeˇ four
+          five
+          six
+          "})
+        .await;
+
+    cx.set_shared_state(indoc! {"
+      Λ‡one
+      two
+      three
+      four
+      five
+      six
+      "})
+        .await;
+    cx.simulate_shared_keystrokes(["j", "v", "3", "j", "shift-j"])
+        .await;
+    cx.assert_shared_state(indoc! {"
+      one
+      two three fourˇ five
+      six
+      "})
+        .await;
+}
+
 #[gpui::test]
 async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) {
     let mut cx = NeovimBackedTestContext::new(cx).await;
@@ -449,6 +498,13 @@ async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) {
         fourteen char
     "})
         .await;
+    cx.simulate_shared_keystrokes(["j", "shift-f", "e", "f", "r"])
+        .await;
+    cx.assert_shared_state(indoc! {"
+        fourteenβ€’
+        fourteen chaˇr
+    "})
+        .await;
 }
 
 #[gpui::test]
@@ -518,3 +574,47 @@ async fn test_folds(cx: &mut gpui::TestAppContext) {
     "})
         .await;
 }
+
+#[gpui::test]
+async fn test_clear_counts(cx: &mut gpui::TestAppContext) {
+    let mut cx = NeovimBackedTestContext::new(cx).await;
+
+    cx.set_shared_state(indoc! {"
+        The quick brown
+        fox juˇmps over
+        the lazy dog"})
+        .await;
+
+    cx.simulate_shared_keystrokes(["4", "escape", "3", "d", "l"])
+        .await;
+    cx.assert_shared_state(indoc! {"
+        The quick brown
+        fox juˇ over
+        the lazy dog"})
+        .await;
+}
+
+#[gpui::test]
+async fn test_zero(cx: &mut gpui::TestAppContext) {
+    let mut cx = NeovimBackedTestContext::new(cx).await;
+
+    cx.set_shared_state(indoc! {"
+        The quˇick brown
+        fox jumps over
+        the lazy dog"})
+        .await;
+
+    cx.simulate_shared_keystrokes(["0"]).await;
+    cx.assert_shared_state(indoc! {"
+        Λ‡The quick brown
+        fox jumps over
+        the lazy dog"})
+        .await;
+
+    cx.simulate_shared_keystrokes(["1", "0", "l"]).await;
+    cx.assert_shared_state(indoc! {"
+        The quick Λ‡brown
+        fox jumps over
+        the lazy dog"})
+        .await;
+}

crates/vim/src/test/neovim_backed_test_context.rs πŸ”—

@@ -13,20 +13,13 @@ use util::test::{generate_marked_text, marked_text_offsets};
 use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
 use crate::state::Mode;
 
-pub const SUPPORTED_FEATURES: &[ExemptionFeatures] = &[
-    ExemptionFeatures::DeletionOnEmptyLine,
-    ExemptionFeatures::OperatorAbortsOnFailedMotion,
-];
+pub const SUPPORTED_FEATURES: &[ExemptionFeatures] = &[];
 
 /// Enum representing features we have tests for but which don't work, yet. Used
 /// to add exemptions and automatically
 #[derive(PartialEq, Eq)]
 pub enum ExemptionFeatures {
     // MOTIONS
-    // Deletions on empty lines miss some newlines
-    DeletionOnEmptyLine,
-    // When a motion fails, it should should not apply linewise operations
-    OperatorAbortsOnFailedMotion,
     // When an operator completes at the end of the file, an extra newline is left
     OperatorLastNewlineRemains,
     // Deleting a word on an empty line doesn't remove the newline
@@ -68,6 +61,8 @@ pub struct NeovimBackedTestContext<'a> {
 
     last_set_state: Option<String>,
     recent_keystrokes: Vec<String>,
+
+    is_dirty: bool,
 }
 
 impl<'a> NeovimBackedTestContext<'a> {
@@ -81,6 +76,7 @@ impl<'a> NeovimBackedTestContext<'a> {
 
             last_set_state: None,
             recent_keystrokes: Default::default(),
+            is_dirty: false,
         }
     }
 
@@ -128,6 +124,7 @@ impl<'a> NeovimBackedTestContext<'a> {
         self.last_set_state = Some(marked_text.to_string());
         self.recent_keystrokes = Vec::new();
         self.neovim.set_state(marked_text).await;
+        self.is_dirty = true;
         context_handle
     }
 
@@ -153,6 +150,7 @@ impl<'a> NeovimBackedTestContext<'a> {
     }
 
     pub async fn assert_shared_state(&mut self, marked_text: &str) {
+        self.is_dirty = false;
         let marked_text = marked_text.replace("β€’", " ");
         let neovim = self.neovim_state().await;
         let editor = self.editor_state();
@@ -258,6 +256,7 @@ impl<'a> NeovimBackedTestContext<'a> {
     }
 
     pub async fn assert_state_matches(&mut self) {
+        self.is_dirty = false;
         let neovim = self.neovim_state().await;
         let editor = self.editor_state();
         let initial_state = self
@@ -383,6 +382,17 @@ impl<'a> DerefMut for NeovimBackedTestContext<'a> {
     }
 }
 
+// a common mistake in tests is to call set_shared_state when
+// you mean asswert_shared_state. This notices that and lets
+// you know.
+impl<'a> Drop for NeovimBackedTestContext<'a> {
+    fn drop(&mut self) {
+        if self.is_dirty {
+            panic!("Test context was dropped after set_shared_state before assert_shared_state")
+        }
+    }
+}
+
 #[cfg(test)]
 mod test {
     use gpui::TestAppContext;

crates/vim/src/test/vim_test_context.rs πŸ”—

@@ -3,7 +3,9 @@ use std::ops::{Deref, DerefMut};
 use editor::test::{
     editor_lsp_test_context::EditorLspTestContext, editor_test_context::EditorTestContext,
 };
+use futures::Future;
 use gpui::ContextHandle;
+use lsp::request;
 use search::{BufferSearchBar, ProjectSearchBar};
 
 use crate::{state::Operator, *};
@@ -124,6 +126,19 @@ impl<'a> VimTestContext<'a> {
         assert_eq!(self.mode(), mode_after, "{}", self.assertion_context());
         assert_eq!(self.active_operator(), None, "{}", self.assertion_context());
     }
+
+    pub fn handle_request<T, F, Fut>(
+        &self,
+        handler: F,
+    ) -> futures::channel::mpsc::UnboundedReceiver<()>
+    where
+        T: 'static + request::Request,
+        T::Params: 'static + Send,
+        F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
+        Fut: 'static + Send + Future<Output = Result<T::Result>>,
+    {
+        self.cx.handle_request::<T, F, Fut>(handler)
+    }
 }
 
 impl<'a> Deref for VimTestContext<'a> {

crates/vim/src/vim.rs πŸ”—

@@ -15,20 +15,22 @@ use anyhow::Result;
 use collections::{CommandPaletteFilter, HashMap};
 use editor::{movement, Editor, EditorMode, Event};
 use gpui::{
-    actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext,
-    Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
+    actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, Action,
+    AppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
 };
-use language::{CursorShape, Selection, SelectionGoal};
+use language::{CursorShape, Point, Selection, SelectionGoal};
 pub use mode_indicator::ModeIndicator;
 use motion::Motion;
 use normal::normal_replace;
 use serde::Deserialize;
 use settings::{Setting, SettingsStore};
-use state::{EditorState, Mode, Operator, WorkspaceState};
-use std::sync::Arc;
+use state::{EditorState, Mode, Operator, RecordedSelection, WorkspaceState};
+use std::{ops::Range, sync::Arc};
 use visual::{visual_block_motion, visual_replace};
 use workspace::{self, Workspace};
 
+use crate::state::ReplayableAction;
+
 struct VimModeSetting(bool);
 
 #[derive(Clone, Deserialize, PartialEq)]
@@ -38,9 +40,12 @@ pub struct SwitchMode(pub Mode);
 pub struct PushOperator(pub Operator);
 
 #[derive(Clone, Deserialize, PartialEq)]
-struct Number(u8);
+struct Number(usize);
 
-actions!(vim, [Tab, Enter]);
+actions!(
+    vim,
+    [Tab, Enter, Object, InnerObject, FindForward, FindBackward]
+);
 impl_actions!(vim, [Number, SwitchMode, PushOperator]);
 
 #[derive(Copy, Clone, Debug)]
@@ -68,7 +73,7 @@ pub fn init(cx: &mut AppContext) {
         },
     );
     cx.add_action(|_: &mut Workspace, n: &Number, cx: _| {
-        Vim::update(cx, |vim, cx| vim.push_number(n, cx));
+        Vim::update(cx, |vim, cx| vim.push_count_digit(n.0, cx));
     });
 
     cx.add_action(|_: &mut Workspace, _: &Tab, cx| {
@@ -102,6 +107,19 @@ pub fn observe_keystrokes(cx: &mut WindowContext) {
             return true;
         }
         if let Some(handled_by) = handled_by {
+            Vim::update(cx, |vim, _| {
+                if vim.workspace_state.recording {
+                    vim.workspace_state
+                        .recorded_actions
+                        .push(ReplayableAction::Action(handled_by.boxed_clone()));
+
+                    if vim.workspace_state.stop_recording_after_next_action {
+                        vim.workspace_state.recording = false;
+                        vim.workspace_state.stop_recording_after_next_action = false;
+                    }
+                }
+            });
+
             // Keystroke is handled by the vim system, so continue forward
             if handled_by.namespace() == "vim" {
                 return true;
@@ -156,7 +174,12 @@ impl Vim {
             }
             Event::InputIgnored { text } => {
                 Vim::active_editor_input_ignored(text.clone(), cx);
+                Vim::record_insertion(text, None, cx)
             }
+            Event::InputHandled {
+                text,
+                utf16_range_to_replace: range_to_replace,
+            } => Vim::record_insertion(text, range_to_replace.clone(), cx),
             _ => {}
         }));
 
@@ -176,6 +199,27 @@ impl Vim {
         self.sync_vim_settings(cx);
     }
 
+    fn record_insertion(
+        text: &Arc<str>,
+        range_to_replace: Option<Range<isize>>,
+        cx: &mut WindowContext,
+    ) {
+        Vim::update(cx, |vim, _| {
+            if vim.workspace_state.recording {
+                vim.workspace_state
+                    .recorded_actions
+                    .push(ReplayableAction::Insertion {
+                        text: text.clone(),
+                        utf16_range_to_replace: range_to_replace,
+                    });
+                if vim.workspace_state.stop_recording_after_next_action {
+                    vim.workspace_state.recording = false;
+                    vim.workspace_state.stop_recording_after_next_action = false;
+                }
+            }
+        });
+    }
+
     fn update_active_editor<S>(
         &self,
         cx: &mut WindowContext,
@@ -185,6 +229,70 @@ impl Vim {
         Some(editor.update(cx, update))
     }
 
+    pub fn start_recording(&mut self, cx: &mut WindowContext) {
+        if !self.workspace_state.replaying {
+            self.workspace_state.recording = true;
+            self.workspace_state.recorded_actions = Default::default();
+            self.workspace_state.recorded_count = None;
+
+            let selections = self
+                .active_editor
+                .and_then(|editor| editor.upgrade(cx))
+                .map(|editor| {
+                    let editor = editor.read(cx);
+                    (
+                        editor.selections.oldest::<Point>(cx),
+                        editor.selections.newest::<Point>(cx),
+                    )
+                });
+
+            if let Some((oldest, newest)) = selections {
+                self.workspace_state.recorded_selection = match self.state().mode {
+                    Mode::Visual if newest.end.row == newest.start.row => {
+                        RecordedSelection::SingleLine {
+                            cols: newest.end.column - newest.start.column,
+                        }
+                    }
+                    Mode::Visual => RecordedSelection::Visual {
+                        rows: newest.end.row - newest.start.row,
+                        cols: newest.end.column,
+                    },
+                    Mode::VisualLine => RecordedSelection::VisualLine {
+                        rows: newest.end.row - newest.start.row,
+                    },
+                    Mode::VisualBlock => RecordedSelection::VisualBlock {
+                        rows: newest.end.row.abs_diff(oldest.start.row),
+                        cols: newest.end.column.abs_diff(oldest.start.column),
+                    },
+                    _ => RecordedSelection::None,
+                }
+            } else {
+                self.workspace_state.recorded_selection = RecordedSelection::None;
+            }
+        }
+    }
+
+    pub fn stop_recording(&mut self) {
+        if self.workspace_state.recording {
+            self.workspace_state.stop_recording_after_next_action = true;
+        }
+    }
+
+    pub fn stop_recording_immediately(&mut self, action: Box<dyn Action>) {
+        if self.workspace_state.recording {
+            self.workspace_state
+                .recorded_actions
+                .push(ReplayableAction::Action(action.boxed_clone()));
+            self.workspace_state.recording = false;
+            self.workspace_state.stop_recording_after_next_action = false;
+        }
+    }
+
+    pub fn record_current_action(&mut self, cx: &mut WindowContext) {
+        self.start_recording(cx);
+        self.stop_recording();
+    }
+
     fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut WindowContext) {
         let state = self.state();
         let last_mode = state.mode;
@@ -194,6 +302,9 @@ impl Vim {
             state.mode = mode;
             state.operator_stack.clear();
         });
+        if mode != Mode::Insert {
+            self.take_count(cx);
+        }
 
         cx.emit_global(VimEvent::ModeChanged { mode });
 
@@ -246,18 +357,48 @@ impl Vim {
         });
     }
 
-    fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) {
-        self.update_state(|state| state.operator_stack.push(operator));
-        self.sync_vim_settings(cx);
+    fn push_count_digit(&mut self, number: usize, cx: &mut WindowContext) {
+        if self.active_operator().is_some() {
+            self.update_state(|state| {
+                state.post_count = Some(state.post_count.unwrap_or(0) * 10 + number)
+            })
+        } else {
+            self.update_state(|state| {
+                state.pre_count = Some(state.pre_count.unwrap_or(0) * 10 + number)
+            })
+        }
+        // update the keymap so that 0 works
+        self.sync_vim_settings(cx)
     }
 
-    fn push_number(&mut self, Number(number): &Number, cx: &mut WindowContext) {
-        if let Some(Operator::Number(current_number)) = self.active_operator() {
-            self.pop_operator(cx);
-            self.push_operator(Operator::Number(current_number * 10 + *number as usize), cx);
+    fn take_count(&mut self, cx: &mut WindowContext) -> Option<usize> {
+        if self.workspace_state.replaying {
+            return self.workspace_state.recorded_count;
+        }
+
+        let count = if self.state().post_count == None && self.state().pre_count == None {
+            return None;
         } else {
-            self.push_operator(Operator::Number(*number as usize), cx);
+            Some(self.update_state(|state| {
+                state.post_count.take().unwrap_or(1) * state.pre_count.take().unwrap_or(1)
+            }))
+        };
+        if self.workspace_state.recording {
+            self.workspace_state.recorded_count = count;
         }
+        self.sync_vim_settings(cx);
+        count
+    }
+
+    fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) {
+        if matches!(
+            operator,
+            Operator::Change | Operator::Delete | Operator::Replace
+        ) {
+            self.start_recording(cx)
+        };
+        self.update_state(|state| state.operator_stack.push(operator));
+        self.sync_vim_settings(cx);
     }
 
     fn maybe_pop_operator(&mut self) -> Option<Operator> {
@@ -270,16 +411,8 @@ impl Vim {
         self.sync_vim_settings(cx);
         popped_operator
     }
-
-    fn pop_number_operator(&mut self, cx: &mut WindowContext) -> Option<usize> {
-        if let Some(Operator::Number(number)) = self.active_operator() {
-            self.pop_operator(cx);
-            return Some(number);
-        }
-        None
-    }
-
     fn clear_operator(&mut self, cx: &mut WindowContext) {
+        self.take_count(cx);
         self.update_state(|state| state.operator_stack.clear());
         self.sync_vim_settings(cx);
     }
@@ -295,14 +428,20 @@ impl Vim {
 
         match Vim::read(cx).active_operator() {
             Some(Operator::FindForward { before }) => {
-                let find = Motion::FindForward { before, text };
+                let find = Motion::FindForward {
+                    before,
+                    char: text.chars().next().unwrap(),
+                };
                 Vim::update(cx, |vim, _| {
                     vim.workspace_state.last_find = Some(find.clone())
                 });
                 motion::motion(find, cx)
             }
             Some(Operator::FindBackward { after }) => {
-                let find = Motion::FindBackward { after, text };
+                let find = Motion::FindBackward {
+                    after,
+                    char: text.chars().next().unwrap(),
+                };
                 Vim::update(cx, |vim, _| {
                     vim.workspace_state.last_find = Some(find.clone())
                 });

crates/vim/src/visual.rs πŸ”—

@@ -277,6 +277,7 @@ pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext<Workspace
 
 pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
     Vim::update(cx, |vim, cx| {
+        vim.record_current_action(cx);
         vim.update_active_editor(cx, |editor, cx| {
             let mut original_columns: HashMap<_, _> = Default::default();
             let line_mode = editor.selections.line_mode;
@@ -339,6 +340,7 @@ pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>)
 
 pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| {
+        vim.stop_recording();
         vim.update_active_editor(cx, |editor, cx| {
             editor.transact(cx, |editor, cx| {
                 let (display_map, selections) = editor.selections.all_adjusted_display(cx);

crates/vim/test_data/test_change_case.json πŸ”—

@@ -16,3 +16,8 @@
 {"Key":"shift-v"}
 {"Key":"~"}
 {"Get":{"state":"Λ‡ABc\n","mode":"Normal"}}
+{"Put":{"state":"Λ‡aa\nbb\ncc"}}
+{"Key":"ctrl-v"}
+{"Key":"j"}
+{"Key":"~"}
+{"Get":{"state":"Λ‡Aa\nBb\ncc","mode":"Normal"}}

crates/vim/test_data/test_clear_counts.json πŸ”—

@@ -0,0 +1,7 @@
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"4"}
+{"Key":"escape"}
+{"Key":"3"}
+{"Key":"d"}
+{"Key":"l"}
+{"Get":{"state":"The quick brown\nfox juˇ over\nthe lazy dog","mode":"Normal"}}

crates/vim/test_data/test_delete_with_counts.json πŸ”—

@@ -0,0 +1,16 @@
+{"Put":{"state":"The Λ‡quick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"d"}
+{"Key":"2"}
+{"Key":"d"}
+{"Get":{"state":"the Λ‡lazy dog","mode":"Normal"}}
+{"Put":{"state":"The Λ‡quick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"2"}
+{"Key":"d"}
+{"Key":"d"}
+{"Get":{"state":"the Λ‡lazy dog","mode":"Normal"}}
+{"Put":{"state":"The Λ‡quick brown\nfox jumps over\nthe moon,\na star, and\nthe lazy dog"}}
+{"Key":"2"}
+{"Key":"d"}
+{"Key":"2"}
+{"Key":"d"}
+{"Get":{"state":"the Λ‡lazy dog","mode":"Normal"}}

crates/vim/test_data/test_dot_repeat.json πŸ”—

@@ -0,0 +1,38 @@
+{"Put":{"state":"Λ‡hello"}}
+{"Key":"o"}
+{"Key":"w"}
+{"Key":"o"}
+{"Key":"r"}
+{"Key":"l"}
+{"Key":"d"}
+{"Key":"escape"}
+{"Get":{"state":"hello\nworlˇd","mode":"Normal"}}
+{"Key":"."}
+{"Get":{"state":"hello\nworld\nworlˇd","mode":"Normal"}}
+{"Key":"^"}
+{"Key":"d"}
+{"Key":"f"}
+{"Key":"o"}
+{"Key":"g"}
+{"Key":"g"}
+{"Key":"."}
+{"Get":{"state":"Λ‡\nworld\nrld","mode":"Normal"}}
+{"Key":"j"}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"p"}
+{"Key":"shift-g"}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"."}
+{"Get":{"state":"\nworld\nworld\nrld\nˇrld","mode":"Normal"}}
+{"Put":{"state":"Λ‡the quick brown fox"}}
+{"Key":"2"}
+{"Key":"~"}
+{"Key":"."}
+{"Put":{"state":"THE Λ‡quick brown fox"}}
+{"Key":"3"}
+{"Key":"."}
+{"Put":{"state":"THE QUIˇck brown fox"}}
+{"Key":"."}
+{"Get":{"state":"THE QUICK Λ‡brown fox","mode":"Normal"}}

crates/vim/test_data/test_insert_with_counts.json πŸ”—

@@ -0,0 +1,36 @@
+{"Put":{"state":"Λ‡hello\n"}}
+{"Key":"5"}
+{"Key":"i"}
+{"Key":"-"}
+{"Key":"escape"}
+{"Get":{"state":"----Λ‡-hello\n","mode":"Normal"}}
+{"Put":{"state":"Λ‡hello\n"}}
+{"Key":"5"}
+{"Key":"a"}
+{"Key":"-"}
+{"Key":"escape"}
+{"Get":{"state":"h----Λ‡-ello\n","mode":"Normal"}}
+{"Key":"4"}
+{"Key":"shift-i"}
+{"Key":"-"}
+{"Key":"escape"}
+{"Get":{"state":"---Λ‡-h-----ello\n","mode":"Normal"}}
+{"Key":"3"}
+{"Key":"shift-a"}
+{"Key":"-"}
+{"Key":"escape"}
+{"Get":{"state":"----h-----ello--Λ‡-\n","mode":"Normal"}}
+{"Put":{"state":"Λ‡hello\n"}}
+{"Key":"3"}
+{"Key":"o"}
+{"Key":"o"}
+{"Key":"i"}
+{"Key":"escape"}
+{"Get":{"state":"hello\noi\noi\noˇi\n","mode":"Normal"}}
+{"Put":{"state":"Λ‡hello\n"}}
+{"Key":"3"}
+{"Key":"shift-o"}
+{"Key":"o"}
+{"Key":"i"}
+{"Key":"escape"}
+{"Get":{"state":"oi\noi\noˇi\nhello\n","mode":"Normal"}}

crates/vim/test_data/test_insert_with_repeat.json πŸ”—

@@ -0,0 +1,23 @@
+{"Put":{"state":"Λ‡hello\n"}}
+{"Key":"3"}
+{"Key":"i"}
+{"Key":"-"}
+{"Key":"escape"}
+{"Get":{"state":"--Λ‡-hello\n","mode":"Normal"}}
+{"Key":"."}
+{"Get":{"state":"----Λ‡--hello\n","mode":"Normal"}}
+{"Key":"2"}
+{"Key":"."}
+{"Get":{"state":"-----Λ‡---hello\n","mode":"Normal"}}
+{"Put":{"state":"Λ‡hello\n"}}
+{"Key":"2"}
+{"Key":"o"}
+{"Key":"k"}
+{"Key":"k"}
+{"Key":"escape"}
+{"Get":{"state":"hello\nkk\nkˇk\n","mode":"Normal"}}
+{"Key":"."}
+{"Get":{"state":"hello\nkk\nkk\nkk\nkˇk\n","mode":"Normal"}}
+{"Key":"1"}
+{"Key":"."}
+{"Get":{"state":"hello\nkk\nkk\nkk\nkk\nkˇk\n","mode":"Normal"}}

crates/vim/test_data/test_join_lines.json πŸ”—

@@ -0,0 +1,13 @@
+{"Put":{"state":"Λ‡one\ntwo\nthree\nfour\nfive\nsix\n"}}
+{"Key":"shift-j"}
+{"Get":{"state":"oneˇ two\nthree\nfour\nfive\nsix\n","mode":"Normal"}}
+{"Key":"3"}
+{"Key":"shift-j"}
+{"Get":{"state":"one two threeˇ four\nfive\nsix\n","mode":"Normal"}}
+{"Put":{"state":"Λ‡one\ntwo\nthree\nfour\nfive\nsix\n"}}
+{"Key":"j"}
+{"Key":"v"}
+{"Key":"3"}
+{"Key":"j"}
+{"Key":"shift-j"}
+{"Get":{"state":"one\ntwo three fourˇ five\nsix\n","mode":"Normal"}}

crates/vim/test_data/test_repeat_motion_counts.json πŸ”—

@@ -0,0 +1,13 @@
+{"Put":{"state":"Λ‡the quick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"3"}
+{"Key":"d"}
+{"Key":"3"}
+{"Key":"l"}
+{"Get":{"state":"Λ‡ brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"Key":"j"}
+{"Key":"."}
+{"Get":{"state":" brown\nˇ over\nthe lazy dog","mode":"Normal"}}
+{"Key":"j"}
+{"Key":"2"}
+{"Key":"."}
+{"Get":{"state":" brown\n over\nˇe lazy dog","mode":"Normal"}}

crates/vim/test_data/test_repeat_visual.json πŸ”—

@@ -0,0 +1,51 @@
+{"Put":{"state":"Λ‡the quick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"w"}
+{"Key":"s"}
+{"Key":"o"}
+{"Key":"escape"}
+{"Get":{"state":"Λ‡o quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"Key":"j"}
+{"Key":"w"}
+{"Key":"."}
+{"Get":{"state":"o quick brown\nfox Λ‡ops over\nthe lazy dog","mode":"Normal"}}
+{"Key":"f"}
+{"Key":"r"}
+{"Key":"."}
+{"Get":{"state":"o quick brown\nfox ops oveˇothe lazy dog","mode":"Normal"}}
+{"Put":{"state":"the Λ‡quick brown\nfox jumps over\nfox jumps over\nfox jumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"j"}
+{"Key":"x"}
+{"Get":{"state":"the Λ‡umps over\nfox jumps over\nfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"Key":"."}
+{"Get":{"state":"the Λ‡umps over\nfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"Key":"w"}
+{"Key":"."}
+{"Get":{"state":"the umps Λ‡umps over\nthe lazy dog","mode":"Normal"}}
+{"Key":"j"}
+{"Key":"."}
+{"Get":{"state":"the umps umps over\nthe Λ‡og","mode":"Normal"}}
+{"Put":{"state":"Λ‡the quick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"ctrl-v"}
+{"Key":"j"}
+{"Key":"j"}
+{"Key":"shift-i"}
+{"Key":"o"}
+{"Key":"escape"}
+{"Get":{"state":"Λ‡othe quick brown\nofox jumps over\nothe lazy dog","mode":"Normal"}}
+{"Key":"j"}
+{"Key":"4"}
+{"Key":"l"}
+{"Key":"."}
+{"Get":{"state":"othe quick brown\nofoxˇo jumps over\notheo lazy dog","mode":"Normal"}}
+{"Put":{"state":"Λ‡the quick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"shift-r"}
+{"Key":"o"}
+{"Key":"escape"}
+{"Get":{"state":"Λ‡o\nfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"Key":"j"}
+{"Key":"."}
+{"Get":{"state":"o\nˇo\nthe lazy dog","mode":"Normal"}}

crates/vim/test_data/test_wrapped_lines.json πŸ”—

@@ -53,3 +53,9 @@
 {"Key":"i"}
 {"Key":"w"}
 {"Get":{"state":"fourteenˇ \nfourteen char\n","mode":"Normal"}}
+{"Key":"j"}
+{"Key":"shift-f"}
+{"Key":"e"}
+{"Key":"f"}
+{"Key":"r"}
+{"Get":{"state":"fourteen \nfourteen chaˇr\n","mode":"Normal"}}

crates/vim/test_data/test_zero.json πŸ”—

@@ -0,0 +1,7 @@
+{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"0"}
+{"Get":{"state":"Λ‡The quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"Key":"1"}
+{"Key":"0"}
+{"Key":"l"}
+{"Get":{"state":"The quick Λ‡brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}

crates/workspace/src/item.rs πŸ”—

@@ -474,8 +474,14 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
                     for item_event in T::to_item_events(event).into_iter() {
                         match item_event {
                             ItemEvent::CloseItem => {
-                                pane.update(cx, |pane, cx| pane.close_item_by_id(item.id(), cx))
-                                    .detach_and_log_err(cx);
+                                pane.update(cx, |pane, cx| {
+                                    pane.close_item_by_id(
+                                        item.id(),
+                                        crate::SaveBehavior::PromptOnWrite,
+                                        cx,
+                                    )
+                                })
+                                .detach_and_log_err(cx);
                                 return;
                             }
 

crates/workspace/src/pane.rs πŸ”—

@@ -25,8 +25,8 @@ use gpui::{
     keymap_matcher::KeymapContext,
     platform::{CursorStyle, MouseButton, NavigationDirection, PromptLevel},
     Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
-    LayoutContext, ModelHandle, MouseRegion, PaintContext, Quad, Task, View, ViewContext,
-    ViewHandle, WeakViewHandle, WindowContext,
+    ModelHandle, MouseRegion, Quad, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+    WindowContext,
 };
 use project::{Project, ProjectEntryId, ProjectPath};
 use serde::Deserialize;
@@ -43,6 +43,19 @@ use std::{
 };
 use theme::{Theme, ThemeSettings};
 
+#[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub enum SaveBehavior {
+    /// ask before overwriting conflicting files (used by default with %s)
+    PromptOnConflict,
+    /// ask before writing any file that wouldn't be auto-saved (used by default with %w)
+    PromptOnWrite,
+    /// never prompt, write on conflict (used with vim's :w!)
+    SilentlyOverwrite,
+    /// skip all save-related behaviour (used with vim's :cq)
+    DontSave,
+}
+
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct ActivateItem(pub usize);
 
@@ -64,13 +77,17 @@ pub struct CloseItemsToTheRightById {
     pub pane: WeakViewHandle<Pane>,
 }
 
+#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+pub struct CloseActiveItem {
+    pub save_behavior: Option<SaveBehavior>,
+}
+
 actions!(
     pane,
     [
         ActivatePrevItem,
         ActivateNextItem,
         ActivateLastItem,
-        CloseActiveItem,
         CloseInactiveItems,
         CloseCleanItems,
         CloseItemsToTheLeft,
@@ -86,7 +103,7 @@ actions!(
     ]
 );
 
-impl_actions!(pane, [ActivateItem]);
+impl_actions!(pane, [ActivateItem, CloseActiveItem]);
 
 const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 
@@ -696,22 +713,29 @@ impl Pane {
 
     pub fn close_active_item(
         &mut self,
-        _: &CloseActiveItem,
+        action: &CloseActiveItem,
         cx: &mut ViewContext<Self>,
     ) -> Option<Task<Result<()>>> {
         if self.items.is_empty() {
             return None;
         }
         let active_item_id = self.items[self.active_item_index].id();
-        Some(self.close_item_by_id(active_item_id, cx))
+        Some(self.close_item_by_id(
+            active_item_id,
+            action.save_behavior.unwrap_or(SaveBehavior::PromptOnWrite),
+            cx,
+        ))
     }
 
     pub fn close_item_by_id(
         &mut self,
         item_id_to_close: usize,
+        save_behavior: SaveBehavior,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>> {
-        self.close_items(cx, move |view_id| view_id == item_id_to_close)
+        self.close_items(cx, save_behavior, move |view_id| {
+            view_id == item_id_to_close
+        })
     }
 
     pub fn close_inactive_items(
@@ -724,7 +748,11 @@ impl Pane {
         }
 
         let active_item_id = self.items[self.active_item_index].id();
-        Some(self.close_items(cx, move |item_id| item_id != active_item_id))
+        Some(
+            self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
+                item_id != active_item_id
+            }),
+        )
     }
 
     pub fn close_clean_items(
@@ -737,7 +765,11 @@ impl Pane {
             .filter(|item| !item.is_dirty(cx))
             .map(|item| item.id())
             .collect();
-        Some(self.close_items(cx, move |item_id| item_ids.contains(&item_id)))
+        Some(
+            self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
+                item_ids.contains(&item_id)
+            }),
+        )
     }
 
     pub fn close_items_to_the_left(
@@ -762,7 +794,9 @@ impl Pane {
             .take_while(|item| item.id() != item_id)
             .map(|item| item.id())
             .collect();
-        self.close_items(cx, move |item_id| item_ids.contains(&item_id))
+        self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
+            item_ids.contains(&item_id)
+        })
     }
 
     pub fn close_items_to_the_right(
@@ -788,7 +822,9 @@ impl Pane {
             .take_while(|item| item.id() != item_id)
             .map(|item| item.id())
             .collect();
-        self.close_items(cx, move |item_id| item_ids.contains(&item_id))
+        self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
+            item_ids.contains(&item_id)
+        })
     }
 
     pub fn close_all_items(
@@ -800,12 +836,13 @@ impl Pane {
             return None;
         }
 
-        Some(self.close_items(cx, move |_| true))
+        Some(self.close_items(cx, SaveBehavior::PromptOnWrite, |_| true))
     }
 
     pub fn close_items(
         &mut self,
         cx: &mut ViewContext<Pane>,
+        save_behavior: SaveBehavior,
         should_close: impl 'static + Fn(usize) -> bool,
     ) -> Task<Result<()>> {
         // Find the items to close.
@@ -858,8 +895,15 @@ impl Pane {
                     .any(|id| saved_project_items_ids.insert(*id));
 
                 if should_save
-                    && !Self::save_item(project.clone(), &pane, item_ix, &*item, true, &mut cx)
-                        .await?
+                    && !Self::save_item(
+                        project.clone(),
+                        &pane,
+                        item_ix,
+                        &*item,
+                        save_behavior,
+                        &mut cx,
+                    )
+                    .await?
                 {
                     break;
                 }
@@ -954,13 +998,17 @@ impl Pane {
         pane: &WeakViewHandle<Pane>,
         item_ix: usize,
         item: &dyn ItemHandle,
-        should_prompt_for_save: bool,
+        save_behavior: SaveBehavior,
         cx: &mut AsyncAppContext,
     ) -> Result<bool> {
         const CONFLICT_MESSAGE: &str =
             "This file has changed on disk since you started editing it. Do you want to overwrite it?";
         const DIRTY_MESSAGE: &str = "This file contains unsaved edits. Do you want to save it?";
 
+        if save_behavior == SaveBehavior::DontSave {
+            return Ok(true);
+        }
+
         let (has_conflict, is_dirty, can_save, is_singleton) = cx.read(|cx| {
             (
                 item.has_conflict(cx),
@@ -971,18 +1019,22 @@ impl Pane {
         });
 
         if has_conflict && can_save {
-            let mut answer = pane.update(cx, |pane, cx| {
-                pane.activate_item(item_ix, true, true, cx);
-                cx.prompt(
-                    PromptLevel::Warning,
-                    CONFLICT_MESSAGE,
-                    &["Overwrite", "Discard", "Cancel"],
-                )
-            })?;
-            match answer.next().await {
-                Some(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?,
-                Some(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
-                _ => return Ok(false),
+            if save_behavior == SaveBehavior::SilentlyOverwrite {
+                pane.update(cx, |_, cx| item.save(project, cx))?.await?;
+            } else {
+                let mut answer = pane.update(cx, |pane, cx| {
+                    pane.activate_item(item_ix, true, true, cx);
+                    cx.prompt(
+                        PromptLevel::Warning,
+                        CONFLICT_MESSAGE,
+                        &["Overwrite", "Discard", "Cancel"],
+                    )
+                })?;
+                match answer.next().await {
+                    Some(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?,
+                    Some(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
+                    _ => return Ok(false),
+                }
             }
         } else if is_dirty && (can_save || is_singleton) {
             let will_autosave = cx.read(|cx| {
@@ -991,7 +1043,7 @@ impl Pane {
                     AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
                 ) && Self::can_autosave_item(&*item, cx)
             });
-            let should_save = if should_prompt_for_save && !will_autosave {
+            let should_save = if save_behavior == SaveBehavior::PromptOnWrite && !will_autosave {
                 let mut answer = pane.update(cx, |pane, cx| {
                     pane.activate_item(item_ix, true, true, cx);
                     cx.prompt(
@@ -1113,7 +1165,12 @@ impl Pane {
                 AnchorCorner::TopLeft,
                 if is_active_item {
                     vec![
-                        ContextMenuItem::action("Close Active Item", CloseActiveItem),
+                        ContextMenuItem::action(
+                            "Close Active Item",
+                            CloseActiveItem {
+                                save_behavior: None,
+                            },
+                        ),
                         ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
                         ContextMenuItem::action("Close Clean Items", CloseCleanItems),
                         ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft),
@@ -1128,8 +1185,12 @@ impl Pane {
                             move |cx| {
                                 if let Some(pane) = pane.upgrade(cx) {
                                     pane.update(cx, |pane, cx| {
-                                        pane.close_item_by_id(target_item_id, cx)
-                                            .detach_and_log_err(cx);
+                                        pane.close_item_by_id(
+                                            target_item_id,
+                                            SaveBehavior::PromptOnWrite,
+                                            cx,
+                                        )
+                                        .detach_and_log_err(cx);
                                     })
                                 }
                             }
@@ -1278,7 +1339,12 @@ impl Pane {
                                 .on_click(MouseButton::Middle, {
                                     let item_id = item.id();
                                     move |_, pane, cx| {
-                                        pane.close_item_by_id(item_id, cx).detach_and_log_err(cx);
+                                        pane.close_item_by_id(
+                                            item_id,
+                                            SaveBehavior::PromptOnWrite,
+                                            cx,
+                                        )
+                                        .detach_and_log_err(cx);
                                     }
                                 })
                                 .on_down(
@@ -1440,10 +1506,10 @@ impl Pane {
                 None
             };
 
-            Canvas::new(move |scene, bounds, _, _, _| {
+            Canvas::new(move |bounds, _, _, cx| {
                 if let Some(color) = icon_color {
                     let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
-                    scene.push_quad(Quad {
+                    cx.scene().push_quad(Quad {
                         bounds: square,
                         background: Some(color),
                         border: Default::default(),
@@ -1486,7 +1552,8 @@ impl Pane {
                     cx.window_context().defer(move |cx| {
                         if let Some(pane) = pane.upgrade(cx) {
                             pane.update(cx, |pane, cx| {
-                                pane.close_item_by_id(item_id, cx).detach_and_log_err(cx);
+                                pane.close_item_by_id(item_id, SaveBehavior::PromptOnWrite, cx)
+                                    .detach_and_log_err(cx);
                             });
                         }
                     });
@@ -1999,7 +2066,7 @@ impl<V: 'static> Element<V> for PaneBackdrop<V> {
         &mut self,
         constraint: gpui::SizeConstraint,
         view: &mut V,
-        cx: &mut LayoutContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         let size = self.child.layout(constraint, view, cx);
         (size, ())
@@ -2007,25 +2074,24 @@ impl<V: 'static> Element<V> for PaneBackdrop<V> {
 
     fn paint(
         &mut self,
-        scene: &mut gpui::SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut PaintContext<V>,
+        cx: &mut ViewContext<V>,
     ) -> Self::PaintState {
         let background = theme::current(cx).editor.background;
 
         let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
 
-        scene.push_quad(gpui::Quad {
+        cx.scene().push_quad(gpui::Quad {
             bounds: RectF::new(bounds.origin(), bounds.size()),
             background: Some(background),
             ..Default::default()
         });
 
         let child_view_id = self.child_view;
-        scene.push_mouse_region(
+        cx.scene().push_mouse_region(
             MouseRegion::new::<Self>(child_view_id, 0, visible_bounds).on_down(
                 gpui::platform::MouseButton::Left,
                 move |_, _: &mut V, cx| {
@@ -2035,10 +2101,9 @@ impl<V: 'static> Element<V> for PaneBackdrop<V> {
             ),
         );
 
-        scene.paint_layer(Some(bounds), |scene| {
-            self.child
-                .paint(scene, bounds.origin(), visible_bounds, view, cx)
-        })
+        cx.scene().push_layer(Some(bounds));
+        self.child.paint(bounds.origin(), visible_bounds, view, cx);
+        cx.scene().pop_layer();
     }
 
     fn rect_for_text_range(
@@ -2089,7 +2154,14 @@ mod tests {
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         pane.update(cx, |pane, cx| {
-            assert!(pane.close_active_item(&CloseActiveItem, cx).is_none())
+            assert!(pane
+                .close_active_item(
+                    &CloseActiveItem {
+                        save_behavior: None
+                    },
+                    cx
+                )
+                .is_none())
         });
     }
 
@@ -2339,31 +2411,59 @@ mod tests {
         add_labeled_item(&pane, "1", false, cx);
         assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
 
-        pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
-            .unwrap()
-            .await
-            .unwrap();
+        pane.update(cx, |pane, cx| {
+            pane.close_active_item(
+                &CloseActiveItem {
+                    save_behavior: None,
+                },
+                cx,
+            )
+        })
+        .unwrap()
+        .await
+        .unwrap();
         assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
 
         pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
         assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
 
-        pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
-            .unwrap()
-            .await
-            .unwrap();
+        pane.update(cx, |pane, cx| {
+            pane.close_active_item(
+                &CloseActiveItem {
+                    save_behavior: None,
+                },
+                cx,
+            )
+        })
+        .unwrap()
+        .await
+        .unwrap();
         assert_item_labels(&pane, ["A", "B*", "C"], cx);
 
-        pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
-            .unwrap()
-            .await
-            .unwrap();
+        pane.update(cx, |pane, cx| {
+            pane.close_active_item(
+                &CloseActiveItem {
+                    save_behavior: None,
+                },
+                cx,
+            )
+        })
+        .unwrap()
+        .await
+        .unwrap();
         assert_item_labels(&pane, ["A", "C*"], cx);
 
-        pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
-            .unwrap()
-            .await
-            .unwrap();
+        pane.update(cx, |pane, cx| {
+            pane.close_active_item(
+                &CloseActiveItem {
+                    save_behavior: None,
+                },
+                cx,
+            )
+        })
+        .unwrap()
+        .await
+        .unwrap();
         assert_item_labels(&pane, ["A*"], cx);
     }
 

crates/workspace/src/pane/dragged_item_receiver.rs πŸ”—

@@ -50,7 +50,7 @@ where
         Stack::new()
             .with_child(render_child(state, cx))
             .with_children(drag_position.map(|drag_position| {
-                Canvas::new(move |scene, bounds, _, _, cx| {
+                Canvas::new(move |bounds, _, _, cx| {
                     if bounds.contains_point(drag_position) {
                         let overlay_region = split_margin
                             .and_then(|split_margin| {
@@ -60,14 +60,15 @@ where
                             .map(|(dir, margin)| dir.along_edge(bounds, margin))
                             .unwrap_or(bounds);
 
-                        scene.paint_stacking_context(None, None, |scene| {
-                            scene.push_quad(Quad {
-                                bounds: overlay_region,
-                                background: Some(overlay_color(cx)),
-                                border: Default::default(),
-                                corner_radii: Default::default(),
-                            });
+                        cx.scene().push_stacking_context(None, None);
+                        let background = overlay_color(cx);
+                        cx.scene().push_quad(Quad {
+                            bounds: overlay_region,
+                            background: Some(background),
+                            border: Default::default(),
+                            corner_radii: Default::default(),
                         });
+                        cx.scene().pop_stacking_context();
                     }
                 })
             }))

crates/workspace/src/pane_group.rs πŸ”—

@@ -9,7 +9,7 @@ use gpui::{
     elements::*,
     geometry::{rect::RectF, vector::Vector2F},
     platform::{CursorStyle, MouseButton},
-    AnyViewHandle, Axis, Border, ModelHandle, ViewContext, ViewHandle,
+    AnyViewHandle, Axis, ModelHandle, ViewContext, ViewHandle,
 };
 use project::Project;
 use serde::Deserialize;
@@ -594,8 +594,8 @@ mod element {
         json::{self, ToJson},
         platform::{CursorStyle, MouseButton},
         scene::MouseDrag,
-        AnyElement, Axis, CursorRegion, Element, EventContext, LayoutContext, MouseRegion,
-        PaintContext, RectFExt, SceneBuilder, SizeConstraint, Vector2FExt, ViewContext,
+        AnyElement, Axis, CursorRegion, Element, EventContext, MouseRegion, RectFExt,
+        SizeConstraint, Vector2FExt, ViewContext,
     };
 
     use crate::{
@@ -641,7 +641,7 @@ mod element {
             remaining_flex: &mut f32,
             cross_axis_max: &mut f32,
             view: &mut Workspace,
-            cx: &mut LayoutContext<Workspace>,
+            cx: &mut ViewContext<Workspace>,
         ) {
             let flexes = self.flexes.borrow();
             let cross_axis = self.axis.invert();
@@ -789,7 +789,7 @@ mod element {
             &mut self,
             constraint: SizeConstraint,
             view: &mut Workspace,
-            cx: &mut LayoutContext<Workspace>,
+            cx: &mut ViewContext<Workspace>,
         ) -> (Vector2F, Self::LayoutState) {
             debug_assert!(self.children.len() == self.flexes.borrow().len());
 
@@ -851,19 +851,18 @@ mod element {
 
         fn paint(
             &mut self,
-            scene: &mut SceneBuilder,
             bounds: RectF,
             visible_bounds: RectF,
             remaining_space: &mut Self::LayoutState,
             view: &mut Workspace,
-            cx: &mut PaintContext<Workspace>,
+            cx: &mut ViewContext<Workspace>,
         ) -> Self::PaintState {
             let can_resize = settings::get::<WorkspaceSettings>(cx).active_pane_magnification == 1.;
             let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
 
             let overflowing = *remaining_space < 0.;
             if overflowing {
-                scene.push_layer(Some(visible_bounds));
+                cx.scene().push_layer(Some(visible_bounds));
             }
 
             let mut child_origin = bounds.origin();
@@ -874,7 +873,7 @@ mod element {
             let mut children_iter = self.children.iter_mut().enumerate().peekable();
             while let Some((ix, child)) = children_iter.next() {
                 let child_start = child_origin.clone();
-                child.paint(scene, child_origin, visible_bounds, view, cx);
+                child.paint(child_origin, visible_bounds, view, cx);
 
                 bounding_boxes.push(Some(RectF::new(child_origin, child.size())));
 
@@ -884,7 +883,7 @@ mod element {
                 }
 
                 if can_resize && children_iter.peek().is_some() {
-                    scene.push_stacking_context(None, None);
+                    cx.scene().push_stacking_context(None, None);
 
                     let handle_origin = match self.axis {
                         Axis::Horizontal => child_origin - vec2f(HANDLE_HITBOX_SIZE / 2., 0.0),
@@ -907,7 +906,7 @@ mod element {
                         Axis::Vertical => CursorStyle::ResizeUpDown,
                     };
 
-                    scene.push_cursor_region(CursorRegion {
+                    cx.scene().push_cursor_region(CursorRegion {
                         bounds: handle_bounds,
                         style,
                     });
@@ -940,14 +939,14 @@ mod element {
                                 }
                             }
                         });
-                    scene.push_mouse_region(mouse_region);
+                    cx.scene().push_mouse_region(mouse_region);
 
-                    scene.pop_stacking_context();
+                    cx.scene().pop_stacking_context();
                 }
             }
 
             if overflowing {
-                scene.pop_layer();
+                cx.scene().pop_layer();
             }
         }
 

crates/workspace/src/searchable.rs πŸ”—

@@ -1,4 +1,4 @@
-use std::any::Any;
+use std::{any::Any, sync::Arc};
 
 use gpui::{
     AnyViewHandle, AnyWeakViewHandle, AppContext, Subscription, Task, ViewContext, ViewHandle,
@@ -25,6 +25,8 @@ pub struct SearchOptions {
     pub case: bool,
     pub word: bool,
     pub regex: bool,
+    /// Specifies whether the item supports search & replace.
+    pub replacement: bool,
 }
 
 pub trait SearchableItem: Item {
@@ -35,6 +37,7 @@ pub trait SearchableItem: Item {
             case: true,
             word: true,
             regex: true,
+            replacement: true,
         }
     }
     fn to_search_event(
@@ -52,6 +55,7 @@ pub trait SearchableItem: Item {
         cx: &mut ViewContext<Self>,
     );
     fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>);
+    fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext<Self>);
     fn match_index_for_direction(
         &mut self,
         matches: &Vec<Self::Match>,
@@ -74,7 +78,7 @@ pub trait SearchableItem: Item {
     }
     fn find_matches(
         &mut self,
-        query: SearchQuery,
+        query: Arc<SearchQuery>,
         cx: &mut ViewContext<Self>,
     ) -> Task<Vec<Self::Match>>;
     fn active_match_index(
@@ -103,6 +107,7 @@ pub trait SearchableItemHandle: ItemHandle {
         cx: &mut WindowContext,
     );
     fn select_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut WindowContext);
+    fn replace(&self, _: &Box<dyn Any + Send>, _: &SearchQuery, _: &mut WindowContext);
     fn match_index_for_direction(
         &self,
         matches: &Vec<Box<dyn Any + Send>>,
@@ -113,7 +118,7 @@ pub trait SearchableItemHandle: ItemHandle {
     ) -> usize;
     fn find_matches(
         &self,
-        query: SearchQuery,
+        query: Arc<SearchQuery>,
         cx: &mut WindowContext,
     ) -> Task<Vec<Box<dyn Any + Send>>>;
     fn active_match_index(
@@ -189,7 +194,7 @@ impl<T: SearchableItem> SearchableItemHandle for ViewHandle<T> {
     }
     fn find_matches(
         &self,
-        query: SearchQuery,
+        query: Arc<SearchQuery>,
         cx: &mut WindowContext,
     ) -> Task<Vec<Box<dyn Any + Send>>> {
         let matches = self.update(cx, |this, cx| this.find_matches(query, cx));
@@ -209,6 +214,11 @@ impl<T: SearchableItem> SearchableItemHandle for ViewHandle<T> {
         let matches = downcast_matches(matches);
         self.update(cx, |this, cx| this.active_match_index(matches, cx))
     }
+
+    fn replace(&self, matches: &Box<dyn Any + Send>, query: &SearchQuery, cx: &mut WindowContext) {
+        let matches = matches.downcast_ref().unwrap();
+        self.update(cx, |this, cx| this.replace(matches, query, cx))
+    }
 }
 
 fn downcast_matches<T: Any + Clone>(matches: &Vec<Box<dyn Any + Send>>) -> Vec<T> {

crates/workspace/src/shared_screen.rs πŸ”—

@@ -73,14 +73,14 @@ impl View for SharedScreen {
 
         let frame = self.frame.clone();
         MouseEventHandler::new::<Focus, _>(0, cx, |_, cx| {
-            Canvas::new(move |scene, bounds, _, _, _| {
+            Canvas::new(move |bounds, _, _, cx| {
                 if let Some(frame) = frame.clone() {
                     let size = constrain_size_preserving_aspect_ratio(
                         bounds.size(),
                         vec2f(frame.width() as f32, frame.height() as f32),
                     );
                     let origin = bounds.origin() + (bounds.size() / 2.) - size / 2.;
-                    scene.push_surface(gpui::platform::mac::Surface {
+                    cx.scene().push_surface(gpui::platform::mac::Surface {
                         bounds: RectF::new(origin, size),
                         image_buffer: frame.image(),
                     });

crates/workspace/src/status_bar.rs πŸ”—

@@ -8,8 +8,8 @@ use gpui::{
         vector::{vec2f, Vector2F},
     },
     json::{json, ToJson},
-    AnyElement, AnyViewHandle, Entity, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
-    Subscription, View, ViewContext, ViewHandle, WindowContext,
+    AnyElement, AnyViewHandle, Entity, SizeConstraint, Subscription, View, ViewContext, ViewHandle,
+    WindowContext,
 };
 
 pub trait StatusItemView: View {
@@ -208,7 +208,7 @@ impl Element<StatusBar> for StatusBarElement {
         &mut self,
         mut constraint: SizeConstraint,
         view: &mut StatusBar,
-        cx: &mut LayoutContext<StatusBar>,
+        cx: &mut ViewContext<StatusBar>,
     ) -> (Vector2F, Self::LayoutState) {
         let max_width = constraint.max.x();
         constraint.min = vec2f(0., constraint.min.y());
@@ -226,23 +226,20 @@ impl Element<StatusBar> for StatusBarElement {
 
     fn paint(
         &mut self,
-        scene: &mut SceneBuilder,
         bounds: RectF,
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut StatusBar,
-        cx: &mut PaintContext<StatusBar>,
+        cx: &mut ViewContext<StatusBar>,
     ) -> Self::PaintState {
         let origin_y = bounds.upper_right().y();
         let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
 
         let left_origin = vec2f(bounds.lower_left().x(), origin_y);
-        self.left
-            .paint(scene, left_origin, visible_bounds, view, cx);
+        self.left.paint(left_origin, visible_bounds, view, cx);
 
         let right_origin = vec2f(bounds.upper_right().x() - self.right.size().x(), origin_y);
-        self.right
-            .paint(scene, right_origin, visible_bounds, view, cx);
+        self.right.paint(right_origin, visible_bounds, view, cx);
     }
 
     fn rect_for_text_range(

crates/workspace/src/workspace.rs πŸ”—

@@ -1308,13 +1308,15 @@ impl Workspace {
             }
 
             Ok(this
-                .update(&mut cx, |this, cx| this.save_all_internal(true, cx))?
+                .update(&mut cx, |this, cx| {
+                    this.save_all_internal(SaveBehavior::PromptOnWrite, cx)
+                })?
                 .await?)
         })
     }
 
     fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
-        let save_all = self.save_all_internal(false, cx);
+        let save_all = self.save_all_internal(SaveBehavior::PromptOnConflict, cx);
         Some(cx.foreground().spawn(async move {
             save_all.await?;
             Ok(())
@@ -1323,7 +1325,7 @@ impl Workspace {
 
     fn save_all_internal(
         &mut self,
-        should_prompt_to_save: bool,
+        save_behaviour: SaveBehavior,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<bool>> {
         if self.project.read(cx).is_read_only() {
@@ -1358,7 +1360,7 @@ impl Workspace {
                             &pane,
                             ix,
                             &*item,
-                            should_prompt_to_save,
+                            save_behaviour,
                             &mut cx,
                         )
                         .await?
@@ -3626,13 +3628,13 @@ fn notify_of_new_dock(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppCo
                             "Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.",
                             text,
                         )
-                        .with_custom_runs(vec![26..32, 34..46], |_, bounds, scene, cx| {
+                        .with_custom_runs(vec![26..32, 34..46], |_, bounds, cx| {
                             let code_span_background_color = settings::get::<ThemeSettings>(cx)
                                 .theme
                                 .editor
                                 .document_highlight_read_background;
 
-                            scene.push_quad(gpui::Quad {
+                            cx.scene().push_quad(gpui::Quad {
                                 bounds,
                                 background: Some(code_span_background_color),
                                 border: Default::default(),
@@ -4358,7 +4360,9 @@ mod tests {
             let item1_id = item1.id();
             let item3_id = item3.id();
             let item4_id = item4.id();
-            pane.close_items(cx, move |id| [item1_id, item3_id, item4_id].contains(&id))
+            pane.close_items(cx, SaveBehavior::PromptOnWrite, move |id| {
+                [item1_id, item3_id, item4_id].contains(&id)
+            })
         });
         cx.foreground().run_until_parked();
 
@@ -4493,7 +4497,9 @@ mod tests {
         // once for project entry 0, and once for project entry 2. After those two
         // prompts, the task should complete.
 
-        let close = left_pane.update(cx, |pane, cx| pane.close_items(cx, |_| true));
+        let close = left_pane.update(cx, |pane, cx| {
+            pane.close_items(cx, SaveBehavior::PromptOnWrite, move |_| true)
+        });
         cx.foreground().run_until_parked();
         left_pane.read_with(cx, |pane, cx| {
             assert_eq!(
@@ -4609,9 +4615,11 @@ mod tests {
             item.is_dirty = true;
         });
 
-        pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id))
-            .await
-            .unwrap();
+        pane.update(cx, |pane, cx| {
+            pane.close_items(cx, SaveBehavior::PromptOnWrite, move |id| id == item_id)
+        })
+        .await
+        .unwrap();
         assert!(!window.has_pending_prompt(cx));
         item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
 
@@ -4630,8 +4638,9 @@ mod tests {
         item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
 
         // Ensure autosave is prevented for deleted files also when closing the buffer.
-        let _close_items =
-            pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id));
+        let _close_items = pane.update(cx, |pane, cx| {
+            pane.close_items(cx, SaveBehavior::PromptOnWrite, move |id| id == item_id)
+        });
         deterministic.run_until_parked();
         assert!(window.has_pending_prompt(cx));
         item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));

crates/zed/Cargo.toml πŸ”—

@@ -132,6 +132,7 @@ tree-sitter-racket.workspace = true
 tree-sitter-yaml.workspace = true
 tree-sitter-lua.workspace = true
 tree-sitter-nix.workspace = true
+tree-sitter-nu.workspace = true
 
 url = "2.2"
 urlencoding = "2.1.2"

crates/zed/src/languages.rs πŸ”—

@@ -170,6 +170,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: Arc<dyn NodeRuntime>
     language("elm", tree_sitter_elm::language(), vec![]);
     language("glsl", tree_sitter_glsl::language(), vec![]);
     language("nix", tree_sitter_nix::language(), vec![]);
+    language("nu", tree_sitter_nu::language(), vec![]);
 }
 
 #[cfg(any(test, feature = "test-support"))]

crates/zed/src/languages/nu/config.toml πŸ”—

@@ -0,0 +1,9 @@
+name = "Nu"
+path_suffixes = ["nu"]
+line_comment = "# "
+autoclose_before = ";:.,=}])>` \n\t\""
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+]

crates/zed/src/languages/nu/highlights.scm πŸ”—

@@ -0,0 +1,302 @@
+;;; ---
+;;; keywords
+[
+    "def"
+    "def-env"
+    "alias"
+    "export-env"
+    "export"
+    "extern"
+    "module"
+
+    "let"
+    "let-env"
+    "mut"
+    "const"
+
+    "hide-env"
+
+    "source"
+    "source-env"
+
+    "overlay"
+    "register"
+
+    "loop"
+    "while"
+    "error"
+
+    "do"
+    "if"
+    "else"
+    "try"
+    "catch"
+    "match"
+
+    "break"
+    "continue"
+    "return"
+
+] @keyword
+
+(hide_mod "hide" @keyword)
+(decl_use "use" @keyword)
+
+(ctrl_for
+    "for" @keyword
+    "in" @keyword
+)
+(overlay_list "list" @keyword)
+(overlay_hide "hide" @keyword)
+(overlay_new "new" @keyword)
+(overlay_use
+    "use" @keyword
+    "as" @keyword
+)
+(ctrl_error "make" @keyword)
+
+;;; ---
+;;; literals
+(val_number) @constant
+(val_duration
+    unit: [
+        "ns" "Β΅s" "us" "ms" "sec" "min" "hr" "day" "wk"
+    ] @variable
+)
+(val_filesize
+    unit: [
+        "b" "B"
+
+        "kb" "kB" "Kb" "KB"
+        "mb" "mB" "Mb" "MB"
+        "gb" "gB" "Gb" "GB"
+        "tb" "tB" "Tb" "TB"
+        "pb" "pB" "Pb" "PB"
+        "eb" "eB" "Eb" "EB"
+        "zb" "zB" "Zb" "ZB"
+
+        "kib" "kiB" "kIB" "kIb" "Kib" "KIb" "KIB"
+        "mib" "miB" "mIB" "mIb" "Mib" "MIb" "MIB"
+        "gib" "giB" "gIB" "gIb" "Gib" "GIb" "GIB"
+        "tib" "tiB" "tIB" "tIb" "Tib" "TIb" "TIB"
+        "pib" "piB" "pIB" "pIb" "Pib" "PIb" "PIB"
+        "eib" "eiB" "eIB" "eIb" "Eib" "EIb" "EIB"
+        "zib" "ziB" "zIB" "zIb" "Zib" "ZIb" "ZIB"
+    ] @variable
+)
+(val_binary
+    [
+       "0b"
+       "0o"
+       "0x"
+    ] @constant
+    "[" @punctuation.bracket
+    digit: [
+        "," @punctuation.delimiter
+        (hex_digit) @constant
+    ]
+    "]" @punctuation.bracket
+) @constant
+(val_bool) @constant.builtin
+(val_nothing) @constant.builtin
+(val_string) @string
+(val_date) @constant
+(inter_escape_sequence) @constant
+(escape_sequence) @constant
+(val_interpolated [
+    "$\""
+    "$\'"
+    "\""
+    "\'"
+] @string)
+(unescaped_interpolated_content) @string
+(escaped_interpolated_content) @string
+(expr_interpolated ["(" ")"] @variable)
+
+;;; ---
+;;; operators
+(expr_binary [
+    "+"
+    "-"
+    "*"
+    "/"
+    "mod"
+    "//"
+    "++"
+    "**"
+    "=="
+    "!="
+    "<"
+    "<="
+    ">"
+    ">="
+    "=~"
+    "!~"
+    "and"
+    "or"
+    "xor"
+    "bit-or"
+    "bit-xor"
+    "bit-and"
+    "bit-shl"
+    "bit-shr"
+    "in"
+    "not-in"
+    "starts-with"
+    "ends-with"
+] @operator)
+
+(expr_binary opr: ([
+    "and"
+    "or"
+    "xor"
+    "bit-or"
+    "bit-xor"
+    "bit-and"
+    "bit-shl"
+    "bit-shr"
+    "in"
+    "not-in"
+    "starts-with"
+    "ends-with"
+]) @keyword)
+
+(where_command [
+    "+"
+    "-"
+    "*"
+    "/"
+    "mod"
+    "//"
+    "++"
+    "**"
+    "=="
+    "!="
+    "<"
+    "<="
+    ">"
+    ">="
+    "=~"
+    "!~"
+    "and"
+    "or"
+    "xor"
+    "bit-or"
+    "bit-xor"
+    "bit-and"
+    "bit-shl"
+    "bit-shr"
+    "in"
+    "not-in"
+    "starts-with"
+    "ends-with"
+] @operator)
+
+(assignment [
+    "="
+    "+="
+    "-="
+    "*="
+    "/="
+    "++="
+] @operator)
+
+(expr_unary ["not" "-"] @operator)
+
+(val_range [
+    ".."
+    "..="
+    "..<"
+] @operator)
+
+["=>" "=" "|"] @operator
+
+[
+    "o>"   "out>"
+    "e>"   "err>"
+    "e+o>" "err+out>"
+    "o+e>" "out+err>"
+] @special
+
+;;; ---
+;;; punctuation
+[
+    ","
+    ";"
+] @punctuation.delimiter
+
+(param_short_flag "-" @punctuation.delimiter)
+(param_long_flag ["--"] @punctuation.delimiter)
+(long_flag ["--"] @punctuation.delimiter)
+(param_rest "..." @punctuation.delimiter)
+(param_type [":"] @punctuation.special)
+(param_value ["="] @punctuation.special)
+(param_cmd ["@"] @punctuation.special)
+(param_opt ["?"] @punctuation.special)
+
+[
+    "(" ")"
+    "{" "}"
+    "[" "]"
+] @punctuation.bracket
+
+(val_record
+  (record_entry ":" @punctuation.delimiter))
+;;; ---
+;;; identifiers
+(param_rest
+    name: (_) @variable)
+(param_opt
+    name: (_) @variable)
+(parameter
+    param_name: (_) @variable)
+(param_cmd
+    (cmd_identifier) @string)
+(param_long_flag) @variable
+(param_short_flag) @variable
+
+(short_flag) @variable
+(long_flag) @variable
+
+(scope_pattern [(wild_card) @function])
+
+(cmd_identifier) @function
+
+(command
+    "^" @punctuation.delimiter
+    head: (_) @function
+)
+
+"where" @function
+
+(path
+  ["." "?"] @punctuation.delimiter
+) @variable
+
+(val_variable
+  "$" @operator
+  [
+   (identifier) @variable
+   "in" @type.builtin
+   "nu" @type.builtin
+   "env" @type.builtin
+   "nothing" @type.builtin
+   ]  ; If we have a special styling, use it here
+)
+;;; ---
+;;; types
+(flat_type) @type.builtin
+(list_type
+    "list" @type
+    ["<" ">"] @punctuation.bracket
+)
+(collection_type
+    ["record" "table"] @type
+    "<" @punctuation.bracket
+    key: (_) @variable
+    ["," ":"] @punctuation.delimiter
+    ">" @punctuation.bracket
+)
+
+(shebang) @comment
+(comment) @comment

crates/zed/src/languages/python.rs πŸ”—

@@ -1,6 +1,5 @@
-use anyhow::{anyhow, Result};
+use anyhow::Result;
 use async_trait::async_trait;
-use futures::StreamExt;
 use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
 use lsp::LanguageServerBinary;
 use node_runtime::NodeRuntime;
@@ -164,31 +163,16 @@ async fn get_cached_server_binary(
     container_dir: PathBuf,
     node: &dyn NodeRuntime,
 ) -> Option<LanguageServerBinary> {
-    (|| async move {
-        let mut last_version_dir = None;
-        let mut entries = fs::read_dir(&container_dir).await?;
-        while let Some(entry) = entries.next().await {
-            let entry = entry?;
-            if entry.file_type().await?.is_dir() {
-                last_version_dir = Some(entry.path());
-            }
-        }
-        let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
-        let server_path = last_version_dir.join(SERVER_PATH);
-        if server_path.exists() {
-            Ok(LanguageServerBinary {
-                path: node.binary_path().await?,
-                arguments: server_binary_arguments(&server_path),
-            })
-        } else {
-            Err(anyhow!(
-                "missing executable in directory {:?}",
-                last_version_dir
-            ))
-        }
-    })()
-    .await
-    .log_err()
+    let server_path = container_dir.join(SERVER_PATH);
+    if server_path.exists() {
+        Some(LanguageServerBinary {
+            path: node.binary_path().await.log_err()?,
+            arguments: server_binary_arguments(&server_path),
+        })
+    } else {
+        log::error!("missing executable in directory {:?}", server_path);
+        None
+    }
 }
 
 #[cfg(test)]

crates/zed/src/languages/rust.rs πŸ”—

@@ -262,6 +262,7 @@ impl LspAdapter for RustLspAdapter {
         })
     }
 }
+
 async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
     (|| async move {
         let mut last = None;

crates/zed/src/menus.rs πŸ”—

@@ -41,7 +41,12 @@ pub fn menus() -> Vec<Menu<'static>> {
                 MenuItem::action("Save", workspace::Save),
                 MenuItem::action("Save As…", workspace::SaveAs),
                 MenuItem::action("Save All", workspace::SaveAll),
-                MenuItem::action("Close Editor", workspace::CloseActiveItem),
+                MenuItem::action(
+                    "Close Editor",
+                    workspace::CloseActiveItem {
+                        save_behavior: None,
+                    },
+                ),
                 MenuItem::action("Close Window", workspace::CloseWindow),
             ],
         },

crates/zed/src/zed.rs πŸ”—

@@ -733,7 +733,7 @@ mod tests {
     use theme::{ThemeRegistry, ThemeSettings};
     use workspace::{
         item::{Item, ItemHandle},
-        open_new, open_paths, pane, NewFile, SplitDirection, WorkspaceHandle,
+        open_new, open_paths, pane, NewFile, SaveBehavior, SplitDirection, WorkspaceHandle,
     };
 
     #[gpui::test]
@@ -1495,7 +1495,12 @@ mod tests {
 
             pane2_item.downcast::<Editor>().unwrap().downgrade()
         });
-        cx.dispatch_action(window.into(), workspace::CloseActiveItem);
+        cx.dispatch_action(
+            window.into(),
+            workspace::CloseActiveItem {
+                save_behavior: None,
+            },
+        );
 
         cx.foreground().run_until_parked();
         workspace.read_with(cx, |workspace, _| {
@@ -1503,7 +1508,12 @@ mod tests {
             assert_eq!(workspace.active_pane(), &pane_1);
         });
 
-        cx.dispatch_action(window.into(), workspace::CloseActiveItem);
+        cx.dispatch_action(
+            window.into(),
+            workspace::CloseActiveItem {
+                save_behavior: None,
+            },
+        );
         cx.foreground().run_until_parked();
         window.simulate_prompt_answer(1, cx);
         cx.foreground().run_until_parked();
@@ -1661,7 +1671,7 @@ mod tests {
         pane.update(cx, |pane, cx| {
             let editor3_id = editor3.id();
             drop(editor3);
-            pane.close_item_by_id(editor3_id, cx)
+            pane.close_item_by_id(editor3_id, SaveBehavior::PromptOnWrite, cx)
         })
         .await
         .unwrap();
@@ -1696,7 +1706,7 @@ mod tests {
         pane.update(cx, |pane, cx| {
             let editor2_id = editor2.id();
             drop(editor2);
-            pane.close_item_by_id(editor2_id, cx)
+            pane.close_item_by_id(editor2_id, SaveBehavior::PromptOnWrite, cx)
         })
         .await
         .unwrap();
@@ -1852,24 +1862,32 @@ mod tests {
         assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
 
         // Close all the pane items in some arbitrary order.
-        pane.update(cx, |pane, cx| pane.close_item_by_id(file1_item_id, cx))
-            .await
-            .unwrap();
+        pane.update(cx, |pane, cx| {
+            pane.close_item_by_id(file1_item_id, SaveBehavior::PromptOnWrite, cx)
+        })
+        .await
+        .unwrap();
         assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
 
-        pane.update(cx, |pane, cx| pane.close_item_by_id(file4_item_id, cx))
-            .await
-            .unwrap();
+        pane.update(cx, |pane, cx| {
+            pane.close_item_by_id(file4_item_id, SaveBehavior::PromptOnWrite, cx)
+        })
+        .await
+        .unwrap();
         assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
 
-        pane.update(cx, |pane, cx| pane.close_item_by_id(file2_item_id, cx))
-            .await
-            .unwrap();
+        pane.update(cx, |pane, cx| {
+            pane.close_item_by_id(file2_item_id, SaveBehavior::PromptOnWrite, cx)
+        })
+        .await
+        .unwrap();
         assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
 
-        pane.update(cx, |pane, cx| pane.close_item_by_id(file3_item_id, cx))
-            .await
-            .unwrap();
+        pane.update(cx, |pane, cx| {
+            pane.close_item_by_id(file3_item_id, SaveBehavior::PromptOnWrite, cx)
+        })
+        .await
+        .unwrap();
         assert_eq!(active_path(&workspace, cx), None);
 
         // Reopen all the closed items, ensuring they are reopened in the same order

styles/src/build_themes.ts πŸ”—

@@ -30,6 +30,9 @@ function write_themes(themes: Theme[], output_directory: string) {
         setTheme(theme)
 
         const style_tree = app()
+        // Nathan: New elements will read directly from the theme colors.
+        // Adding this during the transition. Afterwards, we can port all themes to Rust.
+        style_tree.base_theme = theme
         const style_tree_json = JSON.stringify(style_tree, null, 2)
         const temp_path = path.join(temp_directory, `${theme.name}.json`)
         const out_path = path.join(output_directory, `${theme.name}.json`)

styles/src/style_tree/search.ts πŸ”—

@@ -30,15 +30,13 @@ export default function search(): any {
         selection: theme.players[0],
         text: text(theme.highest, "mono", "default"),
         border: border(theme.highest),
-        margin: {
-            right: SEARCH_ROW_SPACING,
-        },
         padding: {
             top: 4,
             bottom: 4,
             left: 10,
             right: 4,
         },
+        margin: { right: SEARCH_ROW_SPACING }
     }
 
     const include_exclude_editor = {
@@ -125,7 +123,7 @@ export default function search(): any {
 
                     button_width: 32,
                     background: background(theme.highest, "on"),
-                    corner_radius: 2,
+                    corner_radius: 6,
                     margin: { right: 2 },
                     border: {
                         width: 1,
@@ -185,26 +183,6 @@ export default function search(): any {
                 },
             },
         }),
-        // Search tool buttons
-        // HACK: This is not how disabled elements should be created
-        // Disabled elements should use a disabled state of an interactive element, not a toggleable element with the inactive state being disabled
-        action_button: toggleable({
-            state: {
-                inactive: text_button({
-                    variant: "ghost",
-                    layer: theme.highest,
-                    disabled: true,
-                    margin: { right: SEARCH_ROW_SPACING },
-                    text_properties: { size: "sm" },
-                }),
-                active: text_button({
-                    variant: "ghost",
-                    layer: theme.highest,
-                    margin: { right: SEARCH_ROW_SPACING },
-                    text_properties: { size: "sm" },
-                }),
-            },
-        }),
         editor,
         invalid_editor: {
             ...editor,
@@ -218,12 +196,12 @@ export default function search(): any {
         match_index: {
             ...text(theme.highest, "mono", { size: "sm" }),
             padding: {
+                left: SEARCH_ROW_SPACING,
                 right: SEARCH_ROW_SPACING,
             },
         },
         option_button_group: {
             padding: {
-                left: SEARCH_ROW_SPACING,
                 right: SEARCH_ROW_SPACING,
             },
         },
@@ -397,7 +375,64 @@ export default function search(): any {
         search_bar_row_height: 34,
         search_row_spacing: 8,
         option_button_height: 22,
-        modes_container: {},
+        modes_container: {
+            padding: {
+                right: SEARCH_ROW_SPACING,
+            }
+        },
+        replace_icon: {
+            icon: {
+                color: foreground(theme.highest, "disabled"),
+                asset: "icons/replace.svg",
+                dimensions: {
+                    width: 14,
+                    height: 14,
+                },
+            },
+            container: {
+                margin: { right: 4 },
+                padding: { left: 1, right: 1 },
+            },
+        },
+        action_button: interactive({
+            base: {
+                icon_size: 14,
+                color: foreground(theme.highest, "variant"),
+
+                button_width: 32,
+                background: background(theme.highest, "on"),
+                corner_radius: 6,
+                margin: { right: 2 },
+                border: {
+                    width: 1,
+                    color: background(theme.highest, "on"),
+                },
+                padding: {
+                    left: 4,
+                    right: 4,
+                    top: 4,
+                    bottom: 4,
+                },
+            },
+            state: {
+                hovered: {
+                    ...text(theme.highest, "mono", "variant", "hovered"),
+                    background: background(theme.highest, "on", "hovered"),
+                    border: {
+                        width: 1,
+                        color: background(theme.highest, "on", "hovered"),
+                    },
+                },
+                clicked: {
+                    ...text(theme.highest, "mono", "variant", "pressed"),
+                    background: background(theme.highest, "on", "pressed"),
+                    border: {
+                        width: 1,
+                        color: background(theme.highest, "on", "pressed"),
+                    },
+                },
+            },
+        }),
         ...search_results(),
     }
 }

test.rs πŸ”—

@@ -17,10 +17,10 @@ use simplelog::SimpleLogger;
 use themes::{rose_pine, ThemeColors};
 use view::view;
 mod adapter {
+    use crate::element::AnyElement;
     use crate::element::{LayoutContext, PaintContext};
     use gpui::{geometry::rect::RectF, LayoutEngine};
     use util::ResultExt;
-    use crate::element::AnyElement;
     pub struct Adapter<V>(pub(crate) AnyElement<V>);
     impl<V: 'static> gpui::Element<V> for Adapter<V> {
         type LayoutState = Option<LayoutEngine>;
@@ -90,8 +90,8 @@ mod adapter {
 }
 mod color {
     #![allow(dead_code)]
-    use std::{num::ParseIntError, ops::Range};
     use smallvec::SmallVec;
+    use std::{num::ParseIntError, ops::Range};
     pub fn rgb<C: From<Rgba>>(hex: u32) -> C {
         let r = ((hex >> 16) & 0xFF) as f32 / 255.0;
         let g = ((hex >> 8) & 0xFF) as f32 / 255.0;
@@ -130,16 +130,7 @@ mod color {
     impl ::core::fmt::Debug for Rgba {
         fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
             ::core::fmt::Formatter::debug_struct_field4_finish(
-                f,
-                "Rgba",
-                "r",
-                &self.r,
-                "g",
-                &self.g,
-                "b",
-                &self.b,
-                "a",
-                &&self.a,
+                f, "Rgba", "r", &self.r, "g", &self.g, "b", &self.b, "a", &&self.a,
             )
         }
     }
@@ -185,7 +176,12 @@ mod color {
                 4 => (xm, m, cm),
                 _ => (cm, m, xm),
             };
-            Rgba { r, g, b, a: color.a }
+            Rgba {
+                r,
+                g,
+                b,
+                a: color.a,
+            }
         }
     }
     impl TryFrom<&'_ str> for Rgba {
@@ -239,16 +235,7 @@ mod color {
     impl ::core::fmt::Debug for Hsla {
         fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
             ::core::fmt::Formatter::debug_struct_field4_finish(
-                f,
-                "Hsla",
-                "h",
-                &self.h,
-                "s",
-                &self.s,
-                "l",
-                &self.l,
-                "a",
-                &&self.a,
+                f, "Hsla", "h", &self.h, "s", &self.s, "l", &self.l, "a", &&self.a,
             )
         }
     }
@@ -258,8 +245,7 @@ mod color {
     impl ::core::cmp::PartialEq for Hsla {
         #[inline]
         fn eq(&self, other: &Hsla) -> bool {
-            self.h == other.h && self.s == other.s && self.l == other.l
-                && self.a == other.a
+            self.h == other.h && self.s == other.s && self.l == other.l && self.a == other.a
         }
     }
     pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
@@ -271,7 +257,12 @@ mod color {
         }
     }
     pub fn black() -> Hsla {
-        Hsla { h: 0., s: 0., l: 0., a: 1. }
+        Hsla {
+            h: 0.,
+            s: 0.,
+            l: 0.,
+            a: 1.,
+        }
     }
     impl From<Rgba> for Hsla {
         fn from(color: Rgba) -> Self {
@@ -298,7 +289,12 @@ mod color {
             } else {
                 ((r - g) / delta + 4.0) / 6.0
             };
-            Hsla { h, s, l, a: color.a }
+            Hsla {
+                h,
+                s,
+                l,
+                a: color.a,
+            }
         }
     }
     impl Hsla {
@@ -364,8 +360,7 @@ mod color {
             positions: SmallVec::new(),
         };
         let num_colors: f32 = scale.colors.len() as f32 - 1.0;
-        scale
-            .positions = (0..scale.colors.len())
+        scale.positions = (0..scale.colors.len())
             .map(|i| i as f32 / num_colors)
             .collect();
         scale
@@ -375,12 +370,10 @@ mod color {
             if true {
                 if !(0.0 <= t && t <= 1.0) {
                     {
-                        ::core::panicking::panic_fmt(
-                            format_args!(
-                                "t value {0} is out of range. Expected value in range 0.0 to 1.0",
-                                t,
-                            ),
-                        );
+                        ::core::panicking::panic_fmt(format_args!(
+                            "t value {0} is out of range. Expected value in range 0.0 to 1.0",
+                            t,
+                        ));
                     }
                 }
             }
@@ -412,10 +405,12 @@ mod color {
 mod components {
     use crate::{
         element::{Element, ElementMetadata},
-        frame, text::ArcCow, themes::rose_pine,
+        frame,
+        text::ArcCow,
+        themes::rose_pine,
     };
     use gpui::{platform::MouseButton, ViewContext};
-    use playground_macros::Element;
+    use gpui2_macros::Element;
     use std::{marker::PhantomData, rc::Rc};
     struct ButtonHandlers<V, D> {
         click: Option<Rc<dyn Fn(&mut V, &D, &mut ViewContext<V>)>>,
@@ -498,18 +493,11 @@ mod components {
             self.icon = Some(icon.into());
             self
         }
-        pub fn click(
-            self,
-            handler: impl Fn(&mut V, &D, &mut ViewContext<V>) + 'static,
-        ) -> Self {
+        pub fn click(self, handler: impl Fn(&mut V, &D, &mut ViewContext<V>) + 'static) -> Self {
             let data = self.data.clone();
-            Element::click(
-                self,
-                MouseButton::Left,
-                move |view, _, cx| {
-                    handler(view, data.as_ref(), cx);
-                },
-            )
+            Element::click(self, MouseButton::Left, move |view, _, cx| {
+                handler(view, data.as_ref(), cx);
+            })
         }
     }
     pub fn button<V>() -> Button<V, ()> {
@@ -523,11 +511,9 @@ mod components {
                 .children(self.label.clone());
             if let Some(handler) = self.handlers.click.clone() {
                 let data = self.data.clone();
-                button
-                    .mouse_down(
-                        MouseButton::Left,
-                        move |view, event, cx| { handler(view, data.as_ref(), cx) },
-                    )
+                button.mouse_down(MouseButton::Left, move |view, event, cx| {
+                    handler(view, data.as_ref(), cx)
+                })
             } else {
                 button
             }
@@ -535,8 +521,11 @@ mod components {
     }
 }
 mod element {
+    pub use crate::paint_context::PaintContext;
     use crate::{
-        adapter::Adapter, color::Hsla, hoverable::Hoverable,
+        adapter::Adapter,
+        color::Hsla,
+        hoverable::Hoverable,
         style::{Display, Fill, OptionalStyle, Overflow, Position},
     };
     use anyhow::Result;
@@ -546,12 +535,12 @@ mod element {
         platform::{MouseButton, MouseButtonEvent},
         EngineLayout, EventContext, RenderContext, ViewContext,
     };
-    use playground_macros::tailwind_lengths;
+    use gpui2_macros::tailwind_lengths;
     use std::{
         any::{Any, TypeId},
-        cell::Cell, rc::Rc,
+        cell::Cell,
+        rc::Rc,
     };
-    pub use crate::paint_context::PaintContext;
     pub use taffy::tree::NodeId;
     pub struct Layout<'a, E: ?Sized> {
         pub from_engine: EngineLayout,
@@ -627,33 +616,24 @@ mod element {
             Self: Sized,
         {
             let pressed: Rc<Cell<bool>> = Default::default();
-            self.mouse_down(
-                    button,
-                    {
-                        let pressed = pressed.clone();
-                        move |_, _, _| {
-                            pressed.set(true);
-                        }
-                    },
-                )
-                .mouse_up_outside(
-                    button,
-                    {
-                        let pressed = pressed.clone();
-                        move |_, _, _| {
-                            pressed.set(false);
-                        }
-                    },
-                )
-                .mouse_up(
-                    button,
-                    move |view, event, event_cx| {
-                        if pressed.get() {
-                            pressed.set(false);
-                            handler(view, event, event_cx);
-                        }
-                    },
-                )
+            self.mouse_down(button, {
+                let pressed = pressed.clone();
+                move |_, _, _| {
+                    pressed.set(true);
+                }
+            })
+            .mouse_up_outside(button, {
+                let pressed = pressed.clone();
+                move |_, _, _| {
+                    pressed.set(false);
+                }
+            })
+            .mouse_up(button, move |view, event, event_cx| {
+                if pressed.get() {
+                    pressed.set(false);
+                    handler(view, event, event_cx);
+                }
+            })
         }
         fn mouse_down(
             mut self,
@@ -663,17 +643,16 @@ mod element {
         where
             Self: Sized,
         {
-            self.handlers_mut()
-                .push(EventHandler {
-                    handler: Rc::new(move |view, event, event_cx| {
-                        let event = event.downcast_ref::<MouseButtonEvent>().unwrap();
-                        if event.button == button && event.is_down {
-                            handler(view, event, event_cx);
-                        }
-                    }),
-                    event_type: TypeId::of::<MouseButtonEvent>(),
-                    outside_bounds: false,
-                });
+            self.handlers_mut().push(EventHandler {
+                handler: Rc::new(move |view, event, event_cx| {
+                    let event = event.downcast_ref::<MouseButtonEvent>().unwrap();
+                    if event.button == button && event.is_down {
+                        handler(view, event, event_cx);
+                    }
+                }),
+                event_type: TypeId::of::<MouseButtonEvent>(),
+                outside_bounds: false,
+            });
             self
         }
         fn mouse_down_outside(
@@ -684,17 +663,16 @@ mod element {
         where
             Self: Sized,
         {
-            self.handlers_mut()
-                .push(EventHandler {
-                    handler: Rc::new(move |view, event, event_cx| {
-                        let event = event.downcast_ref::<MouseButtonEvent>().unwrap();
-                        if event.button == button && event.is_down {
-                            handler(view, event, event_cx);
-                        }
-                    }),
-                    event_type: TypeId::of::<MouseButtonEvent>(),
-                    outside_bounds: true,
-                });
+            self.handlers_mut().push(EventHandler {
+                handler: Rc::new(move |view, event, event_cx| {
+                    let event = event.downcast_ref::<MouseButtonEvent>().unwrap();
+                    if event.button == button && event.is_down {
+                        handler(view, event, event_cx);
+                    }
+                }),
+                event_type: TypeId::of::<MouseButtonEvent>(),
+                outside_bounds: true,
+            });
             self
         }
         fn mouse_up(
@@ -705,17 +683,16 @@ mod element {
         where
             Self: Sized,
         {
-            self.handlers_mut()
-                .push(EventHandler {
-                    handler: Rc::new(move |view, event, event_cx| {
-                        let event = event.downcast_ref::<MouseButtonEvent>().unwrap();
-                        if event.button == button && !event.is_down {
-                            handler(view, event, event_cx);
-                        }
-                    }),
-                    event_type: TypeId::of::<MouseButtonEvent>(),
-                    outside_bounds: false,
-                });
+            self.handlers_mut().push(EventHandler {
+                handler: Rc::new(move |view, event, event_cx| {
+                    let event = event.downcast_ref::<MouseButtonEvent>().unwrap();
+                    if event.button == button && !event.is_down {
+                        handler(view, event, event_cx);
+                    }
+                }),
+                event_type: TypeId::of::<MouseButtonEvent>(),
+                outside_bounds: false,
+            });
             self
         }
         fn mouse_up_outside(
@@ -726,17 +703,16 @@ mod element {
         where
             Self: Sized,
         {
-            self.handlers_mut()
-                .push(EventHandler {
-                    handler: Rc::new(move |view, event, event_cx| {
-                        let event = event.downcast_ref::<MouseButtonEvent>().unwrap();
-                        if event.button == button && !event.is_down {
-                            handler(view, event, event_cx);
-                        }
-                    }),
-                    event_type: TypeId::of::<MouseButtonEvent>(),
-                    outside_bounds: true,
-                });
+            self.handlers_mut().push(EventHandler {
+                handler: Rc::new(move |view, event, event_cx| {
+                    let event = event.downcast_ref::<MouseButtonEvent>().unwrap();
+                    if event.button == button && !event.is_down {
+                        handler(view, event, event_cx);
+                    }
+                }),
+                event_type: TypeId::of::<MouseButtonEvent>(),
+                outside_bounds: true,
+            });
             self
         }
         fn block(mut self) -> Self
@@ -764,9 +740,7 @@ mod element {
         where
             Self: Sized,
         {
-            self
-                .declared_style()
-                .overflow = OptionalPoint {
+            self.declared_style().overflow = OptionalPoint {
                 x: Some(Overflow::Visible),
                 y: Some(Overflow::Visible),
             };
@@ -776,9 +750,7 @@ mod element {
         where
             Self: Sized,
         {
-            self
-                .declared_style()
-                .overflow = OptionalPoint {
+            self.declared_style().overflow = OptionalPoint {
                 x: Some(Overflow::Hidden),
                 y: Some(Overflow::Hidden),
             };
@@ -788,9 +760,7 @@ mod element {
         where
             Self: Sized,
         {
-            self
-                .declared_style()
-                .overflow = OptionalPoint {
+            self.declared_style().overflow = OptionalPoint {
                 x: Some(Overflow::Scroll),
                 y: Some(Overflow::Scroll),
             };
@@ -4485,11 +4455,7 @@ mod element {
         layout: Option<(NodeId, Box<dyn Any>)>,
     }
     impl<V: 'static> AnyElement<V> {
-        pub fn layout(
-            &mut self,
-            view: &mut V,
-            cx: &mut LayoutContext<V>,
-        ) -> Result<NodeId> {
+        pub fn layout(&mut self, view: &mut V, cx: &mut LayoutContext<V>) -> Result<NodeId> {
             let pushed_text_style = self.push_text_style(cx);
             let (node_id, layout) = self.element.layout(view, cx)?;
             self.layout = Some((node_id, layout));
@@ -4511,30 +4477,25 @@ mod element {
         }
         pub fn paint(&mut self, view: &mut V, cx: &mut PaintContext<V>) -> Result<()> {
             let pushed_text_style = self.push_text_style(cx);
-            let (layout_node_id, element_layout) = self
-                .layout
-                .as_mut()
-                .expect("paint called before layout");
+            let (layout_node_id, element_layout) =
+                self.layout.as_mut().expect("paint called before layout");
             let layout = Layout {
                 from_engine: cx
                     .layout_engine()
                     .unwrap()
                     .computed_layout(*layout_node_id)
-                    .expect(
-                        "you can currently only use playground elements within an adapter",
-                    ),
+                    .expect("make sure you're using this within a gpui2 adapter element"),
                 from_element: element_layout.as_mut(),
             };
             let style = self.element.style();
             let fill_color = style.fill.flatten().and_then(|fill| fill.color());
             if let Some(fill_color) = fill_color {
-                cx.scene
-                    .push_quad(gpui::scene::Quad {
-                        bounds: layout.from_engine.bounds,
-                        background: Some(fill_color.into()),
-                        border: Default::default(),
-                        corner_radii: Default::default(),
-                    });
+                cx.scene.push_quad(gpui::scene::Quad {
+                    bounds: layout.from_engine.bounds,
+                    background: Some(fill_color.into()),
+                    border: Default::default(),
+                    corner_radii: Default::default(),
+                });
             }
             for event_handler in self.element.handlers_mut().iter().cloned() {
                 let EngineLayout { order, bounds } = layout.from_engine;
@@ -4547,10 +4508,7 @@ mod element {
                         bounds,
                         outside_bounds: event_handler.outside_bounds,
                         event_handler: Rc::new(move |view, event, window_cx, view_id| {
-                            let mut view_context = ViewContext::mutable(
-                                window_cx,
-                                view_id,
-                            );
+                            let mut view_context = ViewContext::mutable(window_cx, view_id);
                             let mut event_context = EventContext::new(&mut view_context);
                             view_event_handler(
                                 view.downcast_mut().unwrap(),
@@ -4607,14 +4565,14 @@ mod element {
 mod frame {
     use crate::{
         element::{
-            AnyElement, Element, EventHandler, IntoElement, Layout, LayoutContext,
-            NodeId, PaintContext,
+            AnyElement, Element, EventHandler, IntoElement, Layout, LayoutContext, NodeId,
+            PaintContext,
         },
         style::{OptionalStyle, Style},
     };
     use anyhow::{anyhow, Result};
     use gpui::LayoutNodeId;
-    use playground_macros::IntoElement;
+    use gpui2_macros::IntoElement;
     #[element_crate = "crate"]
     pub struct Frame<V: 'static> {
         style: OptionalStyle,
@@ -4656,12 +4614,13 @@ mod frame {
             let style: Style = self.style.into();
             let node_id = cx
                 .layout_engine()
-                .ok_or_else(|| ::anyhow::__private::must_use({
-                    let error = ::anyhow::__private::format_err(
-                        format_args!("no layout engine"),
-                    );
-                    error
-                }))?
+                .ok_or_else(|| {
+                    ::anyhow::__private::must_use({
+                        let error =
+                            ::anyhow::__private::format_err(format_args!("no layout engine"));
+                        error
+                    })
+                })?
                 .add_node(style.to_taffy(rem_size), child_layout_node_ids)?;
             Ok((node_id, ()))
         }
@@ -4687,18 +4646,23 @@ mod frame {
             I: IntoIterator<Item = E>,
             E: IntoElement<V>,
         {
-            self.children.extend(children.into_iter().map(|e| e.into_any_element()));
+            self.children
+                .extend(children.into_iter().map(|e| e.into_any_element()));
             self
         }
     }
 }
 mod hoverable {
-    use std::{cell::Cell, marker::PhantomData, rc::Rc};
+    use crate::{
+        element::Element,
+        style::{OptionalStyle, Style},
+    };
     use gpui::{
         geometry::{rect::RectF, vector::Vector2F},
-        scene::MouseMove, EngineLayout,
+        scene::MouseMove,
+        EngineLayout,
     };
-    use crate::{element::Element, style::{OptionalStyle, Style}};
+    use std::{cell::Cell, marker::PhantomData, rc::Rc};
     pub struct Hoverable<V, E> {
         hover_style: OptionalStyle,
         computed_style: Option<Style>,
@@ -4760,10 +4724,10 @@ mod hoverable {
     }
 }
 mod paint_context {
-    use std::{any::TypeId, rc::Rc};
     use derive_more::{Deref, DerefMut};
     use gpui::{geometry::rect::RectF, EventContext, RenderContext, ViewContext};
     pub use gpui::{LayoutContext, PaintContext as LegacyPaintContext};
+    use std::{any::TypeId, rc::Rc};
     pub use taffy::tree::NodeId;
     pub struct PaintContext<'a, 'b, 'c, 'd, V> {
         #[deref]
@@ -4833,13 +4797,12 @@ mod paint_context {
 mod style {
     use crate::color::Hsla;
     use gpui::geometry::{
-        DefinedLength, Edges, Length, OptionalEdges, OptionalPoint, OptionalSize, Point,
-        Size,
+        DefinedLength, Edges, Length, OptionalEdges, OptionalPoint, OptionalSize, Point, Size,
     };
     use optional::Optional;
     pub use taffy::style::{
-        AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap,
-        JustifyContent, Overflow, Position,
+        AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap, JustifyContent,
+        Overflow, Position,
     };
     pub struct Style {
         /// What layout strategy should be used?
@@ -5194,9 +5157,7 @@ mod style {
         #[inline]
         fn clone(&self) -> Fill {
             match self {
-                Fill::Color(__self_0) => {
-                    Fill::Color(::core::clone::Clone::clone(__self_0))
-                }
+                Fill::Color(__self_0) => Fill::Color(::core::clone::Clone::clone(__self_0)),
             }
         }
     }
@@ -5257,32 +5218,25 @@ mod text {
             let text = self.text.clone();
             let layout = Arc::new(Mutex::new(None));
             let style: Style = self.metadata.style.into();
-            let node_id = layout_engine
-                .add_measured_node(
-                    style.to_taffy(rem_size),
-                    {
-                        let layout = layout.clone();
-                        move |params| {
-                            let line_layout = fonts
-                                .layout_line(
-                                    text.as_ref(),
-                                    text_style.font_size,
-                                    &[(text.len(), text_style.to_run())],
-                                );
-                            let size = Size {
-                                width: line_layout.width,
-                                height: line_height,
-                            };
-                            layout
-                                .lock()
-                                .replace(TextLayout {
-                                    line_layout: Arc::new(line_layout),
-                                    line_height,
-                                });
-                            size
-                        }
-                    },
-                )?;
+            let node_id = layout_engine.add_measured_node(style.to_taffy(rem_size), {
+                let layout = layout.clone();
+                move |params| {
+                    let line_layout = fonts.layout_line(
+                        text.as_ref(),
+                        text_style.font_size,
+                        &[(text.len(), text_style.to_run())],
+                    );
+                    let size = Size {
+                        width: line_layout.width,
+                        height: line_height,
+                    };
+                    layout.lock().replace(TextLayout {
+                        line_layout: Arc::new(line_layout),
+                        line_height,
+                    });
+                    size
+                }
+            })?;
             Ok((node_id, layout))
         }
         fn paint<'a>(
@@ -5369,11 +5323,11 @@ mod themes {
     use crate::color::{Hsla, Lerp};
     use std::ops::Range;
     pub mod rose_pine {
-        use std::ops::Range;
         use crate::{
             color::{hsla, rgb, Hsla},
             ThemeColors,
         };
+        use std::ops::Range;
         pub struct RosePineThemes {
             pub default: RosePinePalette,
             pub dawn: RosePinePalette,
@@ -5426,7 +5380,7 @@ mod themes {
                     "highlight_med",
                     "highlight_high",
                 ];
-                let values: &[&dyn ::core::fmt::Debug] = &[
+                let values: &[&dyn::core::fmt::Debug] = &[
                     &self.base,
                     &self.surface,
                     &self.overlay,
@@ -5636,35 +5590,29 @@ mod view {
     }
 }
 fn main() {
-    SimpleLogger::init(LevelFilter::Info, Default::default())
-        .expect("could not initialize logger");
-    gpui::App::new(())
-        .unwrap()
-        .run(|cx| {
-            cx.add_window(
-                WindowOptions {
-                    bounds: gpui::platform::WindowBounds::Fixed(
-                        RectF::new(vec2f(0., 0.), vec2f(400., 300.)),
-                    ),
-                    center: true,
-                    ..Default::default()
-                },
-                |_| view(|_| playground(&rose_pine::moon())),
-            );
-            cx.platform().activate(true);
-        });
+    SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
+    gpui::App::new(()).unwrap().run(|cx| {
+        cx.add_window(
+            WindowOptions {
+                bounds: gpui::platform::WindowBounds::Fixed(RectF::new(
+                    vec2f(0., 0.),
+                    vec2f(400., 300.),
+                )),
+                center: true,
+                ..Default::default()
+            },
+            |_| view(|_| storybook(&rose_pine::moon())),
+        );
+        cx.platform().activate(true);
+    });
 }
-fn playground<V: 'static>(theme: &ThemeColors) -> impl Element<V> {
+fn storybook<V: 'static>(theme: &ThemeColors) -> impl Element<V> {
     frame()
         .text_color(black())
         .h_full()
         .w_half()
         .fill(theme.success(0.5))
-        .child(
-            button()
-                .label("Hello")
-                .click(|_, _, _| {
-                    ::std::io::_print(format_args!("click!\n"));
-                }),
-        )
+        .child(button().label("Hello").click(|_, _, _| {
+            ::std::io::_print(format_args!("click!\n"));
+        }))
 }