Compare commits

..

58 Commits

Author SHA1 Message Date
Antonio Scandurra
2205430814 WIP 2024-04-19 18:04:04 +02:00
Antonio Scandurra
fa410ec150 WIP: start on EditorElement::after_layout 2024-04-19 14:14:21 +02:00
Antonio Scandurra
504ea1aaea Merge remote-tracking branch 'origin/main' into before-paint 2024-04-19 10:33:18 +02:00
Kirill Bulatov
222034cacf Always provide default task context (#10764)
Based on
https://github.com/zed-industries/zed/issues/8324?notification_referrer_id=NT_kwDOACkO1bI5NTk0NjM0NzkyOjI2OTA3NzM&notifications_query=repo%3Azed-industries%2Fzed+is%3Aunread#issuecomment-2065551553

Release Notes:

- Fixed certain files' task modal not showing context-based tasks
2024-04-19 10:51:50 +03:00
张小白
9863b920b0 windows: Fix panic with some unicode characters (#10750)
Fix #10749 

Release Notes:

- N/A
2024-04-18 22:28:39 -07:00
Joseph T. Lyons
ea952b2a95 Remove empty script 2024-04-18 22:24:48 -04:00
Jason Wen
dd7eced2b6 Prevent command prompt from opening when running git blame on windows (#10747)
Release Notes:

- Fixed issue reported in discord where the git blame feature would open
command prompt windows
2024-04-18 16:02:24 -07:00
apricotbucket28
d4922eb10b wayland: Fix window close (#10702)
Partially fixes https://github.com/zed-industries/zed/issues/10483 (X11
still has this issue)

Also adds some missing destroy() calls for some objects.
Thanks @phisch!

Release Notes:

- N/A
2024-04-18 16:02:05 -07:00
Kirill Bulatov
95827d4c49 Fix the typo 2024-04-19 01:52:16 +03:00
Kirill Bulatov
2602fc47bb Match user selection when renaming (#10748)
Initial state:
<img width="337" alt="Screenshot 2024-04-19 at 01 35 34"
src="https://github.com/zed-industries/zed/assets/2690773/1720d06c-54ed-4479-b694-ea478ac5a55a">

Before the fix:
<img width="319" alt="Screenshot 2024-04-19 at 01 35 39"
src="https://github.com/zed-industries/zed/assets/2690773/64429088-e75b-44c3-b5d4-31a841e69a1d">

After:
<img width="336" alt="Screenshot 2024-04-19 at 01 36 43"
src="https://github.com/zed-industries/zed/assets/2690773/c523e549-c546-4a70-aa33-629912598466">
 

Release Notes:

- Improved rename selections to match the user ones
2024-04-19 01:49:14 +03:00
Kirill Bulatov
6d1ea782a4 Show tooltip in task spawn modal (#10744)
Tooltip shows original task template's label, if it differs from the one
displayed in the modal. Also, a resolved command with args will be shown
in the tooltip if different from the modal entry text.

<img width="578" alt="Screenshot 2024-04-19 at 00 40 28"
src="https://github.com/zed-industries/zed/assets/2690773/c89369d6-8ffc-4464-ab3b-ea5e8fb7625a">
<img width="761" alt="Screenshot 2024-04-19 at 00 40 32"
src="https://github.com/zed-industries/zed/assets/2690773/b02f1518-976a-4a9b-ba7c-f88c6e056217">
<img width="738" alt="Screenshot 2024-04-19 at 00 40 56"
src="https://github.com/zed-industries/zed/assets/2690773/be502537-f4bd-4ae0-a5e7-78e37fe8fb00">
<img width="785" alt="Screenshot 2024-04-19 at 00 41 01"
src="https://github.com/zed-industries/zed/assets/2690773/9bedcd21-8729-44c8-9a17-46a5a01c7f26">


Release Notes:

- Added tooltips into task spawn modal
2024-04-19 01:43:52 +03:00
Kirill Bulatov
870a61dd4d Add "Open in Terminal" context menu entries for project panel, editor and tab context menus (#10741)
Closes https://github.com/zed-industries/zed/issues/4566

Pane tabs (does not exist for multibuffer tabs):
<img width="439" alt="Screenshot 2024-04-18 at 23 01 08"
src="https://github.com/zed-industries/zed/assets/2690773/3af79ed8-07ea-4cf2-bcf9-735b1b3be8c4">

Editor context menu:
<img width="404" alt="Screenshot 2024-04-18 at 23 01 14"
src="https://github.com/zed-industries/zed/assets/2690773/38ea7afc-df2b-45ef-8331-eb6a4588af9f">

Project panel context menu (was not shown for file entries before this):
<img width="408" alt="Screenshot 2024-04-18 at 23 01 18"
src="https://github.com/zed-industries/zed/assets/2690773/e336fce1-7da0-4671-b8d2-8d3409c23eb6">

Release Notes:

- (breaking change) Moved `project_panel::OpenInTerminal` into
`workspace::OpenInTerminal` action and add it in editors, tab context
menus and proper panel file entries
([4566](https://github.com/zed-industries/zed/issues/4566))
2024-04-19 01:43:46 +03:00
Kirill Bulatov
250b71fb44 Select buffer search query on follow-up cmd-f (#10745)
https://github.com/zed-industries/zed/assets/2690773/fd754cfc-aca5-4c4d-9b42-53d1c8eca0e9

Unfortunately, the tests did not work for me, as the `FocusSearch`
action handlers were never called for me.

Release Notes:

- Improved buffer search workflow: follow-up cmd-f select query string
2024-04-19 01:43:33 +03:00
Philipp Schaffrath
15c4c4a308 wayland: Fix input_handler out of range access (#10724)
The wayland implementation takes an input handler from the state, but
only puts it back if the event was an IME key. I flipped the logic to
ensure it's always put back.

This should fix both:
- #10344
- #10652

Release Notes:

- Fixed input_handler out of range access
([#10344](https://github.com/zed-industries/zed/issues/10344),
[#10652](https://github.com/zed-industries/zed/issues/10652)).
2024-04-18 15:20:23 -07:00
apricotbucket28
b31df39ab0 linux: Primary clipboard (#10534)
Implements copying from and pasting to the primary selection.

Release Notes:

- N/A
2024-04-18 14:54:18 -07:00
Owen Law
98db7fa61e Use XI2 for Scrolling on X11 (#10695)
Changes the X11 platform code to use the xinput extension which allows
for smooth scrolling and horizontal scrolling.

Release Notes:

- Added smooth scrolling to X11 on Linux
- Added horizontal scrolling to X11 on Linux
2024-04-18 14:44:21 -07:00
Nimit Savant
bd5473a582 docs: Install Windows 10/11 SDK with VS installation (#10550)
This installation is also needed with VS installation. Only then they
would be able to target the WINDOWS SDK

Optionally, include screenshots / media showcasing your addition that
can be included in the release notes.

I was getting link.exe error before this stating that my MSVC was not
installed properly. But MSVC was perfectly installed. I went on
stackoverflow and checked for similar instances with `cargo run` on
windows and found this

https://stackoverflow.com/a/55603112/12859779

where in comment Fasis states that we need to install WINDOWS 10 SDK. I
had Windos 11 so Installed that and it worked :)

Release Notes:

- N/A
2024-04-18 14:42:20 -07:00
Marshall Bowers
1fbc04104c Move lints section to the top of Cargo.toml, to match the others 2024-04-18 15:53:48 -04:00
Philipp Schaffrath
2f892e3523 Improve drop targets (#10643)
This introduces multiple improvements to the drop targets.

## Hitbox shape
Currently, hitboxes are rectangles, where the vertical ones reach all
the way to the ends, which reduces the space for the horizontal ones,
making the hitboxes a bit awkward in the corners. This new approach just
determines the closest side.

Visual representation:
![Frame
3](https://github.com/zed-industries/zed/assets/1282767/1cd2ca31-d9d4-41dd-87fb-1a8fbb8b7fcc)

## Hitbox size
The width of the hitbox was currently always 8 rem all around. In setups
with many columns or rows, or when the font size was very large, this
could potentially overlap the center hitbox, not allowing to drop a tab
without another split. Now the width of the hitboxes are a fraction of
the smaller size of its parents width and height. This makes sure the
hitboxes have the same width all around, but never fully block the
center hitbox.

I've also made this value configurable through the new
`drop_target_size` config which takes a `f32` fraction and is set to 0.2
by default.

Not sure if this is worth mentioning, but this technically allows to
remove the split hitboxes all together by setting it to `0.0`, or
removing the center hitbox by setting it to any value `>=0.5`. Not that
this is necessary, but it would be possible now.

## Larger visualization
The visual overlay when using one of the side hitboxes were also `8em`
wide. Since their logical size now changed, and it can't currently be
represented with GPUI (without abusing the `canvas` element), I made the
visual feedback take half of the width or height of the available space,
just like how other editors do this.

Also, the opacity/alpha value set by a theme is currently ignored. This
change now respects the themes opacity for it!

## Respect alpha value
Currently, the alpha value of `drop_target.background` is ignored. Even
the default themes set a value that is overwritten by a hard coded
value. I have removed this hard coded value and it now respects the
alpha value.

This change affects existing themes, see
https://github.com/zed-industries/zed/pull/10643#issuecomment-2059641528


## ~~No more lag while dragging over gutter~~ Extracted into #10737
~~It looks like the editor had a small optimization to drop events when
hovering the gutter. This also happens while dragging a tab over the
gutter, and causes some stuttering. Please correct me if this wasn't
just a small optimization, but I could not derive a different reason for
this code to exist.~~

Here is a video that tries to show all those changes with a before on
the left, and the after on the right:


https://github.com/zed-industries/zed/assets/1282767/f97f3420-513f-410f-a1c8-7966429ad348


Release Notes:

- Added `drop_target_size` setting. This should be a fractional percent
(e.g., `0.5`).
- Improved the hitboxes for drop targets.
- Updated drop targets to respect the alpha channel of the
`drop_target.background` color.
2024-04-18 15:28:25 -04:00
张小白
5c3e5cc45d windows: Support emoji inputs (#10125)
To work properly, needs #10119 to be merged.



https://github.com/zed-industries/zed/assets/14981363/2bb0c51b-6c70-4b29-8baa-302fb4fb9e89



Release Notes:

- N/A
2024-04-18 11:59:22 -07:00
张小白
11a3d2b04b windows: Introduce Direct Write (#10119)
This PR brings `Direct Write` to Zed. Now, Zed first trys to query
dwrite interface, if not supported, which means runing on Windows below
win10 1703), will choose `cosmic` as a fallback text system.

This direct write text system supports:
- Full font features support
- Emoji support
- Default system fonts as fallback

### Font features


https://github.com/zed-industries/zed/assets/14981363/198eff88-47df-4bc8-a257-e3acf81fd61d

### Emoji

![Screenshot 2024-04-03
211354](https://github.com/zed-industries/zed/assets/14981363/a5bc5845-42e8-4af1-af7e-abba598c1e72)

**Note: input emoji through IME or IMM not working yet, copy paste emoji
works fine (will be fixed by #10125 )**

### Font fallback

I use `Zed mono` which dose not support chinese chars to test font
fallback



https://github.com/zed-industries/zed/assets/14981363/c97d0847-0ac5-47e6-aa00-f3ce6d1e50a5



Release Notes:

- N/A
2024-04-18 11:58:46 -07:00
Antonio Scandurra
d75f1e64b8 Merge remote-tracking branch 'origin/main' into before-paint 2024-04-18 18:39:25 +02:00
Antonio Scandurra
1358758226 Fix errors in editor.rs 2024-04-18 18:39:19 +02:00
Antonio Scandurra
9de4dc12dd Implement PaneAxisElement::after_layout 2024-04-18 18:34:15 +02:00
Marshall Bowers
1127b1a0de glsl: Bump to v0.1.0 (#10734)
This PR bumps the GLSL extension to v0.1.0.

Changes:

- #10694

Release Notes:

- N/A
2024-04-18 11:57:21 -04:00
Antonio Scandurra
7ae13c9753 Implement DisconnectedOverlay::after_layout 2024-04-18 17:31:22 +02:00
jansol
c55055599a glsl: Add glsl_analyzer (LSP) (#10694)
<img width="691" alt="image"
src="https://github.com/zed-industries/zed/assets/2588851/c5e02d12-d1e4-4407-971c-72de7e6599f0">

@mikayla-maki the extension lists you as the original author.

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-04-18 11:23:11 -04:00
Antonio Scandurra
d4bae0475f Add RightClickMenu::after_layout 2024-04-18 15:57:48 +02:00
Antonio Scandurra
72a4e525d7 Add PopoverMenu::after_layout 2024-04-18 15:47:40 +02:00
Antonio Scandurra
15f4e93102 Re-enable view caching 2024-04-18 15:41:04 +02:00
Antonio Scandurra
dbfb588e3e Wire up List::after_layout
Note that we're still not making use of the new focus target bounds returned by
the child elements.
2024-04-18 14:34:27 +02:00
Bennet Bo Fenner
a202499c9a markdown preview: Update channel notes when other collaborator changes buffer (#10718)
https://github.com/zed-industries/zed/assets/53836821/9a57885e-83b0-49fb-b3a8-0a7868566b85


Release Notes:

- Markdown preview now re-renders when another collaborator changes the
content of channel notes
2024-04-18 14:26:32 +02:00
Thorsten Ball
c2428f9f5d git blame: Parse permalinks client side (#10714)
Release Notes:

- N/A
2024-04-18 12:36:22 +02:00
Kirill Bulatov
d5c5394693 Open exactly one terminal on workspace::NewTerminal action (#10721)
Fixes https://github.com/zed-industries/zed/issues/4567

Release Notes:

- Fixed multiple terminals being opened on `workspace::NewTerminal`
calls ([4567](https://github.com/zed-industries/zed/issues/4567))
2024-04-18 12:44:40 +03:00
Antonio Scandurra
8ead209099 Implement UniformList::after_layout
The list doesn't really have a mechanism for scrolling to the focused
element right now, so we should consider adding it before merging this
branch.
2024-04-18 11:37:31 +02:00
Shreekar Halvi
bb97432e9a Add minimum column option to git inline blame (#10682)
Release Notes:

- Added a setting to determine the minimum column where the inline blame
information is shown. Example: `{{"git": {"inline_blame": {"min_column":
80}}}` ([#10555](https://github.com/zed-industries/zed/issues/10555)).

Demo Video:


https://github.com/zed-industries/zed/assets/1185253/61343dbe-9002-4bd1-b0d4-403f8da79050

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
2024-04-18 11:28:47 +02:00
David Baldwin
1b75f9d620 Add a setting to show/hide the project panel button in the status bar (#10707)
### What?

A setting has been added to control the visibility of the Project Panel
button in the status bar.

### Why?

I don't tend to use the Project Panel, but use a keyboard shortcut to
access if needed. Thus, the button in the status bar provides me little
more than visual clutter. Additionally, there is precedent for this
configurability with other panels (collaboration, chat, notification,
terminal, etc).

Release Notes:

- Added a setting to show/hide the Project Panel button in the status
bar. `{"project_panel": {"button": false}}`
2024-04-18 10:14:06 +03:00
Conrad Irwin
4c3178e7a8 Have the CI server draft the release notes (#10700)
While I don't expect these to be useful for our weekly minor releases, I
hope that this will save a step for people doing mid-week patches.

Release Notes:

- N/A
2024-04-17 15:51:45 -06:00
Conrad Irwin
41c8f2caa6 Attempt to fix segfault in window drop (#10690)
By default NSWindow's release themselves when closed, which doesn't
interact well with rust's lifetime system.

Disable that behaviour, and explicitly release the NSWindow when the
window handle is dropped.

Release Notes:

- Fixed a (rare) panic when closing a window.
2024-04-17 15:29:10 -06:00
Piotr Osiewicz
b9e0269991 project panel: do not expand collapsed worktrees on "collapse all entries" (#10687)
Fixes #10597

Release Notes:

- Fixed "project panel: collapse all entries" expanding collapsed
worktrees.
2024-04-17 22:55:20 +02:00
Piotr Osiewicz
4f2214e1d6 terminal: Treat paths with non-digit col/rows as paths nonetheless (#10691)
This relaxes path parsing to allow paths like ./foo.rs:food or
./food/foo_bar.rs:2:12:food as some tools may add a suffix without
regard for col/row end.

Fixes #10688 

Release Notes:

- Made path parsing in terminal (for directory links) more lenient with
regards to row/column fields.
2024-04-17 22:55:08 +02:00
Marshall Bowers
e25f0dfb0a v0.133.x dev 2024-04-17 13:05:26 -04:00
Marshall Bowers
3c805d4c6b prisma: Bump to v0.0.2 (#10689)
This PR bumps the Prisma extension to v0.02.

Changes:

- The Prisma extension now provides its own `tab_size` setting
([#10296](https://github.com/zed-industries/zed/pull/10296))

Release Notes:

- N/A
2024-04-17 12:40:01 -04:00
Antonio Scandurra
eb14932950 WIP: start migrating UniformList 2024-04-17 17:58:23 +02:00
Thorsten Ball
4f1861edb6 git blame: ignore uncommitted files or repos without commits (#10685)
This fixes useless error messages popping up in case a file hasn't been
committed yet or the repo doesn't have commits yet.

Release Notes:

- Fixed git blame functionality not handling errors correctly when there
are no commits yet or when file isn't committed yet.
2024-04-17 17:51:26 +02:00
Antonio Scandurra
d3756cdab3 WIP
Co-Authored-By: Nathan <nathan@zed.dev>
2024-04-17 16:52:46 +02:00
Thorsten Ball
d7becce9aa git: Only show inline git blame when editor is focused (#10680)
Release Notes:

- N/A
2024-04-17 13:28:11 +02:00
Thorsten Ball
62171387f6 Do not show tooltip for editor controls if clicked (#10679)
This avoids the tooltip showing up when the context menu is visible.

It fixes this:

![screenshot-2024-04-17-13 17
41@2x](https://github.com/zed-industries/zed/assets/1185253/373bb70e-9c7f-4b9f-a928-8206697c6039)


Release Notes:

- N/A
2024-04-17 13:20:47 +02:00
Thorsten Ball
47ad010901 Backport documentation for inline git blame (#10677)
Only noticed this when editing zed.dev.

Release Notes:

- N/A
2024-04-17 13:08:04 +02:00
Piotr Osiewicz
06987edadb project panel: Fix alignment of entries overflowing the panel. (#10676)
With file icons turned off, we still reserve space for an icon and make
it invisible. However, that space was marked as flex, which made it
shrink in case subsequent file name could not fit in the current width
of the project panel. Fixes #10622



https://github.com/zed-industries/zed/assets/24362066/d565a03a-3712-49d1-bf52-407e4508a8cf


Release Notes:


- Fixed project panel entries misalignment with narrow panel & file
icons turned off.
2024-04-17 12:54:56 +02:00
Thorsten Ball
1e1a2807db Document inline git blame (#10675)
Release Notes:

- N/A
2024-04-17 12:53:53 +02:00
Antonio Scandurra
d4c54f3479 WIP: Start introducing a new after_layout pass 2024-04-17 12:47:59 +02:00
Bennet Bo Fenner
9782dd342f docs: Sync with zed.dev version (#10674)
This PR brings the docs in line with the version we have on
https://zed.dev

Release Notes:

- N/A
2024-04-17 12:23:30 +02:00
Antonio Scandurra
c024526db2 Rename after_layout to before_paint 2024-04-17 12:14:14 +02:00
Keith
535bcfad10 Update crates/ui/docs/hello-world.md TODO with explanation of SharedString usage (#10664)
Filled out a comment where there was a TODO to explain SharedString
usage.

Release Notes:

- N/A
2024-04-17 13:04:28 +03:00
Thorsten Ball
c76bacb974 Rename label to toggle inline git blame on/off (#10673)
cc @iamnbutler I think we should differentiate between inline blame and
the gutter blame.

Release Notes:

- N/A
2024-04-17 11:34:34 +02:00
Kirill Bulatov
20554d0296 Fix center element wrapper size (#10672)
Fixes
https://github.com/zed-industries/zed/pull/9754#pullrequestreview-2005401133
Fixes
https://github.com/zed-industries/zed/pull/9754#issuecomment-2060536590
Closes https://github.com/zed-industries/zed/pull/10669

* Updates the docs to use a proper max value for the centered layout
padding (0.4 instead of 0.45)
* Makes the `center` wrapper (`h_flex`) to be of size of the `center`
element always, to ensure terminal lines are displayed correctly

The letter fix is somewhat hacky: while it does the right thing right
now, it does not prevent us from future mistakes like these, and does
not explain why the bottom dock could be of one, smaller, height, and
its contents, the terminal pane/terminal element/something else would
think that it has a larger height, thus breaking the scrolling and
rendering.
cc @alygin if you're interested to solve another layout-related thing.

Release Notes:

- N/A
2024-04-17 12:34:18 +03:00
Thorsten Ball
2c78cf349b Regenerate git blame info when buffer's dirty bit changed (#10670)
This fixes the https://github.com/zed-industries/zed/issues/10583 by
regenerating the blame information after the
buffer is edited. It uses a debounce of 2seconds. Meaning that undone
deletions show up again after 2secs.

Release Notes:

- Fixed `git blame` data not handling the undoing of deletions
correctly.
([#10583](https://github.com/zed-industries/zed/issues/10583)).
2024-04-17 11:25:53 +02:00
96 changed files with 4394 additions and 967 deletions

View File

@@ -205,6 +205,7 @@ jobs:
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
exit 1
fi
script/draft-release-notes "$version" "$channel" > target/release-notes.md
- name: Generate license file
run: script/generate-licenses
@@ -248,7 +249,7 @@ jobs:
target/aarch64-apple-darwin/release/Zed-aarch64.dmg
target/x86_64-apple-darwin/release/Zed-x86_64.dmg
target/release/Zed.dmg
body: ""
body_file: target/release-notes.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

14
Cargo.lock generated
View File

@@ -4327,6 +4327,7 @@ dependencies = [
"time",
"unindent",
"url",
"windows 0.53.0",
]
[[package]]
@@ -9736,7 +9737,6 @@ dependencies = [
"file_icons",
"fuzzy",
"gpui",
"itertools 0.11.0",
"language",
"picker",
"project",
@@ -12265,6 +12265,7 @@ dependencies = [
"any_vec",
"anyhow",
"async-recursion 1.0.5",
"bincode",
"call",
"client",
"clock",
@@ -12521,7 +12522,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.132.0"
version = "0.133.0"
dependencies = [
"activity_indicator",
"anyhow",
@@ -12698,6 +12699,13 @@ dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zed_glsl"
version = "0.1.0"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zed_haskell"
version = "0.1.0"
@@ -12735,7 +12743,7 @@ dependencies = [
[[package]]
name = "zed_prisma"
version = "0.0.1"
version = "0.0.2"
dependencies = [
"zed_extension_api 0.0.4",
]

View File

@@ -110,6 +110,7 @@ members = [
"extensions/emmet",
"extensions/erlang",
"extensions/gleam",
"extensions/glsl",
"extensions/haskell",
"extensions/html",
"extensions/lua",
@@ -259,7 +260,9 @@ futures-batch = "0.6.1"
futures-lite = "1.13"
git2 = { version = "0.18", default-features = false }
globset = "0.4"
heed = { git = "https://github.com/meilisearch/heed", rev = "036ac23f73a021894974b9adc815bc95b3e0482a", features = ["read-txn-no-tls"] }
heed = { git = "https://github.com/meilisearch/heed", rev = "036ac23f73a021894974b9adc815bc95b3e0482a", features = [
"read-txn-no-tls",
] }
hex = "0.4.3"
ignore = "0.4.22"
indoc = "1"
@@ -364,10 +367,16 @@ sys-locale = "0.3.1"
version = "0.53.0"
features = [
"implement",
"Foundation_Numerics",
"Wdk_System_SystemServices",
"Win32_Globalization",
"Win32_Graphics_Direct2D",
"Win32_Graphics_Direct2D_Common",
"Win32_Graphics_DirectWrite",
"Win32_Graphics_Dxgi_Common",
"Win32_Graphics_Gdi",
"Win32_Graphics_Imaging",
"Win32_Graphics_Imaging_D2D",
"Win32_Media",
"Win32_Security",
"Win32_Security_Credentials",

View File

@@ -69,6 +69,8 @@
"confirm_quit": false,
// Whether to restore last closed project when fresh Zed instance is opened.
"restore_on_startup": "last_workspace",
// Size of the drop target in the editor.
"drop_target_size": 0.2,
// Whether the cursor blinks in the editor.
"cursor_blink": true,
// Whether to pop the completions menu while typing in an editor without
@@ -212,6 +214,8 @@
"scroll_debounce_ms": 50
},
"project_panel": {
// Whether to show the project panel button in the status bar
"button": true,
// Default width of the project panel.
"default_width": 240,
// Where to dock the project panel. Can be 'left' or 'right'.

View File

@@ -5,6 +5,9 @@ edition = "2021"
publish = false
license = "AGPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/anthropic.rs"
@@ -17,6 +20,3 @@ util.workspace = true
[dev-dependencies]
tokio.workspace = true
[lints]
workspace = true

View File

@@ -1094,31 +1094,37 @@ impl AssistantPanel {
let view = cx.view().clone();
let scroll_handle = self.saved_conversations_scroll_handle.clone();
let conversation_count = self.saved_conversations.len();
canvas(
move |bounds, cx| {
let mut saved_conversations = uniform_list(
view,
"saved_conversations",
conversation_count,
|this, range, cx| {
range
.map(|ix| this.render_saved_conversation(ix, cx))
.collect()
},
)
.track_scroll(scroll_handle)
.into_any_element();
saved_conversations.layout(
bounds.origin,
bounds.size.map(AvailableSpace::Definite),
cx,
);
saved_conversations
},
|_bounds, mut saved_conversations, cx| saved_conversations.paint(cx),
)
.size_full()
.into_any_element()
todo!("replace canvas")
// canvas(
// move |_, cx| {
// let saved_conversations = uniform_list(
// view.clone(),
// "saved_conversations",
// conversation_count,
// |this, range, cx| {
// range
// .map(|ix| this.render_saved_conversation(ix, cx))
// .collect()
// },
// )
// .track_scroll(scroll_handle.clone())
// .into_any_element();
// saved_conversations.layout(absolute_offset, available_space, cx)
// // compute layout for saved conversations
// saved_conversations
// },
// move |bounds, saved_conversations, cx| {
// saved_conversations.layout(
// bounds.origin,
// bounds.size.map(AvailableSpace::Definite),
// cx,
// );
// saved_conversations
// },
// |_bounds, mut saved_conversations, cx| saved_conversations.paint(cx),
// )
// .size_full()
// .into_any_element()
} else if let Some(editor) = self.active_conversation_editor() {
let editor = editor.clone();
let conversation = editor.read(cx).conversation.clone();

View File

@@ -3,6 +3,7 @@ use crate::{
tests::{rust_lang, TestServer},
};
use call::ActiveCall;
use collections::HashMap;
use editor::{
actions::{
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Redo, Rename, RevertSelectedHunks,
@@ -735,12 +736,60 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
6..9
);
rename.editor.update(cx, |rename_editor, cx| {
let rename_selection = rename_editor.selections.newest::<usize>(cx);
assert_eq!(
rename_selection.range(),
0..3,
"Rename that was triggered from zero selection caret, should propose the whole word."
);
rename_editor.buffer().update(cx, |rename_buffer, cx| {
rename_buffer.edit([(0..3, "THREE")], None, cx);
});
});
});
// Cancel the rename, and repeat the same, but use selections instead of cursor movement
editor_b.update(cx_b, |editor, cx| {
editor.cancel(&editor::actions::Cancel, cx);
});
let prepare_rename = editor_b.update(cx_b, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([7..8]));
editor.rename(&Rename, cx).unwrap()
});
fake_language_server
.handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
assert_eq!(params.position, lsp::Position::new(0, 8));
Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
lsp::Position::new(0, 6),
lsp::Position::new(0, 9),
))))
})
.next()
.await
.unwrap();
prepare_rename.await.unwrap();
editor_b.update(cx_b, |editor, cx| {
use editor::ToOffset;
let rename = editor.pending_rename().unwrap();
let buffer = editor.buffer().read(cx).snapshot(cx);
let lsp_rename_start = rename.range.start.to_offset(&buffer);
let lsp_rename_end = rename.range.end.to_offset(&buffer);
assert_eq!(lsp_rename_start..lsp_rename_end, 6..9);
rename.editor.update(cx, |rename_editor, cx| {
let rename_selection = rename_editor.selections.newest::<usize>(cx);
assert_eq!(
rename_selection.range(),
1..2,
"Rename that was triggered from a selection, should have the same selection range in the rename proposal"
);
rename_editor.buffer().update(cx, |rename_buffer, cx| {
rename_buffer.edit([(0..lsp_rename_end - lsp_rename_start, "THREE")], None, cx);
});
});
});
let confirm_rename = editor_b.update(cx_b, |editor, cx| {
Editor::confirm_rename(editor, &ConfirmRename, cx).unwrap()
});
@@ -2006,6 +2055,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
let inline_blame_off_settings = Some(InlineBlameSettings {
enabled: false,
delay_ms: None,
min_column: None,
});
cx_a.update(|cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
@@ -2040,15 +2090,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
blame_entry("3a3a3a", 2..3),
blame_entry("4c4c4c", 3..4),
],
permalinks: [
("1b1b1b", "http://example.com/codehost/idx-0"),
("0d0d0d", "http://example.com/codehost/idx-1"),
("3a3a3a", "http://example.com/codehost/idx-2"),
("4c4c4c", "http://example.com/codehost/idx-3"),
]
.into_iter()
.map(|(sha, url)| (sha.parse().unwrap(), url.parse().unwrap()))
.collect(),
permalinks: HashMap::default(), // This field is deprecrated
messages: [
("1b1b1b", "message for idx-0"),
("0d0d0d", "message for idx-1"),
@@ -2058,6 +2100,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
.into_iter()
.map(|(sha, message)| (sha.parse().unwrap(), message.into()))
.collect(),
remote_url: Some("git@github.com:zed-industries/zed.git".to_string()),
};
client_a.fs().set_blame_for_repo(
Path::new("/my-repo/.git"),
@@ -2126,7 +2169,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
assert_eq!(details.message, format!("message for idx-{}", idx));
assert_eq!(
details.permalink.unwrap().to_string(),
format!("http://example.com/codehost/idx-{}", idx)
format!("https://github.com/zed-industries/zed/commit/{}", entry.sha)
);
}
});

View File

@@ -305,6 +305,10 @@ impl ChannelView {
});
}
ChannelBufferEvent::BufferEdited => {
// Emit the edited event on the editor context so that other views can update it's state (e.g. markdown preview)
self.editor.update(cx, |_, cx| {
cx.emit(EditorEvent::Edited);
});
if self.editor.read(cx).is_focused(cx) {
self.acknowledge_buffer_version(cx);
} else {

View File

@@ -2834,34 +2834,31 @@ fn render_tree_branch(is_last: bool, overdraw: bool, cx: &mut WindowContext) ->
let thickness = px(1.);
let color = cx.theme().colors().text;
canvas(
|_, _| {},
move |bounds, _, cx| {
let start_x = (bounds.left() + bounds.right() - thickness) / 2.;
let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.;
let right = bounds.right();
let top = bounds.top();
canvas(move |bounds, cx| {
let start_x = (bounds.left() + bounds.right() - thickness) / 2.;
let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.;
let right = bounds.right();
let top = bounds.top();
cx.paint_quad(fill(
Bounds::from_corners(
point(start_x, top),
point(
start_x + thickness,
if is_last {
start_y
} else {
bounds.bottom() + if overdraw { px(1.) } else { px(0.) }
},
),
cx.paint_quad(fill(
Bounds::from_corners(
point(start_x, top),
point(
start_x + thickness,
if is_last {
start_y
} else {
bounds.bottom() + if overdraw { px(1.) } else { px(0.) }
},
),
color,
));
cx.paint_quad(fill(
Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
color,
));
},
)
),
color,
));
cx.paint_quad(fill(
Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
color,
));
})
.w(width)
.h(line_height)
}

View File

@@ -318,26 +318,23 @@ impl Render for CollabTitlebarItem {
}
fn render_color_ribbon(color: Hsla) -> impl Element {
canvas(
move |_, _| {},
move |bounds, _, cx| {
let height = bounds.size.height;
let horizontal_offset = height;
let vertical_offset = px(height.0 / 2.0);
let mut path = Path::new(bounds.lower_left());
path.curve_to(
bounds.origin + point(horizontal_offset, vertical_offset),
bounds.origin + point(px(0.0), vertical_offset),
);
path.line_to(bounds.upper_right() + point(-horizontal_offset, vertical_offset));
path.curve_to(
bounds.lower_right(),
bounds.upper_right() + point(px(0.0), vertical_offset),
);
path.line_to(bounds.lower_left());
cx.paint_path(path, color);
},
)
canvas(move |bounds, cx| {
let height = bounds.size.height;
let horizontal_offset = height;
let vertical_offset = px(height.0 / 2.0);
let mut path = Path::new(bounds.lower_left());
path.curve_to(
bounds.origin + point(horizontal_offset, vertical_offset),
bounds.origin + point(px(0.0), vertical_offset),
);
path.line_to(bounds.upper_right() + point(-horizontal_offset, vertical_offset));
path.curve_to(
bounds.lower_right(),
bounds.upper_right() + point(px(0.0), vertical_offset),
);
path.line_to(bounds.lower_left());
cx.paint_path(path, color);
})
.h_1()
.w_full()
}

View File

@@ -79,7 +79,6 @@ pub async fn open_db<M: Migrator + 'static>(
}
async fn open_main_db<M: Migrator>(db_path: &PathBuf) -> Option<ThreadSafeConnection<M>> {
dbg!(&db_path);
log::info!("Opening main db");
ThreadSafeConnection::<M>::builder(db_path.to_string_lossy().as_ref(), true)
.with_db_initialization_query(DB_INITIALIZE_QUERY)

View File

@@ -131,10 +131,10 @@ use ui::{
use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::item::ItemHandle;
use workspace::notifications::NotificationId;
use workspace::Toast;
use workspace::{
searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace, WorkspaceId,
};
use workspace::{OpenInTerminal, OpenTerminal, Toast};
use crate::hover_links::find_url;
@@ -476,8 +476,8 @@ pub struct Editor {
+ Fn(&mut Self, DisplayPoint, &mut ViewContext<Self>) -> Option<View<ui::ContextMenu>>,
>,
>,
last_bounds: Option<Bounds<Pixels>>,
expect_bounds_change: Option<Bounds<Pixels>>,
last_layout_bounds: Option<Bounds<Pixels>>,
expect_layout_bounds_change: Option<Bounds<Pixels>>,
}
#[derive(Clone)]
@@ -1492,8 +1492,8 @@ impl Editor {
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
gutter_hovered: false,
pixel_position_of_newest_cursor: None,
last_bounds: None,
expect_bounds_change: None,
last_layout_bounds: None,
expect_layout_bounds_change: None,
gutter_width: Default::default(),
style: None,
show_cursor_names: false,
@@ -1828,6 +1828,29 @@ impl Editor {
old_cursor_position: &Anchor,
cx: &mut ViewContext<Self>,
) {
// Copy selections to primary selection buffer
#[cfg(target_os = "linux")]
if local {
let selections = self.selections.all::<usize>(cx);
let buffer_handle = self.buffer.read(cx).read(cx);
let mut text = String::new();
for (index, selection) in selections.iter().enumerate() {
let text_for_selection = buffer_handle
.text_for_range(selection.start..selection.end)
.collect::<String>();
text.push_str(&text_for_selection);
if index != selections.len() - 1 {
text.push('\n');
}
}
if !text.is_empty() {
cx.write_to_primary(ClipboardItem::new(text));
}
}
if self.focus_handle.is_focused(cx) && self.leader_peer_id.is_none() {
self.buffer.update(cx, |buffer, cx| {
buffer.set_active_selections(
@@ -4920,6 +4943,25 @@ impl Editor {
}
}
pub fn open_active_item_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
if let Some(working_directory) = self.active_excerpt(cx).and_then(|(_, buffer, _)| {
let project_path = buffer.read(cx).project_path(cx)?;
let project = self.project.as_ref()?.read(cx);
let entry = project.entry_for_path(&project_path, cx)?;
let abs_path = project.absolute_path(&project_path, cx)?;
let parent = if entry.is_symlink {
abs_path.canonicalize().ok()?
} else {
abs_path
}
.parent()?
.to_path_buf();
Some(parent)
}) {
cx.dispatch_action(OpenTerminal { working_directory }.boxed_clone());
}
}
fn gather_revert_changes(
&mut self,
selections: &[Selection<Anchor>],
@@ -8052,7 +8094,7 @@ impl Editor {
.buffer
.read(cx)
.text_anchor_for_position(selection.head(), cx)?;
let (tail_buffer, _) = self
let (tail_buffer, cursor_buffer_position_end) = self
.buffer
.read(cx)
.text_anchor_for_position(selection.tail(), cx)?;
@@ -8062,6 +8104,7 @@ impl Editor {
let snapshot = cursor_buffer.read(cx).snapshot();
let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot);
let cursor_buffer_offset_end = cursor_buffer_position_end.to_offset(&snapshot);
let prepare_rename = project.update(cx, |project, cx| {
project.prepare_rename(cursor_buffer.clone(), cursor_buffer_offset, cx)
});
@@ -8090,6 +8133,8 @@ impl Editor {
let rename_buffer_range = rename_range.to_offset(&snapshot);
let cursor_offset_in_rename_range =
cursor_buffer_offset.saturating_sub(rename_buffer_range.start);
let cursor_offset_in_rename_range_end =
cursor_buffer_offset_end.saturating_sub(rename_buffer_range.start);
this.take_rename(false, cx);
let buffer = this.buffer.read(cx).read(cx);
@@ -8118,7 +8163,23 @@ impl Editor {
editor.buffer.update(cx, |buffer, cx| {
buffer.edit([(0..0, old_name.clone())], None, cx)
});
editor.select_all(&SelectAll, cx);
let rename_selection_range = match cursor_offset_in_rename_range
.cmp(&cursor_offset_in_rename_range_end)
{
Ordering::Equal => {
editor.select_all(&SelectAll, cx);
return editor;
}
Ordering::Less => {
cursor_offset_in_rename_range..cursor_offset_in_rename_range_end
}
Ordering::Greater => {
cursor_offset_in_rename_range_end..cursor_offset_in_rename_range
}
};
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges([rename_selection_range]);
});
editor
});
@@ -8946,7 +9007,7 @@ impl Editor {
}
pub fn render_git_blame_inline(&mut self, cx: &mut WindowContext) -> bool {
self.show_git_blame_inline && self.has_blame_entries(cx)
self.focus_handle.is_focused(cx) && self.show_git_blame_inline && self.has_blame_entries(cx)
}
fn has_blame_entries(&self, cx: &mut WindowContext) -> bool {
@@ -10717,7 +10778,8 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
let icon_size = buttons(&diagnostic, cx.block_id)
.into_any_element()
.measure(AvailableSpace::min_size(), cx);
.layout(AvailableSpace::min_size(), cx)
.size;
h_flex()
.id(cx.block_id)

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,10 @@
use std::sync::Arc;
use std::{sync::Arc, time::Duration};
use anyhow::Result;
use collections::HashMap;
use git::{
blame::{Blame, BlameEntry},
permalink::{build_commit_permalink, parse_git_remote_url},
Oid,
};
use gpui::{Model, ModelContext, Subscription, Task};
@@ -63,7 +64,8 @@ pub struct GitBlame {
task: Task<Result<()>>,
generated: bool,
user_triggered: bool,
_refresh_subscription: Subscription,
regenerate_on_edit_task: Task<Result<()>>,
_regenerate_subscriptions: Vec<Subscription>,
}
impl GitBlame {
@@ -81,7 +83,19 @@ impl GitBlame {
&(),
);
let refresh_subscription = cx.subscribe(&project, {
let buffer_subscriptions = cx.subscribe(&buffer, |this, buffer, event, cx| match event {
language::Event::DirtyChanged => {
if !buffer.read(cx).is_dirty() {
this.generate(cx);
}
}
language::Event::Edited => {
this.regenerate_on_edit(cx);
}
_ => {}
});
let project_subscription = cx.subscribe(&project, {
let buffer = buffer.clone();
move |this, _, event, cx| match event {
@@ -116,7 +130,8 @@ impl GitBlame {
commit_details: HashMap::default(),
task: Task::ready(Ok(())),
generated: false,
_refresh_subscription: refresh_subscription,
regenerate_on_edit_task: Task::ready(Ok(())),
_regenerate_subscriptions: vec![buffer_subscriptions, project_subscription],
};
this.generate(cx);
this
@@ -272,11 +287,13 @@ impl GitBlame {
entries,
permalinks,
messages,
remote_url,
} = blame.await?;
let entries = build_blame_entry_sum_tree(entries, snapshot.max_point().row);
let commit_details =
parse_commit_messages(messages, &permalinks, &languages).await;
parse_commit_messages(messages, remote_url, &permalinks, &languages)
.await;
anyhow::Ok((entries, commit_details))
}
@@ -310,8 +327,22 @@ impl GitBlame {
})
});
}
fn regenerate_on_edit(&mut self, cx: &mut ModelContext<Self>) {
self.regenerate_on_edit_task = cx.spawn(|this, mut cx| async move {
cx.background_executor()
.timer(REGENERATE_ON_EDIT_DEBOUNCE_INTERVAL)
.await;
this.update(&mut cx, |this, cx| {
this.generate(cx);
})
});
}
}
const REGENERATE_ON_EDIT_DEBOUNCE_INTERVAL: Duration = Duration::from_secs(2);
fn build_blame_entry_sum_tree(entries: Vec<BlameEntry>, max_row: u32) -> SumTree<GitBlameEntry> {
let mut current_row = 0;
let mut entries = SumTree::from_iter(
@@ -351,13 +382,31 @@ fn build_blame_entry_sum_tree(entries: Vec<BlameEntry>, max_row: u32) -> SumTree
async fn parse_commit_messages(
messages: impl IntoIterator<Item = (Oid, String)>,
permalinks: &HashMap<Oid, Url>,
remote_url: Option<String>,
deprecated_permalinks: &HashMap<Oid, Url>,
languages: &Arc<LanguageRegistry>,
) -> HashMap<Oid, CommitDetails> {
let mut commit_details = HashMap::default();
let parsed_remote_url = remote_url.as_deref().and_then(parse_git_remote_url);
for (oid, message) in messages {
let parsed_message = parse_markdown(&message, &languages).await;
let permalink = permalinks.get(&oid).cloned();
let permalink = if let Some(git_remote) = parsed_remote_url.as_ref() {
Some(build_commit_permalink(
git::permalink::BuildCommitPermalinkParams {
remote: git_remote,
sha: oid.to_string().as_str(),
},
))
} else {
// DEPRECATED (18 Apr 24): Sending permalinks over the wire is deprecated. Clients
// now do the parsing. This is here for backwards compatibility, so that
// when an old peer sends a client no `parsed_remote_url` but `deprecated_permalinks`,
// we fall back to that.
deprecated_permalinks.get(&oid).cloned()
};
commit_details.insert(
oid,

View File

@@ -1173,7 +1173,7 @@ impl SearchableItem for Editor {
}
fn search_bar_visibility_changed(&mut self, _visible: bool, _cx: &mut ViewContext<Self>) {
self.expect_bounds_change = self.last_bounds;
self.expect_layout_bounds_change = self.last_layout_bounds;
}
}

View File

@@ -3,6 +3,7 @@ use crate::{
GoToTypeDefinition, Rename, RevealInFinder, SelectMode, ToggleCodeActions,
};
use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext};
use workspace::OpenInTerminal;
pub struct MouseContextMenu {
pub(crate) position: Point<Pixels>,
@@ -83,6 +84,7 @@ pub fn deploy_context_menu(
)
.separator()
.action("Reveal in Finder", Box::new(RevealInFinder))
.action("Open in Terminal", Box::new(OpenInTerminal))
})
};
let mouse_context_menu = MouseContextMenu::new(position, context_menu, cx);

View File

@@ -72,7 +72,7 @@ impl Editor {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
let original_y = scroll_position.y;
if let Some(last_bounds) = self.expect_bounds_change.take() {
if let Some(last_bounds) = self.expect_layout_bounds_change.take() {
if scroll_position.y != 0. {
scroll_position.y += (bounds.top() - last_bounds.top()) / line_height;
if scroll_position.y < 0. {

View File

@@ -936,24 +936,25 @@ impl Render for ExtensionsPage {
let view = cx.view().clone();
let scroll_handle = self.list.clone();
this.child(
canvas(
move |bounds, cx| {
let mut list = uniform_list::<_, ExtensionCard, _>(
view,
"entries",
count,
Self::render_extensions,
)
.size_full()
.pb_4()
.track_scroll(scroll_handle)
.into_any_element();
list.layout(bounds.origin, bounds.size.into(), cx);
list
},
|_bounds, mut list, cx| list.paint(cx),
)
.size_full(),
// canvas(
// move |bounds, cx| {
// let mut list = uniform_list::<_, ExtensionCard, _>(
// view,
// "entries",
// count,
// Self::render_extensions,
// )
// .size_full()
// .pb_4()
// .track_scroll(scroll_handle)
// .into_any_element();
// list.layout(bounds.origin, bounds.size.into(), cx);
// list
// },
// |_bounds, mut list, cx| list.paint(cx),
// )
// .size_full(),
todo!("replace canvas"),
)
}))
}

View File

@@ -24,6 +24,7 @@ text.workspace = true
time.workspace = true
url.workspace = true
serde.workspace = true
windows.workspace = true
[dev-dependencies]
unindent.workspace = true

View File

@@ -14,6 +14,9 @@ use time::OffsetDateTime;
use time::UtcOffset;
use url::Url;
#[cfg(windows)]
use std::os::windows::process::CommandExt;
pub use git2 as libgit;
#[derive(Debug, Clone, Default)]
@@ -21,6 +24,7 @@ pub struct Blame {
pub entries: Vec<BlameEntry>,
pub messages: HashMap<Oid, String>,
pub permalinks: HashMap<Oid, Url>,
pub remote_url: Option<String>,
}
impl Blame {
@@ -41,6 +45,8 @@ impl Blame {
for entry in entries.iter_mut() {
unique_shas.insert(entry.sha);
// DEPRECATED (18 Apr 24): Sending permalinks over the wire is deprecated. Clients
// now do the parsing.
if let Some(remote) = parsed_remote_url.as_ref() {
permalinks.entry(entry.sha).or_insert_with(|| {
build_commit_permalink(BuildCommitPermalinkParams {
@@ -59,17 +65,23 @@ impl Blame {
entries,
permalinks,
messages,
remote_url,
})
}
}
const GIT_BLAME_NO_COMMIT_ERROR: &'static str = "fatal: no such ref: HEAD";
const GIT_BLAME_NO_PATH: &'static str = "fatal: no such path";
fn run_git_blame(
git_binary: &Path,
working_directory: &Path,
path: &Path,
contents: &Rope,
) -> Result<String> {
let child = Command::new(git_binary)
let mut child = Command::new(git_binary);
child
.current_dir(working_directory)
.arg("blame")
.arg("--incremental")
@@ -78,7 +90,12 @@ fn run_git_blame(
.arg(path.as_os_str())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stderr(Stdio::piped());
#[cfg(windows)]
child.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
let child = child
.spawn()
.map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;
@@ -98,6 +115,10 @@ fn run_git_blame(
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let trimmed = stderr.trim();
if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
return Ok(String::new());
}
return Err(anyhow!("git blame process failed: {}", stderr));
}

View File

@@ -4,15 +4,25 @@ use collections::HashMap;
use std::path::Path;
use std::process::Command;
#[cfg(windows)]
use std::os::windows::process::CommandExt;
pub fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<HashMap<Oid, String>> {
const MARKER: &'static str = "<MARKER>";
let output = Command::new("git")
let mut command = Command::new("git");
command
.current_dir(working_directory)
.arg("show")
.arg("-s")
.arg(format!("--format=%B{}", MARKER))
.args(shas.iter().map(ToString::to_string))
.args(shas.iter().map(ToString::to_string));
#[cfg(windows)]
command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
let output = command
.output()
.map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;

View File

@@ -3,7 +3,7 @@ use std::ops::Range;
use anyhow::{anyhow, Result};
use url::Url;
pub(crate) enum GitHostingProvider {
pub enum GitHostingProvider {
Github,
Gitlab,
Gitee,
@@ -90,18 +90,18 @@ pub fn build_permalink(params: BuildPermalinkParams) -> Result<Url> {
Ok(permalink)
}
pub(crate) struct ParsedGitRemote<'a> {
pub struct ParsedGitRemote<'a> {
pub provider: GitHostingProvider,
pub owner: &'a str,
pub repo: &'a str,
}
pub(crate) struct BuildCommitPermalinkParams<'a> {
pub struct BuildCommitPermalinkParams<'a> {
pub remote: &'a ParsedGitRemote<'a>,
pub sha: &'a str,
}
pub(crate) fn build_commit_permalink(params: BuildCommitPermalinkParams) -> Url {
pub fn build_commit_permalink(params: BuildCommitPermalinkParams) -> Url {
let BuildCommitPermalinkParams { sha, remote } = params;
let ParsedGitRemote {
@@ -122,7 +122,7 @@ pub(crate) fn build_commit_permalink(params: BuildCommitPermalinkParams) -> Url
provider.base_url().join(&path).unwrap()
}
pub(crate) fn parse_git_remote_url(url: &str) -> Option<ParsedGitRemote> {
pub fn parse_git_remote_url(url: &str) -> Option<ParsedGitRemote> {
if url.starts_with("git@github.com:") || url.starts_with("https://github.com/") {
let repo_with_owner = url
.trim_start_matches("git@github.com:")

View File

@@ -115,7 +115,7 @@ wayland-protocols = { version = "0.31.2", features = [
] }
oo7 = "0.3.0"
open = "5.1.2"
x11rb = { version = "0.13.0", features = ["allow-unsafe-code", "xkb", "randr"] }
x11rb = { version = "0.13.0", features = ["allow-unsafe-code", "xkb", "randr", "xinput"] }
xkbcommon = { version = "0.7", features = ["wayland", "x11"] }
[target.'cfg(windows)'.dependencies]

View File

@@ -547,11 +547,23 @@ impl AppContext {
self.platform.window_appearance()
}
/// Writes data to the primary selection buffer.
/// Only available on Linux.
pub fn write_to_primary(&self, item: ClipboardItem) {
self.platform.write_to_primary(item)
}
/// Writes data to the platform clipboard.
pub fn write_to_clipboard(&self, item: ClipboardItem) {
self.platform.write_to_clipboard(item)
}
/// Reads data from the primary selection buffer.
/// Only available on Linux.
pub fn read_from_primary(&self) -> Option<ClipboardItem> {
self.platform.read_from_primary()
}
/// Reads data from the platform clipboard.
pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
self.platform.read_from_clipboard()

View File

@@ -734,7 +734,8 @@ impl VisualTestContext {
self.update(|cx| {
cx.with_element_context(|cx| {
let mut element = f(cx);
element.layout(origin, space, cx);
element.layout(space, cx);
cx.with_element_offset(origin, |cx| element.before_paint(cx));
element.paint(cx);
});

View File

@@ -33,7 +33,7 @@
use crate::{
util::FluentBuilder, ArenaBox, AvailableSpace, Bounds, DispatchNodeId, ElementContext,
ElementId, LayoutId, Pixels, Point, Size, ViewContext, WindowContext, ELEMENT_ARENA,
ElementId, LayoutId, Pixels, Size, ViewContext, WindowContext, ELEMENT_ARENA,
};
use derive_more::{Deref, DerefMut};
pub(crate) use smallvec::SmallVec;
@@ -45,33 +45,46 @@ use std::{any::Any, fmt::Debug, mem, ops::DerefMut};
/// for more details.
pub trait Element: 'static + IntoElement {
/// The type of state returned from [`Element::before_layout`]. A mutable reference to this state is subsequently
/// provided to [`Element::after_layout`] and [`Element::paint`].
/// provided to [`Element::after_layout`], [`Element::before_paint`] and [`Element::paint`].
type BeforeLayout: 'static;
/// The type of state returned from [`Element::after_layout`]. A mutable reference to this state is subsequently
/// provided to [`Element::paint`].
/// provided to [`Element::before_paint`] and [`Element::paint`].
type AfterLayout: 'static;
/// The type of state returned from [`Element::before_paint`]. A mutable reference to this state is subsequently
/// provided to [`Element::paint`].
type BeforePaint: 'static;
/// Before an element can be painted, we need to know where it's going to be and how big it is.
/// Use this method to request a layout from Taffy and initialize the element's state.
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout);
/// After laying out an element, we need to commit its bounds to the current frame for hitbox
/// purposes. The state argument is the same state that was returned from [`Element::before_layout()`].
/// todo!()
fn after_layout(
&mut self,
bounds: Bounds<Pixels>,
before_layout: &mut Self::BeforeLayout,
cx: &mut ElementContext,
) -> Self::AfterLayout;
) -> (Option<Bounds<Pixels>>, Self::AfterLayout);
/// Before painting an element, we need to commit its bounds to the current frame for hitbox
/// purposes.
fn before_paint(
&mut self,
bounds: Bounds<Pixels>,
before_layout: &mut Self::BeforeLayout,
after_layout: &mut Self::AfterLayout,
cx: &mut ElementContext,
) -> Self::BeforePaint;
/// Once layout has been completed, this method will be called to paint the element to the screen.
/// The state argument is the same state that was returned from [`Element::before_layout()`].
fn paint(
&mut self,
bounds: Bounds<Pixels>,
before_layout: &mut Self::BeforeLayout,
after_layout: &mut Self::AfterLayout,
before_paint: &mut Self::BeforePaint,
cx: &mut ElementContext,
);
@@ -163,6 +176,7 @@ impl<C: RenderOnce> Component<C> {
impl<C: RenderOnce> Element for Component<C> {
type BeforeLayout = AnyElement;
type AfterLayout = ();
type BeforePaint = ();
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
let mut element = self
@@ -176,12 +190,23 @@ impl<C: RenderOnce> Element for Component<C> {
}
fn after_layout(
&mut self,
_bounds: Bounds<Pixels>,
element: &mut Self::BeforeLayout,
cx: &mut ElementContext,
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
let bounds = element.after_layout(cx);
(bounds, ())
}
fn before_paint(
&mut self,
_: Bounds<Pixels>,
element: &mut AnyElement,
_: &mut Self::AfterLayout,
cx: &mut ElementContext,
) {
element.after_layout(cx);
element.before_paint(cx);
}
fn paint(
@@ -189,6 +214,7 @@ impl<C: RenderOnce> Element for Component<C> {
_: Bounds<Pixels>,
element: &mut Self::BeforeLayout,
_: &mut Self::AfterLayout,
_: &mut Self::BeforePaint,
cx: &mut ElementContext,
) {
element.paint(cx)
@@ -212,46 +238,59 @@ trait ElementObject {
fn before_layout(&mut self, cx: &mut ElementContext) -> LayoutId;
fn after_layout(&mut self, cx: &mut ElementContext);
fn after_layout(&mut self, cx: &mut ElementContext) -> Option<Bounds<Pixels>>;
fn before_paint(&mut self, cx: &mut ElementContext);
fn paint(&mut self, cx: &mut ElementContext);
fn measure(
fn layout(
&mut self,
available_space: Size<AvailableSpace>,
cx: &mut ElementContext,
) -> Size<Pixels>;
) -> ElementMeasurement;
}
/// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window.
pub struct Drawable<E: Element> {
/// The drawn element.
pub element: E,
phase: ElementDrawPhase<E::BeforeLayout, E::AfterLayout>,
phase: ElementDrawPhase<E::BeforeLayout, E::AfterLayout, E::BeforePaint>,
}
#[derive(Default)]
enum ElementDrawPhase<BeforeLayout, AfterLayout> {
enum ElementDrawPhase<BeforeLayout, AfterLayout, BeforePaint> {
#[default]
Start,
BeforeLayout {
layout_id: LayoutId,
before_layout: BeforeLayout,
},
LayoutComputed {
layout_id: LayoutId,
available_space: Size<AvailableSpace>,
before_layout: BeforeLayout,
},
AfterLayout {
layout_id: LayoutId,
available_space: Option<Size<AvailableSpace>>,
before_layout: BeforeLayout,
after_layout: AfterLayout,
},
BeforePaint {
node_id: DispatchNodeId,
bounds: Bounds<Pixels>,
before_layout: BeforeLayout,
after_layout: AfterLayout,
before_paint: BeforePaint,
},
Painted,
}
/// todo!()
#[derive(Default)]
pub struct ElementMeasurement {
/// The size of the element.
pub size: Size<Pixels>,
/// The bounds for the focus target inside of the measured element.
pub focus_target_bounds: Option<Bounds<Pixels>>,
}
/// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window.
impl<E: Element> Drawable<E> {
fn new(element: E) -> Self {
@@ -275,92 +314,133 @@ impl<E: Element> Drawable<E> {
}
}
fn after_layout(&mut self, cx: &mut ElementContext) {
fn after_layout(&mut self, cx: &mut ElementContext) -> Option<Bounds<Pixels>> {
match mem::take(&mut self.phase) {
ElementDrawPhase::BeforeLayout {
layout_id,
mut before_layout,
}
| ElementDrawPhase::LayoutComputed {
layout_id,
mut before_layout,
..
} => {
let bounds = cx.layout_bounds(layout_id);
let node_id = cx.window.next_frame.dispatch_tree.push_node();
let after_layout = self.element.after_layout(bounds, &mut before_layout, cx);
let (focus_target_bounds, after_layout) =
self.element.after_layout(bounds, &mut before_layout, cx);
self.phase = ElementDrawPhase::AfterLayout {
node_id,
bounds,
layout_id,
available_space: None,
before_layout,
after_layout,
};
cx.window.next_frame.dispatch_tree.pop_node();
focus_target_bounds
}
_ => panic!("must call before_layout before after_layout"),
}
}
fn paint(&mut self, cx: &mut ElementContext) -> E::BeforeLayout {
fn before_paint(&mut self, cx: &mut ElementContext) {
match mem::take(&mut self.phase) {
ElementDrawPhase::AfterLayout {
node_id,
bounds,
layout_id,
mut before_layout,
mut after_layout,
..
} => {
cx.window.next_frame.dispatch_tree.set_active_node(node_id);
self.element
.paint(bounds, &mut before_layout, &mut after_layout, cx);
self.phase = ElementDrawPhase::Painted;
before_layout
let bounds = cx.layout_bounds(layout_id);
let node_id = cx.window.next_frame.dispatch_tree.push_node();
let before_paint =
self.element
.before_paint(bounds, &mut before_layout, &mut after_layout, cx);
self.phase = ElementDrawPhase::BeforePaint {
node_id,
bounds,
before_layout,
after_layout,
before_paint,
};
cx.window.next_frame.dispatch_tree.pop_node();
}
_ => panic!("must call after_layout before paint"),
_ => panic!("must call after_layout before before_paint"),
}
}
fn measure(
fn paint(&mut self, cx: &mut ElementContext) -> E::BeforeLayout {
match mem::take(&mut self.phase) {
ElementDrawPhase::BeforePaint {
node_id,
bounds,
mut before_layout,
mut after_layout,
mut before_paint,
..
} => {
cx.window.next_frame.dispatch_tree.set_active_node(node_id);
self.element.paint(
bounds,
&mut before_layout,
&mut after_layout,
&mut before_paint,
cx,
);
self.phase = ElementDrawPhase::Painted;
before_layout
}
_ => panic!("must call before_paint before paint"),
}
}
fn layout(
&mut self,
available_space: Size<AvailableSpace>,
cx: &mut ElementContext,
) -> Size<Pixels> {
) -> ElementMeasurement {
if matches!(&self.phase, ElementDrawPhase::Start) {
self.before_layout(cx);
}
let layout_id = match mem::take(&mut self.phase) {
match mem::take(&mut self.phase) {
ElementDrawPhase::BeforeLayout {
layout_id,
before_layout,
mut before_layout,
} => {
cx.compute_layout(layout_id, available_space);
self.phase = ElementDrawPhase::LayoutComputed {
let bounds = cx.layout_bounds(layout_id);
let (focus_target_bounds, after_layout) =
self.element.after_layout(bounds, &mut before_layout, cx);
self.phase = ElementDrawPhase::AfterLayout {
layout_id,
available_space,
available_space: Some(available_space),
before_layout,
after_layout,
};
layout_id
ElementMeasurement {
size: bounds.size,
focus_target_bounds,
}
}
ElementDrawPhase::LayoutComputed {
ElementDrawPhase::AfterLayout {
layout_id,
available_space: prev_available_space,
before_layout,
mut before_layout,
..
} => {
if available_space != prev_available_space {
if Some(available_space) != prev_available_space {
cx.compute_layout(layout_id, available_space);
}
self.phase = ElementDrawPhase::LayoutComputed {
let bounds = cx.layout_bounds(layout_id);
let (focus_target_bounds, after_layout) =
self.element.after_layout(bounds, &mut before_layout, cx);
self.phase = ElementDrawPhase::AfterLayout {
layout_id,
available_space,
available_space: Some(available_space),
before_layout,
after_layout,
};
layout_id
ElementMeasurement {
size: bounds.size,
focus_target_bounds,
}
}
_ => panic!("cannot measure after painting"),
};
cx.layout_bounds(layout_id).size
_ => panic!("cannot layout after painting"),
}
}
}
@@ -377,20 +457,24 @@ where
Drawable::before_layout(self, cx)
}
fn after_layout(&mut self, cx: &mut ElementContext) {
Drawable::after_layout(self, cx);
fn after_layout(&mut self, cx: &mut ElementContext) -> Option<Bounds<Pixels>> {
Drawable::after_layout(self, cx)
}
fn before_paint(&mut self, cx: &mut ElementContext) {
Drawable::before_paint(self, cx);
}
fn paint(&mut self, cx: &mut ElementContext) {
Drawable::paint(self, cx);
}
fn measure(
fn layout(
&mut self,
available_space: Size<AvailableSpace>,
cx: &mut ElementContext,
) -> Size<Pixels> {
Drawable::measure(self, available_space, cx)
) -> ElementMeasurement {
Drawable::layout(self, available_space, cx)
}
}
@@ -420,41 +504,35 @@ impl AnyElement {
self.0.before_layout(cx)
}
/// Commits the element bounds of this [AnyElement] for hitbox purposes.
pub fn after_layout(&mut self, cx: &mut ElementContext) {
/// todo!()
pub fn after_layout(&mut self, cx: &mut ElementContext) -> Option<Bounds<Pixels>> {
self.0.after_layout(cx)
}
/// Commits the element bounds of this [AnyElement] for hitbox purposes.
pub fn before_paint(&mut self, cx: &mut ElementContext) {
self.0.before_paint(cx)
}
/// Paints the element stored in this `AnyElement`.
pub fn paint(&mut self, cx: &mut ElementContext) {
self.0.paint(cx)
}
/// Initializes this element and performs layout within the given available space to determine its size.
pub fn measure(
&mut self,
available_space: Size<AvailableSpace>,
cx: &mut ElementContext,
) -> Size<Pixels> {
self.0.measure(available_space, cx)
}
/// Initializes this element, performs layout if needed and commits its bounds for hitbox purposes.
pub fn layout(
&mut self,
absolute_offset: Point<Pixels>,
available_space: Size<AvailableSpace>,
cx: &mut ElementContext,
) -> Size<Pixels> {
let size = self.measure(available_space, cx);
cx.with_absolute_element_offset(absolute_offset, |cx| self.after_layout(cx));
size
) -> ElementMeasurement {
self.0.layout(available_space, cx)
}
}
impl Element for AnyElement {
type BeforeLayout = ();
type AfterLayout = ();
type BeforePaint = ();
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
let layout_id = self.before_layout(cx);
@@ -466,8 +544,19 @@ impl Element for AnyElement {
_: Bounds<Pixels>,
_: &mut Self::BeforeLayout,
cx: &mut ElementContext,
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
let bounds = self.after_layout(cx);
(bounds, ())
}
fn before_paint(
&mut self,
_: Bounds<Pixels>,
_: &mut Self::BeforeLayout,
_: &mut Self::AfterLayout,
cx: &mut ElementContext,
) {
self.after_layout(cx)
self.before_paint(cx)
}
fn paint(
@@ -475,6 +564,7 @@ impl Element for AnyElement {
_: Bounds<Pixels>,
_: &mut Self::BeforeLayout,
_: &mut Self::AfterLayout,
_: &mut Self::BeforePaint,
cx: &mut ElementContext,
) {
self.paint(cx)
@@ -507,6 +597,7 @@ impl IntoElement for Empty {
impl Element for Empty {
type BeforeLayout = ();
type AfterLayout = ();
type BeforePaint = ();
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
(cx.request_layout(&crate::Style::default(), None), ())
@@ -515,7 +606,17 @@ impl Element for Empty {
fn after_layout(
&mut self,
_bounds: Bounds<Pixels>,
_state: &mut Self::BeforeLayout,
_before_layout: &mut Self::BeforeLayout,
_cx: &mut ElementContext,
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
(None, ())
}
fn before_paint(
&mut self,
_bounds: Bounds<Pixels>,
_before_layout: &mut Self::BeforeLayout,
_after_layout: &mut Self::AfterLayout,
_cx: &mut ElementContext,
) {
}
@@ -525,6 +626,7 @@ impl Element for Empty {
_bounds: Bounds<Pixels>,
_before_layout: &mut Self::BeforeLayout,
_after_layout: &mut Self::AfterLayout,
_before_paint: &mut Self::BeforePaint,
_cx: &mut ElementContext,
) {
}

View File

@@ -71,6 +71,7 @@ impl ParentElement for Anchored {
impl Element for Anchored {
type BeforeLayout = AnchoredState;
type AfterLayout = ();
type BeforePaint = ();
fn before_layout(&mut self, cx: &mut ElementContext) -> (crate::LayoutId, Self::BeforeLayout) {
let child_layout_ids = self
@@ -91,9 +92,27 @@ impl Element for Anchored {
}
fn after_layout(
&mut self,
_bounds: Bounds<Pixels>,
_before_layout: &mut Self::BeforeLayout,
cx: &mut ElementContext,
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
let mut focus_target_bounds = None;
for child in &mut self.children {
if let Some(child_focus_target_bounds) = child.after_layout(cx) {
if focus_target_bounds.is_none() {
focus_target_bounds = Some(child_focus_target_bounds);
}
}
}
(focus_target_bounds, ())
}
fn before_paint(
&mut self,
bounds: Bounds<Pixels>,
before_layout: &mut Self::BeforeLayout,
_after_layout: &mut Self::AfterLayout,
cx: &mut ElementContext,
) {
if before_layout.child_layout_ids.is_empty() {
@@ -167,7 +186,7 @@ impl Element for Anchored {
cx.with_element_offset(offset, |cx| {
for child in &mut self.children {
child.after_layout(cx);
child.before_paint(cx);
}
})
}
@@ -177,6 +196,7 @@ impl Element for Anchored {
_bounds: crate::Bounds<crate::Pixels>,
_before_layout: &mut Self::BeforeLayout,
_after_layout: &mut Self::AfterLayout,
_before_paint: &mut Self::BeforePaint,
cx: &mut ElementContext,
) {
for child in &mut self.children {

View File

@@ -1,6 +1,6 @@
use std::time::{Duration, Instant};
use crate::{AnyElement, Element, ElementId, IntoElement};
use crate::{AnyElement, Bounds, Element, ElementId, IntoElement, Pixels};
pub use easing::*;
@@ -86,8 +86,8 @@ struct AnimationState {
impl<E: IntoElement + 'static> Element for AnimationElement<E> {
type BeforeLayout = AnyElement;
type AfterLayout = ();
type BeforePaint = ();
fn before_layout(
&mut self,
@@ -136,18 +136,29 @@ impl<E: IntoElement + 'static> Element for AnimationElement<E> {
fn after_layout(
&mut self,
_bounds: crate::Bounds<crate::Pixels>,
_bounds: Bounds<Pixels>,
element: &mut Self::BeforeLayout,
cx: &mut crate::ElementContext,
) -> Self::AfterLayout {
element.after_layout(cx);
) -> (Option<Bounds<Pixels>>, ()) {
(element.after_layout(cx), ())
}
fn before_paint(
&mut self,
_bounds: Bounds<Pixels>,
element: &mut Self::BeforeLayout,
_after_layout: &mut Self::AfterLayout,
cx: &mut crate::ElementContext,
) -> Self::BeforePaint {
element.before_paint(cx);
}
fn paint(
&mut self,
_bounds: crate::Bounds<crate::Pixels>,
_bounds: Bounds<Pixels>,
element: &mut Self::BeforeLayout,
_: &mut Self::AfterLayout,
_after_layout: &mut Self::AfterLayout,
_before_paint: &mut Self::BeforePaint,
cx: &mut crate::ElementContext,
) {
element.paint(cx);

View File

@@ -4,12 +4,8 @@ use crate::{Bounds, Element, ElementContext, IntoElement, Pixels, Style, StyleRe
/// Construct a canvas element with the given paint callback.
/// Useful for adding short term custom drawing to a view.
pub fn canvas<T>(
after_layout: impl 'static + FnOnce(Bounds<Pixels>, &mut ElementContext) -> T,
paint: impl 'static + FnOnce(Bounds<Pixels>, T, &mut ElementContext),
) -> Canvas<T> {
pub fn canvas(paint: impl 'static + FnOnce(Bounds<Pixels>, &mut ElementContext)) -> Canvas {
Canvas {
after_layout: Some(Box::new(after_layout)),
paint: Some(Box::new(paint)),
style: StyleRefinement::default(),
}
@@ -17,13 +13,12 @@ pub fn canvas<T>(
/// A canvas element, meant for accessing the low level paint API without defining a whole
/// custom element
pub struct Canvas<T> {
after_layout: Option<Box<dyn FnOnce(Bounds<Pixels>, &mut ElementContext) -> T>>,
paint: Option<Box<dyn FnOnce(Bounds<Pixels>, T, &mut ElementContext)>>,
pub struct Canvas {
paint: Option<Box<dyn FnOnce(Bounds<Pixels>, &mut ElementContext)>>,
style: StyleRefinement,
}
impl<T: 'static> IntoElement for Canvas<T> {
impl IntoElement for Canvas {
type Element = Self;
fn into_element(self) -> Self::Element {
@@ -31,9 +26,10 @@ impl<T: 'static> IntoElement for Canvas<T> {
}
}
impl<T: 'static> Element for Canvas<T> {
impl Element for Canvas {
type BeforeLayout = Style;
type AfterLayout = Option<T>;
type AfterLayout = ();
type BeforePaint = ();
fn before_layout(&mut self, cx: &mut ElementContext) -> (crate::LayoutId, Self::BeforeLayout) {
let mut style = Style::default();
@@ -44,28 +40,35 @@ impl<T: 'static> Element for Canvas<T> {
fn after_layout(
&mut self,
bounds: Bounds<Pixels>,
_bounds: Bounds<Pixels>,
_before_layout: &mut Self::BeforeLayout,
_cx: &mut ElementContext,
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
(None, ())
}
fn before_paint(
&mut self,
_bounds: Bounds<Pixels>,
_before_layout: &mut Style,
cx: &mut ElementContext,
) -> Option<T> {
Some(self.after_layout.take().unwrap()(bounds, cx))
_after_layout: &mut Self::AfterLayout,
_cx: &mut ElementContext,
) {
}
fn paint(
&mut self,
bounds: Bounds<Pixels>,
style: &mut Style,
after_layout: &mut Self::AfterLayout,
_after_layout: &mut Self::AfterLayout,
_before_paint: &mut Self::BeforePaint,
cx: &mut ElementContext,
) {
let after_layout = after_layout.take().unwrap();
style.paint(bounds, cx, |cx| {
(self.paint.take().unwrap())(bounds, after_layout, cx)
});
style.paint(bounds, cx, |cx| (self.paint.take().unwrap())(bounds, cx));
}
}
impl<T> Styled for Canvas<T> {
impl Styled for Canvas {
fn style(&mut self) -> &mut crate::StyleRefinement {
&mut self.style
}

View File

@@ -28,6 +28,7 @@ impl Deferred {
impl Element for Deferred {
type BeforeLayout = ();
type AfterLayout = ();
type BeforePaint = ();
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, ()) {
let layout_id = self.child.as_mut().unwrap().before_layout(cx);
@@ -39,6 +40,17 @@ impl Element for Deferred {
_bounds: Bounds<Pixels>,
_before_layout: &mut Self::BeforeLayout,
cx: &mut ElementContext,
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
let bounds = self.child.as_mut().unwrap().after_layout(cx);
(bounds, ())
}
fn before_paint(
&mut self,
_bounds: Bounds<Pixels>,
_before_layout: &mut Self::BeforeLayout,
_after_layout: &mut Self::AfterLayout,
cx: &mut ElementContext,
) {
let child = self.child.take().unwrap();
let element_offset = cx.element_offset();
@@ -50,6 +62,7 @@ impl Element for Deferred {
_bounds: Bounds<Pixels>,
_before_layout: &mut Self::BeforeLayout,
_after_layout: &mut Self::AfterLayout,
_before_paint: &mut Self::BeforePaint,
_cx: &mut ElementContext,
) {
}

View File

@@ -1121,7 +1121,8 @@ impl ParentElement for Div {
impl Element for Div {
type BeforeLayout = DivFrameState;
type AfterLayout = Option<Hitbox>;
type AfterLayout = ();
type BeforePaint = Option<Hitbox>;
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
let mut child_layout_ids = SmallVec::new();
@@ -1143,7 +1144,7 @@ impl Element for Div {
bounds: Bounds<Pixels>,
before_layout: &mut Self::BeforeLayout,
cx: &mut ElementContext,
) -> Option<Hitbox> {
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
let mut child_min = point(Pixels::MAX, Pixels::MAX);
let mut child_max = Point::default();
let content_size = if before_layout.child_layout_ids.is_empty() {
@@ -1177,25 +1178,49 @@ impl Element for Div {
(child_max - child_min).into()
};
self.interactivity.after_layout(
let focus_target_bounds = self.interactivity.after_layout(
bounds,
content_size,
cx,
|_style, scroll_offset, hitbox, cx| {
|_, scroll_offset, mut focus_target_bounds, cx| {
cx.with_element_offset(scroll_offset, |cx| {
for child in &mut self.children {
child.after_layout(cx);
if let Some(child_focus_bounds) = child.after_layout(cx) {
focus_target_bounds = Some(child_focus_bounds);
}
}
focus_target_bounds
})
},
);
(focus_target_bounds, ())
}
fn before_paint(
&mut self,
bounds: Bounds<Pixels>,
_before_layout: &mut Self::BeforeLayout,
_after_layout: &mut Self::AfterLayout,
cx: &mut ElementContext,
) -> Option<Hitbox> {
self.interactivity
.before_paint(bounds, cx, |_style, scroll_offset, hitbox, cx| {
cx.with_element_offset(scroll_offset, |cx| {
for child in &mut self.children {
child.before_paint(cx);
}
});
hitbox
},
)
})
}
fn paint(
&mut self,
bounds: Bounds<Pixels>,
_before_layout: &mut Self::BeforeLayout,
_after_layout: &mut Self::AfterLayout,
hitbox: &mut Option<Hitbox>,
cx: &mut ElementContext,
) {
@@ -1336,13 +1361,13 @@ impl Interactivity {
)
}
/// Commit the bounds of this element according to this interactivity state's configured styles.
/// todo!()
pub fn after_layout<R>(
&mut self,
bounds: Bounds<Pixels>,
content_size: Size<Pixels>,
cx: &mut ElementContext,
f: impl FnOnce(&Style, Point<Pixels>, Option<Hitbox>, &mut ElementContext) -> R,
f: impl FnOnce(&Style, Point<Pixels>, Option<Bounds<Pixels>>, &mut ElementContext) -> R,
) -> R {
self.content_size = content_size;
cx.with_element_state::<InteractiveElementState, _>(
@@ -1367,6 +1392,40 @@ impl Interactivity {
}
}
cx.with_text_style(style.text_style().cloned(), |cx| {
cx.with_content_mask(style.overflow_mask(bounds, cx.rem_size()), |cx| {
let scroll_offset = self.clamp_scroll_position(bounds, &style, cx);
let focus_target_bounds =
self.tracked_focus_handle.as_ref().and_then(|focus_handle| {
if focus_handle.is_focused(cx) {
Some(bounds)
} else {
None
}
});
let result = f(&style, scroll_offset, focus_target_bounds, cx);
(result, element_state)
})
})
},
)
}
/// Commit the bounds of this element according to this interactivity state's configured styles.
pub fn before_paint<R>(
&mut self,
bounds: Bounds<Pixels>,
cx: &mut ElementContext,
f: impl FnOnce(&Style, Point<Pixels>, Option<Hitbox>, &mut ElementContext) -> R,
) -> R {
cx.with_element_state::<InteractiveElementState, _>(
self.element_id.clone(),
|element_state, cx| {
let mut element_state =
element_state.map(|element_state| element_state.unwrap_or_default());
let style = self.compute_style_internal(None, element_state.as_mut(), cx);
cx.with_text_style(style.text_style().cloned(), |cx| {
cx.with_content_mask(style.overflow_mask(bounds, cx.rem_size()), |cx| {
let hitbox = if self.should_insert_hitbox(&style) {
@@ -1375,7 +1434,11 @@ impl Interactivity {
None
};
let scroll_offset = self.clamp_scroll_position(bounds, &style, cx);
let scroll_offset = self
.scroll_offset
.as_ref()
.map(|scroll_offset| *scroll_offset.borrow())
.unwrap_or_default();
let result = f(&style, scroll_offset, hitbox, cx);
(result, element_state)
})
@@ -2263,6 +2326,7 @@ where
{
type BeforeLayout = E::BeforeLayout;
type AfterLayout = E::AfterLayout;
type BeforePaint = E::BeforePaint;
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
self.element.before_layout(cx)
@@ -2271,10 +2335,21 @@ where
fn after_layout(
&mut self,
bounds: Bounds<Pixels>,
state: &mut Self::BeforeLayout,
before_layout: &mut Self::BeforeLayout,
cx: &mut ElementContext,
) -> E::AfterLayout {
self.element.after_layout(bounds, state, cx)
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
self.element.after_layout(bounds, before_layout, cx)
}
fn before_paint(
&mut self,
bounds: Bounds<Pixels>,
before_layout: &mut Self::BeforeLayout,
after_layout: &mut Self::AfterLayout,
cx: &mut ElementContext,
) -> E::BeforePaint {
self.element
.before_paint(bounds, before_layout, after_layout, cx)
}
fn paint(
@@ -2282,9 +2357,11 @@ where
bounds: Bounds<Pixels>,
before_layout: &mut Self::BeforeLayout,
after_layout: &mut Self::AfterLayout,
before_paint: &mut Self::BeforePaint,
cx: &mut ElementContext,
) {
self.element.paint(bounds, before_layout, after_layout, cx)
self.element
.paint(bounds, before_layout, after_layout, before_paint, cx)
}
}
@@ -2346,6 +2423,7 @@ where
{
type BeforeLayout = E::BeforeLayout;
type AfterLayout = E::AfterLayout;
type BeforePaint = E::BeforePaint;
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
self.element.before_layout(cx)
@@ -2354,10 +2432,21 @@ where
fn after_layout(
&mut self,
bounds: Bounds<Pixels>,
state: &mut Self::BeforeLayout,
before_layout: &mut Self::BeforeLayout,
cx: &mut ElementContext,
) -> E::AfterLayout {
self.element.after_layout(bounds, state, cx)
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
self.element.after_layout(bounds, before_layout, cx)
}
fn before_paint(
&mut self,
bounds: Bounds<Pixels>,
before_layout: &mut Self::BeforeLayout,
after_layout: &mut Self::AfterLayout,
cx: &mut ElementContext,
) -> E::BeforePaint {
self.element
.before_paint(bounds, before_layout, after_layout, cx)
}
fn paint(
@@ -2365,9 +2454,11 @@ where
bounds: Bounds<Pixels>,
before_layout: &mut Self::BeforeLayout,
after_layout: &mut Self::AfterLayout,
before_paint: &mut Self::BeforePaint,
cx: &mut ElementContext,
) {
self.element.paint(bounds, before_layout, after_layout, cx);
self.element
.paint(bounds, before_layout, after_layout, before_paint, cx);
}
}

View File

@@ -230,7 +230,8 @@ impl Img {
impl Element for Img {
type BeforeLayout = ();
type AfterLayout = Option<Hitbox>;
type AfterLayout = ();
type BeforePaint = Option<Hitbox>;
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
let layout_id = self.interactivity.before_layout(cx, |mut style, cx| {
@@ -261,16 +262,29 @@ impl Element for Img {
bounds: Bounds<Pixels>,
_before_layout: &mut Self::BeforeLayout,
cx: &mut ElementContext,
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
self.interactivity
.after_layout(bounds, bounds.size, cx, |_, _, _, _| {});
(None, ())
}
fn before_paint(
&mut self,
bounds: Bounds<Pixels>,
_before_layout: &mut Self::BeforeLayout,
_after_layout: &mut Self::AfterLayout,
cx: &mut ElementContext,
) -> Option<Hitbox> {
self.interactivity
.after_layout(bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
.before_paint(bounds, cx, |_, _, hitbox, _| hitbox)
}
fn paint(
&mut self,
bounds: Bounds<Pixels>,
_: &mut Self::BeforeLayout,
hitbox: &mut Self::AfterLayout,
_before_layout: &mut Self::BeforeLayout,
_after_layout: &mut Self::AfterLayout,
hitbox: &mut Self::BeforePaint,
cx: &mut ElementContext,
) {
let source = self.source.clone();

View File

@@ -89,17 +89,28 @@ pub enum ListSizingBehavior {
Auto,
}
struct LayoutItemsResponse {
/// Layout information computed by the [List] element during `after_layout`.
pub struct ListLayout {
max_item_width: Pixels,
scroll_top: ListOffset,
available_item_space: Size<AvailableSpace>,
item_elements: VecDeque<AnyElement>,
items: VecDeque<MeasuredItem>,
focus_target: Option<FocusTarget>,
}
/// Frame state used by the [List] element after layout.
pub struct ListAfterLayoutState {
struct MeasuredItem {
element: AnyElement,
size: Size<Pixels>,
}
struct FocusTarget {
item_index: usize,
bounds_in_item: Bounds<Pixels>,
}
/// Frame state used by the [List] element during paint.
pub struct ListBeforePaintState {
hitbox: Hitbox,
layout: LayoutItemsResponse,
layout: ListLayout,
}
#[derive(Clone)]
@@ -376,13 +387,14 @@ impl StateInner {
available_height: Pixels,
padding: &Edges<Pixels>,
cx: &mut ElementContext,
) -> LayoutItemsResponse {
) -> ListLayout {
let old_items = self.items.clone();
let mut measured_items = VecDeque::new();
let mut item_elements = VecDeque::new();
let mut items = VecDeque::new();
let mut rendered_height = padding.top;
let mut max_item_width = px(0.);
let mut scroll_top = self.logical_scroll_top();
let mut focus_target = None;
let available_item_space = size(
available_width.map_or(AvailableSpace::MinContent, |width| {
@@ -411,10 +423,20 @@ impl StateInner {
// If we're within the visible area or the height wasn't cached, render and measure the item's element
if visible_height < available_height || size.is_none() {
let mut element = (self.render_item)(scroll_top.item_ix + ix, cx);
let element_size = element.measure(available_item_space, cx);
size = Some(element_size);
let element_measurement = element.layout(available_item_space, cx);
size = Some(element_measurement.size);
if visible_height < available_height {
item_elements.push_back(element);
if let Some(focus_target_bounds) = element_measurement.focus_target_bounds {
focus_target = Some(FocusTarget {
item_index: items.len(),
bounds_in_item: focus_target_bounds,
});
}
items.push_back(MeasuredItem {
element,
size: element_measurement.size,
});
}
}
@@ -435,11 +457,26 @@ impl StateInner {
cursor.prev(&());
if cursor.item().is_some() {
let mut element = (self.render_item)(cursor.start().0, cx);
let element_size = element.measure(available_item_space, cx);
let element_measurement = element.layout(available_item_space, cx);
rendered_height += element_size.height;
measured_items.push_front(ListItem::Rendered { size: element_size });
item_elements.push_front(element)
rendered_height += element_measurement.size.height;
measured_items.push_front(ListItem::Rendered {
size: element_measurement.size,
});
items.push_front(MeasuredItem {
element,
size: element_measurement.size,
});
if let Some(focus_target_bounds) = element_measurement.focus_target_bounds {
focus_target = Some(FocusTarget {
item_index: 0,
bounds_in_item: focus_target_bounds,
});
} else if let Some(focus_target) = focus_target.as_mut() {
focus_target.item_index += 1;
}
} else {
break;
}
@@ -474,7 +511,7 @@ impl StateInner {
*size
} else {
let mut element = (self.render_item)(cursor.start().0, cx);
element.measure(available_item_space, cx)
element.layout(available_item_space, cx).size
};
leading_overdraw += size.height;
@@ -493,11 +530,11 @@ impl StateInner {
self.items = new_items;
LayoutItemsResponse {
ListLayout {
max_item_width,
scroll_top,
available_item_space,
item_elements,
items,
focus_target,
}
}
}
@@ -523,7 +560,8 @@ pub struct ListOffset {
impl Element for List {
type BeforeLayout = ();
type AfterLayout = ListAfterLayoutState;
type AfterLayout = Option<ListLayout>;
type BeforePaint = ListBeforePaintState;
fn before_layout(
&mut self,
@@ -592,17 +630,15 @@ impl Element for List {
fn after_layout(
&mut self,
bounds: Bounds<Pixels>,
_: &mut Self::BeforeLayout,
_before_layout: &mut Self::BeforeLayout,
cx: &mut ElementContext,
) -> ListAfterLayoutState {
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
let state = &mut *self.state.0.borrow_mut();
state.reset = false;
let mut style = Style::default();
style.refine(&self.style);
let hitbox = cx.insert_hitbox(bounds, false);
// If the width of the list has changed, invalidate all cached item heights
if state.last_layout_bounds.map_or(true, |last_bounds| {
last_bounds.size.width != bounds.size.width
@@ -614,48 +650,77 @@ impl Element for List {
}
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
let mut layout_response =
state.layout_items(Some(bounds.size.width), bounds.size.height, &padding, cx);
let layout = state.layout_items(Some(bounds.size.width), bounds.size.height, &padding, cx);
let focus_target_bounds = layout.focus_target.as_ref().map(|focus_target| {
let mut item_origin =
bounds.origin + Point::new(px(0.), padding.top - layout.scroll_top.offset_in_item);
item_origin.y -= layout.scroll_top.offset_in_item;
for item in layout.items.iter().take(focus_target.item_index) {
item_origin.y += item.size.height;
}
Bounds::new(
item_origin + focus_target.bounds_in_item.origin,
focus_target.bounds_in_item.size,
)
});
(focus_target_bounds, Some(layout))
}
fn before_paint(
&mut self,
bounds: Bounds<Pixels>,
_: &mut Self::BeforeLayout,
layout: &mut Self::AfterLayout,
cx: &mut ElementContext,
) -> ListBeforePaintState {
let mut layout = layout.take().unwrap();
let state = &mut *self.state.0.borrow_mut();
state.reset = false;
let mut style = Style::default();
style.refine(&self.style);
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
let hitbox = cx.insert_hitbox(bounds, false);
// Only paint the visible items, if there is actually any space for them (taking padding into account)
if bounds.size.height > padding.top + padding.bottom {
// Paint the visible items
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
let mut item_origin = bounds.origin + Point::new(px(0.), padding.top);
item_origin.y -= layout_response.scroll_top.offset_in_item;
for mut item_element in &mut layout_response.item_elements {
let item_size = item_element.measure(layout_response.available_item_space, cx);
item_element.layout(item_origin, layout_response.available_item_space, cx);
item_origin.y += item_size.height;
item_origin.y -= layout.scroll_top.offset_in_item;
for item in &mut layout.items {
cx.with_absolute_element_offset(item_origin, |cx| {
item.element.before_paint(cx)
});
item_origin.y += item.size.height;
}
});
}
state.last_layout_bounds = Some(bounds);
state.last_padding = Some(padding);
ListAfterLayoutState {
hitbox,
layout: layout_response,
}
ListBeforePaintState { hitbox, layout }
}
fn paint(
&mut self,
bounds: Bounds<crate::Pixels>,
_: &mut Self::BeforeLayout,
after_layout: &mut Self::AfterLayout,
_: &mut Self::AfterLayout,
before_paint: &mut Self::BeforePaint,
cx: &mut crate::ElementContext,
) {
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
for item in &mut after_layout.layout.item_elements {
item.paint(cx);
for item in &mut before_paint.layout.items {
item.element.paint(cx);
}
});
let list_state = self.state.clone();
let height = bounds.size.height;
let scroll_top = after_layout.layout.scroll_top;
let hitbox_id = after_layout.hitbox.id;
let scroll_top = before_paint.layout.scroll_top;
let hitbox_id = before_paint.hitbox.id;
cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
if phase == DispatchPhase::Bubble && hitbox_id.is_hovered(cx) {
list_state.0.borrow_mut().scroll(

View File

@@ -38,7 +38,8 @@ impl Svg {
impl Element for Svg {
type BeforeLayout = ();
type AfterLayout = Option<Hitbox>;
type AfterLayout = ();
type BeforePaint = Option<Hitbox>;
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
let layout_id = self
@@ -52,15 +53,28 @@ impl Element for Svg {
bounds: Bounds<Pixels>,
_before_layout: &mut Self::BeforeLayout,
cx: &mut ElementContext,
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
self.interactivity
.after_layout(bounds, bounds.size, cx, |_, _, _, _| {});
(None, ())
}
fn before_paint(
&mut self,
bounds: Bounds<Pixels>,
_before_layout: &mut Self::BeforeLayout,
_after_layout: &mut Self::AfterLayout,
cx: &mut ElementContext,
) -> Option<Hitbox> {
self.interactivity
.after_layout(bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
.before_paint(bounds, cx, |_, _, hitbox, _| hitbox)
}
fn paint(
&mut self,
bounds: Bounds<Pixels>,
_before_layout: &mut Self::BeforeLayout,
_after_layout: &mut Self::AfterLayout,
hitbox: &mut Option<Hitbox>,
cx: &mut ElementContext,
) where

View File

@@ -19,6 +19,7 @@ use util::ResultExt;
impl Element for &'static str {
type BeforeLayout = TextState;
type AfterLayout = ();
type BeforePaint = ();
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
let mut state = TextState::default();
@@ -27,9 +28,19 @@ impl Element for &'static str {
}
fn after_layout(
&mut self,
_bounds: Bounds<Pixels>,
_before_layout: &mut Self::BeforeLayout,
_cx: &mut ElementContext,
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
(None, ())
}
fn before_paint(
&mut self,
_bounds: Bounds<Pixels>,
_text_state: &mut Self::BeforeLayout,
_: &mut Self::AfterLayout,
_cx: &mut ElementContext,
) {
}
@@ -38,6 +49,7 @@ impl Element for &'static str {
&mut self,
bounds: Bounds<Pixels>,
text_state: &mut TextState,
_: &mut Self::AfterLayout,
_: &mut (),
cx: &mut ElementContext,
) {
@@ -64,6 +76,7 @@ impl IntoElement for String {
impl Element for SharedString {
type BeforeLayout = TextState;
type AfterLayout = ();
type BeforePaint = ();
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
let mut state = TextState::default();
@@ -72,9 +85,19 @@ impl Element for SharedString {
}
fn after_layout(
&mut self,
_bounds: Bounds<Pixels>,
_before_layout: &mut Self::BeforeLayout,
_cx: &mut ElementContext,
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
(None, ())
}
fn before_paint(
&mut self,
_bounds: Bounds<Pixels>,
_text_state: &mut Self::BeforeLayout,
_after_layout: &mut Self::AfterLayout,
_cx: &mut ElementContext,
) {
}
@@ -83,7 +106,8 @@ impl Element for SharedString {
&mut self,
bounds: Bounds<Pixels>,
text_state: &mut Self::BeforeLayout,
_: &mut Self::AfterLayout,
_after_layout: &mut Self::AfterLayout,
_: &mut Self::BeforePaint,
cx: &mut ElementContext,
) {
let text_str: &str = self.as_ref();
@@ -150,6 +174,7 @@ impl StyledText {
impl Element for StyledText {
type BeforeLayout = TextState;
type AfterLayout = ();
type BeforePaint = ();
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
let mut state = TextState::default();
@@ -158,9 +183,19 @@ impl Element for StyledText {
}
fn after_layout(
&mut self,
_bounds: Bounds<Pixels>,
_before_layout: &mut Self::BeforeLayout,
_cx: &mut ElementContext,
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
(None, ())
}
fn before_paint(
&mut self,
_bounds: Bounds<Pixels>,
_state: &mut Self::BeforeLayout,
_: &mut Self::AfterLayout,
_cx: &mut ElementContext,
) {
}
@@ -170,6 +205,7 @@ impl Element for StyledText {
bounds: Bounds<Pixels>,
text_state: &mut Self::BeforeLayout,
_: &mut Self::AfterLayout,
_: &mut (),
cx: &mut ElementContext,
) {
text_state.paint(bounds, &self.text, cx)
@@ -403,16 +439,27 @@ impl InteractiveText {
impl Element for InteractiveText {
type BeforeLayout = TextState;
type AfterLayout = Hitbox;
type AfterLayout = ();
type BeforePaint = Hitbox;
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
self.text.before_layout(cx)
}
fn after_layout(
&mut self,
_bounds: Bounds<Pixels>,
_before_layout: &mut Self::BeforeLayout,
_cx: &mut ElementContext,
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
(None, ())
}
fn before_paint(
&mut self,
bounds: Bounds<Pixels>,
state: &mut Self::BeforeLayout,
_after_layout: &mut Self::AfterLayout,
cx: &mut ElementContext,
) -> Hitbox {
cx.with_element_state::<InteractiveTextState, _>(
@@ -430,7 +477,7 @@ impl Element for InteractiveText {
}
}
self.text.after_layout(bounds, state, cx);
self.text.before_paint(bounds, state, &mut (), cx);
let hitbox = cx.insert_hitbox(bounds, false);
(hitbox, interactive_state)
},
@@ -441,6 +488,7 @@ impl Element for InteractiveText {
&mut self,
bounds: Bounds<Pixels>,
text_state: &mut Self::BeforeLayout,
_after_layout: &mut Self::AfterLayout,
hitbox: &mut Hitbox,
cx: &mut ElementContext,
) {
@@ -577,7 +625,7 @@ impl Element for InteractiveText {
});
}
self.text.paint(bounds, text_state, &mut (), cx);
self.text.paint(bounds, text_state, &mut (), &mut (), cx);
((), Some(interactive_state))
},

View File

@@ -6,8 +6,9 @@
use crate::{
point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, Element, ElementContext,
ElementId, Hitbox, InteractiveElement, Interactivity, IntoElement, LayoutId, Pixels, Render,
ScrollHandle, Size, StyleRefinement, Styled, View, ViewContext, WindowContext,
ElementId, ElementMeasurement, Hitbox, InteractiveElement, Interactivity, IntoElement,
LayoutId, Pixels, Render, ScrollHandle, Size, StyleRefinement, Styled, View, ViewContext,
WindowContext,
};
use smallvec::SmallVec;
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
@@ -70,8 +71,9 @@ pub struct UniformList {
/// Frame state used by the [UniformList].
pub struct UniformListFrameState {
item_size: Size<Pixels>,
item_height: Pixels,
items: SmallVec<[AnyElement; 32]>,
visible_range: Range<usize>,
}
/// A handle for controlling the scroll position of a uniform list.
@@ -105,19 +107,22 @@ impl Styled for UniformList {
impl Element for UniformList {
type BeforeLayout = UniformListFrameState;
type AfterLayout = Option<Hitbox>;
type AfterLayout = ();
type BeforePaint = Option<Hitbox>;
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
let max_items = self.item_count;
let item_size = self.measure_item(None, cx);
let item_measurement = self.measure_item(None, cx);
let layout_id = self.interactivity.before_layout(cx, |style, cx| {
cx.request_measured_layout(style, move |known_dimensions, available_space, _cx| {
let desired_height = item_size.height * max_items;
let desired_height = item_measurement.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,
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
item_measurement.size.width
}
});
let height = match available_space.height {
@@ -131,8 +136,9 @@ impl Element for UniformList {
(
layout_id,
UniformListFrameState {
item_size,
item_height: Pixels::default(),
items: SmallVec::new(),
visible_range: 0..0,
},
)
}
@@ -140,28 +146,26 @@ impl Element for UniformList {
fn after_layout(
&mut self,
bounds: Bounds<Pixels>,
before_layout: &mut Self::BeforeLayout,
state: &mut Self::BeforeLayout,
cx: &mut ElementContext,
) -> Option<Hitbox> {
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
let style = self.interactivity.compute_style(None, cx);
let border = style.border_widths.to_pixels(cx.rem_size());
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
let padded_bounds = Bounds::from_corners(
bounds.origin + point(border.left + padding.left, border.top + padding.top),
bounds.lower_right()
- point(border.right + padding.right, border.bottom + padding.bottom),
let padded_size = size(
bounds.size.width - border.left - padding.left - border.right - padding.right,
bounds.size.height - border.top - padding.top - border.bottom - padding.bottom,
);
let item_height = self.measure_item(Some(padded_size.width), cx).size.height;
let content_size = Size {
width: padded_bounds.size.width,
height: before_layout.item_size.height * self.item_count + padding.top + padding.bottom,
width: padded_size.width,
height: item_height * self.item_count + padding.top + padding.bottom,
};
let shared_scroll_offset = self.interactivity.scroll_offset.clone().unwrap();
let item_height = self.measure_item(Some(padded_bounds.size.width), cx).height;
let shared_scroll_to_item = self
// todo!("add support for scrolling to the focused element?");
let deferred_scroll_to_item = self
.scroll_handle
.as_mut()
.and_then(|handle| handle.deferred_scroll_to_item.take());
@@ -170,73 +174,103 @@ impl Element for UniformList {
bounds,
content_size,
cx,
|style, mut scroll_offset, hitbox, cx| {
|style, mut scroll_offset, mut focus_target_bounds, cx| {
if self.item_count == 0 {
return (focus_target_bounds, ());
}
let border = style.border_widths.to_pixels(cx.rem_size());
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
let padded_size = size(
bounds.size.width - border.left - padding.left - border.right - padding.right,
bounds.size.height - border.top - padding.top - border.bottom - padding.bottom,
);
let content_height = item_height * self.item_count + padding.top + padding.bottom;
let min_scroll_offset = padded_size.height - content_height;
let is_scrolled = scroll_offset.y != px(0.);
if is_scrolled && scroll_offset.y < min_scroll_offset {
shared_scroll_offset.borrow_mut().y = min_scroll_offset;
scroll_offset.y = min_scroll_offset;
}
if let Some(ix) = deferred_scroll_to_item {
let list_height = padded_size.height;
let mut updated_scroll_offset = shared_scroll_offset.borrow_mut();
let item_top = item_height * ix + padding.top;
let item_bottom = item_top + item_height;
let scroll_top = -updated_scroll_offset.y;
if item_top < scroll_top + padding.top {
updated_scroll_offset.y = -(item_top) + padding.top;
} else if item_bottom > scroll_top + list_height - padding.bottom {
updated_scroll_offset.y = -(item_bottom - list_height) - padding.bottom;
}
scroll_offset = *updated_scroll_offset;
}
let first_visible_element_ix =
(-(scroll_offset.y + padding.top) / item_height).floor() as usize;
let last_visible_element_ix =
((-scroll_offset.y + padded_size.height) / item_height).ceil() as usize;
let visible_range =
first_visible_element_ix..cmp::min(last_visible_element_ix, self.item_count);
let mut items = (self.render_items)(visible_range.clone(), cx);
let available_space = size(
AvailableSpace::Definite(padded_size.width),
AvailableSpace::Definite(item_height),
);
for mut item in items {
let measurement = item.layout(available_space, cx);
if measurement.focus_target_bounds.is_some() {
focus_target_bounds = measurement.focus_target_bounds;
}
state.items.push(item);
}
state.item_height = item_height;
state.visible_range = visible_range;
(focus_target_bounds, ())
},
)
}
fn before_paint(
&mut self,
bounds: Bounds<Pixels>,
state: &mut Self::BeforeLayout,
_after_layout: &mut Self::AfterLayout,
cx: &mut ElementContext,
) -> Option<Hitbox> {
self.interactivity
.before_paint(bounds, cx, |style, scroll_offset, hitbox, cx| {
let border = style.border_widths.to_pixels(cx.rem_size());
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
let padded_bounds = Bounds::from_corners(
bounds.origin + point(border.left + padding.left, border.top),
bounds.lower_right() - point(border.right + padding.right, border.bottom),
);
if self.item_count > 0 {
let content_height =
item_height * self.item_count + padding.top + padding.bottom;
let min_scroll_offset = padded_bounds.size.height - content_height;
let is_scrolled = scroll_offset.y != px(0.);
if is_scrolled && scroll_offset.y < min_scroll_offset {
shared_scroll_offset.borrow_mut().y = min_scroll_offset;
scroll_offset.y = min_scroll_offset;
let content_mask = ContentMask { bounds };
cx.with_content_mask(Some(content_mask), |cx| {
for (item, ix) in state.items.iter_mut().zip(state.visible_range.clone()) {
let item_y = state.item_height * ix + scroll_offset.y + padding.top;
let item_origin = padded_bounds.origin + point(px(0.), item_y);
cx.with_absolute_element_offset(item_origin, |cx| item.before_paint(cx));
}
if let Some(ix) = shared_scroll_to_item {
let list_height = padded_bounds.size.height;
let mut updated_scroll_offset = shared_scroll_offset.borrow_mut();
let item_top = item_height * ix + padding.top;
let item_bottom = item_top + item_height;
let scroll_top = -updated_scroll_offset.y;
if item_top < scroll_top + padding.top {
updated_scroll_offset.y = -(item_top) + padding.top;
} else if item_bottom > scroll_top + list_height - padding.bottom {
updated_scroll_offset.y = -(item_bottom - list_height) - padding.bottom;
}
scroll_offset = *updated_scroll_offset;
}
let first_visible_element_ix =
(-(scroll_offset.y + padding.top) / item_height).floor() as usize;
let last_visible_element_ix = ((-scroll_offset.y + padded_bounds.size.height)
/ item_height)
.ceil() as usize;
let visible_range = first_visible_element_ix
..cmp::min(last_visible_element_ix, self.item_count);
let mut items = (self.render_items)(visible_range.clone(), cx);
let content_mask = ContentMask { bounds };
cx.with_content_mask(Some(content_mask), |cx| {
for (mut item, ix) in items.into_iter().zip(visible_range) {
let item_origin = padded_bounds.origin
+ point(px(0.), item_height * ix + scroll_offset.y + padding.top);
let available_space = size(
AvailableSpace::Definite(padded_bounds.size.width),
AvailableSpace::Definite(item_height),
);
item.layout(item_origin, available_space, cx);
before_layout.items.push(item);
}
});
}
});
hitbox
},
)
})
}
fn paint(
&mut self,
bounds: Bounds<crate::Pixels>,
before_layout: &mut Self::BeforeLayout,
_after_layout: &mut Self::AfterLayout,
hitbox: &mut Option<Hitbox>,
cx: &mut ElementContext,
) {
@@ -264,9 +298,13 @@ impl UniformList {
self
}
fn measure_item(&self, list_width: Option<Pixels>, cx: &mut ElementContext) -> Size<Pixels> {
fn measure_item(
&self,
list_width: Option<Pixels>,
cx: &mut ElementContext,
) -> ElementMeasurement {
if self.item_count == 0 {
return Size::default();
return ElementMeasurement::default();
}
let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1);
@@ -278,7 +316,7 @@ impl UniformList {
}),
AvailableSpace::MinContent,
);
item_to_measure.measure(available_space, cx)
item_to_measure.layout(available_space, cx)
}
/// Track and render scroll state of this list with reference to the given scroll handle.

View File

@@ -151,7 +151,9 @@ pub(crate) trait Platform: 'static {
fn set_cursor_style(&self, style: CursorStyle);
fn should_auto_hide_scrollbars(&self) -> bool;
fn write_to_primary(&self, item: ClipboardItem);
fn write_to_clipboard(&self, item: ClipboardItem);
fn read_from_primary(&self) -> Option<ClipboardItem>;
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;

View File

@@ -79,8 +79,14 @@ impl LinuxClient for HeadlessClient {
//todo(linux)
fn set_cursor_style(&self, _style: CursorStyle) {}
fn write_to_primary(&self, item: crate::ClipboardItem) {}
fn write_to_clipboard(&self, item: crate::ClipboardItem) {}
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
None
}
fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
None
}

View File

@@ -56,7 +56,9 @@ pub trait LinuxClient {
options: WindowParams,
) -> Box<dyn PlatformWindow>;
fn set_cursor_style(&self, style: CursorStyle);
fn write_to_primary(&self, item: ClipboardItem);
fn write_to_clipboard(&self, item: ClipboardItem);
fn read_from_primary(&self) -> Option<ClipboardItem>;
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
fn run(&self);
}
@@ -406,7 +408,6 @@ impl<P: LinuxClient + 'static> Platform for P {
})
}
//todo(linux): add trait methods for accessing the primary selection
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
let url = url.to_string();
self.background_executor().spawn(async move {
@@ -461,10 +462,18 @@ impl<P: LinuxClient + 'static> Platform for P {
Task::ready(Err(anyhow!("register_url_scheme unimplemented")))
}
fn write_to_primary(&self, item: ClipboardItem) {
self.write_to_primary(item)
}
fn write_to_clipboard(&self, item: ClipboardItem) {
self.write_to_clipboard(item)
}
fn read_from_primary(&self) -> Option<ClipboardItem> {
self.read_from_primary()
}
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
self.read_from_clipboard()
}

View File

@@ -118,8 +118,8 @@ pub(crate) struct WaylandClientState {
loop_handle: LoopHandle<'static, WaylandClientStatePtr>,
cursor_icon_name: String,
cursor: Cursor,
clipboard: Clipboard,
primary: Primary,
clipboard: Option<Clipboard>,
primary: Option<Primary>,
event_loop: Option<EventLoop<'static, WaylandClientStatePtr>>,
common: LinuxCommon,
}
@@ -163,6 +163,12 @@ impl WaylandClientStatePtr {
state.mouse_focused_window = Some(window);
}
}
if state.windows.is_empty() {
// Drop the clipboard to prevent a seg fault after we've closed all Wayland connections.
state.clipboard = None;
state.primary = None;
state.common.signal.stop();
}
}
}
@@ -278,8 +284,8 @@ impl WaylandClient {
cursor_icon_name: "arrow".to_string(),
enter_token: None,
cursor,
clipboard,
primary,
clipboard: Some(clipboard),
primary: Some(primary),
event_loop: Some(event_loop),
}));
@@ -377,14 +383,44 @@ impl LinuxClient for WaylandClient {
.log_err();
}
fn write_to_primary(&self, item: crate::ClipboardItem) {
self.0
.borrow_mut()
.primary
.as_mut()
.unwrap()
.set_contents(item.text);
}
fn write_to_clipboard(&self, item: crate::ClipboardItem) {
self.0.borrow_mut().clipboard.set_contents(item.text);
self.0
.borrow_mut()
.clipboard
.as_mut()
.unwrap()
.set_contents(item.text);
}
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
self.0
.borrow_mut()
.primary
.as_mut()
.unwrap()
.get_contents()
.ok()
.map(|s| crate::ClipboardItem {
text: s,
metadata: None,
})
}
fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
self.0
.borrow_mut()
.clipboard
.as_mut()
.unwrap()
.get_contents()
.ok()
.map(|s| crate::ClipboardItem {

View File

@@ -68,6 +68,7 @@ unsafe impl HasRawDisplayHandle for RawWindow {
pub struct WaylandWindowState {
xdg_surface: xdg_surface::XdgSurface,
pub surface: wl_surface::WlSurface,
decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
toplevel: xdg_toplevel::XdgToplevel,
viewport: Option<wp_viewport::WpViewport>,
outputs: HashSet<ObjectId>,
@@ -90,11 +91,13 @@ pub struct WaylandWindowStatePtr {
}
impl WaylandWindowState {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
surface: wl_surface::WlSurface,
xdg_surface: xdg_surface::XdgSurface,
viewport: Option<wp_viewport::WpViewport>,
toplevel: xdg_toplevel::XdgToplevel,
decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
viewport: Option<wp_viewport::WpViewport>,
client: WaylandClientStatePtr,
globals: Globals,
options: WindowParams,
@@ -132,6 +135,7 @@ impl WaylandWindowState {
Self {
xdg_surface,
surface,
decoration,
toplevel,
viewport,
globals,
@@ -158,16 +162,27 @@ impl Drop for WaylandWindow {
let mut state = self.0.state.borrow_mut();
let surface_id = state.surface.id();
let client = state.client.clone();
state.renderer.destroy();
if let Some(decoration) = &state.decoration {
decoration.destroy();
}
state.toplevel.destroy();
if let Some(viewport) = &state.viewport {
viewport.destroy();
}
state.xdg_surface.destroy();
state.surface.destroy();
let state_ptr = self.0.clone();
state.globals.executor.spawn(async move {
state_ptr.close();
client.drop_window(&surface_id)
});
state
.globals
.executor
.spawn(async move {
state_ptr.close();
client.drop_window(&surface_id)
})
.detach();
drop(state);
}
}
@@ -197,13 +212,18 @@ impl WaylandWindow {
}
// Attempt to set up window decorations based on the requested configuration
if let Some(decoration_manager) = globals.decoration_manager.as_ref() {
let decoration =
decoration_manager.get_toplevel_decoration(&toplevel, &globals.qh, surface.id());
// Request client side decorations if possible
decoration.set_mode(zxdg_toplevel_decoration_v1::Mode::ClientSide);
}
let decoration = globals
.decoration_manager
.as_ref()
.map(|decoration_manager| {
let decoration = decoration_manager.get_toplevel_decoration(
&toplevel,
&globals.qh,
surface.id(),
);
decoration.set_mode(zxdg_toplevel_decoration_v1::Mode::ClientSide);
decoration
});
let viewport = globals
.viewporter
@@ -216,8 +236,9 @@ impl WaylandWindow {
state: Rc::new(RefCell::new(WaylandWindowState::new(
surface.clone(),
xdg_surface,
viewport,
toplevel,
decoration,
viewport,
client,
globals,
params,
@@ -319,7 +340,7 @@ impl WaylandWindowStatePtr {
}
result
} else {
false
true
}
}
_ => false,
@@ -481,13 +502,12 @@ impl WaylandWindowStatePtr {
}
}
if let PlatformInput::KeyDown(event) = input {
let mut state = self.state.borrow_mut();
if let Some(mut input_handler) = state.input_handler.take() {
if let Some(ime_key) = &event.keystroke.ime_key {
if let Some(ime_key) = &event.keystroke.ime_key {
let mut state = self.state.borrow_mut();
if let Some(mut input_handler) = state.input_handler.take() {
drop(state);
input_handler.replace_text_in_range(None, ime_key);
let mut state = self.state.borrow_mut();
state.input_handler = Some(input_handler);
self.state.borrow_mut().input_handler = Some(input_handler);
}
}
}

View File

@@ -12,9 +12,10 @@ use util::ResultExt;
use x11rb::connection::{Connection, RequestConnection};
use x11rb::errors::ConnectionError;
use x11rb::protocol::randr::ConnectionExt as _;
use x11rb::protocol::xinput::ConnectionExt;
use x11rb::protocol::xkb::ConnectionExt as _;
use x11rb::protocol::xproto::ConnectionExt as _;
use x11rb::protocol::{randr, xkb, xproto, Event};
use x11rb::protocol::{randr, xinput, xkb, xproto, Event};
use x11rb::xcb_ffi::XCBConnection;
use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION};
use xkbcommon::xkb as xkbc;
@@ -63,6 +64,10 @@ pub struct X11ClientState {
pub(crate) focused_window: Option<xproto::Window>,
pub(crate) xkb: xkbc::State,
pub(crate) scroll_devices: Vec<xinput::DeviceInfo>,
pub(crate) scroll_x: Option<f32>,
pub(crate) scroll_y: Option<f32>,
pub(crate) common: LinuxCommon,
pub(crate) clipboard: X11ClipboardContext<Clipboard>,
pub(crate) primary: X11ClipboardContext<Primary>,
@@ -92,6 +97,19 @@ impl X11Client {
xcb_connection
.prefetch_extension_information(randr::X11_EXTENSION_NAME)
.unwrap();
xcb_connection
.prefetch_extension_information(xinput::X11_EXTENSION_NAME)
.unwrap();
let xinput_version = xcb_connection
.xinput_xi_query_version(2, 0)
.unwrap()
.reply()
.unwrap();
assert!(
xinput_version.major_version >= 2,
"XInput Extension v2 not supported."
);
let atoms = XcbAtoms::new(&xcb_connection).unwrap();
let xkb = xcb_connection
@@ -125,6 +143,46 @@ impl X11Client {
xkbc::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id)
};
let device_list = xcb_connection
.xinput_list_input_devices()
.unwrap()
.reply()
.unwrap();
let scroll_devices = device_list
.devices
.iter()
.scan(0, |class_info_idx, device_info| {
Some(*class_info_idx + device_info.num_class_info as usize)
})
.zip(device_list.devices.iter())
.map(|(class_info_idx, device_info)| {
(
device_info,
device_list.infos
[(class_info_idx - device_info.num_class_info as usize)..(class_info_idx)]
.to_vec(),
)
})
.filter(|(device_info, class_info)| {
device_info.device_use == xinput::DeviceUse::IS_X_EXTENSION_POINTER
&& class_info.iter().any(|class_info| match class_info.info {
xinput::InputInfoInfo::Valuator(xinput::InputInfoInfoValuator {
mode,
..
}) => mode == xinput::ValuatorMode::RELATIVE,
_ => false,
})
})
.map(|(device_info, _)| *device_info)
.collect::<Vec<_>>();
for device in &scroll_devices {
xcb_connection
.xinput_open_device(device.device_id)
.unwrap()
.reply()
.unwrap();
}
let clipboard = X11ClipboardContext::<Clipboard>::new().unwrap();
let primary = X11ClipboardContext::<Primary>::new().unwrap();
@@ -166,6 +224,11 @@ impl X11Client {
windows: HashMap::default(),
focused_window: None,
xkb: xkb_state,
scroll_devices,
scroll_x: None,
scroll_y: None,
clipboard,
primary,
})))
@@ -314,22 +377,58 @@ impl X11Client {
click_count: current_count,
first_mouse: false,
}));
} else if event.detail >= 4 && event.detail <= 5 {
// https://stackoverflow.com/questions/15510472/scrollwheel-event-in-x11
let scroll_direction = if event.detail == 4 { 1.0 } else { -1.0 };
let scroll_y = SCROLL_LINES * scroll_direction;
drop(state);
window.handle_input(PlatformInput::ScrollWheel(crate::ScrollWheelEvent {
position,
delta: ScrollDelta::Lines(Point::new(0.0, scroll_y as f32)),
modifiers,
touch_phase: TouchPhase::Moved,
}));
} else {
log::warn!("Unknown button press: {event:?}");
}
}
Event::XinputMotion(event) => {
let window = self.get_window(event.event)?;
let position = Point::new(
(event.event_x as f32 / u16::MAX as f32).into(),
(event.event_y as f32 / u16::MAX as f32).into(),
);
let axisvalues = event
.axisvalues
.iter()
.map(|axisvalue| {
axisvalue.integral as f32 + axisvalue.frac as f32 / u32::MAX as f32
})
.collect::<Vec<_>>();
if event.valuator_mask[0] & 4 == 4 {
let new_scroll = axisvalues[0];
let old_scroll = self.0.borrow().scroll_x;
self.0.borrow_mut().scroll_x = Some(new_scroll);
if let Some(old_scroll) = old_scroll {
let delta_scroll = (old_scroll - new_scroll).into();
window.handle_input(PlatformInput::ScrollWheel(crate::ScrollWheelEvent {
position,
delta: ScrollDelta::Pixels(Point::new(delta_scroll, 0.0.into())),
modifiers: crate::Modifiers::none(),
touch_phase: TouchPhase::default(),
}));
}
}
if event.valuator_mask[0] & 8 == 8 {
let new_scroll = axisvalues[0] / 2.0;
let old_scroll = self.0.borrow().scroll_y;
self.0.borrow_mut().scroll_y = Some(new_scroll);
if let Some(old_scroll) = old_scroll {
let delta_scroll = (old_scroll - new_scroll).into();
window.handle_input(PlatformInput::ScrollWheel(crate::ScrollWheelEvent {
position,
delta: ScrollDelta::Pixels(Point::new(0.0.into(), delta_scroll)),
modifiers: crate::Modifiers::none(),
touch_phase: TouchPhase::default(),
}));
}
}
}
Event::ButtonRelease(event) => {
let window = self.get_window(event.event)?;
let state = self.0.borrow();
@@ -429,6 +528,7 @@ impl LinuxClient for X11Client {
state.x_root_index,
x_window,
&state.atoms,
&state.scroll_devices,
);
let screen_resources = state
@@ -503,10 +603,26 @@ impl LinuxClient for X11Client {
//todo(linux)
fn set_cursor_style(&self, _style: CursorStyle) {}
fn write_to_primary(&self, item: crate::ClipboardItem) {
self.0.borrow_mut().primary.set_contents(item.text);
}
fn write_to_clipboard(&self, item: crate::ClipboardItem) {
self.0.borrow_mut().clipboard.set_contents(item.text);
}
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
self.0
.borrow_mut()
.primary
.get_contents()
.ok()
.map(|text| crate::ClipboardItem {
text,
metadata: None,
})
}
fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
self.0
.borrow_mut()

View File

@@ -13,7 +13,10 @@ use raw_window_handle as rwh;
use util::ResultExt;
use x11rb::{
connection::Connection,
protocol::xproto::{self, ConnectionExt as _, CreateWindowAux},
protocol::{
xinput,
xproto::{self, ConnectionExt as _, CreateWindowAux},
},
wrapper::ConnectionExt,
xcb_ffi::XCBConnection,
};
@@ -140,6 +143,7 @@ impl X11WindowState {
x_main_screen_index: usize,
x_window: xproto::Window,
atoms: &XcbAtoms,
scroll_devices: &Vec<xinput::DeviceInfo>,
) -> Self {
let x_screen_index = params
.display_id
@@ -160,8 +164,6 @@ impl X11WindowState {
| xproto::EventMask::BUTTON1_MOTION
| xproto::EventMask::BUTTON2_MOTION
| xproto::EventMask::BUTTON3_MOTION
| xproto::EventMask::BUTTON4_MOTION
| xproto::EventMask::BUTTON5_MOTION
| xproto::EventMask::BUTTON_MOTION,
);
@@ -181,6 +183,20 @@ impl X11WindowState {
)
.unwrap();
for device in scroll_devices {
xinput::ConnectionExt::xinput_xi_select_events(
&xcb_connection,
x_window,
&[xinput::EventMask {
deviceid: device.device_id as u16,
mask: vec![xinput::XIEventMask::MOTION],
}],
)
.unwrap()
.check()
.unwrap();
}
if let Some(titlebar) = params.titlebar {
if let Some(title) = titlebar.title {
xcb_connection
@@ -262,6 +278,7 @@ impl X11Window {
x_main_screen_index: usize,
x_window: xproto::Window,
atoms: &XcbAtoms,
scroll_devices: &Vec<xinput::DeviceInfo>,
) -> Self {
X11Window {
state: Rc::new(RefCell::new(X11WindowState::new(
@@ -270,6 +287,7 @@ impl X11Window {
x_main_screen_index,
x_window,
atoms,
scroll_devices,
))),
callbacks: Rc::new(RefCell::new(Callbacks::default())),
xcb_connection: xcb_connection.clone(),

View File

@@ -849,6 +849,8 @@ impl Platform for MacPlatform {
}
}
fn write_to_primary(&self, _item: ClipboardItem) {}
fn write_to_clipboard(&self, item: ClipboardItem) {
let state = self.0.lock();
unsafe {
@@ -886,6 +888,10 @@ impl Platform for MacPlatform {
}
}
fn read_from_primary(&self) -> Option<ClipboardItem> {
None
}
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
let state = self.0.lock();
unsafe {

View File

@@ -337,7 +337,6 @@ struct MacWindowState {
handle: AnyWindowHandle,
executor: ForegroundExecutor,
native_window: id,
native_window_was_closed: bool,
native_view: NonNull<Object>,
display_link: Option<DisplayLink>,
renderer: renderer::Renderer,
@@ -605,6 +604,10 @@ impl MacWindow {
registerForDraggedTypes:
NSArray::arrayWithObject(nil, NSFilenamesPboardType)
];
let () = msg_send![
native_window,
setReleasedWhenClosed: NO
];
let native_view: id = msg_send![VIEW_CLASS, alloc];
let native_view = NSView::init(native_view);
@@ -622,7 +625,6 @@ impl MacWindow {
handle,
executor,
native_window,
native_window_was_closed: false,
native_view: NonNull::new_unchecked(native_view),
display_link: None,
renderer: renderer::new_renderer(
@@ -770,19 +772,17 @@ impl Drop for MacWindow {
this.renderer.destroy();
let window = this.native_window;
this.display_link.take();
if !this.native_window_was_closed {
unsafe {
this.native_window.setDelegate_(nil);
}
this.executor
.spawn(async move {
unsafe {
window.close();
}
})
.detach();
unsafe {
this.native_window.setDelegate_(nil);
}
this.executor
.spawn(async move {
unsafe {
window.close();
window.autorelease();
}
})
.detach();
}
}
@@ -1592,7 +1592,6 @@ extern "C" fn close_window(this: &Object, _: Sel) {
let close_callback = {
let window_state = get_window_state(this);
let mut lock = window_state.as_ref().lock();
lock.native_window_was_closed = true;
lock.close_callback.take()
};

View File

@@ -23,6 +23,7 @@ pub(crate) struct TestPlatform {
active_display: Rc<dyn PlatformDisplay>,
active_cursor: Mutex<CursorStyle>,
current_clipboard_item: Mutex<Option<ClipboardItem>>,
current_primary_item: Mutex<Option<ClipboardItem>>,
pub(crate) prompts: RefCell<TestPrompts>,
pub opened_url: RefCell<Option<String>>,
weak: Weak<Self>,
@@ -44,6 +45,7 @@ impl TestPlatform {
active_display: Rc::new(TestDisplay::new()),
active_window: Default::default(),
current_clipboard_item: Mutex::new(None),
current_primary_item: Mutex::new(None),
weak: weak.clone(),
opened_url: Default::default(),
})
@@ -125,8 +127,11 @@ impl Platform for TestPlatform {
#[cfg(target_os = "macos")]
return Arc::new(crate::platform::mac::MacTextSystem::new());
#[cfg(not(target_os = "macos"))]
#[cfg(target_os = "linux")]
return Arc::new(crate::platform::cosmic_text::CosmicTextSystem::new());
#[cfg(target_os = "windows")]
return Arc::new(crate::platform::windows::DirectWriteTextSystem::new().unwrap());
}
fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
@@ -279,10 +284,18 @@ impl Platform for TestPlatform {
false
}
fn write_to_primary(&self, item: ClipboardItem) {
*self.current_primary_item.lock() = Some(item);
}
fn write_to_clipboard(&self, item: ClipboardItem) {
*self.current_clipboard_item.lock() = Some(item);
}
fn read_from_primary(&self) -> Option<ClipboardItem> {
self.current_primary_item.lock().clone()
}
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
self.current_clipboard_item.lock().clone()
}

View File

@@ -1,9 +1,11 @@
mod direct_write;
mod dispatcher;
mod display;
mod platform;
mod util;
mod window;
pub(crate) use direct_write::*;
pub(crate) use dispatcher::*;
pub(crate) use display::*;
pub(crate) use platform::*;

File diff suppressed because it is too large Load Diff

View File

@@ -57,7 +57,7 @@ pub(crate) struct WindowsPlatformInner {
background_executor: BackgroundExecutor,
pub(crate) foreground_executor: ForegroundExecutor,
main_receiver: flume::Receiver<Runnable>,
text_system: Arc<CosmicTextSystem>,
text_system: Arc<dyn PlatformTextSystem>,
callbacks: Mutex<Callbacks>,
pub raw_window_handles: RwLock<SmallVec<[HWND; 4]>>,
pub(crate) dispatch_event: OwnedHandle,
@@ -155,7 +155,13 @@ impl WindowsPlatform {
let dispatcher = Arc::new(WindowsDispatcher::new(main_sender, dispatch_event.to_raw()));
let background_executor = BackgroundExecutor::new(dispatcher.clone());
let foreground_executor = ForegroundExecutor::new(dispatcher);
let text_system = Arc::new(CosmicTextSystem::new());
let text_system = if let Some(direct_write) = DirectWriteTextSystem::new().log_err() {
log::info!("Using direct write text system.");
Arc::new(direct_write) as Arc<dyn PlatformTextSystem>
} else {
log::info!("Using cosmic text system.");
Arc::new(CosmicTextSystem::new()) as Arc<dyn PlatformTextSystem>
};
let callbacks = Mutex::new(Callbacks::default());
let raw_window_handles = RwLock::new(SmallVec::new());
let settings = RefCell::new(WindowsPlatformSystemSettings::new());
@@ -686,6 +692,8 @@ impl Platform for WindowsPlatform {
false
}
fn write_to_primary(&self, _item: ClipboardItem) {}
fn write_to_clipboard(&self, item: ClipboardItem) {
if item.text.len() > 0 {
let mut ctx = ClipboardContext::new().unwrap();
@@ -693,6 +701,10 @@ impl Platform for WindowsPlatform {
}
}
fn read_from_primary(&self) -> Option<ClipboardItem> {
None
}
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
let mut ctx = ClipboardContext::new().unwrap();
let content = ctx.get_contents().unwrap();

View File

@@ -284,7 +284,6 @@ impl WindowsWindowInner {
WM_CHAR => self.handle_char_msg(msg, wparam, lparam),
WM_IME_STARTCOMPOSITION => self.handle_ime_position(),
WM_IME_COMPOSITION => self.handle_ime_composition(lparam),
WM_IME_CHAR => self.handle_ime_char(wparam),
WM_SETCURSOR => self.handle_set_cursor(lparam),
_ => None,
};
@@ -782,7 +781,6 @@ impl WindowsWindowInner {
let string_len = ImmGetCompositionStringW(ctx, GCS_COMPSTR, None, 0);
let result = if string_len >= 0 {
let mut buffer = vec![0u8; string_len as usize + 2];
// let mut buffer = [0u8; MAX_PATH as _];
ImmGetCompositionStringW(
ctx,
GCS_COMPSTR,
@@ -812,6 +810,32 @@ impl WindowsWindowInner {
}
}
fn parse_ime_compostion_result(&self) -> Option<String> {
unsafe {
let ctx = ImmGetContext(self.hwnd);
let string_len = ImmGetCompositionStringW(ctx, GCS_RESULTSTR, None, 0);
let result = if string_len >= 0 {
let mut buffer = vec![0u8; string_len as usize + 2];
ImmGetCompositionStringW(
ctx,
GCS_RESULTSTR,
Some(buffer.as_mut_ptr() as _),
string_len as _,
);
let wstring = std::slice::from_raw_parts::<u16>(
buffer.as_mut_ptr().cast::<u16>(),
string_len as usize / 2,
);
let string = String::from_utf16_lossy(wstring);
Some(string)
} else {
None
};
ImmReleaseContext(self.hwnd, ctx);
result
}
}
fn handle_ime_composition(&self, lparam: LPARAM) -> Option<isize> {
let mut ime_input = None;
if lparam.0 as u32 & GCS_COMPSTR.0 > 0 {
@@ -840,31 +864,22 @@ impl WindowsWindowInner {
input_handler.replace_and_mark_text_in_range(None, comp_string, Some(0..caret_pos));
self.input_handler.set(Some(input_handler));
}
if lparam.0 as u32 & GCS_RESULTSTR.0 > 0 {
let Some(comp_result) = self.parse_ime_compostion_result() else {
return None;
};
let Some(mut input_handler) = self.input_handler.take() else {
return Some(1);
};
input_handler.replace_text_in_range(None, &comp_result);
self.input_handler.set(Some(input_handler));
self.invalidate_client_area();
return Some(0);
}
// currently, we don't care other stuff
None
}
fn parse_ime_char(&self, wparam: WPARAM) -> Option<String> {
let src = [wparam.0 as u16];
let Ok(first_char) = char::decode_utf16(src).collect::<Vec<_>>()[0] else {
return None;
};
Some(first_char.to_string())
}
fn handle_ime_char(&self, wparam: WPARAM) -> Option<isize> {
let Some(ime_char) = self.parse_ime_char(wparam) else {
return Some(1);
};
let Some(mut input_handler) = self.input_handler.take() else {
return Some(1);
};
input_handler.replace_text_in_range(None, &ime_char);
self.input_handler.set(Some(input_handler));
self.invalidate_client_area();
Some(0)
}
fn handle_drag_drop(&self, input: PlatformInput) {
let mut callbacks = self.callbacks.borrow_mut();
let Some(ref mut func) = callbacks.input else {

View File

@@ -26,6 +26,29 @@ macro_rules! create_definitions {
}
}
)*
/// Get the tag name list of the font OpenType features
/// only enabled or disabled features are returned
#[cfg(target_os = "windows")]
pub fn tag_value_list(&self) -> Vec<(String, bool)> {
let mut result = Vec::new();
$(
{
let value = if (self.enabled & (1 << $idx)) != 0 {
Some(true)
} else if (self.disabled & (1 << $idx)) != 0 {
Some(false)
} else {
None
};
if let Some(enable) = value {
let tag_name = stringify!($name).to_owned();
result.push((tag_name, enable));
}
}
)*
result
}
}
impl std::fmt::Debug for FontFeatures {
@@ -94,9 +117,11 @@ macro_rules! create_definitions {
let mut map = serializer.serialize_map(None)?;
$(
let feature = stringify!($name);
if let Some(value) = self.$name() {
map.serialize_entry(feature, &value)?;
{
let feature = stringify!($name);
if let Some(value) = self.$name() {
map.serialize_entry(feature, &value)?;
}
}
)*
@@ -161,5 +186,5 @@ create_definitions!(
(swsh, 30),
(titl, 31),
(tnum, 32),
(zero, 33)
(zero, 33),
);

View File

@@ -1,8 +1,8 @@
use crate::{
seal::Sealed, AfterLayoutIndex, AnyElement, AnyModel, AnyWeakModel, AppContext, Bounds,
ContentMask, Element, ElementContext, ElementId, Entity, EntityId, Flatten, FocusHandle,
FocusableView, IntoElement, LayoutId, Model, PaintIndex, Pixels, Render, Style,
StyleRefinement, TextStyle, ViewContext, VisualContext, WeakModel,
seal::Sealed, AfterLayoutIndex, AnyElement, AnyModel, AnyWeakModel, AppContext,
BeforePaintIndex, Bounds, ContentMask, Element, ElementContext, ElementId, Entity, EntityId,
Flatten, FocusHandle, FocusableView, IntoElement, LayoutId, Model, PaintIndex, Pixels, Render,
Style, StyleRefinement, TextStyle, ViewContext, VisualContext, WeakModel,
};
use anyhow::{Context, Result};
use refineable::Refineable;
@@ -24,13 +24,16 @@ impl<V> Sealed for View<V> {}
struct AnyViewState {
after_layout_range: Range<AfterLayoutIndex>,
before_paint_range: Range<BeforePaintIndex>,
paint_range: Range<PaintIndex>,
focus_target_bounds: Option<Bounds<Pixels>>,
cache_key: ViewCacheKey,
}
#[derive(Default)]
struct ViewCacheKey {
bounds: Bounds<Pixels>,
after_layout_bounds: Bounds<Pixels>,
before_paint_bounds: Bounds<Pixels>,
content_mask: ContentMask<Pixels>,
text_style: TextStyle,
}
@@ -92,6 +95,7 @@ impl<V: 'static> View<V> {
impl<V: Render> Element for View<V> {
type BeforeLayout = AnyElement;
type AfterLayout = ();
type BeforePaint = ();
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| {
@@ -102,14 +106,24 @@ impl<V: Render> Element for View<V> {
}
fn after_layout(
&mut self,
_bounds: Bounds<Pixels>,
element: &mut Self::BeforeLayout,
cx: &mut ElementContext,
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
(element.after_layout(cx), ())
}
fn before_paint(
&mut self,
_: Bounds<Pixels>,
element: &mut Self::BeforeLayout,
_: &mut Self::AfterLayout,
cx: &mut ElementContext,
) {
cx.set_view_id(self.entity_id());
cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| {
element.after_layout(cx)
element.before_paint(cx)
})
}
@@ -118,6 +132,7 @@ impl<V: Render> Element for View<V> {
_: Bounds<Pixels>,
element: &mut Self::BeforeLayout,
_: &mut Self::AfterLayout,
_: &mut Self::BeforePaint,
cx: &mut ElementContext,
) {
cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| {
@@ -277,7 +292,8 @@ impl<V: Render> From<View<V>> for AnyView {
impl Element for AnyView {
type BeforeLayout = Option<AnyElement>;
type AfterLayout = Option<AnyElement>;
type AfterLayout = ();
type BeforePaint = ();
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
if let Some(style) = self.cached_style.as_ref() {
@@ -299,20 +315,16 @@ impl Element for AnyView {
bounds: Bounds<Pixels>,
element: &mut Self::BeforeLayout,
cx: &mut ElementContext,
) -> Option<AnyElement> {
cx.set_view_id(self.entity_id());
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
if self.cached_style.is_some() {
cx.with_element_state::<AnyViewState, _>(
let focus_target_bounds = cx.with_element_state::<AnyViewState, _>(
Some(ElementId::View(self.entity_id())),
|element_state, cx| {
let mut element_state = element_state.unwrap();
let content_mask = cx.content_mask();
let text_style = cx.text_style();
if let Some(mut element_state) = element_state {
if element_state.cache_key.bounds == bounds
&& element_state.cache_key.content_mask == content_mask
if element_state.cache_key.after_layout_bounds == bounds
&& element_state.cache_key.text_style == text_style
&& !cx.window.dirty_views.contains(&self.entity_id())
&& !cx.window.refreshing
@@ -321,34 +333,106 @@ impl Element for AnyView {
cx.reuse_after_layout(element_state.after_layout_range.clone());
let after_layout_end = cx.after_layout_index();
element_state.after_layout_range = after_layout_start..after_layout_end;
return (None, Some(element_state));
return (element_state.focus_target_bounds, Some(element_state));
}
}
let after_layout_start = cx.after_layout_index();
let mut element = (self.render)(self, cx);
element.layout(bounds.origin, bounds.size.into(), cx);
let mut rendered_element = (self.render)(self, cx);
let element_measurement = rendered_element.layout(bounds.size.into(), cx);
*element = Some(rendered_element);
let focus_target_bounds =
element_measurement
.focus_target_bounds
.map(|focus_target_bounds| {
Bounds::new(
bounds.origin + focus_target_bounds.origin,
focus_target_bounds.size,
)
});
let after_layout_end = cx.after_layout_index();
(
Some(element),
Some(AnyViewState {
after_layout_range: after_layout_start..after_layout_end,
paint_range: PaintIndex::default()..PaintIndex::default(),
cache_key: ViewCacheKey {
bounds,
content_mask,
text_style,
},
}),
)
let view_state = AnyViewState {
after_layout_range: after_layout_start..after_layout_end,
before_paint_range: BeforePaintIndex::default()
..BeforePaintIndex::default(),
paint_range: PaintIndex::default()..PaintIndex::default(),
focus_target_bounds,
cache_key: ViewCacheKey {
after_layout_bounds: bounds,
text_style,
..Default::default()
},
};
(focus_target_bounds, Some(view_state))
},
);
(focus_target_bounds, ())
} else {
cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| {
let bounds = element.as_mut().unwrap().after_layout(cx);
(bounds, ())
})
}
}
fn before_paint(
&mut self,
bounds: Bounds<Pixels>,
element: &mut Self::BeforeLayout,
_: &mut Self::AfterLayout,
cx: &mut ElementContext,
) {
cx.set_view_id(self.entity_id());
if self.cached_style.is_some() {
cx.with_element_state::<AnyViewState, _>(
Some(ElementId::View(self.entity_id())),
|element_state, cx| {
let mut element_state = element_state.unwrap().unwrap();
let content_mask = cx.content_mask();
if let Some(element) = element {
let before_paint_start = cx.before_paint_index();
cx.with_absolute_element_offset(bounds.origin, |cx| {
element.before_paint(cx)
});
let before_paint_end = cx.before_paint_index();
element_state.before_paint_range = before_paint_start..before_paint_end;
element_state.cache_key.before_paint_bounds = bounds;
element_state.cache_key.content_mask = content_mask;
} else if element_state.cache_key.before_paint_bounds == bounds
&& element_state.cache_key.content_mask == content_mask
{
let before_paint_start = cx.before_paint_index();
cx.reuse_before_paint(element_state.before_paint_range.clone());
let before_paint_end = cx.before_paint_index();
element_state.before_paint_range = before_paint_start..before_paint_end;
} else {
let mut rendered_element = (self.render)(self, cx);
let after_layout_start = cx.after_layout_index();
rendered_element.layout(bounds.size.into(), cx);
let after_layout_end = cx.after_layout_index();
let before_paint_start = cx.before_paint_index();
cx.with_absolute_element_offset(bounds.origin, |cx| {
rendered_element.before_paint(cx)
});
let before_paint_end = cx.before_paint_index();
element_state.after_layout_range = after_layout_start..after_layout_end;
element_state.before_paint_range = before_paint_start..before_paint_end;
element_state.cache_key.before_paint_bounds = bounds;
element_state.cache_key.content_mask = content_mask;
*element = Some(rendered_element);
}
((), Some(element_state))
},
)
} else {
cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| {
let mut element = element.take().unwrap();
element.after_layout(cx);
Some(element)
element.as_mut().unwrap().before_paint(cx)
})
}
}
@@ -356,8 +440,9 @@ impl Element for AnyView {
fn paint(
&mut self,
_bounds: Bounds<Pixels>,
_: &mut Self::BeforeLayout,
element: &mut Self::AfterLayout,
element: &mut Self::BeforeLayout,
_: &mut Self::AfterLayout,
_: &mut Self::BeforePaint,
cx: &mut ElementContext,
) {
if self.cached_style.is_some() {

View File

@@ -121,7 +121,7 @@ pub(crate) struct DeferredDraw {
text_style_stack: Vec<TextStyleRefinement>,
element: Option<AnyElement>,
absolute_offset: Point<Pixels>,
layout_range: Range<AfterLayoutIndex>,
before_paint_range: Range<BeforePaintIndex>,
paint_range: Range<PaintIndex>,
}
@@ -146,6 +146,12 @@ pub(crate) struct Frame {
#[derive(Clone, Default)]
pub(crate) struct AfterLayoutIndex {
accessed_element_states_index: usize,
line_layout_index: LineLayoutIndex,
}
#[derive(Clone, Default)]
pub(crate) struct BeforePaintIndex {
hitboxes_index: usize,
tooltips_index: usize,
deferred_draws_index: usize,
@@ -399,7 +405,8 @@ impl<'a> ElementContext<'a> {
// Layout all root elements.
let mut root_element = self.window.root_view.as_ref().unwrap().clone().into_any();
root_element.layout(Point::default(), self.window.viewport_size.into(), self);
root_element.layout(self.window.viewport_size.into(), self);
root_element.before_paint(self);
let mut sorted_deferred_draws =
(0..self.window.next_frame.deferred_draws.len()).collect::<SmallVec<[_; 8]>>();
@@ -411,13 +418,15 @@ impl<'a> ElementContext<'a> {
let mut tooltip_element = None;
if let Some(prompt) = self.window.prompt.take() {
let mut element = prompt.view.any_view().into_any();
element.layout(Point::default(), self.window.viewport_size.into(), self);
element.layout(self.window.viewport_size.into(), self);
element.before_paint(self);
prompt_element = Some(element);
self.window.prompt = Some(prompt);
} else if let Some(active_drag) = self.app.active_drag.take() {
let mut element = active_drag.view.clone().into_any();
element.layout(AvailableSpace::min_size(), self);
let offset = self.mouse_position() - active_drag.cursor_offset;
element.layout(offset, AvailableSpace::min_size(), self);
self.with_element_offset(offset, |cx| element.before_paint(cx));
active_drag_element = Some(element);
self.app.active_drag = Some(active_drag);
} else {
@@ -446,7 +455,7 @@ impl<'a> ElementContext<'a> {
let tooltip_request = tooltip_request.unwrap();
let mut element = tooltip_request.tooltip.view.clone().into_any();
let mouse_position = tooltip_request.tooltip.mouse_position;
let tooltip_size = element.measure(AvailableSpace::min_size(), self);
let tooltip_size = element.layout(AvailableSpace::min_size(), self).size;
let mut tooltip_bounds = Bounds::new(mouse_position + point(px(1.), px(1.)), tooltip_size);
let window_bounds = Bounds {
@@ -478,7 +487,7 @@ impl<'a> ElementContext<'a> {
}
}
self.with_absolute_element_offset(tooltip_bounds.origin, |cx| element.after_layout(cx));
self.with_absolute_element_offset(tooltip_bounds.origin, |cx| element.before_paint(cx));
self.window.tooltip_bounds = Some(TooltipBounds {
id: tooltip_request.id,
@@ -500,16 +509,16 @@ impl<'a> ElementContext<'a> {
.dispatch_tree
.set_active_node(deferred_draw.parent_node);
let layout_start = self.after_layout_index();
let layout_start = self.before_paint_index();
if let Some(element) = deferred_draw.element.as_mut() {
self.with_absolute_element_offset(deferred_draw.absolute_offset, |cx| {
element.after_layout(cx)
element.before_paint(cx)
});
} else {
self.reuse_after_layout(deferred_draw.layout_range.clone());
self.reuse_before_paint(deferred_draw.before_paint_range.clone());
}
let layout_end = self.after_layout_index();
deferred_draw.layout_range = layout_start..layout_end;
let layout_end = self.before_paint_index();
deferred_draw.before_paint_range = layout_start..layout_end;
}
assert_eq!(
self.window.next_frame.deferred_draws.len(),
@@ -548,6 +557,26 @@ impl<'a> ElementContext<'a> {
pub(crate) fn after_layout_index(&self) -> AfterLayoutIndex {
AfterLayoutIndex {
accessed_element_states_index: self.window.next_frame.accessed_element_states.len(),
line_layout_index: self.window.text_system.layout_index(),
}
}
pub(crate) fn reuse_after_layout(&mut self, range: Range<AfterLayoutIndex>) {
let window = &mut self.window;
window.next_frame.accessed_element_states.extend(
window.rendered_frame.accessed_element_states[range.start.accessed_element_states_index
..range.end.accessed_element_states_index]
.iter()
.cloned(),
);
window
.text_system
.reuse_layouts(range.start.line_layout_index..range.end.line_layout_index);
}
pub(crate) fn before_paint_index(&self) -> BeforePaintIndex {
BeforePaintIndex {
hitboxes_index: self.window.next_frame.hitboxes.len(),
tooltips_index: self.window.next_frame.tooltip_requests.len(),
deferred_draws_index: self.window.next_frame.deferred_draws.len(),
@@ -557,7 +586,7 @@ impl<'a> ElementContext<'a> {
}
}
pub(crate) fn reuse_after_layout(&mut self, range: Range<AfterLayoutIndex>) {
pub(crate) fn reuse_before_paint(&mut self, range: Range<BeforePaintIndex>) {
let window = &mut self.window;
window.next_frame.hitboxes.extend(
window.rendered_frame.hitboxes[range.start.hitboxes_index..range.end.hitboxes_index]
@@ -595,7 +624,7 @@ impl<'a> ElementContext<'a> {
priority: deferred_draw.priority,
element: None,
absolute_offset: deferred_draw.absolute_offset,
layout_range: deferred_draw.layout_range.clone(),
before_paint_range: deferred_draw.before_paint_range.clone(),
paint_range: deferred_draw.paint_range.clone(),
}),
);
@@ -974,7 +1003,7 @@ impl<'a> ElementContext<'a> {
assert_eq!(
window.draw_phase,
DrawPhase::Layout,
"defer_draw can only be called during before_layout or after_layout"
"defer_draw can only be called during before_layout or before_paint"
);
let parent_node = window.next_frame.dispatch_tree.active_node_id().unwrap();
window.next_frame.deferred_draws.push(DeferredDraw {
@@ -984,7 +1013,7 @@ impl<'a> ElementContext<'a> {
priority,
element: Some(element),
absolute_offset,
layout_range: AfterLayoutIndex::default()..AfterLayoutIndex::default(),
before_paint_range: BeforePaintIndex::default()..BeforePaintIndex::default(),
paint_range: PaintIndex::default()..PaintIndex::default(),
});
}
@@ -1397,7 +1426,7 @@ impl<'a> ElementContext<'a> {
bounds
}
/// This method should be called during `after_layout`. You can use
/// This method should be called during `before_paint`. You can use
/// the returned [Hitbox] during `paint` or in an event handler
/// to determine whether the inserted hitbox was the topmost.
pub fn insert_hitbox(&mut self, bounds: Bounds<Pixels>, opaque: bool) -> Hitbox {

View File

@@ -155,7 +155,7 @@ impl FocusableView for ImageView {
impl Render for ImageView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let checkered_background = |bounds: Bounds<Pixels>, _, cx: &mut ElementContext| {
let checkered_background = |bounds: Bounds<Pixels>, cx: &mut ElementContext| {
let square_size = 32.0;
let start_y = bounds.origin.y.0;
@@ -190,7 +190,7 @@ impl Render for ImageView {
}
};
let checkered_background = canvas(|_, _| (), checkered_background)
let checkered_background = canvas(checkered_background)
.border_2()
.border_color(cx.theme().styles.colors.border)
.size_full()

View File

@@ -15,9 +15,9 @@ pub trait ContextProvider: Send + Sync {
/// Builds a specific context to be placed on top of the basic one (replacing all conflicting entries) and to be used for task resolving later.
fn build_context(
&self,
_: Option<&Path>,
_: &Location,
_: &mut AppContext,
_worktree_abs_path: Option<&Path>,
_location: &Location,
_cx: &mut AppContext,
) -> Result<TaskVariables> {
Ok(TaskVariables::default())
}

View File

@@ -363,14 +363,15 @@ impl Render for SyntaxTreeView {
.text_bg(cx.theme().colors().background).into_any_element();
rendered = rendered.child(
canvas(
move |bounds, cx| {
list.layout(bounds.origin, bounds.size.into(), cx);
list
},
|_, mut list, cx| list.paint(cx),
)
.size_full(),
// canvas(
// move |bounds, cx| {
// list.layout(bounds.origin, bounds.size.into(), cx);
// list
// },
// |_, mut list, cx| list.paint(cx),
// )
// .size_full(),
todo!("replace canvas"),
);
}

View File

@@ -85,13 +85,7 @@ pub fn init(
config.name.clone(),
config.grammar.clone(),
config.matcher.clone(),
move || {
Ok((
config.clone(),
load_queries($name),
Some(Arc::new(language::BasicContextProvider)),
))
},
move || Ok((config.clone(), load_queries($name), None)),
);
};
($name:literal, $adapters:expr) => {
@@ -105,13 +99,7 @@ pub fn init(
config.name.clone(),
config.grammar.clone(),
config.matcher.clone(),
move || {
Ok((
config.clone(),
load_queries($name),
Some(Arc::new(language::BasicContextProvider)),
))
},
move || Ok((config.clone(), load_queries($name), None)),
);
};
($name:literal, $adapters:expr, $context_provider:expr) => {

View File

@@ -10727,6 +10727,7 @@ fn serialize_blame_buffer_response(blame: git::blame::Blame) -> proto::BlameBuff
entries,
messages,
permalinks,
remote_url: blame.remote_url,
}
}
@@ -10775,6 +10776,7 @@ fn deserialize_blame_buffer_response(response: proto::BlameBufferResponse) -> gi
entries,
permalinks,
messages,
remote_url: response.remote_url,
}
}

View File

@@ -79,6 +79,10 @@ pub struct InlineBlameSettings {
///
/// Default: 0
pub delay_ms: Option<u64>,
/// The minimum column number to show the inline blame information at
///
/// Default: 0
pub min_column: Option<u32>,
}
const fn true_value() -> bool {

View File

@@ -37,7 +37,7 @@ use util::{maybe, NumericPrefixWithSuffix, ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
notifications::DetachAndPromptErr,
Workspace,
OpenInTerminal, Workspace,
};
const PROJECT_PANEL_KEY: &str = "ProjectPanel";
@@ -127,7 +127,6 @@ actions!(
CopyPath,
CopyRelativePath,
RevealInFinder,
OpenInTerminal,
Cut,
Paste,
Rename,
@@ -441,9 +440,7 @@ impl ProjectPanel {
.action("New Folder", Box::new(NewDirectory))
.separator()
.action("Reveal in Finder", Box::new(RevealInFinder))
.when(is_dir, |menu| {
menu.action("Open in Terminal…", Box::new(OpenInTerminal))
})
.action("Open in Terminal", Box::new(OpenInTerminal))
.when(is_dir, |menu| {
menu.separator()
.action("Find in Folder…", Box::new(NewSearchInDirectory))
@@ -599,7 +596,10 @@ impl ProjectPanel {
}
pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext<Self>) {
self.expanded_dir_ids.clear();
// By keeping entries for fully collapsed worktrees, we avoid expanding them within update_visible_entries
// (which is it's default behaviour when there's no entry for a worktree in expanded_dir_ids).
self.expanded_dir_ids
.retain(|_, expanded_entries| expanded_entries.is_empty());
self.update_visible_entries(None, cx);
cx.notify();
}
@@ -1128,13 +1128,20 @@ impl ProjectPanel {
fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
if let Some((worktree, entry)) = self.selected_entry(cx) {
let path = worktree.abs_path().join(&entry.path);
cx.dispatch_action(
workspace::OpenTerminal {
working_directory: path,
let abs_path = worktree.abs_path().join(&entry.path);
let working_directory = if entry.is_dir() {
Some(abs_path)
} else {
if entry.is_symlink {
abs_path.canonicalize().ok()
} else {
Some(abs_path)
}
.boxed_clone(),
)
.and_then(|path| Some(path.parent()?.to_path_buf()))
};
if let Some(working_directory) = working_directory {
cx.dispatch_action(workspace::OpenTerminal { working_directory }.boxed_clone())
}
}
}
@@ -1642,7 +1649,10 @@ impl ProjectPanel {
.child(if let Some(icon) = &icon {
h_flex().child(Icon::from_path(icon.to_string()).color(filename_text_color))
} else {
h_flex().size(IconSize::default().rems()).invisible()
h_flex()
.size(IconSize::default().rems())
.invisible()
.flex_none()
})
.child(
if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) {
@@ -1893,8 +1903,10 @@ impl Panel for ProjectPanel {
cx.notify();
}
fn icon(&self, _: &WindowContext) -> Option<ui::IconName> {
Some(ui::IconName::FileTree)
fn icon(&self, cx: &WindowContext) -> Option<IconName> {
ProjectPanelSettings::get_global(cx)
.button
.then(|| IconName::FileTree)
}
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {

View File

@@ -13,6 +13,7 @@ pub enum ProjectPanelDockPosition {
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
pub struct ProjectPanelSettings {
pub button: bool,
pub default_width: Pixels,
pub dock: ProjectPanelDockPosition,
pub file_icons: bool,
@@ -25,6 +26,10 @@ pub struct ProjectPanelSettings {
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct ProjectPanelSettingsContent {
/// Whether to show the project panel button in the status bar.
///
/// Default: true
pub button: Option<bool>,
/// Customise default width (in pixels) taken by project panel
///
/// Default: 240

View File

@@ -155,7 +155,7 @@ impl Render for QuickActionBar {
}
menu = menu.toggleable_entry(
"Show Git Blame",
"Show Git Blame Inline",
git_blame_inline_enabled,
Some(editor::actions::ToggleGitBlameInline.boxed_clone()),
{
@@ -180,7 +180,9 @@ impl Render for QuickActionBar {
quick_action_bar.toggle_settings_menu = Some(menu);
})
})
.tooltip(|cx| Tooltip::text("Editor Controls", cx));
.when(self.toggle_settings_menu.is_none(), |this| {
this.tooltip(|cx| Tooltip::text("Editor Controls", cx))
});
h_flex()
.id("quick action bar")

View File

@@ -1963,6 +1963,7 @@ message BlameBufferResponse {
repeated BlameEntry entries = 1;
repeated CommitMessage messages = 2;
repeated CommitPermalink permalinks = 3;
optional string remote_url = 4;
}
message MultiLspQuery {

View File

@@ -436,6 +436,7 @@ impl BufferSearchBar {
pub fn register(registrar: &mut impl SearchActionsRegistrar) {
registrar.register_handler(ForDeployed(|this, _: &FocusSearch, cx| {
this.query_editor.focus_handle(cx).focus(cx);
this.select_query(cx);
}));
registrar.register_handler(ForDeployed(|this, action: &ToggleCaseSensitive, cx| {
if this.supported_options().case {

View File

@@ -6,6 +6,9 @@ edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/semantic_index.rs"
@@ -43,6 +46,3 @@ project = { workspace = true, features = ["test-support"] }
tempfile.workspace = true
util = { workspace = true, features = ["test-support"] }
worktree = { workspace = true, features = ["test-support"] }
[lints]
workspace = true

View File

@@ -75,6 +75,26 @@ impl ResolvedTask {
pub fn substituted_variables(&self) -> &HashSet<VariableName> {
&self.substituted_variables
}
/// If the resolution produced a task with the command, returns a string, combined from its command and arguments.
pub fn resolved_command(&self) -> Option<String> {
self.resolved.as_ref().map(|resolved| {
let mut command = resolved.command.clone();
for arg in &resolved.args {
command.push(' ');
command.push_str(arg);
}
command
})
}
/// A human-readable label to display in the UI.
pub fn display_label(&self) -> &str {
self.resolved
.as_ref()
.map(|resolved| resolved.label.as_str())
.unwrap_or_else(|| self.resolved_label.as_str())
}
}
/// Variables, available for use in [`TaskContext`] when a Zed's [`TaskTemplate`] gets resolved into a [`ResolvedTask`].

View File

@@ -25,7 +25,6 @@ util.workspace = true
terminal.workspace = true
workspace.workspace = true
language.workspace = true
itertools.workspace = true
[dev-dependencies]

View File

@@ -168,8 +168,8 @@ fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> TaskContex
let language_context_provider = buffer
.read(cx)
.language()
.and_then(|language| language.context_provider())?;
.and_then(|language| language.context_provider())
.unwrap_or_else(|| Arc::new(BasicContextProvider));
let selection_range = selection.range();
let start = editor_snapshot
.display_snapshot

View File

@@ -233,11 +233,7 @@ impl PickerDelegate for TasksModalDelegate {
.map(|(index, (_, candidate))| StringMatchCandidate {
id: index,
char_bag: candidate.resolved_label.chars().collect(),
string: candidate
.resolved
.as_ref()
.map(|resolved| resolved.label.clone())
.unwrap_or_else(|| candidate.resolved_label.clone()),
string: candidate.display_label().to_owned(),
})
.collect::<Vec<_>>()
})
@@ -306,7 +302,28 @@ impl PickerDelegate for TasksModalDelegate {
) -> Option<Self::ListItem> {
let candidates = self.candidates.as_ref()?;
let hit = &self.matches[ix];
let (source_kind, _) = &candidates.get(hit.candidate_id)?;
let (source_kind, resolved_task) = &candidates.get(hit.candidate_id)?;
let template = resolved_task.original_task();
let display_label = resolved_task.display_label();
let mut tooltip_label_text = if display_label != &template.label {
template.label.clone()
} else {
String::new()
};
if let Some(resolved_command) = resolved_task.resolved_command() {
if display_label != resolved_command {
if !tooltip_label_text.trim().is_empty() {
tooltip_label_text.push('\n');
}
tooltip_label_text.push_str(&resolved_command);
}
}
let tooltip_label = if tooltip_label_text.trim().is_empty() {
None
} else {
Some(Tooltip::text(tooltip_label_text, cx))
};
let highlighted_location = HighlightedText {
text: hit.string.clone(),
@@ -321,10 +338,14 @@ impl PickerDelegate for TasksModalDelegate {
.get_type_icon(&name.to_lowercase())
.map(|icon_path| Icon::from_path(icon_path)),
};
Some(
ListItem::new(SharedString::from(format!("tasks-modal-{ix}")))
.inset(true)
.spacing(ListItemSpacing::Sparse)
.when_some(tooltip_label, |list_item, item_label| {
list_item.tooltip(move |_| item_label.clone())
})
.map(|item| {
let item = if matches!(source_kind, TaskSourceKind::UserInput)
|| Some(ix) <= self.last_used_candidate_index
@@ -368,18 +389,10 @@ impl PickerDelegate for TasksModalDelegate {
}
fn selected_as_query(&self) -> Option<String> {
use itertools::intersperse;
let task_index = self.matches.get(self.selected_index())?.candidate_id;
let tasks = self.candidates.as_ref()?;
let (_, task) = tasks.get(task_index)?;
task.resolved.as_ref().map(|spawn_in_terminal| {
let mut command = spawn_in_terminal.command.clone();
if !spawn_in_terminal.args.is_empty() {
command.push(' ');
command.extend(intersperse(spawn_in_terminal.args.clone(), " ".to_string()));
}
command
})
task.resolved_command()
}
fn confirm_input(&mut self, omit_history_entry: bool, cx: &mut ViewContext<Picker<Self>>) {
@@ -405,7 +418,11 @@ impl PickerDelegate for TasksModalDelegate {
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use editor::Editor;
use gpui::{TestAppContext, VisualTestContext};
use language::Point;
use project::{FakeFs, Project};
use serde_json::json;
@@ -561,6 +578,100 @@ mod tests {
);
}
#[gpui::test]
async fn test_basic_context_for_simple_files(cx: &mut TestAppContext) {
crate::tests::init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/dir",
json!({
".zed": {
"tasks.json": r#"[
{
"label": "hello from $ZED_FILE:$ZED_ROW:$ZED_COLUMN",
"command": "echo",
"args": ["hello", "from", "$ZED_FILE", ":", "$ZED_ROW", ":", "$ZED_COLUMN"]
},
{
"label": "opened now: $ZED_WORKTREE_ROOT",
"command": "echo",
"args": ["opened", "now:", "$ZED_WORKTREE_ROOT"]
}
]"#,
},
"file_without_extension": "aaaaaaaaaaaaaaaaaaaa\naaaaaaaaaaaaaaaaaa",
"file_with.odd_extension": "b",
}),
)
.await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
let tasks_picker = open_spawn_tasks(&workspace, cx);
assert_eq!(
task_names(&tasks_picker, cx),
Vec::<String>::new(),
"Should list no file or worktree context-dependent when no file is open"
);
tasks_picker.update(cx, |_, cx| {
cx.emit(DismissEvent);
});
drop(tasks_picker);
cx.executor().run_until_parked();
let _ = workspace
.update(cx, |workspace, cx| {
workspace.open_abs_path(PathBuf::from("/dir/file_with.odd_extension"), true, cx)
})
.await
.unwrap();
cx.executor().run_until_parked();
let tasks_picker = open_spawn_tasks(&workspace, cx);
assert_eq!(
task_names(&tasks_picker, cx),
vec![
"hello from …th.odd_extension:1:1".to_string(),
"opened now: /dir".to_string()
],
"Second opened buffer should fill the context, labels should be trimmed if long enough"
);
tasks_picker.update(cx, |_, cx| {
cx.emit(DismissEvent);
});
drop(tasks_picker);
cx.executor().run_until_parked();
let second_item = workspace
.update(cx, |workspace, cx| {
workspace.open_abs_path(PathBuf::from("/dir/file_without_extension"), true, cx)
})
.await
.unwrap();
let editor = cx.update(|cx| second_item.act_as::<Editor>(cx)).unwrap();
editor.update(cx, |editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_ranges(Some(Point::new(1, 2)..Point::new(1, 5)))
})
});
cx.executor().run_until_parked();
let tasks_picker = open_spawn_tasks(&workspace, cx);
assert_eq!(
task_names(&tasks_picker, cx),
vec![
"hello from …ithout_extension:2:3".to_string(),
"opened now: /dir".to_string()
],
"Opened buffer should fill the context, labels should be trimmed if long enough"
);
tasks_picker.update(cx, |_, cx| {
cx.emit(DismissEvent);
});
drop(tasks_picker);
cx.executor().run_until_parked();
}
fn open_spawn_tasks(
workspace: &View<Workspace>,
cx: &mut VisualTestContext,
@@ -569,7 +680,7 @@ mod tests {
workspace.update(cx, |workspace, cx| {
workspace
.active_modal::<TasksModal>(cx)
.unwrap()
.expect("no task modal after `Spawn` action was dispatched")
.read(cx)
.picker
.clone()

View File

@@ -750,6 +750,11 @@ impl Terminal {
InternalEvent::SetSelection(selection) => {
term.selection = selection.as_ref().map(|(sel, _)| sel.clone());
#[cfg(target_os = "linux")]
if let Some(selection_text) = term.selection_to_string() {
cx.write_to_primary(ClipboardItem::new(selection_text));
}
if let Some((_, head)) = selection {
self.selection_head = Some(*head);
}
@@ -766,6 +771,11 @@ impl Terminal {
selection.update(point, side);
term.selection = Some(selection);
#[cfg(target_os = "linux")]
if let Some(selection_text) = term.selection_to_string() {
cx.write_to_primary(ClipboardItem::new(selection_text));
}
self.selection_head = Some(point);
cx.emit(Event::SelectionsChanged)
}
@@ -1192,7 +1202,12 @@ impl Terminal {
Some(scroll_delta)
}
pub fn mouse_down(&mut self, e: &MouseDownEvent, origin: Point<Pixels>) {
pub fn mouse_down(
&mut self,
e: &MouseDownEvent,
origin: Point<Pixels>,
cx: &mut ModelContext<Self>,
) {
let position = e.position - origin;
let point = grid_point(
position,
@@ -1229,6 +1244,11 @@ impl Terminal {
self.events
.push_back(InternalEvent::SetSelection(Some((sel, point))));
}
} else if e.button == MouseButton::Middle {
if let Some(item) = cx.read_from_primary() {
let text = item.text().to_string();
self.input(text);
}
}
}

View File

@@ -429,7 +429,7 @@ impl TerminalElement {
move |e, cx| {
cx.focus(&focus);
terminal.update(cx, |terminal, cx| {
terminal.mouse_down(&e, origin);
terminal.mouse_down(&e, origin, cx);
cx.notify();
})
}
@@ -479,6 +479,17 @@ impl TerminalElement {
},
),
);
self.interactivity.on_mouse_down(
MouseButton::Middle,
TerminalElement::generic_button_handler(
terminal.clone(),
origin,
focus.clone(),
move |terminal, origin, e, cx| {
terminal.mouse_down(&e, origin, cx);
},
),
);
self.interactivity.on_scroll_wheel({
let terminal = terminal.clone();
move |e, cx| {
@@ -498,19 +509,8 @@ impl TerminalElement {
terminal.clone(),
origin,
focus.clone(),
move |terminal, origin, e, _cx| {
terminal.mouse_down(&e, origin);
},
),
);
self.interactivity.on_mouse_down(
MouseButton::Middle,
TerminalElement::generic_button_handler(
terminal.clone(),
origin,
focus.clone(),
move |terminal, origin, e, _cx| {
terminal.mouse_down(&e, origin);
move |terminal, origin, e, cx| {
terminal.mouse_down(&e, origin, cx);
},
),
);
@@ -542,7 +542,7 @@ impl TerminalElement {
impl Element for TerminalElement {
type BeforeLayout = ();
type AfterLayout = LayoutState;
type BeforePaint = LayoutState;
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
self.interactivity.occlude_mouse();
@@ -556,14 +556,14 @@ impl Element for TerminalElement {
(layout_id, ())
}
fn after_layout(
fn before_paint(
&mut self,
bounds: Bounds<Pixels>,
_: &mut Self::BeforeLayout,
cx: &mut ElementContext,
) -> Self::AfterLayout {
) -> Self::BeforePaint {
self.interactivity
.after_layout(bounds, bounds.size, cx, |_, _, hitbox, cx| {
.before_paint(bounds, bounds.size, cx, |_, _, hitbox, cx| {
let hitbox = hitbox.unwrap();
let settings = ThemeSettings::get_global(cx).clone();
@@ -776,7 +776,7 @@ impl Element for TerminalElement {
&mut self,
bounds: Bounds<Pixels>,
_: &mut Self::BeforeLayout,
layout: &mut Self::AfterLayout,
layout: &mut Self::BeforePaint,
cx: &mut ElementContext<'_>,
) {
cx.paint_quad(fill(bounds, layout.background_color));

View File

@@ -98,7 +98,6 @@ impl TerminalPanel {
.on_click(cx.listener(|pane, _, cx| {
pane.toggle_zoom(&workspace::ToggleZoom, cx);
}))
// TODO kb
.tooltip(move |cx| {
Tooltip::for_action(
if zoomed { "Zoom Out" } else { "Zoom In" },
@@ -292,13 +291,13 @@ impl TerminalPanel {
action: &workspace::OpenTerminal,
cx: &mut ViewContext<Workspace>,
) {
let Some(this) = workspace.focus_panel::<Self>(cx) else {
let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
return;
};
this.update(cx, |this, cx| {
this.add_terminal(Some(action.working_directory.clone()), None, cx)
})
terminal_panel.update(cx, |panel, cx| {
panel.add_terminal(Some(action.working_directory.clone()), None, cx)
});
workspace.focus_panel::<Self>(cx);
}
fn spawn_task(&mut self, spawn_in_terminal: &SpawnInTerminal, cx: &mut ViewContext<Self>) {
@@ -427,17 +426,17 @@ impl TerminalPanel {
}
}
///Create a new Terminal in the current working directory or the user's home directory
/// Create a new Terminal in the current working directory or the user's home directory
fn new_terminal(
workspace: &mut Workspace,
_: &workspace::NewTerminal,
cx: &mut ViewContext<Workspace>,
) {
let Some(this) = workspace.focus_panel::<Self>(cx) else {
let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
return;
};
this.update(cx, |this, cx| this.add_terminal(None, None, cx))
terminal_panel.update(cx, |this, cx| this.add_terminal(None, None, cx));
workspace.focus_panel::<Self>(cx);
}
fn terminals_for_task(
@@ -598,9 +597,14 @@ impl TerminalPanel {
Some(())
}
pub fn pane(&self) -> &View<Pane> {
&self.pane
}
fn has_no_terminals(&mut self, cx: &mut ViewContext<'_, Self>) -> bool {
self.pane.read(cx).items_len() == 0 && self.pending_terminals_to_add == 0
}
}
async fn wait_for_terminals_tasks(
@@ -713,7 +717,7 @@ impl Panel for TerminalPanel {
}
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
if active && self.pane.read(cx).items_len() == 0 && self.pending_terminals_to_add == 0 {
if active && self.has_no_terminals(cx) {
self.add_terminal(None, None, cx)
}
}

View File

@@ -185,7 +185,7 @@ impl TerminalView {
self.has_bell
}
pub fn clear_bel(&mut self, cx: &mut ViewContext<TerminalView>) {
pub fn clear_bell(&mut self, cx: &mut ViewContext<TerminalView>) {
self.has_bell = false;
cx.emit(Event::Wakeup);
}
@@ -332,7 +332,7 @@ impl TerminalView {
}
fn send_text(&mut self, text: &SendText, cx: &mut ViewContext<Self>) {
self.clear_bel(cx);
self.clear_bell(cx);
self.terminal.update(cx, |term, _| {
term.input(text.0.to_string());
});
@@ -340,7 +340,7 @@ impl TerminalView {
fn send_keystroke(&mut self, text: &SendKeystroke, cx: &mut ViewContext<Self>) {
if let Some(keystroke) = Keystroke::parse(&text.0).log_err() {
self.clear_bel(cx);
self.clear_bell(cx);
self.terminal.update(cx, |term, cx| {
term.try_keystroke(&keystroke, TerminalSettings::get_global(cx).option_as_meta);
});
@@ -700,7 +700,7 @@ pub fn regex_search_for_query(query: &project::search::SearchQuery) -> Option<Re
impl TerminalView {
fn key_down(&mut self, event: &KeyDownEvent, cx: &mut ViewContext<Self>) {
self.clear_bel(cx);
self.clear_bell(cx);
self.pause_cursor_blinking(cx);
self.terminal.update(cx, |term, cx| {

View File

@@ -40,7 +40,7 @@ impl<V: 'static> TodoList<V> {
All of this is relatively straightforward.
We use [gpui::SharedString] in components instead of [std::string::String]. This allows us to [TODO: someone who actually knows please explain why we use SharedString].
We use [gpui::SharedString] in components instead of [std::string::String]. This allows us to efficiently handle shared string data across multiple components and threads without the performance overhead of copying strings.
When we want to pass an action we pass a `ClickHandler`. Whenever we want to add an action, the struct it belongs to needs to be generic over the view type `V`.

View File

@@ -169,7 +169,8 @@ pub struct PopoverMenuFrameState {
impl<M: ManagedView> Element for PopoverMenu<M> {
type BeforeLayout = PopoverMenuFrameState;
type AfterLayout = Option<HitboxId>;
type AfterLayout = ();
type BeforePaint = Option<HitboxId>;
fn before_layout(&mut self, cx: &mut ElementContext) -> (gpui::LayoutId, Self::BeforeLayout) {
self.with_element_state(cx, |this, element_state, cx| {
@@ -219,14 +220,40 @@ impl<M: ManagedView> Element for PopoverMenu<M> {
_bounds: Bounds<Pixels>,
before_layout: &mut Self::BeforeLayout,
cx: &mut ElementContext,
) -> Option<HitboxId> {
self.with_element_state(cx, |_this, element_state, cx| {
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
cx.with_element_id(Some(self.id.clone()), |cx| {
let mut focus_target_bounds = None;
if let Some(child) = before_layout.child_element.as_mut() {
child.after_layout(cx);
if let Some(child_focus_target_bounds) = child.after_layout(cx) {
focus_target_bounds = Some(child_focus_target_bounds);
}
}
if let Some(menu) = before_layout.menu_element.as_mut() {
menu.after_layout(cx);
if let Some(menu_focus_target_bounds) = menu.after_layout(cx) {
focus_target_bounds = Some(menu_focus_target_bounds);
}
}
(focus_target_bounds, ())
})
}
fn before_paint(
&mut self,
_bounds: Bounds<Pixels>,
before_layout: &mut Self::BeforeLayout,
_after_layout: &mut Self::AfterLayout,
cx: &mut ElementContext,
) -> Option<HitboxId> {
self.with_element_state(cx, |_this, element_state, cx| {
if let Some(child) = before_layout.child_element.as_mut() {
child.before_paint(cx);
}
if let Some(menu) = before_layout.menu_element.as_mut() {
menu.before_paint(cx);
}
before_layout.child_layout_id.map(|layout_id| {
@@ -241,6 +268,7 @@ impl<M: ManagedView> Element for PopoverMenu<M> {
&mut self,
_: Bounds<gpui::Pixels>,
before_layout: &mut Self::BeforeLayout,
_after_layout: &mut Self::AfterLayout,
child_hitbox: &mut Option<HitboxId>,
cx: &mut ElementContext,
) {

View File

@@ -97,7 +97,8 @@ pub struct MenuHandleFrameState {
impl<M: ManagedView> Element for RightClickMenu<M> {
type BeforeLayout = MenuHandleFrameState;
type AfterLayout = Hitbox;
type AfterLayout = ();
type BeforePaint = Hitbox;
fn before_layout(&mut self, cx: &mut ElementContext) -> (gpui::LayoutId, Self::BeforeLayout) {
self.with_element_state(cx, |this, element_state, cx| {
@@ -144,20 +145,46 @@ impl<M: ManagedView> Element for RightClickMenu<M> {
}
fn after_layout(
&mut self,
_bounds: Bounds<Pixels>,
before_layout: &mut Self::BeforeLayout,
cx: &mut ElementContext,
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
cx.with_element_id(Some(self.id.clone()), |cx| {
let mut focus_target_bounds = None;
if let Some(child) = before_layout.child_element.as_mut() {
if let Some(child_focus_target_bounds) = child.after_layout(cx) {
focus_target_bounds = Some(child_focus_target_bounds);
}
}
if let Some(menu) = before_layout.menu_element.as_mut() {
if let Some(menu_focus_target_bounds) = menu.after_layout(cx) {
focus_target_bounds = Some(menu_focus_target_bounds);
}
}
(focus_target_bounds, ())
})
}
fn before_paint(
&mut self,
bounds: Bounds<Pixels>,
before_layout: &mut Self::BeforeLayout,
_after_layout: &mut Self::AfterLayout,
cx: &mut ElementContext,
) -> Hitbox {
cx.with_element_id(Some(self.id.clone()), |cx| {
let hitbox = cx.insert_hitbox(bounds, false);
if let Some(child) = before_layout.child_element.as_mut() {
child.after_layout(cx);
child.before_paint(cx);
}
if let Some(menu) = before_layout.menu_element.as_mut() {
menu.after_layout(cx);
menu.before_paint(cx);
}
hitbox
@@ -168,7 +195,8 @@ impl<M: ManagedView> Element for RightClickMenu<M> {
&mut self,
_bounds: Bounds<gpui::Pixels>,
before_layout: &mut Self::BeforeLayout,
hitbox: &mut Self::AfterLayout,
_after_layout: &mut Self::AfterLayout,
hitbox: &mut Self::BeforePaint,
cx: &mut ElementContext,
) {
self.with_element_state(cx, |this, element_state, cx| {

View File

@@ -205,23 +205,27 @@ impl<P> PathLikeWithPosition<P> {
column: None,
})
} else {
let maybe_col_str =
if maybe_col_str.ends_with(FILE_ROW_COLUMN_DELIMITER) {
&maybe_col_str[..maybe_col_str.len() - 1]
} else {
maybe_col_str
};
let (maybe_col_str, _) =
maybe_col_str.split_once(':').unwrap_or((maybe_col_str, ""));
match maybe_col_str.parse::<u32>() {
Ok(col) => Ok(Self {
path_like: parse_path_like_str(path_like_str)?,
row: Some(row),
column: Some(col),
}),
Err(_) => fallback(s),
Err(_) => Ok(Self {
path_like: parse_path_like_str(path_like_str)?,
row: Some(row),
column: None,
}),
}
}
}
Err(_) => fallback(s),
Err(_) => Ok(Self {
path_like: parse_path_like_str(path_like_str)?,
row: None,
column: None,
}),
}
}
}
@@ -352,23 +356,23 @@ mod tests {
#[test]
fn path_with_position_parsing_negative() {
for input in [
"test_file.rs:a",
"test_file.rs:a:b",
"test_file.rs::",
"test_file.rs::1",
"test_file.rs:1::",
"test_file.rs::1:2",
"test_file.rs:1::2",
"test_file.rs:1:2:3",
for (input, row, column) in [
("test_file.rs:a", None, None),
("test_file.rs:a:b", None, None),
("test_file.rs::", None, None),
("test_file.rs::1", None, None),
("test_file.rs:1::", Some(1), None),
("test_file.rs::1:2", None, None),
("test_file.rs:1::2", Some(1), None),
("test_file.rs:1:2:3", Some(1), Some(2)),
] {
let actual = parse_str(input);
assert_eq!(
actual,
PathLikeWithPosition {
path_like: input.to_string(),
row: None,
column: None,
path_like: "test_file.rs".to_string(),
row,
column,
},
"For negative case input str '{input}', got a parse mismatch"
);

View File

@@ -27,6 +27,7 @@ test-support = [
anyhow.workspace = true
any_vec.workspace = true
async-recursion.workspace = true
bincode = "1.2.1"
call.workspace = true
client.workspace = true
clock.workspace = true

View File

@@ -5,7 +5,8 @@ use crate::{
},
toolbar::Toolbar,
workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings},
NewCenterTerminal, NewFile, NewSearch, OpenVisible, SplitDirection, ToggleZoom, Workspace,
NewCenterTerminal, NewFile, NewSearch, OpenInTerminal, OpenTerminal, OpenVisible,
SplitDirection, ToggleZoom, Workspace,
};
use anyhow::Result;
use collections::{HashMap, HashSet, VecDeque};
@@ -1597,20 +1598,58 @@ impl Pane {
);
if let Some(entry) = single_entry_to_resolve {
let parent_abs_path = pane
.update(cx, |pane, cx| {
pane.workspace.update(cx, |workspace, cx| {
let project = workspace.project().read(cx);
project.worktree_for_entry(entry, cx).and_then(|worktree| {
let worktree = worktree.read(cx);
let entry = worktree.entry_for_id(entry)?;
let abs_path = worktree.absolutize(&entry.path).ok()?;
let parent = if entry.is_symlink {
abs_path.canonicalize().ok()?
} else {
abs_path
}
.parent()?
.to_path_buf();
Some(parent)
})
})
})
.ok()
.flatten();
let entry_id = entry.to_proto();
menu = menu.separator().entry(
"Reveal In Project Panel",
Some(Box::new(RevealInProjectPanel {
entry_id: Some(entry_id),
})),
cx.handler_for(&pane, move |pane, cx| {
pane.project.update(cx, |_, cx| {
cx.emit(project::Event::RevealInProjectPanel(
ProjectEntryId::from_proto(entry_id),
))
});
}),
);
menu = menu
.separator()
.entry(
"Reveal In Project Panel",
Some(Box::new(RevealInProjectPanel {
entry_id: Some(entry_id),
})),
cx.handler_for(&pane, move |pane, cx| {
pane.project.update(cx, |_, cx| {
cx.emit(project::Event::RevealInProjectPanel(
ProjectEntryId::from_proto(entry_id),
))
});
}),
)
.when_some(parent_abs_path, |menu, abs_path| {
menu.entry(
"Open in Terminal",
Some(Box::new(OpenInTerminal)),
cx.handler_for(&pane, move |_, cx| {
cx.dispatch_action(
OpenTerminal {
working_directory: abs_path.clone(),
}
.boxed_clone(),
);
}),
)
});
}
}
@@ -1721,16 +1760,35 @@ impl Pane {
return;
}
let edge_width = cx.rem_size() * 8;
let cursor = event.event.position;
let direction = if cursor.x < event.bounds.left() + edge_width {
Some(SplitDirection::Left)
} else if cursor.x > event.bounds.right() - edge_width {
Some(SplitDirection::Right)
} else if cursor.y < event.bounds.top() + edge_width {
Some(SplitDirection::Up)
} else if cursor.y > event.bounds.bottom() - edge_width {
Some(SplitDirection::Down)
let rect = event.bounds.size;
let size = event.bounds.size.width.min(event.bounds.size.height)
* WorkspaceSettings::get_global(cx).drop_target_size;
let relative_cursor = Point::new(
event.event.position.x - event.bounds.left(),
event.event.position.y - event.bounds.top(),
);
let direction = if relative_cursor.x < size
|| relative_cursor.x > rect.width - size
|| relative_cursor.y < size
|| relative_cursor.y > rect.height - size
{
[
SplitDirection::Up,
SplitDirection::Right,
SplitDirection::Down,
SplitDirection::Left,
]
.iter()
.min_by_key(|side| match side {
SplitDirection::Up => relative_cursor.y,
SplitDirection::Right => rect.width - relative_cursor.x,
SplitDirection::Down => rect.height - relative_cursor.y,
SplitDirection::Left => relative_cursor.x,
})
.cloned()
} else {
None
};
@@ -2013,10 +2071,7 @@ impl Render for Pane {
div()
.invisible()
.absolute()
.bg(theme::color_alpha(
cx.theme().colors().drop_target_background,
0.75,
))
.bg(cx.theme().colors().drop_target_background)
.group_drag_over::<DraggedTab>("", |style| style.visible())
.group_drag_over::<ProjectEntryId>("", |style| style.visible())
.group_drag_over::<ExternalPaths>("", |style| style.visible())
@@ -2032,17 +2087,22 @@ impl Render for Pane {
.on_drop(cx.listener(move |this, paths, cx| {
this.handle_external_paths_drop(paths, cx)
}))
.map(|div| match self.drag_split_direction {
None => div.top_0().left_0().right_0().bottom_0(),
Some(SplitDirection::Up) => div.top_0().left_0().right_0().h_32(),
Some(SplitDirection::Down) => {
div.left_0().bottom_0().right_0().h_32()
}
Some(SplitDirection::Left) => {
div.top_0().left_0().bottom_0().w_32()
}
Some(SplitDirection::Right) => {
div.top_0().bottom_0().right_0().w_32()
.map(|div| {
let size = DefiniteLength::Fraction(0.5);
match self.drag_split_direction {
None => div.top_0().right_0().bottom_0().left_0(),
Some(SplitDirection::Up) => {
div.top_0().left_0().right_0().h(size)
}
Some(SplitDirection::Down) => {
div.left_0().bottom_0().right_0().h(size)
}
Some(SplitDirection::Left) => {
div.top_0().left_0().bottom_0().w(size)
}
Some(SplitDirection::Right) => {
div.top_0().bottom_0().right_0().w(size)
}
}
}),
)

View File

@@ -649,7 +649,7 @@ mod element {
children: Vec<PaneAxisChildLayout>,
}
struct PaneAxisChildLayout {
pub struct PaneAxisChildLayout {
bounds: Bounds<Pixels>,
element: AnyElement,
handle: Option<PaneAxisHandleLayout>,
@@ -793,7 +793,8 @@ mod element {
impl Element for PaneAxisElement {
type BeforeLayout = ();
type AfterLayout = PaneAxisLayout;
type AfterLayout = Vec<PaneAxisChildLayout>;
type BeforePaint = PaneAxisLayout;
fn before_layout(
&mut self,
@@ -811,18 +812,9 @@ mod element {
fn after_layout(
&mut self,
bounds: Bounds<Pixels>,
_state: &mut Self::BeforeLayout,
_before_layout: &mut Self::BeforeLayout,
cx: &mut ElementContext,
) -> PaneAxisLayout {
let dragged_handle = cx.with_element_state::<Rc<RefCell<Option<usize>>>, _>(
Some(self.basis.into()),
|state, _cx| {
let state = state
.unwrap()
.unwrap_or_else(|| Rc::new(RefCell::new(None)));
(state.clone(), Some(state))
},
);
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
let flexes = self.flexes.lock().clone();
let len = self.children.len();
debug_assert!(flexes.len() == len);
@@ -844,13 +836,9 @@ mod element {
let mut origin = bounds.origin;
let space_per_flex = bounds.size.along(self.axis) / total_flex;
let mut bounding_boxes = self.bounding_boxes.lock();
bounding_boxes.clear();
let mut focus_target_bounds = None;
let mut layout = PaneAxisLayout {
dragged_handle: dragged_handle.clone(),
children: Vec::new(),
};
let mut children = Vec::new();
for (ix, mut child) in mem::take(&mut self.children).into_iter().enumerate() {
let child_flex = active_pane_magnification
.map(|magnification| {
@@ -866,25 +854,76 @@ mod element {
.size
.apply_along(self.axis, |_| space_per_flex * child_flex)
.map(|d| d.round());
let child_bounds = Bounds::new(origin, child_size);
let child_bounds = Bounds {
origin,
size: child_size,
};
bounding_boxes.push(Some(child_bounds));
child.layout(origin, child_size.into(), cx);
let child_measurement = child.layout(child_size.into(), cx);
if let Some(child_focus_target_bounds) = child_measurement.focus_target_bounds {
focus_target_bounds = Some(Bounds::new(
child_bounds.origin + child_focus_target_bounds.origin,
child_focus_target_bounds.size,
));
}
origin = origin.apply_along(self.axis, |val| val + child_size.along(self.axis));
layout.children.push(PaneAxisChildLayout {
children.push(PaneAxisChildLayout {
bounds: child_bounds,
element: child,
handle: None,
})
}
for (ix, child_layout) in layout.children.iter_mut().enumerate() {
if active_pane_magnification.is_none() {
if ix < len - 1 {
(focus_target_bounds, children)
}
fn before_paint(
&mut self,
bounds: Bounds<Pixels>,
_before_layout: &mut Self::BeforeLayout,
children: &mut Self::AfterLayout,
cx: &mut ElementContext,
) -> PaneAxisLayout {
let dragged_handle = cx.with_element_state::<Rc<RefCell<Option<usize>>>, _>(
Some(self.basis.into()),
|state, _cx| {
let state = state
.unwrap()
.unwrap_or_else(|| Rc::new(RefCell::new(None)));
(state.clone(), Some(state))
},
);
let magnification_value = WorkspaceSettings::get(None, cx).active_pane_magnification;
let active_pane_magnification = if magnification_value == 1. {
None
} else {
Some(magnification_value)
};
let mut origin = bounds.origin;
let mut bounding_boxes = self.bounding_boxes.lock();
bounding_boxes.clear();
let mut layout = PaneAxisLayout {
dragged_handle: dragged_handle.clone(),
children: Vec::new(),
};
for child in children {
let child_bounds = Bounds {
origin,
size: child.bounds.size,
};
bounding_boxes.push(Some(child_bounds));
cx.with_absolute_element_offset(origin, |cx| child.element.before_paint(cx));
origin =
origin.apply_along(self.axis, |val| val + child_bounds.size.along(self.axis));
child.bounds = child_bounds;
}
if active_pane_magnification.is_none() {
let mut child_layouts = layout.children.iter_mut().peekable();
while let Some(child_layout) = child_layouts.next() {
if child_layouts.peek().is_some() {
child_layout.handle =
Some(Self::layout_handle(self.axis, child_layout.bounds, cx));
}
@@ -898,7 +937,8 @@ mod element {
&mut self,
bounds: gpui::Bounds<ui::prelude::Pixels>,
_: &mut Self::BeforeLayout,
layout: &mut Self::AfterLayout,
_: &mut Self::AfterLayout,
layout: &mut Self::BeforePaint,
cx: &mut ui::prelude::ElementContext,
) {
for child in &mut layout.children {

View File

@@ -125,7 +125,7 @@ define_connection! {
//
// workspaces(
// workspace_id: usize, // Primary key for workspaces
// workspace_location: Vec<PathBuf>,
// workspace_location: Bincode<Vec<PathBuf>>,
// dock_visible: bool, // Deprecated
// dock_anchor: DockAnchor, // Deprecated
// dock_pane: Option<usize>, // Deprecated
@@ -289,64 +289,7 @@ define_connection! {
sql!(
ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool
),
// remove bincode
sql!(
CREATE TABLE workspaces_2(
workspace_id INTEGER PRIMARY KEY,
workspace_location TEXT UNIQUE,
dock_visible INTEGER,
dock_anchor TEXT,
dock_pane INTEGER,
left_sidebar_open INTEGER,
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
window_state TEXT,
window_x REAL,
window_y REAL,
window_width REAL,
window_height REAL,
display BLOB,
left_dock_visible INTEGER,
left_dock_active_panel TEXT,
right_dock_visible INTEGER,
right_dock_active_panel TEXT,
bottom_dock_visible INTEGER,
bottom_dock_active_panel TEXT,
left_dock_zoom INTEGER,
right_dock_zoom INTEGER,
bottom_dock_zoom INTEGER,
fullscreen INTEGER,
centered_layout INTEGER
) STRICT;
INSERT INTO workspaces_2 SELECT
workspace_id,
json_array(cast(unhex(substr(hex(workspace_location), 33)) as text)) as workspace_location,
dock_visible,
dock_anchor,
dock_pane,
left_sidebar_open,
timestamp,
window_state,
window_x,
window_y,
window_width,
window_height,
display,
left_dock_visible,
left_dock_active_panel,
right_dock_visible,
right_dock_active_panel,
bottom_dock_visible,
bottom_dock_active_panel,
left_dock_zoom,
right_dock_zoom,
bottom_dock_zoom,
fullscreen,
centered_layout
FROM workspaces
WHERE substr(hex(workspace_location), 0, 17) == "0100000000000000";
DROP TABLE workspaces;
ALTER TABLE workspaces_2 RENAME TO workspaces;
),
];
}

View File

@@ -48,15 +48,17 @@ impl<P: AsRef<Path>, T: IntoIterator<Item = P>> From<T> for WorkspaceLocation {
impl StaticColumnCount for WorkspaceLocation {}
impl Bind for &WorkspaceLocation {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement.bind(&dbg!(serde_json::to_string(&self.0)?), start_index)
bincode::serialize(&self.0)
.expect("Bincode serialization of paths should not fail")
.bind(statement, start_index)
}
}
impl Column for WorkspaceLocation {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let blob = statement.column_text(start_index)?;
let blob = statement.column_blob(start_index)?;
Ok((
WorkspaceLocation(serde_json::from_str(blob).context("JSON failed")?),
WorkspaceLocation(bincode::deserialize(blob).context("Bincode failed")?),
start_index + 1,
))
}

View File

@@ -112,6 +112,7 @@ actions!(
workspace,
[
Open,
OpenInTerminal,
NewFile,
NewWindow,
CloseWindow,
@@ -4006,12 +4007,9 @@ impl Render for Workspace {
.border_color(colors.border)
.child({
let this = cx.view().clone();
canvas(
move |bounds, cx| this.update(cx, |this, _cx| this.bounds = bounds),
|_, _, _| {},
)
.absolute()
.size_full()
canvas(move |bounds, cx| this.update(cx, |this, _cx| this.bounds = bounds))
.absolute()
.size_full()
})
.when(self.zoomed.is_none(), |this| {
this.on_drag_move(cx.listener(
@@ -4061,7 +4059,7 @@ impl Render for Workspace {
.overflow_hidden()
.child(
h_flex()
.h_full()
.flex_1()
.when_some(paddings.0, |this, p| {
this.child(p.border_r_1())
})
@@ -4972,6 +4970,7 @@ struct DisconnectedOverlay;
impl Element for DisconnectedOverlay {
type BeforeLayout = AnyElement;
type AfterLayout = ();
type BeforePaint = ();
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
let mut background = cx.theme().colors().elevated_surface_background;
@@ -4995,20 +4994,31 @@ impl Element for DisconnectedOverlay {
}
fn after_layout(
&mut self,
_bounds: Bounds<Pixels>,
overlay: &mut Self::BeforeLayout,
cx: &mut ElementContext,
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
(overlay.after_layout(cx), ())
}
fn before_paint(
&mut self,
bounds: Bounds<Pixels>,
overlay: &mut Self::BeforeLayout,
_after_layout: &mut Self::AfterLayout,
cx: &mut ElementContext,
) {
cx.insert_hitbox(bounds, true);
overlay.after_layout(cx);
overlay.before_paint(cx);
}
fn paint(
&mut self,
_: Bounds<Pixels>,
overlay: &mut Self::BeforeLayout,
_: &mut Self::AfterLayout,
_after_layout: &mut Self::AfterLayout,
_: &mut Self::BeforePaint,
cx: &mut ElementContext,
) {
overlay.paint(cx)

View File

@@ -12,6 +12,7 @@ pub struct WorkspaceSettings {
pub show_call_status_icon: bool,
pub autosave: AutosaveSetting,
pub restore_on_startup: RestoreOnStartupBehaviour,
pub drop_target_size: f32,
}
#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema)]
@@ -50,6 +51,11 @@ pub struct WorkspaceSettingsContent {
/// Values: none, last_workspace
/// Default: last_workspace
pub restore_on_startup: Option<RestoreOnStartupBehaviour>,
/// The size of the workspace split drop targets on the outer edges.
/// Given as a fraction that will be multiplied by the smaller dimension of the workspace.
///
/// Default: `0.2` (20% of the smaller dimension of the workspace)
pub drop_target_size: Option<f32>,
}
#[derive(Deserialize)]

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.132.0"
version = "0.133.0"
publish = false
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]

View File

@@ -158,7 +158,7 @@ For example, to disable ligatures for a given font you can add the following to
**Options**
The `left_padding` and `right_padding` options define the relative width of the
left and right padding of the central pane from the workspace when the centered layout mode is activated. Valid values range is from `0` to `0.45`.
left and right padding of the central pane from the workspace when the centered layout mode is activated. Valid values range is from `0` to `0.4`.
## Copilot
@@ -555,9 +555,14 @@ To interpret all `.c` files as C++, and files called `MyLockFile` as TOML:
- Default:
```json
"git": {
"git_gutter": "tracked_files"
},
{
"git": {
"git_gutter": "tracked_files",
"inline_blame": {
"enabled": true
}
}
}
```
### Git Gutter
@@ -572,7 +577,9 @@ To interpret all `.c` files as C++, and files called `MyLockFile` as TOML:
```json
{
"git_gutter": "tracked_files"
"git": {
"git_gutter": "tracked_files"
}
}
```
@@ -580,7 +587,52 @@ To interpret all `.c` files as C++, and files called `MyLockFile` as TOML:
```json
{
"git_gutter": "hide"
"git": {
"git_gutter": "hide"
}
}
```
### Inline Git Blame
- Description: Whether or not to show git blame information inline, on the currently focused line (requires Zed `0.132.0`).
- Setting: `inline_blame`
- Default:
```json
{
"git": {
"inline_blame": {
"enabled": true
}
}
}
```
**Options**
1. Disable inline git blame:
```json
{
"git": {
"inline_blame": {
"enabled": false
}
}
}
```
2. Only show inline git blame after a delay (that starts after cursor stops moving):
```json
{
"git": {
"inline_blame": {
"enabled": false,
"delay_ms": 500
}
}
}
```
@@ -730,12 +782,13 @@ These values take in the same options as the root-level settings with the same n
## Preview tabs
- Description:
(requires Zed `0.132.x`) \
Preview tabs allow you to open files in preview mode, where they close automatically when you switch to another file unless you explicitly pin them. This is useful for quickly viewing files without cluttering your workspace. Preview tabs display their file names in italics. \
There are several ways to convert a preview tab into a regular tab:
- Double-clicking on the file
- Double-clicking on the tab header
- Using the 'project_panel::OpenPermanent' action
- Using the `project_panel::OpenPermanent` action
- Editing the file
- Dragging the file to a different pane
@@ -749,8 +802,6 @@ These values take in the same options as the root-level settings with the same n
}
```
**Options**
### Enable preview from file finder
- Description: Determines whether to open files in preview mode when selected from the file finder.
@@ -926,7 +977,7 @@ These values take in the same options as the root-level settings with the same n
"font_features": null,
"font_size": null,
"option_as_meta": false,
"button": false
"button": false,
"shell": {},
"toolbar": {
"title": true
@@ -1243,6 +1294,7 @@ Run the `theme selector: toggle` action in the command palette to see a current
```json
"project_panel": {
"button": true,
"dock": "left",
"git_status": true,
"default_width": "N/A - width in pixels"
@@ -1305,6 +1357,21 @@ Run the `theme selector: toggle` action in the command palette to see a current
`boolean` values
## Calls
- Description: Customise behaviour when participating in a call
- Setting: `calls`
- Default:
```json
"calls": {
// Join calls with the microphone live by default
"mute_on_join": false,
// Share your project when you are the first to join a channel
"share_on_join": true
},
```
## An example configuration:
```json

View File

@@ -25,7 +25,7 @@ git submodule update --init --recursive
rustup target add wasm32-wasi
```
- Install [Visual Studio](https://visualstudio.microsoft.com/downloads/) with optional component `MSVC v*** - VS YYYY C++ x64/x86 build tools`.
- Install [Visual Studio](https://visualstudio.microsoft.com/downloads/) with optional component `MSVC v*** - VS YYYY C++ x64/x86 build tools` and install Windows 11 or 10 SDK depending on your system
> [!NOTE]
> `v***` is your VS version and `YYYY` is year when your VS was released.

View File

@@ -17,25 +17,31 @@ You will need write access to the Zed repository to do this:
- Run `./script/bump-zed-minor-versions` and push the tags
and branches as instructed.
- Wait for the builds to appear at https://github.com/zed-industries/zed/releases (typically takes around 30 minutes)
- Copy the release notes from the previous Preview release(s) to the current Stable release.
- Write new release notes for Preview. `/script/get-preview-channel-changes` can help with this, but you'll need to edit and format the output to make it good.
- Download the artifacts for each release and test that you can run them locally.
- Publish the releases.
- While you're waiting:
- Start creating the new release notes for preview. You can start with the output of `./script/get-preview-channel-changes`.
- Start drafting the release tweets.
- Once the builds are ready:
- Copy the release notes from the previous Preview release(s) to the current Stable release.
- Download the artifacts for each release and test that you can run them locally.
- Publish the releases on GitHub.
- Tweet the tweets (Credentials are in 1password).
## Patch release process
If your PR fixes a panic or a crash, you should cherry-pick it to the current stable and preview branches. If your PR fixes a regression in recently released code, you should cherry-pick it to the appropriate branch.
If your PR fixes a panic or a crash, you should cherry-pick it to the current stable and preview branches. If your PR fixes a regression in recently released code, you should cherry-pick it to preview.
You will need write access to the Zed repository to do this:
- Cherry pick them onto the correct branch. You can either do this manually, or leave a comment of the form `/cherry-pick v0.XXX.x` on the PR, and the GitHub bot should do it for you.
- Run `./script/trigger-release {preview|stable}`
- Send a PR containing your change to `main` as normal.
- Leave a comment on the PR `/cherry-pick v0.XXX.x`. Once your PR is merged, the Github bot will send a PR to the branch.
- In case of a merge conflict, you will have to cherry-pick manually and push the change to the `v0.XXX.x` branch.
- After the commits are cherry-picked onto the branch, run `./script/trigger-release {preview|stable}`. This will bump the version numbers, create a new release tag, and kick off a release build.
- Wait for the builds to appear at https://github.com/zed-industries/zed/releases (typically takes around 30 minutes)
- Add release notes using the `Release notes:` section of each cherry-picked PR.
- Proof-read and edit the release notes as needed.
- Download the artifacts for each release and test that you can run them locally.
- Publish the release.
## Nightly release process
- Merge your changes to main
- Run `./script/trigger-release {nightly}`
In addition to the public releases, we also have a nightly build that we encourage employees to use.
Nightly is released by cron once a day, and can be shipped as often as you'd like. There are no release notes or announcements, so you can just merge your changes to main and run `./script/trigger-release nightly`.

View File

@@ -0,0 +1,16 @@
[package]
name = "zed_glsl"
version = "0.1.0"
edition = "2021"
publish = false
license = "Apache-2.0"
[lints]
workspace = true
[lib]
path = "src/glsl.rs"
crate-type = ["cdylib"]
[dependencies]
zed_extension_api = "0.0.6"

View File

@@ -1,11 +1,15 @@
id = "glsl"
name = "GLSL"
description = "GLSL support."
version = "0.0.1"
version = "0.1.0"
schema_version = 1
authors = ["Mikayla Maki <mikayla@zed.dev>"]
repository = "https://github.com/zed-industries/zed"
[language_servers.glsl_analyzer]
name = "GLSL Analyzer LSP"
language = "GLSL"
[grammars.glsl]
repository = "https://github.com/theHamsta/tree-sitter-glsl"
commit = "31064ce53385150f894a6c72d61b94076adf640a"

View File

@@ -1,6 +1,15 @@
name = "GLSL"
grammar = "glsl"
path_suffixes = ["vert", "frag", "tesc", "tese", "geom", "comp"]
path_suffixes = [
# Traditional rasterization pipeline shaders
"vert", "frag", "tesc", "tese", "geom",
# Compute shaders
"comp",
# Ray tracing pipeline shaders
"rgen", "rint", "rahit", "rchit", "rmiss", "rcall",
# Other
"glsl"
]
line_comments = ["// "]
block_comment = ["/* ", " */"]
brackets = [

131
extensions/glsl/src/glsl.rs Normal file
View File

@@ -0,0 +1,131 @@
use std::fs;
use zed::settings::LspSettings;
use zed_extension_api::{self as zed, serde_json, LanguageServerId, Result};
struct GlslExtension {
cached_binary_path: Option<String>,
}
impl GlslExtension {
fn language_server_binary_path(
&mut self,
language_server_id: &LanguageServerId,
worktree: &zed::Worktree,
) -> Result<String> {
if let Some(path) = worktree.which("glsl_analyzer") {
return Ok(path);
}
if let Some(path) = &self.cached_binary_path {
if fs::metadata(path).map_or(false, |stat| stat.is_file()) {
return Ok(path.clone());
}
}
zed::set_language_server_installation_status(
&language_server_id,
&zed::LanguageServerInstallationStatus::CheckingForUpdate,
);
let release = zed::latest_github_release(
"nolanderc/glsl_analyzer",
zed::GithubReleaseOptions {
require_assets: true,
pre_release: false,
},
)?;
let (platform, arch) = zed::current_platform();
let asset_name = format!(
"{arch}-{os}.zip",
arch = match arch {
zed::Architecture::Aarch64 => "aarch64",
zed::Architecture::X86 => "x86",
zed::Architecture::X8664 => "x86_64",
},
os = match platform {
zed::Os::Mac => "macos",
zed::Os::Linux => "linux-musl",
zed::Os::Windows => "windows",
}
);
let asset = release
.assets
.iter()
.find(|asset| asset.name == asset_name)
.ok_or_else(|| format!("no asset found matching {:?}", asset_name))?;
let version_dir = format!("glsl_analyzer-{}", release.version);
fs::create_dir_all(&version_dir)
.map_err(|err| format!("failed to create directory '{version_dir}': {err}"))?;
let binary_path = format!("{version_dir}/bin/glsl_analyzer");
if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) {
zed::set_language_server_installation_status(
&language_server_id,
&zed::LanguageServerInstallationStatus::Downloading,
);
zed::download_file(
&asset.download_url,
&version_dir,
match platform {
zed::Os::Mac | zed::Os::Linux => zed::DownloadedFileType::Zip,
zed::Os::Windows => zed::DownloadedFileType::Zip,
},
)
.map_err(|e| format!("failed to download file: {e}"))?;
zed::make_file_executable(&binary_path)?;
let entries =
fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?;
for entry in entries {
let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?;
if entry.file_name().to_str() != Some(&version_dir) {
fs::remove_dir_all(&entry.path()).ok();
}
}
}
self.cached_binary_path = Some(binary_path.clone());
Ok(binary_path)
}
}
impl zed::Extension for GlslExtension {
fn new() -> Self {
Self {
cached_binary_path: None,
}
}
fn language_server_command(
&mut self,
language_server_id: &zed::LanguageServerId,
worktree: &zed::Worktree,
) -> Result<zed::Command> {
Ok(zed::Command {
command: self.language_server_binary_path(language_server_id, worktree)?,
args: vec![],
env: Default::default(),
})
}
fn language_server_workspace_configuration(
&mut self,
_language_server_id: &zed::LanguageServerId,
worktree: &zed::Worktree,
) -> Result<Option<serde_json::Value>> {
let settings = LspSettings::for_worktree("glsl_analyzer", worktree)
.ok()
.and_then(|lsp_settings| lsp_settings.settings.clone())
.unwrap_or_default();
Ok(Some(serde_json::json!({
"glsl_analyzer": settings
})))
}
}
zed::register_extension!(GlslExtension);

View File

@@ -1,6 +1,6 @@
[package]
name = "zed_prisma"
version = "0.0.1"
version = "0.0.2"
edition = "2021"
publish = false
license = "Apache-2.0"

View File

@@ -1,7 +1,7 @@
id = "prisma"
name = "Prisma"
description = "Prisma support."
version = "0.0.1"
version = "0.0.2"
schema_version = 1
authors = ["Matthew Gramigna <matthewgramigna@gmail.com>"]
repository = "https://github.com/zed-industries/zed"

109
script/draft-release-notes Executable file
View File

@@ -0,0 +1,109 @@
#!/usr/bin/env node --redirect-warnings=/dev/null
const { execFileSync } = require("child_process");
main();
async function main() {
let version = process.argv[2];
let channel = process.argv[3];
let parts = version.split(".");
if (
process.argv.length != 4 ||
parts.length != 3 ||
parts.find((part) => isNaN(part)) != null ||
(channel != "stable" && channel != "preview")
) {
console.log("Usage: draft-release-notes <version> {stable|preview}");
process.exit(1);
}
let priorVersion = [parts[0], parts[1], parts[2] - 1].join(".");
let suffix = "";
if (channel == "preview") {
suffix = "-pre";
if (parts[2] == 0) {
priorVersion = [parts[0], parts[1] - 1, 0].join(".");
}
} else if (!tagExists("v${priorVersion}")) {
console.log("Copy the release notes from preview.");
process.exit(0);
}
let [tag, priorTag] = [`v${version}${suffix}`, `v${priorVersion}${suffix}`];
const newCommits = getCommits(priorTag, tag);
let releaseNotes = [];
let missing = [];
let skipped = [];
for (const commit of newCommits) {
let link = "https://github.com/zed-industries/zed/pull/" + commit.pr;
let notes = commit.releaseNotes;
if (commit.pr == "") {
link = "https://github.com/zed-industries/zed/commits/" + commit.hash;
} else if (!notes.includes("zed-industries/zed/issues")) {
notes = notes + " ([#" + commit.pr + "](" + link + "))";
}
if (commit.releaseNotes == "") {
missing.push("- MISSING " + commit.firstLine + " " + link);
} else if (commit.releaseNotes.startsWith("- N/A")) {
skipped.push("- N/A " + commit.firstLine + " " + link);
} else {
releaseNotes.push(notes);
}
}
console.log(releaseNotes.join("\n") + "\n");
console.log("<!-- ");
console.log(missing.join("\n"));
console.log(skipped.join("\n"));
console.log("-->");
}
function getCommits(oldTag, newTag) {
const pullRequestNumbers = execFileSync(
"git",
["log", `${oldTag}..${newTag}`, "--format=DIVIDER\n%H|||%B"],
{ encoding: "utf8" },
)
.replace(/\r\n/g, "\n")
.split("DIVIDER\n")
.filter((commit) => commit.length > 0)
.map((commit) => {
let [hash, firstLine] = commit.split("\n")[0].split("|||");
let cherryPick = firstLine.match(/\(cherry-pick #([0-9]+)\)/)?.[1] || "";
let pr = firstLine.match(/\(#(\d+)\)$/)?.[1] || "";
let releaseNotes = (commit.split(/Release notes:.*\n/i)[1] || "")
.split("\n\n")[0]
.trim()
.replace(/\n(?![\n-])/g, " ");
if (releaseNotes.includes("<public_issue_number_if_exists>")) {
releaseNotes = "";
}
return {
hash,
pr,
cherryPick,
releaseNotes,
firstLine,
};
});
return pullRequestNumbers;
}
function tagExists(tag) {
try {
execFileSync("git", ["rev-parse", "--verify", tag]);
return true;
} catch (e) {
return false;
}
}

View File

@@ -1 +0,0 @@