Compare commits

...

101 Commits

Author SHA1 Message Date
Max Brunsfeld
a03d062120 Make the commit sha availabe in the app, via a global
Co-authored-by: Mikayla <mikayla@zed.dev>
Co-authored-by: Kirill <kirill@zed.dev>
2023-11-17 14:52:43 -08:00
Max Brunsfeld
a8bea41ad8 Put release channel back to dev 2023-11-17 14:31:49 -08:00
Max Brunsfeld
6a22ab83cf Fix cargo check --tests invocation 2023-11-17 14:16:12 -08:00
Max Brunsfeld
5e2eb436ff Check out repo outside of reusable actions
Co-authored-by: Mikayla <mikayla@zed.dev>
Co-authored-by: Kirill <kirill@zed.dev>
2023-11-17 14:14:03 -08:00
Max Brunsfeld
c684f08e30 Wire up release_nightly workflow, to upload artifacts to DO spaces
Co-authored-by: Kirill <kirill@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.dev>
2023-11-17 14:07:51 -08:00
Mikayla
f44f60c6e6 WIP: 2023-11-17 12:54:16 -08:00
Mikayla
6976af5029 Push some sketches 2023-11-17 12:16:55 -08:00
Mikayla
5cf953d559 Add error messages to server deployment for nightly 2023-11-17 10:31:10 -08:00
Kirill Bulatov
6680e1e9fe Create new Zed release channel: nightly 2023-11-17 12:13:32 +02:00
Kirill Bulatov
9d8184670a Remove binary target collisions between zed & zed2 (#3352)
Get rid of the following warnings:

```
The example target `test_app` in package `live_kit_client2 v0.1.0 (/Users/someonetoignore/work/zed/zed/crates/live_kit_client2)` has the same output filename as the example target `test_app` in package `live_kit_client v0.1.0 (/Users/someonetoignore/work/zed/zed/crates/live_kit_client)`.
Colliding filename is: /Users/someonetoignore/work/zed/zed/target/debug/examples/test_app
The targets should have unique names.
Consider changing their names to be unique or compiling them separately.
This may become a hard error in the future; see <https://github.com/rust-lang/cargo/issues/6313>.
warning: output filename collision.
The example target `test_app` in package `live_kit_client2 v0.1.0 (/Users/someonetoignore/work/zed/zed/crates/live_kit_client2)` has the same output filename as the example target `test_app` in package `live_kit_client v0.1.0 (/Users/someonetoignore/work/zed/zed/crates/live_kit_client)`.
Colliding filename is: /Users/someonetoignore/work/zed/zed/target/debug/examples/test_app.dSYM
The targets should have unique names.
Consider changing their names to be unique or compiling them separately.
This may become a hard error in the future; see <https://github.com/rust-lang/cargo/issues/6313>.

---------------

warning: output filename collision.
The bin target `Zed` in package `zed2 v0.109.0 (/Users/administrator/actions-runner-1/_work/zed/zed/crates/zed2)` has the same output filename as the bin target `Zed` in package `zed v0.113.0 (/Users/administrator/actions-runner-1/_work/zed/zed/crates/zed)`.
Colliding filename is: /Users/administrator/actions-runner-1/_work/zed/zed/target/debug/Zed.dSYM
The targets should have unique names.
Consider changing their names to be unique or compiling them separately.
This may become a hard error in the future; see <https://github.com/rust-lang/cargo/issues/6313>.
```

as we plant to build *.dmg of both versions for a while.

Release Notes:

- N/A
2023-11-17 10:02:27 +02:00
Kirill Bulatov
89e44d4901 Remove binary target collisions between zed & zed2 2023-11-17 09:56:28 +02:00
Mikayla Maki
41ac8ef909 Restore saving (#3351)
and a bunch of random workspace stuff!

Release Notes:

-
2023-11-16 23:22:01 -08:00
Mikayla
1693718637 Merge branch 'main' into saving-2 2023-11-16 23:11:38 -08:00
Mikayla
9a3cd073c7 Restore a bunch of random workspace stuff 2023-11-16 23:05:28 -08:00
Nathan Sobo
2fb13cf1ca Separate WrappedLines from ShapedLines (#3350)
ShapedLines are never wrapped, whereas WrappedLines are optionally
wrapped if they are associated with a wrap width. Originally, when
rewriting GPUI, I tried to combine everything because wrapping is
inherently optional for the Text element, but we have a bunch of APIs
that don't make sense on a line that may wrap, so we need a distinct
type for that case.

This is a precursor to implementing clickable links in markdown. I
noticed multiple places where we were confused about whether or not the
line was wrapped so this felt important.

Release Notes:

- N/A
2023-11-16 23:20:35 -07:00
Conrad Irwin
391ab8fa1b Add more detail to panel switcher interaction (#3344)
Also featuring:
* IconButton#action()
* gpui::ManagedView
* `menu_handle()`

Release Notes:

- N/A
2023-11-16 23:16:50 -07:00
Nathan Sobo
9558da8681 Separate WrappedLines from ShapedLines
ShapedLines are never wrapped, whereas WrappedLines are optionally wrapped if
they are associated with a wrap width. I tried to combine everything because
wrapping is inherently optional for the Text element, but we have a bunch of
APIs that don't make sense on a line that may wrap, so we need a distinct type
for that case.
2023-11-16 23:10:51 -07:00
Conrad Irwin
2d1d75f482 +ManagedView
And some games with rust traits
2023-11-16 23:02:10 -07:00
Conrad Irwin
2182cb2656 Ooh generics 2023-11-16 22:16:29 -07:00
Conrad Irwin
6d4276ea5f Merge branch 'main' into collab_ui2 2023-11-16 22:08:42 -07:00
Conrad Irwin
c0ad15756c More attachment configuration for context menus 2023-11-16 21:59:23 -07:00
Max Brunsfeld
e67c44a562 Fix file-reloading race condition (#3348)
### Summary

This PR fixes a bug that @as-cii and @osiewicz saw when the on-disk
contents of files changed due to running `git checkout` at the command
line. It caused a buffer's contents to diverge from the file's on disk
contents, but the buffer to show an *unmodified* status.

I've also introduced new APIs on gpui's deterministic executor, which
make it possible to write a test that reliably triggered the bug.

### Details

The bug is triggered by the following sequence of events:
1. A buffer's file changes on disk while the buffer is *unmodified*
2. Zed reloads the new content of the file
3. Before updating the buffer itself, Zed computes a *diff* between the
buffer's current contents, and the newly-loaded contents
4. While this diff is being computed, one of two things happens:
     1. the buffer changes on-disk *again*.
2. the user edits the buffer, but undoes the edit, so that the buffer
returns to an unmodified state

The bug itself was caused by a few things:
* The buffer diffing algorithm is pretty slow, because we perform a
character-wise diff
* We previously allowed multiple reload tasks to run concurrently
* When discarding an out-of-date diff, we failed to update parts of the
buffer's state (`saved_fingerprint`) which allow us to recognize that
the buffer's content differs from the file.

It was also difficult to reproduce the problem in tests, because under
deterministic execution, because it was extremely unlikely for other
tasks to make progress *after* a file had been reloaded, but *before*
the disk task has resolved. To help with testing, I introduced a pair of
executor APIs:

`spawn_labeled`, - for spawning a background task with a given *label*
`deprioritize_task` - for forcing tasks with a given label to run
*after* all other concurrent tasks.

I also made the `Model::next_event` test helper method more useful, in
that it no longer runs *until* parked in order to wait for the next
event to occur. It just steps the executor one poll at a time until the
model emits an event.

Release Notes:

- Fixed a bug that caused buffers to report incorrect modified/conflict
status when their buffers changed on disk multiple times in rapid
succession.
2023-11-16 20:19:16 -08:00
Max Brunsfeld
32979f3aca Rename deprioritize_task -> deprioritize
It applies to a family of tasks, not a task.
2023-11-16 20:03:18 -08:00
Max Brunsfeld
f3b6719c76 Rename both PlatformDispatcher::poll and Executor::run_step to 'tick'
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2023-11-16 19:58:26 -08:00
Conrad Irwin
9547e88d88 TEMP 2023-11-16 19:50:31 -07:00
Conrad Irwin
547888942f Add storybook3 2023-11-16 19:42:25 -07:00
Mikayla Maki
61bd6bab09 Actions‽ (#3349)
This PR re-implements our actions with macros instead of a blanket impl.

Release Notes:

- N/A
2023-11-16 18:18:04 -08:00
Mikayla
432572c592 #RemoveThe2 2023-11-16 18:04:35 -08:00
Max Brunsfeld
17b2b112bc Don't update file's saved mtime when reload is aborted 2023-11-16 18:02:49 -08:00
Mikayla
49d3e1cc4b Add default derive 2023-11-16 17:39:05 -08:00
Mikayla
4de2c0f7ef Re-implement actions as derive macros instead of blanket impls 2023-11-16 17:32:02 -08:00
Max Brunsfeld
0bed5e4562 Port buffer reload bug fixes back to gpui1 crates 2023-11-16 16:03:14 -08:00
Max Brunsfeld
5f1acae0d3 Fix race conditions and bugs in Buffer::reload 2023-11-16 16:03:14 -08:00
Max Brunsfeld
89d73f713a Label the buffer's diff task so it can be deprioritized in tests 2023-11-16 16:03:13 -08:00
Max Brunsfeld
b2451d9dd6 Combine adjacent edits in buffer's diff 2023-11-16 16:03:05 -08:00
Conrad Irwin
074a221e0f Progress on ContextMenu 2023-11-16 16:59:27 -07:00
Max Brunsfeld
f9650b3111 Don't run until all the way until parked when waiting for a model's next event 2023-11-16 15:54:00 -08:00
Max Brunsfeld
6397c05835 Add the ability to deprioritize specific labeled tasks in tests 2023-11-16 15:54:00 -08:00
Conrad Irwin
9456f716c2 Only send one right click event 2023-11-16 15:30:53 -07:00
Nate Butler
83dfb191d6 UI size + other theme refinements (#3347)
[[PR Description]]

- Fixes bad titlebar states when the ui scale is set very low
- Tidies up a number of workspace styles

Release Notes:

- N/A
2023-11-16 16:30:47 -05:00
Nate Butler
3223e21d9f Add dock borders 2023-11-16 16:17:17 -05:00
Nate Butler
9c5f580012 Use Selected for active IconButtons 2023-11-16 16:17:10 -05:00
Nate Butler
3d8e63b93b Buttons should always use cursor_pointer 2023-11-16 16:09:11 -05:00
Nate Butler
c2d6d24952 Ensure the titlebar stays large enough even with small ui sizes 2023-11-16 16:01:42 -05:00
Nate Butler
a526f23c81 Add ui_font settings and set default rem size accordingly (#3346)
[[PR Description]]
- Adds ui_font_family, ui_font_features, ui_font_size to settings and
default settings
- Use the new ui font settings to set the rem size when the workspace is
created.

Release Notes:

- N/A
2023-11-16 15:40:33 -05:00
Conrad Irwin
267e07472d Checkpoint, MenuHandle can open one 2023-11-16 13:32:19 -07:00
Nate Butler
ffd092a098 Add ui_font_* for tests 2023-11-16 15:30:50 -05:00
Nate Butler
fa9f4a9355 Init rem_size in the workspace at the start of the render
Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-11-16 13:43:36 -05:00
Mikayla
a0e976599c Salvage old distributed slice code 2023-11-16 10:32:55 -08:00
Nate Butler
08dddf0b26 Revert change to default buffer font size 2023-11-16 13:13:03 -05:00
Marshall Bowers
b559bfd80f Parameterize theme2::init to allow loading just the base theme (#3345)
This PR adds a parameter to the `theme2::init` method to indicate what
the theme-loading behavior should be.

This allows us to indicate when we want to load all of the additional
built-in user themes (like in the Zed binary and in the storybook), and
when we don't want to load the user themes (like in tests).

We're using an enum over just a `bool` here for clarity at the call
site.

Release Notes:

- N/A
2023-11-16 13:03:30 -05:00
Conrad Irwin
d782426491 Dismiss tooltips on click 2023-11-16 10:26:09 -07:00
Conrad Irwin
4540f04dbe Add more detail to panel switcher interaction 2023-11-16 09:57:59 -07:00
Nate Butler
38d0fdc09a Remove todo
Co-Authored-By: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com>
2023-11-16 11:42:23 -05:00
Nate Butler
b2f9c454b0 Change the default buffer font size to 16
Co-Authored-By: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com>
2023-11-16 11:38:04 -05:00
Nate Butler
c71e522b4e Allow users to set UI font properties in their settings
Co-Authored-By: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com>
2023-11-16 11:37:46 -05:00
Kirill Bulatov
2aa7c6f2b4 Add a simple context menu into terminal2 (#3343)
Release Notes:

- N/A
2023-11-16 16:41:27 +02:00
Kirill Bulatov
ab0a3f19ab Add an actual context menu into terminal-2
(click the text, not the pane!)

Co-Authored-By: Piotr <piotr@zed.dev>
2023-11-16 16:36:44 +02:00
Kirill Bulatov
f638d4ce1d Add basic context menu element 2023-11-16 14:35:13 +02:00
Kirill Bulatov
0f0b7090b8 Fix more errors in terminal element 2023-11-16 13:10:43 +02:00
Kirill Bulatov
03b4c7c464 Terminal 2 (#3342)
Release Notes:

- N/A
2023-11-16 10:59:42 +02:00
Kirill Bulatov
fd61683c46 WIP 2023-11-16 10:40:02 +02:00
Kirill Bulatov
e3465fbcf9 Comment out the terminal view 2023-11-16 10:04:20 +02:00
Kirill Bulatov
a238368296 More compilation fixes 2023-11-16 10:04:18 +02:00
Kirill Bulatov
61d6cb880c Start porting terminal_element to gpui2
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2023-11-16 10:04:03 +02:00
Kirill Bulatov
d89c51135a Start porting terminal_view to gpui2
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2023-11-16 10:04:03 +02:00
Mikayla Maki
c8f83e2d4d Tabs n splits (#3340)
Adds back:

- [x] Tab clicking
- [x] Splits ~
- [x] Workspace deserialization

Also adds a new `FocusableView` API to GPUI

Release Notes:

- N/A
2023-11-15 23:52:43 -08:00
Mikayla
9da0b78ead Merge branch 'main' into tabs-n-splits 2023-11-15 23:41:25 -08:00
Mikayla
e9e1170976 Compile taffy in release mode 2023-11-15 23:15:36 -08:00
Mikayla
c70c0f9ae9 Remove initialize_workspace 2023-11-15 23:11:31 -08:00
Conrad Irwin
613add0aed collab ui2 (#3341)
Panel switching

* Also: Tooltip::text("whatever", cx);
* And: Tooltip::for_action("whatever", &collab_panel::Toggle, cx);
* And `overlay()` in Gpui2 (similar to `Overlay` in gpui).

Release Notes:

- N/A
2023-11-15 23:10:52 -07:00
Conrad Irwin
74afa62a55 Add Overlay component to gpui2 2023-11-15 23:00:36 -07:00
Conrad Irwin
8c14a8fa95 Merge branch 'main' into collab_ui2 2023-11-15 21:04:47 -07:00
Conrad Irwin
0a9fb3978b Enable panel switching 2023-11-15 21:01:00 -07:00
Nathan Sobo
e5ada92b7b Remove initialize from the Element trait (#3338)
Initially, we imagined registering keyboard handlers in the initialize
phase so we would understand the relationships between focus handles
during the layout pass, which would allow us to assign assign `focus_in`
styles that impact layout.

However, we soon realized that many elements aren't created until paint
time anyway, such as within the uniform list. Since it's impossible to
know prior to paint whether an element contains the focused element, it
makes more sense to eliminate the `focus_in` styling helper.

Release Notes:

- N/A
2023-11-15 19:36:35 -07:00
Mikayla
78cea69172 Add focusable view and restore workspace deserialization. Partially restore split and tab functions 2023-11-15 16:36:43 -08:00
Conrad Irwin
7f70712dac Uncomment enough to register the panel 2023-11-15 16:44:21 -07:00
Conrad Irwin
0e79a7f26b collab ui2 (#3339)
- Move placeholder titlebar render to collab_ui

Release Notes:

- N/A
2023-11-15 16:21:52 -07:00
Mikayla
e905ababcd Add back splitting 2023-11-15 15:21:19 -08:00
Conrad Irwin
793fa6e3a4 Move placeholder titlebar render to collab_ui 2023-11-15 16:09:21 -07:00
Conrad Irwin
84bcbf1128 Add collab_ui2 2023-11-15 16:08:03 -07:00
Mikayla
faf93aed4e checkpoint 2023-11-15 14:17:04 -08:00
Nathan Sobo
4f09633379 Remove focus_in styling helper 2023-11-15 14:17:49 -07:00
Nathan Sobo
c6b374ebc9 Remove initialize method from Element trait 2023-11-15 14:11:19 -07:00
Conrad Irwin
26d90a5e48 Leaky, but better, test abstraction (#3337)
Make test view creation a bit more ergonomic 

Release Notes:

- N/A
2023-11-15 14:09:02 -07:00
Conrad Irwin
0a51784dd0 Leaky, but better, test abstraction 2023-11-15 14:01:10 -07:00
Conrad Irwin
19c0b390d2 FileFinder tests (#3336)
Also including:
* Fixes for focus when closing the last item in a pane
* Workspace#active_item_as::<Editor>()
* cx.simulate_input()

Release Notes:

- N/A
2023-11-15 13:24:34 -07:00
Conrad Irwin
cebc8428c8 FileFinder tests 2023-11-15 13:16:28 -07:00
Antonio Scandurra
33a808a49b WIP 2023-11-15 20:41:09 +01:00
Antonio Scandurra
ffcc1cbf77 Clear the state of keystroke matchers when focus changes (#3335)
Release Notes:

- N/A
2023-11-15 20:41:01 +01:00
Antonio Scandurra
759ce7440c Avoid unnecessary call to with_element_id in RenderViewWith
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-11-15 20:12:30 +01:00
Antonio Scandurra
1d04dc5dbf Clear the state of keystroke matchers when focus changes
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-11-15 20:11:07 +01:00
Antonio Scandurra
3978d4e872 Show fold indicators (#3334)
Release Notes:

- N/A
2023-11-15 20:10:24 +01:00
Antonio Scandurra
08dfcba68a Fix panic when painting blocks 2023-11-15 20:02:35 +01:00
Antonio Scandurra
c225a3e5af Don't use Mutex or Arc now that app state is not Send
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-11-15 18:50:34 +01:00
Antonio Scandurra
c7b7f7dfd5 Move render_view into View::render_with
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-11-15 18:50:09 +01:00
Joseph T. Lyons
d791fc707a v0.114.x dev 2023-11-15 12:47:40 -05:00
Antonio Scandurra
9ff238921f Fix clicking on code action and fold indicators
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-11-15 18:29:53 +01:00
Antonio Scandurra
17b8e4a684 Handle clicking folded ranges
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-11-15 18:19:26 +01:00
Antonio Scandurra
3ff8c78b58 Return a Fold struct when querying the FoldMap
This contains a new `id` field that lets us distinguish among folds.

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-11-15 17:09:59 +01:00
Antonio Scandurra
851a60a68e Render fold indicators in the gutter 2023-11-15 14:07:37 +01:00
148 changed files with 18403 additions and 4775 deletions

View File

@@ -0,0 +1,15 @@
name: 'Check formatting'
description: 'Checks code formatting use cargo fmt'
runs:
using: "composite"
steps:
- name: Install Rust
shell: bash -euxo pipefail {0}
run: |
rustup set profile minimal
rustup update stable
- name: cargo fmt
shell: bash -euxo pipefail {0}
run: cargo fmt --all -- --check

42
.github/actions/run_tests/action.yml vendored Normal file
View File

@@ -0,0 +1,42 @@
name: 'Run tests'
description: 'Runs the tests'
runs:
using: "composite"
steps:
- name: Install Rust
shell: bash -euxo pipefail {0}
run: |
rustup set profile minimal
rustup update stable
rustup target add wasm32-wasi
cargo install cargo-nextest
- name: Install Node
uses: actions/setup-node@v3
with:
node-version: "18"
- name: Limit target directory size
shell: bash -euxo pipefail {0}
run: script/clear-target-dir-if-larger-than 70
- name: Run check
env:
RUSTFLAGS: -D warnings
shell: bash -euxo pipefail {0}
run: cargo check --tests --workspace
- name: Run tests
env:
RUSTFLAGS: -D warnings
shell: bash -euxo pipefail {0}
run: cargo nextest run --workspace --no-fail-fast
- name: Build collab
shell: bash -euxo pipefail {0}
run: cargo build -p collab
- name: Build other binaries
shell: bash -euxo pipefail {0}
run: cargo build --workspace --bins --all-features

View File

@@ -23,19 +23,14 @@ jobs:
- self-hosted
- test
steps:
- name: Install Rust
run: |
rustup set profile minimal
rustup update stable
- name: Checkout repo
uses: actions/checkout@v3
with:
clean: false
submodules: "recursive"
- name: cargo fmt
run: cargo fmt --all -- --check
- name: Run rustfmt
uses: ./.github/actions/check_formatting
tests:
name: Run tests
@@ -43,41 +38,15 @@ jobs:
- self-hosted
- test
needs: rustfmt
env:
RUSTFLAGS: -D warnings
steps:
- name: Install Rust
run: |
rustup set profile minimal
rustup update stable
rustup target add wasm32-wasi
cargo install cargo-nextest
- name: Install Node
uses: actions/setup-node@v3
with:
node-version: "18"
- name: Checkout repo
uses: actions/checkout@v3
with:
clean: false
submodules: "recursive"
- name: Limit target directory size
run: script/clear-target-dir-if-larger-than 70
- name: Run check
run: cargo check --workspace
- name: Run tests
run: cargo nextest run --workspace --no-fail-fast
- name: Build collab
run: cargo build -p collab
- name: Build other binaries
run: cargo build --workspace --bins --all-features
uses: ./.github/actions/run_tests
bundle:
name: Bundle app
@@ -130,6 +99,8 @@ jobs:
expected_tag_name="v${version}";;
preview)
expected_tag_name="v${version}-pre";;
nightly)
expected_tag_name="v${version}-nightly";;
*)
echo "can't publish a release on channel ${channel}"
exit 1;;
@@ -154,7 +125,9 @@ jobs:
- uses: softprops/action-gh-release@v1
name: Upload app bundle to release
if: ${{ env.RELEASE_CHANNEL }}
# TODO kb seems that zed.dev relies on GitHub releases for release version tracking.
# Find alternatives for `nightly` or just go on with more releases?
if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
with:
draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}

98
.github/workflows/release_nightly.yml vendored Normal file
View File

@@ -0,0 +1,98 @@
name: Release Nightly
on:
schedule:
# Fire every night at 1:00am
- cron: "0 1 * * *"
push:
tags:
- "nightly*"
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
jobs:
rustfmt:
name: Check formatting
runs-on:
- self-hosted
- test
steps:
- name: Checkout repo
uses: actions/checkout@v3
with:
clean: false
submodules: "recursive"
- name: Run rustfmt
uses: ./.github/actions/check_formatting
tests:
name: Run tests
runs-on:
- self-hosted
- test
needs: rustfmt
steps:
- name: Checkout repo
uses: actions/checkout@v3
with:
clean: false
submodules: "recursive"
- name: Run tests
uses: ./.github/actions/run_tests
bundle:
name: Bundle app
runs-on:
- self-hosted
- bundle
needs: tests
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
steps:
- name: Install Rust
run: |
rustup set profile minimal
rustup update stable
rustup target add aarch64-apple-darwin
rustup target add x86_64-apple-darwin
rustup target add wasm32-wasi
- name: Install Node
uses: actions/setup-node@v3
with:
node-version: "18"
- name: Checkout repo
uses: actions/checkout@v3
with:
clean: false
submodules: "recursive"
- name: Limit target directory size
run: script/clear-target-dir-if-larger-than 70
- name: Set release channel to nightly
run: |
set -eu
version=$(git rev-parse --short HEAD)
echo "Publishing version: ${version} on release channel nightly"
echo "nightly" > crates/zed/RELEASE_CHANNEL
- name: Generate license file
run: script/generate-licenses
- name: Create app bundle
run: script/bundle
- name: Upload Zed Nightly
run: script/upload-nightly

111
Cargo.lock generated
View File

@@ -1829,6 +1829,47 @@ dependencies = [
"zed-actions",
]
[[package]]
name = "collab_ui2"
version = "0.1.0"
dependencies = [
"anyhow",
"call2",
"channel2",
"client2",
"clock",
"collections",
"db2",
"editor2",
"feature_flags2",
"futures 0.3.28",
"fuzzy",
"gpui2",
"language2",
"lazy_static",
"log",
"menu2",
"notifications2",
"picker2",
"postage",
"pretty_assertions",
"project2",
"rich_text2",
"rpc2",
"schemars",
"serde",
"serde_derive",
"settings2",
"smallvec",
"theme2",
"time",
"tree-sitter-markdown",
"ui2",
"util",
"workspace2",
"zed_actions2",
]
[[package]]
name = "collections"
version = "0.1.0"
@@ -3756,6 +3797,7 @@ dependencies = [
"image",
"itertools 0.10.5",
"lazy_static",
"linkme",
"log",
"media",
"metal",
@@ -4774,6 +4816,26 @@ version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linkme"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91ed2ee9464ff9707af8e9ad834cffa4802f072caad90639c583dd3c62e6e608"
dependencies = [
"linkme-impl",
]
[[package]]
name = "linkme-impl"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba125974b109d512fccbc6c0244e7580143e460895dfd6ea7f8bbb692fd94396"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
]
[[package]]
name = "linux-raw-sys"
version = "0.0.42"
@@ -8761,6 +8823,17 @@ dependencies = [
"util",
]
[[package]]
name = "storybook3"
version = "0.1.0"
dependencies = [
"anyhow",
"gpui2",
"settings2",
"theme2",
"ui2",
]
[[package]]
name = "stringprep"
version = "0.1.4"
@@ -9134,6 +9207,39 @@ dependencies = [
"workspace",
]
[[package]]
name = "terminal_view2"
version = "0.1.0"
dependencies = [
"anyhow",
"client2",
"db2",
"dirs 4.0.0",
"editor2",
"futures 0.3.28",
"gpui2",
"itertools 0.10.5",
"language2",
"lazy_static",
"libc",
"mio-extras",
"ordered-float 2.10.0",
"procinfo",
"project2",
"rand 0.8.5",
"serde",
"serde_derive",
"settings2",
"shellexpand",
"smallvec",
"smol",
"terminal2",
"theme2",
"thiserror",
"util",
"workspace2",
]
[[package]]
name = "text"
version = "0.1.0"
@@ -10058,6 +10164,7 @@ dependencies = [
"chrono",
"gpui2",
"itertools 0.11.0",
"menu2",
"rand 0.8.5",
"serde",
"settings2",
@@ -11288,7 +11395,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.113.0"
version = "0.114.0"
dependencies = [
"activity_indicator",
"ai",
@@ -11441,6 +11548,7 @@ dependencies = [
"chrono",
"cli",
"client2",
"collab_ui2",
"collections",
"command_palette2",
"copilot2",
@@ -11492,6 +11600,7 @@ dependencies = [
"smol",
"sum_tree",
"tempdir",
"terminal_view2",
"text2",
"theme2",
"thiserror",

View File

@@ -18,6 +18,7 @@ members = [
"crates/collab",
"crates/collab2",
"crates/collab_ui",
"crates/collab_ui2",
"crates/collections",
"crates/command_palette",
"crates/command_palette2",
@@ -94,9 +95,11 @@ members = [
"crates/sqlez_macros",
"crates/rich_text",
"crates/storybook2",
"crates/storybook3",
"crates/sum_tree",
"crates/terminal",
"crates/terminal2",
"crates/terminal_view2",
"crates/text",
"crates/theme",
"crates/theme2",
@@ -203,6 +206,9 @@ core-graphics = { git = "https://github.com/servo/core-foundation-rs", rev = "07
[profile.dev]
split-debuginfo = "unpacked"
[profile.dev.package.taffy]
opt-level = 3
[profile.release]
debug = true
lto = "thin"

View File

@@ -35,6 +35,15 @@
// "custom": 2
// },
"buffer_line_height": "comfortable",
// The name of a font to use for rendering text in the UI
"ui_font_family": "Zed Mono",
// The OpenType features to enable for text in the UI
"ui_font_features": {
// Disable ligatures:
"calt": false
},
// The default font size for text in the UI
"ui_font_size": 14,
// The factor to grow the active pane by. Defaults to 1.0
// which gives the same size as all other panes.
"active_pane_magnification": 1.0,

View File

@@ -118,14 +118,18 @@ fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
let auto_updater = auto_updater.read(cx);
let server_url = &auto_updater.server_url;
let current_version = auto_updater.current_version;
let latest_release_url = if cx.has_global::<ReleaseChannel>()
&& *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
{
format!("{server_url}/releases/preview/{current_version}")
} else {
format!("{server_url}/releases/stable/{current_version}")
};
cx.platform().open_url(&latest_release_url);
if cx.has_global::<ReleaseChannel>() {
match cx.global::<ReleaseChannel>() {
ReleaseChannel::Dev => {}
ReleaseChannel::Nightly => {}
ReleaseChannel::Preview => cx
.platform()
.open_url(&format!("{server_url}/releases/preview/{current_version}")),
ReleaseChannel::Stable => cx
.platform()
.open_url(&format!("{server_url}/releases/stable/{current_version}")),
}
}
}
}
@@ -224,22 +228,19 @@ impl AutoUpdater {
)
});
let preview_param = cx.read(|cx| {
let mut url_string = format!(
"{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg"
);
cx.read(|cx| {
if cx.has_global::<ReleaseChannel>() {
if *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview {
return "&preview=1";
if let Some(param) = cx.global::<ReleaseChannel>().release_query_param() {
url_string += "&";
url_string += param;
}
}
""
});
let mut response = client
.get(
&format!("{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg{preview_param}"),
Default::default(),
true,
)
.await?;
let mut response = client.get(&url_string, Default::default(), true).await?;
let mut body = Vec::new();
response

View File

@@ -987,9 +987,17 @@ impl Client {
self.establish_websocket_connection(credentials, cx)
}
async fn get_rpc_url(http: Arc<dyn HttpClient>, is_preview: bool) -> Result<Url> {
let preview_param = if is_preview { "?preview=1" } else { "" };
let url = format!("{}/rpc{preview_param}", *ZED_SERVER_URL);
async fn get_rpc_url(
http: Arc<dyn HttpClient>,
release_channel: Option<ReleaseChannel>,
) -> Result<Url> {
let mut url = format!("{}/rpc", *ZED_SERVER_URL);
if let Some(preview_param) =
release_channel.and_then(|channel| channel.release_query_param())
{
url += "?";
url += preview_param;
}
let response = http.get(&url, Default::default(), false).await?;
// Normally, ZED_SERVER_URL is set to the URL of zed.dev website.
@@ -1024,11 +1032,11 @@ impl Client {
credentials: &Credentials,
cx: &AsyncAppContext,
) -> Task<Result<Connection, EstablishConnectionError>> {
let use_preview_server = cx.read(|cx| {
let release_channel = cx.read(|cx| {
if cx.has_global::<ReleaseChannel>() {
*cx.global::<ReleaseChannel>() != ReleaseChannel::Stable
Some(*cx.global::<ReleaseChannel>())
} else {
false
None
}
});
@@ -1041,7 +1049,7 @@ impl Client {
let http = self.http.clone();
cx.background().spawn(async move {
let mut rpc_url = Self::get_rpc_url(http, use_preview_server).await?;
let mut rpc_url = Self::get_rpc_url(http, release_channel).await?;
let rpc_host = rpc_url
.host_str()
.zip(rpc_url.port_or_known_default())
@@ -1191,7 +1199,7 @@ impl Client {
// Use the collab server's admin API to retrieve the id
// of the impersonated user.
let mut url = Self::get_rpc_url(http.clone(), false).await?;
let mut url = Self::get_rpc_url(http.clone(), None).await?;
url.set_path("/user");
url.set_query(Some(&format!("github_login={login}")));
let request = Request::get(url.as_str())

View File

@@ -20,7 +20,7 @@ pub struct Telemetry {
#[derive(Default)]
struct TelemetryState {
metrics_id: Option<Arc<str>>, // Per logged-in user
installation_id: Option<Arc<str>>, // Per app installation (different for dev, preview, and stable)
installation_id: Option<Arc<str>>, // Per app installation (different for dev, nightly, preview, and stable)
session_id: Option<Arc<str>>, // Per app launch
app_version: Option<Arc<str>>,
release_channel: Option<&'static str>,

View File

@@ -923,9 +923,17 @@ impl Client {
self.establish_websocket_connection(credentials, cx)
}
async fn get_rpc_url(http: Arc<dyn HttpClient>, is_preview: bool) -> Result<Url> {
let preview_param = if is_preview { "?preview=1" } else { "" };
let url = format!("{}/rpc{preview_param}", *ZED_SERVER_URL);
async fn get_rpc_url(
http: Arc<dyn HttpClient>,
release_channel: Option<ReleaseChannel>,
) -> Result<Url> {
let mut url = format!("{}/rpc", *ZED_SERVER_URL);
if let Some(preview_param) =
release_channel.and_then(|channel| channel.release_query_param())
{
url += "?";
url += preview_param;
}
let response = http.get(&url, Default::default(), false).await?;
// Normally, ZED_SERVER_URL is set to the URL of zed.dev website.
@@ -960,9 +968,7 @@ impl Client {
credentials: &Credentials,
cx: &AsyncAppContext,
) -> Task<Result<Connection, EstablishConnectionError>> {
let use_preview_server = cx
.try_read_global(|channel: &ReleaseChannel, _| *channel != ReleaseChannel::Stable)
.unwrap_or(false);
let release_channel = cx.try_read_global(|channel: &ReleaseChannel, _| *channel);
let request = Request::builder()
.header(
@@ -973,7 +979,7 @@ impl Client {
let http = self.http.clone();
cx.background_executor().spawn(async move {
let mut rpc_url = Self::get_rpc_url(http, use_preview_server).await?;
let mut rpc_url = Self::get_rpc_url(http, release_channel).await?;
let rpc_host = rpc_url
.host_str()
.zip(rpc_url.port_or_known_default())
@@ -1120,7 +1126,7 @@ impl Client {
// Use the collab server's admin API to retrieve the id
// of the impersonated user.
let mut url = Self::get_rpc_url(http.clone(), false).await?;
let mut url = Self::get_rpc_url(http.clone(), None).await?;
url.set_path("/user");
url.set_query(Some(&format!("github_login={login}")));
let request = Request::get(url.as_str())

View File

@@ -20,7 +20,7 @@ pub struct Telemetry {
struct TelemetryState {
metrics_id: Option<Arc<str>>, // Per logged-in user
installation_id: Option<Arc<str>>, // Per app installation (different for dev, preview, and stable)
installation_id: Option<Arc<str>>, // Per app installation (different for dev, nightly, preview, and stable)
session_id: Option<Arc<str>>, // Per app launch
release_channel: Option<&'static str>,
app_metadata: AppMetadata,

View File

@@ -220,12 +220,11 @@ impl TestServer {
languages: Arc::new(language_registry),
fs: fs.clone(),
build_window_options: |_, _, _| Default::default(),
initialize_workspace: |_, _, _, _| gpui::Task::ready(Ok(())),
node_runtime: FakeNodeRuntime::new(),
});
cx.update(|cx| {
theme::init(cx);
theme::init(theme::LoadThemes::JustBase, cx);
Project::init(&client, cx);
client::init(&client, cx);
language::init(cx);

View File

@@ -0,0 +1,81 @@
[package]
name = "collab_ui2"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/collab_ui.rs"
doctest = false
[features]
test-support = [
"call/test-support",
"client/test-support",
"collections/test-support",
"editor/test-support",
"gpui/test-support",
"project/test-support",
"settings/test-support",
"util/test-support",
"workspace/test-support",
]
[dependencies]
# auto_update = { path = "../auto_update" }
db = { package = "db2", path = "../db2" }
call = { package = "call2", path = "../call2" }
client = { package = "client2", path = "../client2" }
channel = { package = "channel2", path = "../channel2" }
clock = { path = "../clock" }
collections = { path = "../collections" }
# context_menu = { path = "../context_menu" }
# drag_and_drop = { path = "../drag_and_drop" }
editor = { package="editor2", path = "../editor2" }
#feedback = { path = "../feedback" }
fuzzy = { path = "../fuzzy" }
gpui = { package = "gpui2", path = "../gpui2" }
language = { package = "language2", path = "../language2" }
menu = { package = "menu2", path = "../menu2" }
notifications = { package = "notifications2", path = "../notifications2" }
rich_text = { package = "rich_text2", path = "../rich_text2" }
picker = { package = "picker2", path = "../picker2" }
project = { package = "project2", path = "../project2" }
# recent_projects = { path = "../recent_projects" }
rpc = { package ="rpc2", path = "../rpc2" }
settings = { package = "settings2", path = "../settings2" }
feature_flags = { package = "feature_flags2", path = "../feature_flags2"}
theme = { package = "theme2", path = "../theme2" }
# theme_selector = { path = "../theme_selector" }
# vcs_menu = { path = "../vcs_menu" }
ui = { package = "ui2", path = "../ui2" }
util = { path = "../util" }
workspace = { package = "workspace2", path = "../workspace2" }
zed-actions = { package="zed_actions2", path = "../zed_actions2"}
anyhow.workspace = true
futures.workspace = true
lazy_static.workspace = true
log.workspace = true
schemars.workspace = true
postage.workspace = true
serde.workspace = true
serde_derive.workspace = true
time.workspace = true
smallvec.workspace = true
[dev-dependencies]
call = { package = "call2", path = "../call2", features = ["test-support"] }
client = { package = "client2", path = "../client2", features = ["test-support"] }
collections = { path = "../collections", features = ["test-support"] }
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
notifications = { package = "notifications2", path = "../notifications2", features = ["test-support"] }
project = { package = "project2", path = "../project2", features = ["test-support"] }
rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] }
settings = { package = "settings2", path = "../settings2", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
pretty_assertions.workspace = true
tree-sitter-markdown.workspace = true

View File

@@ -0,0 +1,454 @@
// use anyhow::{anyhow, Result};
// use call::report_call_event_for_channel;
// use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore};
// use client::{
// proto::{self, PeerId},
// Collaborator, ParticipantIndex,
// };
// use collections::HashMap;
// use editor::{CollaborationHub, Editor};
// use gpui::{
// actions,
// elements::{ChildView, Label},
// geometry::vector::Vector2F,
// AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View,
// ViewContext, ViewHandle,
// };
// use project::Project;
// use smallvec::SmallVec;
// use std::{
// any::{Any, TypeId},
// sync::Arc,
// };
// use util::ResultExt;
// use workspace::{
// item::{FollowableItem, Item, ItemEvent, ItemHandle},
// register_followable_item,
// searchable::SearchableItemHandle,
// ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
// };
// actions!(channel_view, [Deploy]);
// pub fn init(cx: &mut AppContext) {
// register_followable_item::<ChannelView>(cx)
// }
// pub struct ChannelView {
// pub editor: ViewHandle<Editor>,
// project: ModelHandle<Project>,
// channel_store: ModelHandle<ChannelStore>,
// channel_buffer: ModelHandle<ChannelBuffer>,
// remote_id: Option<ViewId>,
// _editor_event_subscription: Subscription,
// }
// impl ChannelView {
// pub fn open(
// channel_id: ChannelId,
// workspace: ViewHandle<Workspace>,
// cx: &mut AppContext,
// ) -> Task<Result<ViewHandle<Self>>> {
// let pane = workspace.read(cx).active_pane().clone();
// let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
// cx.spawn(|mut cx| async move {
// let channel_view = channel_view.await?;
// pane.update(&mut cx, |pane, cx| {
// report_call_event_for_channel(
// "open channel notes",
// channel_id,
// &workspace.read(cx).app_state().client,
// cx,
// );
// pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
// });
// anyhow::Ok(channel_view)
// })
// }
// pub fn open_in_pane(
// channel_id: ChannelId,
// pane: ViewHandle<Pane>,
// workspace: ViewHandle<Workspace>,
// cx: &mut AppContext,
// ) -> Task<Result<ViewHandle<Self>>> {
// let workspace = workspace.read(cx);
// let project = workspace.project().to_owned();
// let channel_store = ChannelStore::global(cx);
// let language_registry = workspace.app_state().languages.clone();
// let markdown = language_registry.language_for_name("Markdown");
// let channel_buffer =
// channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
// cx.spawn(|mut cx| async move {
// let channel_buffer = channel_buffer.await?;
// let markdown = markdown.await.log_err();
// channel_buffer.update(&mut cx, |buffer, cx| {
// buffer.buffer().update(cx, |buffer, cx| {
// buffer.set_language_registry(language_registry);
// if let Some(markdown) = markdown {
// buffer.set_language(Some(markdown), cx);
// }
// })
// });
// pane.update(&mut cx, |pane, cx| {
// let buffer_id = channel_buffer.read(cx).remote_id(cx);
// let existing_view = pane
// .items_of_type::<Self>()
// .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
// // If this channel buffer is already open in this pane, just return it.
// if let Some(existing_view) = existing_view.clone() {
// if existing_view.read(cx).channel_buffer == channel_buffer {
// return existing_view;
// }
// }
// let view = cx.add_view(|cx| {
// let mut this = Self::new(project, channel_store, channel_buffer, cx);
// this.acknowledge_buffer_version(cx);
// this
// });
// // If the pane contained a disconnected view for this channel buffer,
// // replace that.
// if let Some(existing_item) = existing_view {
// if let Some(ix) = pane.index_for_item(&existing_item) {
// pane.close_item_by_id(existing_item.id(), SaveIntent::Skip, cx)
// .detach();
// pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
// }
// }
// view
// })
// .ok_or_else(|| anyhow!("pane was dropped"))
// })
// }
// pub fn new(
// project: ModelHandle<Project>,
// channel_store: ModelHandle<ChannelStore>,
// channel_buffer: ModelHandle<ChannelBuffer>,
// cx: &mut ViewContext<Self>,
// ) -> Self {
// let buffer = channel_buffer.read(cx).buffer();
// let editor = cx.add_view(|cx| {
// let mut editor = Editor::for_buffer(buffer, None, cx);
// editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
// channel_buffer.clone(),
// )));
// editor.set_read_only(
// !channel_buffer
// .read(cx)
// .channel(cx)
// .is_some_and(|c| c.can_edit_notes()),
// );
// editor
// });
// let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
// cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
// .detach();
// Self {
// editor,
// project,
// channel_store,
// channel_buffer,
// remote_id: None,
// _editor_event_subscription,
// }
// }
// pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
// self.channel_buffer.read(cx).channel(cx)
// }
// fn handle_channel_buffer_event(
// &mut self,
// _: ModelHandle<ChannelBuffer>,
// event: &ChannelBufferEvent,
// cx: &mut ViewContext<Self>,
// ) {
// match event {
// ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
// editor.set_read_only(true);
// cx.notify();
// }),
// ChannelBufferEvent::ChannelChanged => {
// self.editor.update(cx, |editor, cx| {
// editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes()));
// cx.emit(editor::Event::TitleChanged);
// cx.notify()
// });
// }
// ChannelBufferEvent::BufferEdited => {
// if cx.is_self_focused() || self.editor.is_focused(cx) {
// self.acknowledge_buffer_version(cx);
// } else {
// self.channel_store.update(cx, |store, cx| {
// let channel_buffer = self.channel_buffer.read(cx);
// store.notes_changed(
// channel_buffer.channel_id,
// channel_buffer.epoch(),
// &channel_buffer.buffer().read(cx).version(),
// cx,
// )
// });
// }
// }
// ChannelBufferEvent::CollaboratorsChanged => {}
// }
// }
// fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<'_, '_, ChannelView>) {
// self.channel_store.update(cx, |store, cx| {
// let channel_buffer = self.channel_buffer.read(cx);
// store.acknowledge_notes_version(
// channel_buffer.channel_id,
// channel_buffer.epoch(),
// &channel_buffer.buffer().read(cx).version(),
// cx,
// )
// });
// self.channel_buffer.update(cx, |buffer, cx| {
// buffer.acknowledge_buffer_version(cx);
// });
// }
// }
// impl Entity for ChannelView {
// type Event = editor::Event;
// }
// impl View for ChannelView {
// fn ui_name() -> &'static str {
// "ChannelView"
// }
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
// ChildView::new(self.editor.as_any(), cx).into_any()
// }
// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
// if cx.is_self_focused() {
// self.acknowledge_buffer_version(cx);
// cx.focus(self.editor.as_any())
// }
// }
// }
// impl Item for ChannelView {
// fn act_as_type<'a>(
// &'a self,
// type_id: TypeId,
// self_handle: &'a ViewHandle<Self>,
// _: &'a AppContext,
// ) -> Option<&'a AnyViewHandle> {
// if type_id == TypeId::of::<Self>() {
// Some(self_handle)
// } else if type_id == TypeId::of::<Editor>() {
// Some(&self.editor)
// } else {
// None
// }
// }
// fn tab_content<V: 'static>(
// &self,
// _: Option<usize>,
// style: &theme::Tab,
// cx: &gpui::AppContext,
// ) -> AnyElement<V> {
// let label = if let Some(channel) = self.channel(cx) {
// match (
// channel.can_edit_notes(),
// self.channel_buffer.read(cx).is_connected(),
// ) {
// (true, true) => format!("#{}", channel.name),
// (false, true) => format!("#{} (read-only)", channel.name),
// (_, false) => format!("#{} (disconnected)", channel.name),
// }
// } else {
// format!("channel notes (disconnected)")
// };
// Label::new(label, style.label.to_owned()).into_any()
// }
// fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self> {
// Some(Self::new(
// self.project.clone(),
// self.channel_store.clone(),
// self.channel_buffer.clone(),
// cx,
// ))
// }
// fn is_singleton(&self, _cx: &AppContext) -> bool {
// false
// }
// fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
// self.editor
// .update(cx, |editor, cx| editor.navigate(data, cx))
// }
// fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
// self.editor
// .update(cx, |editor, cx| Item::deactivated(editor, cx))
// }
// fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
// self.editor
// .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
// }
// fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
// Some(Box::new(self.editor.clone()))
// }
// fn show_toolbar(&self) -> bool {
// true
// }
// fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
// self.editor.read(cx).pixel_position_of_cursor(cx)
// }
// fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
// editor::Editor::to_item_events(event)
// }
// }
// impl FollowableItem for ChannelView {
// fn remote_id(&self) -> Option<workspace::ViewId> {
// self.remote_id
// }
// fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
// let channel_buffer = self.channel_buffer.read(cx);
// if !channel_buffer.is_connected() {
// return None;
// }
// Some(proto::view::Variant::ChannelView(
// proto::view::ChannelView {
// channel_id: channel_buffer.channel_id,
// editor: if let Some(proto::view::Variant::Editor(proto)) =
// self.editor.read(cx).to_state_proto(cx)
// {
// Some(proto)
// } else {
// None
// },
// },
// ))
// }
// fn from_state_proto(
// pane: ViewHandle<workspace::Pane>,
// workspace: ViewHandle<workspace::Workspace>,
// remote_id: workspace::ViewId,
// state: &mut Option<proto::view::Variant>,
// cx: &mut AppContext,
// ) -> Option<gpui::Task<anyhow::Result<ViewHandle<Self>>>> {
// let Some(proto::view::Variant::ChannelView(_)) = state else {
// return None;
// };
// let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
// unreachable!()
// };
// let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
// Some(cx.spawn(|mut cx| async move {
// let this = open.await?;
// let task = this
// .update(&mut cx, |this, cx| {
// this.remote_id = Some(remote_id);
// if let Some(state) = state.editor {
// Some(this.editor.update(cx, |editor, cx| {
// editor.apply_update_proto(
// &this.project,
// proto::update_view::Variant::Editor(proto::update_view::Editor {
// selections: state.selections,
// pending_selection: state.pending_selection,
// scroll_top_anchor: state.scroll_top_anchor,
// scroll_x: state.scroll_x,
// scroll_y: state.scroll_y,
// ..Default::default()
// }),
// cx,
// )
// }))
// } else {
// None
// }
// })
// .ok_or_else(|| anyhow!("window was closed"))?;
// if let Some(task) = task {
// task.await?;
// }
// Ok(this)
// }))
// }
// fn add_event_to_update_proto(
// &self,
// event: &Self::Event,
// update: &mut Option<proto::update_view::Variant>,
// cx: &AppContext,
// ) -> bool {
// self.editor
// .read(cx)
// .add_event_to_update_proto(event, update, cx)
// }
// fn apply_update_proto(
// &mut self,
// project: &ModelHandle<Project>,
// message: proto::update_view::Variant,
// cx: &mut ViewContext<Self>,
// ) -> gpui::Task<anyhow::Result<()>> {
// self.editor.update(cx, |editor, cx| {
// editor.apply_update_proto(project, message, cx)
// })
// }
// fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
// self.editor.update(cx, |editor, cx| {
// editor.set_leader_peer_id(leader_peer_id, cx)
// })
// }
// fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
// Editor::should_unfollow_on_event(event, cx)
// }
// fn is_project_item(&self, _cx: &AppContext) -> bool {
// false
// }
// }
// struct ChannelBufferCollaborationHub(ModelHandle<ChannelBuffer>);
// impl CollaborationHub for ChannelBufferCollaborationHub {
// fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
// self.0.read(cx).collaborators()
// }
// fn user_participant_indices<'a>(
// &self,
// cx: &'a AppContext,
// ) -> &'a HashMap<u64, ParticipantIndex> {
// self.0.read(cx).user_store().read(cx).participant_indices()
// }
// }

View File

@@ -0,0 +1,983 @@
// use crate::{
// channel_view::ChannelView, is_channels_feature_enabled, render_avatar, ChatPanelSettings,
// };
// use anyhow::Result;
// use call::ActiveCall;
// use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
// use client::Client;
// use collections::HashMap;
// use db::kvp::KEY_VALUE_STORE;
// use editor::Editor;
// use gpui::{
// actions,
// elements::*,
// platform::{CursorStyle, MouseButton},
// serde_json,
// views::{ItemType, Select, SelectStyle},
// AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View,
// ViewContext, ViewHandle, WeakViewHandle,
// };
// use language::LanguageRegistry;
// use menu::Confirm;
// use message_editor::MessageEditor;
// use project::Fs;
// use rich_text::RichText;
// use serde::{Deserialize, Serialize};
// use settings::SettingsStore;
// use std::sync::Arc;
// use theme::{IconButton, Theme};
// use time::{OffsetDateTime, UtcOffset};
// use util::{ResultExt, TryFutureExt};
// use workspace::{
// dock::{DockPosition, Panel},
// Workspace,
// };
// mod message_editor;
// const MESSAGE_LOADING_THRESHOLD: usize = 50;
// const CHAT_PANEL_KEY: &'static str = "ChatPanel";
// pub struct ChatPanel {
// client: Arc<Client>,
// channel_store: ModelHandle<ChannelStore>,
// languages: Arc<LanguageRegistry>,
// active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
// message_list: ListState<ChatPanel>,
// input_editor: ViewHandle<MessageEditor>,
// channel_select: ViewHandle<Select>,
// local_timezone: UtcOffset,
// fs: Arc<dyn Fs>,
// width: Option<f32>,
// active: bool,
// pending_serialization: Task<Option<()>>,
// subscriptions: Vec<gpui::Subscription>,
// workspace: WeakViewHandle<Workspace>,
// is_scrolled_to_bottom: bool,
// has_focus: bool,
// markdown_data: HashMap<ChannelMessageId, RichText>,
// }
// #[derive(Serialize, Deserialize)]
// struct SerializedChatPanel {
// width: Option<f32>,
// }
// #[derive(Debug)]
// pub enum Event {
// DockPositionChanged,
// Focus,
// Dismissed,
// }
// actions!(
// chat_panel,
// [LoadMoreMessages, ToggleFocus, OpenChannelNotes, JoinCall]
// );
// pub fn init(cx: &mut AppContext) {
// cx.add_action(ChatPanel::send);
// cx.add_action(ChatPanel::load_more_messages);
// cx.add_action(ChatPanel::open_notes);
// cx.add_action(ChatPanel::join_call);
// }
// impl ChatPanel {
// pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
// let fs = workspace.app_state().fs.clone();
// let client = workspace.app_state().client.clone();
// let channel_store = ChannelStore::global(cx);
// let languages = workspace.app_state().languages.clone();
// let input_editor = cx.add_view(|cx| {
// MessageEditor::new(
// languages.clone(),
// channel_store.clone(),
// cx.add_view(|cx| {
// Editor::auto_height(
// 4,
// Some(Arc::new(|theme| theme.chat_panel.input_editor.clone())),
// cx,
// )
// }),
// cx,
// )
// });
// let workspace_handle = workspace.weak_handle();
// let channel_select = cx.add_view(|cx| {
// let channel_store = channel_store.clone();
// let workspace = workspace_handle.clone();
// Select::new(0, cx, {
// move |ix, item_type, is_hovered, cx| {
// Self::render_channel_name(
// &channel_store,
// ix,
// item_type,
// is_hovered,
// workspace,
// cx,
// )
// }
// })
// .with_style(move |cx| {
// let style = &theme::current(cx).chat_panel.channel_select;
// SelectStyle {
// header: Default::default(),
// menu: style.menu,
// }
// })
// });
// let mut message_list =
// ListState::<Self>::new(0, Orientation::Bottom, 10., move |this, ix, cx| {
// this.render_message(ix, cx)
// });
// message_list.set_scroll_handler(|visible_range, count, this, cx| {
// if visible_range.start < MESSAGE_LOADING_THRESHOLD {
// this.load_more_messages(&LoadMoreMessages, cx);
// }
// this.is_scrolled_to_bottom = visible_range.end == count;
// });
// cx.add_view(|cx| {
// let mut this = Self {
// fs,
// client,
// channel_store,
// languages,
// active_chat: Default::default(),
// pending_serialization: Task::ready(None),
// message_list,
// input_editor,
// channel_select,
// local_timezone: cx.platform().local_timezone(),
// has_focus: false,
// subscriptions: Vec::new(),
// workspace: workspace_handle,
// is_scrolled_to_bottom: true,
// active: false,
// width: None,
// markdown_data: Default::default(),
// };
// let mut old_dock_position = this.position(cx);
// this.subscriptions
// .push(
// cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
// let new_dock_position = this.position(cx);
// if new_dock_position != old_dock_position {
// old_dock_position = new_dock_position;
// cx.emit(Event::DockPositionChanged);
// }
// cx.notify();
// }),
// );
// this.update_channel_count(cx);
// cx.observe(&this.channel_store, |this, _, cx| {
// this.update_channel_count(cx)
// })
// .detach();
// cx.observe(&this.channel_select, |this, channel_select, cx| {
// let selected_ix = channel_select.read(cx).selected_index();
// let selected_channel_id = this
// .channel_store
// .read(cx)
// .channel_at(selected_ix)
// .map(|e| e.id);
// if let Some(selected_channel_id) = selected_channel_id {
// this.select_channel(selected_channel_id, None, cx)
// .detach_and_log_err(cx);
// }
// })
// .detach();
// this
// })
// }
// pub fn is_scrolled_to_bottom(&self) -> bool {
// self.is_scrolled_to_bottom
// }
// pub fn active_chat(&self) -> Option<ModelHandle<ChannelChat>> {
// self.active_chat.as_ref().map(|(chat, _)| chat.clone())
// }
// pub fn load(
// workspace: WeakViewHandle<Workspace>,
// cx: AsyncAppContext,
// ) -> Task<Result<ViewHandle<Self>>> {
// cx.spawn(|mut cx| async move {
// let serialized_panel = if let Some(panel) = cx
// .background()
// .spawn(async move { KEY_VALUE_STORE.read_kvp(CHAT_PANEL_KEY) })
// .await
// .log_err()
// .flatten()
// {
// Some(serde_json::from_str::<SerializedChatPanel>(&panel)?)
// } else {
// None
// };
// workspace.update(&mut cx, |workspace, cx| {
// let panel = Self::new(workspace, cx);
// if let Some(serialized_panel) = serialized_panel {
// panel.update(cx, |panel, cx| {
// panel.width = serialized_panel.width;
// cx.notify();
// });
// }
// panel
// })
// })
// }
// fn serialize(&mut self, cx: &mut ViewContext<Self>) {
// let width = self.width;
// self.pending_serialization = cx.background().spawn(
// async move {
// KEY_VALUE_STORE
// .write_kvp(
// CHAT_PANEL_KEY.into(),
// serde_json::to_string(&SerializedChatPanel { width })?,
// )
// .await?;
// anyhow::Ok(())
// }
// .log_err(),
// );
// }
// fn update_channel_count(&mut self, cx: &mut ViewContext<Self>) {
// let channel_count = self.channel_store.read(cx).channel_count();
// self.channel_select.update(cx, |select, cx| {
// select.set_item_count(channel_count, cx);
// });
// }
// fn set_active_chat(&mut self, chat: ModelHandle<ChannelChat>, cx: &mut ViewContext<Self>) {
// if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
// let channel_id = chat.read(cx).channel_id;
// {
// self.markdown_data.clear();
// let chat = chat.read(cx);
// self.message_list.reset(chat.message_count());
// let channel_name = chat.channel(cx).map(|channel| channel.name.clone());
// self.input_editor.update(cx, |editor, cx| {
// editor.set_channel(channel_id, channel_name, cx);
// });
// };
// let subscription = cx.subscribe(&chat, Self::channel_did_change);
// self.active_chat = Some((chat, subscription));
// self.acknowledge_last_message(cx);
// self.channel_select.update(cx, |select, cx| {
// if let Some(ix) = self.channel_store.read(cx).index_of_channel(channel_id) {
// select.set_selected_index(ix, cx);
// }
// });
// cx.notify();
// }
// }
// fn channel_did_change(
// &mut self,
// _: ModelHandle<ChannelChat>,
// event: &ChannelChatEvent,
// cx: &mut ViewContext<Self>,
// ) {
// match event {
// ChannelChatEvent::MessagesUpdated {
// old_range,
// new_count,
// } => {
// self.message_list.splice(old_range.clone(), *new_count);
// if self.active {
// self.acknowledge_last_message(cx);
// }
// }
// ChannelChatEvent::NewMessage {
// channel_id,
// message_id,
// } => {
// if !self.active {
// self.channel_store.update(cx, |store, cx| {
// store.new_message(*channel_id, *message_id, cx)
// })
// }
// }
// }
// cx.notify();
// }
// fn acknowledge_last_message(&mut self, cx: &mut ViewContext<'_, '_, ChatPanel>) {
// if self.active && self.is_scrolled_to_bottom {
// if let Some((chat, _)) = &self.active_chat {
// chat.update(cx, |chat, cx| {
// chat.acknowledge_last_message(cx);
// });
// }
// }
// }
// fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
// let theme = theme::current(cx);
// Flex::column()
// .with_child(
// ChildView::new(&self.channel_select, cx)
// .contained()
// .with_style(theme.chat_panel.channel_select.container),
// )
// .with_child(self.render_active_channel_messages(&theme))
// .with_child(self.render_input_box(&theme, cx))
// .into_any()
// }
// fn render_active_channel_messages(&self, theme: &Arc<Theme>) -> AnyElement<Self> {
// let messages = if self.active_chat.is_some() {
// List::new(self.message_list.clone())
// .contained()
// .with_style(theme.chat_panel.list)
// .into_any()
// } else {
// Empty::new().into_any()
// };
// messages.flex(1., true).into_any()
// }
// fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
// let (message, is_continuation, is_last, is_admin) = self
// .active_chat
// .as_ref()
// .unwrap()
// .0
// .update(cx, |active_chat, cx| {
// let is_admin = self
// .channel_store
// .read(cx)
// .is_channel_admin(active_chat.channel_id);
// let last_message = active_chat.message(ix.saturating_sub(1));
// let this_message = active_chat.message(ix).clone();
// let is_continuation = last_message.id != this_message.id
// && this_message.sender.id == last_message.sender.id;
// if let ChannelMessageId::Saved(id) = this_message.id {
// if this_message
// .mentions
// .iter()
// .any(|(_, user_id)| Some(*user_id) == self.client.user_id())
// {
// active_chat.acknowledge_message(id);
// }
// }
// (
// this_message,
// is_continuation,
// active_chat.message_count() == ix + 1,
// is_admin,
// )
// });
// let is_pending = message.is_pending();
// let theme = theme::current(cx);
// let text = self.markdown_data.entry(message.id).or_insert_with(|| {
// Self::render_markdown_with_mentions(&self.languages, self.client.id(), &message)
// });
// let now = OffsetDateTime::now_utc();
// let style = if is_pending {
// &theme.chat_panel.pending_message
// } else if is_continuation {
// &theme.chat_panel.continuation_message
// } else {
// &theme.chat_panel.message
// };
// let belongs_to_user = Some(message.sender.id) == self.client.user_id();
// let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
// (message.id, belongs_to_user || is_admin)
// {
// Some(id)
// } else {
// None
// };
// enum MessageBackgroundHighlight {}
// MouseEventHandler::new::<MessageBackgroundHighlight, _>(ix, cx, |state, cx| {
// let container = style.style_for(state);
// if is_continuation {
// Flex::row()
// .with_child(
// text.element(
// theme.editor.syntax.clone(),
// theme.chat_panel.rich_text.clone(),
// cx,
// )
// .flex(1., true),
// )
// .with_child(render_remove(message_id_to_remove, cx, &theme))
// .contained()
// .with_style(*container)
// .with_margin_bottom(if is_last {
// theme.chat_panel.last_message_bottom_spacing
// } else {
// 0.
// })
// .into_any()
// } else {
// Flex::column()
// .with_child(
// Flex::row()
// .with_child(
// Flex::row()
// .with_child(render_avatar(
// message.sender.avatar.clone(),
// &theme.chat_panel.avatar,
// theme.chat_panel.avatar_container,
// ))
// .with_child(
// Label::new(
// message.sender.github_login.clone(),
// theme.chat_panel.message_sender.text.clone(),
// )
// .contained()
// .with_style(theme.chat_panel.message_sender.container),
// )
// .with_child(
// Label::new(
// format_timestamp(
// message.timestamp,
// now,
// self.local_timezone,
// ),
// theme.chat_panel.message_timestamp.text.clone(),
// )
// .contained()
// .with_style(theme.chat_panel.message_timestamp.container),
// )
// .align_children_center()
// .flex(1., true),
// )
// .with_child(render_remove(message_id_to_remove, cx, &theme))
// .align_children_center(),
// )
// .with_child(
// Flex::row()
// .with_child(
// text.element(
// theme.editor.syntax.clone(),
// theme.chat_panel.rich_text.clone(),
// cx,
// )
// .flex(1., true),
// )
// // Add a spacer to make everything line up
// .with_child(render_remove(None, cx, &theme)),
// )
// .contained()
// .with_style(*container)
// .with_margin_bottom(if is_last {
// theme.chat_panel.last_message_bottom_spacing
// } else {
// 0.
// })
// .into_any()
// }
// })
// .into_any()
// }
// fn render_markdown_with_mentions(
// language_registry: &Arc<LanguageRegistry>,
// current_user_id: u64,
// message: &channel::ChannelMessage,
// ) -> RichText {
// let mentions = message
// .mentions
// .iter()
// .map(|(range, user_id)| rich_text::Mention {
// range: range.clone(),
// is_self_mention: *user_id == current_user_id,
// })
// .collect::<Vec<_>>();
// rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None)
// }
// fn render_input_box(&self, theme: &Arc<Theme>, cx: &AppContext) -> AnyElement<Self> {
// ChildView::new(&self.input_editor, cx)
// .contained()
// .with_style(theme.chat_panel.input_editor.container)
// .into_any()
// }
// fn render_channel_name(
// channel_store: &ModelHandle<ChannelStore>,
// ix: usize,
// item_type: ItemType,
// is_hovered: bool,
// workspace: WeakViewHandle<Workspace>,
// cx: &mut ViewContext<Select>,
// ) -> AnyElement<Select> {
// let theme = theme::current(cx);
// let tooltip_style = &theme.tooltip;
// let theme = &theme.chat_panel;
// let style = match (&item_type, is_hovered) {
// (ItemType::Header, _) => &theme.channel_select.header,
// (ItemType::Selected, _) => &theme.channel_select.active_item,
// (ItemType::Unselected, false) => &theme.channel_select.item,
// (ItemType::Unselected, true) => &theme.channel_select.hovered_item,
// };
// let channel = &channel_store.read(cx).channel_at(ix).unwrap();
// let channel_id = channel.id;
// let mut row = Flex::row()
// .with_child(
// Label::new("#".to_string(), style.hash.text.clone())
// .contained()
// .with_style(style.hash.container),
// )
// .with_child(Label::new(channel.name.clone(), style.name.clone()));
// if matches!(item_type, ItemType::Header) {
// row.add_children([
// MouseEventHandler::new::<OpenChannelNotes, _>(0, cx, |mouse_state, _| {
// render_icon_button(theme.icon_button.style_for(mouse_state), "icons/file.svg")
// })
// .on_click(MouseButton::Left, move |_, _, cx| {
// if let Some(workspace) = workspace.upgrade(cx) {
// ChannelView::open(channel_id, workspace, cx).detach();
// }
// })
// .with_tooltip::<OpenChannelNotes>(
// channel_id as usize,
// "Open Notes",
// Some(Box::new(OpenChannelNotes)),
// tooltip_style.clone(),
// cx,
// )
// .flex_float(),
// MouseEventHandler::new::<ActiveCall, _>(0, cx, |mouse_state, _| {
// render_icon_button(
// theme.icon_button.style_for(mouse_state),
// "icons/speaker-loud.svg",
// )
// })
// .on_click(MouseButton::Left, move |_, _, cx| {
// ActiveCall::global(cx)
// .update(cx, |call, cx| call.join_channel(channel_id, cx))
// .detach_and_log_err(cx);
// })
// .with_tooltip::<ActiveCall>(
// channel_id as usize,
// "Join Call",
// Some(Box::new(JoinCall)),
// tooltip_style.clone(),
// cx,
// )
// .flex_float(),
// ]);
// }
// row.align_children_center()
// .contained()
// .with_style(style.container)
// .into_any()
// }
// fn render_sign_in_prompt(
// &self,
// theme: &Arc<Theme>,
// cx: &mut ViewContext<Self>,
// ) -> AnyElement<Self> {
// enum SignInPromptLabel {}
// MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
// Label::new(
// "Sign in to use chat".to_string(),
// theme
// .chat_panel
// .sign_in_prompt
// .style_for(mouse_state)
// .clone(),
// )
// })
// .with_cursor_style(CursorStyle::PointingHand)
// .on_click(MouseButton::Left, move |_, this, cx| {
// let client = this.client.clone();
// cx.spawn(|this, mut cx| async move {
// if client
// .authenticate_and_connect(true, &cx)
// .log_err()
// .await
// .is_some()
// {
// this.update(&mut cx, |this, cx| {
// if cx.handle().is_focused(cx) {
// cx.focus(&this.input_editor);
// }
// })
// .ok();
// }
// })
// .detach();
// })
// .aligned()
// .into_any()
// }
// fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
// if let Some((chat, _)) = self.active_chat.as_ref() {
// let message = self
// .input_editor
// .update(cx, |editor, cx| editor.take_message(cx));
// if let Some(task) = chat
// .update(cx, |chat, cx| chat.send_message(message, cx))
// .log_err()
// {
// task.detach();
// }
// }
// }
// fn remove_message(&mut self, id: u64, cx: &mut ViewContext<Self>) {
// if let Some((chat, _)) = self.active_chat.as_ref() {
// chat.update(cx, |chat, cx| chat.remove_message(id, cx).detach())
// }
// }
// fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
// if let Some((chat, _)) = self.active_chat.as_ref() {
// chat.update(cx, |channel, cx| {
// if let Some(task) = channel.load_more_messages(cx) {
// task.detach();
// }
// })
// }
// }
// pub fn select_channel(
// &mut self,
// selected_channel_id: u64,
// scroll_to_message_id: Option<u64>,
// cx: &mut ViewContext<ChatPanel>,
// ) -> Task<Result<()>> {
// let open_chat = self
// .active_chat
// .as_ref()
// .and_then(|(chat, _)| {
// (chat.read(cx).channel_id == selected_channel_id)
// .then(|| Task::ready(anyhow::Ok(chat.clone())))
// })
// .unwrap_or_else(|| {
// self.channel_store.update(cx, |store, cx| {
// store.open_channel_chat(selected_channel_id, cx)
// })
// });
// cx.spawn(|this, mut cx| async move {
// let chat = open_chat.await?;
// this.update(&mut cx, |this, cx| {
// this.set_active_chat(chat.clone(), cx);
// })?;
// if let Some(message_id) = scroll_to_message_id {
// if let Some(item_ix) =
// ChannelChat::load_history_since_message(chat.clone(), message_id, cx.clone())
// .await
// {
// this.update(&mut cx, |this, cx| {
// if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) {
// this.message_list.scroll_to(ListOffset {
// item_ix,
// offset_in_item: 0.,
// });
// cx.notify();
// }
// })?;
// }
// }
// Ok(())
// })
// }
// fn open_notes(&mut self, _: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
// if let Some((chat, _)) = &self.active_chat {
// let channel_id = chat.read(cx).channel_id;
// if let Some(workspace) = self.workspace.upgrade(cx) {
// ChannelView::open(channel_id, workspace, cx).detach();
// }
// }
// }
// fn join_call(&mut self, _: &JoinCall, cx: &mut ViewContext<Self>) {
// if let Some((chat, _)) = &self.active_chat {
// let channel_id = chat.read(cx).channel_id;
// ActiveCall::global(cx)
// .update(cx, |call, cx| call.join_channel(channel_id, cx))
// .detach_and_log_err(cx);
// }
// }
// }
// fn render_remove(
// message_id_to_remove: Option<u64>,
// cx: &mut ViewContext<'_, '_, ChatPanel>,
// theme: &Arc<Theme>,
// ) -> AnyElement<ChatPanel> {
// enum DeleteMessage {}
// message_id_to_remove
// .map(|id| {
// MouseEventHandler::new::<DeleteMessage, _>(id as usize, cx, |mouse_state, _| {
// let button_style = theme.chat_panel.icon_button.style_for(mouse_state);
// render_icon_button(button_style, "icons/x.svg")
// .aligned()
// .into_any()
// })
// .with_padding(Padding::uniform(2.))
// .with_cursor_style(CursorStyle::PointingHand)
// .on_click(MouseButton::Left, move |_, this, cx| {
// this.remove_message(id, cx);
// })
// .flex_float()
// .into_any()
// })
// .unwrap_or_else(|| {
// let style = theme.chat_panel.icon_button.default;
// Empty::new()
// .constrained()
// .with_width(style.icon_width)
// .aligned()
// .constrained()
// .with_width(style.button_width)
// .with_height(style.button_width)
// .contained()
// .with_uniform_padding(2.)
// .flex_float()
// .into_any()
// })
// }
// impl Entity for ChatPanel {
// type Event = Event;
// }
// impl View for ChatPanel {
// fn ui_name() -> &'static str {
// "ChatPanel"
// }
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
// let theme = theme::current(cx);
// let element = if self.client.user_id().is_some() {
// self.render_channel(cx)
// } else {
// self.render_sign_in_prompt(&theme, cx)
// };
// element
// .contained()
// .with_style(theme.chat_panel.container)
// .constrained()
// .with_min_width(150.)
// .into_any()
// }
// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
// self.has_focus = true;
// if matches!(
// *self.client.status().borrow(),
// client::Status::Connected { .. }
// ) {
// let editor = self.input_editor.read(cx).editor.clone();
// cx.focus(&editor);
// }
// }
// fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
// self.has_focus = false;
// }
// }
// impl Panel for ChatPanel {
// fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
// settings::get::<ChatPanelSettings>(cx).dock
// }
// fn position_is_valid(&self, position: DockPosition) -> bool {
// matches!(position, DockPosition::Left | DockPosition::Right)
// }
// fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
// settings::update_settings_file::<ChatPanelSettings>(self.fs.clone(), cx, move |settings| {
// settings.dock = Some(position)
// });
// }
// fn size(&self, cx: &gpui::WindowContext) -> f32 {
// self.width
// .unwrap_or_else(|| settings::get::<ChatPanelSettings>(cx).default_width)
// }
// fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
// self.width = size;
// self.serialize(cx);
// cx.notify();
// }
// fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
// self.active = active;
// if active {
// self.acknowledge_last_message(cx);
// if !is_channels_feature_enabled(cx) {
// cx.emit(Event::Dismissed);
// }
// }
// }
// fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
// (settings::get::<ChatPanelSettings>(cx).button && is_channels_feature_enabled(cx))
// .then(|| "icons/conversations.svg")
// }
// fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
// ("Chat Panel".to_string(), Some(Box::new(ToggleFocus)))
// }
// fn should_change_position_on_event(event: &Self::Event) -> bool {
// matches!(event, Event::DockPositionChanged)
// }
// fn should_close_on_event(event: &Self::Event) -> bool {
// matches!(event, Event::Dismissed)
// }
// fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
// self.has_focus
// }
// fn is_focus_event(event: &Self::Event) -> bool {
// matches!(event, Event::Focus)
// }
// }
// fn format_timestamp(
// mut timestamp: OffsetDateTime,
// mut now: OffsetDateTime,
// local_timezone: UtcOffset,
// ) -> String {
// timestamp = timestamp.to_offset(local_timezone);
// now = now.to_offset(local_timezone);
// let today = now.date();
// let date = timestamp.date();
// let mut hour = timestamp.hour();
// let mut part = "am";
// if hour > 12 {
// hour -= 12;
// part = "pm";
// }
// if date == today {
// format!("{:02}:{:02}{}", hour, timestamp.minute(), part)
// } else if date.next_day() == Some(today) {
// format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part)
// } else {
// format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
// }
// }
// fn render_icon_button<V: View>(style: &IconButton, svg_path: &'static str) -> impl Element<V> {
// Svg::new(svg_path)
// .with_color(style.color)
// .constrained()
// .with_width(style.icon_width)
// .aligned()
// .constrained()
// .with_width(style.button_width)
// .with_height(style.button_width)
// .contained()
// .with_style(style.container)
// }
// #[cfg(test)]
// mod tests {
// use super::*;
// use gpui::fonts::HighlightStyle;
// use pretty_assertions::assert_eq;
// use rich_text::{BackgroundKind, Highlight, RenderedRegion};
// use util::test::marked_text_ranges;
// #[gpui::test]
// fn test_render_markdown_with_mentions() {
// let language_registry = Arc::new(LanguageRegistry::test());
// let (body, ranges) = marked_text_ranges("*hi*, «@abc», let's **call** «@fgh»", false);
// let message = channel::ChannelMessage {
// id: ChannelMessageId::Saved(0),
// body,
// timestamp: OffsetDateTime::now_utc(),
// sender: Arc::new(client::User {
// github_login: "fgh".into(),
// avatar: None,
// id: 103,
// }),
// nonce: 5,
// mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
// };
// let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
// // Note that the "'" was replaced with due to smart punctuation.
// let (body, ranges) = marked_text_ranges("«hi», «@abc», lets «call» «@fgh»", false);
// assert_eq!(message.text, body);
// assert_eq!(
// message.highlights,
// vec![
// (
// ranges[0].clone(),
// HighlightStyle {
// italic: Some(true),
// ..Default::default()
// }
// .into()
// ),
// (ranges[1].clone(), Highlight::Mention),
// (
// ranges[2].clone(),
// HighlightStyle {
// weight: Some(gpui::fonts::Weight::BOLD),
// ..Default::default()
// }
// .into()
// ),
// (ranges[3].clone(), Highlight::SelfMention)
// ]
// );
// assert_eq!(
// message.regions,
// vec![
// RenderedRegion {
// background_kind: Some(BackgroundKind::Mention),
// link_url: None
// },
// RenderedRegion {
// background_kind: Some(BackgroundKind::SelfMention),
// link_url: None
// },
// ]
// );
// }
// }

View File

@@ -0,0 +1,313 @@
use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
use client::UserId;
use collections::HashMap;
use editor::{AnchorRangeExt, Editor};
use gpui::{
elements::ChildView, AnyElement, AsyncAppContext, Element, Entity, ModelHandle, Task, View,
ViewContext, ViewHandle, WeakViewHandle,
};
use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
use lazy_static::lazy_static;
use project::search::SearchQuery;
use std::{sync::Arc, time::Duration};
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
lazy_static! {
static ref MENTIONS_SEARCH: SearchQuery = SearchQuery::regex(
"@[-_\\w]+",
false,
false,
Default::default(),
Default::default()
)
.unwrap();
}
pub struct MessageEditor {
pub editor: ViewHandle<Editor>,
channel_store: ModelHandle<ChannelStore>,
users: HashMap<String, UserId>,
mentions: Vec<UserId>,
mentions_task: Option<Task<()>>,
channel_id: Option<ChannelId>,
}
impl MessageEditor {
pub fn new(
language_registry: Arc<LanguageRegistry>,
channel_store: ModelHandle<ChannelStore>,
editor: ViewHandle<Editor>,
cx: &mut ViewContext<Self>,
) -> Self {
editor.update(cx, |editor, cx| {
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
});
let buffer = editor
.read(cx)
.buffer()
.read(cx)
.as_singleton()
.expect("message editor must be singleton");
cx.subscribe(&buffer, Self::on_buffer_event).detach();
let markdown = language_registry.language_for_name("Markdown");
cx.app_context()
.spawn(|mut cx| async move {
let markdown = markdown.await?;
buffer.update(&mut cx, |buffer, cx| {
buffer.set_language(Some(markdown), cx)
});
anyhow::Ok(())
})
.detach_and_log_err(cx);
Self {
editor,
channel_store,
users: HashMap::default(),
channel_id: None,
mentions: Vec::new(),
mentions_task: None,
}
}
pub fn set_channel(
&mut self,
channel_id: u64,
channel_name: Option<String>,
cx: &mut ViewContext<Self>,
) {
self.editor.update(cx, |editor, cx| {
if let Some(channel_name) = channel_name {
editor.set_placeholder_text(format!("Message #{}", channel_name), cx);
} else {
editor.set_placeholder_text(format!("Message Channel"), cx);
}
});
self.channel_id = Some(channel_id);
self.refresh_users(cx);
}
pub fn refresh_users(&mut self, cx: &mut ViewContext<Self>) {
if let Some(channel_id) = self.channel_id {
let members = self.channel_store.update(cx, |store, cx| {
store.get_channel_member_details(channel_id, cx)
});
cx.spawn(|this, mut cx| async move {
let members = members.await?;
this.update(&mut cx, |this, cx| this.set_members(members, cx))?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
}
pub fn set_members(&mut self, members: Vec<ChannelMembership>, _: &mut ViewContext<Self>) {
self.users.clear();
self.users.extend(
members
.into_iter()
.map(|member| (member.user.github_login.clone(), member.user.id)),
);
}
pub fn take_message(&mut self, cx: &mut ViewContext<Self>) -> MessageParams {
self.editor.update(cx, |editor, cx| {
let highlights = editor.text_highlights::<Self>(cx);
let text = editor.text(cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
let mentions = if let Some((_, ranges)) = highlights {
ranges
.iter()
.map(|range| range.to_offset(&snapshot))
.zip(self.mentions.iter().copied())
.collect()
} else {
Vec::new()
};
editor.clear(cx);
self.mentions.clear();
MessageParams { text, mentions }
})
}
fn on_buffer_event(
&mut self,
buffer: ModelHandle<Buffer>,
event: &language::Event,
cx: &mut ViewContext<Self>,
) {
if let language::Event::Reparsed | language::Event::Edited = event {
let buffer = buffer.read(cx).snapshot();
self.mentions_task = Some(cx.spawn(|this, cx| async move {
cx.background().timer(MENTIONS_DEBOUNCE_INTERVAL).await;
Self::find_mentions(this, buffer, cx).await;
}));
}
}
async fn find_mentions(
this: WeakViewHandle<MessageEditor>,
buffer: BufferSnapshot,
mut cx: AsyncAppContext,
) {
let (buffer, ranges) = cx
.background()
.spawn(async move {
let ranges = MENTIONS_SEARCH.search(&buffer, None).await;
(buffer, ranges)
})
.await;
this.update(&mut cx, |this, cx| {
let mut anchor_ranges = Vec::new();
let mut mentioned_user_ids = Vec::new();
let mut text = String::new();
this.editor.update(cx, |editor, cx| {
let multi_buffer = editor.buffer().read(cx).snapshot(cx);
for range in ranges {
text.clear();
text.extend(buffer.text_for_range(range.clone()));
if let Some(username) = text.strip_prefix("@") {
if let Some(user_id) = this.users.get(username) {
let start = multi_buffer.anchor_after(range.start);
let end = multi_buffer.anchor_after(range.end);
mentioned_user_ids.push(*user_id);
anchor_ranges.push(start..end);
}
}
}
editor.clear_highlights::<Self>(cx);
editor.highlight_text::<Self>(
anchor_ranges,
theme::current(cx).chat_panel.rich_text.mention_highlight,
cx,
)
});
this.mentions = mentioned_user_ids;
this.mentions_task.take();
})
.ok();
}
}
impl Entity for MessageEditor {
type Event = ();
}
impl View for MessageEditor {
fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
ChildView::new(&self.editor, cx).into_any()
}
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
cx.focus(&self.editor);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use client::{Client, User, UserStore};
use gpui::{TestAppContext, WindowHandle};
use language::{Language, LanguageConfig};
use rpc::proto;
use settings::SettingsStore;
use util::{http::FakeHttpClient, test::marked_text_ranges};
#[gpui::test]
async fn test_message_editor(cx: &mut TestAppContext) {
let editor = init_test(cx);
let editor = editor.root(cx);
editor.update(cx, |editor, cx| {
editor.set_members(
vec![
ChannelMembership {
user: Arc::new(User {
github_login: "a-b".into(),
id: 101,
avatar: None,
}),
kind: proto::channel_member::Kind::Member,
role: proto::ChannelRole::Member,
},
ChannelMembership {
user: Arc::new(User {
github_login: "C_D".into(),
id: 102,
avatar: None,
}),
kind: proto::channel_member::Kind::Member,
role: proto::ChannelRole::Member,
},
],
cx,
);
editor.editor.update(cx, |editor, cx| {
editor.set_text("Hello, @a-b! Have you met @C_D?", cx)
});
});
cx.foreground().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
editor.update(cx, |editor, cx| {
let (text, ranges) = marked_text_ranges("Hello, «@a-b»! Have you met «@C_D»?", false);
assert_eq!(
editor.take_message(cx),
MessageParams {
text,
mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
}
);
});
}
fn init_test(cx: &mut TestAppContext) -> WindowHandle<MessageEditor> {
cx.foreground().forbid_parking();
cx.update(|cx| {
let http = FakeHttpClient::with_404_response();
let client = Client::new(http.clone(), cx);
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
cx.set_global(SettingsStore::test(cx));
theme::init((), cx);
language::init(cx);
editor::init(cx);
client::init(&client, cx);
channel::init(&client, user_store, cx);
});
let language_registry = Arc::new(LanguageRegistry::test());
language_registry.add(Arc::new(Language::new(
LanguageConfig {
name: "Markdown".into(),
..Default::default()
},
Some(tree_sitter_markdown::language()),
)));
let editor = cx.add_window(|cx| {
MessageEditor::new(
language_registry,
ChannelStore::global(cx),
cx.add_view(|cx| Editor::auto_height(4, None, cx)),
cx,
)
});
cx.foreground().run_until_parked();
editor
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,717 @@
use channel::{ChannelId, ChannelMembership, ChannelStore};
use client::{
proto::{self, ChannelRole, ChannelVisibility},
User, UserId, UserStore,
};
use context_menu::{ContextMenu, ContextMenuItem};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
actions,
elements::*,
platform::{CursorStyle, MouseButton},
AppContext, ClipboardItem, Entity, ModelHandle, MouseState, Task, View, ViewContext,
ViewHandle,
};
use picker::{Picker, PickerDelegate, PickerEvent};
use std::sync::Arc;
use util::TryFutureExt;
use workspace::Modal;
actions!(
channel_modal,
[
SelectNextControl,
ToggleMode,
ToggleMemberAdmin,
RemoveMember
]
);
pub fn init(cx: &mut AppContext) {
Picker::<ChannelModalDelegate>::init(cx);
cx.add_action(ChannelModal::toggle_mode);
cx.add_action(ChannelModal::toggle_member_admin);
cx.add_action(ChannelModal::remove_member);
cx.add_action(ChannelModal::dismiss);
}
pub struct ChannelModal {
picker: ViewHandle<Picker<ChannelModalDelegate>>,
channel_store: ModelHandle<ChannelStore>,
channel_id: ChannelId,
has_focus: bool,
}
impl ChannelModal {
pub fn new(
user_store: ModelHandle<UserStore>,
channel_store: ModelHandle<ChannelStore>,
channel_id: ChannelId,
mode: Mode,
members: Vec<ChannelMembership>,
cx: &mut ViewContext<Self>,
) -> Self {
cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
let picker = cx.add_view(|cx| {
Picker::new(
ChannelModalDelegate {
matching_users: Vec::new(),
matching_member_indices: Vec::new(),
selected_index: 0,
user_store: user_store.clone(),
channel_store: channel_store.clone(),
channel_id,
match_candidates: Vec::new(),
members,
mode,
context_menu: cx.add_view(|cx| {
let mut menu = ContextMenu::new(cx.view_id(), cx);
menu.set_position_mode(OverlayPositionMode::Local);
menu
}),
},
cx,
)
.with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
});
cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
let has_focus = picker.read(cx).has_focus();
Self {
picker,
channel_store,
channel_id,
has_focus,
}
}
fn toggle_mode(&mut self, _: &ToggleMode, cx: &mut ViewContext<Self>) {
let mode = match self.picker.read(cx).delegate().mode {
Mode::ManageMembers => Mode::InviteMembers,
Mode::InviteMembers => Mode::ManageMembers,
};
self.set_mode(mode, cx);
}
fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
let channel_store = self.channel_store.clone();
let channel_id = self.channel_id;
cx.spawn(|this, mut cx| async move {
if mode == Mode::ManageMembers {
let mut members = channel_store
.update(&mut cx, |channel_store, cx| {
channel_store.get_channel_member_details(channel_id, cx)
})
.await?;
members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
this.update(&mut cx, |this, cx| {
this.picker
.update(cx, |picker, _| picker.delegate_mut().members = members);
})?;
}
this.update(&mut cx, |this, cx| {
this.picker.update(cx, |picker, cx| {
let delegate = picker.delegate_mut();
delegate.mode = mode;
delegate.selected_index = 0;
picker.set_query("", cx);
picker.update_matches(picker.query(cx), cx);
cx.notify()
});
cx.notify()
})
})
.detach();
}
fn toggle_member_admin(&mut self, _: &ToggleMemberAdmin, cx: &mut ViewContext<Self>) {
self.picker.update(cx, |picker, cx| {
picker.delegate_mut().toggle_selected_member_admin(cx);
})
}
fn remove_member(&mut self, _: &RemoveMember, cx: &mut ViewContext<Self>) {
self.picker.update(cx, |picker, cx| {
picker.delegate_mut().remove_selected_member(cx);
});
}
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(PickerEvent::Dismiss);
}
}
impl Entity for ChannelModal {
type Event = PickerEvent;
}
impl View for ChannelModal {
fn ui_name() -> &'static str {
"ChannelModal"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = &theme::current(cx).collab_panel.tabbed_modal;
let mode = self.picker.read(cx).delegate().mode;
let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else {
return Empty::new().into_any();
};
enum InviteMembers {}
enum ManageMembers {}
fn render_mode_button<T: 'static>(
mode: Mode,
text: &'static str,
current_mode: Mode,
theme: &theme::TabbedModal,
cx: &mut ViewContext<ChannelModal>,
) -> AnyElement<ChannelModal> {
let active = mode == current_mode;
MouseEventHandler::new::<T, _>(0, cx, move |state, _| {
let contained_text = theme.tab_button.style_for(active, state);
Label::new(text, contained_text.text.clone())
.contained()
.with_style(contained_text.container.clone())
})
.on_click(MouseButton::Left, move |_, this, cx| {
if !active {
this.set_mode(mode, cx);
}
})
.with_cursor_style(CursorStyle::PointingHand)
.into_any()
}
fn render_visibility(
channel_id: ChannelId,
visibility: ChannelVisibility,
theme: &theme::TabbedModal,
cx: &mut ViewContext<ChannelModal>,
) -> AnyElement<ChannelModal> {
enum TogglePublic {}
if visibility == ChannelVisibility::Members {
return Flex::row()
.with_child(
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
let style = theme.visibility_toggle.style_for(state);
Label::new(format!("{}", "Public access: OFF"), style.text.clone())
.contained()
.with_style(style.container.clone())
})
.on_click(MouseButton::Left, move |_, this, cx| {
this.channel_store
.update(cx, |channel_store, cx| {
channel_store.set_channel_visibility(
channel_id,
ChannelVisibility::Public,
cx,
)
})
.detach_and_log_err(cx);
})
.with_cursor_style(CursorStyle::PointingHand),
)
.into_any();
}
Flex::row()
.with_child(
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
let style = theme.visibility_toggle.style_for(state);
Label::new(format!("{}", "Public access: ON"), style.text.clone())
.contained()
.with_style(style.container.clone())
})
.on_click(MouseButton::Left, move |_, this, cx| {
this.channel_store
.update(cx, |channel_store, cx| {
channel_store.set_channel_visibility(
channel_id,
ChannelVisibility::Members,
cx,
)
})
.detach_and_log_err(cx);
})
.with_cursor_style(CursorStyle::PointingHand),
)
.with_spacing(14.0)
.with_child(
MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
let style = theme.channel_link.style_for(state);
Label::new(format!("{}", "copy link"), style.text.clone())
.contained()
.with_style(style.container.clone())
})
.on_click(MouseButton::Left, move |_, this, cx| {
if let Some(channel) =
this.channel_store.read(cx).channel_for_id(channel_id)
{
let item = ClipboardItem::new(channel.link());
cx.write_to_clipboard(item);
}
})
.with_cursor_style(CursorStyle::PointingHand),
)
.into_any()
}
Flex::column()
.with_child(
Flex::column()
.with_child(
Label::new(format!("#{}", channel.name), theme.title.text.clone())
.contained()
.with_style(theme.title.container.clone()),
)
.with_child(render_visibility(channel.id, channel.visibility, theme, cx))
.with_child(Flex::row().with_children([
render_mode_button::<InviteMembers>(
Mode::InviteMembers,
"Invite members",
mode,
theme,
cx,
),
render_mode_button::<ManageMembers>(
Mode::ManageMembers,
"Manage members",
mode,
theme,
cx,
),
]))
.expanded()
.contained()
.with_style(theme.header),
)
.with_child(
ChildView::new(&self.picker, cx)
.contained()
.with_style(theme.body),
)
.constrained()
.with_max_height(theme.max_height)
.with_max_width(theme.max_width)
.contained()
.with_style(theme.modal)
.into_any()
}
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
self.has_focus = true;
if cx.is_self_focused() {
cx.focus(&self.picker)
}
}
fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
self.has_focus = false;
}
}
impl Modal for ChannelModal {
fn has_focus(&self) -> bool {
self.has_focus
}
fn dismiss_on_event(event: &Self::Event) -> bool {
match event {
PickerEvent::Dismiss => true,
}
}
}
#[derive(Copy, Clone, PartialEq)]
pub enum Mode {
ManageMembers,
InviteMembers,
}
pub struct ChannelModalDelegate {
matching_users: Vec<Arc<User>>,
matching_member_indices: Vec<usize>,
user_store: ModelHandle<UserStore>,
channel_store: ModelHandle<ChannelStore>,
channel_id: ChannelId,
selected_index: usize,
mode: Mode,
match_candidates: Vec<StringMatchCandidate>,
members: Vec<ChannelMembership>,
context_menu: ViewHandle<ContextMenu>,
}
impl PickerDelegate for ChannelModalDelegate {
fn placeholder_text(&self) -> Arc<str> {
"Search collaborator by username...".into()
}
fn match_count(&self) -> usize {
match self.mode {
Mode::ManageMembers => self.matching_member_indices.len(),
Mode::InviteMembers => self.matching_users.len(),
}
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
match self.mode {
Mode::ManageMembers => {
self.match_candidates.clear();
self.match_candidates
.extend(self.members.iter().enumerate().map(|(id, member)| {
StringMatchCandidate {
id,
string: member.user.github_login.clone(),
char_bag: member.user.github_login.chars().collect(),
}
}));
let matches = cx.background().block(match_strings(
&self.match_candidates,
&query,
true,
usize::MAX,
&Default::default(),
cx.background().clone(),
));
cx.spawn(|picker, mut cx| async move {
picker
.update(&mut cx, |picker, cx| {
let delegate = picker.delegate_mut();
delegate.matching_member_indices.clear();
delegate
.matching_member_indices
.extend(matches.into_iter().map(|m| m.candidate_id));
cx.notify();
})
.ok();
})
}
Mode::InviteMembers => {
let search_users = self
.user_store
.update(cx, |store, cx| store.fuzzy_search_users(query, cx));
cx.spawn(|picker, mut cx| async move {
async {
let users = search_users.await?;
picker.update(&mut cx, |picker, cx| {
let delegate = picker.delegate_mut();
delegate.matching_users = users;
cx.notify();
})?;
anyhow::Ok(())
}
.log_err()
.await;
})
}
}
}
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some((selected_user, role)) = self.user_at_index(self.selected_index) {
match self.mode {
Mode::ManageMembers => {
self.show_context_menu(role.unwrap_or(ChannelRole::Member), cx)
}
Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
Some(proto::channel_member::Kind::Invitee) => {
self.remove_selected_member(cx);
}
Some(proto::channel_member::Kind::AncestorMember) | None => {
self.invite_member(selected_user, cx)
}
Some(proto::channel_member::Kind::Member) => {}
},
}
}
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
cx.emit(PickerEvent::Dismiss);
}
fn render_match(
&self,
ix: usize,
mouse_state: &mut MouseState,
selected: bool,
cx: &gpui::AppContext,
) -> AnyElement<Picker<Self>> {
let full_theme = &theme::current(cx);
let theme = &full_theme.collab_panel.channel_modal;
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
let (user, role) = self.user_at_index(ix).unwrap();
let request_status = self.member_status(user.id, cx);
let style = tabbed_modal
.picker
.item
.in_state(selected)
.style_for(mouse_state);
let in_manage = matches!(self.mode, Mode::ManageMembers);
let mut result = Flex::row()
.with_children(user.avatar.clone().map(|avatar| {
Image::from_data(avatar)
.with_style(theme.contact_avatar)
.aligned()
.left()
}))
.with_child(
Label::new(user.github_login.clone(), style.label.clone())
.contained()
.with_style(theme.contact_username)
.aligned()
.left(),
)
.with_children({
(in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
|| {
Label::new("Invited", theme.member_tag.text.clone())
.contained()
.with_style(theme.member_tag.container)
.aligned()
.left()
},
)
})
.with_children(if in_manage && role == Some(ChannelRole::Admin) {
Some(
Label::new("Admin", theme.member_tag.text.clone())
.contained()
.with_style(theme.member_tag.container)
.aligned()
.left(),
)
} else if in_manage && role == Some(ChannelRole::Guest) {
Some(
Label::new("Guest", theme.member_tag.text.clone())
.contained()
.with_style(theme.member_tag.container)
.aligned()
.left(),
)
} else {
None
})
.with_children({
let svg = match self.mode {
Mode::ManageMembers => Some(
Svg::new("icons/ellipsis.svg")
.with_color(theme.member_icon.color)
.constrained()
.with_width(theme.member_icon.icon_width)
.aligned()
.constrained()
.with_width(theme.member_icon.button_width)
.with_height(theme.member_icon.button_width)
.contained()
.with_style(theme.member_icon.container),
),
Mode::InviteMembers => match request_status {
Some(proto::channel_member::Kind::Member) => Some(
Svg::new("icons/check.svg")
.with_color(theme.member_icon.color)
.constrained()
.with_width(theme.member_icon.icon_width)
.aligned()
.constrained()
.with_width(theme.member_icon.button_width)
.with_height(theme.member_icon.button_width)
.contained()
.with_style(theme.member_icon.container),
),
Some(proto::channel_member::Kind::Invitee) => Some(
Svg::new("icons/check.svg")
.with_color(theme.invitee_icon.color)
.constrained()
.with_width(theme.invitee_icon.icon_width)
.aligned()
.constrained()
.with_width(theme.invitee_icon.button_width)
.with_height(theme.invitee_icon.button_width)
.contained()
.with_style(theme.invitee_icon.container),
),
Some(proto::channel_member::Kind::AncestorMember) | None => None,
},
};
svg.map(|svg| svg.aligned().flex_float().into_any())
})
.contained()
.with_style(style.container)
.constrained()
.with_height(tabbed_modal.row_height)
.into_any();
if selected {
result = Stack::new()
.with_child(result)
.with_child(
ChildView::new(&self.context_menu, cx)
.aligned()
.top()
.right(),
)
.into_any();
}
result
}
}
impl ChannelModalDelegate {
fn member_status(
&self,
user_id: UserId,
cx: &AppContext,
) -> Option<proto::channel_member::Kind> {
self.members
.iter()
.find_map(|membership| (membership.user.id == user_id).then_some(membership.kind))
.or_else(|| {
self.channel_store
.read(cx)
.has_pending_channel_invite(self.channel_id, user_id)
.then_some(proto::channel_member::Kind::Invitee)
})
}
fn user_at_index(&self, ix: usize) -> Option<(Arc<User>, Option<ChannelRole>)> {
match self.mode {
Mode::ManageMembers => self.matching_member_indices.get(ix).and_then(|ix| {
let channel_membership = self.members.get(*ix)?;
Some((
channel_membership.user.clone(),
Some(channel_membership.role),
))
}),
Mode::InviteMembers => Some((self.matching_users.get(ix).cloned()?, None)),
}
}
fn toggle_selected_member_admin(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
let (user, role) = self.user_at_index(self.selected_index)?;
let new_role = if role == Some(ChannelRole::Admin) {
ChannelRole::Member
} else {
ChannelRole::Admin
};
let update = self.channel_store.update(cx, |store, cx| {
store.set_member_role(self.channel_id, user.id, new_role, cx)
});
cx.spawn(|picker, mut cx| async move {
update.await?;
picker.update(&mut cx, |picker, cx| {
let this = picker.delegate_mut();
if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
member.role = new_role;
}
cx.focus_self();
cx.notify();
})
})
.detach_and_log_err(cx);
Some(())
}
fn remove_selected_member(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
let (user, _) = self.user_at_index(self.selected_index)?;
let user_id = user.id;
let update = self.channel_store.update(cx, |store, cx| {
store.remove_member(self.channel_id, user_id, cx)
});
cx.spawn(|picker, mut cx| async move {
update.await?;
picker.update(&mut cx, |picker, cx| {
let this = picker.delegate_mut();
if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) {
this.members.remove(ix);
this.matching_member_indices.retain_mut(|member_ix| {
if *member_ix == ix {
return false;
} else if *member_ix > ix {
*member_ix -= 1;
}
true
})
}
this.selected_index = this
.selected_index
.min(this.matching_member_indices.len().saturating_sub(1));
cx.focus_self();
cx.notify();
})
})
.detach_and_log_err(cx);
Some(())
}
fn invite_member(&mut self, user: Arc<User>, cx: &mut ViewContext<Picker<Self>>) {
let invite_member = self.channel_store.update(cx, |store, cx| {
store.invite_member(self.channel_id, user.id, ChannelRole::Member, cx)
});
cx.spawn(|this, mut cx| async move {
invite_member.await?;
this.update(&mut cx, |this, cx| {
let new_member = ChannelMembership {
user,
kind: proto::channel_member::Kind::Invitee,
role: ChannelRole::Member,
};
let members = &mut this.delegate_mut().members;
match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) {
Ok(ix) | Err(ix) => members.insert(ix, new_member),
}
cx.notify();
})
})
.detach_and_log_err(cx);
}
fn show_context_menu(&mut self, role: ChannelRole, cx: &mut ViewContext<Picker<Self>>) {
self.context_menu.update(cx, |context_menu, cx| {
context_menu.show(
Default::default(),
AnchorCorner::TopRight,
vec![
ContextMenuItem::action("Remove", RemoveMember),
ContextMenuItem::action(
if role == ChannelRole::Admin {
"Make non-admin"
} else {
"Make admin"
},
ToggleMemberAdmin,
),
],
cx,
)
})
}
}

View File

@@ -0,0 +1,261 @@
use client::{ContactRequestStatus, User, UserStore};
use gpui::{
elements::*, AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
};
use picker::{Picker, PickerDelegate, PickerEvent};
use std::sync::Arc;
use util::TryFutureExt;
use workspace::Modal;
pub fn init(cx: &mut AppContext) {
Picker::<ContactFinderDelegate>::init(cx);
cx.add_action(ContactFinder::dismiss)
}
pub struct ContactFinder {
picker: ViewHandle<Picker<ContactFinderDelegate>>,
has_focus: bool,
}
impl ContactFinder {
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
let picker = cx.add_view(|cx| {
Picker::new(
ContactFinderDelegate {
user_store,
potential_contacts: Arc::from([]),
selected_index: 0,
},
cx,
)
.with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
});
cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
Self {
picker,
has_focus: false,
}
}
pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
self.picker.update(cx, |picker, cx| {
picker.set_query(query, cx);
});
}
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(PickerEvent::Dismiss);
}
}
impl Entity for ContactFinder {
type Event = PickerEvent;
}
impl View for ContactFinder {
fn ui_name() -> &'static str {
"ContactFinder"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let full_theme = &theme::current(cx);
let theme = &full_theme.collab_panel.tabbed_modal;
fn render_mode_button(
text: &'static str,
theme: &theme::TabbedModal,
_cx: &mut ViewContext<ContactFinder>,
) -> AnyElement<ContactFinder> {
let contained_text = &theme.tab_button.active_state().default;
Label::new(text, contained_text.text.clone())
.contained()
.with_style(contained_text.container.clone())
.into_any()
}
Flex::column()
.with_child(
Flex::column()
.with_child(
Label::new("Contacts", theme.title.text.clone())
.contained()
.with_style(theme.title.container.clone()),
)
.with_child(Flex::row().with_children([render_mode_button(
"Invite new contacts",
&theme,
cx,
)]))
.expanded()
.contained()
.with_style(theme.header),
)
.with_child(
ChildView::new(&self.picker, cx)
.contained()
.with_style(theme.body),
)
.constrained()
.with_max_height(theme.max_height)
.with_max_width(theme.max_width)
.contained()
.with_style(theme.modal)
.into_any()
}
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
self.has_focus = true;
if cx.is_self_focused() {
cx.focus(&self.picker)
}
}
fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
self.has_focus = false;
}
}
impl Modal for ContactFinder {
fn has_focus(&self) -> bool {
self.has_focus
}
fn dismiss_on_event(event: &Self::Event) -> bool {
match event {
PickerEvent::Dismiss => true,
}
}
}
pub struct ContactFinderDelegate {
potential_contacts: Arc<[Arc<User>]>,
user_store: ModelHandle<UserStore>,
selected_index: usize,
}
impl PickerDelegate for ContactFinderDelegate {
fn placeholder_text(&self) -> Arc<str> {
"Search collaborator by username...".into()
}
fn match_count(&self) -> usize {
self.potential_contacts.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let search_users = self
.user_store
.update(cx, |store, cx| store.fuzzy_search_users(query, cx));
cx.spawn(|picker, mut cx| async move {
async {
let potential_contacts = search_users.await?;
picker.update(&mut cx, |picker, cx| {
picker.delegate_mut().potential_contacts = potential_contacts.into();
cx.notify();
})?;
anyhow::Ok(())
}
.log_err()
.await;
})
}
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some(user) = self.potential_contacts.get(self.selected_index) {
let user_store = self.user_store.read(cx);
match user_store.contact_request_status(user) {
ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
self.user_store
.update(cx, |store, cx| store.request_contact(user.id, cx))
.detach();
}
ContactRequestStatus::RequestSent => {
self.user_store
.update(cx, |store, cx| store.remove_contact(user.id, cx))
.detach();
}
_ => {}
}
}
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
cx.emit(PickerEvent::Dismiss);
}
fn render_match(
&self,
ix: usize,
mouse_state: &mut MouseState,
selected: bool,
cx: &gpui::AppContext,
) -> AnyElement<Picker<Self>> {
let full_theme = &theme::current(cx);
let theme = &full_theme.collab_panel.contact_finder;
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
let user = &self.potential_contacts[ix];
let request_status = self.user_store.read(cx).contact_request_status(user);
let icon_path = match request_status {
ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
Some("icons/check.svg")
}
ContactRequestStatus::RequestSent => Some("icons/x.svg"),
ContactRequestStatus::RequestAccepted => None,
};
let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
&theme.disabled_contact_button
} else {
&theme.contact_button
};
let style = tabbed_modal
.picker
.item
.in_state(selected)
.style_for(mouse_state);
Flex::row()
.with_children(user.avatar.clone().map(|avatar| {
Image::from_data(avatar)
.with_style(theme.contact_avatar)
.aligned()
.left()
}))
.with_child(
Label::new(user.github_login.clone(), style.label.clone())
.contained()
.with_style(theme.contact_username)
.aligned()
.left(),
)
.with_children(icon_path.map(|icon_path| {
Svg::new(icon_path)
.with_color(button_style.color)
.constrained()
.with_width(button_style.icon_width)
.aligned()
.contained()
.with_style(button_style.container)
.constrained()
.with_width(button_style.button_width)
.with_height(button_style.button_width)
.aligned()
.flex_float()
}))
.contained()
.with_style(style.container)
.constrained()
.with_height(tabbed_modal.row_height)
.into_any()
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,154 @@
pub mod channel_view;
pub mod chat_panel;
pub mod collab_panel;
mod collab_titlebar_item;
mod face_pile;
pub mod notification_panel;
pub mod notifications;
mod panel_settings;
use std::sync::Arc;
pub use collab_panel::CollabPanel;
pub use collab_titlebar_item::CollabTitlebarItem;
use gpui::AppContext;
pub use panel_settings::{
ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
};
use settings::Settings;
use workspace::AppState;
// actions!(
// collab,
// [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
// );
pub fn init(_app_state: &Arc<AppState>, cx: &mut AppContext) {
CollaborationPanelSettings::register(cx);
ChatPanelSettings::register(cx);
NotificationPanelSettings::register(cx);
// vcs_menu::init(cx);
collab_titlebar_item::init(cx);
collab_panel::init(cx);
// chat_panel::init(cx);
// notifications::init(&app_state, cx);
// cx.add_global_action(toggle_screen_sharing);
// cx.add_global_action(toggle_mute);
// cx.add_global_action(toggle_deafen);
}
// pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
// let call = ActiveCall::global(cx).read(cx);
// if let Some(room) = call.room().cloned() {
// let client = call.client();
// let toggle_screen_sharing = room.update(cx, |room, cx| {
// if room.is_screen_sharing() {
// report_call_event_for_room(
// "disable screen share",
// room.id(),
// room.channel_id(),
// &client,
// cx,
// );
// Task::ready(room.unshare_screen(cx))
// } else {
// report_call_event_for_room(
// "enable screen share",
// room.id(),
// room.channel_id(),
// &client,
// cx,
// );
// room.share_screen(cx)
// }
// });
// toggle_screen_sharing.detach_and_log_err(cx);
// }
// }
// pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
// let call = ActiveCall::global(cx).read(cx);
// if let Some(room) = call.room().cloned() {
// let client = call.client();
// room.update(cx, |room, cx| {
// let operation = if room.is_muted(cx) {
// "enable microphone"
// } else {
// "disable microphone"
// };
// report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx);
// room.toggle_mute(cx)
// })
// .map(|task| task.detach_and_log_err(cx))
// .log_err();
// }
// }
// pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
// if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
// room.update(cx, Room::toggle_deafen)
// .map(|task| task.detach_and_log_err(cx))
// .log_err();
// }
// }
// fn notification_window_options(
// screen: Rc<dyn Screen>,
// window_size: Vector2F,
// ) -> WindowOptions<'static> {
// const NOTIFICATION_PADDING: f32 = 16.;
// let screen_bounds = screen.content_bounds();
// WindowOptions {
// bounds: WindowBounds::Fixed(RectF::new(
// screen_bounds.upper_right()
// + vec2f(
// -NOTIFICATION_PADDING - window_size.x(),
// NOTIFICATION_PADDING,
// ),
// window_size,
// )),
// titlebar: None,
// center: false,
// focus: false,
// show: true,
// kind: WindowKind::PopUp,
// is_movable: false,
// screen: Some(screen),
// }
// }
// fn render_avatar<T: 'static>(
// avatar: Option<Arc<ImageData>>,
// avatar_style: &AvatarStyle,
// container: ContainerStyle,
// ) -> AnyElement<T> {
// avatar
// .map(|avatar| {
// Image::from_data(avatar)
// .with_style(avatar_style.image)
// .aligned()
// .contained()
// .with_corner_radius(avatar_style.outer_corner_radius)
// .constrained()
// .with_width(avatar_style.outer_width)
// .with_height(avatar_style.outer_width)
// .into_any()
// })
// .unwrap_or_else(|| {
// Empty::new()
// .constrained()
// .with_width(avatar_style.outer_width)
// .into_any()
// })
// .contained()
// .with_style(container)
// .into_any()
// }
// fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
// cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
// }

View File

@@ -0,0 +1,113 @@
// use std::ops::Range;
// use gpui::{
// geometry::{
// rect::RectF,
// vector::{vec2f, Vector2F},
// },
// json::ToJson,
// serde_json::{self, json},
// AnyElement, Axis, Element, View, ViewContext,
// };
// pub(crate) struct FacePile<V: View> {
// overlap: f32,
// faces: Vec<AnyElement<V>>,
// }
// impl<V: View> FacePile<V> {
// pub fn new(overlap: f32) -> Self {
// Self {
// overlap,
// faces: Vec::new(),
// }
// }
// }
// impl<V: View> Element<V> for FacePile<V> {
// type LayoutState = ();
// type PaintState = ();
// fn layout(
// &mut self,
// constraint: gpui::SizeConstraint,
// view: &mut V,
// cx: &mut ViewContext<V>,
// ) -> (Vector2F, Self::LayoutState) {
// debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
// let mut width = 0.;
// let mut max_height = 0.;
// for face in &mut self.faces {
// let layout = face.layout(constraint, view, cx);
// width += layout.x();
// max_height = f32::max(max_height, layout.y());
// }
// width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
// (
// Vector2F::new(width, max_height.clamp(1., constraint.max.y())),
// (),
// )
// }
// fn paint(
// &mut self,
// bounds: RectF,
// visible_bounds: RectF,
// _layout: &mut Self::LayoutState,
// view: &mut V,
// cx: &mut ViewContext<V>,
// ) -> Self::PaintState {
// let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
// let origin_y = bounds.upper_right().y();
// let mut origin_x = bounds.upper_right().x();
// for face in self.faces.iter_mut().rev() {
// let size = face.size();
// origin_x -= size.x();
// let origin_y = origin_y + (bounds.height() - size.y()) / 2.0;
// cx.scene().push_layer(None);
// face.paint(vec2f(origin_x, origin_y), visible_bounds, view, cx);
// cx.scene().pop_layer();
// origin_x += self.overlap;
// }
// ()
// }
// fn rect_for_text_range(
// &self,
// _: Range<usize>,
// _: RectF,
// _: RectF,
// _: &Self::LayoutState,
// _: &Self::PaintState,
// _: &V,
// _: &ViewContext<V>,
// ) -> Option<RectF> {
// None
// }
// fn debug(
// &self,
// bounds: RectF,
// _: &Self::LayoutState,
// _: &Self::PaintState,
// _: &V,
// _: &ViewContext<V>,
// ) -> serde_json::Value {
// json!({
// "type": "FacePile",
// "bounds": bounds.to_json()
// })
// }
// }
// impl<V: View> Extend<AnyElement<V>> for FacePile<V> {
// fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
// self.faces.extend(children);
// }
// }

View File

@@ -0,0 +1,884 @@
// use crate::{chat_panel::ChatPanel, render_avatar, NotificationPanelSettings};
// use anyhow::Result;
// use channel::ChannelStore;
// use client::{Client, Notification, User, UserStore};
// use collections::HashMap;
// use db::kvp::KEY_VALUE_STORE;
// use futures::StreamExt;
// use gpui::{
// actions,
// elements::*,
// platform::{CursorStyle, MouseButton},
// serde_json, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Task, View,
// ViewContext, ViewHandle, WeakViewHandle, WindowContext,
// };
// use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
// use project::Fs;
// use rpc::proto;
// use serde::{Deserialize, Serialize};
// use settings::SettingsStore;
// use std::{sync::Arc, time::Duration};
// use theme::{ui, Theme};
// use time::{OffsetDateTime, UtcOffset};
// use util::{ResultExt, TryFutureExt};
// use workspace::{
// dock::{DockPosition, Panel},
// Workspace,
// };
// const LOADING_THRESHOLD: usize = 30;
// const MARK_AS_READ_DELAY: Duration = Duration::from_secs(1);
// const TOAST_DURATION: Duration = Duration::from_secs(5);
// const NOTIFICATION_PANEL_KEY: &'static str = "NotificationPanel";
// pub struct NotificationPanel {
// client: Arc<Client>,
// user_store: ModelHandle<UserStore>,
// channel_store: ModelHandle<ChannelStore>,
// notification_store: ModelHandle<NotificationStore>,
// fs: Arc<dyn Fs>,
// width: Option<f32>,
// active: bool,
// notification_list: ListState<Self>,
// pending_serialization: Task<Option<()>>,
// subscriptions: Vec<gpui::Subscription>,
// workspace: WeakViewHandle<Workspace>,
// current_notification_toast: Option<(u64, Task<()>)>,
// local_timezone: UtcOffset,
// has_focus: bool,
// mark_as_read_tasks: HashMap<u64, Task<Result<()>>>,
// }
// #[derive(Serialize, Deserialize)]
// struct SerializedNotificationPanel {
// width: Option<f32>,
// }
// #[derive(Debug)]
// pub enum Event {
// DockPositionChanged,
// Focus,
// Dismissed,
// }
// pub struct NotificationPresenter {
// pub actor: Option<Arc<client::User>>,
// pub text: String,
// pub icon: &'static str,
// pub needs_response: bool,
// pub can_navigate: bool,
// }
// actions!(notification_panel, [ToggleFocus]);
// pub fn init(_cx: &mut AppContext) {}
// impl NotificationPanel {
// pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
// let fs = workspace.app_state().fs.clone();
// let client = workspace.app_state().client.clone();
// let user_store = workspace.app_state().user_store.clone();
// let workspace_handle = workspace.weak_handle();
// cx.add_view(|cx| {
// let mut status = client.status();
// cx.spawn(|this, mut cx| async move {
// while let Some(_) = status.next().await {
// if this
// .update(&mut cx, |_, cx| {
// cx.notify();
// })
// .is_err()
// {
// break;
// }
// }
// })
// .detach();
// let mut notification_list =
// ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
// this.render_notification(ix, cx)
// .unwrap_or_else(|| Empty::new().into_any())
// });
// notification_list.set_scroll_handler(|visible_range, count, this, cx| {
// if count.saturating_sub(visible_range.end) < LOADING_THRESHOLD {
// if let Some(task) = this
// .notification_store
// .update(cx, |store, cx| store.load_more_notifications(false, cx))
// {
// task.detach();
// }
// }
// });
// let mut this = Self {
// fs,
// client,
// user_store,
// local_timezone: cx.platform().local_timezone(),
// channel_store: ChannelStore::global(cx),
// notification_store: NotificationStore::global(cx),
// notification_list,
// pending_serialization: Task::ready(None),
// workspace: workspace_handle,
// has_focus: false,
// current_notification_toast: None,
// subscriptions: Vec::new(),
// active: false,
// mark_as_read_tasks: HashMap::default(),
// width: None,
// };
// let mut old_dock_position = this.position(cx);
// this.subscriptions.extend([
// cx.observe(&this.notification_store, |_, _, cx| cx.notify()),
// cx.subscribe(&this.notification_store, Self::on_notification_event),
// cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
// let new_dock_position = this.position(cx);
// if new_dock_position != old_dock_position {
// old_dock_position = new_dock_position;
// cx.emit(Event::DockPositionChanged);
// }
// cx.notify();
// }),
// ]);
// this
// })
// }
// pub fn load(
// workspace: WeakViewHandle<Workspace>,
// cx: AsyncAppContext,
// ) -> Task<Result<ViewHandle<Self>>> {
// cx.spawn(|mut cx| async move {
// let serialized_panel = if let Some(panel) = cx
// .background()
// .spawn(async move { KEY_VALUE_STORE.read_kvp(NOTIFICATION_PANEL_KEY) })
// .await
// .log_err()
// .flatten()
// {
// Some(serde_json::from_str::<SerializedNotificationPanel>(&panel)?)
// } else {
// None
// };
// workspace.update(&mut cx, |workspace, cx| {
// let panel = Self::new(workspace, cx);
// if let Some(serialized_panel) = serialized_panel {
// panel.update(cx, |panel, cx| {
// panel.width = serialized_panel.width;
// cx.notify();
// });
// }
// panel
// })
// })
// }
// fn serialize(&mut self, cx: &mut ViewContext<Self>) {
// let width = self.width;
// self.pending_serialization = cx.background().spawn(
// async move {
// KEY_VALUE_STORE
// .write_kvp(
// NOTIFICATION_PANEL_KEY.into(),
// serde_json::to_string(&SerializedNotificationPanel { width })?,
// )
// .await?;
// anyhow::Ok(())
// }
// .log_err(),
// );
// }
// fn render_notification(
// &mut self,
// ix: usize,
// cx: &mut ViewContext<Self>,
// ) -> Option<AnyElement<Self>> {
// let entry = self.notification_store.read(cx).notification_at(ix)?;
// let notification_id = entry.id;
// let now = OffsetDateTime::now_utc();
// let timestamp = entry.timestamp;
// let NotificationPresenter {
// actor,
// text,
// needs_response,
// can_navigate,
// ..
// } = self.present_notification(entry, cx)?;
// let theme = theme::current(cx);
// let style = &theme.notification_panel;
// let response = entry.response;
// let notification = entry.notification.clone();
// let message_style = if entry.is_read {
// style.read_text.clone()
// } else {
// style.unread_text.clone()
// };
// if self.active && !entry.is_read {
// self.did_render_notification(notification_id, &notification, cx);
// }
// enum Decline {}
// enum Accept {}
// Some(
// MouseEventHandler::new::<NotificationEntry, _>(ix, cx, |_, cx| {
// let container = message_style.container;
// Flex::row()
// .with_children(actor.map(|actor| {
// render_avatar(actor.avatar.clone(), &style.avatar, style.avatar_container)
// }))
// .with_child(
// Flex::column()
// .with_child(Text::new(text, message_style.text.clone()))
// .with_child(
// Flex::row()
// .with_child(
// Label::new(
// format_timestamp(timestamp, now, self.local_timezone),
// style.timestamp.text.clone(),
// )
// .contained()
// .with_style(style.timestamp.container),
// )
// .with_children(if let Some(is_accepted) = response {
// Some(
// Label::new(
// if is_accepted {
// "You accepted"
// } else {
// "You declined"
// },
// style.read_text.text.clone(),
// )
// .flex_float()
// .into_any(),
// )
// } else if needs_response {
// Some(
// Flex::row()
// .with_children([
// MouseEventHandler::new::<Decline, _>(
// ix,
// cx,
// |state, _| {
// let button =
// style.button.style_for(state);
// Label::new(
// "Decline",
// button.text.clone(),
// )
// .contained()
// .with_style(button.container)
// },
// )
// .with_cursor_style(CursorStyle::PointingHand)
// .on_click(MouseButton::Left, {
// let notification = notification.clone();
// move |_, view, cx| {
// view.respond_to_notification(
// notification.clone(),
// false,
// cx,
// );
// }
// }),
// MouseEventHandler::new::<Accept, _>(
// ix,
// cx,
// |state, _| {
// let button =
// style.button.style_for(state);
// Label::new(
// "Accept",
// button.text.clone(),
// )
// .contained()
// .with_style(button.container)
// },
// )
// .with_cursor_style(CursorStyle::PointingHand)
// .on_click(MouseButton::Left, {
// let notification = notification.clone();
// move |_, view, cx| {
// view.respond_to_notification(
// notification.clone(),
// true,
// cx,
// );
// }
// }),
// ])
// .flex_float()
// .into_any(),
// )
// } else {
// None
// }),
// )
// .flex(1.0, true),
// )
// .contained()
// .with_style(container)
// .into_any()
// })
// .with_cursor_style(if can_navigate {
// CursorStyle::PointingHand
// } else {
// CursorStyle::default()
// })
// .on_click(MouseButton::Left, {
// let notification = notification.clone();
// move |_, this, cx| this.did_click_notification(&notification, cx)
// })
// .into_any(),
// )
// }
// fn present_notification(
// &self,
// entry: &NotificationEntry,
// cx: &AppContext,
// ) -> Option<NotificationPresenter> {
// let user_store = self.user_store.read(cx);
// let channel_store = self.channel_store.read(cx);
// match entry.notification {
// Notification::ContactRequest { sender_id } => {
// let requester = user_store.get_cached_user(sender_id)?;
// Some(NotificationPresenter {
// icon: "icons/plus.svg",
// text: format!("{} wants to add you as a contact", requester.github_login),
// needs_response: user_store.has_incoming_contact_request(requester.id),
// actor: Some(requester),
// can_navigate: false,
// })
// }
// Notification::ContactRequestAccepted { responder_id } => {
// let responder = user_store.get_cached_user(responder_id)?;
// Some(NotificationPresenter {
// icon: "icons/plus.svg",
// text: format!("{} accepted your contact invite", responder.github_login),
// needs_response: false,
// actor: Some(responder),
// can_navigate: false,
// })
// }
// Notification::ChannelInvitation {
// ref channel_name,
// channel_id,
// inviter_id,
// } => {
// let inviter = user_store.get_cached_user(inviter_id)?;
// Some(NotificationPresenter {
// icon: "icons/hash.svg",
// text: format!(
// "{} invited you to join the #{channel_name} channel",
// inviter.github_login
// ),
// needs_response: channel_store.has_channel_invitation(channel_id),
// actor: Some(inviter),
// can_navigate: false,
// })
// }
// Notification::ChannelMessageMention {
// sender_id,
// channel_id,
// message_id,
// } => {
// let sender = user_store.get_cached_user(sender_id)?;
// let channel = channel_store.channel_for_id(channel_id)?;
// let message = self
// .notification_store
// .read(cx)
// .channel_message_for_id(message_id)?;
// Some(NotificationPresenter {
// icon: "icons/conversations.svg",
// text: format!(
// "{} mentioned you in #{}:\n{}",
// sender.github_login, channel.name, message.body,
// ),
// needs_response: false,
// actor: Some(sender),
// can_navigate: true,
// })
// }
// }
// }
// fn did_render_notification(
// &mut self,
// notification_id: u64,
// notification: &Notification,
// cx: &mut ViewContext<Self>,
// ) {
// let should_mark_as_read = match notification {
// Notification::ContactRequestAccepted { .. } => true,
// Notification::ContactRequest { .. }
// | Notification::ChannelInvitation { .. }
// | Notification::ChannelMessageMention { .. } => false,
// };
// if should_mark_as_read {
// self.mark_as_read_tasks
// .entry(notification_id)
// .or_insert_with(|| {
// let client = self.client.clone();
// cx.spawn(|this, mut cx| async move {
// cx.background().timer(MARK_AS_READ_DELAY).await;
// client
// .request(proto::MarkNotificationRead { notification_id })
// .await?;
// this.update(&mut cx, |this, _| {
// this.mark_as_read_tasks.remove(&notification_id);
// })?;
// Ok(())
// })
// });
// }
// }
// fn did_click_notification(&mut self, notification: &Notification, cx: &mut ViewContext<Self>) {
// if let Notification::ChannelMessageMention {
// message_id,
// channel_id,
// ..
// } = notification.clone()
// {
// if let Some(workspace) = self.workspace.upgrade(cx) {
// cx.app_context().defer(move |cx| {
// workspace.update(cx, |workspace, cx| {
// if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
// panel.update(cx, |panel, cx| {
// panel
// .select_channel(channel_id, Some(message_id), cx)
// .detach_and_log_err(cx);
// });
// }
// });
// });
// }
// }
// }
// fn is_showing_notification(&self, notification: &Notification, cx: &AppContext) -> bool {
// if let Notification::ChannelMessageMention { channel_id, .. } = &notification {
// if let Some(workspace) = self.workspace.upgrade(cx) {
// return workspace
// .read_with(cx, |workspace, cx| {
// if let Some(panel) = workspace.panel::<ChatPanel>(cx) {
// return panel.read_with(cx, |panel, cx| {
// panel.is_scrolled_to_bottom()
// && panel.active_chat().map_or(false, |chat| {
// chat.read(cx).channel_id == *channel_id
// })
// });
// }
// false
// })
// .unwrap_or_default();
// }
// }
// false
// }
// fn render_sign_in_prompt(
// &self,
// theme: &Arc<Theme>,
// cx: &mut ViewContext<Self>,
// ) -> AnyElement<Self> {
// enum SignInPromptLabel {}
// MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
// Label::new(
// "Sign in to view your notifications".to_string(),
// theme
// .chat_panel
// .sign_in_prompt
// .style_for(mouse_state)
// .clone(),
// )
// })
// .with_cursor_style(CursorStyle::PointingHand)
// .on_click(MouseButton::Left, move |_, this, cx| {
// let client = this.client.clone();
// cx.spawn(|_, cx| async move {
// client.authenticate_and_connect(true, &cx).log_err().await;
// })
// .detach();
// })
// .aligned()
// .into_any()
// }
// fn render_empty_state(
// &self,
// theme: &Arc<Theme>,
// _cx: &mut ViewContext<Self>,
// ) -> AnyElement<Self> {
// Label::new(
// "You have no notifications".to_string(),
// theme.chat_panel.sign_in_prompt.default.clone(),
// )
// .aligned()
// .into_any()
// }
// fn on_notification_event(
// &mut self,
// _: ModelHandle<NotificationStore>,
// event: &NotificationEvent,
// cx: &mut ViewContext<Self>,
// ) {
// match event {
// NotificationEvent::NewNotification { entry } => self.add_toast(entry, cx),
// NotificationEvent::NotificationRemoved { entry }
// | NotificationEvent::NotificationRead { entry } => self.remove_toast(entry.id, cx),
// NotificationEvent::NotificationsUpdated {
// old_range,
// new_count,
// } => {
// self.notification_list.splice(old_range.clone(), *new_count);
// cx.notify();
// }
// }
// }
// fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext<Self>) {
// if self.is_showing_notification(&entry.notification, cx) {
// return;
// }
// let Some(NotificationPresenter { actor, text, .. }) = self.present_notification(entry, cx)
// else {
// return;
// };
// let notification_id = entry.id;
// self.current_notification_toast = Some((
// notification_id,
// cx.spawn(|this, mut cx| async move {
// cx.background().timer(TOAST_DURATION).await;
// this.update(&mut cx, |this, cx| this.remove_toast(notification_id, cx))
// .ok();
// }),
// ));
// self.workspace
// .update(cx, |workspace, cx| {
// workspace.dismiss_notification::<NotificationToast>(0, cx);
// workspace.show_notification(0, cx, |cx| {
// let workspace = cx.weak_handle();
// cx.add_view(|_| NotificationToast {
// notification_id,
// actor,
// text,
// workspace,
// })
// })
// })
// .ok();
// }
// fn remove_toast(&mut self, notification_id: u64, cx: &mut ViewContext<Self>) {
// if let Some((current_id, _)) = &self.current_notification_toast {
// if *current_id == notification_id {
// self.current_notification_toast.take();
// self.workspace
// .update(cx, |workspace, cx| {
// workspace.dismiss_notification::<NotificationToast>(0, cx)
// })
// .ok();
// }
// }
// }
// fn respond_to_notification(
// &mut self,
// notification: Notification,
// response: bool,
// cx: &mut ViewContext<Self>,
// ) {
// self.notification_store.update(cx, |store, cx| {
// store.respond_to_notification(notification, response, cx);
// });
// }
// }
// impl Entity for NotificationPanel {
// type Event = Event;
// }
// impl View for NotificationPanel {
// fn ui_name() -> &'static str {
// "NotificationPanel"
// }
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
// let theme = theme::current(cx);
// let style = &theme.notification_panel;
// let element = if self.client.user_id().is_none() {
// self.render_sign_in_prompt(&theme, cx)
// } else if self.notification_list.item_count() == 0 {
// self.render_empty_state(&theme, cx)
// } else {
// Flex::column()
// .with_child(
// Flex::row()
// .with_child(Label::new("Notifications", style.title.text.clone()))
// .with_child(ui::svg(&style.title_icon).flex_float())
// .align_children_center()
// .contained()
// .with_style(style.title.container)
// .constrained()
// .with_height(style.title_height),
// )
// .with_child(
// List::new(self.notification_list.clone())
// .contained()
// .with_style(style.list)
// .flex(1., true),
// )
// .into_any()
// };
// element
// .contained()
// .with_style(style.container)
// .constrained()
// .with_min_width(150.)
// .into_any()
// }
// fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
// self.has_focus = true;
// }
// fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
// self.has_focus = false;
// }
// }
// impl Panel for NotificationPanel {
// fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
// settings::get::<NotificationPanelSettings>(cx).dock
// }
// fn position_is_valid(&self, position: DockPosition) -> bool {
// matches!(position, DockPosition::Left | DockPosition::Right)
// }
// fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
// settings::update_settings_file::<NotificationPanelSettings>(
// self.fs.clone(),
// cx,
// move |settings| settings.dock = Some(position),
// );
// }
// fn size(&self, cx: &gpui::WindowContext) -> f32 {
// self.width
// .unwrap_or_else(|| settings::get::<NotificationPanelSettings>(cx).default_width)
// }
// fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
// self.width = size;
// self.serialize(cx);
// cx.notify();
// }
// fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
// self.active = active;
// if self.notification_store.read(cx).notification_count() == 0 {
// cx.emit(Event::Dismissed);
// }
// }
// fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
// (settings::get::<NotificationPanelSettings>(cx).button
// && self.notification_store.read(cx).notification_count() > 0)
// .then(|| "icons/bell.svg")
// }
// fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
// (
// "Notification Panel".to_string(),
// Some(Box::new(ToggleFocus)),
// )
// }
// fn icon_label(&self, cx: &WindowContext) -> Option<String> {
// let count = self.notification_store.read(cx).unread_notification_count();
// if count == 0 {
// None
// } else {
// Some(count.to_string())
// }
// }
// fn should_change_position_on_event(event: &Self::Event) -> bool {
// matches!(event, Event::DockPositionChanged)
// }
// fn should_close_on_event(event: &Self::Event) -> bool {
// matches!(event, Event::Dismissed)
// }
// fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
// self.has_focus
// }
// fn is_focus_event(event: &Self::Event) -> bool {
// matches!(event, Event::Focus)
// }
// }
// pub struct NotificationToast {
// notification_id: u64,
// actor: Option<Arc<User>>,
// text: String,
// workspace: WeakViewHandle<Workspace>,
// }
// pub enum ToastEvent {
// Dismiss,
// }
// impl NotificationToast {
// fn focus_notification_panel(&self, cx: &mut AppContext) {
// let workspace = self.workspace.clone();
// let notification_id = self.notification_id;
// cx.defer(move |cx| {
// workspace
// .update(cx, |workspace, cx| {
// if let Some(panel) = workspace.focus_panel::<NotificationPanel>(cx) {
// panel.update(cx, |panel, cx| {
// let store = panel.notification_store.read(cx);
// if let Some(entry) = store.notification_for_id(notification_id) {
// panel.did_click_notification(&entry.clone().notification, cx);
// }
// });
// }
// })
// .ok();
// })
// }
// }
// impl Entity for NotificationToast {
// type Event = ToastEvent;
// }
// impl View for NotificationToast {
// fn ui_name() -> &'static str {
// "ContactNotification"
// }
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
// let user = self.actor.clone();
// let theme = theme::current(cx).clone();
// let theme = &theme.contact_notification;
// MouseEventHandler::new::<Self, _>(0, cx, |_, cx| {
// Flex::row()
// .with_children(user.and_then(|user| {
// Some(
// Image::from_data(user.avatar.clone()?)
// .with_style(theme.header_avatar)
// .aligned()
// .constrained()
// .with_height(
// cx.font_cache()
// .line_height(theme.header_message.text.font_size),
// )
// .aligned()
// .top(),
// )
// }))
// .with_child(
// Text::new(self.text.clone(), theme.header_message.text.clone())
// .contained()
// .with_style(theme.header_message.container)
// .aligned()
// .top()
// .left()
// .flex(1., true),
// )
// .with_child(
// MouseEventHandler::new::<ToastEvent, _>(0, cx, |state, _| {
// let style = theme.dismiss_button.style_for(state);
// Svg::new("icons/x.svg")
// .with_color(style.color)
// .constrained()
// .with_width(style.icon_width)
// .aligned()
// .contained()
// .with_style(style.container)
// .constrained()
// .with_width(style.button_width)
// .with_height(style.button_width)
// })
// .with_cursor_style(CursorStyle::PointingHand)
// .with_padding(Padding::uniform(5.))
// .on_click(MouseButton::Left, move |_, _, cx| {
// cx.emit(ToastEvent::Dismiss)
// })
// .aligned()
// .constrained()
// .with_height(
// cx.font_cache()
// .line_height(theme.header_message.text.font_size),
// )
// .aligned()
// .top()
// .flex_float(),
// )
// .contained()
// })
// .with_cursor_style(CursorStyle::PointingHand)
// .on_click(MouseButton::Left, move |_, this, cx| {
// this.focus_notification_panel(cx);
// cx.emit(ToastEvent::Dismiss);
// })
// .into_any()
// }
// }
// impl workspace::notifications::Notification for NotificationToast {
// fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
// matches!(event, ToastEvent::Dismiss)
// }
// }
// fn format_timestamp(
// mut timestamp: OffsetDateTime,
// mut now: OffsetDateTime,
// local_timezone: UtcOffset,
// ) -> String {
// timestamp = timestamp.to_offset(local_timezone);
// now = now.to_offset(local_timezone);
// let today = now.date();
// let date = timestamp.date();
// if date == today {
// let difference = now - timestamp;
// if difference >= Duration::from_secs(3600) {
// format!("{}h", difference.whole_seconds() / 3600)
// } else if difference >= Duration::from_secs(60) {
// format!("{}m", difference.whole_seconds() / 60)
// } else {
// "just now".to_string()
// }
// } else if date.next_day() == Some(today) {
// format!("yesterday")
// } else {
// format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
// }
// }

View File

@@ -0,0 +1,11 @@
// use gpui::AppContext;
// use std::sync::Arc;
// use workspace::AppState;
// pub mod incoming_call_notification;
// pub mod project_shared_notification;
// pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
// incoming_call_notification::init(app_state, cx);
// project_shared_notification::init(app_state, cx);
// }

View File

@@ -0,0 +1,213 @@
use crate::notification_window_options;
use call::{ActiveCall, IncomingCall};
use client::proto;
use futures::StreamExt;
use gpui::{
elements::*,
geometry::vector::vec2f,
platform::{CursorStyle, MouseButton},
AnyElement, AppContext, Entity, View, ViewContext, WindowHandle,
};
use std::sync::{Arc, Weak};
use util::ResultExt;
use workspace::AppState;
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
let app_state = Arc::downgrade(app_state);
let mut incoming_call = ActiveCall::global(cx).read(cx).incoming();
cx.spawn(|mut cx| async move {
let mut notification_windows: Vec<WindowHandle<IncomingCallNotification>> = Vec::new();
while let Some(incoming_call) = incoming_call.next().await {
for window in notification_windows.drain(..) {
window.remove(&mut cx);
}
if let Some(incoming_call) = incoming_call {
let window_size = cx.read(|cx| {
let theme = &theme::current(cx).incoming_call_notification;
vec2f(theme.window_width, theme.window_height)
});
for screen in cx.platform().screens() {
let window = cx
.add_window(notification_window_options(screen, window_size), |_| {
IncomingCallNotification::new(incoming_call.clone(), app_state.clone())
});
notification_windows.push(window);
}
}
}
})
.detach();
}
#[derive(Clone, PartialEq)]
struct RespondToCall {
accept: bool,
}
pub struct IncomingCallNotification {
call: IncomingCall,
app_state: Weak<AppState>,
}
impl IncomingCallNotification {
pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
Self { call, app_state }
}
fn respond(&mut self, accept: bool, cx: &mut ViewContext<Self>) {
let active_call = ActiveCall::global(cx);
if accept {
let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
let caller_user_id = self.call.calling_user.id;
let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
let app_state = self.app_state.clone();
cx.app_context()
.spawn(|mut cx| async move {
join.await?;
if let Some(project_id) = initial_project_id {
cx.update(|cx| {
if let Some(app_state) = app_state.upgrade() {
workspace::join_remote_project(
project_id,
caller_user_id,
app_state,
cx,
)
.detach_and_log_err(cx);
}
});
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
} else {
active_call.update(cx, |active_call, cx| {
active_call.decline_incoming(cx).log_err();
});
}
}
fn render_caller(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = &theme::current(cx).incoming_call_notification;
let default_project = proto::ParticipantProject::default();
let initial_project = self
.call
.initial_project
.as_ref()
.unwrap_or(&default_project);
Flex::row()
.with_children(self.call.calling_user.avatar.clone().map(|avatar| {
Image::from_data(avatar)
.with_style(theme.caller_avatar)
.aligned()
}))
.with_child(
Flex::column()
.with_child(
Label::new(
self.call.calling_user.github_login.clone(),
theme.caller_username.text.clone(),
)
.contained()
.with_style(theme.caller_username.container),
)
.with_child(
Label::new(
format!(
"is sharing a project in Zed{}",
if initial_project.worktree_root_names.is_empty() {
""
} else {
":"
}
),
theme.caller_message.text.clone(),
)
.contained()
.with_style(theme.caller_message.container),
)
.with_children(if initial_project.worktree_root_names.is_empty() {
None
} else {
Some(
Label::new(
initial_project.worktree_root_names.join(", "),
theme.worktree_roots.text.clone(),
)
.contained()
.with_style(theme.worktree_roots.container),
)
})
.contained()
.with_style(theme.caller_metadata)
.aligned(),
)
.contained()
.with_style(theme.caller_container)
.flex(1., true)
.into_any()
}
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
enum Accept {}
enum Decline {}
let theme = theme::current(cx);
Flex::column()
.with_child(
MouseEventHandler::new::<Accept, _>(0, cx, |_, _| {
let theme = &theme.incoming_call_notification;
Label::new("Accept", theme.accept_button.text.clone())
.aligned()
.contained()
.with_style(theme.accept_button.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, this, cx| {
this.respond(true, cx);
})
.flex(1., true),
)
.with_child(
MouseEventHandler::new::<Decline, _>(0, cx, |_, _| {
let theme = &theme.incoming_call_notification;
Label::new("Decline", theme.decline_button.text.clone())
.aligned()
.contained()
.with_style(theme.decline_button.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, this, cx| {
this.respond(false, cx);
})
.flex(1., true),
)
.constrained()
.with_width(theme.incoming_call_notification.button_width)
.into_any()
}
}
impl Entity for IncomingCallNotification {
type Event = ();
}
impl View for IncomingCallNotification {
fn ui_name() -> &'static str {
"IncomingCallNotification"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let background = theme::current(cx).incoming_call_notification.background;
Flex::row()
.with_child(self.render_caller(cx))
.with_child(self.render_buttons(cx))
.contained()
.with_background_color(background)
.expanded()
.into_any()
}
}

View File

@@ -0,0 +1,217 @@
use crate::notification_window_options;
use call::{room, ActiveCall};
use client::User;
use collections::HashMap;
use gpui::{
elements::*,
geometry::vector::vec2f,
platform::{CursorStyle, MouseButton},
AppContext, Entity, View, ViewContext,
};
use std::sync::{Arc, Weak};
use workspace::AppState;
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
let app_state = Arc::downgrade(app_state);
let active_call = ActiveCall::global(cx);
let mut notification_windows = HashMap::default();
cx.subscribe(&active_call, move |_, event, cx| match event {
room::Event::RemoteProjectShared {
owner,
project_id,
worktree_root_names,
} => {
let theme = &theme::current(cx).project_shared_notification;
let window_size = vec2f(theme.window_width, theme.window_height);
for screen in cx.platform().screens() {
let window =
cx.add_window(notification_window_options(screen, window_size), |_| {
ProjectSharedNotification::new(
owner.clone(),
*project_id,
worktree_root_names.clone(),
app_state.clone(),
)
});
notification_windows
.entry(*project_id)
.or_insert(Vec::new())
.push(window);
}
}
room::Event::RemoteProjectUnshared { project_id }
| room::Event::RemoteProjectJoined { project_id }
| room::Event::RemoteProjectInvitationDiscarded { project_id } => {
if let Some(windows) = notification_windows.remove(&project_id) {
for window in windows {
window.remove(cx);
}
}
}
room::Event::Left => {
for (_, windows) in notification_windows.drain() {
for window in windows {
window.remove(cx);
}
}
}
_ => {}
})
.detach();
}
pub struct ProjectSharedNotification {
project_id: u64,
worktree_root_names: Vec<String>,
owner: Arc<User>,
app_state: Weak<AppState>,
}
impl ProjectSharedNotification {
fn new(
owner: Arc<User>,
project_id: u64,
worktree_root_names: Vec<String>,
app_state: Weak<AppState>,
) -> Self {
Self {
project_id,
worktree_root_names,
owner,
app_state,
}
}
fn join(&mut self, cx: &mut ViewContext<Self>) {
if let Some(app_state) = self.app_state.upgrade() {
workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx)
.detach_and_log_err(cx);
}
}
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
if let Some(active_room) =
ActiveCall::global(cx).read_with(cx, |call, _| call.room().cloned())
{
active_room.update(cx, |_, cx| {
cx.emit(room::Event::RemoteProjectInvitationDiscarded {
project_id: self.project_id,
});
});
}
}
fn render_owner(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = &theme::current(cx).project_shared_notification;
Flex::row()
.with_children(self.owner.avatar.clone().map(|avatar| {
Image::from_data(avatar)
.with_style(theme.owner_avatar)
.aligned()
}))
.with_child(
Flex::column()
.with_child(
Label::new(
self.owner.github_login.clone(),
theme.owner_username.text.clone(),
)
.contained()
.with_style(theme.owner_username.container),
)
.with_child(
Label::new(
format!(
"is sharing a project in Zed{}",
if self.worktree_root_names.is_empty() {
""
} else {
":"
}
),
theme.message.text.clone(),
)
.contained()
.with_style(theme.message.container),
)
.with_children(if self.worktree_root_names.is_empty() {
None
} else {
Some(
Label::new(
self.worktree_root_names.join(", "),
theme.worktree_roots.text.clone(),
)
.contained()
.with_style(theme.worktree_roots.container),
)
})
.contained()
.with_style(theme.owner_metadata)
.aligned(),
)
.contained()
.with_style(theme.owner_container)
.flex(1., true)
.into_any()
}
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
enum Open {}
enum Dismiss {}
let theme = theme::current(cx);
Flex::column()
.with_child(
MouseEventHandler::new::<Open, _>(0, cx, |_, _| {
let theme = &theme.project_shared_notification;
Label::new("Open", theme.open_button.text.clone())
.aligned()
.contained()
.with_style(theme.open_button.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| this.join(cx))
.flex(1., true),
)
.with_child(
MouseEventHandler::new::<Dismiss, _>(0, cx, |_, _| {
let theme = &theme.project_shared_notification;
Label::new("Dismiss", theme.dismiss_button.text.clone())
.aligned()
.contained()
.with_style(theme.dismiss_button.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, this, cx| {
this.dismiss(cx);
})
.flex(1., true),
)
.constrained()
.with_width(theme.project_shared_notification.button_width)
.into_any()
}
}
impl Entity for ProjectSharedNotification {
type Event = ();
}
impl View for ProjectSharedNotification {
fn ui_name() -> &'static str {
"ProjectSharedNotification"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
let background = theme::current(cx).project_shared_notification.background;
Flex::row()
.with_child(self.render_owner(cx))
.with_child(self.render_buttons(cx))
.contained()
.with_background_color(background)
.expanded()
.into_any()
}
}

View File

@@ -0,0 +1,69 @@
use anyhow;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use settings::Settings;
use workspace::dock::DockPosition;
#[derive(Deserialize, Debug)]
pub struct CollaborationPanelSettings {
pub button: bool,
pub dock: DockPosition,
pub default_width: f32,
}
#[derive(Deserialize, Debug)]
pub struct ChatPanelSettings {
pub button: bool,
pub dock: DockPosition,
pub default_width: f32,
}
#[derive(Deserialize, Debug)]
pub struct NotificationPanelSettings {
pub button: bool,
pub dock: DockPosition,
pub default_width: f32,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct PanelSettingsContent {
pub button: Option<bool>,
pub dock: Option<DockPosition>,
pub default_width: Option<f32>,
}
impl Settings for CollaborationPanelSettings {
const KEY: Option<&'static str> = Some("collaboration_panel");
type FileContent = PanelSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}
impl Settings for ChatPanelSettings {
const KEY: Option<&'static str> = Some("chat_panel");
type FileContent = PanelSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}
impl Settings for NotificationPanelSettings {
const KEY: Option<&'static str> = Some("notification_panel");
type FileContent = PanelSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}

View File

@@ -1,9 +1,8 @@
use collections::{CommandPaletteFilter, HashMap};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
actions, div, prelude::*, Action, AppContext, Component, Div, EventEmitter, FocusHandle,
Keystroke, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView,
WindowContext,
actions, div, prelude::*, Action, AppContext, Component, Dismiss, Div, FocusHandle, Keystroke,
ManagedView, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView,
};
use picker::{Picker, PickerDelegate};
use std::{
@@ -16,7 +15,7 @@ use util::{
channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
ResultExt,
};
use workspace::{Modal, ModalEvent, Workspace};
use workspace::Workspace;
use zed_actions::OpenZedURL;
actions!(Toggle);
@@ -47,7 +46,7 @@ impl CommandPalette {
.available_actions()
.into_iter()
.filter_map(|action| {
let name = action.name();
let name = gpui::remove_the_2(action.name());
let namespace = name.split("::").next().unwrap_or("malformed action name");
if filter.is_some_and(|f| f.filtered_namespaces.contains(namespace)) {
return None;
@@ -69,10 +68,9 @@ impl CommandPalette {
}
}
impl EventEmitter<ModalEvent> for CommandPalette {}
impl Modal for CommandPalette {
fn focus(&self, cx: &mut WindowContext) {
self.picker.update(cx, |picker, cx| picker.focus(cx));
impl ManagedView for CommandPalette {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
@@ -267,7 +265,7 @@ impl PickerDelegate for CommandPaletteDelegate {
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.command_palette
.update(cx, |_, cx| cx.emit(ModalEvent::Dismissed))
.update(cx, |_, cx| cx.emit(Dismiss))
.log_err();
}
@@ -385,8 +383,7 @@ mod tests {
let app_state = init_test(cx);
let project = Project::test(app_state.fs.clone(), [], cx).await;
let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
let cx = &mut cx;
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
let editor = cx.build_view(|cx| {
let mut editor = Editor::single_line(cx);
@@ -403,7 +400,7 @@ mod tests {
let palette = workspace.update(cx, |workspace, cx| {
workspace
.current_modal::<CommandPalette>(cx)
.active_modal::<CommandPalette>(cx)
.unwrap()
.read(cx)
.picker
@@ -417,7 +414,7 @@ mod tests {
assert!(is_sorted(&palette.delegate.commands));
});
cx.simulate_keystrokes("b c k s p");
cx.simulate_input("bcksp");
palette.update(cx, |palette, _| {
assert_eq!(palette.delegate.matches[0].string, "editor: backspace");
@@ -426,7 +423,7 @@ mod tests {
cx.simulate_keystrokes("enter");
workspace.update(cx, |workspace, cx| {
assert!(workspace.current_modal::<CommandPalette>(cx).is_none());
assert!(workspace.active_modal::<CommandPalette>(cx).is_none());
assert_eq!(editor.read(cx).text(cx), "ab")
});
@@ -439,11 +436,11 @@ mod tests {
});
cx.simulate_keystrokes("cmd-shift-p");
cx.simulate_keystrokes("b c k s p");
cx.simulate_input("bcksp");
let palette = workspace.update(cx, |workspace, cx| {
workspace
.current_modal::<CommandPalette>(cx)
.active_modal::<CommandPalette>(cx)
.unwrap()
.read(cx)
.picker
@@ -457,7 +454,7 @@ mod tests {
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
cx.update(|cx| {
let app_state = AppState::test(cx);
theme::init(cx);
theme::init(theme::LoadThemes::JustBase, cx);
language::init(cx);
editor::init(cx);
workspace::init(app_state.clone(), cx);

View File

@@ -1051,17 +1051,15 @@ mod tests {
);
// Ensure updates to the file are reflected in the LSP.
buffer_1
.update(cx, |buffer, cx| {
buffer.file_updated(
Arc::new(File {
abs_path: "/root/child/buffer-1".into(),
path: Path::new("child/buffer-1").into(),
}),
cx,
)
})
.await;
buffer_1.update(cx, |buffer, cx| {
buffer.file_updated(
Arc::new(File {
abs_path: "/root/child/buffer-1".into(),
path: Path::new("child/buffer-1").into(),
}),
cx,
)
});
assert_eq!(
lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
.await,

View File

@@ -13,7 +13,8 @@ pub use block_map::{BlockMap, BlockPoint};
use collections::{BTreeMap, HashMap, HashSet};
use fold_map::FoldMap;
use gpui::{
Font, FontId, HighlightStyle, Hsla, Line, Model, ModelContext, Pixels, TextRun, UnderlineStyle,
Font, FontId, HighlightStyle, Hsla, LineLayout, Model, ModelContext, Pixels, ShapedLine,
TextRun, UnderlineStyle, WrappedLine,
};
use inlay_map::InlayMap;
use language::{
@@ -31,7 +32,7 @@ pub use block_map::{
BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
};
pub use self::fold_map::FoldPoint;
pub use self::fold_map::{Fold, FoldPoint};
pub use self::inlay_map::{Inlay, InlayOffset, InlayPoint};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -124,7 +125,7 @@ impl DisplayMap {
self.fold(
other
.folds_in_range(0..other.buffer_snapshot.len())
.map(|fold| fold.to_offset(&other.buffer_snapshot)),
.map(|fold| fold.range.to_offset(&other.buffer_snapshot)),
cx,
);
}
@@ -561,7 +562,7 @@ impl DisplaySnapshot {
})
}
pub fn lay_out_line_for_row(
pub fn layout_row(
&self,
display_row: u32,
TextLayoutDetails {
@@ -569,7 +570,7 @@ impl DisplaySnapshot {
editor_style,
rem_size,
}: &TextLayoutDetails,
) -> Line {
) -> Arc<LineLayout> {
let mut runs = Vec::new();
let mut line = String::new();
@@ -598,29 +599,27 @@ impl DisplaySnapshot {
let font_size = editor_style.text.font_size.to_pixels(*rem_size);
text_system
.layout_text(&line, font_size, &runs, None)
.unwrap()
.pop()
.unwrap()
.layout_line(&line, font_size, &runs)
.expect("we expect the font to be loaded because it's rendered by the editor")
}
pub fn x_for_point(
pub fn x_for_display_point(
&self,
display_point: DisplayPoint,
text_layout_details: &TextLayoutDetails,
) -> Pixels {
let layout_line = self.lay_out_line_for_row(display_point.row(), text_layout_details);
layout_line.x_for_index(display_point.column() as usize)
let line = self.layout_row(display_point.row(), text_layout_details);
line.x_for_index(display_point.column() as usize)
}
pub fn column_for_x(
pub fn display_column_for_x(
&self,
display_row: u32,
x_coordinate: Pixels,
text_layout_details: &TextLayoutDetails,
x: Pixels,
details: &TextLayoutDetails,
) -> u32 {
let layout_line = self.lay_out_line_for_row(display_row, text_layout_details);
layout_line.closest_index_for_x(x_coordinate) as u32
let layout_line = self.layout_row(display_row, details);
layout_line.closest_index_for_x(x) as u32
}
pub fn chars_at(
@@ -723,7 +722,7 @@ impl DisplaySnapshot {
DisplayPoint(point)
}
pub fn folds_in_range<T>(&self, range: Range<T>) -> impl Iterator<Item = &Range<Anchor>>
pub fn folds_in_range<T>(&self, range: Range<T>) -> impl Iterator<Item = &Fold>
where
T: ToOffset,
{

View File

@@ -3,15 +3,16 @@ use super::{
Highlights,
};
use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset};
use gpui::{HighlightStyle, Hsla};
use gpui::{ElementId, HighlightStyle, Hsla};
use language::{Chunk, Edit, Point, TextSummary};
use std::{
any::TypeId,
cmp::{self, Ordering},
iter,
ops::{Add, AddAssign, Range, Sub},
ops::{Add, AddAssign, Deref, DerefMut, Range, Sub},
};
use sum_tree::{Bias, Cursor, FilterCursor, SumTree};
use util::post_inc;
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct FoldPoint(pub Point);
@@ -90,12 +91,16 @@ impl<'a> FoldMapWriter<'a> {
}
// For now, ignore any ranges that span an excerpt boundary.
let fold = Fold(buffer.anchor_after(range.start)..buffer.anchor_before(range.end));
if fold.0.start.excerpt_id != fold.0.end.excerpt_id {
let fold_range =
FoldRange(buffer.anchor_after(range.start)..buffer.anchor_before(range.end));
if fold_range.0.start.excerpt_id != fold_range.0.end.excerpt_id {
continue;
}
folds.push(fold);
folds.push(Fold {
id: FoldId(post_inc(&mut self.0.next_fold_id.0)),
range: fold_range,
});
let inlay_range =
snapshot.to_inlay_offset(range.start)..snapshot.to_inlay_offset(range.end);
@@ -106,13 +111,13 @@ impl<'a> FoldMapWriter<'a> {
}
let buffer = &snapshot.buffer;
folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(a, b, buffer));
folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(&a.range, &b.range, buffer));
self.0.snapshot.folds = {
let mut new_tree = SumTree::new();
let mut cursor = self.0.snapshot.folds.cursor::<Fold>();
let mut cursor = self.0.snapshot.folds.cursor::<FoldRange>();
for fold in folds {
new_tree.append(cursor.slice(&fold, Bias::Right, buffer), buffer);
new_tree.append(cursor.slice(&fold.range, Bias::Right, buffer), buffer);
new_tree.push(fold, buffer);
}
new_tree.append(cursor.suffix(buffer), buffer);
@@ -138,7 +143,8 @@ impl<'a> FoldMapWriter<'a> {
let mut folds_cursor =
intersecting_folds(&snapshot, &self.0.snapshot.folds, range, inclusive);
while let Some(fold) = folds_cursor.item() {
let offset_range = fold.0.start.to_offset(buffer)..fold.0.end.to_offset(buffer);
let offset_range =
fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer);
if offset_range.end > offset_range.start {
let inlay_range = snapshot.to_inlay_offset(offset_range.start)
..snapshot.to_inlay_offset(offset_range.end);
@@ -175,6 +181,7 @@ impl<'a> FoldMapWriter<'a> {
pub struct FoldMap {
snapshot: FoldSnapshot,
ellipses_color: Option<Hsla>,
next_fold_id: FoldId,
}
impl FoldMap {
@@ -197,6 +204,7 @@ impl FoldMap {
ellipses_color: None,
},
ellipses_color: None,
next_fold_id: FoldId::default(),
};
let snapshot = this.snapshot.clone();
(this, snapshot)
@@ -242,8 +250,8 @@ impl FoldMap {
while let Some(fold) = folds.next() {
if let Some(next_fold) = folds.peek() {
let comparison = fold
.0
.cmp(&next_fold.0, &self.snapshot.inlay_snapshot.buffer);
.range
.cmp(&next_fold.range, &self.snapshot.inlay_snapshot.buffer);
assert!(comparison.is_le());
}
}
@@ -304,9 +312,9 @@ impl FoldMap {
let anchor = inlay_snapshot
.buffer
.anchor_before(inlay_snapshot.to_buffer_offset(edit.new.start));
let mut folds_cursor = self.snapshot.folds.cursor::<Fold>();
let mut folds_cursor = self.snapshot.folds.cursor::<FoldRange>();
folds_cursor.seek(
&Fold(anchor..Anchor::max()),
&FoldRange(anchor..Anchor::max()),
Bias::Left,
&inlay_snapshot.buffer,
);
@@ -315,8 +323,8 @@ impl FoldMap {
let inlay_snapshot = &inlay_snapshot;
move || {
let item = folds_cursor.item().map(|f| {
let buffer_start = f.0.start.to_offset(&inlay_snapshot.buffer);
let buffer_end = f.0.end.to_offset(&inlay_snapshot.buffer);
let buffer_start = f.range.start.to_offset(&inlay_snapshot.buffer);
let buffer_end = f.range.end.to_offset(&inlay_snapshot.buffer);
inlay_snapshot.to_inlay_offset(buffer_start)
..inlay_snapshot.to_inlay_offset(buffer_end)
});
@@ -596,13 +604,13 @@ impl FoldSnapshot {
self.transforms.summary().output.longest_row
}
pub fn folds_in_range<T>(&self, range: Range<T>) -> impl Iterator<Item = &Range<Anchor>>
pub fn folds_in_range<T>(&self, range: Range<T>) -> impl Iterator<Item = &Fold>
where
T: ToOffset,
{
let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false);
iter::from_fn(move || {
let item = folds.item().map(|f| &f.0);
let item = folds.item();
folds.next(&self.inlay_snapshot.buffer);
item
})
@@ -830,10 +838,39 @@ impl sum_tree::Summary for TransformSummary {
}
}
#[derive(Clone, Debug)]
struct Fold(Range<Anchor>);
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
pub struct FoldId(usize);
impl Default for Fold {
impl Into<ElementId> for FoldId {
fn into(self) -> ElementId {
ElementId::Integer(self.0)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Fold {
pub id: FoldId,
pub range: FoldRange,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FoldRange(Range<Anchor>);
impl Deref for FoldRange {
type Target = Range<Anchor>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for FoldRange {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Default for FoldRange {
fn default() -> Self {
Self(Anchor::min()..Anchor::max())
}
@@ -844,17 +881,17 @@ impl sum_tree::Item for Fold {
fn summary(&self) -> Self::Summary {
FoldSummary {
start: self.0.start.clone(),
end: self.0.end.clone(),
min_start: self.0.start.clone(),
max_end: self.0.end.clone(),
start: self.range.start.clone(),
end: self.range.end.clone(),
min_start: self.range.start.clone(),
max_end: self.range.end.clone(),
count: 1,
}
}
}
#[derive(Clone, Debug)]
struct FoldSummary {
pub struct FoldSummary {
start: Anchor,
end: Anchor,
min_start: Anchor,
@@ -900,14 +937,14 @@ impl sum_tree::Summary for FoldSummary {
}
}
impl<'a> sum_tree::Dimension<'a, FoldSummary> for Fold {
impl<'a> sum_tree::Dimension<'a, FoldSummary> for FoldRange {
fn add_summary(&mut self, summary: &'a FoldSummary, _: &MultiBufferSnapshot) {
self.0.start = summary.start.clone();
self.0.end = summary.end.clone();
}
}
impl<'a> sum_tree::SeekTarget<'a, FoldSummary, Fold> for Fold {
impl<'a> sum_tree::SeekTarget<'a, FoldSummary, FoldRange> for FoldRange {
fn cmp(&self, other: &Self, buffer: &MultiBufferSnapshot) -> Ordering {
self.0.cmp(&other.0, buffer)
}
@@ -1321,7 +1358,10 @@ mod tests {
let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
let fold_ranges = snapshot
.folds_in_range(Point::new(1, 0)..Point::new(1, 3))
.map(|fold| fold.start.to_point(&buffer_snapshot)..fold.end.to_point(&buffer_snapshot))
.map(|fold| {
fold.range.start.to_point(&buffer_snapshot)
..fold.range.end.to_point(&buffer_snapshot)
})
.collect::<Vec<_>>();
assert_eq!(
fold_ranges,
@@ -1553,10 +1593,9 @@ mod tests {
.filter(|fold| {
let start = buffer_snapshot.anchor_before(start);
let end = buffer_snapshot.anchor_after(end);
start.cmp(&fold.0.end, &buffer_snapshot) == Ordering::Less
&& end.cmp(&fold.0.start, &buffer_snapshot) == Ordering::Greater
start.cmp(&fold.range.end, &buffer_snapshot) == Ordering::Less
&& end.cmp(&fold.range.start, &buffer_snapshot) == Ordering::Greater
})
.map(|fold| fold.0)
.collect::<Vec<_>>();
assert_eq!(
@@ -1639,10 +1678,10 @@ mod tests {
let buffer = &inlay_snapshot.buffer;
let mut folds = self.snapshot.folds.items(buffer);
// Ensure sorting doesn't change how folds get merged and displayed.
folds.sort_by(|a, b| a.0.cmp(&b.0, buffer));
folds.sort_by(|a, b| a.range.cmp(&b.range, buffer));
let mut fold_ranges = folds
.iter()
.map(|fold| fold.0.start.to_offset(buffer)..fold.0.end.to_offset(buffer))
.map(|fold| fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer))
.peekable();
let mut merged_ranges = Vec::new();

View File

@@ -1891,6 +1891,6 @@ mod tests {
fn init_test(cx: &mut AppContext) {
let store = SettingsStore::test(cx);
cx.set_global(store);
theme::init(cx);
theme::init(theme::LoadThemes::JustBase, cx);
}
}

View File

@@ -39,12 +39,12 @@ use futures::FutureExt;
use fuzzy::{StringMatch, StringMatchCandidate};
use git::diff_hunk_to_display;
use gpui::{
action, actions, div, point, prelude::*, px, relative, rems, render_view, size, uniform_list,
AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem,
Component, Context, EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight,
HighlightStyle, Hsla, InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels,
Render, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext,
VisualContext, WeakView, WindowContext,
actions, div, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement,
AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context,
EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle,
Hsla, InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels, Render, Styled,
Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext,
WeakView, WindowContext,
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState};
@@ -97,7 +97,7 @@ use text::{OffsetUtf16, Rope};
use theme::{
ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings,
};
use ui::{v_stack, HighlightedLabel, IconButton, StyledExt, TextTooltip};
use ui::{v_stack, HighlightedLabel, IconButton, StyledExt, Tooltip};
use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::{
item::{ItemEvent, ItemHandle},
@@ -180,78 +180,78 @@ pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
// // .with_soft_wrap(true)
// }
#[action]
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
pub struct SelectNext {
#[serde(default)]
pub replace_newest: bool,
}
#[action]
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
pub struct SelectPrevious {
#[serde(default)]
pub replace_newest: bool,
}
#[action]
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
pub struct SelectAllMatches {
#[serde(default)]
pub replace_newest: bool,
}
#[action]
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
pub struct SelectToBeginningOfLine {
#[serde(default)]
stop_at_soft_wraps: bool,
}
#[action]
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
pub struct MovePageUp {
#[serde(default)]
center_cursor: bool,
}
#[action]
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
pub struct MovePageDown {
#[serde(default)]
center_cursor: bool,
}
#[action]
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
pub struct SelectToEndOfLine {
#[serde(default)]
stop_at_soft_wraps: bool,
}
#[action]
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
pub struct ToggleCodeActions {
#[serde(default)]
pub deployed_from_indicator: bool,
}
#[action]
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
pub struct ConfirmCompletion {
#[serde(default)]
pub item_ix: Option<usize>,
}
#[action]
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
pub struct ConfirmCodeAction {
#[serde(default)]
pub item_ix: Option<usize>,
}
#[action]
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
pub struct ToggleComments {
#[serde(default)]
pub advance_downwards: bool,
}
#[action]
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
pub struct FoldAt {
pub buffer_row: u32,
}
#[action]
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
pub struct UnfoldAt {
pub buffer_row: u32,
}
@@ -4372,69 +4372,42 @@ impl Editor {
}
}
// pub fn render_fold_indicators(
// &self,
// fold_data: Vec<Option<(FoldStatus, u32, bool)>>,
// style: &EditorStyle,
// gutter_hovered: bool,
// line_height: f32,
// gutter_margin: f32,
// cx: &mut ViewContext<Self>,
// ) -> Vec<Option<AnyElement<Self>>> {
// enum FoldIndicators {}
// let style = style.folds.clone();
// fold_data
// .iter()
// .enumerate()
// .map(|(ix, fold_data)| {
// fold_data
// .map(|(fold_status, buffer_row, active)| {
// (active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| {
// MouseEventHandler::new::<FoldIndicators, _>(
// ix as usize,
// cx,
// |mouse_state, _| {
// Svg::new(match fold_status {
// FoldStatus::Folded => style.folded_icon.clone(),
// FoldStatus::Foldable => style.foldable_icon.clone(),
// })
// .with_color(
// style
// .indicator
// .in_state(fold_status == FoldStatus::Folded)
// .style_for(mouse_state)
// .color,
// )
// .constrained()
// .with_width(gutter_margin * style.icon_margin_scale)
// .aligned()
// .constrained()
// .with_height(line_height)
// .with_width(gutter_margin)
// .aligned()
// },
// )
// .with_cursor_style(CursorStyle::PointingHand)
// .with_padding(Padding::uniform(3.))
// .on_click(MouseButton::Left, {
// move |_, editor, cx| match fold_status {
// FoldStatus::Folded => {
// editor.unfold_at(&UnfoldAt { buffer_row }, cx);
// }
// FoldStatus::Foldable => {
// editor.fold_at(&FoldAt { buffer_row }, cx);
// }
// }
// })
// .into_any()
// })
// })
// .flatten()
// })
// .collect()
// }
pub fn render_fold_indicators(
&self,
fold_data: Vec<Option<(FoldStatus, u32, bool)>>,
style: &EditorStyle,
gutter_hovered: bool,
line_height: Pixels,
gutter_margin: Pixels,
cx: &mut ViewContext<Self>,
) -> Vec<Option<AnyElement<Self>>> {
fold_data
.iter()
.enumerate()
.map(|(ix, fold_data)| {
fold_data
.map(|(fold_status, buffer_row, active)| {
(active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| {
let icon = match fold_status {
FoldStatus::Folded => ui::Icon::ChevronRight,
FoldStatus::Foldable => ui::Icon::ChevronDown,
};
IconButton::new(ix as usize, icon)
.on_click(move |editor: &mut Editor, cx| match fold_status {
FoldStatus::Folded => {
editor.unfold_at(&UnfoldAt { buffer_row }, cx);
}
FoldStatus::Foldable => {
editor.fold_at(&FoldAt { buffer_row }, cx);
}
})
.render()
})
})
.flatten()
})
.collect()
}
pub fn context_menu_visible(&self) -> bool {
self.context_menu
@@ -5330,8 +5303,8 @@ impl Editor {
buffer.anchor_before(range_to_move.start)
..buffer.anchor_after(range_to_move.end),
) {
let mut start = fold.start.to_point(&buffer);
let mut end = fold.end.to_point(&buffer);
let mut start = fold.range.start.to_point(&buffer);
let mut end = fold.range.end.to_point(&buffer);
start.row -= row_delta;
end.row -= row_delta;
refold_ranges.push(start..end);
@@ -5421,8 +5394,8 @@ impl Editor {
buffer.anchor_before(range_to_move.start)
..buffer.anchor_after(range_to_move.end),
) {
let mut start = fold.start.to_point(&buffer);
let mut end = fold.end.to_point(&buffer);
let mut start = fold.range.start.to_point(&buffer);
let mut end = fold.range.end.to_point(&buffer);
start.row += row_delta;
end.row += row_delta;
refold_ranges.push(start..end);
@@ -5472,7 +5445,9 @@ impl Editor {
*head.column_mut() += 1;
head = display_map.clip_point(head, Bias::Right);
let goal = SelectionGoal::HorizontalPosition(
display_map.x_for_point(head, &text_layout_details).into(),
display_map
.x_for_display_point(head, &text_layout_details)
.into(),
);
selection.collapse_to(head, goal);
@@ -6418,8 +6393,8 @@ impl Editor {
let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone();
let range = oldest_selection.display_range(&display_map).sorted();
let start_x = display_map.x_for_point(range.start, &text_layout_details);
let end_x = display_map.x_for_point(range.end, &text_layout_details);
let start_x = display_map.x_for_display_point(range.start, &text_layout_details);
let end_x = display_map.x_for_display_point(range.end, &text_layout_details);
let positions = start_x.min(end_x)..start_x.max(end_x);
selections.clear();
@@ -6458,15 +6433,16 @@ impl Editor {
let range = selection.display_range(&display_map).sorted();
debug_assert_eq!(range.start.row(), range.end.row());
let mut row = range.start.row();
let positions = if let SelectionGoal::HorizontalRange { start, end } =
selection.goal
{
px(start)..px(end)
} else {
let start_x = display_map.x_for_point(range.start, &text_layout_details);
let end_x = display_map.x_for_point(range.end, &text_layout_details);
start_x.min(end_x)..start_x.max(end_x)
};
let positions =
if let SelectionGoal::HorizontalRange { start, end } = selection.goal {
px(start)..px(end)
} else {
let start_x =
display_map.x_for_display_point(range.start, &text_layout_details);
let end_x =
display_map.x_for_display_point(range.end, &text_layout_details);
start_x.min(end_x)..start_x.max(end_x)
};
while row != end_row {
if above {
@@ -7019,7 +6995,7 @@ impl Editor {
let display_point = point.to_display_point(display_snapshot);
let goal = SelectionGoal::HorizontalPosition(
display_snapshot
.x_for_point(display_point, &text_layout_details)
.x_for_display_point(display_point, &text_layout_details)
.into(),
);
(display_point, goal)
@@ -7804,25 +7780,18 @@ impl Editor {
}
div()
.pl(cx.anchor_x)
.child(render_view(
.child(rename_editor.render_with(EditorElement::new(
&rename_editor,
EditorElement::new(
&rename_editor,
EditorStyle {
background: cx.theme().system().transparent,
local_player: cx.editor_style.local_player,
text: text_style,
scrollbar_width: cx
.editor_style
.scrollbar_width,
syntax: cx.editor_style.syntax.clone(),
diagnostic_style: cx
.editor_style
.diagnostic_style
.clone(),
},
),
))
EditorStyle {
background: cx.theme().system().transparent,
local_player: cx.editor_style.local_player,
text: text_style,
scrollbar_width: cx.editor_style.scrollbar_width,
syntax: cx.editor_style.syntax.clone(),
diagnostic_style:
cx.editor_style.diagnostic_style.clone(),
},
)))
.render()
}
}),
@@ -9401,24 +9370,28 @@ pub struct EditorReleased(pub WeakView<Editor>);
//
impl EventEmitter<Event> for Editor {}
impl FocusableView for Editor {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for Editor {
type Element = EditorElement;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let settings = ThemeSettings::get_global(cx);
let text_style = match self.mode {
EditorMode::SingleLine => {
TextStyle {
color: cx.theme().colors().text,
font_family: settings.ui_font.family.clone(), // todo!()
font_features: settings.ui_font.features,
font_size: rems(0.875).into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
line_height: relative(1.3).into(), // TODO relative(settings.buffer_line_height.value()),
underline: None,
}
}
EditorMode::SingleLine => TextStyle {
color: cx.theme().colors().text,
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features,
font_size: rems(0.875).into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
line_height: relative(1.).into(),
underline: None,
},
EditorMode::AutoHeight { max_lines } => todo!(),
@@ -9789,7 +9762,8 @@ impl InputHandler for Editor {
let scroll_left = scroll_position.x * em_width;
let start = OffsetUtf16(range_utf16.start).to_display_point(&snapshot);
let x = snapshot.x_for_point(start, &text_layout_details) - scroll_left + self.gutter_width;
let x = snapshot.x_for_display_point(start, &text_layout_details) - scroll_left
+ self.gutter_width;
let y = line_height * (start.row() as f32 - scroll_position.y);
Some(Bounds {
@@ -10019,7 +9993,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
.on_click(move |_, _, cx| {
cx.write_to_clipboard(ClipboardItem::new(message.clone()));
})
.tooltip(|_, cx| cx.build_view(|cx| TextTooltip::new("Copy diagnostic message")))
.tooltip(|_, cx| Tooltip::text("Copy diagnostic message", cx))
.render()
})
}

View File

@@ -3851,12 +3851,12 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
});
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
view.condition::<crate::Event>(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
.await;
view.update(&mut cx, |view, cx| {
view.update(cx, |view, cx| {
view.change_selections(None, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
@@ -3867,7 +3867,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
});
assert_eq!(
view.update(&mut cx, |view, cx| { view.selections.display_ranges(cx) }),
view.update(cx, |view, cx| { view.selections.display_ranges(cx) }),
&[
DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
@@ -3875,50 +3875,50 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
]
);
view.update(&mut cx, |view, cx| {
view.update(cx, |view, cx| {
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
});
assert_eq!(
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
&[
DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
]
);
view.update(&mut cx, |view, cx| {
view.update(cx, |view, cx| {
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
});
assert_eq!(
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
&[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
);
// Trying to expand the selected syntax node one more time has no effect.
view.update(&mut cx, |view, cx| {
view.update(cx, |view, cx| {
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
});
assert_eq!(
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
&[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
);
view.update(&mut cx, |view, cx| {
view.update(cx, |view, cx| {
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
});
assert_eq!(
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
&[
DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
]
);
view.update(&mut cx, |view, cx| {
view.update(cx, |view, cx| {
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
});
assert_eq!(
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
&[
DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
@@ -3926,11 +3926,11 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
]
);
view.update(&mut cx, |view, cx| {
view.update(cx, |view, cx| {
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
});
assert_eq!(
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
&[
DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
@@ -3939,11 +3939,11 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
);
// Trying to shrink the selected syntax node one more time has no effect.
view.update(&mut cx, |view, cx| {
view.update(cx, |view, cx| {
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
});
assert_eq!(
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
&[
DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
@@ -3953,7 +3953,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
// Ensure that we keep expanding the selection if the larger selection starts or ends within
// a fold.
view.update(&mut cx, |view, cx| {
view.update(cx, |view, cx| {
view.fold_ranges(
vec![
Point::new(0, 21)..Point::new(0, 24),
@@ -3965,7 +3965,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
});
assert_eq!(
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
&[
DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
@@ -4017,8 +4017,7 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
});
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
let cx = &mut cx;
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
editor
.condition::<crate::Event>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
.await;
@@ -4583,8 +4582,7 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
});
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
let cx = &mut cx;
let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
view.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
.await;
@@ -4734,8 +4732,7 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
});
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
let cx = &mut cx;
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
editor
.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
.await;
@@ -4957,8 +4954,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
let fake_server = fake_servers.next().await.unwrap();
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
let cx = &mut cx;
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
assert!(cx.read(|cx| editor.is_dirty(cx)));
@@ -5077,8 +5073,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
let fake_server = fake_servers.next().await.unwrap();
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
let cx = &mut cx;
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
assert!(cx.read(|cx| editor.is_dirty(cx)));
@@ -5205,8 +5200,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
let fake_server = fake_servers.next().await.unwrap();
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
let cx = &mut cx;
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
let format = editor
@@ -5993,8 +5987,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
multibuffer
});
let (view, mut cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
let cx = &mut cx;
let (view, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
view.update(cx, |view, cx| {
assert_eq!(view.text(cx), "aaaa\nbbbb");
view.change_selections(None, cx, |s| {
@@ -6064,8 +6057,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
multibuffer
});
let (view, mut cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
let cx = &mut cx;
let (view, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
view.update(cx, |view, cx| {
let (expected_text, selection_ranges) = marked_text_ranges(
indoc! {"
@@ -6302,8 +6294,7 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
});
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
let cx = &mut cx;
let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
view.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
.await;
@@ -8112,8 +8103,7 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
let buffer_text = "one\ntwo\nthree\n";
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
let cx = &mut cx;
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx));
editor
@@ -8287,7 +8277,7 @@ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsC
cx.update(|cx| {
let store = SettingsStore::test(cx);
cx.set_global(store);
theme::init(cx);
theme::init(theme::LoadThemes::JustBase, cx);
client::init_settings(cx);
language::init(cx);
Project::init_settings(cx);

File diff suppressed because it is too large Load Diff

View File

@@ -60,8 +60,8 @@ pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) ->
let folds_end = Point::new(hunk.buffer_range.end + 2, 0);
let folds_range = folds_start..folds_end;
let containing_fold = snapshot.folds_in_range(folds_range).find(|fold_range| {
let fold_point_range = fold_range.to_point(&snapshot.buffer_snapshot);
let containing_fold = snapshot.folds_in_range(folds_range).find(|fold| {
let fold_point_range = fold.range.to_point(&snapshot.buffer_snapshot);
let fold_point_range = fold_point_range.start..=fold_point_range.end;
let folded_start = fold_point_range.contains(&hunk_start_point);
@@ -72,7 +72,7 @@ pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) ->
});
if let Some(fold) = containing_fold {
let row = fold.start.to_display_point(snapshot).row();
let row = fold.range.start.to_display_point(snapshot).row();
DisplayDiffHunk::Folded { display_row: row }
} else {
let start = hunk_start_point.to_display_point(snapshot).row();

View File

@@ -3179,7 +3179,7 @@ all hints should be invalidated and requeried for all of its visible excerpts"
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
theme::init(cx);
theme::init(theme::LoadThemes::JustBase, cx);
client::init_settings(cx);
language::init(cx);
Project::init_settings(cx);

View File

@@ -527,10 +527,6 @@ fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor)
}
impl Item for Editor {
fn focus_handle(&self) -> FocusHandle {
self.focus_handle.clone()
}
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
todo!();
// if let Ok(data) = data.downcast::<NavigationData>() {
@@ -801,7 +797,7 @@ impl Item for Editor {
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
let workspace_id = workspace.database_id();
let item_id = cx.view().entity_id().as_u64() as ItemId;
let item_id = cx.view().item_id().as_u64() as ItemId;
self.workspace = Some((workspace.weak_handle(), workspace.database_id()));
fn serialize(
@@ -832,7 +828,7 @@ impl Item for Editor {
serialize(
buffer,
*workspace_id,
cx.view().entity_id().as_u64() as ItemId,
cx.view().item_id().as_u64() as ItemId,
cx,
);
}

View File

@@ -98,7 +98,7 @@ pub fn up_by_rows(
SelectionGoal::HorizontalPosition(x) => x.into(), // todo!("Can the fields in SelectionGoal by Pixels? We should extract a geometry crate and depend on that.")
SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
SelectionGoal::HorizontalRange { end, .. } => end.into(),
_ => map.x_for_point(start, text_layout_details),
_ => map.x_for_display_point(start, text_layout_details),
};
let prev_row = start.row().saturating_sub(row_count);
@@ -107,7 +107,7 @@ pub fn up_by_rows(
Bias::Left,
);
if point.row() < start.row() {
*point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
*point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details)
} else if preserve_column_at_start {
return (start, goal);
} else {
@@ -137,18 +137,18 @@ pub fn down_by_rows(
SelectionGoal::HorizontalPosition(x) => x.into(),
SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
SelectionGoal::HorizontalRange { end, .. } => end.into(),
_ => map.x_for_point(start, text_layout_details),
_ => map.x_for_display_point(start, text_layout_details),
};
let new_row = start.row() + row_count;
let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
if point.row() > start.row() {
*point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
*point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details)
} else if preserve_column_at_end {
return (start, goal);
} else {
point = map.max_point();
goal_x = map.x_for_point(point, text_layout_details)
goal_x = map.x_for_display_point(point, text_layout_details)
}
let mut clipped_point = map.clip_point(point, Bias::Right);

View File

@@ -426,7 +426,7 @@ impl Editor {
pub fn read_scroll_position_from_db(
&mut self,
item_id: usize,
item_id: u64,
workspace_id: WorkspaceId,
cx: &mut ViewContext<Editor>,
) {

View File

@@ -313,14 +313,14 @@ impl SelectionsCollection {
let is_empty = positions.start == positions.end;
let line_len = display_map.line_len(row);
let layed_out_line = display_map.lay_out_line_for_row(row, &text_layout_details);
let line = display_map.layout_row(row, &text_layout_details);
dbg!("****START COL****");
let start_col = layed_out_line.closest_index_for_x(positions.start) as u32;
if start_col < line_len || (is_empty && positions.start == layed_out_line.width) {
let start_col = line.closest_index_for_x(positions.start) as u32;
if start_col < line_len || (is_empty && positions.start == line.width) {
let start = DisplayPoint::new(row, start_col);
dbg!("****END COL****");
let end_col = layed_out_line.closest_index_for_x(positions.end) as u32;
let end_col = line.closest_index_for_x(positions.end) as u32;
let end = DisplayPoint::new(row, end_col);
dbg!(start_col, end_col);

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,13 @@
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
use gpui::{
actions, div, prelude::*, AppContext, Div, EventEmitter, ParentComponent, Render, SharedString,
Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
actions, div, prelude::*, AppContext, Dismiss, Div, FocusHandle, ManagedView, ParentComponent,
Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
};
use text::{Bias, Point};
use theme::ActiveTheme;
use ui::{h_stack, v_stack, Label, StyledExt, TextColor};
use util::paths::FILE_ROW_COLUMN_DELIMITER;
use workspace::{Modal, ModalEvent, Workspace};
use workspace::Workspace;
actions!(Toggle);
@@ -23,10 +23,9 @@ pub struct GoToLine {
_subscriptions: Vec<Subscription>,
}
impl EventEmitter<ModalEvent> for GoToLine {}
impl Modal for GoToLine {
fn focus(&self, cx: &mut WindowContext) {
self.line_editor.update(cx, |editor, cx| editor.focus(cx))
impl ManagedView for GoToLine {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.line_editor.focus_handle(cx)
}
}
@@ -88,7 +87,7 @@ impl GoToLine {
) {
match event {
// todo!() this isn't working...
editor::Event::Blurred => cx.emit(ModalEvent::Dismissed),
editor::Event::Blurred => cx.emit(Dismiss),
editor::Event::BufferEdited { .. } => self.highlight_current_line(cx),
_ => {}
}
@@ -123,7 +122,7 @@ impl GoToLine {
}
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(ModalEvent::Dismissed);
cx.emit(Dismiss);
}
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
@@ -140,7 +139,7 @@ impl GoToLine {
self.prev_scroll_position.take();
}
cx.emit(ModalEvent::Dismissed);
cx.emit(Dismiss);
}
}

View File

@@ -22,6 +22,7 @@ sqlez = { path = "../sqlez" }
async-task = "4.0.3"
backtrace = { version = "0.3", optional = true }
ctor.workspace = true
linkme = "0.3"
derive_more.workspace = true
dhat = { version = "0.3", optional = true }
env_logger = { version = "0.9", optional = true }

View File

@@ -1,10 +1,12 @@
use crate::SharedString;
use anyhow::{anyhow, Context, Result};
use collections::HashMap;
use lazy_static::lazy_static;
use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard};
use serde::Deserialize;
use std::any::{type_name, Any, TypeId};
pub use no_action::NoAction;
use serde_json::json;
use std::{
any::{Any, TypeId},
ops::Deref,
};
/// Actions are used to implement keyboard-driven UI.
/// When you declare an action, you can bind keys to the action in the keymap and
@@ -15,24 +17,16 @@ use std::any::{type_name, Any, TypeId};
/// ```rust
/// actions!(MoveUp, MoveDown, MoveLeft, MoveRight, Newline);
/// ```
/// More complex data types can also be actions. If you annotate your type with the `#[action]` proc macro,
/// it will automatically
/// More complex data types can also be actions. If you annotate your type with the action derive macro
/// it will be implemented and registered automatically.
/// ```
/// #[action]
/// #[derive(Clone, PartialEq, serde_derive::Deserialize, Action)]
/// pub struct SelectNext {
/// pub replace_newest: bool,
/// }
///
/// Any type A that satisfies the following bounds is automatically an action:
///
/// ```
/// A: for<'a> Deserialize<'a> + PartialEq + Clone + Default + std::fmt::Debug + 'static,
/// ```
///
/// The `#[action]` annotation will derive these implementations for your struct automatically. If you
/// want to control them manually, you can use the lower-level `#[register_action]` macro, which only
/// generates the code needed to register your action before `main`. Then you'll need to implement all
/// the traits manually.
/// If you want to control the behavior of the action trait manually, you can use the lower-level `#[register_action]`
/// macro, which only generates the code needed to register your action before `main`.
///
/// ```
/// #[gpui::register_action]
@@ -41,77 +35,29 @@ use std::any::{type_name, Any, TypeId};
/// pub content: SharedString,
/// }
///
/// impl std::default::Default for Paste {
/// fn default() -> Self {
/// Self {
/// content: SharedString::from("🍝"),
/// }
/// }
/// impl gpui::Action for Paste {
/// ///...
/// }
/// ```
pub trait Action: std::fmt::Debug + 'static {
fn qualified_name() -> SharedString
where
Self: Sized;
fn build(value: Option<serde_json::Value>) -> Result<Box<dyn Action>>
where
Self: Sized;
fn is_registered() -> bool
where
Self: Sized;
fn partial_eq(&self, action: &dyn Action) -> bool;
pub trait Action: 'static {
fn boxed_clone(&self) -> Box<dyn Action>;
fn as_any(&self) -> &dyn Any;
fn partial_eq(&self, action: &dyn Action) -> bool;
fn name(&self) -> &str;
fn debug_name() -> &'static str
where
Self: Sized;
fn build(value: serde_json::Value) -> Result<Box<dyn Action>>
where
Self: Sized;
}
// Types become actions by satisfying a list of trait bounds.
impl<A> Action for A
where
A: for<'a> Deserialize<'a> + PartialEq + Clone + Default + std::fmt::Debug + 'static,
{
fn qualified_name() -> SharedString {
let name = type_name::<A>();
let mut separator_matches = name.rmatch_indices("::");
separator_matches.next().unwrap();
let name_start_ix = separator_matches.next().map_or(0, |(ix, _)| ix + 2);
// todo!() remove the 2 replacement when migration is done
name[name_start_ix..].replace("2::", "::").into()
}
fn build(params: Option<serde_json::Value>) -> Result<Box<dyn Action>>
where
Self: Sized,
{
let action = if let Some(params) = params {
serde_json::from_value(params).context("failed to deserialize action")?
} else {
Self::default()
};
Ok(Box::new(action))
}
fn is_registered() -> bool {
ACTION_REGISTRY
.read()
.names_by_type_id
.get(&TypeId::of::<A>())
.is_some()
}
fn partial_eq(&self, action: &dyn Action) -> bool {
action
.as_any()
.downcast_ref::<Self>()
.map_or(false, |a| self == a)
}
fn boxed_clone(&self) -> Box<dyn Action> {
Box::new(self.clone())
}
fn as_any(&self) -> &dyn Any {
self
impl std::fmt::Debug for dyn Action {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("dyn Action")
.field("type_name", &self.name())
.finish()
}
}
@@ -119,69 +65,93 @@ impl dyn Action {
pub fn type_id(&self) -> TypeId {
self.as_any().type_id()
}
pub fn name(&self) -> SharedString {
ACTION_REGISTRY
.read()
.names_by_type_id
.get(&self.type_id())
.expect("type is not a registered action")
.clone()
}
}
type ActionBuilder = fn(json: Option<serde_json::Value>) -> anyhow::Result<Box<dyn Action>>;
type ActionBuilder = fn(json: serde_json::Value) -> anyhow::Result<Box<dyn Action>>;
lazy_static! {
static ref ACTION_REGISTRY: RwLock<ActionRegistry> = RwLock::default();
}
#[derive(Default)]
struct ActionRegistry {
pub(crate) struct ActionRegistry {
builders_by_name: HashMap<SharedString, ActionBuilder>,
names_by_type_id: HashMap<TypeId, SharedString>,
all_names: Vec<SharedString>, // So we can return a static slice.
}
/// Register an action type to allow it to be referenced in keymaps.
pub fn register_action<A: Action>() {
let name = A::qualified_name();
let mut lock = ACTION_REGISTRY.write();
lock.builders_by_name.insert(name.clone(), A::build);
lock.names_by_type_id
.insert(TypeId::of::<A>(), name.clone());
lock.all_names.push(name);
impl Default for ActionRegistry {
fn default() -> Self {
let mut this = ActionRegistry {
builders_by_name: Default::default(),
names_by_type_id: Default::default(),
all_names: Default::default(),
};
this.load_actions();
this
}
}
/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
pub fn build_action_from_type(type_id: &TypeId) -> Result<Box<dyn Action>> {
let lock = ACTION_REGISTRY.read();
let name = lock
.names_by_type_id
.get(type_id)
.ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))?
.clone();
drop(lock);
/// This type must be public so that our macros can build it in other crates.
/// But this is an implementation detail and should not be used directly.
#[doc(hidden)]
pub type MacroActionBuilder = fn() -> ActionData;
build_action(&name, None)
/// This type must be public so that our macros can build it in other crates.
/// But this is an implementation detail and should not be used directly.
#[doc(hidden)]
pub struct ActionData {
pub name: &'static str,
pub type_id: TypeId,
pub build: ActionBuilder,
}
/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
pub fn build_action(name: &str, params: Option<serde_json::Value>) -> Result<Box<dyn Action>> {
let lock = ACTION_REGISTRY.read();
/// This constant must be public to be accessible from other crates.
/// But it's existence is an implementation detail and should not be used directly.
#[doc(hidden)]
#[linkme::distributed_slice]
pub static __GPUI_ACTIONS: [MacroActionBuilder];
let build_action = lock
.builders_by_name
.get(name)
.ok_or_else(|| anyhow!("no action type registered for {}", name))?;
(build_action)(params)
}
impl ActionRegistry {
/// Load all registered actions into the registry.
pub(crate) fn load_actions(&mut self) {
for builder in __GPUI_ACTIONS {
let action = builder();
//todo(remove)
let name: SharedString = remove_the_2(action.name).into();
self.builders_by_name.insert(name.clone(), action.build);
self.names_by_type_id.insert(action.type_id, name.clone());
self.all_names.push(name);
}
}
pub fn all_action_names() -> MappedRwLockReadGuard<'static, [SharedString]> {
let lock = ACTION_REGISTRY.read();
RwLockReadGuard::map(lock, |registry: &ActionRegistry| {
registry.all_names.as_slice()
})
/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
pub fn build_action_type(&self, type_id: &TypeId) -> Result<Box<dyn Action>> {
let name = self
.names_by_type_id
.get(type_id)
.ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))?
.clone();
self.build_action(&name, None)
}
/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
pub fn build_action(
&self,
name: &str,
params: Option<serde_json::Value>,
) -> Result<Box<dyn Action>> {
//todo(remove)
let name = remove_the_2(name);
let build_action = self
.builders_by_name
.get(name.deref())
.ok_or_else(|| anyhow!("no action type registered for {}", name))?;
(build_action)(params.unwrap_or_else(|| json!({})))
.with_context(|| format!("Attempting to build action {}", name))
}
pub fn all_action_names(&self) -> &[SharedString] {
self.all_names.as_slice()
}
}
/// Defines unit structs that can be used as actions.
@@ -191,7 +161,7 @@ macro_rules! actions {
() => {};
( $name:ident ) => {
#[gpui::action]
#[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize, gpui::Action)]
pub struct $name;
};
@@ -200,3 +170,20 @@ macro_rules! actions {
actions!($($rest)*);
};
}
//todo!(remove)
pub fn remove_the_2(action_name: &str) -> String {
let mut separator_matches = action_name.rmatch_indices("::");
separator_matches.next().unwrap();
let name_start_ix = separator_matches.next().map_or(0, |(ix, _)| ix + 2);
// todo!() remove the 2 replacement when migration is done
action_name[name_start_ix..]
.replace("2::", "::")
.to_string()
}
mod no_action {
use crate as gpui;
actions!(NoAction);
}

View File

@@ -14,12 +14,13 @@ use smallvec::SmallVec;
pub use test_context::*;
use crate::{
current_platform, image_cache::ImageCache, Action, AnyBox, AnyView, AnyWindowHandle,
AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context, DispatchPhase, DisplayId,
Entity, EventEmitter, FocusEvent, FocusHandle, FocusId, ForegroundExecutor, KeyBinding, Keymap,
LayoutId, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point, Render, SubscriberSet,
Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View, ViewContext,
Window, WindowContext, WindowHandle, WindowId,
current_platform, image_cache::ImageCache, Action, ActionRegistry, AnyBox, AnyView,
AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
DispatchPhase, DisplayId, Entity, EventEmitter, FocusEvent, FocusHandle, FocusId,
ForegroundExecutor, KeyBinding, Keymap, LayoutId, PathPromptOptions, Pixels, Platform,
PlatformDisplay, Point, Render, SharedString, SubscriberSet, Subscription, SvgRenderer, Task,
TextStyle, TextStyleRefinement, TextSystem, View, ViewContext, Window, WindowContext,
WindowHandle, WindowId,
};
use anyhow::{anyhow, Result};
use collections::{HashMap, HashSet, VecDeque};
@@ -182,6 +183,7 @@ pub struct AppContext {
text_system: Arc<TextSystem>,
flushing_effects: bool,
pending_updates: usize,
pub(crate) actions: Rc<ActionRegistry>,
pub(crate) active_drag: Option<AnyDrag>,
pub(crate) active_tooltip: Option<AnyTooltip>,
pub(crate) next_frame_callbacks: HashMap<DisplayId, Vec<FrameCallback>>,
@@ -240,6 +242,7 @@ impl AppContext {
platform: platform.clone(),
app_metadata,
text_system,
actions: Rc::new(ActionRegistry::default()),
flushing_effects: false,
pending_updates: 0,
active_drag: None,
@@ -964,6 +967,18 @@ impl AppContext {
pub fn propagate(&mut self) {
self.propagate_event = true;
}
pub fn build_action(
&self,
name: &str,
data: Option<serde_json::Value>,
) -> Result<Box<dyn Action>> {
self.actions.build_action(name, data)
}
pub fn all_action_names(&self) -> &[SharedString] {
self.actions.all_action_names()
}
}
impl Context for AppContext {

View File

@@ -1,7 +1,7 @@
use crate::{
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, ForegroundExecutor,
Model, ModelContext, Render, Result, Task, View, ViewContext, VisualContext, WindowContext,
WindowHandle,
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, FocusableView,
ForegroundExecutor, Model, ModelContext, Render, Result, Task, View, ViewContext,
VisualContext, WindowContext, WindowHandle,
};
use anyhow::{anyhow, Context as _};
use derive_more::{Deref, DerefMut};
@@ -182,6 +182,10 @@ pub struct AsyncWindowContext {
}
impl AsyncWindowContext {
pub fn window_handle(&self) -> AnyWindowHandle {
self.window
}
pub(crate) fn new(app: AsyncAppContext, window: AnyWindowHandle) -> Self {
Self { app, window }
}
@@ -307,4 +311,13 @@ impl VisualContext for AsyncWindowContext {
self.window
.update(self, |_, cx| cx.replace_root_view(build_view))
}
fn focus_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
where
V: FocusableView,
{
self.window.update(self, |_, cx| {
view.read(cx).focus_handle(cx).clone().focus(cx);
})
}
}

View File

@@ -68,6 +68,7 @@ impl EntityMap {
}
/// Move an entity to the stack.
#[track_caller]
pub fn lease<'a, T>(&mut self, model: &'a Model<T>) -> Lease<'a, T> {
self.assert_valid_context(model);
let entity = Some(

View File

@@ -140,7 +140,7 @@ impl TestAppContext {
.any_handle
}
pub fn add_window_view<F, V>(&mut self, build_window: F) -> (View<V>, VisualTestContext)
pub fn add_window_view<F, V>(&mut self, build_window: F) -> (View<V>, &mut VisualTestContext)
where
F: FnOnce(&mut ViewContext<V>) -> V,
V: Render,
@@ -149,7 +149,9 @@ impl TestAppContext {
let window = cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window));
drop(cx);
let view = window.root_view(self).unwrap();
(view, VisualTestContext::from_window(*window.deref(), self))
let cx = Box::new(VisualTestContext::from_window(*window.deref(), self));
// it might be nice to try and cleanup these at the end of each test.
(view, Box::leak(cx))
}
pub fn simulate_new_path_selection(
@@ -225,6 +227,9 @@ impl TestAppContext {
self.background_executor.run_until_parked()
}
/// simulate_keystrokes takes a space-separated list of keys to type.
/// cx.simulate_keystrokes("cmd-shift-p b k s p enter")
/// will run backspace on the current editor through the command palette.
pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) {
for keystroke in keystrokes
.split(" ")
@@ -237,6 +242,17 @@ impl TestAppContext {
self.background_executor.run_until_parked()
}
/// simulate_input takes a string of text to type.
/// cx.simulate_input("abc")
/// will type abc into your current editor.
pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) {
for keystroke in input.split("").map(Keystroke::parse).map(Result::unwrap) {
self.dispatch_keystroke(window, keystroke.into(), false);
}
self.background_executor.run_until_parked()
}
pub fn dispatch_keystroke(
&mut self,
window: AnyWindowHandle,
@@ -354,10 +370,19 @@ impl<T: Send> Model<T> {
})
});
cx.executor().run_until_parked();
rx.try_next()
.expect("no event received")
.expect("model was dropped")
// Run other tasks until the event is emitted.
loop {
match rx.try_next() {
Ok(Some(event)) => return event,
Ok(None) => panic!("model was dropped"),
Err(_) => {
if !cx.executor().tick() {
break;
}
}
}
}
panic!("no event received")
}
}
@@ -455,6 +480,10 @@ impl<'a> VisualTestContext<'a> {
pub fn simulate_keystrokes(&mut self, keystrokes: &str) {
self.cx.simulate_keystrokes(self.window, keystrokes)
}
pub fn simulate_input(&mut self, input: &str) {
self.cx.simulate_input(self.window, input)
}
}
impl<'a> Context for VisualTestContext<'a> {
@@ -542,6 +571,14 @@ impl<'a> VisualContext for VisualTestContext<'a> {
.update(self.cx, |_, cx| cx.replace_root_view(build_view))
.unwrap()
}
fn focus_view<V: crate::FocusableView>(&mut self, view: &View<V>) -> Self::Result<()> {
self.window
.update(self.cx, |_, cx| {
view.read(cx).focus_handle(cx).clone().focus(cx)
})
.unwrap()
}
}
impl AnyWindowHandle {

View File

@@ -3,28 +3,19 @@ use crate::{
};
use derive_more::{Deref, DerefMut};
pub(crate) use smallvec::SmallVec;
use std::{any::Any, mem};
use std::{any::Any, fmt::Debug, mem};
pub trait Element<V: 'static> {
type ElementState: 'static;
fn element_id(&self) -> Option<ElementId>;
/// Called to initialize this element for the current frame. If this
/// element had state in a previous frame, it will be passed in for the 3rd argument.
fn initialize(
fn layout(
&mut self,
view_state: &mut V,
element_state: Option<Self::ElementState>,
cx: &mut ViewContext<V>,
) -> Self::ElementState;
fn layout(
&mut self,
view_state: &mut V,
element_state: &mut Self::ElementState,
cx: &mut ViewContext<V>,
) -> LayoutId;
) -> (LayoutId, Self::ElementState);
fn paint(
&mut self,
@@ -33,6 +24,42 @@ pub trait Element<V: 'static> {
element_state: &mut Self::ElementState,
cx: &mut ViewContext<V>,
);
fn draw<T, R>(
self,
origin: Point<Pixels>,
available_space: Size<T>,
view_state: &mut V,
cx: &mut ViewContext<V>,
f: impl FnOnce(&Self::ElementState, &mut ViewContext<V>) -> R,
) -> R
where
Self: Sized,
T: Clone + Default + Debug + Into<AvailableSpace>,
{
let mut element = RenderedElement {
element: self,
phase: ElementRenderPhase::Start,
};
element.draw(origin, available_space.map(Into::into), view_state, cx);
if let ElementRenderPhase::Painted { frame_state } = &element.phase {
if let Some(frame_state) = frame_state.as_ref() {
f(&frame_state, cx)
} else {
let element_id = element
.element
.element_id()
.expect("we either have some frame_state or some element_id");
cx.with_element_state(element_id, |element_state, cx| {
let element_state = element_state.unwrap();
let result = f(&element_state, cx);
(result, element_state)
})
}
} else {
unreachable!()
}
}
}
#[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)]
@@ -60,7 +87,6 @@ pub trait ParentComponent<V: 'static> {
}
trait ElementObject<V> {
fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext<V>);
fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId;
fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>);
fn measure(
@@ -87,9 +113,6 @@ struct RenderedElement<V: 'static, E: Element<V>> {
enum ElementRenderPhase<V> {
#[default]
Start,
Initialized {
frame_state: Option<V>,
},
LayoutRequested {
layout_id: LayoutId,
frame_state: Option<V>,
@@ -99,7 +122,9 @@ enum ElementRenderPhase<V> {
available_space: Size<AvailableSpace>,
frame_state: Option<V>,
},
Painted,
Painted {
frame_state: Option<V>,
},
}
/// Internal struct that wraps an element to store Layout and ElementState after the element is rendered.
@@ -119,45 +144,22 @@ where
E: Element<V>,
E::ElementState: 'static,
{
fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
let frame_state = if let Some(id) = self.element.element_id() {
cx.with_element_state(id, |element_state, cx| {
let element_state = self.element.initialize(view_state, element_state, cx);
((), element_state)
});
None
} else {
let frame_state = self.element.initialize(view_state, None, cx);
Some(frame_state)
};
self.phase = ElementRenderPhase::Initialized { frame_state };
}
fn layout(&mut self, state: &mut V, cx: &mut ViewContext<V>) -> LayoutId {
let layout_id;
let mut frame_state;
match mem::take(&mut self.phase) {
ElementRenderPhase::Initialized {
frame_state: initial_frame_state,
} => {
frame_state = initial_frame_state;
let (layout_id, frame_state) = match mem::take(&mut self.phase) {
ElementRenderPhase::Start => {
if let Some(id) = self.element.element_id() {
layout_id = cx.with_element_state(id, |element_state, cx| {
let mut element_state = element_state.unwrap();
let layout_id = self.element.layout(state, &mut element_state, cx);
(layout_id, element_state)
let layout_id = cx.with_element_state(id, |element_state, cx| {
self.element.layout(state, element_state, cx)
});
(layout_id, None)
} else {
layout_id = self
.element
.layout(state, frame_state.as_mut().unwrap(), cx);
let (layout_id, frame_state) = self.element.layout(state, None, cx);
(layout_id, Some(frame_state))
}
}
ElementRenderPhase::Start => panic!("must call initialize before layout"),
ElementRenderPhase::LayoutRequested { .. }
| ElementRenderPhase::LayoutComputed { .. }
| ElementRenderPhase::Painted => {
| ElementRenderPhase::Painted { .. } => {
panic!("element rendered twice")
}
};
@@ -192,7 +194,7 @@ where
self.element
.paint(bounds, view_state, frame_state.as_mut().unwrap(), cx);
}
ElementRenderPhase::Painted
ElementRenderPhase::Painted { frame_state }
}
_ => panic!("must call layout before paint"),
@@ -206,10 +208,6 @@ where
cx: &mut ViewContext<V>,
) -> Size<Pixels> {
if matches!(&self.phase, ElementRenderPhase::Start) {
self.initialize(view_state, cx);
}
if matches!(&self.phase, ElementRenderPhase::Initialized { .. }) {
self.layout(view_state, cx);
}
@@ -246,16 +244,13 @@ where
fn draw(
&mut self,
mut origin: Point<Pixels>,
origin: Point<Pixels>,
available_space: Size<AvailableSpace>,
view_state: &mut V,
cx: &mut ViewContext<V>,
) {
self.measure(available_space, view_state, cx);
// Ignore the element offset when drawing this element, as the origin is already specified
// in absolute terms.
origin -= cx.element_offset();
cx.with_element_offset(origin, |cx| self.paint(view_state, cx))
cx.with_absolute_element_offset(origin, |cx| self.paint(view_state, cx))
}
}
@@ -271,10 +266,6 @@ impl<V> AnyElement<V> {
AnyElement(Box::new(RenderedElement::new(element)))
}
pub fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
self.0.initialize(view_state, cx);
}
pub fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId {
self.0.layout(view_state, cx)
}
@@ -355,25 +346,16 @@ where
None
}
fn initialize(
&mut self,
view_state: &mut V,
_rendered_element: Option<Self::ElementState>,
cx: &mut ViewContext<V>,
) -> Self::ElementState {
let render = self.take().unwrap();
let mut rendered_element = (render)(view_state, cx).render();
rendered_element.initialize(view_state, cx);
rendered_element
}
fn layout(
&mut self,
view_state: &mut V,
rendered_element: &mut Self::ElementState,
_: Option<Self::ElementState>,
cx: &mut ViewContext<V>,
) -> LayoutId {
rendered_element.layout(view_state, cx)
) -> (LayoutId, Self::ElementState) {
let render = self.take().unwrap();
let mut rendered_element = (render)(view_state, cx).render();
let layout_id = rendered_element.layout(view_state, cx);
(layout_id, rendered_element)
}
fn paint(

View File

@@ -6,15 +6,15 @@ use crate::{
SharedString, Size, Style, StyleRefinement, Styled, Task, View, ViewContext, Visibility,
};
use collections::HashMap;
use parking_lot::Mutex;
use refineable::Refineable;
use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
cell::RefCell,
fmt::Debug,
marker::PhantomData,
mem,
sync::Arc,
rc::Rc,
time::Duration,
};
use taffy::style::Overflow;
@@ -22,7 +22,6 @@ use util::ResultExt;
const DRAG_THRESHOLD: f64 = 2.;
const TOOLTIP_DELAY: Duration = Duration::from_millis(500);
const TOOLTIP_OFFSET: Point<Pixels> = Point::new(px(10.0), px(8.0));
pub struct GroupStyle {
pub group: SharedString,
@@ -238,11 +237,11 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
//
// if we are relying on this side-effect still, removing the debug_assert!
// likely breaks the command_palette tests.
debug_assert!(
A::is_registered(),
"{:?} is not registered as an action",
A::qualified_name()
);
// debug_assert!(
// A::is_registered(),
// "{:?} is not registered as an action",
// A::qualified_name()
// );
self.interactivity().action_listeners.push((
TypeId::of::<A>(),
Box::new(move |view, action, phase, cx| {
@@ -408,21 +407,19 @@ pub trait StatefulInteractiveComponent<V: 'static, E: Element<V>>: InteractiveCo
self
}
fn tooltip<W>(
fn tooltip(
mut self,
build_tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> View<W> + 'static,
build_tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static,
) -> Self
where
Self: Sized,
W: 'static + Render,
{
debug_assert!(
self.interactivity().tooltip_builder.is_none(),
"calling tooltip more than once on the same element is not supported"
);
self.interactivity().tooltip_builder = Some(Arc::new(move |view_state, cx| {
build_tooltip(view_state, cx).into()
}));
self.interactivity().tooltip_builder =
Some(Rc::new(move |view_state, cx| build_tooltip(view_state, cx)));
self
}
@@ -437,14 +434,6 @@ pub trait FocusableComponent<V: 'static>: InteractiveComponent<V> {
self
}
fn focus_in(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
where
Self: Sized,
{
self.interactivity().focus_in_style = f(StyleRefinement::default());
self
}
fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
where
Self: Sized,
@@ -569,7 +558,7 @@ type DropListener<V> = dyn Fn(&mut V, AnyView, &mut ViewContext<V>) + 'static;
pub type HoverListener<V> = Box<dyn Fn(&mut V, bool, &mut ViewContext<V>) + 'static>;
pub type TooltipBuilder<V> = Arc<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>;
pub type TooltipBuilder<V> = Rc<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>;
pub type KeyDownListener<V> =
Box<dyn Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext<V>) + 'static>;
@@ -617,46 +606,36 @@ impl<V: 'static> Element<V> for Div<V> {
self.interactivity.element_id.clone()
}
fn initialize(
fn layout(
&mut self,
view_state: &mut V,
element_state: Option<Self::ElementState>,
cx: &mut ViewContext<V>,
) -> Self::ElementState {
let interactive_state = self
.interactivity
.initialize(element_state.map(|s| s.interactive_state), cx);
for child in &mut self.children {
child.initialize(view_state, cx);
}
DivState {
interactive_state,
child_layout_ids: SmallVec::new(),
}
}
fn layout(
&mut self,
view_state: &mut V,
element_state: &mut Self::ElementState,
cx: &mut ViewContext<V>,
) -> crate::LayoutId {
) -> (LayoutId, Self::ElementState) {
let mut child_layout_ids = SmallVec::new();
let mut interactivity = mem::take(&mut self.interactivity);
let layout_id =
interactivity.layout(&mut element_state.interactive_state, cx, |style, cx| {
let (layout_id, interactive_state) = interactivity.layout(
element_state.map(|s| s.interactive_state),
cx,
|style, cx| {
cx.with_text_style(style.text_style().cloned(), |cx| {
element_state.child_layout_ids = self
child_layout_ids = self
.children
.iter_mut()
.map(|child| child.layout(view_state, cx))
.collect::<SmallVec<_>>();
cx.request_layout(&style, element_state.child_layout_ids.iter().copied())
cx.request_layout(&style, child_layout_ids.iter().copied())
})
});
},
);
self.interactivity = interactivity;
layout_id
(
layout_id,
DivState {
interactive_state,
child_layout_ids,
},
)
}
fn paint(
@@ -725,6 +704,12 @@ pub struct DivState {
interactive_state: InteractiveElementState,
}
impl DivState {
pub fn is_active(&self) -> bool {
self.interactive_state.pending_mouse_down.borrow().is_some()
}
}
pub struct Interactivity<V> {
pub element_id: Option<ElementId>,
pub key_context: KeyContext,
@@ -734,7 +719,6 @@ pub struct Interactivity<V> {
pub group: Option<SharedString>,
pub base_style: StyleRefinement,
pub focus_style: StyleRefinement,
pub focus_in_style: StyleRefinement,
pub in_focus_style: StyleRefinement,
pub hover_style: StyleRefinement,
pub group_hover_style: Option<GroupStyle>,
@@ -760,11 +744,12 @@ impl<V> Interactivity<V>
where
V: 'static,
{
pub fn initialize(
pub fn layout(
&mut self,
element_state: Option<InteractiveElementState>,
cx: &mut ViewContext<V>,
) -> InteractiveElementState {
f: impl FnOnce(Style, &mut ViewContext<V>) -> LayoutId,
) -> (LayoutId, InteractiveElementState) {
let mut element_state = element_state.unwrap_or_default();
// Ensure we store a focus handle in our element state if we're focusable.
@@ -779,17 +764,9 @@ where
});
}
element_state
}
pub fn layout(
&mut self,
element_state: &mut InteractiveElementState,
cx: &mut ViewContext<V>,
f: impl FnOnce(Style, &mut ViewContext<V>) -> LayoutId,
) -> LayoutId {
let style = self.compute_style(None, element_state, cx);
f(style, cx)
let style = self.compute_style(None, &mut element_state, cx);
let layout_id = f(style, cx);
(layout_id, element_state)
}
pub fn paint(
@@ -890,7 +867,7 @@ where
if !click_listeners.is_empty() || drag_listener.is_some() {
let pending_mouse_down = element_state.pending_mouse_down.clone();
let mouse_down = pending_mouse_down.lock().clone();
let mouse_down = pending_mouse_down.borrow().clone();
if let Some(mouse_down) = mouse_down {
if let Some(drag_listener) = drag_listener {
let active_state = element_state.clicked_state.clone();
@@ -904,7 +881,7 @@ where
&& bounds.contains_point(&event.position)
&& (event.position - mouse_down.position).magnitude() > DRAG_THRESHOLD
{
*active_state.lock() = ElementClickedState::default();
*active_state.borrow_mut() = ElementClickedState::default();
let cursor_offset = event.position - bounds.origin;
let drag = drag_listener(view_state, cursor_offset, cx);
cx.active_drag = Some(drag);
@@ -924,13 +901,13 @@ where
listener(view_state, &mouse_click, cx);
}
}
*pending_mouse_down.lock() = None;
*pending_mouse_down.borrow_mut() = None;
cx.notify();
});
} else {
cx.on_mouse_event(move |_view_state, event: &MouseDownEvent, phase, cx| {
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
*pending_mouse_down.lock() = Some(event.clone());
*pending_mouse_down.borrow_mut() = Some(event.clone());
cx.notify();
}
});
@@ -946,8 +923,8 @@ where
return;
}
let is_hovered =
bounds.contains_point(&event.position) && has_mouse_down.lock().is_none();
let mut was_hovered = was_hovered.lock();
bounds.contains_point(&event.position) && has_mouse_down.borrow().is_none();
let mut was_hovered = was_hovered.borrow_mut();
if is_hovered != was_hovered.clone() {
*was_hovered = is_hovered;
@@ -968,13 +945,13 @@ where
}
let is_hovered =
bounds.contains_point(&event.position) && pending_mouse_down.lock().is_none();
bounds.contains_point(&event.position) && pending_mouse_down.borrow().is_none();
if !is_hovered {
active_tooltip.lock().take();
active_tooltip.borrow_mut().take();
return;
}
if active_tooltip.lock().is_none() {
if active_tooltip.borrow().is_none() {
let task = cx.spawn({
let active_tooltip = active_tooltip.clone();
let tooltip_builder = tooltip_builder.clone();
@@ -982,26 +959,31 @@ where
move |view, mut cx| async move {
cx.background_executor().timer(TOOLTIP_DELAY).await;
view.update(&mut cx, move |view_state, cx| {
active_tooltip.lock().replace(ActiveTooltip {
waiting: None,
active_tooltip.borrow_mut().replace(ActiveTooltip {
tooltip: Some(AnyTooltip {
view: tooltip_builder(view_state, cx),
cursor_offset: cx.mouse_position() + TOOLTIP_OFFSET,
cursor_offset: cx.mouse_position(),
}),
_task: None,
});
cx.notify();
})
.ok();
}
});
active_tooltip.lock().replace(ActiveTooltip {
waiting: Some(task),
active_tooltip.borrow_mut().replace(ActiveTooltip {
tooltip: None,
_task: Some(task),
});
}
});
if let Some(active_tooltip) = element_state.active_tooltip.lock().as_ref() {
let active_tooltip = element_state.active_tooltip.clone();
cx.on_mouse_event(move |_, _: &MouseDownEvent, _, _| {
active_tooltip.borrow_mut().take();
});
if let Some(active_tooltip) = element_state.active_tooltip.borrow().as_ref() {
if active_tooltip.tooltip.is_some() {
cx.active_tooltip = active_tooltip.tooltip.clone()
}
@@ -1009,10 +991,10 @@ where
}
let active_state = element_state.clicked_state.clone();
if !active_state.lock().is_clicked() {
if !active_state.borrow().is_clicked() {
cx.on_mouse_event(move |_, _: &MouseUpEvent, phase, cx| {
if phase == DispatchPhase::Capture {
*active_state.lock() = ElementClickedState::default();
*active_state.borrow_mut() = ElementClickedState::default();
cx.notify();
}
});
@@ -1027,7 +1009,7 @@ where
.map_or(false, |bounds| bounds.contains_point(&down.position));
let element = bounds.contains_point(&down.position);
if group || element {
*active_state.lock() = ElementClickedState { group, element };
*active_state.borrow_mut() = ElementClickedState { group, element };
cx.notify();
}
}
@@ -1038,14 +1020,14 @@ where
if overflow.x == Overflow::Scroll || overflow.y == Overflow::Scroll {
let scroll_offset = element_state
.scroll_offset
.get_or_insert_with(Arc::default)
.get_or_insert_with(Rc::default)
.clone();
let line_height = cx.line_height();
let scroll_max = (content_size - bounds.size).max(&Size::default());
cx.on_mouse_event(move |_, event: &ScrollWheelEvent, phase, cx| {
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
let mut scroll_offset = scroll_offset.lock();
let mut scroll_offset = scroll_offset.borrow_mut();
let old_scroll_offset = *scroll_offset;
let delta = event.delta.pixel_delta(line_height);
@@ -1074,7 +1056,7 @@ where
let scroll_offset = element_state
.scroll_offset
.as_ref()
.map(|scroll_offset| *scroll_offset.lock());
.map(|scroll_offset| *scroll_offset.borrow());
cx.with_key_dispatch(
self.key_context.clone(),
@@ -1124,10 +1106,6 @@ where
style.refine(&self.base_style);
if let Some(focus_handle) = self.tracked_focus_handle.as_ref() {
if focus_handle.contains_focused(cx) {
style.refine(&self.focus_in_style);
}
if focus_handle.within_focused(cx) {
style.refine(&self.in_focus_style);
}
@@ -1173,7 +1151,7 @@ where
}
}
let clicked_state = element_state.clicked_state.lock();
let clicked_state = element_state.clicked_state.borrow();
if clicked_state.group {
if let Some(group) = self.group_active_style.as_ref() {
style.refine(&group.style)
@@ -1200,7 +1178,6 @@ impl<V: 'static> Default for Interactivity<V> {
group: None,
base_style: StyleRefinement::default(),
focus_style: StyleRefinement::default(),
focus_in_style: StyleRefinement::default(),
in_focus_style: StyleRefinement::default(),
hover_style: StyleRefinement::default(),
group_hover_style: None,
@@ -1227,17 +1204,16 @@ impl<V: 'static> Default for Interactivity<V> {
#[derive(Default)]
pub struct InteractiveElementState {
pub focus_handle: Option<FocusHandle>,
pub clicked_state: Arc<Mutex<ElementClickedState>>,
pub hover_state: Arc<Mutex<bool>>,
pub pending_mouse_down: Arc<Mutex<Option<MouseDownEvent>>>,
pub scroll_offset: Option<Arc<Mutex<Point<Pixels>>>>,
pub active_tooltip: Arc<Mutex<Option<ActiveTooltip>>>,
pub clicked_state: Rc<RefCell<ElementClickedState>>,
pub hover_state: Rc<RefCell<bool>>,
pub pending_mouse_down: Rc<RefCell<Option<MouseDownEvent>>>,
pub scroll_offset: Option<Rc<RefCell<Point<Pixels>>>>,
pub active_tooltip: Rc<RefCell<Option<ActiveTooltip>>>,
}
pub struct ActiveTooltip {
#[allow(unused)] // used to drop the task
waiting: Option<Task<()>>,
tooltip: Option<AnyTooltip>,
_task: Option<Task<()>>,
}
/// Whether or not the element or a group that contains it is clicked by the mouse.
@@ -1321,21 +1297,12 @@ where
self.element.element_id()
}
fn initialize(
fn layout(
&mut self,
view_state: &mut V,
element_state: Option<Self::ElementState>,
cx: &mut ViewContext<V>,
) -> Self::ElementState {
self.element.initialize(view_state, element_state, cx)
}
fn layout(
&mut self,
view_state: &mut V,
element_state: &mut Self::ElementState,
cx: &mut ViewContext<V>,
) -> LayoutId {
) -> (LayoutId, Self::ElementState) {
self.element.layout(view_state, element_state, cx)
}
@@ -1416,21 +1383,12 @@ where
self.element.element_id()
}
fn initialize(
fn layout(
&mut self,
view_state: &mut V,
element_state: Option<Self::ElementState>,
cx: &mut ViewContext<V>,
) -> Self::ElementState {
self.element.initialize(view_state, element_state, cx)
}
fn layout(
&mut self,
view_state: &mut V,
element_state: &mut Self::ElementState,
cx: &mut ViewContext<V>,
) -> LayoutId {
) -> (LayoutId, Self::ElementState) {
self.element.layout(view_state, element_state, cx)
}

View File

@@ -48,21 +48,12 @@ impl<V> Element<V> for Img<V> {
self.interactivity.element_id.clone()
}
fn initialize(
fn layout(
&mut self,
_view_state: &mut V,
element_state: Option<Self::ElementState>,
cx: &mut ViewContext<V>,
) -> Self::ElementState {
self.interactivity.initialize(element_state, cx)
}
fn layout(
&mut self,
_view_state: &mut V,
element_state: &mut Self::ElementState,
cx: &mut ViewContext<V>,
) -> LayoutId {
) -> (LayoutId, Self::ElementState) {
self.interactivity.layout(element_state, cx, |style, cx| {
cx.request_layout(&style, None)
})

View File

@@ -1,11 +1,13 @@
mod div;
mod img;
mod overlay;
mod svg;
mod text;
mod uniform_list;
pub use div::*;
pub use img::*;
pub use overlay::*;
pub use svg::*;
pub use text::*;
pub use uniform_list::*;

View File

@@ -0,0 +1,232 @@
use smallvec::SmallVec;
use taffy::style::{Display, Position};
use crate::{
point, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, ParentComponent, Pixels,
Point, Size, Style,
};
pub struct OverlayState {
child_layout_ids: SmallVec<[LayoutId; 4]>,
}
pub struct Overlay<V> {
children: SmallVec<[AnyElement<V>; 2]>,
anchor_corner: AnchorCorner,
fit_mode: OverlayFitMode,
// todo!();
anchor_position: Option<Point<Pixels>>,
// position_mode: OverlayPositionMode,
}
/// overlay gives you a floating element that will avoid overflowing the window bounds.
/// Its children should have no margin to avoid measurement issues.
pub fn overlay<V: 'static>() -> Overlay<V> {
Overlay {
children: SmallVec::new(),
anchor_corner: AnchorCorner::TopLeft,
fit_mode: OverlayFitMode::SwitchAnchor,
anchor_position: None,
}
}
impl<V> Overlay<V> {
/// Sets which corner of the overlay should be anchored to the current position.
pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
self.anchor_corner = anchor;
self
}
/// Sets the position in window co-ordinates
/// (otherwise the location the overlay is rendered is used)
pub fn position(mut self, anchor: Point<Pixels>) -> Self {
self.anchor_position = Some(anchor);
self
}
/// Snap to window edge instead of switching anchor corner when an overflow would occur.
pub fn snap_to_window(mut self) -> Self {
self.fit_mode = OverlayFitMode::SnapToWindow;
self
}
}
impl<V: 'static> ParentComponent<V> for Overlay<V> {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
&mut self.children
}
}
impl<V: 'static> Component<V> for Overlay<V> {
fn render(self) -> AnyElement<V> {
AnyElement::new(self)
}
}
impl<V: 'static> Element<V> for Overlay<V> {
type ElementState = OverlayState;
fn element_id(&self) -> Option<crate::ElementId> {
None
}
fn layout(
&mut self,
view_state: &mut V,
_: Option<Self::ElementState>,
cx: &mut crate::ViewContext<V>,
) -> (crate::LayoutId, Self::ElementState) {
let child_layout_ids = self
.children
.iter_mut()
.map(|child| child.layout(view_state, cx))
.collect::<SmallVec<_>>();
let mut overlay_style = Style::default();
overlay_style.position = Position::Absolute;
overlay_style.display = Display::Flex;
let layout_id = cx.request_layout(&overlay_style, child_layout_ids.iter().copied());
(layout_id, OverlayState { child_layout_ids })
}
fn paint(
&mut self,
bounds: crate::Bounds<crate::Pixels>,
view_state: &mut V,
element_state: &mut Self::ElementState,
cx: &mut crate::ViewContext<V>,
) {
if element_state.child_layout_ids.is_empty() {
return;
}
let mut child_min = point(Pixels::MAX, Pixels::MAX);
let mut child_max = Point::default();
for child_layout_id in &element_state.child_layout_ids {
let child_bounds = cx.layout_bounds(*child_layout_id);
child_min = child_min.min(&child_bounds.origin);
child_max = child_max.max(&child_bounds.lower_right());
}
let size: Size<Pixels> = (child_max - child_min).into();
let origin = self.anchor_position.unwrap_or(bounds.origin);
let mut desired = self.anchor_corner.get_bounds(origin, size);
let limits = Bounds {
origin: Point::zero(),
size: cx.viewport_size(),
};
match self.fit_mode {
OverlayFitMode::SnapToWindow => {
// Snap the horizontal edges of the overlay to the horizontal edges of the window if
// its horizontal bounds overflow
if desired.right() > limits.right() {
desired.origin.x -= desired.right() - limits.right();
} else if desired.left() < limits.left() {
desired.origin.x = limits.origin.x;
}
// Snap the vertical edges of the overlay to the vertical edges of the window if
// its vertical bounds overflow.
if desired.bottom() > limits.bottom() {
desired.origin.y -= desired.bottom() - limits.bottom();
} else if desired.top() < limits.top() {
desired.origin.y = limits.origin.y;
}
}
OverlayFitMode::SwitchAnchor => {
let mut anchor_corner = self.anchor_corner;
if desired.left() < limits.left() || desired.right() > limits.right() {
anchor_corner = anchor_corner.switch_axis(Axis::Horizontal);
}
if bounds.top() < limits.top() || bounds.bottom() > limits.bottom() {
anchor_corner = anchor_corner.switch_axis(Axis::Vertical);
}
// Update bounds if needed
if anchor_corner != self.anchor_corner {
desired = anchor_corner.get_bounds(origin, size)
}
}
OverlayFitMode::None => {}
}
cx.with_element_offset(desired.origin - bounds.origin, |cx| {
for child in &mut self.children {
child.paint(view_state, cx);
}
})
}
}
enum Axis {
Horizontal,
Vertical,
}
#[derive(Copy, Clone)]
pub enum OverlayFitMode {
SnapToWindow,
SwitchAnchor,
None,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum AnchorCorner {
TopLeft,
TopRight,
BottomLeft,
BottomRight,
}
impl AnchorCorner {
fn get_bounds(&self, origin: Point<Pixels>, size: Size<Pixels>) -> Bounds<Pixels> {
let origin = match self {
Self::TopLeft => origin,
Self::TopRight => Point {
x: origin.x - size.width,
y: origin.y,
},
Self::BottomLeft => Point {
x: origin.x,
y: origin.y - size.height,
},
Self::BottomRight => Point {
x: origin.x - size.width,
y: origin.y - size.height,
},
};
Bounds { origin, size }
}
pub fn corner(&self, bounds: Bounds<Pixels>) -> Point<Pixels> {
match self {
Self::TopLeft => bounds.origin,
Self::TopRight => bounds.upper_right(),
Self::BottomLeft => bounds.lower_left(),
Self::BottomRight => bounds.lower_right(),
}
}
fn switch_axis(self, axis: Axis) -> Self {
match axis {
Axis::Vertical => match self {
AnchorCorner::TopLeft => AnchorCorner::BottomLeft,
AnchorCorner::TopRight => AnchorCorner::BottomRight,
AnchorCorner::BottomLeft => AnchorCorner::TopLeft,
AnchorCorner::BottomRight => AnchorCorner::TopRight,
},
Axis::Horizontal => match self {
AnchorCorner::TopLeft => AnchorCorner::TopRight,
AnchorCorner::TopRight => AnchorCorner::TopLeft,
AnchorCorner::BottomLeft => AnchorCorner::BottomRight,
AnchorCorner::BottomRight => AnchorCorner::BottomLeft,
},
}
}
}

View File

@@ -37,21 +37,12 @@ impl<V> Element<V> for Svg<V> {
self.interactivity.element_id.clone()
}
fn initialize(
fn layout(
&mut self,
_view_state: &mut V,
element_state: Option<Self::ElementState>,
cx: &mut ViewContext<V>,
) -> Self::ElementState {
self.interactivity.initialize(element_state, cx)
}
fn layout(
&mut self,
_view_state: &mut V,
element_state: &mut Self::ElementState,
cx: &mut ViewContext<V>,
) -> LayoutId {
) -> (LayoutId, Self::ElementState) {
self.interactivity.layout(element_state, cx, |style, cx| {
cx.request_layout(&style, None)
})

View File

@@ -1,96 +1,51 @@
use crate::{
AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, Line, Pixels, SharedString,
Size, TextRun, ViewContext,
AnyElement, BorrowWindow, Bounds, Component, Element, ElementId, LayoutId, Pixels,
SharedString, Size, TextRun, ViewContext, WrappedLine,
};
use parking_lot::Mutex;
use parking_lot::{Mutex, MutexGuard};
use smallvec::SmallVec;
use std::{marker::PhantomData, sync::Arc};
use std::{cell::Cell, rc::Rc, sync::Arc};
use util::ResultExt;
impl<V: 'static> Component<V> for SharedString {
fn render(self) -> AnyElement<V> {
Text {
text: self,
runs: None,
state_type: PhantomData,
}
.render()
}
}
impl<V: 'static> Component<V> for &'static str {
fn render(self) -> AnyElement<V> {
Text {
text: self.into(),
runs: None,
state_type: PhantomData,
}
.render()
}
}
// TODO: Figure out how to pass `String` to `child` without this.
// This impl doesn't exist in the `gpui2` crate.
impl<V: 'static> Component<V> for String {
fn render(self) -> AnyElement<V> {
Text {
text: self.into(),
runs: None,
state_type: PhantomData,
}
.render()
}
}
pub struct Text<V> {
pub struct Text {
text: SharedString,
runs: Option<Vec<TextRun>>,
state_type: PhantomData<V>,
}
impl<V: 'static> Text<V> {
/// styled renders text that has different runs of different styles.
/// callers are responsible for setting the correct style for each run.
////
/// For uniform text you can usually just pass a string as a child, and
/// cx.text_style() will be used automatically.
impl Text {
/// Renders text with runs of different styles.
///
/// Callers are responsible for setting the correct style for each run.
/// For text with a uniform style, you can usually avoid calling this constructor
/// and just pass text directly.
pub fn styled(text: SharedString, runs: Vec<TextRun>) -> Self {
Text {
text,
runs: Some(runs),
state_type: Default::default(),
}
}
}
impl<V: 'static> Component<V> for Text<V> {
impl<V: 'static> Component<V> for Text {
fn render(self) -> AnyElement<V> {
AnyElement::new(self)
}
}
impl<V: 'static> Element<V> for Text<V> {
type ElementState = Arc<Mutex<Option<TextElementState>>>;
impl<V: 'static> Element<V> for Text {
type ElementState = TextState;
fn element_id(&self) -> Option<crate::ElementId> {
None
}
fn initialize(
&mut self,
_view_state: &mut V,
element_state: Option<Self::ElementState>,
_cx: &mut ViewContext<V>,
) -> Self::ElementState {
element_state.unwrap_or_default()
}
fn layout(
&mut self,
_view: &mut V,
element_state: &mut Self::ElementState,
element_state: Option<Self::ElementState>,
cx: &mut ViewContext<V>,
) -> LayoutId {
) -> (LayoutId, Self::ElementState) {
let element_state = element_state.unwrap_or_default();
let text_system = cx.text_system().clone();
let text_style = cx.text_style();
let font_size = text_style.font_size.to_pixels(cx.rem_size());
@@ -111,7 +66,7 @@ impl<V: 'static> Element<V> for Text<V> {
let element_state = element_state.clone();
move |known_dimensions, _| {
let Some(lines) = text_system
.layout_text(
.shape_text(
&text,
font_size,
&runs[..],
@@ -119,36 +74,29 @@ impl<V: 'static> Element<V> for Text<V> {
)
.log_err()
else {
element_state.lock().replace(TextElementState {
element_state.lock().replace(TextStateInner {
lines: Default::default(),
line_height,
});
return Size::default();
};
let line_count = lines
.iter()
.map(|line| line.wrap_count() + 1)
.sum::<usize>();
let size = Size {
width: lines
.iter()
.map(|line| line.layout.width)
.max()
.unwrap()
.ceil(),
height: line_height * line_count,
};
let mut size: Size<Pixels> = Size::default();
for line in &lines {
let line_size = line.size(line_height);
size.height += line_size.height;
size.width = size.width.max(line_size.width);
}
element_state
.lock()
.replace(TextElementState { lines, line_height });
.replace(TextStateInner { lines, line_height });
size
}
});
layout_id
(layout_id, element_state)
}
fn paint(
@@ -173,7 +121,104 @@ impl<V: 'static> Element<V> for Text<V> {
}
}
pub struct TextElementState {
lines: SmallVec<[Line; 1]>,
#[derive(Default, Clone)]
pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
impl TextState {
fn lock(&self) -> MutexGuard<Option<TextStateInner>> {
self.0.lock()
}
}
struct TextStateInner {
lines: SmallVec<[WrappedLine; 1]>,
line_height: Pixels,
}
struct InteractiveText {
id: ElementId,
text: Text,
}
struct InteractiveTextState {
text_state: TextState,
clicked_range_ixs: Rc<Cell<SmallVec<[usize; 1]>>>,
}
impl<V: 'static> Element<V> for InteractiveText {
type ElementState = InteractiveTextState;
fn element_id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn layout(
&mut self,
view_state: &mut V,
element_state: Option<Self::ElementState>,
cx: &mut ViewContext<V>,
) -> (LayoutId, Self::ElementState) {
if let Some(InteractiveTextState {
text_state,
clicked_range_ixs,
}) = element_state
{
let (layout_id, text_state) = self.text.layout(view_state, Some(text_state), cx);
let element_state = InteractiveTextState {
text_state,
clicked_range_ixs,
};
(layout_id, element_state)
} else {
let (layout_id, text_state) = self.text.layout(view_state, None, cx);
let element_state = InteractiveTextState {
text_state,
clicked_range_ixs: Rc::default(),
};
(layout_id, element_state)
}
}
fn paint(
&mut self,
bounds: Bounds<Pixels>,
view_state: &mut V,
element_state: &mut Self::ElementState,
cx: &mut ViewContext<V>,
) {
self.text
.paint(bounds, view_state, &mut element_state.text_state, cx)
}
}
impl<V: 'static> Component<V> for SharedString {
fn render(self) -> AnyElement<V> {
Text {
text: self,
runs: None,
}
.render()
}
}
impl<V: 'static> Component<V> for &'static str {
fn render(self) -> AnyElement<V> {
Text {
text: self.into(),
runs: None,
}
.render()
}
}
// TODO: Figure out how to pass `String` to `child` without this.
// This impl doesn't exist in the `gpui2` crate.
impl<V: 'static> Component<V> for String {
fn render(self) -> AnyElement<V> {
Text {
text: self.into(),
runs: None,
}
.render()
}
}

View File

@@ -3,9 +3,8 @@ use crate::{
ElementId, InteractiveComponent, InteractiveElementState, Interactivity, LayoutId, Pixels,
Point, Size, StyleRefinement, Styled, ViewContext,
};
use parking_lot::Mutex;
use smallvec::SmallVec;
use std::{cmp, mem, ops::Range, sync::Arc};
use std::{cell::RefCell, cmp, mem, ops::Range, rc::Rc};
use taffy::style::Overflow;
/// uniform_list provides lazy rendering for a set of items that are of uniform height.
@@ -61,23 +60,23 @@ pub struct UniformList<V: 'static> {
}
#[derive(Clone, Default)]
pub struct UniformListScrollHandle(Arc<Mutex<Option<ScrollHandleState>>>);
pub struct UniformListScrollHandle(Rc<RefCell<Option<ScrollHandleState>>>);
#[derive(Clone, Debug)]
struct ScrollHandleState {
item_height: Pixels,
list_height: Pixels,
scroll_offset: Arc<Mutex<Point<Pixels>>>,
scroll_offset: Rc<RefCell<Point<Pixels>>>,
}
impl UniformListScrollHandle {
pub fn new() -> Self {
Self(Arc::new(Mutex::new(None)))
Self(Rc::new(RefCell::new(None)))
}
pub fn scroll_to_item(&self, ix: usize) {
if let Some(state) = &*self.0.lock() {
let mut scroll_offset = state.scroll_offset.lock();
if let Some(state) = &*self.0.borrow() {
let mut scroll_offset = state.scroll_offset.borrow_mut();
let item_top = state.item_height * ix;
let item_bottom = item_top + state.item_height;
let scroll_top = -scroll_offset.y;
@@ -109,62 +108,54 @@ impl<V: 'static> Element<V> for UniformList<V> {
Some(self.id.clone())
}
fn initialize(
fn layout(
&mut self,
view_state: &mut V,
element_state: Option<Self::ElementState>,
cx: &mut ViewContext<V>,
) -> Self::ElementState {
if let Some(mut element_state) = element_state {
element_state.interactive = self
.interactivity
.initialize(Some(element_state.interactive), cx);
element_state
} else {
let item_size = self.measure_item(view_state, None, cx);
UniformListState {
interactive: self.interactivity.initialize(None, cx),
item_size,
}
}
}
fn layout(
&mut self,
_view_state: &mut V,
element_state: &mut Self::ElementState,
cx: &mut ViewContext<V>,
) -> LayoutId {
) -> (LayoutId, Self::ElementState) {
let max_items = self.item_count;
let item_size = element_state.item_size;
let rem_size = cx.rem_size();
let item_size = element_state
.as_ref()
.map(|s| s.item_size)
.unwrap_or_else(|| self.measure_item(view_state, None, cx));
self.interactivity
.layout(&mut element_state.interactive, cx, |style, cx| {
cx.request_measured_layout(
style,
rem_size,
move |known_dimensions: Size<Option<Pixels>>,
available_space: Size<AvailableSpace>| {
let desired_height = item_size.height * max_items;
let width = known_dimensions
.width
.unwrap_or(match available_space.width {
AvailableSpace::Definite(x) => x,
let (layout_id, interactive) =
self.interactivity
.layout(element_state.map(|s| s.interactive), cx, |style, cx| {
cx.request_measured_layout(
style,
rem_size,
move |known_dimensions: Size<Option<Pixels>>,
available_space: Size<AvailableSpace>| {
let desired_height = item_size.height * max_items;
let width =
known_dimensions
.width
.unwrap_or(match available_space.width {
AvailableSpace::Definite(x) => x,
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
item_size.width
}
});
let height = match available_space.height {
AvailableSpace::Definite(x) => desired_height.min(x),
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
item_size.width
desired_height
}
});
let height = match available_space.height {
AvailableSpace::Definite(x) => desired_height.min(x),
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
desired_height
}
};
size(width, height)
},
)
})
};
size(width, height)
},
)
});
let element_state = UniformListState {
interactive,
item_size,
};
(layout_id, element_state)
}
fn paint(
@@ -196,7 +187,7 @@ impl<V: 'static> Element<V> for UniformList<V> {
let shared_scroll_offset = element_state
.interactive
.scroll_offset
.get_or_insert_with(Arc::default)
.get_or_insert_with(Rc::default)
.clone();
interactivity.paint(
@@ -222,7 +213,7 @@ impl<V: 'static> Element<V> for UniformList<V> {
.measure_item(view_state, Some(padded_bounds.size.width), cx)
.height;
if let Some(scroll_handle) = self.scroll_handle.clone() {
scroll_handle.0.lock().replace(ScrollHandleState {
scroll_handle.0.borrow_mut().replace(ScrollHandleState {
item_height,
list_height: padded_bounds.size.height,
scroll_offset: shared_scroll_offset,

View File

@@ -5,10 +5,11 @@ use std::{
fmt::Debug,
marker::PhantomData,
mem,
num::NonZeroUsize,
pin::Pin,
rc::Rc,
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
Arc,
},
task::{Context, Poll},
@@ -71,30 +72,57 @@ impl<T> Future for Task<T> {
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct TaskLabel(NonZeroUsize);
impl TaskLabel {
pub fn new() -> Self {
static NEXT_TASK_LABEL: AtomicUsize = AtomicUsize::new(1);
Self(NEXT_TASK_LABEL.fetch_add(1, SeqCst).try_into().unwrap())
}
}
type AnyLocalFuture<R> = Pin<Box<dyn 'static + Future<Output = R>>>;
type AnyFuture<R> = Pin<Box<dyn 'static + Send + Future<Output = R>>>;
impl BackgroundExecutor {
pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
Self { dispatcher }
}
/// Enqueues the given closure to be run on any thread. The closure returns
/// a future which will be run to completion on any available thread.
/// Enqueues the given future to be run to completion on a background thread.
pub fn spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
where
R: Send + 'static,
{
self.spawn_internal::<R>(Box::pin(future), None)
}
/// Enqueues the given future to be run to completion on a background thread.
/// The given label can be used to control the priority of the task in tests.
pub fn spawn_labeled<R>(
&self,
label: TaskLabel,
future: impl Future<Output = R> + Send + 'static,
) -> Task<R>
where
R: Send + 'static,
{
self.spawn_internal::<R>(Box::pin(future), Some(label))
}
fn spawn_internal<R: Send + 'static>(
&self,
future: AnyFuture<R>,
label: Option<TaskLabel>,
) -> Task<R> {
let dispatcher = self.dispatcher.clone();
fn inner<R: Send + 'static>(
dispatcher: Arc<dyn PlatformDispatcher>,
future: AnyFuture<R>,
) -> Task<R> {
let (runnable, task) =
async_task::spawn(future, move |runnable| dispatcher.dispatch(runnable));
runnable.schedule();
Task::Spawned(task)
}
inner::<R>(dispatcher, Box::pin(future))
let (runnable, task) =
async_task::spawn(future, move |runnable| dispatcher.dispatch(runnable, label));
runnable.schedule();
Task::Spawned(task)
}
#[cfg(any(test, feature = "test-support"))]
@@ -130,7 +158,7 @@ impl BackgroundExecutor {
match future.as_mut().poll(&mut cx) {
Poll::Ready(result) => return result,
Poll::Pending => {
if !self.dispatcher.poll(background_only) {
if !self.dispatcher.tick(background_only) {
if awoken.swap(false, SeqCst) {
continue;
}
@@ -216,11 +244,21 @@ impl BackgroundExecutor {
self.dispatcher.as_test().unwrap().simulate_random_delay()
}
#[cfg(any(test, feature = "test-support"))]
pub fn deprioritize(&self, task_label: TaskLabel) {
self.dispatcher.as_test().unwrap().deprioritize(task_label)
}
#[cfg(any(test, feature = "test-support"))]
pub fn advance_clock(&self, duration: Duration) {
self.dispatcher.as_test().unwrap().advance_clock(duration)
}
#[cfg(any(test, feature = "test-support"))]
pub fn tick(&self) -> bool {
self.dispatcher.as_test().unwrap().tick(false)
}
#[cfg(any(test, feature = "test-support"))]
pub fn run_until_parked(&self) {
self.dispatcher.as_test().unwrap().run_until_parked()

View File

@@ -335,11 +335,15 @@ where
};
Bounds { origin, size }
}
pub fn new(origin: Point<T>, size: Size<T>) -> Self {
Bounds { origin, size }
}
}
impl<T> Bounds<T>
where
T: Clone + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T> + Default,
T: Clone + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T> + Default + Half,
{
pub fn intersects(&self, other: &Bounds<T>) -> bool {
let my_lower_right = self.lower_right();
@@ -358,6 +362,13 @@ where
self.size.width = self.size.width.clone() + double_amount.clone();
self.size.height = self.size.height.clone() + double_amount;
}
pub fn center(&self) -> Point<T> {
Point {
x: self.origin.x.clone() + self.size.width.clone().half(),
y: self.origin.y.clone() + self.size.height.clone().half(),
}
}
}
impl<T: Clone + Default + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T>> Bounds<T> {
@@ -421,6 +432,22 @@ impl<T> Bounds<T>
where
T: Add<T, Output = T> + Clone + Default + Debug,
{
pub fn top(&self) -> T {
self.origin.y.clone()
}
pub fn bottom(&self) -> T {
self.origin.y.clone() + self.size.height.clone()
}
pub fn left(&self) -> T {
self.origin.x.clone()
}
pub fn right(&self) -> T {
self.origin.x.clone() + self.size.width.clone()
}
pub fn upper_right(&self) -> Point<T> {
Point {
x: self.origin.x.clone() + self.size.width.clone(),
@@ -1191,6 +1218,46 @@ impl From<()> for Length {
}
}
pub trait Half {
fn half(&self) -> Self;
}
impl Half for f32 {
fn half(&self) -> Self {
self / 2.
}
}
impl Half for DevicePixels {
fn half(&self) -> Self {
Self(self.0 / 2)
}
}
impl Half for ScaledPixels {
fn half(&self) -> Self {
Self(self.0 / 2.)
}
}
impl Half for Pixels {
fn half(&self) -> Self {
Self(self.0 / 2.)
}
}
impl Half for Rems {
fn half(&self) -> Self {
Self(self.0 / 2.)
}
}
impl Half for GlobalPixels {
fn half(&self) -> Self {
Self(self.0 / 2.)
}
}
pub trait IsZero {
fn is_zero(&self) -> bool;
}

View File

@@ -49,11 +49,13 @@ pub use input::*;
pub use interactive::*;
pub use key_dispatch::*;
pub use keymap::*;
pub use linkme;
pub use platform::*;
use private::Sealed;
pub use refineable::*;
pub use scene::*;
pub use serde;
pub use serde_derive;
pub use serde_json;
pub use smallvec;
pub use smol::Timer;
@@ -135,6 +137,10 @@ pub trait VisualContext: Context {
) -> Self::Result<View<V>>
where
V: Render;
fn focus_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
where
V: FocusableView;
}
pub trait Entity<T>: Sealed {

View File

@@ -1,6 +1,6 @@
use crate::{
build_action_from_type, Action, DispatchPhase, FocusId, KeyBinding, KeyContext, KeyMatch,
Keymap, Keystroke, KeystrokeMatcher, WindowContext,
Action, ActionRegistry, DispatchPhase, FocusId, KeyBinding, KeyContext, KeyMatch, Keymap,
Keystroke, KeystrokeMatcher, WindowContext,
};
use collections::HashMap;
use parking_lot::Mutex;
@@ -10,7 +10,6 @@ use std::{
rc::Rc,
sync::Arc,
};
use util::ResultExt;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct DispatchNodeId(usize);
@@ -22,6 +21,7 @@ pub(crate) struct DispatchTree {
focusable_node_ids: HashMap<FocusId, DispatchNodeId>,
keystroke_matchers: HashMap<SmallVec<[KeyContext; 4]>, KeystrokeMatcher>,
keymap: Arc<Mutex<Keymap>>,
action_registry: Rc<ActionRegistry>,
}
#[derive(Default)]
@@ -41,7 +41,7 @@ pub(crate) struct DispatchActionListener {
}
impl DispatchTree {
pub fn new(keymap: Arc<Mutex<Keymap>>) -> Self {
pub fn new(keymap: Arc<Mutex<Keymap>>, action_registry: Rc<ActionRegistry>) -> Self {
Self {
node_stack: Vec::new(),
context_stack: Vec::new(),
@@ -49,6 +49,7 @@ impl DispatchTree {
focusable_node_ids: HashMap::default(),
keystroke_matchers: HashMap::default(),
keymap,
action_registry,
}
}
@@ -60,7 +61,7 @@ impl DispatchTree {
self.keystroke_matchers.clear();
}
pub fn push_node(&mut self, context: KeyContext, old_dispatcher: &mut Self) {
pub fn push_node(&mut self, context: KeyContext) {
let parent = self.node_stack.last().copied();
let node_id = DispatchNodeId(self.nodes.len());
self.nodes.push(DispatchNode {
@@ -71,12 +72,6 @@ impl DispatchTree {
if !context.is_empty() {
self.active_node().context = context.clone();
self.context_stack.push(context);
if let Some((context_stack, matcher)) = old_dispatcher
.keystroke_matchers
.remove_entry(self.context_stack.as_slice())
{
self.keystroke_matchers.insert(context_stack, matcher);
}
}
}
@@ -87,6 +82,33 @@ impl DispatchTree {
}
}
pub fn clear_keystroke_matchers(&mut self) {
self.keystroke_matchers.clear();
}
/// Preserve keystroke matchers from previous frames to support multi-stroke
/// bindings across multiple frames.
pub fn preserve_keystroke_matchers(&mut self, old_tree: &mut Self, focus_id: Option<FocusId>) {
if let Some(node_id) = focus_id.and_then(|focus_id| self.focusable_node_id(focus_id)) {
let dispatch_path = self.dispatch_path(node_id);
self.context_stack.clear();
for node_id in dispatch_path {
let node = self.node(node_id);
if !node.context.is_empty() {
self.context_stack.push(node.context.clone());
}
if let Some((context_stack, matcher)) = old_tree
.keystroke_matchers
.remove_entry(self.context_stack.as_slice())
{
self.keystroke_matchers.insert(context_stack, matcher);
}
}
}
}
pub fn on_key_event(&mut self, listener: KeyListener) {
self.active_node().key_listeners.push(listener);
}
@@ -132,7 +154,9 @@ impl DispatchTree {
for node_id in self.dispatch_path(*node) {
let node = &self.nodes[node_id.0];
for DispatchActionListener { action_type, .. } in &node.action_listeners {
actions.extend(build_action_from_type(action_type).log_err());
// Intentionally silence these errors without logging.
// If an action cannot be built by default, it's not available.
actions.extend(self.action_registry.build_action_type(action_type).ok());
}
}
}

View File

@@ -1,7 +1,10 @@
use crate::{KeyBinding, KeyBindingContextPredicate, Keystroke};
use crate::{KeyBinding, KeyBindingContextPredicate, Keystroke, NoAction};
use collections::HashSet;
use smallvec::SmallVec;
use std::{any::TypeId, collections::HashMap};
use std::{
any::{Any, TypeId},
collections::HashMap,
};
#[derive(Copy, Clone, Eq, PartialEq, Default)]
pub struct KeymapVersion(usize);
@@ -37,20 +40,19 @@ impl Keymap {
}
pub fn add_bindings<T: IntoIterator<Item = KeyBinding>>(&mut self, bindings: T) {
// todo!("no action")
// let no_action_id = (NoAction {}).id();
let no_action_id = &(NoAction {}).type_id();
let mut new_bindings = Vec::new();
let has_new_disabled_keystrokes = false;
let mut has_new_disabled_keystrokes = false;
for binding in bindings {
// if binding.action().id() == no_action_id {
// has_new_disabled_keystrokes |= self
// .disabled_keystrokes
// .entry(binding.keystrokes)
// .or_default()
// .insert(binding.context_predicate);
// } else {
new_bindings.push(binding);
// }
if binding.action.type_id() == *no_action_id {
has_new_disabled_keystrokes |= self
.disabled_keystrokes
.entry(binding.keystrokes)
.or_default()
.insert(binding.context_predicate);
} else {
new_bindings.push(binding);
}
}
if has_new_disabled_keystrokes {

View File

@@ -8,7 +8,7 @@ use crate::{
point, size, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId,
FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, LineLayout,
Pixels, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, Scene,
SharedString, Size,
SharedString, Size, TaskLabel,
};
use anyhow::{anyhow, bail};
use async_task::Runnable;
@@ -162,10 +162,10 @@ pub(crate) trait PlatformWindow {
pub trait PlatformDispatcher: Send + Sync {
fn is_main_thread(&self) -> bool;
fn dispatch(&self, runnable: Runnable);
fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>);
fn dispatch_on_main_thread(&self, runnable: Runnable);
fn dispatch_after(&self, duration: Duration, runnable: Runnable);
fn poll(&self, background_only: bool) -> bool;
fn tick(&self, background_only: bool) -> bool;
fn park(&self);
fn unparker(&self) -> Unparker;

View File

@@ -2,7 +2,7 @@
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
use crate::PlatformDispatcher;
use crate::{PlatformDispatcher, TaskLabel};
use async_task::Runnable;
use objc::{
class, msg_send,
@@ -37,7 +37,7 @@ impl PlatformDispatcher for MacDispatcher {
is_main_thread == YES
}
fn dispatch(&self, runnable: Runnable) {
fn dispatch(&self, runnable: Runnable, _: Option<TaskLabel>) {
unsafe {
dispatch_async_f(
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.try_into().unwrap(), 0),
@@ -71,7 +71,7 @@ impl PlatformDispatcher for MacDispatcher {
}
}
fn poll(&self, _background_only: bool) -> bool {
fn tick(&self, _background_only: bool) -> bool {
false
}

View File

@@ -343,10 +343,10 @@ impl MacTextSystemState {
// Construct the attributed string, converting UTF8 ranges to UTF16 ranges.
let mut string = CFMutableAttributedString::new();
{
string.replace_str(&CFString::new(text), CFRange::init(0, 0));
string.replace_str(&CFString::new(text.as_ref()), CFRange::init(0, 0));
let utf16_line_len = string.char_len() as usize;
let mut ix_converter = StringIndexConverter::new(text);
let mut ix_converter = StringIndexConverter::new(text.as_ref());
for run in font_runs {
let utf8_end = ix_converter.utf8_ix + run.len;
let utf16_start = ix_converter.utf16_ix;
@@ -390,7 +390,7 @@ impl MacTextSystemState {
};
let font_id = self.id_for_native_font(font);
let mut ix_converter = StringIndexConverter::new(text);
let mut ix_converter = StringIndexConverter::new(text.as_ref());
let mut glyphs = SmallVec::new();
for ((glyph_id, position), glyph_utf16_ix) in run
.glyphs()
@@ -413,11 +413,11 @@ impl MacTextSystemState {
let typographic_bounds = line.get_typographic_bounds();
LineLayout {
runs,
font_size,
width: typographic_bounds.width.into(),
ascent: typographic_bounds.ascent.into(),
descent: typographic_bounds.descent.into(),
runs,
font_size,
len: text.len(),
}
}

View File

@@ -1141,7 +1141,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
let event = unsafe { InputEvent::from_native(native_event, Some(window_height)) };
if let Some(mut event) = event {
let synthesized_second_event = match &mut event {
match &mut event {
InputEvent::MouseDown(
event @ MouseDownEvent {
button: MouseButton::Left,
@@ -1149,6 +1149,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
..
},
) => {
// On mac, a ctrl-left click should be handled as a right click.
*event = MouseDownEvent {
button: MouseButton::Right,
modifiers: Modifiers {
@@ -1158,26 +1159,30 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
click_count: 1,
..*event
};
Some(InputEvent::MouseDown(MouseDownEvent {
button: MouseButton::Right,
..*event
}))
}
// Because we map a ctrl-left_down to a right_down -> right_up let's ignore
// the ctrl-left_up to avoid having a mismatch in button down/up events if the
// user is still holding ctrl when releasing the left mouse button
InputEvent::MouseUp(MouseUpEvent {
button: MouseButton::Left,
modifiers: Modifiers { control: true, .. },
..
}) => {
lock.synthetic_drag_counter += 1;
return;
InputEvent::MouseUp(
event @ MouseUpEvent {
button: MouseButton::Left,
modifiers: Modifiers { control: true, .. },
..
},
) => {
*event = MouseUpEvent {
button: MouseButton::Right,
modifiers: Modifiers {
control: false,
..event.modifiers
},
click_count: 1,
..*event
};
}
_ => None,
_ => {}
};
match &event {
@@ -1227,9 +1232,6 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
if let Some(mut callback) = lock.event_callback.take() {
drop(lock);
callback(event);
if let Some(event) = synthesized_second_event {
callback(event);
}
window_state.lock().event_callback = Some(callback);
}
}

View File

@@ -1,7 +1,7 @@
use crate::PlatformDispatcher;
use crate::{PlatformDispatcher, TaskLabel};
use async_task::Runnable;
use backtrace::Backtrace;
use collections::{HashMap, VecDeque};
use collections::{HashMap, HashSet, VecDeque};
use parking::{Parker, Unparker};
use parking_lot::Mutex;
use rand::prelude::*;
@@ -28,12 +28,14 @@ struct TestDispatcherState {
random: StdRng,
foreground: HashMap<TestDispatcherId, VecDeque<Runnable>>,
background: Vec<Runnable>,
deprioritized_background: Vec<Runnable>,
delayed: Vec<(Duration, Runnable)>,
time: Duration,
is_main_thread: bool,
next_id: TestDispatcherId,
allow_parking: bool,
waiting_backtrace: Option<Backtrace>,
deprioritized_task_labels: HashSet<TaskLabel>,
}
impl TestDispatcher {
@@ -43,12 +45,14 @@ impl TestDispatcher {
random,
foreground: HashMap::default(),
background: Vec::new(),
deprioritized_background: Vec::new(),
delayed: Vec::new(),
time: Duration::ZERO,
is_main_thread: true,
next_id: TestDispatcherId(1),
allow_parking: false,
waiting_backtrace: None,
deprioritized_task_labels: Default::default(),
};
TestDispatcher {
@@ -101,8 +105,15 @@ impl TestDispatcher {
}
}
pub fn deprioritize(&self, task_label: TaskLabel) {
self.state
.lock()
.deprioritized_task_labels
.insert(task_label);
}
pub fn run_until_parked(&self) {
while self.poll(false) {}
while self.tick(false) {}
}
pub fn parking_allowed(&self) -> bool {
@@ -150,8 +161,17 @@ impl PlatformDispatcher for TestDispatcher {
self.state.lock().is_main_thread
}
fn dispatch(&self, runnable: Runnable) {
self.state.lock().background.push(runnable);
fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>) {
{
let mut state = self.state.lock();
if label.map_or(false, |label| {
state.deprioritized_task_labels.contains(&label)
}) {
state.deprioritized_background.push(runnable);
} else {
state.background.push(runnable);
}
}
self.unparker.unpark();
}
@@ -174,7 +194,7 @@ impl PlatformDispatcher for TestDispatcher {
state.delayed.insert(ix, (next_time, runnable));
}
fn poll(&self, background_only: bool) -> bool {
fn tick(&self, background_only: bool) -> bool {
let mut state = self.state.lock();
while let Some((deadline, _)) = state.delayed.first() {
@@ -196,34 +216,41 @@ impl PlatformDispatcher for TestDispatcher {
};
let background_len = state.background.len();
let runnable;
let main_thread;
if foreground_len == 0 && background_len == 0 {
return false;
}
let main_thread = state.random.gen_ratio(
foreground_len as u32,
(foreground_len + background_len) as u32,
);
let was_main_thread = state.is_main_thread;
state.is_main_thread = main_thread;
let runnable = if main_thread {
let state = &mut *state;
let runnables = state
.foreground
.values_mut()
.filter(|runnables| !runnables.is_empty())
.choose(&mut state.random)
.unwrap();
runnables.pop_front().unwrap()
let deprioritized_background_len = state.deprioritized_background.len();
if deprioritized_background_len == 0 {
return false;
}
let ix = state.random.gen_range(0..deprioritized_background_len);
main_thread = false;
runnable = state.deprioritized_background.swap_remove(ix);
} else {
let ix = state.random.gen_range(0..background_len);
state.background.swap_remove(ix)
main_thread = state.random.gen_ratio(
foreground_len as u32,
(foreground_len + background_len) as u32,
);
if main_thread {
let state = &mut *state;
runnable = state
.foreground
.values_mut()
.filter(|runnables| !runnables.is_empty())
.choose(&mut state.random)
.unwrap()
.pop_front()
.unwrap();
} else {
let ix = state.random.gen_range(0..background_len);
runnable = state.background.swap_remove(ix);
};
};
let was_main_thread = state.is_main_thread;
state.is_main_thread = main_thread;
drop(state);
runnable.run();
self.state.lock().is_main_thread = was_main_thread;
true

View File

@@ -203,6 +203,7 @@ impl TextStyle {
style: self.font_style,
},
color: self.color,
background_color: None,
underline: self.underline.clone(),
}
}

View File

@@ -3,20 +3,20 @@ mod line;
mod line_layout;
mod line_wrapper;
use anyhow::anyhow;
pub use font_features::*;
pub use line::*;
pub use line_layout::*;
pub use line_wrapper::*;
use smallvec::SmallVec;
use crate::{
px, Bounds, DevicePixels, Hsla, Pixels, PlatformTextSystem, Point, Result, SharedString, Size,
UnderlineStyle,
};
use anyhow::anyhow;
use collections::HashMap;
use core::fmt;
use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
use smallvec::SmallVec;
use std::{
cmp,
fmt::{Debug, Display, Formatter},
@@ -151,13 +151,79 @@ impl TextSystem {
}
}
pub fn layout_text(
pub fn layout_line(
&self,
text: &str,
font_size: Pixels,
runs: &[TextRun],
) -> Result<Arc<LineLayout>> {
let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
for run in runs.iter() {
let font_id = self.font_id(&run.font)?;
if let Some(last_run) = font_runs.last_mut() {
if last_run.font_id == font_id {
last_run.len += run.len;
continue;
}
}
font_runs.push(FontRun {
len: run.len,
font_id,
});
}
let layout = self
.line_layout_cache
.layout_line(&text, font_size, &font_runs);
font_runs.clear();
self.font_runs_pool.lock().push(font_runs);
Ok(layout)
}
pub fn shape_line(
&self,
text: SharedString,
font_size: Pixels,
runs: &[TextRun],
) -> Result<ShapedLine> {
debug_assert!(
text.find('\n').is_none(),
"text argument should not contain newlines"
);
let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
for run in runs {
if let Some(last_run) = decoration_runs.last_mut() {
if last_run.color == run.color && last_run.underline == run.underline {
last_run.len += run.len as u32;
continue;
}
}
decoration_runs.push(DecorationRun {
len: run.len as u32,
color: run.color,
underline: run.underline.clone(),
});
}
let layout = self.layout_line(text.as_ref(), font_size, runs)?;
Ok(ShapedLine {
layout,
text,
decoration_runs,
})
}
pub fn shape_text(
&self,
text: &str, // todo!("pass a SharedString and preserve it when passed a single line?")
font_size: Pixels,
runs: &[TextRun],
wrap_width: Option<Pixels>,
) -> Result<SmallVec<[Line; 1]>> {
) -> Result<SmallVec<[WrappedLine; 1]>> {
let mut runs = runs.iter().cloned().peekable();
let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
@@ -210,10 +276,11 @@ impl TextSystem {
let layout = self
.line_layout_cache
.layout_line(&line_text, font_size, &font_runs, wrap_width);
lines.push(Line {
.layout_wrapped_line(&line_text, font_size, &font_runs, wrap_width);
lines.push(WrappedLine {
layout,
decorations: decoration_runs,
decoration_runs,
text: SharedString::from(line_text),
});
line_start = line_end + 1; // Skip `\n` character.
@@ -384,6 +451,7 @@ pub struct TextRun {
pub len: usize,
pub font: Font,
pub color: Hsla,
pub background_color: Option<Hsla>,
pub underline: Option<UnderlineStyle>,
}

View File

@@ -1,5 +1,5 @@
use crate::{
black, point, px, size, BorrowWindow, Bounds, Hsla, Pixels, Point, Result, Size,
black, point, px, BorrowWindow, Bounds, Hsla, LineLayout, Pixels, Point, Result, SharedString,
UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout,
};
use derive_more::{Deref, DerefMut};
@@ -14,23 +14,17 @@ pub struct DecorationRun {
}
#[derive(Clone, Default, Debug, Deref, DerefMut)]
pub struct Line {
pub struct ShapedLine {
#[deref]
#[deref_mut]
pub(crate) layout: Arc<WrappedLineLayout>,
pub(crate) decorations: SmallVec<[DecorationRun; 32]>,
pub(crate) layout: Arc<LineLayout>,
pub text: SharedString,
pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>,
}
impl Line {
pub fn size(&self, line_height: Pixels) -> Size<Pixels> {
size(
self.layout.width,
line_height * (self.layout.wrap_boundaries.len() + 1),
)
}
pub fn wrap_count(&self) -> usize {
self.layout.wrap_boundaries.len()
impl ShapedLine {
pub fn len(&self) -> usize {
self.layout.len
}
pub fn paint(
@@ -39,75 +33,84 @@ impl Line {
line_height: Pixels,
cx: &mut WindowContext,
) -> Result<()> {
let padding_top =
(line_height - self.layout.layout.ascent - self.layout.layout.descent) / 2.;
let baseline_offset = point(px(0.), padding_top + self.layout.layout.ascent);
paint_line(
origin,
&self.layout,
line_height,
&self.decoration_runs,
None,
&[],
cx,
)?;
let mut style_runs = self.decorations.iter();
let mut wraps = self.layout.wrap_boundaries.iter().peekable();
let mut run_end = 0;
let mut color = black();
let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
let text_system = cx.text_system().clone();
Ok(())
}
}
let mut glyph_origin = origin;
let mut prev_glyph_position = Point::default();
for (run_ix, run) in self.layout.layout.runs.iter().enumerate() {
let max_glyph_size = text_system
.bounding_box(run.font_id, self.layout.layout.font_size)?
.size;
#[derive(Clone, Default, Debug, Deref, DerefMut)]
pub struct WrappedLine {
#[deref]
#[deref_mut]
pub(crate) layout: Arc<WrappedLineLayout>,
pub text: SharedString,
pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>,
}
for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
glyph_origin.x += glyph.position.x - prev_glyph_position.x;
impl WrappedLine {
pub fn len(&self) -> usize {
self.layout.len()
}
if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
wraps.next();
if let Some((underline_origin, underline_style)) = current_underline.take() {
cx.paint_underline(
underline_origin,
glyph_origin.x - underline_origin.x,
&underline_style,
)?;
}
pub fn paint(
&self,
origin: Point<Pixels>,
line_height: Pixels,
cx: &mut WindowContext,
) -> Result<()> {
paint_line(
origin,
&self.layout.unwrapped_layout,
line_height,
&self.decoration_runs,
self.wrap_width,
&self.wrap_boundaries,
cx,
)?;
glyph_origin.x = origin.x;
glyph_origin.y += line_height;
}
prev_glyph_position = glyph.position;
Ok(())
}
}
let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
if glyph.index >= run_end {
if let Some(style_run) = style_runs.next() {
if let Some((_, underline_style)) = &mut current_underline {
if style_run.underline.as_ref() != Some(underline_style) {
finished_underline = current_underline.take();
}
}
if let Some(run_underline) = style_run.underline.as_ref() {
current_underline.get_or_insert((
point(
glyph_origin.x,
origin.y
+ baseline_offset.y
+ (self.layout.layout.descent * 0.618),
),
UnderlineStyle {
color: Some(run_underline.color.unwrap_or(style_run.color)),
thickness: run_underline.thickness,
wavy: run_underline.wavy,
},
));
}
fn paint_line(
origin: Point<Pixels>,
layout: &LineLayout,
line_height: Pixels,
decoration_runs: &[DecorationRun],
wrap_width: Option<Pixels>,
wrap_boundaries: &[WrapBoundary],
cx: &mut WindowContext<'_>,
) -> Result<()> {
let padding_top = (line_height - layout.ascent - layout.descent) / 2.;
let baseline_offset = point(px(0.), padding_top + layout.ascent);
let mut decoration_runs = decoration_runs.iter();
let mut wraps = wrap_boundaries.iter().peekable();
let mut run_end = 0;
let mut color = black();
let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
let text_system = cx.text_system().clone();
let mut glyph_origin = origin;
let mut prev_glyph_position = Point::default();
for (run_ix, run) in layout.runs.iter().enumerate() {
let max_glyph_size = text_system
.bounding_box(run.font_id, layout.font_size)?
.size;
run_end += style_run.len as usize;
color = style_run.color;
} else {
run_end = self.layout.text.len();
finished_underline = current_underline.take();
}
}
for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
glyph_origin.x += glyph.position.x - prev_glyph_position.x;
if let Some((underline_origin, underline_style)) = finished_underline {
if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
wraps.next();
if let Some((underline_origin, underline_style)) = current_underline.take() {
cx.paint_underline(
underline_origin,
glyph_origin.x - underline_origin.x,
@@ -115,42 +118,84 @@ impl Line {
)?;
}
let max_glyph_bounds = Bounds {
origin: glyph_origin,
size: max_glyph_size,
};
glyph_origin.x = origin.x;
glyph_origin.y += line_height;
}
prev_glyph_position = glyph.position;
let content_mask = cx.content_mask();
if max_glyph_bounds.intersects(&content_mask.bounds) {
if glyph.is_emoji {
cx.paint_emoji(
glyph_origin + baseline_offset,
run.font_id,
glyph.id,
self.layout.layout.font_size,
)?;
} else {
cx.paint_glyph(
glyph_origin + baseline_offset,
run.font_id,
glyph.id,
self.layout.layout.font_size,
color,
)?;
let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
if glyph.index >= run_end {
if let Some(style_run) = decoration_runs.next() {
if let Some((_, underline_style)) = &mut current_underline {
if style_run.underline.as_ref() != Some(underline_style) {
finished_underline = current_underline.take();
}
}
if let Some(run_underline) = style_run.underline.as_ref() {
current_underline.get_or_insert((
point(
glyph_origin.x,
origin.y + baseline_offset.y + (layout.descent * 0.618),
),
UnderlineStyle {
color: Some(run_underline.color.unwrap_or(style_run.color)),
thickness: run_underline.thickness,
wavy: run_underline.wavy,
},
));
}
run_end += style_run.len as usize;
color = style_run.color;
} else {
run_end = layout.len;
finished_underline = current_underline.take();
}
}
if let Some((underline_origin, underline_style)) = finished_underline {
cx.paint_underline(
underline_origin,
glyph_origin.x - underline_origin.x,
&underline_style,
)?;
}
let max_glyph_bounds = Bounds {
origin: glyph_origin,
size: max_glyph_size,
};
let content_mask = cx.content_mask();
if max_glyph_bounds.intersects(&content_mask.bounds) {
if glyph.is_emoji {
cx.paint_emoji(
glyph_origin + baseline_offset,
run.font_id,
glyph.id,
layout.font_size,
)?;
} else {
cx.paint_glyph(
glyph_origin + baseline_offset,
run.font_id,
glyph.id,
layout.font_size,
color,
)?;
}
}
}
if let Some((underline_start, underline_style)) = current_underline.take() {
let line_end_x = origin.x + self.layout.layout.width;
cx.paint_underline(
underline_start,
line_end_x - underline_start.x,
&underline_style,
)?;
}
Ok(())
}
if let Some((underline_start, underline_style)) = current_underline.take() {
let line_end_x = origin.x + wrap_width.unwrap_or(Pixels::MAX).min(layout.width);
cx.paint_underline(
underline_start,
line_end_x - underline_start.x,
&underline_style,
)?;
}
Ok(())
}

View File

@@ -1,5 +1,4 @@
use crate::{px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, SharedString};
use derive_more::{Deref, DerefMut};
use crate::{px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, Size};
use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
use smallvec::SmallVec;
use std::{
@@ -149,13 +148,11 @@ impl LineLayout {
}
}
#[derive(Deref, DerefMut, Default, Debug)]
#[derive(Default, Debug)]
pub struct WrappedLineLayout {
#[deref]
#[deref_mut]
pub layout: LineLayout,
pub text: SharedString,
pub unwrapped_layout: Arc<LineLayout>,
pub wrap_boundaries: SmallVec<[WrapBoundary; 1]>,
pub wrap_width: Option<Pixels>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
@@ -164,31 +161,74 @@ pub struct WrapBoundary {
pub glyph_ix: usize,
}
impl WrappedLineLayout {
pub fn len(&self) -> usize {
self.unwrapped_layout.len
}
pub fn width(&self) -> Pixels {
self.wrap_width
.unwrap_or(Pixels::MAX)
.min(self.unwrapped_layout.width)
}
pub fn size(&self, line_height: Pixels) -> Size<Pixels> {
Size {
width: self.width(),
height: line_height * (self.wrap_boundaries.len() + 1),
}
}
pub fn ascent(&self) -> Pixels {
self.unwrapped_layout.ascent
}
pub fn descent(&self) -> Pixels {
self.unwrapped_layout.descent
}
pub fn wrap_boundaries(&self) -> &[WrapBoundary] {
&self.wrap_boundaries
}
pub fn font_size(&self) -> Pixels {
self.unwrapped_layout.font_size
}
pub fn runs(&self) -> &[ShapedRun] {
&self.unwrapped_layout.runs
}
}
pub(crate) struct LineLayoutCache {
prev_frame: Mutex<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
curr_frame: RwLock<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
previous_frame: Mutex<HashMap<CacheKey, Arc<LineLayout>>>,
current_frame: RwLock<HashMap<CacheKey, Arc<LineLayout>>>,
previous_frame_wrapped: Mutex<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
current_frame_wrapped: RwLock<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
platform_text_system: Arc<dyn PlatformTextSystem>,
}
impl LineLayoutCache {
pub fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
Self {
prev_frame: Mutex::new(HashMap::new()),
curr_frame: RwLock::new(HashMap::new()),
previous_frame: Mutex::default(),
current_frame: RwLock::default(),
previous_frame_wrapped: Mutex::default(),
current_frame_wrapped: RwLock::default(),
platform_text_system,
}
}
pub fn start_frame(&self) {
let mut prev_frame = self.prev_frame.lock();
let mut curr_frame = self.curr_frame.write();
let mut prev_frame = self.previous_frame.lock();
let mut curr_frame = self.current_frame.write();
std::mem::swap(&mut *prev_frame, &mut *curr_frame);
curr_frame.clear();
}
pub fn layout_line(
pub fn layout_wrapped_line(
&self,
text: &SharedString,
text: &str,
font_size: Pixels,
runs: &[FontRun],
wrap_width: Option<Pixels>,
@@ -199,34 +239,66 @@ impl LineLayoutCache {
runs,
wrap_width,
} as &dyn AsCacheKeyRef;
let curr_frame = self.curr_frame.upgradable_read();
if let Some(layout) = curr_frame.get(key) {
let current_frame = self.current_frame_wrapped.upgradable_read();
if let Some(layout) = current_frame.get(key) {
return layout.clone();
}
let mut curr_frame = RwLockUpgradableReadGuard::upgrade(curr_frame);
if let Some((key, layout)) = self.prev_frame.lock().remove_entry(key) {
curr_frame.insert(key, layout.clone());
let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
if let Some((key, layout)) = self.previous_frame_wrapped.lock().remove_entry(key) {
current_frame.insert(key, layout.clone());
layout
} else {
let layout = self.platform_text_system.layout_line(text, font_size, runs);
let wrap_boundaries = wrap_width
.map(|wrap_width| layout.compute_wrap_boundaries(text.as_ref(), wrap_width))
.unwrap_or_default();
let wrapped_line = Arc::new(WrappedLineLayout {
layout,
text: text.clone(),
let unwrapped_layout = self.layout_line(text, font_size, runs);
let wrap_boundaries = if let Some(wrap_width) = wrap_width {
unwrapped_layout.compute_wrap_boundaries(text.as_ref(), wrap_width)
} else {
SmallVec::new()
};
let layout = Arc::new(WrappedLineLayout {
unwrapped_layout,
wrap_boundaries,
wrap_width,
});
let key = CacheKey {
text: text.clone(),
text: text.into(),
font_size,
runs: SmallVec::from(runs),
wrap_width,
};
curr_frame.insert(key, wrapped_line.clone());
wrapped_line
current_frame.insert(key, layout.clone());
layout
}
}
pub fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> Arc<LineLayout> {
let key = &CacheKeyRef {
text,
font_size,
runs,
wrap_width: None,
} as &dyn AsCacheKeyRef;
let current_frame = self.current_frame.upgradable_read();
if let Some(layout) = current_frame.get(key) {
return layout.clone();
}
let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
if let Some((key, layout)) = self.previous_frame.lock().remove_entry(key) {
current_frame.insert(key, layout.clone());
layout
} else {
let layout = Arc::new(self.platform_text_system.layout_line(text, font_size, runs));
let key = CacheKey {
text: text.into(),
font_size,
runs: SmallVec::from(runs),
wrap_width: None,
};
current_frame.insert(key, layout.clone());
layout
}
}
}
@@ -243,7 +315,7 @@ trait AsCacheKeyRef {
#[derive(Eq)]
struct CacheKey {
text: SharedString,
text: String,
font_size: Pixels,
runs: SmallVec<[FontRun; 1]>,
wrap_width: Option<Pixels>,

View File

@@ -1,7 +1,8 @@
use crate::{
private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace,
BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, LayoutId,
Model, Pixels, Size, ViewContext, VisualContext, WeakModel, WindowContext,
BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, FocusHandle,
FocusableView, LayoutId, Model, Pixels, Point, Size, ViewContext, VisualContext, WeakModel,
WindowContext,
};
use anyhow::{Context, Result};
use std::{
@@ -63,6 +64,23 @@ impl<V: 'static> View<V> {
pub fn read<'a>(&self, cx: &'a AppContext) -> &'a V {
self.model.read(cx)
}
pub fn render_with<C>(&self, component: C) -> RenderViewWith<C, V>
where
C: 'static + Component<V>,
{
RenderViewWith {
view: self.clone(),
component: Some(component),
}
}
pub fn focus_handle(&self, cx: &AppContext) -> FocusHandle
where
V: FocusableView,
{
self.read(cx).focus_handle(cx)
}
}
impl<V> Clone for View<V> {
@@ -145,8 +163,7 @@ impl<V> Eq for WeakView<V> {}
#[derive(Clone, Debug)]
pub struct AnyView {
model: AnyModel,
initialize: fn(&AnyView, &mut WindowContext) -> AnyBox,
layout: fn(&AnyView, &mut AnyBox, &mut WindowContext) -> LayoutId,
layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box<dyn Any>),
paint: fn(&AnyView, &mut AnyBox, &mut WindowContext),
}
@@ -154,7 +171,6 @@ impl AnyView {
pub fn downgrade(&self) -> AnyWeakView {
AnyWeakView {
model: self.model.downgrade(),
initialize: self.initialize,
layout: self.layout,
paint: self.paint,
}
@@ -165,7 +181,6 @@ impl AnyView {
Ok(model) => Ok(View { model }),
Err(model) => Err(Self {
model,
initialize: self.initialize,
layout: self.layout,
paint: self.paint,
}),
@@ -176,13 +191,19 @@ impl AnyView {
self.model.entity_type
}
pub(crate) fn draw(&self, available_space: Size<AvailableSpace>, cx: &mut WindowContext) {
let mut rendered_element = (self.initialize)(self, cx);
let layout_id = (self.layout)(self, &mut rendered_element, cx);
cx.window
.layout_engine
.compute_layout(layout_id, available_space);
(self.paint)(self, &mut rendered_element, cx);
pub(crate) fn draw(
&self,
origin: Point<Pixels>,
available_space: Size<AvailableSpace>,
cx: &mut WindowContext,
) {
cx.with_absolute_element_offset(origin, |cx| {
let (layout_id, mut rendered_element) = (self.layout)(self, cx);
cx.window
.layout_engine
.compute_layout(layout_id, available_space);
(self.paint)(self, &mut rendered_element, cx);
})
}
}
@@ -196,7 +217,6 @@ impl<V: Render> From<View<V>> for AnyView {
fn from(value: View<V>) -> Self {
AnyView {
model: value.model.into_any(),
initialize: any_view::initialize::<V>,
layout: any_view::layout::<V>,
paint: any_view::paint::<V>,
}
@@ -210,22 +230,13 @@ impl<ParentViewState: 'static> Element<ParentViewState> for AnyView {
Some(self.model.entity_id.into())
}
fn initialize(
fn layout(
&mut self,
_view_state: &mut ParentViewState,
_element_state: Option<Self::ElementState>,
cx: &mut ViewContext<ParentViewState>,
) -> Self::ElementState {
(self.initialize)(self, cx)
}
fn layout(
&mut self,
_view_state: &mut ParentViewState,
rendered_element: &mut Self::ElementState,
cx: &mut ViewContext<ParentViewState>,
) -> LayoutId {
(self.layout)(self, rendered_element, cx)
) -> (LayoutId, Self::ElementState) {
(self.layout)(self, cx)
}
fn paint(
@@ -241,8 +252,7 @@ impl<ParentViewState: 'static> Element<ParentViewState> for AnyView {
pub struct AnyWeakView {
model: AnyWeakModel,
initialize: fn(&AnyView, &mut WindowContext) -> AnyBox,
layout: fn(&AnyView, &mut AnyBox, &mut WindowContext) -> LayoutId,
layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box<dyn Any>),
paint: fn(&AnyView, &mut AnyBox, &mut WindowContext),
}
@@ -251,7 +261,6 @@ impl AnyWeakView {
let model = self.model.upgrade()?;
Some(AnyView {
model,
initialize: self.initialize,
layout: self.layout,
paint: self.paint,
})
@@ -262,7 +271,6 @@ impl<V: Render> From<WeakView<V>> for AnyWeakView {
fn from(view: WeakView<V>) -> Self {
Self {
model: view.model.into(),
initialize: any_view::initialize::<V>,
layout: any_view::layout::<V>,
paint: any_view::paint::<V>,
}
@@ -281,12 +289,12 @@ where
}
}
pub struct RenderView<C, V> {
pub struct RenderViewWith<C, V> {
view: View<V>,
component: Option<C>,
}
impl<C, ParentViewState, ViewState> Component<ParentViewState> for RenderView<C, ViewState>
impl<C, ParentViewState, ViewState> Component<ParentViewState> for RenderViewWith<C, ViewState>
where
C: 'static + Component<ViewState>,
ParentViewState: 'static,
@@ -297,7 +305,7 @@ where
}
}
impl<C, ParentViewState, ViewState> Element<ParentViewState> for RenderView<C, ViewState>
impl<C, ParentViewState, ViewState> Element<ParentViewState> for RenderViewWith<C, ViewState>
where
C: 'static + Component<ViewState>,
ParentViewState: 'static,
@@ -309,29 +317,16 @@ where
Some(self.view.entity_id().into())
}
fn initialize(
fn layout(
&mut self,
_: &mut ParentViewState,
_: Option<Self::ElementState>,
cx: &mut ViewContext<ParentViewState>,
) -> Self::ElementState {
cx.with_element_id(Some(self.view.entity_id()), |cx| {
self.view.update(cx, |view, cx| {
let mut element = self.component.take().unwrap().render();
element.initialize(view, cx);
element
})
})
}
fn layout(
&mut self,
_: &mut ParentViewState,
element: &mut Self::ElementState,
cx: &mut ViewContext<ParentViewState>,
) -> LayoutId {
cx.with_element_id(Some(self.view.entity_id()), |cx| {
self.view.update(cx, |view, cx| element.layout(view, cx))
) -> (LayoutId, Self::ElementState) {
self.view.update(cx, |view, cx| {
let mut element = self.component.take().unwrap().render();
let layout_id = element.layout(view, cx);
(layout_id, element)
})
}
@@ -342,20 +337,7 @@ where
element: &mut Self::ElementState,
cx: &mut ViewContext<ParentViewState>,
) {
cx.with_element_id(Some(self.view.entity_id()), |cx| {
self.view.update(cx, |view, cx| element.paint(view, cx))
})
}
}
pub fn render_view<C, V>(view: &View<V>, component: C) -> RenderView<C, V>
where
C: 'static + Component<V>,
V: 'static,
{
RenderView {
view: view.clone(),
component: Some(component),
self.view.update(cx, |view, cx| element.paint(view, cx))
}
}
@@ -363,27 +345,17 @@ mod any_view {
use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext};
use std::any::Any;
pub(crate) fn initialize<V: Render>(view: &AnyView, cx: &mut WindowContext) -> Box<dyn Any> {
cx.with_element_id(Some(view.model.entity_id), |cx| {
let view = view.clone().downcast::<V>().unwrap();
let element = view.update(cx, |view, cx| {
let mut element = AnyElement::new(view.render(cx));
element.initialize(view, cx);
element
});
Box::new(element)
})
}
pub(crate) fn layout<V: Render>(
view: &AnyView,
element: &mut Box<dyn Any>,
cx: &mut WindowContext,
) -> LayoutId {
) -> (LayoutId, Box<dyn Any>) {
cx.with_element_id(Some(view.model.entity_id), |cx| {
let view = view.clone().downcast::<V>().unwrap();
let element = element.downcast_mut::<AnyElement<V>>().unwrap();
view.update(cx, |view, cx| element.layout(view, cx))
view.update(cx, |view, cx| {
let mut element = AnyElement::new(view.render(cx));
let layout_id = element.layout(view, cx);
(layout_id, Box::new(element) as Box<dyn Any>)
})
})
}

View File

@@ -185,6 +185,27 @@ impl Drop for FocusHandle {
}
}
/// FocusableView allows users of your view to easily
/// focus it (using cx.focus_view(view))
pub trait FocusableView: Render {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
}
/// ManagedView is a view (like a Modal, Popover, Menu, etc.)
/// where the lifecycle of the view is handled by another view.
pub trait ManagedView: Render {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
}
pub struct Dismiss;
impl<T: ManagedView> EventEmitter<Dismiss> for T {}
impl<T: ManagedView> FocusableView for T {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.focus_handle(cx)
}
}
// Holds the state for a specific window.
pub struct Window {
pub(crate) handle: AnyWindowHandle,
@@ -216,7 +237,7 @@ pub struct Window {
// #[derive(Default)]
pub(crate) struct Frame {
element_states: HashMap<GlobalElementId, AnyBox>,
pub(crate) element_states: HashMap<GlobalElementId, AnyBox>,
mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyMouseListener)>>,
pub(crate) dispatch_tree: DispatchTree,
pub(crate) focus_listeners: Vec<AnyFocusListener>,
@@ -307,8 +328,8 @@ impl Window {
layout_engine: TaffyLayoutEngine::new(),
root_view: None,
element_id_stack: GlobalElementId::default(),
previous_frame: Frame::new(DispatchTree::new(cx.keymap.clone())),
current_frame: Frame::new(DispatchTree::new(cx.keymap.clone())),
previous_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
current_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
focus_listeners: SubscriberSet::new(),
default_prevented: true,
@@ -393,6 +414,10 @@ impl<'a> WindowContext<'a> {
/// Move focus to the element associated with the given `FocusHandle`.
pub fn focus(&mut self, handle: &FocusHandle) {
if self.window.focus == Some(handle.id) {
return;
}
let focus_id = handle.id;
if self.window.last_blur.is_none() {
@@ -400,6 +425,10 @@ impl<'a> WindowContext<'a> {
}
self.window.focus = Some(focus_id);
self.window
.current_frame
.dispatch_tree
.clear_keystroke_matchers();
self.app.push_effect(Effect::FocusChanged {
window_handle: self.window.handle,
focused: Some(focus_id),
@@ -562,6 +591,7 @@ impl<'a> WindowContext<'a> {
result
}
#[must_use]
/// Add a node to the layout tree for the current frame. Takes the `Style` of the element for which
/// layout is being requested, along with the layout ids of any children. This method is called during
/// calls to the `Element::layout` trait method and enables any element to participate in layout.
@@ -1068,29 +1098,33 @@ impl<'a> WindowContext<'a> {
self.with_z_index(0, |cx| {
let available_space = cx.window.viewport_size.map(Into::into);
root_view.draw(available_space, cx);
root_view.draw(Point::zero(), available_space, cx);
});
if let Some(active_drag) = self.app.active_drag.take() {
self.with_z_index(1, |cx| {
let offset = cx.mouse_position() - active_drag.cursor_offset;
cx.with_element_offset(offset, |cx| {
let available_space =
size(AvailableSpace::MinContent, AvailableSpace::MinContent);
active_drag.view.draw(available_space, cx);
cx.active_drag = Some(active_drag);
});
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
active_drag.view.draw(offset, available_space, cx);
cx.active_drag = Some(active_drag);
});
} else if let Some(active_tooltip) = self.app.active_tooltip.take() {
self.with_z_index(1, |cx| {
cx.with_element_offset(active_tooltip.cursor_offset, |cx| {
let available_space =
size(AvailableSpace::MinContent, AvailableSpace::MinContent);
active_tooltip.view.draw(available_space, cx);
});
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
active_tooltip
.view
.draw(active_tooltip.cursor_offset, available_space, cx);
});
}
self.window
.current_frame
.dispatch_tree
.preserve_keystroke_matchers(
&mut self.window.previous_frame.dispatch_tree,
self.window.focus,
);
self.window.root_view = Some(root_view);
let scene = self.window.current_frame.scene_builder.build();
@@ -1134,6 +1168,14 @@ impl<'a> WindowContext<'a> {
self.window.mouse_position = mouse_move.position;
InputEvent::MouseMove(mouse_move)
}
InputEvent::MouseDown(mouse_down) => {
self.window.mouse_position = mouse_down.position;
InputEvent::MouseDown(mouse_down)
}
InputEvent::MouseUp(mouse_up) => {
self.window.mouse_position = mouse_up.position;
InputEvent::MouseUp(mouse_up)
}
// Translate dragging and dropping of external files from the operating system
// to internal drag and drop events.
InputEvent::FileDrop(file_drop) => match file_drop {
@@ -1534,6 +1576,12 @@ impl VisualContext for WindowContext<'_> {
self.window.root_view = Some(view.clone().into());
view
}
fn focus_view<V: crate::FocusableView>(&mut self, view: &View<V>) -> Self::Result<()> {
self.update_view(view, |view, cx| {
view.focus_handle(cx).clone().focus(cx);
})
}
}
impl<'a> std::ops::Deref for WindowContext<'a> {
@@ -1617,8 +1665,8 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
}
}
/// Update the global element offset based on the given offset. This is used to implement
/// scrolling and position drag handles.
/// Update the global element offset relative to the current offset. This is used to implement
/// scrolling.
fn with_element_offset<R>(
&mut self,
offset: Point<Pixels>,
@@ -1628,7 +1676,17 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
return f(self);
};
let offset = self.element_offset() + offset;
let abs_offset = self.element_offset() + offset;
self.with_absolute_element_offset(abs_offset, f)
}
/// Update the global element offset based on the given offset. This is used to implement
/// drag handles and other manual painting of elements.
fn with_absolute_element_offset<R>(
&mut self,
offset: Point<Pixels>,
f: impl FnOnce(&mut Self) -> R,
) -> R {
self.window_mut()
.current_frame
.element_offset_stack
@@ -1798,8 +1856,8 @@ impl<'a, V: 'static> ViewContext<'a, V> {
self.view
}
pub fn model(&self) -> Model<V> {
self.view.model.clone()
pub fn model(&self) -> &Model<V> {
&self.view.model
}
/// Access the underlying window context.
@@ -2093,7 +2151,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
window
.current_frame
.dispatch_tree
.push_node(context.clone(), &mut window.previous_frame.dispatch_tree);
.push_node(context.clone());
if let Some(focus_handle) = focus_handle.as_ref() {
window
.current_frame
@@ -2131,7 +2189,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
pub fn observe_global<G: 'static>(
&mut self,
f: impl Fn(&mut V, &mut ViewContext<'_, V>) + 'static,
mut f: impl FnMut(&mut V, &mut ViewContext<'_, V>) + 'static,
) -> Subscription {
let window_handle = self.window.handle;
let view = self.view().downgrade();
@@ -2197,9 +2255,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
.set_input_handler(Box::new(input_handler));
}
}
}
impl<V> ViewContext<'_, V> {
pub fn emit<Evt>(&mut self, event: Evt)
where
Evt: 'static,
@@ -2212,6 +2268,13 @@ impl<V> ViewContext<'_, V> {
event: Box::new(event),
});
}
pub fn focus_self(&mut self)
where
V: FocusableView,
{
self.defer(|view, cx| view.focus_handle(cx).focus(cx))
}
}
impl<V> Context for ViewContext<'_, V> {
@@ -2287,6 +2350,10 @@ impl<V: 'static> VisualContext for ViewContext<'_, V> {
{
self.window_cx.replace_root_view(build_view)
}
fn focus_view<W: FocusableView>(&mut self, view: &View<W>) -> Self::Result<()> {
self.window_cx.focus_view(view)
}
}
impl<'a, V> std::ops::Deref for ViewContext<'a, V> {
@@ -2471,7 +2538,7 @@ impl From<SmallVec<[u32; 16]>> for StackingOrder {
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum ElementId {
View(EntityId),
Number(usize),
Integer(usize),
Name(SharedString),
FocusHandle(FocusId),
}
@@ -2484,13 +2551,13 @@ impl From<EntityId> for ElementId {
impl From<usize> for ElementId {
fn from(id: usize) -> Self {
ElementId::Number(id)
ElementId::Integer(id)
}
}
impl From<i32> for ElementId {
fn from(id: i32) -> Self {
Self::Number(id as usize)
Self::Integer(id as usize)
}
}

View File

@@ -0,0 +1,45 @@
use serde_derive::Deserialize;
#[test]
fn test_derive() {
use gpui2 as gpui;
#[derive(PartialEq, Clone, Deserialize, gpui2_macros::Action)]
struct AnotherTestAction;
#[gpui2_macros::register_action]
#[derive(PartialEq, Clone, gpui::serde_derive::Deserialize)]
struct RegisterableAction {}
impl gpui::Action for RegisterableAction {
fn boxed_clone(&self) -> Box<dyn gpui::Action> {
todo!()
}
fn as_any(&self) -> &dyn std::any::Any {
todo!()
}
fn partial_eq(&self, _action: &dyn gpui::Action) -> bool {
todo!()
}
fn name(&self) -> &str {
todo!()
}
fn debug_name() -> &'static str
where
Self: Sized,
{
todo!()
}
fn build(_value: serde_json::Value) -> anyhow::Result<Box<dyn gpui::Action>>
where
Self: Sized,
{
todo!()
}
}
}

View File

@@ -9,6 +9,6 @@ path = "src/gpui2_macros.rs"
proc-macro = true
[dependencies]
syn = "1.0.72"
syn = { version = "1.0.72", features = ["full"] }
quote = "1.0.9"
proc-macro2 = "1.0.66"

View File

@@ -15,48 +15,81 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
use syn::{parse_macro_input, DeriveInput, Error};
use crate::register_action::register_action;
pub fn action(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
pub fn action(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as DeriveInput);
let name = &input.ident;
let attrs = input
.attrs
.into_iter()
.filter(|attr| !attr.path.is_ident("action"))
.collect::<Vec<_>>();
let attributes = quote! {
#[gpui::register_action]
#[derive(gpui::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone, std::default::Default, std::fmt::Debug)]
#(#attrs)*
if input.generics.lt_token.is_some() {
return Error::new(name.span(), "Actions must be a concrete type")
.into_compile_error()
.into();
}
let is_unit_struct = match input.data {
syn::Data::Struct(struct_data) => struct_data.fields.is_empty(),
syn::Data::Enum(_) => false,
syn::Data::Union(_) => false,
};
let visibility = input.vis;
let output = match input.data {
syn::Data::Struct(ref struct_data) => match &struct_data.fields {
syn::Fields::Named(_) | syn::Fields::Unnamed(_) => {
let fields = &struct_data.fields;
quote! {
#attributes
#visibility struct #name #fields
}
let build_impl = if is_unit_struct {
quote! {
Ok(std::boxed::Box::new(Self {}))
}
} else {
quote! {
Ok(std::boxed::Box::new(gpui::serde_json::from_value::<Self>(value)?))
}
};
let register_action = register_action(&name);
let output = quote! {
const _: fn() = || {
fn assert_impl<T: ?Sized + for<'a> gpui::serde::Deserialize<'a> + ::std::cmp::PartialEq + ::std::clone::Clone>() {}
assert_impl::<#name>();
};
impl gpui::Action for #name {
fn name(&self) -> &'static str
{
::std::any::type_name::<#name>()
}
syn::Fields::Unit => {
quote! {
#attributes
#visibility struct #name;
}
fn debug_name() -> &'static str
where
Self: ::std::marker::Sized
{
::std::any::type_name::<#name>()
}
},
syn::Data::Enum(ref enum_data) => {
let variants = &enum_data.variants;
quote! {
#attributes
#visibility enum #name { #variants }
fn build(value: gpui::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>>
where
Self: ::std::marker::Sized {
#build_impl
}
fn partial_eq(&self, action: &dyn gpui::Action) -> bool {
action
.as_any()
.downcast_ref::<Self>()
.map_or(false, |a| self == a)
}
fn boxed_clone(&self) -> std::boxed::Box<dyn gpui::Action> {
::std::boxed::Box::new(self.clone())
}
fn as_any(&self) -> &dyn ::std::any::Any {
self
}
}
_ => panic!("Expected a struct or an enum."),
#register_action
};
TokenStream::from(output)

View File

@@ -11,14 +11,14 @@ pub fn style_helpers(args: TokenStream) -> TokenStream {
style_helpers::style_helpers(args)
}
#[proc_macro_attribute]
pub fn action(attr: TokenStream, item: TokenStream) -> TokenStream {
action::action(attr, item)
#[proc_macro_derive(Action)]
pub fn action(input: TokenStream) -> TokenStream {
action::action(input)
}
#[proc_macro_attribute]
pub fn register_action(attr: TokenStream, item: TokenStream) -> TokenStream {
register_action::register_action(attr, item)
register_action::register_action_macro(attr, item)
}
#[proc_macro_derive(Component, attributes(component))]

View File

@@ -12,22 +12,76 @@
// gpui2::register_action_builder::<Foo>()
// }
use proc_macro::TokenStream;
use proc_macro2::Ident;
use quote::{format_ident, quote};
use syn::{parse_macro_input, DeriveInput};
use syn::{parse_macro_input, DeriveInput, Error};
pub fn register_action(_attr: TokenStream, item: TokenStream) -> TokenStream {
pub fn register_action_macro(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as DeriveInput);
let type_name = &input.ident;
let ctor_fn_name = format_ident!("register_{}_builder", type_name.to_string().to_lowercase());
let registration = register_action(&input.ident);
let expanded = quote! {
let has_action_derive = input
.attrs
.iter()
.find(|attr| {
(|| {
let meta = attr.parse_meta().ok()?;
meta.path().is_ident("derive").then(|| match meta {
syn::Meta::Path(_) => None,
syn::Meta::NameValue(_) => None,
syn::Meta::List(list) => list
.nested
.iter()
.find(|list| match list {
syn::NestedMeta::Meta(meta) => meta.path().is_ident("Action"),
syn::NestedMeta::Lit(_) => false,
})
.map(|_| true),
})?
})()
.unwrap_or(false)
})
.is_some();
if has_action_derive {
return Error::new(
input.ident.span(),
"The Action derive macro has already registered this action",
)
.into_compile_error()
.into();
}
TokenStream::from(quote! {
#input
#[allow(non_snake_case)]
#[gpui::ctor]
fn #ctor_fn_name() {
gpui::register_action::<#type_name>()
}
};
TokenStream::from(expanded)
#registration
})
}
pub(crate) fn register_action(type_name: &Ident) -> proc_macro2::TokenStream {
let static_slice_name =
format_ident!("__GPUI_ACTIONS_{}", type_name.to_string().to_uppercase());
let action_builder_fn_name = format_ident!(
"__gpui_actions_builder_{}",
type_name.to_string().to_lowercase()
);
quote! {
#[doc(hidden)]
#[gpui::linkme::distributed_slice(gpui::__GPUI_ACTIONS)]
#[linkme(crate = gpui::linkme)]
static #static_slice_name: gpui::MacroActionBuilder = #action_builder_fn_name;
/// This is an auto generated function, do not use.
#[doc(hidden)]
fn #action_builder_fn_name() -> gpui::ActionData {
gpui::ActionData {
name: ::std::any::type_name::<#type_name>(),
type_id: ::std::any::TypeId::of::<#type_name>(),
build: <#type_name as gpui::Action>::build,
}
}
}
}

View File

@@ -17,7 +17,7 @@ use crate::{
};
use anyhow::{anyhow, Result};
pub use clock::ReplicaId;
use futures::FutureExt as _;
use futures::channel::oneshot;
use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, Task};
use lsp::LanguageServerId;
use parking_lot::Mutex;
@@ -45,7 +45,7 @@ pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, *};
use theme::SyntaxTheme;
#[cfg(any(test, feature = "test-support"))]
use util::RandomCharIter;
use util::{RangeExt, TryFutureExt as _};
use util::RangeExt;
#[cfg(any(test, feature = "test-support"))]
pub use {tree_sitter_rust, tree_sitter_typescript};
@@ -62,6 +62,7 @@ pub struct Buffer {
saved_mtime: SystemTime,
transaction_depth: usize,
was_dirty_before_starting_transaction: Option<bool>,
reload_task: Option<Task<Result<()>>>,
language: Option<Arc<Language>>,
autoindent_requests: Vec<Arc<AutoindentRequest>>,
pending_autoindent: Option<Task<()>>,
@@ -509,6 +510,7 @@ impl Buffer {
saved_mtime,
saved_version: buffer.version(),
saved_version_fingerprint: buffer.as_rope().fingerprint(),
reload_task: None,
transaction_depth: 0,
was_dirty_before_starting_transaction: None,
text: buffer,
@@ -608,37 +610,52 @@ impl Buffer {
cx.notify();
}
pub fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<Option<Transaction>>> {
cx.spawn(|this, mut cx| async move {
if let Some((new_mtime, new_text)) = this.read_with(&cx, |this, cx| {
pub fn reload(
&mut self,
cx: &mut ModelContext<Self>,
) -> oneshot::Receiver<Option<Transaction>> {
let (tx, rx) = futures::channel::oneshot::channel();
let prev_version = self.text.version();
self.reload_task = Some(cx.spawn(|this, mut cx| async move {
let Some((new_mtime, new_text)) = this.update(&mut cx, |this, cx| {
let file = this.file.as_ref()?.as_local()?;
Some((file.mtime(), file.load(cx)))
}) {
let new_text = new_text.await?;
let diff = this
.read_with(&cx, |this, cx| this.diff(new_text, cx))
.await;
this.update(&mut cx, |this, cx| {
if this.version() == diff.base_version {
this.finalize_last_transaction();
this.apply_diff(diff, cx);
if let Some(transaction) = this.finalize_last_transaction().cloned() {
this.did_reload(
this.version(),
this.as_rope().fingerprint(),
this.line_ending(),
new_mtime,
cx,
);
return Ok(Some(transaction));
}
}
Ok(None)
})
} else {
Ok(None)
}
})
}) else {
return Ok(());
};
let new_text = new_text.await?;
let diff = this
.update(&mut cx, |this, cx| this.diff(new_text.clone(), cx))
.await;
this.update(&mut cx, |this, cx| {
if this.version() == diff.base_version {
this.finalize_last_transaction();
this.apply_diff(diff, cx);
tx.send(this.finalize_last_transaction().cloned()).ok();
this.did_reload(
this.version(),
this.as_rope().fingerprint(),
this.line_ending(),
new_mtime,
cx,
);
} else {
this.did_reload(
prev_version,
Rope::text_fingerprint(&new_text),
this.line_ending(),
this.saved_mtime,
cx,
);
}
this.reload_task.take();
});
Ok(())
}));
rx
}
pub fn did_reload(
@@ -667,13 +684,8 @@ impl Buffer {
cx.notify();
}
pub fn file_updated(
&mut self,
new_file: Arc<dyn File>,
cx: &mut ModelContext<Self>,
) -> Task<()> {
pub fn file_updated(&mut self, new_file: Arc<dyn File>, cx: &mut ModelContext<Self>) {
let mut file_changed = false;
let mut task = Task::ready(());
if let Some(old_file) = self.file.as_ref() {
if new_file.path() != old_file.path() {
@@ -693,8 +705,7 @@ impl Buffer {
file_changed = true;
if !self.is_dirty() {
let reload = self.reload(cx).log_err().map(drop);
task = cx.foreground().spawn(reload);
self.reload(cx).close();
}
}
}
@@ -708,7 +719,6 @@ impl Buffer {
cx.emit(Event::FileHandleChanged);
cx.notify();
}
task
}
pub fn diff_base(&self) -> Option<&str> {

View File

@@ -16,8 +16,9 @@ use crate::{
};
use anyhow::{anyhow, Result};
pub use clock::ReplicaId;
use futures::FutureExt as _;
use gpui::{AppContext, EventEmitter, HighlightStyle, ModelContext, Task};
use futures::channel::oneshot;
use gpui::{AppContext, EventEmitter, HighlightStyle, ModelContext, Task, TaskLabel};
use lazy_static::lazy_static;
use lsp::LanguageServerId;
use parking_lot::Mutex;
use similar::{ChangeTag, TextDiff};
@@ -44,23 +45,33 @@ pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, *};
use theme::SyntaxTheme;
#[cfg(any(test, feature = "test-support"))]
use util::RandomCharIter;
use util::{RangeExt, TryFutureExt as _};
use util::RangeExt;
#[cfg(any(test, feature = "test-support"))]
pub use {tree_sitter_rust, tree_sitter_typescript};
pub use lsp::DiagnosticSeverity;
lazy_static! {
pub static ref BUFFER_DIFF_TASK: TaskLabel = TaskLabel::new();
}
pub struct Buffer {
text: TextBuffer,
diff_base: Option<String>,
git_diff: git::diff::BufferDiff,
file: Option<Arc<dyn File>>,
saved_version: clock::Global,
saved_version_fingerprint: RopeFingerprint,
/// The mtime of the file when this buffer was last loaded from
/// or saved to disk.
saved_mtime: SystemTime,
/// The version vector when this buffer was last loaded from
/// or saved to disk.
saved_version: clock::Global,
/// A hash of the current contents of the buffer's file.
file_fingerprint: RopeFingerprint,
transaction_depth: usize,
was_dirty_before_starting_transaction: Option<bool>,
reload_task: Option<Task<Result<()>>>,
language: Option<Arc<Language>>,
autoindent_requests: Vec<Arc<AutoindentRequest>>,
pending_autoindent: Option<Task<()>>,
@@ -380,8 +391,7 @@ impl Buffer {
.ok_or_else(|| anyhow!("missing line_ending"))?,
));
this.saved_version = proto::deserialize_version(&message.saved_version);
this.saved_version_fingerprint =
proto::deserialize_fingerprint(&message.saved_version_fingerprint)?;
this.file_fingerprint = proto::deserialize_fingerprint(&message.saved_version_fingerprint)?;
this.saved_mtime = message
.saved_mtime
.ok_or_else(|| anyhow!("invalid saved_mtime"))?
@@ -397,7 +407,7 @@ impl Buffer {
diff_base: self.diff_base.as_ref().map(|h| h.to_string()),
line_ending: proto::serialize_line_ending(self.line_ending()) as i32,
saved_version: proto::serialize_version(&self.saved_version),
saved_version_fingerprint: proto::serialize_fingerprint(self.saved_version_fingerprint),
saved_version_fingerprint: proto::serialize_fingerprint(self.file_fingerprint),
saved_mtime: Some(self.saved_mtime.into()),
}
}
@@ -467,7 +477,8 @@ impl Buffer {
Self {
saved_mtime,
saved_version: buffer.version(),
saved_version_fingerprint: buffer.as_rope().fingerprint(),
file_fingerprint: buffer.as_rope().fingerprint(),
reload_task: None,
transaction_depth: 0,
was_dirty_before_starting_transaction: None,
text: buffer,
@@ -533,7 +544,7 @@ impl Buffer {
}
pub fn saved_version_fingerprint(&self) -> RopeFingerprint {
self.saved_version_fingerprint
self.file_fingerprint
}
pub fn saved_mtime(&self) -> SystemTime {
@@ -561,43 +572,58 @@ impl Buffer {
cx: &mut ModelContext<Self>,
) {
self.saved_version = version;
self.saved_version_fingerprint = fingerprint;
self.file_fingerprint = fingerprint;
self.saved_mtime = mtime;
cx.emit(Event::Saved);
cx.notify();
}
pub fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<Option<Transaction>>> {
cx.spawn(|this, mut cx| async move {
if let Some((new_mtime, new_text)) = this.update(&mut cx, |this, cx| {
pub fn reload(
&mut self,
cx: &mut ModelContext<Self>,
) -> oneshot::Receiver<Option<Transaction>> {
let (tx, rx) = futures::channel::oneshot::channel();
let prev_version = self.text.version();
self.reload_task = Some(cx.spawn(|this, mut cx| async move {
let Some((new_mtime, new_text)) = this.update(&mut cx, |this, cx| {
let file = this.file.as_ref()?.as_local()?;
Some((file.mtime(), file.load(cx)))
})? {
let new_text = new_text.await?;
let diff = this
.update(&mut cx, |this, cx| this.diff(new_text, cx))?
.await;
this.update(&mut cx, |this, cx| {
if this.version() == diff.base_version {
this.finalize_last_transaction();
this.apply_diff(diff, cx);
if let Some(transaction) = this.finalize_last_transaction().cloned() {
this.did_reload(
this.version(),
this.as_rope().fingerprint(),
this.line_ending(),
new_mtime,
cx,
);
return Some(transaction);
}
}
None
})
} else {
Ok(None)
}
})
})?
else {
return Ok(());
};
let new_text = new_text.await?;
let diff = this
.update(&mut cx, |this, cx| this.diff(new_text.clone(), cx))?
.await;
this.update(&mut cx, |this, cx| {
if this.version() == diff.base_version {
this.finalize_last_transaction();
this.apply_diff(diff, cx);
tx.send(this.finalize_last_transaction().cloned()).ok();
this.did_reload(
this.version(),
this.as_rope().fingerprint(),
this.line_ending(),
new_mtime,
cx,
);
} else {
this.did_reload(
prev_version,
Rope::text_fingerprint(&new_text),
this.line_ending(),
this.saved_mtime,
cx,
);
}
this.reload_task.take();
})
}));
rx
}
pub fn did_reload(
@@ -609,14 +635,14 @@ impl Buffer {
cx: &mut ModelContext<Self>,
) {
self.saved_version = version;
self.saved_version_fingerprint = fingerprint;
self.file_fingerprint = fingerprint;
self.text.set_line_ending(line_ending);
self.saved_mtime = mtime;
if let Some(file) = self.file.as_ref().and_then(|f| f.as_local()) {
file.buffer_reloaded(
self.remote_id(),
&self.saved_version,
self.saved_version_fingerprint,
self.file_fingerprint,
self.line_ending(),
self.saved_mtime,
cx,
@@ -626,13 +652,8 @@ impl Buffer {
cx.notify();
}
pub fn file_updated(
&mut self,
new_file: Arc<dyn File>,
cx: &mut ModelContext<Self>,
) -> Task<()> {
pub fn file_updated(&mut self, new_file: Arc<dyn File>, cx: &mut ModelContext<Self>) {
let mut file_changed = false;
let mut task = Task::ready(());
if let Some(old_file) = self.file.as_ref() {
if new_file.path() != old_file.path() {
@@ -652,8 +673,7 @@ impl Buffer {
file_changed = true;
if !self.is_dirty() {
let reload = self.reload(cx).log_err().map(drop);
task = cx.background_executor().spawn(reload);
self.reload(cx).close();
}
}
}
@@ -667,7 +687,6 @@ impl Buffer {
cx.emit(Event::FileHandleChanged);
cx.notify();
}
task
}
pub fn diff_base(&self) -> Option<&str> {
@@ -1118,36 +1137,72 @@ impl Buffer {
pub fn diff(&self, mut new_text: String, cx: &AppContext) -> Task<Diff> {
let old_text = self.as_rope().clone();
let base_version = self.version();
cx.background_executor().spawn(async move {
let old_text = old_text.to_string();
let line_ending = LineEnding::detect(&new_text);
LineEnding::normalize(&mut new_text);
let diff = TextDiff::from_chars(old_text.as_str(), new_text.as_str());
let mut edits = Vec::new();
let mut offset = 0;
let empty: Arc<str> = "".into();
for change in diff.iter_all_changes() {
let value = change.value();
let end_offset = offset + value.len();
match change.tag() {
ChangeTag::Equal => {
offset = end_offset;
cx.background_executor()
.spawn_labeled(*BUFFER_DIFF_TASK, async move {
let old_text = old_text.to_string();
let line_ending = LineEnding::detect(&new_text);
LineEnding::normalize(&mut new_text);
let diff = TextDiff::from_chars(old_text.as_str(), new_text.as_str());
let empty: Arc<str> = "".into();
let mut edits = Vec::new();
let mut old_offset = 0;
let mut new_offset = 0;
let mut last_edit: Option<(Range<usize>, Range<usize>)> = None;
for change in diff.iter_all_changes().map(Some).chain([None]) {
if let Some(change) = &change {
let len = change.value().len();
match change.tag() {
ChangeTag::Equal => {
old_offset += len;
new_offset += len;
}
ChangeTag::Delete => {
let old_end_offset = old_offset + len;
if let Some((last_old_range, _)) = &mut last_edit {
last_old_range.end = old_end_offset;
} else {
last_edit =
Some((old_offset..old_end_offset, new_offset..new_offset));
}
old_offset = old_end_offset;
}
ChangeTag::Insert => {
let new_end_offset = new_offset + len;
if let Some((_, last_new_range)) = &mut last_edit {
last_new_range.end = new_end_offset;
} else {
last_edit =
Some((old_offset..old_offset, new_offset..new_end_offset));
}
new_offset = new_end_offset;
}
}
}
ChangeTag::Delete => {
edits.push((offset..end_offset, empty.clone()));
offset = end_offset;
}
ChangeTag::Insert => {
edits.push((offset..offset, value.into()));
if let Some((old_range, new_range)) = &last_edit {
if old_offset > old_range.end
|| new_offset > new_range.end
|| change.is_none()
{
let text = if new_range.is_empty() {
empty.clone()
} else {
new_text[new_range.clone()].into()
};
edits.push((old_range.clone(), text));
last_edit.take();
}
}
}
}
Diff {
base_version,
line_ending,
edits,
}
})
Diff {
base_version,
line_ending,
edits,
}
})
}
/// Spawn a background task that searches the buffer for any whitespace
@@ -1231,12 +1286,12 @@ impl Buffer {
}
pub fn is_dirty(&self) -> bool {
self.saved_version_fingerprint != self.as_rope().fingerprint()
self.file_fingerprint != self.as_rope().fingerprint()
|| self.file.as_ref().map_or(false, |file| file.is_deleted())
}
pub fn has_conflict(&self) -> bool {
self.saved_version_fingerprint != self.as_rope().fingerprint()
self.file_fingerprint != self.as_rope().fingerprint()
&& self
.file
.as_ref()

View File

@@ -10,7 +10,7 @@ path = "src/live_kit_client2.rs"
doctest = false
[[example]]
name = "test_app"
name = "test_app2"
[features]
test-support = [

View File

@@ -1,7 +1,7 @@
use std::{sync::Arc, time::Duration};
use futures::StreamExt;
use gpui::KeyBinding;
use gpui::{Action, KeyBinding};
use live_kit_client2::{
LocalAudioTrack, LocalVideoTrack, RemoteAudioTrackUpdate, RemoteVideoTrackUpdate, Room,
};
@@ -10,7 +10,7 @@ use log::LevelFilter;
use serde_derive::Deserialize;
use simplelog::SimpleLogger;
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Action)]
struct Quit;
fn main() {

View File

@@ -1,7 +1,7 @@
use editor::Editor;
use gpui::{
div, prelude::*, uniform_list, Component, Div, MouseButton, Render, Task,
UniformListScrollHandle, View, ViewContext, WindowContext,
div, prelude::*, uniform_list, AppContext, Component, Div, FocusHandle, FocusableView,
MouseButton, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,
};
use std::{cmp, sync::Arc};
use ui::{prelude::*, v_stack, Divider, Label, TextColor};
@@ -35,6 +35,12 @@ pub trait PickerDelegate: Sized + 'static {
) -> Self::ListItem;
}
impl<D: PickerDelegate> FocusableView for Picker<D> {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.editor.focus_handle(cx)
}
}
impl<D: PickerDelegate> Picker<D> {
pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
let editor = cx.build_view(|cx| {

View File

@@ -6190,7 +6190,7 @@ impl Project {
.log_err();
}
buffer.file_updated(Arc::new(new_file), cx).detach();
buffer.file_updated(Arc::new(new_file), cx);
}
}
});
@@ -7182,7 +7182,7 @@ impl Project {
.ok_or_else(|| anyhow!("no such worktree"))?;
let file = File::from_proto(file, worktree, cx)?;
buffer.update(cx, |buffer, cx| {
buffer.file_updated(Arc::new(file), cx).detach();
buffer.file_updated(Arc::new(file), cx);
});
this.detect_language_for_buffer(&buffer, cx);
}

View File

@@ -959,7 +959,7 @@ impl LocalWorktree {
buffer_handle.update(&mut cx, |buffer, cx| {
if has_changed_file {
buffer.file_updated(new_file, cx).detach();
buffer.file_updated(new_file, cx);
}
});
}

View File

@@ -6262,7 +6262,7 @@ impl Project {
.log_err();
}
buffer.file_updated(Arc::new(new_file), cx).detach();
buffer.file_updated(Arc::new(new_file), cx);
}
}
});
@@ -7256,7 +7256,7 @@ impl Project {
.ok_or_else(|| anyhow!("no such worktree"))?;
let file = File::from_proto(file, worktree, cx)?;
buffer.update(cx, |buffer, cx| {
buffer.file_updated(Arc::new(file), cx).detach();
buffer.file_updated(Arc::new(file), cx);
});
this.detect_language_for_buffer(&buffer, cx);
}

View File

@@ -2587,6 +2587,73 @@ async fn test_save_file(cx: &mut gpui::TestAppContext) {
assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text()));
}
#[gpui::test(iterations = 30)]
async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor().clone());
fs.insert_tree(
"/dir",
json!({
"file1": "the original contents",
}),
)
.await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
let worktree = project.read_with(cx, |project, _| project.worktrees().next().unwrap());
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
.await
.unwrap();
// Simulate buffer diffs being slow, so that they don't complete before
// the next file change occurs.
cx.executor().deprioritize(*language::BUFFER_DIFF_TASK);
// Change the buffer's file on disk, and then wait for the file change
// to be detected by the worktree, so that the buffer starts reloading.
fs.save(
"/dir/file1".as_ref(),
&"the first contents".into(),
Default::default(),
)
.await
.unwrap();
worktree.next_event(cx);
// Change the buffer's file again. Depending on the random seed, the
// previous file change may still be in progress.
fs.save(
"/dir/file1".as_ref(),
&"the second contents".into(),
Default::default(),
)
.await
.unwrap();
worktree.next_event(cx);
cx.executor().run_until_parked();
let on_disk_text = fs.load(Path::new("/dir/file1")).await.unwrap();
buffer.read_with(cx, |buffer, _| {
let buffer_text = buffer.text();
if buffer_text == on_disk_text {
assert!(
!buffer.is_dirty() && !buffer.has_conflict(),
"buffer shouldn't be dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}",
);
}
// If the file change occurred while the buffer was processing the first
// change, the buffer will be in a conflicting state.
else {
assert!(
buffer.is_dirty() && buffer.has_conflict(),
"buffer should report that it has a conflict. text: {buffer_text:?}, disk text: {on_disk_text:?}"
);
}
});
}
#[gpui::test]
async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) {
init_test(cx);

View File

@@ -276,6 +276,7 @@ struct ShareState {
_maintain_remote_snapshot: Task<Option<()>>,
}
#[derive(Clone)]
pub enum Event {
UpdatedEntries(UpdatedEntriesSet),
UpdatedGitRepositories(UpdatedGitRepositoriesSet),
@@ -961,7 +962,7 @@ impl LocalWorktree {
buffer_handle.update(&mut cx, |buffer, cx| {
if has_changed_file {
buffer.file_updated(new_file, cx).detach();
buffer.file_updated(new_file, cx);
}
})?;
}

View File

@@ -9,10 +9,10 @@ use file_associations::FileAssociations;
use anyhow::{anyhow, Result};
use gpui::{
actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
ClipboardItem, Component, Div, EventEmitter, FocusHandle, Focusable, InteractiveComponent,
Model, MouseButton, ParentComponent, Pixels, Point, PromptLevel, Render, Stateful,
StatefulInteractiveComponent, Styled, Task, UniformListScrollHandle, View, ViewContext,
VisualContext as _, WeakView, WindowContext,
ClipboardItem, Component, Div, EventEmitter, FocusHandle, Focusable, FocusableView,
InteractiveComponent, Model, MouseButton, ParentComponent, Pixels, Point, PromptLevel, Render,
Stateful, StatefulInteractiveComponent, Styled, Task, UniformListScrollHandle, View,
ViewContext, VisualContext as _, WeakView, WindowContext,
};
use menu::{Confirm, SelectNext, SelectPrev};
use project::{
@@ -32,7 +32,7 @@ use std::{
use theme::ActiveTheme as _;
use ui::{h_stack, v_stack, IconElement, Label};
use unicase::UniCase;
use util::{maybe, TryFutureExt};
use util::{maybe, ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, PanelEvent},
Workspace,
@@ -130,6 +130,13 @@ pub fn init_settings(cx: &mut AppContext) {
pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
init_settings(cx);
file_associations::init(assets, cx);
cx.observe_new_views(|workspace: &mut Workspace, _| {
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
workspace.toggle_panel_focus::<ProjectPanel>(cx);
});
})
.detach();
}
#[derive(Debug)]
@@ -303,32 +310,31 @@ impl ProjectPanel {
project_panel
}
pub fn load(
pub async fn load(
workspace: WeakView<Workspace>,
cx: AsyncWindowContext,
) -> Task<Result<View<Self>>> {
cx.spawn(|mut cx| async move {
// let serialized_panel = if let Some(panel) = cx
// .background_executor()
// .spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) })
// .await
// .log_err()
// .flatten()
// {
// Some(serde_json::from_str::<SerializedProjectPanel>(&panel)?)
// } else {
// None
// };
workspace.update(&mut cx, |workspace, cx| {
let panel = ProjectPanel::new(workspace, cx);
// if let Some(serialized_panel) = serialized_panel {
// panel.update(cx, |panel, cx| {
// panel.width = serialized_panel.width;
// cx.notify();
// });
// }
panel
})
mut cx: AsyncWindowContext,
) -> Result<View<Self>> {
let serialized_panel = cx
.background_executor()
.spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) })
.await
.map_err(|e| anyhow!("Failed to load project panel: {}", e))
.log_err()
.flatten()
.map(|panel| serde_json::from_str::<SerializedProjectPanel>(&panel))
.transpose()
.log_err()
.flatten();
workspace.update(&mut cx, |workspace, cx| {
let panel = ProjectPanel::new(workspace, cx);
if let Some(serialized_panel) = serialized_panel {
panel.update(cx, |panel, cx| {
panel.width = serialized_panel.width;
cx.notify();
});
}
panel
})
}
@@ -1516,33 +1522,27 @@ impl workspace::dock::Panel for ProjectPanel {
cx.notify();
}
fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
Some("icons/project.svg")
fn icon(&self, _: &WindowContext) -> Option<ui::Icon> {
Some(ui::Icon::FileTree)
}
fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
("Project Panel".into(), Some(Box::new(ToggleFocus)))
fn toggle_action(&self) -> Box<dyn Action> {
Box::new(ToggleFocus)
}
// fn should_change_position_on_event(event: &Self::Event) -> bool {
// matches!(event, Event::DockPositionChanged)
// }
fn has_focus(&self, _: &WindowContext) -> bool {
self.has_focus
}
fn persistent_name(&self) -> &'static str {
fn persistent_name() -> &'static str {
"Project Panel"
}
}
fn focus_handle(&self, _cx: &WindowContext) -> FocusHandle {
impl FocusableView for ProjectPanel {
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
// fn is_focus_event(event: &Self::Event) -> bool {
// matches!(event, Event::Focus)
// }
}
impl ClipboardEntry {
@@ -1579,7 +1579,7 @@ mod tests {
path::{Path, PathBuf},
sync::atomic::{self, AtomicUsize},
};
use workspace::{pane, AppState};
use workspace::AppState;
#[gpui::test]
async fn test_visible_list(cx: &mut gpui::TestAppContext) {
@@ -2785,7 +2785,7 @@ mod tests {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
init_settings(cx);
theme::init(cx);
theme::init(theme::LoadThemes::JustBase, cx);
language::init(cx);
editor::init_settings(cx);
crate::init((), cx);
@@ -2798,11 +2798,10 @@ mod tests {
fn init_test_with_editor(cx: &mut TestAppContext) {
cx.update(|cx| {
let app_state = AppState::test(cx);
theme::init(cx);
theme::init(theme::LoadThemes::JustBase, cx);
init_settings(cx);
language::init(cx);
editor::init(cx);
pane::init(cx);
crate::init((), cx);
workspace::init(app_state.clone(), cx);
Project::init_settings(cx);

View File

@@ -41,6 +41,10 @@ impl Rope {
Self::default()
}
pub fn text_fingerprint(text: &str) -> RopeFingerprint {
bromberg_sl2::hash_strict(text.as_bytes())
}
pub fn append(&mut self, rope: Rope) {
let mut chunks = rope.chunks.cursor::<()>();
chunks.next(&());
@@ -931,7 +935,7 @@ impl<'a> From<&'a str> for ChunkSummary {
fn from(text: &'a str) -> Self {
Self {
text: TextSummary::from(text),
fingerprint: bromberg_sl2::hash_strict(text.as_bytes()),
fingerprint: Rope::text_fingerprint(text),
}
}
}

View File

@@ -41,6 +41,10 @@ impl Rope {
Self::default()
}
pub fn text_fingerprint(text: &str) -> RopeFingerprint {
bromberg_sl2::hash_strict(text.as_bytes())
}
pub fn append(&mut self, rope: Rope) {
let mut chunks = rope.chunks.cursor::<()>();
chunks.next(&());
@@ -931,7 +935,7 @@ impl<'a> From<&'a str> for ChunkSummary {
fn from(text: &'a str) -> Self {
Self {
text: TextSummary::from(text),
fingerprint: bromberg_sl2::hash_strict(text.as_bytes()),
fingerprint: Rope::text_fingerprint(text),
}
}
}

View File

@@ -9,7 +9,7 @@ use schemars::{
};
use serde::Deserialize;
use serde_json::Value;
use util::asset_str;
use util::{asset_str, ResultExt};
#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
#[serde(transparent)]
@@ -73,9 +73,9 @@ impl KeymapFile {
"Expected first item in array to be a string."
)));
};
gpui::build_action(&name, Some(data))
cx.build_action(&name, Some(data))
}
Value::String(name) => gpui::build_action(&name, None),
Value::String(name) => cx.build_action(&name, None),
Value::Null => Ok(no_action()),
_ => {
return Some(Err(anyhow!("Expected two-element array, got {action:?}")))
@@ -86,9 +86,7 @@ impl KeymapFile {
"invalid binding value for keystroke {keystroke}, context {context:?}"
)
})
// todo!()
.ok()
// .log_err()
.log_err()
.map(|action| KeyBinding::load(&keystroke, action, context.as_deref()))
})
.collect::<Result<Vec<_>>>()?;

View File

@@ -16,6 +16,9 @@ pub fn test_settings() -> String {
.unwrap();
util::merge_non_null_json_value_into(
serde_json::json!({
"ui_font_family": "Courier",
"ui_font_features": {},
"ui_font_size": 14,
"buffer_font_family": "Courier",
"buffer_font_features": {},
"buffer_font_size": 14,

View File

@@ -164,6 +164,23 @@ impl Column for i64 {
}
}
impl StaticColumnCount for u64 {}
impl Bind for u64 {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement
.bind_int64(start_index, (*self) as i64)
.with_context(|| format!("Failed to bind i64 at index {start_index}"))?;
Ok(start_index + 1)
}
}
impl Column for u64 {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let result = statement.column_int64(start_index)? as u64;
Ok((result, start_index + 1))
}
}
impl StaticColumnCount for u32 {}
impl Bind for u32 {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {

View File

@@ -57,7 +57,6 @@ impl Render for FocusStory {
.size_full()
.bg(color_1)
.focus(|style| style.bg(color_2))
.focus_in(|style| style.bg(color_3))
.child(
div()
.track_focus(&self.child_1_focus)

View File

@@ -1,5 +1,6 @@
use gpui::{div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext};
use theme2::ActiveTheme;
use ui::Tooltip;
pub struct ScrollStory;
@@ -35,16 +36,18 @@ impl Render for ScrollStory {
} else {
color_2
};
div().id(id).bg(bg).size(px(100. as f32)).when(
row >= 5 && column >= 5,
|d| {
div()
.id(id)
.tooltip(move |_, cx| Tooltip::text(format!("{}, {}", row, column), cx))
.bg(bg)
.size(px(100. as f32))
.when(row >= 5 && column >= 5, |d| {
d.overflow_scroll()
.child(div().size(px(50.)).bg(color_1))
.child(div().size(px(50.)).bg(color_2))
.child(div().size(px(50.)).bg(color_1))
.child(div().size(px(50.)).bg(color_2))
},
)
})
}))
}))
}

View File

@@ -60,13 +60,12 @@ fn main() {
.unwrap();
cx.set_global(store);
theme2::init(cx);
theme2::init(theme2::LoadThemes::All, cx);
let selector =
story_selector.unwrap_or(StorySelector::Component(ComponentStory::Workspace));
let theme_registry = cx.global::<ThemeRegistry>();
let mut theme_settings = ThemeSettings::get_global(cx).clone();
theme_settings.active_theme = theme_registry.get(&theme_name).unwrap();
ThemeSettings::override_global(theme_settings, cx);
@@ -114,6 +113,7 @@ impl Render for StoryWrapper {
.flex()
.flex_col()
.size_full()
.font("Zed Mono")
.child(self.story.clone())
}
}

View File

@@ -0,0 +1,17 @@
[package]
name = "storybook3"
version = "0.1.0"
edition = "2021"
publish = false
[[bin]]
name = "storybook"
path = "src/storybook3.rs"
[dependencies]
anyhow.workspace = true
gpui = { package = "gpui2", path = "../gpui2" }
ui = { package = "ui2", path = "../ui2", features = ["stories"] }
theme = { package = "theme2", path = "../theme2", features = ["stories"] }
settings = { package = "settings2", path = "../settings2"}

View File

@@ -0,0 +1,73 @@
use anyhow::Result;
use gpui::AssetSource;
use gpui::{
div, px, size, AnyView, Bounds, Div, Render, ViewContext, VisualContext, WindowBounds,
WindowOptions,
};
use settings::{default_settings, Settings, SettingsStore};
use std::borrow::Cow;
use std::sync::Arc;
use theme::ThemeSettings;
use ui::{prelude::*, ContextMenuStory};
struct Assets;
impl AssetSource for Assets {
fn load(&self, _path: &str) -> Result<Cow<[u8]>> {
todo!();
}
fn list(&self, _path: &str) -> Result<Vec<SharedString>> {
Ok(vec![])
}
}
fn main() {
let asset_source = Arc::new(Assets);
gpui::App::production(asset_source).run(move |cx| {
let mut store = SettingsStore::default();
store
.set_default_settings(default_settings().as_ref(), cx)
.unwrap();
cx.set_global(store);
ui::settings::init(cx);
theme::init(theme::LoadThemes::JustBase, cx);
cx.open_window(
WindowOptions {
bounds: WindowBounds::Fixed(Bounds {
origin: Default::default(),
size: size(px(1500.), px(780.)).into(),
}),
..Default::default()
},
move |cx| {
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size;
cx.set_rem_size(ui_font_size);
cx.build_view(|cx| TestView {
story: cx.build_view(|_| ContextMenuStory).into(),
})
},
);
cx.activate(true);
})
}
struct TestView {
story: AnyView,
}
impl Render for TestView {
type Element = Div<Self>;
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
div()
.flex()
.flex_col()
.size_full()
.font("Helvetica")
.child(self.story.clone())
}
}

Some files were not shown because too many files have changed in this diff Show More