Compare commits

...

30 Commits

Author SHA1 Message Date
Thorsten Ball
66f9457bff zed 0.143.4 2024-07-09 11:16:44 +02:00
Thorsten Ball
e9714f7a74 Revert "x11: Differentiate between mouse and keyboard focus #13943" (#13974)
This reverts #13943 and reopens #13897 since the fix in #13943 comes
with a regression:

Sometimes Zed loses keyboard focus and can't be restored. I haven't
figured out yet exactly when and how this happens and can't reliably
reproduce it yet, but there's something off with focus handling.

One reliable way to reproduce _one_ of the problems:

1. Open two zed windows
2. Focus one Zed window
3. Hover with the mouse over the other
4. Try to type in the window that should still be focused

So, to be careful, I'm going to revert the PR first, since I couldn't
find an obvious fix yet. If we do find a fix, we can unrevert.


Release Notes:

- N/A
2024-07-09 11:14:25 +02:00
Aleksei Gusev
477dc1481b Add keyboard shortcuts for the prompts on Linux (#13915)
This change adds ability to choose any action from prompts, not just the
default one and cancel as Zed has right now. For example, when a user
tries to close a file with edits in it the prompt offers "Don't save"
option that can be selected only with mouse. Now you can use arrows,
tab/shift-tab to pick action and enter/space to confirm it.

Fixes [#13906](https://github.com/zed-industries/zed/issues/13906)


Release Notes:

- Added keyboard navigation in the prompts on Linux
([#13906](https://github.com/zed-industries/zed/issues/13906)).


Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
2024-07-09 11:14:07 +02:00
Zed Bot
8af43984d8 Bump to 0.143.3 for @ConradIrwin 2024-07-08 21:00:54 -07:00
Mikayla Maki
5b271b01ad Separate out macOS and Linux keymaps (#13792)
Release Notes:

- Added Linux-Specific keymaps for JetBrains, Atom and Sublime Text
- Improved MacOS-specific keymaps for JetBrains and Atom
- Improved Linux default keymap (VSCode compatibility)
- Windows now uses same keymap as Linux

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2024-07-08 21:54:23 -06:00
fsh
b4c61e5ba5 x11: Properly update XKB group state (#13931)
Pass on all the XkbStateNotify information to XKB.

> "All parameters must always be passed, or the resulting state may be
incoherent."
>
https://docs.rs/xkbcommon/latest/xkbcommon/xkb/struct.State.html#method.update_mask

Previously, many keymaps using multiple groups/layers would not work and
remain in group0.

Release Notes:

- Fixed handling of Xkb keymap groups on X11.
2024-07-08 21:52:39 -06:00
Conrad Irwin
ec270e344b Send IME-supported key downs (#13964)
Release Notes:

- N/A
2024-07-08 21:50:34 -06:00
Sebastijan Kelnerič
f837bc1963 Improve window decorations: check for compositor support (#13822)
Adds the `compositor_support` to the `X11WindowState` struct so that
correct window decorations are selected

Release notes:

- N/A

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-07-08 21:50:27 -06:00
Conrad Irwin
41fdc44d75 x11 calloop 2 (#13955)
Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
2024-07-08 21:50:13 -06:00
apricotbucket28
61f22219ed wayland: Implement activate() API and use portals to open URLs and paths (#13336)
This PR consists of two main changes:
1. The first commit changes the `open` crate for opening URLs/paths for
the `OpenURI` desktop portal. This fixes the activation token not being
passed to programs (at least on KDE).
2. The second commit implements the window `activate()` API on Wayland.
This allows KWin and Mutter to show a visual indicator when the window
is requesting attention. (see
https://github.com/zed-industries/zed/issues/12557)

![image](https://github.com/zed-industries/zed/assets/71973804/ce148f8e-28fd-4249-8f8d-3a5828ed6f83)


Release Notes:

- N/A
2024-07-08 21:49:50 -06:00
apricotbucket28
92aae1cec1 x11: Differentiate between mouse and keyboard focus (#13943)
Fixes https://github.com/zed-industries/zed/issues/13897

Release Notes:

- N/A
2024-07-08 21:49:35 -06:00
Thorsten Ball
5e9139f4a6 linux: Add tracing logs to the x11 client and linux dispatcher (#13928)
This adds some tracing logging that can be toggled on to debug issues.

How I used it:

```
RUST_LOG=gpui=trace cargo run
```


Release Notes:

- N/A
2024-07-08 21:48:56 -06:00
jansol
8cae7eb6af linux: Remove StartupWMClass from .desktop file, add NewWorkspace action (#13807)
Release Notes:

- N/A
2024-07-08 21:48:42 -06:00
Taimuraz Kaitmazov
8a71bc161e linux: Fix dropping action when action is just started (#13840)
```
Thread "main" panicked with "divide by zero error when dividing duration by scalar" at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/core/src/time.rs:1172:31
   0: zed::reliability::init_panic_hook::{{closure}}
             at crates/zed/src/reliability.rs:58:29
   1: <alloc::boxed::Box<F,A> as core::ops::function::Fn<Args>>::call
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/alloc/src/boxed.rs:2036:9
      std::panicking::rust_panic_with_hook
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/panicking.rs:799:13
   2: std::panicking::begin_panic_handler::{{closure}}
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/panicking.rs:664:13
   3: std::sys_common::backtrace::__rust_end_short_backtrace
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/sys_common/backtrace.rs:171:18
   4: rust_begin_unwind
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/panicking.rs:652:5
   5: core::panicking::panic_fmt
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/core/src/panicking.rs:72:14
   6: core::panicking::panic_display
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/core/src/panicking.rs:263:5
   7: core::option::expect_failed
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/core/src/option.rs:1994:5
   8: core::option::Option<T>::expect
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/core/src/option.rs:895:21
      <core::time::Duration as core::ops::arith::Div<u32>>::div
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/core/src/time.rs:1172:31
   9: <gpui::platform::linux::wayland::client::WaylandClientStatePtr as wayland_client::event_queue::Dispatch<wayland_client::protocol::wl_keyboard::WlKeyboard,()>>::event::{{closure}}
             at crates/gpui/src/platform/linux/wayland/client.rs:1211:63
  10: <core::cell::RefCell<calloop::sources::DispatcherInner<S,F>> as calloop::sources::EventDispatcher<Data>>::process_events::{{closure}}
             at /home/atassis/.cargo/registry/src/index.crates.io-6f17d22bba15001f/calloop-0.13.0/src/sources/mod.rs:327:61
  11: <calloop::sources::timer::Timer as calloop::sources::EventSource>::process_events
             at /home/atassis/.cargo/registry/src/index.crates.io-6f17d22bba15001f/calloop-0.13.0/src/sources/timer.rs:122:38
  12: <core::cell::RefCell<calloop::sources::DispatcherInner<S,F>> as calloop::sources::EventDispatcher<Data>>::process_events
             at /home/atassis/.cargo/registry/src/index.crates.io-6f17d22bba15001f/calloop-0.13.0/src/sources/mod.rs:326:9
  13: calloop::loop_logic::EventLoop<Data>::dispatch_events
             at /home/atassis/.cargo/registry/src/index.crates.io-6f17d22bba15001f/calloop-0.13.0/src/loop_logic.rs:445:31
  14: calloop::loop_logic::EventLoop<Data>::dispatch
             at /home/atassis/.cargo/registry/src/index.crates.io-6f17d22bba15001f/calloop-0.13.0/src/loop_logic.rs:559:9
  15: calloop::loop_logic::EventLoop<Data>::run
             at /home/atassis/.cargo/registry/src/index.crates.io-6f17d22bba15001f/calloop-0.13.0/src/loop_logic.rs:596:13
  16: <gpui::platform::linux::wayland::client::WaylandClient as gpui::platform::linux::platform::LinuxClient>::run
             at crates/gpui/src/platform/linux/wayland/client.rs:655:9
  17: gpui::platform::linux::platform::<impl gpui::platform::Platform for P>::run
             at crates/gpui/src/platform/linux/platform.rs:153:9
  18: gpui::app::App::run
             at crates/gpui/src/app.rs:140:9
  19: zed::main
             at crates/zed/src/main.rs:382:5
  20: core::ops::function::FnOnce::call_once
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/core/src/ops/function.rs:250:5
  21: std::sys_common::backtrace::__rust_begin_short_backtrace
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/sys_common/backtrace.rs:155:18
  22: std::rt::lang_start::{{closure}}
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/rt.rs:159:18
  23: core::ops::function::impls::<impl core::ops::function::FnOnce<A> for &F>::call_once
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/core/src/ops/function.rs:284:13
      std::panicking::try::do_call
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/panicking.rs:559:40
      std::panicking::try
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/panicking.rs:523:19
      std::panic::catch_unwind
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/panic.rs:149:14
      std::rt::lang_start_internal::{{closure}}
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/rt.rs:141:48
      std::panicking::try::do_call
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/panicking.rs:559:40
      std::panicking::try
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/panicking.rs:523:19
      std::panic::catch_unwind
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/panic.rs:149:14
      std::rt::lang_start_internal
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/rt.rs:141:20
  24: std::rt::lang_start
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/rt.rs:158:17
  25: main
  26: __libc_start_call_main
  27: __libc_start_main_impl
  28: _start
```
This error was happening when I started typing. This PR fixes this
error.
Fedora 40, latest kernel, gnome 46, wayland.


Release Notes:

- N/A
2024-07-08 21:48:15 -06:00
apricotbucket28
618915f5f1 wayland: Fix window state issues (#13885)
Fixes some issues with the CSD added in
https://github.com/zed-industries/zed/pull/13611

Here's a video comparing the master branch (yellow icon) with this PR
(blue icon):


https://github.com/zed-industries/zed/assets/71973804/35be443a-8f24-4aed-910b-625bad9821e2

_Note: the flicker at the bottom of the window when maximizing is an
issue with the KDE floating task bar, it happens with all programs._

Release Notes:

- N/A
2024-07-08 21:47:56 -06:00
Thorsten Ball
7e6329eabf Configurable window decorations (#13866)
Introduces the `ZED_WINDOW_DECORATIONS` env variable.

- Not set, defaulting to client-side decorations
- Value is "client": client-side decorations
- Value is "server": server-side decorations

I think it's good to have this escape-hatch next to all possible
detection mechanisms.

Release Notes:

- N/A
2024-07-08 21:47:30 -06:00
Thorsten Ball
ec94ec9f2b linux/x11: Fix gap when tiling windows side by side (#13859)
By leveraging the `_GTK_EDGE_CONSTRAINTS` atom we can get all four
booleans for the `Tiling` struct and figure out which side is free when
the window is tiled to half of the screen.

For the logic behind the `_GTK_EDGE_CONSTRAINTS` see:
-
8e9d13aa3b/src/x11/window-x11.c (L65-L75)
-
8e9d13aa3b/src/x11/window-x11.c (L1205-L1231)

(I used Claude 3.5 Sonnet with our code and these pieces from `mutter`
to generate the Rust code, that was pretty sweet)

This fixes the gap in the middle when a GPUI window is tiled to the left
and another window to the right.

It's not _perfect_ but it looks a lot better.

Here's a diff that makes it look better:

```diff
diff --git a/crates/gpui/examples/window_shadow.rs b/crates/gpui/examples/window_shadow.rs
index 122231f6b..7fa29dadc 100644
--- a/crates/gpui/examples/window_shadow.rs
+++ b/crates/gpui/examples/window_shadow.rs
@@ -72,8 +72,8 @@ impl Render for WindowShadow {
                     .when(!(tiling.top || tiling.left), |div| div.rounded_tl(rounding))
                     .when(!tiling.top, |div| div.pt(shadow_size))
                     .when(!tiling.bottom, |div| div.pb(shadow_size))
-                    .when(!tiling.left, |div| div.pl(shadow_size))
-                    .when(!tiling.right, |div| div.pr(shadow_size))
+                    .when(!tiling.left, |div| div.pl(shadow_size - border_size))
+                    .when(!tiling.right, |div| div.pr(shadow_size - border_size))
                     .on_mouse_move(|_e, cx| cx.refresh())
                     .on_mouse_down(MouseButton::Left, move |e, cx| {
                         let size = cx.window_bounds().get_bounds().size;
```

But that makes it look weird on Wayland, so I didn't do it.

I think it's fine for now. Chromium looks bad and has a gap, so we're
already better.

## Before

![before_1](https://github.com/zed-industries/zed/assets/1185253/875c5cdd-c0be-4295-beb0-bb9ba5beaa52)


![before_2](https://github.com/zed-industries/zed/assets/1185253/0b96be70-4c34-4e99-aeb2-ab741171ad14)

## After

![after_1](https://github.com/zed-industries/zed/assets/1185253/aa51da77-daf1-4ef8-a33f-a83731e0c7e1)

![after_2](https://github.com/zed-industries/zed/assets/1185253/8ce7902d-90b6-4f06-ba2c-626e643abe56)


Release Notes:

- N/A
2024-07-08 21:47:08 -06:00
Thorsten Ball
e281abcc5f linux: Set directory in SaveFileRequest dialog (#13850)
This has been bugging me for a while. If you create a new file and then
save it, the dialogue would show the home directory and not the folder
that you were in.

This fixes it.

Release Notes:

- N/A
2024-07-08 21:46:38 -06:00
Thorsten Ball
1e52998493 linux/x11: Set transparency to false by default (#13848)
The previous code lead to a ton of error messages from Blade on my X11
machine, even with *client-side decorations working well!*

As @someone13574 pointed out
[here](https://github.com/zed-industries/zed/pull/13611#issuecomment-2201685030)
things still work with this being false. And if someone changes a theme
with transparent background, then it will get set anyway. We just don't
do it by default.

And as @jansol pointed out
[here](https://github.com/zed-industries/zed/issues/5040#issuecomment-2096560629):

> On X11 it may be possible to configure a compositor to blur window
backgrounds but Zed has no way to influence that.

So we don't lose anything, I think, but get rid of a ton of error
messages in the logs.

Proof of shadows etc. still working:

![Screenshot from 2024-07-05
10-17-38](https://github.com/zed-industries/zed/assets/1185253/1216b38b-8011-46e7-b86f-c0f5fc3f6f64)



Release Notes:

- N/A
2024-07-08 21:46:29 -06:00
Owen Law
d6d75d2a54 linux/x11: Fix bugs related to unflushed commands (#13844)
**Edit**:

This PR adds flushes to functions which should have an immediate affect.
I've observed it fixing the following bugs (but there are probably
more):

- The cursor not updating when just hovering.
- The window not maximising after clicking the full-screen button until
you move the mouse.
- The window not minimising after clicking the minimise button until you
move the mouse.

---

**Original content**:

Following #13646, the cursor style wouldn't change because the
`change_window_attributes` command wasn't being flushed. I guess it was
working before because something else was flushing it somewhere else so
it was never noticed.

I just added `check()` which flushes the command so that the cursor will
actually update when just hovering. Before you would need to interact
with the window so that something else could flush the command.

Release Notes:

- N/A
2024-07-08 21:46:21 -06:00
Thorsten Ball
ae7d49a06b linux/x11: Resize on GTK_EDGE_CONSTRAINTS atom (#13833)
With the new window decorations resizing was _really_ laggy on my X11
machine.

Before:
- Click on window border (hitbox doesn't work properly, but that's
another issue)
- Drag and resize
- 4-5s nothing happens
- Window is resized

After:
- Click on window border
- Drag and resize
- Window is resized

I'm still not 100% sure on why this happens on my machine and not
Conrad's/Mikayla's, but seems like that GTK_EDGE_CONSTRAINTS atom is
sent when resizing.

The other thing that I can't explain is that we get a `ConfigureNotify`
when resizing, with the right size, but maybe not often enough?

Anyway, for now we'll go with this.

Release Notes:

- N/A
2024-07-08 21:46:12 -06:00
Keenan Wresch
daee260300 Allow Shift + Scroll to Horizontally Scroll in X11 and Wayland (#13676)
Release Notes:

- Allows shift + scroll horizontal scrolling on X11 and Wayland.

[Screencast from 2024-06-29
17-17-59.webm](https://github.com/zed-industries/zed/assets/14155062/2cac77b9-ecc8-4ddb-b08d-b5d964c8dc84)
2024-07-08 21:46:02 -06:00
Kirill Bulatov
01902eeeda Explicitly specify php files' formatter for prettier (#13883)
Closes https://github.com/zed-industries/zed/issues/13878

Seems that all regular files types are being recognized by Prettier from
their paths, but the PHP one does not despite the PHP plugin used when
formatting.


Release Notes:

- Fixed PHP prettier formatting, by including `php` parser name into
formatting queries
([13878](https://github.com/zed-industries/zed/issues/13878))
2024-07-08 11:51:57 +03:00
Peter Tripp
ae696c8582 zed 0.143.2 2024-07-05 11:19:02 -04:00
Peter Tripp
9b84af1af4 Fix delay when changing scrolling direction (#13867)
Fixes: #13720.

Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2024-07-05 11:18:34 -04:00
Zed Bot
29a5111ed9 Bump to 0.143.1 for @ConradIrwin 2024-07-03 15:11:26 -07:00
Mikayla Maki
dbf29656b8 fix duplicated code 2024-07-03 13:18:16 -06:00
Mikayla Maki
3eec525351 Linux window decorations (#13611)
This PR adds support for full client side decorations on X11 and Wayland

TODO:
- [x] Adjust GPUI APIs to expose CSD related information
- [x] Implement remaining CSD features (Resizing, window border, window
shadow)
- [x] Integrate with existing background appearance and window
transparency
- [x] Figure out how to check if the window is tiled on X11
- [x] Implement in Zed
- [x] Repeatedly maximizing and unmaximizing can panic
- [x] Resizing is strangely slow
- [x] X11 resizing and movement doesn't work for this:
https://discord.com/channels/869392257814519848/1204679850208657418/1256816908519604305
- [x] The top corner can clip with current styling
- [x] Pressing titlebar buttons doesn't work
- [x] Not showing maximize / unmaximize buttons
- [x] Noisy transparency logs / surface transparency problem
https://github.com/zed-industries/zed/pull/13611#issuecomment-2201685030
- [x] Strange offsets when dragging the project panel
https://github.com/zed-industries/zed/pull/13611#pullrequestreview-2154606261
- [x] Shadow inset with `_GTK_FRAME_EXTENTS` doesn't respect tiling on
X11 (observe by snapping an X11 window in any direction)

Release Notes:

- N/A

---------

Co-authored-by: conrad <conrad@zed.dev>
Co-authored-by: Owen Law <81528246+someone13574@users.noreply.github.com>
Co-authored-by: apricotbucket28 <71973804+apricotbucket28@users.noreply.github.com>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-07-03 13:17:57 -06:00
Conrad Irwin
c59098f07e Linux builds on stable (#13744)
Release Notes:

- First beta version of Linux
2024-07-03 13:17:44 -06:00
Peter Tripp
443f423096 v0.143.x preview 2024-07-03 12:15:06 -04:00
50 changed files with 2808 additions and 1129 deletions

View File

@@ -402,7 +402,7 @@ jobs:
- name: Upload app bundle to release
uses: softprops/action-gh-release@v1
if: ${{ env.RELEASE_CHANNEL == 'preview' }}
if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
with:
draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}

58
Cargo.lock generated
View File

@@ -341,9 +341,8 @@ dependencies = [
[[package]]
name = "ashpd"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd884d7c72877a94102c3715f3b1cd09ff4fac28221add3e57cfbe25c236d093"
version = "0.9.0"
source = "git+https://github.com/bilelmoussaoui/ashpd?rev=29f2e1a#29f2e1a6f4b0911f504658f5f4630c02e01b13f2"
dependencies = [
"async-fs 2.1.1",
"async-net 2.0.0",
@@ -4889,11 +4888,9 @@ dependencies = [
"log",
"media",
"metal",
"mio 1.0.0",
"num_cpus",
"objc",
"oo7",
"open",
"parking",
"parking_lot",
"pathfinder_geometry",
@@ -5691,7 +5688,7 @@ dependencies = [
"fnv",
"lazy_static",
"libc",
"mio 0.8.11",
"mio",
"rand 0.8.5",
"serde",
"tempfile",
@@ -5705,25 +5702,6 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6"
[[package]]
name = "is-docker"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
dependencies = [
"once_cell",
]
[[package]]
name = "is-wsl"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
dependencies = [
"is-docker",
"once_cell",
]
[[package]]
name = "isahc"
version = "1.7.2"
@@ -6640,19 +6618,6 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "mio"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4929e1f84c5e54c3ec6141cd5d8b5a5c055f031f80cf78f2072920173cb4d880"
dependencies = [
"hermit-abi 0.3.9",
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0",
]
[[package]]
name = "miow"
version = "0.6.0"
@@ -6891,7 +6856,7 @@ dependencies = [
"kqueue",
"libc",
"log",
"mio 0.8.11",
"mio",
"walkdir",
"windows-sys 0.48.0",
]
@@ -7225,17 +7190,6 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "open"
version = "5.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "449f0ff855d85ddbf1edd5b646d65249ead3f5e422aaa86b7d2d0b049b103e32"
dependencies = [
"is-wsl",
"libc",
"pathdiff",
]
[[package]]
name = "open_ai"
version = "0.1.0"
@@ -11112,7 +11066,7 @@ dependencies = [
"backtrace",
"bytes 1.5.0",
"libc",
"mio 0.8.11",
"mio",
"num_cpus",
"parking_lot",
"pin-project-lite",
@@ -13607,7 +13561,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.143.0"
version = "0.143.4"
dependencies = [
"activity_indicator",
"anyhow",

View File

@@ -272,9 +272,9 @@ zed_actions = { path = "crates/zed_actions" }
alacritty_terminal = "0.23"
any_vec = "0.13"
anyhow = "1.0.57"
ashpd = "0.8.0"
ashpd = { git = "https://github.com/bilelmoussaoui/ashpd", rev = "29f2e1a" }
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
async-dispatcher = { version = "0.1"}
async-dispatcher = { version = "0.1" }
async-fs = "1.6"
async-recursion = "1.0.0"
async-tar = "0.4.2"
@@ -315,7 +315,9 @@ image = "0.25.1"
indexmap = { version = "1.6.2", features = ["serde"] }
indoc = "1"
# We explicitly disable http2 support in isahc.
isahc = { version = "1.7.2", default-features = false, features = [ "text-decoding" ] }
isahc = { version = "1.7.2", default-features = false, features = [
"text-decoding",
] }
itertools = "0.11.0"
lazy_static = "1.4.0"
libc = "0.2"
@@ -341,7 +343,9 @@ rand = "0.8.5"
refineable = { path = "./crates/refineable" }
regex = "1.5"
repair_json = "0.1.0"
runtimelib = { version="0.12", default-features = false, features = ["async-dispatcher-runtime"] }
runtimelib = { version = "0.12", default-features = false, features = [
"async-dispatcher-runtime",
] }
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
rust-embed = { version = "8.4", features = ["include-exclude"] }
schemars = "0.8"

View File

@@ -3,10 +3,14 @@
{
"bindings": {
"up": "menu::SelectPrev",
"shift-tab": "menu::SelectPrev",
"home": "menu::SelectFirst",
"pageup": "menu::SelectFirst",
"shift-pageup": "menu::SelectFirst",
"ctrl-p": "menu::SelectPrev",
"down": "menu::SelectNext",
"tab": "menu::SelectNext",
"end": "menu::SelectLast",
"pagedown": "menu::SelectLast",
"shift-pagedown": "menu::SelectFirst",
"ctrl-n": "menu::SelectNext",
@@ -16,8 +20,18 @@
"ctrl-escape": "menu::Cancel",
"ctrl-c": "menu::Cancel",
"shift-enter": "picker::UseSelectedQuery",
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
"ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }],
"alt-enter": [
"picker::ConfirmInput",
{
"secondary": false
}
],
"ctrl-alt-enter": [
"picker::ConfirmInput",
{
"secondary": true
}
],
"ctrl-shift-w": "workspace::CloseWindow",
"shift-escape": "workspace::ToggleZoom",
"ctrl-o": "workspace::Open",
@@ -148,12 +162,7 @@
"replace_enabled": true
}
],
// "cmd-e": [
// "buffer_search::Deploy",
// {
// "focus": false
// }
// ],
// "cmd-e": ["buffer_search::Deploy", { "focus": false }],
"ctrl->": "assistant::QuoteSelection",
"ctrl-<": "assistant::InsertIntoEditor",
"ctrl-alt-e": "editor::SelectEnclosingSymbol"
@@ -268,6 +277,7 @@
"ctrl-pageup": "pane::ActivatePrevItem",
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-w": "pane::CloseActiveItem",
"ctrl-f4": "pane::CloseActiveItem",
"alt-ctrl-t": "pane::CloseInactiveItems",
"alt-ctrl-shift-w": "workspace::CloseInactiveTabsAndPanes",
"ctrl-k u": "pane::CloseCleanItems",
@@ -349,16 +359,23 @@
"f2": "editor::Rename",
"f12": "editor::GoToDefinition",
"alt-f12": "editor::GoToDefinitionSplit",
"ctrl-shift-f10": "editor::GoToDefinitionSplit",
"ctrl-f12": "editor::GoToTypeDefinition",
"shift-f12": "editor::GoToImplementation",
"alt-ctrl-f12": "editor::GoToTypeDefinitionSplit",
"alt-shift-f12": "editor::FindAllReferences",
"ctrl-m": "editor::MoveToEnclosingBracket",
"ctrl-shift-\\": "editor::MoveToEnclosingBracket",
"ctrl-shift-[": "editor::Fold",
"ctrl-shift-]": "editor::UnfoldLines",
"ctrl-space": "editor::ShowCompletions",
"ctrl-.": "editor::ToggleCodeActions",
"alt-ctrl-r": "editor::RevealInFinder",
"ctrl-k r": "editor::RevealInFinder",
"ctrl-k p": "editor::CopyPath",
"ctrl-\\": "pane::SplitRight",
"ctrl-k v": "markdown::OpenPreviewToTheSide",
"ctrl-shift-v": "markdown::OpenPreview",
"ctrl-alt-shift-c": "editor::DisplayCursorNames"
}
},
@@ -385,6 +402,8 @@
"ctrl-alt--": "pane::GoBack",
"ctrl-alt-_": "pane::GoForward",
"ctrl-shift-t": "pane::ReopenClosedItem",
"f3": "search::SelectNextMatch",
"shift-f3": "search::SelectPrevMatch",
"ctrl-shift-f": "project_search::ToggleFocus"
}
},
@@ -392,12 +411,7 @@
"context": "Workspace",
"bindings": {
// Change the default action on `menu::Confirm` by setting the parameter
// "alt-cmd-o": [
// "projects::OpenRecent",
// {
// "create_new_window": true
// }
// ]
// "alt-ctrl-o": ["projects::OpenRecent", { "create_new_window": true }],
"alt-ctrl-o": "projects::OpenRecent",
"alt-ctrl-shift-b": "branches::OpenRecent",
"ctrl-~": "workspace::NewTerminal",
@@ -432,9 +446,15 @@
"ctrl-shift-t": "project_symbols::Toggle",
"ctrl-p": "file_finder::Toggle",
"ctrl-tab": "tab_switcher::Toggle",
"ctrl-shift-tab": ["tab_switcher::Toggle", { "select_last": true }],
"ctrl-shift-tab": [
"tab_switcher::Toggle",
{
"select_last": true
}
],
"ctrl-e": "file_finder::Toggle",
"ctrl-shift-p": "command_palette::Toggle",
"f1": "command_palette::Toggle",
"ctrl-shift-m": "diagnostics::Deploy",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-b": "outline_panel::ToggleFocus",
@@ -450,6 +470,7 @@
"ctrl-k shift-right": ["workspace::SwapPaneInDirection", "Right"],
"ctrl-k shift-up": ["workspace::SwapPaneInDirection", "Up"],
"ctrl-k shift-down": ["workspace::SwapPaneInDirection", "Down"],
"ctrl-shift-x": "zed::Extensions",
"alt-t": "task::Rerun",
"alt-shift-t": "task::Spawn"
}
@@ -466,7 +487,7 @@
"ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",
"ctrl-alt-d": "editor::DeleteToNextSubwordEnd",
"ctrl-alt-left": "editor::MoveToPreviousSubwordStart",
"ctrl-alt-b": "editor::MoveToPreviousSubwordStart",
// "ctrl-alt-b": "editor::MoveToPreviousSubwordStart",
"ctrl-alt-right": "editor::MoveToNextSubwordEnd",
"ctrl-alt-f": "editor::MoveToNextSubwordEnd",
"ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart",
@@ -591,11 +612,36 @@
"alt-ctrl-shift-c": "project_panel::CopyRelativePath",
"f2": "project_panel::Rename",
"enter": "project_panel::Rename",
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
"shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
"delete": ["project_panel::Trash", { "skip_prompt": false }],
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
"backspace": [
"project_panel::Trash",
{
"skip_prompt": false
}
],
"shift-delete": [
"project_panel::Delete",
{
"skip_prompt": false
}
],
"delete": [
"project_panel::Trash",
{
"skip_prompt": false
}
],
"ctrl-backspace": [
"project_panel::Delete",
{
"skip_prompt": false
}
],
"ctrl-delete": [
"project_panel::Delete",
{
"skip_prompt": false
}
],
"alt-ctrl-r": "project_panel::RevealInFinder",
"alt-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
@@ -636,7 +682,9 @@
},
{
"context": "FileFinder",
"bindings": { "ctrl-shift-p": "file_finder::SelectPrev" }
"bindings": {
"ctrl-shift-p": "file_finder::SelectPrev"
}
},
{
"context": "TabSwitcher",

View File

@@ -3,10 +3,14 @@
{
"bindings": {
"up": "menu::SelectPrev",
"shift-tab": "menu::SelectPrev",
"home": "menu::SelectFirst",
"pageup": "menu::SelectFirst",
"shift-pageup": "menu::SelectFirst",
"ctrl-p": "menu::SelectPrev",
"down": "menu::SelectNext",
"tab": "menu::SelectNext",
"end": "menu::SelectLast",
"pagedown": "menu::SelectLast",
"shift-pagedown": "menu::SelectFirst",
"ctrl-n": "menu::SelectNext",
@@ -285,6 +289,7 @@
"context": "ProjectSearchBar",
"bindings": {
"escape": "project_search::ToggleFocus",
"cmd-shift-j": "project_search::ToggleFilters",
"cmd-shift-f": "search::FocusSearch",
"cmd-shift-h": "search::ToggleReplace",
"alt-cmd-g": "search::ToggleRegex",
@@ -309,6 +314,7 @@
"context": "ProjectSearchView",
"bindings": {
"escape": "project_search::ToggleFocus",
"cmd-shift-j": "project_search::ToggleFilters",
"cmd-shift-h": "search::ToggleReplace",
"alt-cmd-g": "search::ToggleRegex",
"alt-cmd-x": "search::ToggleRegex"
@@ -400,11 +406,17 @@
"alt-cmd-f12": "editor::GoToTypeDefinitionSplit",
"alt-shift-f12": "editor::FindAllReferences",
"ctrl-m": "editor::MoveToEnclosingBracket",
"cmd-shift-\\": "editor::MoveToEnclosingBracket",
"alt-cmd-[": "editor::Fold",
"alt-cmd-]": "editor::UnfoldLines",
"ctrl-space": "editor::ShowCompletions",
"cmd-.": "editor::ToggleCodeActions",
"alt-cmd-r": "editor::RevealInFinder",
"cmd-k r": "editor::RevealInFinder",
"cmd-k p": "editor::CopyPath",
"cmd-\\": "pane::SplitRight",
"cmd-k v": "markdown::OpenPreviewToTheSide",
"cmd-shift-v": "markdown::OpenPreview",
"ctrl-cmd-c": "editor::DisplayCursorNames"
}
},
@@ -495,6 +507,7 @@
"cmd-k shift-right": ["workspace::SwapPaneInDirection", "Right"],
"cmd-k shift-up": ["workspace::SwapPaneInDirection", "Up"],
"cmd-k shift-down": ["workspace::SwapPaneInDirection", "Down"],
"cmd-shift-x": "zed::Extensions",
"alt-t": "task::Rerun",
"alt-shift-t": "task::Spawn"
}
@@ -619,6 +632,7 @@
"cmd-alt-c": "project_panel::CopyPath",
"alt-cmd-shift-c": "project_panel::CopyRelativePath",
"enter": "project_panel::Rename",
"f2": "project_panel::Rename",
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
"delete": ["project_panel::Trash", { "skip_prompt": false }],
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }],
@@ -707,10 +721,14 @@
"escape": ["terminal::SendKeystroke", "escape"],
"enter": ["terminal::SendKeystroke", "enter"],
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
"cmd-up": "terminal::ScrollPageUp",
"cmd-down": "terminal::ScrollPageDown",
"shift-pageup": "terminal::ScrollPageUp",
"shift-pagedown": "terminal::ScrollPageDown",
"shift-up": "terminal::ScrollLineUp",
"shift-down": "terminal::ScrollLineDown",
"cmd-home": "terminal::ScrollToTop",
"cmd-end": "terminal::ScrollToBottom",
"shift-home": "terminal::ScrollToTop",
"shift-end": "terminal::ScrollToBottom"
}

View File

@@ -0,0 +1,93 @@
// Default Keymap (Atom) for Zed on Linux
[
{
"bindings": {
"ctrl-shift-f5": "workspace::Reload", // window:reload
"ctrl-k ctrl-n": "workspace::ActivatePreviousPane", // window:focus-next-pane
"ctrl-k ctrl-p": "workspace::ActivateNextPane" // window:focus-previous-pane
}
},
{
"context": "Editor",
"bindings": {
"ctrl-shift-l": "language_selector::Toggle", // grammar-selector:show
"ctrl-|": "pane::RevealInProjectPanel", // tree-view:reveal-active-file
"ctrl-b": "editor::GoToDefinition", // fuzzy-finder:toggle-buffer-finder
"ctrl-alt-b": "editor::GoToDefinitionSplit", // N/A: From JetBrains
"ctrl-<": "editor::ScrollCursorCenter", // editor:scroll-to-cursor
"f3": ["editor::SelectNext", { "replace_newest": true }], // find-and-replace:find-next
"shift-f3": ["editor::SelectPrevious", { "replace_newest": true }], //find-and-replace:find-previous
"alt-shift-down": "editor::AddSelectionBelow", // editor:add-selection-below
"alt-shift-up": "editor::AddSelectionAbove", // editor:add-selection-above
"ctrl-k ctrl-u": "editor::ConvertToUpperCase", // editor:upper-case
"ctrl-k ctrl-l": "editor::ConvertToLowerCase", // editor:lower-case
"ctrl-j": "editor::JoinLines", // editor:join-lines
"ctrl-shift-d": "editor::DuplicateLineDown", // editor:duplicate-lines
"ctrl-up": "editor::MoveLineUp", // editor:move-line-up
"ctrl-down": "editor::MoveLineDown", // editor:move-line-down
"ctrl-shift-m": "markdown::OpenPreviewToTheSide" // markdown-preview:toggle
}
},
{
"context": "Editor && mode == full",
"bindings": {
"ctrl-r": "outline::Toggle" // symbols-view:toggle-project-symbols
}
},
{
"context": "BufferSearchBar",
"bindings": {
"ctrl-f3": "search::SelectNextMatch", // find-and-replace:find-next-selected
"ctrl-shift-f3": "search::SelectPrevMatch" // find-and-replace:find-previous-selected
}
},
{
"context": "Workspace",
"bindings": {
"ctrl-\\": "workspace::ToggleLeftDock", // tree-view:toggle
"ctrl-k ctrl-b": "workspace::ToggleLeftDock", // tree-view:toggle
"ctrl-t": "file_finder::Toggle", // fuzzy-finder:toggle-file-finder
"ctrl-r": "project_symbols::Toggle" // symbols-view:toggle-project-symbols
}
},
{
"context": "Pane",
"bindings": {
// "ctrl-0": "project_panel::ToggleFocus", // tree-view:toggle-focus
"ctrl-1": ["pane::ActivateItem", 0], // tree-view:open-selected-entry-in-pane-1
"ctrl-2": ["pane::ActivateItem", 1], // tree-view:open-selected-entry-in-pane-2
"ctrl-3": ["pane::ActivateItem", 2], // tree-view:open-selected-entry-in-pane-3
"ctrl-4": ["pane::ActivateItem", 3], // tree-view:open-selected-entry-in-pane-4
"ctrl-5": ["pane::ActivateItem", 4], // tree-view:open-selected-entry-in-pane-5
"ctrl-6": ["pane::ActivateItem", 5], // tree-view:open-selected-entry-in-pane-6
"ctrl-7": ["pane::ActivateItem", 6], // tree-view:open-selected-entry-in-pane-7
"ctrl-8": ["pane::ActivateItem", 7], // tree-view:open-selected-entry-in-pane-8
"ctrl-9": ["pane::ActivateItem", 8] // tree-view:open-selected-entry-in-pane-9
}
},
{
"context": "ProjectPanel",
"bindings": {
"f2": "project_panel::Rename", // tree-view:rename
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
"ctrl-x": "project_panel::Cut", // tree-view:cut
"ctrl-c": "project_panel::Copy", // tree-view:copy
"ctrl-v": "project_panel::Paste" // tree-view:paste
}
},
{
"context": "ProjectPanel && not_editing",
"bindings": {
"ctrl-shift-c": "project_panel::CopyPath", // tree-view:copy-full-path
"ctrl-[": "project_panel::CollapseSelectedEntry", // tree-view:collapse-directory
"ctrl-b": "project_panel::CollapseSelectedEntry", // tree-view:collapse-directory
"ctrl-]": "project_panel::ExpandSelectedEntry", // tree-view:expand-item
"ctrl-f": "project_panel::ExpandSelectedEntry", // tree-view:expand-item
"a": "project_panel::NewFile", // tree-view:add-file
"d": "project_panel::Duplicate", // tree-view:duplicate
"home": "menu::SelectFirst", // core:move-to-top
"end": "menu::SelectLast", // core:move-to-bottom
"shift-a": "project_panel::NewDirectory" // tree-view:add-folder
}
}
]

View File

@@ -0,0 +1,90 @@
[
{
"bindings": {
"ctrl-shift-[": "pane::ActivatePrevItem",
"ctrl-shift-]": "pane::ActivateNextItem"
}
},
{
"context": "Editor",
"bindings": {
"ctrl->": "zed::IncreaseBufferFontSize",
"ctrl-<": "zed::DecreaseBufferFontSize",
"ctrl-shift-j": "editor::JoinLines",
"ctrl-d": "editor::DuplicateLineDown",
"ctrl-y": "editor::DeleteLine",
"ctrl-pagedown": "editor::MovePageDown",
"ctrl-pageup": "editor::MovePageUp",
// "ctrl-alt-shift-b": "editor::SelectToPreviousWordStart",
"ctrl-alt-enter": "editor::NewlineAbove",
"shift-enter": "editor::NewlineBelow",
// "ctrl--": "editor::Fold", // TODO: `ctrl-numpad--` (numpad not implemented)
// "ctrl-+": "editor::UnfoldLines", // TODO: `ctrl-numpad+` (numpad not implemented)
"alt-shift-g": "editor::SplitSelectionIntoLines",
"alt-j": ["editor::SelectNext", { "replace_newest": false }],
"alt-shift-j": ["editor::SelectPrevious", { "replace_newest": false }],
"ctrl-/": ["editor::ToggleComments", { "advance_downwards": true }],
"alt-up": "editor::SelectLargerSyntaxNode",
"alt-down": "editor::SelectSmallerSyntaxNode",
"shift-alt-up": "editor::MoveLineUp",
"shift-alt-down": "editor::MoveLineDown",
"ctrl-alt-l": "editor::Format",
"shift-f6": "editor::Rename",
"ctrl-alt-left": "pane::GoBack",
"ctrl-alt-right": "pane::GoForward",
"alt-f7": "editor::FindAllReferences",
"ctrl-alt-f7": "editor::FindAllReferences",
// "ctrl-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock
// "ctrl-alt-b": "editor::GoToDefinitionSplit", // Conflicts with workspace::ToggleLeftDock
"ctrl-shift-b": "editor::GoToTypeDefinition",
"ctrl-alt-shift-b": "editor::GoToTypeDefinitionSplit",
"f2": "editor::GoToDiagnostic",
"shift-f2": "editor::GoToPrevDiagnostic",
"ctrl-alt-shift-down": "editor::GoToHunk",
"ctrl-alt-shift-up": "editor::GoToPrevHunk",
"ctrl-home": "editor::MoveToBeginning",
"ctrl-end": "editor::MoveToEnd",
"ctrl-shift-home": "editor::SelectToBeginning",
"ctrl-shift-end": "editor::SelectToEnd"
}
},
{
"context": "Editor && mode == full",
"bindings": {
"ctrl-f12": "outline::Toggle",
"alt-7": "outline::Toggle",
"ctrl-shift-n": "file_finder::Toggle",
"ctrl-g": "go_to_line::Toggle",
"alt-enter": "editor::ToggleCodeActions"
}
},
{
"context": "Workspace",
"bindings": {
"ctrl-shift-n": "file_finder::Toggle",
"ctrl-shift-a": "command_palette::Toggle",
"shift shift": "command_palette::Toggle",
"ctrl-alt-shift-n": "project_symbols::Toggle",
"alt-1": "workspace::ToggleLeftDock",
"ctrl-e": "tab_switcher::Toggle",
"alt-6": "diagnostics::Deploy"
}
},
{
"context": "Pane",
"bindings": {
"ctrl-alt-left": "pane::GoBack",
"ctrl-alt-right": "pane::GoForward"
}
},
{
"context": "ProjectPanel",
"bindings": {
"enter": "project_panel::Open",
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
"delete": ["project_panel::Trash", { "skip_prompt": false }],
"shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
"shift-f6": "project_panel::Rename"
}
}
]

View File

@@ -0,0 +1,53 @@
[
{
"bindings": {
"ctrl-shift-[": "pane::ActivatePrevItem",
"ctrl-shift-]": "pane::ActivateNextItem",
"ctrl-pagedown": "pane::ActivatePrevItem",
"ctrl-pageup": "pane::ActivateNextItem",
"ctrl-shift-tab": "pane::ActivateNextItem",
"ctrl-tab": "pane::ActivatePrevItem"
}
},
{
"context": "Editor",
"bindings": {
"ctrl-shift-up": "editor::AddSelectionAbove",
"ctrl-shift-down": "editor::AddSelectionBelow",
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
"ctrl-shift-l": "editor::SplitSelectionIntoLines",
"ctrl-shift-a": "editor::SelectLargerSyntaxNode",
"ctrl-shift-d": "editor::DuplicateLineDown",
"f12": "editor::GoToDefinition",
"ctrl-f12": "editor::GoToDefinitionSplit",
"shift-f12": "editor::FindAllReferences",
"ctrl-shift-f12": "editor::FindAllReferences",
"ctrl-.": "editor::GoToHunk",
"ctrl-,": "editor::GoToPrevHunk",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd"
}
},
{
"context": "Editor && mode == full",
"bindings": {
"ctrl-r": "outline::Toggle"
}
},
{
"context": "Pane",
"bindings": {
"f4": "search::SelectNextMatch",
"shift-f4": "search::SelectPrevMatch"
}
},
{
"context": "Workspace",
"bindings": {
"ctrl-k ctrl-b": "workspace::ToggleLeftDock",
// "ctrl-0": "project_panel::ToggleFocus", // normally resets zoom
"shift-ctrl-r": "project_symbols::Toggle"
}
}
]

View File

@@ -1,6 +1,8 @@
// Default Keymap (Atom) for Zed on MacOS
[
{
"bindings": {
"ctrl-alt-cmd-l": "workspace::Reload",
"cmd-k cmd-p": "workspace::ActivatePreviousPane",
"cmd-k cmd-n": "workspace::ActivateNextPane"
}
@@ -8,24 +10,23 @@
{
"context": "Editor",
"bindings": {
"ctrl-shift-l": "language_selector::Toggle",
"cmd-|": "pane::RevealInProjectPanel",
"cmd-b": "editor::GoToDefinition",
"alt-cmd-b": "editor::GoToDefinitionSplit",
"cmd-<": "editor::ScrollCursorCenter",
"cmd-g": [
"editor::SelectNext",
{
"replace_newest": true
}
],
"ctrl-cmd-g": [
"editor::SelectPrevious",
{
"replace_newest": true
}
],
"cmd-g": ["editor::SelectNext", { "replace_newest": true }],
"cmd-shift-g": ["editor::SelectPrevious", { "replace_newest": true }],
"ctrl-shift-down": "editor::AddSelectionBelow",
"ctrl-shift-up": "editor::AddSelectionAbove",
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine",
"cmd-k cmd-u": "editor::ConvertToUpperCase",
"cmd-k cmd-l": "editor::ConvertToLowerCase",
"alt-enter": "editor::Newline",
"cmd-j": "editor::JoinLines",
"cmd-shift-d": "editor::DuplicateLineDown",
"ctrl-cmd-up": "editor::MoveLineUp",
"ctrl-cmd-down": "editor::MoveLineDown",
"ctrl-shift-m": "markdown::OpenPreviewToTheSide"
}
},
@@ -74,21 +75,22 @@
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
"cmd-x": "project_panel::Cut",
"cmd-c": "project_panel::Copy",
"cmd-v": "project_panel::Paste",
"ctrl-[": "project_panel::CollapseSelectedEntry",
"ctrl-b": "project_panel::CollapseSelectedEntry",
"alt-b": "project_panel::CollapseSelectedEntry",
"ctrl-]": "project_panel::ExpandSelectedEntry",
"ctrl-f": "project_panel::ExpandSelectedEntry",
"ctrl-shift-c": "project_panel::CopyPath"
"cmd-v": "project_panel::Paste"
}
},
{
"context": "ProjectPanel && not_editing",
"bindings": {
"ctrl-shift-c": "project_panel::CopyPath",
"ctrl-[": "project_panel::CollapseSelectedEntry",
"ctrl-b": "project_panel::CollapseSelectedEntry",
"ctrl-]": "project_panel::ExpandSelectedEntry",
"ctrl-f": "project_panel::ExpandSelectedEntry",
"a": "project_panel::NewFile",
"shift-a": "project_panel::NewDirectory",
"shift-d": "project_panel::Duplicate"
"d": "project_panel::Duplicate",
"home": "menu::SelectFirst",
"end": "menu::SelectLast",
"shift-a": "project_panel::NewDirectory"
}
}
]

View File

@@ -54,7 +54,7 @@
"cmd-shift-b": "editor::GoToTypeDefinition",
"cmd-alt-shift-b": "editor::GoToTypeDefinitionSplit",
"f2": "editor::GoToDiagnostic",
"cmd-f2": "editor::GoToPrevDiagnostic",
"shift-f2": "editor::GoToPrevDiagnostic",
"ctrl-alt-shift-down": "editor::GoToHunk",
"ctrl-alt-shift-up": "editor::GoToPrevHunk",
"cmd-home": "editor::MoveToBeginning",

View File

@@ -6,8 +6,7 @@
"ctrl-pagedown": "pane::ActivatePrevItem",
"ctrl-pageup": "pane::ActivateNextItem",
"ctrl-shift-tab": "pane::ActivateNextItem",
"ctrl-tab": "pane::ActivatePrevItem",
"cmd-+": "zed::IncreaseBufferFontSize"
"ctrl-tab": "pane::ActivatePrevItem"
}
},
{
@@ -26,6 +25,9 @@
"alt-shift-cmd-down": "editor::FindAllReferences",
"ctrl-.": "editor::GoToHunk",
"ctrl-,": "editor::GoToPrevHunk",
"cmd-k cmd-u": "editor::ConvertToUpperCase",
"cmd-k cmd-l": "editor::ConvertToLowerCase",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd"
}

View File

@@ -775,7 +775,8 @@
"PHP": {
"prettier": {
"allowed": true,
"plugins": ["@prettier/plugin-php"]
"plugins": ["@prettier/plugin-php"],
"parser": "php"
}
},
"Ruby": {

View File

@@ -10,7 +10,7 @@ use std::{rc::Rc, sync::Arc};
pub use collab_panel::CollabPanel;
use gpui::{
point, AppContext, Pixels, PlatformDisplay, Size, WindowBackgroundAppearance, WindowBounds,
WindowKind, WindowOptions,
WindowDecorations, WindowKind, WindowOptions,
};
use panel_settings::MessageEditorSettings;
pub use panel_settings::{
@@ -63,8 +63,9 @@ fn notification_window_options(
kind: WindowKind::PopUp,
is_movable: false,
display_id: Some(screen.id()),
window_background: WindowBackgroundAppearance::default(),
window_background: WindowBackgroundAppearance::Transparent,
app_id: Some(app_id.to_owned()),
window_min_size: None,
window_decorations: Some(WindowDecorations::Client),
}
}

View File

@@ -124,7 +124,6 @@ wayland-protocols = { version = "0.31.2", features = [
] }
wayland-protocols-plasma = { version = "0.2.0", features = ["client"] }
oo7 = "0.3.0"
open = "5.1.2"
filedescriptor = "0.8.2"
x11rb = { version = "0.13.0", features = [
"allow-unsafe-code",
@@ -133,6 +132,7 @@ x11rb = { version = "0.13.0", features = [
"xinput",
"cursor",
"resource_manager",
"sync",
] }
xkbcommon = { version = "0.7", features = ["wayland", "x11"] }
xim = { git = "https://github.com/npmania/xim-rs", rev = "27132caffc5b9bc9c432ca4afad184ab6e7c16af", features = [
@@ -141,7 +141,6 @@ xim = { git = "https://github.com/npmania/xim-rs", rev = "27132caffc5b9bc9c432ca
] }
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "5a5c4d4", features = ["source-fontconfig-dlopen"] }
x11-clipboard = "0.9.2"
mio = { version = "1.0.0", features = ["os-poll", "os-ext"] }
[target.'cfg(windows)'.dependencies]
windows.workspace = true
@@ -160,6 +159,10 @@ path = "examples/image/image.rs"
name = "set_menus"
path = "examples/set_menus.rs"
[[example]]
name = "window_shadow"
path = "examples/window_shadow.rs"
[[example]]
name = "input"
path = "examples/input.rs"

View File

@@ -23,7 +23,7 @@ impl Render for HelloWorld {
fn main() {
App::new().run(|cx: &mut AppContext| {
let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx);
let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),

View File

@@ -52,6 +52,7 @@ fn main() {
is_movable: false,
app_id: None,
window_min_size: None,
window_decorations: None,
}
};

View File

@@ -0,0 +1,222 @@
use gpui::*;
use prelude::FluentBuilder;
struct WindowShadow {}
/*
Things to do:
1. We need a way of calculating which edge or corner the mouse is on,
and then dispatch on that
2. We need to improve the shadow rendering significantly
3. We need to implement the techniques in here in Zed
*/
impl Render for WindowShadow {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let decorations = cx.window_decorations();
let rounding = px(10.0);
let shadow_size = px(10.0);
let border_size = px(1.0);
let grey = rgb(0x808080);
cx.set_client_inset(shadow_size);
div()
.id("window-backdrop")
.bg(transparent_black())
.map(|div| match decorations {
Decorations::Server => div,
Decorations::Client { tiling, .. } => div
.bg(gpui::transparent_black())
.child(
canvas(
|_bounds, cx| {
cx.insert_hitbox(
Bounds::new(
point(px(0.0), px(0.0)),
cx.window_bounds().get_bounds().size,
),
false,
)
},
move |_bounds, hitbox, cx| {
let mouse = cx.mouse_position();
let size = cx.window_bounds().get_bounds().size;
let Some(edge) = resize_edge(mouse, shadow_size, size) else {
return;
};
cx.set_cursor_style(
match edge {
ResizeEdge::Top | ResizeEdge::Bottom => {
CursorStyle::ResizeUpDown
}
ResizeEdge::Left | ResizeEdge::Right => {
CursorStyle::ResizeLeftRight
}
ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
CursorStyle::ResizeUpLeftDownRight
}
ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
CursorStyle::ResizeUpRightDownLeft
}
},
&hitbox,
);
},
)
.size_full()
.absolute(),
)
.when(!(tiling.top || tiling.right), |div| {
div.rounded_tr(rounding)
})
.when(!(tiling.top || tiling.left), |div| div.rounded_tl(rounding))
.when(!tiling.top, |div| div.pt(shadow_size))
.when(!tiling.bottom, |div| div.pb(shadow_size))
.when(!tiling.left, |div| div.pl(shadow_size))
.when(!tiling.right, |div| div.pr(shadow_size))
.on_mouse_move(|_e, cx| cx.refresh())
.on_mouse_down(MouseButton::Left, move |e, cx| {
let size = cx.window_bounds().get_bounds().size;
let pos = e.position;
match resize_edge(pos, shadow_size, size) {
Some(edge) => cx.start_window_resize(edge),
None => cx.start_window_move(),
};
}),
})
.size_full()
.child(
div()
.cursor(CursorStyle::Arrow)
.map(|div| match decorations {
Decorations::Server => div,
Decorations::Client { tiling } => div
.border_color(grey)
.when(!(tiling.top || tiling.right), |div| {
div.rounded_tr(rounding)
})
.when(!(tiling.top || tiling.left), |div| div.rounded_tl(rounding))
.when(!tiling.top, |div| div.border_t(border_size))
.when(!tiling.bottom, |div| div.border_b(border_size))
.when(!tiling.left, |div| div.border_l(border_size))
.when(!tiling.right, |div| div.border_r(border_size))
.when(!tiling.is_tiled(), |div| {
div.shadow(smallvec::smallvec![gpui::BoxShadow {
color: Hsla {
h: 0.,
s: 0.,
l: 0.,
a: 0.4,
},
blur_radius: shadow_size / 2.,
spread_radius: px(0.),
offset: point(px(0.0), px(0.0)),
}])
}),
})
.on_mouse_move(|_e, cx| {
cx.stop_propagation();
})
.bg(gpui::rgb(0xCCCCFF))
.size_full()
.flex()
.flex_col()
.justify_around()
.child(
div().w_full().flex().flex_row().justify_around().child(
div()
.flex()
.bg(white())
.size(Length::Definite(Pixels(300.0).into()))
.justify_center()
.items_center()
.shadow_lg()
.border_1()
.border_color(rgb(0x0000ff))
.text_xl()
.text_color(rgb(0xffffff))
.child(
div()
.id("hello")
.w(px(200.0))
.h(px(100.0))
.bg(green())
.shadow(smallvec::smallvec![gpui::BoxShadow {
color: Hsla {
h: 0.,
s: 0.,
l: 0.,
a: 1.0,
},
blur_radius: px(20.0),
spread_radius: px(0.0),
offset: point(px(0.0), px(0.0)),
}])
.map(|div| match decorations {
Decorations::Server => div,
Decorations::Client { .. } => div
.on_mouse_down(MouseButton::Left, |_e, cx| {
cx.start_window_move();
})
.on_click(|e, cx| {
if e.down.button == MouseButton::Right {
cx.show_window_menu(e.up.position);
}
})
.text_color(black())
.child("this is the custom titlebar"),
}),
),
),
),
)
}
}
fn resize_edge(pos: Point<Pixels>, shadow_size: Pixels, size: Size<Pixels>) -> Option<ResizeEdge> {
let edge = if pos.y < shadow_size && pos.x < shadow_size {
ResizeEdge::TopLeft
} else if pos.y < shadow_size && pos.x > size.width - shadow_size {
ResizeEdge::TopRight
} else if pos.y < shadow_size {
ResizeEdge::Top
} else if pos.y > size.height - shadow_size && pos.x < shadow_size {
ResizeEdge::BottomLeft
} else if pos.y > size.height - shadow_size && pos.x > size.width - shadow_size {
ResizeEdge::BottomRight
} else if pos.y > size.height - shadow_size {
ResizeEdge::Bottom
} else if pos.x < shadow_size {
ResizeEdge::Left
} else if pos.x > size.width - shadow_size {
ResizeEdge::Right
} else {
return None;
};
Some(edge)
}
fn main() {
App::new().run(|cx: &mut AppContext| {
let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
window_background: WindowBackgroundAppearance::Opaque,
window_decorations: Some(WindowDecorations::Client),
..Default::default()
},
|cx| {
cx.new_view(|cx| {
cx.observe_window_appearance(|_, cx| {
cx.refresh();
})
.detach();
WindowShadow {}
})
},
)
.unwrap();
});
}

View File

@@ -309,6 +309,16 @@ pub fn transparent_black() -> Hsla {
}
}
/// Transparent black in [`Hsla`]
pub fn transparent_white() -> Hsla {
Hsla {
h: 0.,
s: 0.,
l: 1.,
a: 0.,
}
}
/// Opaque grey in [`Hsla`], values will be clamped to the range [0, 1]
pub fn opaque_grey(lightness: f32, opacity: f32) -> Hsla {
Hsla {

View File

@@ -883,6 +883,14 @@ where
self.size.height = self.size.height.clone() + double_amount;
}
/// inset the bounds by a specified amount
/// Note that this may panic if T does not support negative values
pub fn inset(&self, amount: T) -> Self {
let mut result = self.clone();
result.dilate(T::default() - amount);
result
}
/// Returns the center point of the bounds.
///
/// Calculates the center by taking the origin's x and y coordinates and adding half the width and height
@@ -1266,12 +1274,36 @@ where
/// size: Size { width: 10.0, height: 20.0 },
/// });
/// ```
pub fn map_origin(self, f: impl Fn(Point<T>) -> Point<T>) -> Bounds<T> {
pub fn map_origin(self, f: impl Fn(T) -> T) -> Bounds<T> {
Bounds {
origin: f(self.origin),
origin: self.origin.map(f),
size: self.size,
}
}
/// Applies a function to the origin of the bounds, producing a new `Bounds` with the new origin
///
/// # Examples
///
/// ```
/// # use zed::{Bounds, Point, Size};
/// let bounds = Bounds {
/// origin: Point { x: 10.0, y: 10.0 },
/// size: Size { width: 10.0, height: 20.0 },
/// };
/// let new_bounds = bounds.map_size(|value| value * 1.5);
///
/// assert_eq!(new_bounds, Bounds {
/// origin: Point { x: 10.0, y: 10.0 },
/// size: Size { width: 15.0, height: 30.0 },
/// });
/// ```
pub fn map_size(self, f: impl Fn(T) -> T) -> Bounds<T> {
Bounds {
origin: self.origin,
size: self.size.map(f),
}
}
}
/// Checks if the bounds represent an empty area.
@@ -2288,6 +2320,18 @@ impl Pixels {
Self(self.0.abs())
}
/// Returns the sign of the `Pixels` value.
///
/// # Returns
///
/// Returns:
/// * `1.0` if the value is positive
/// * `-1.0` if the value is negative
/// * `0.0` if the value is zero
pub fn signum(&self) -> f32 {
self.0.signum()
}
/// Returns the f64 value of `Pixels`.
///
/// # Returns

View File

@@ -291,14 +291,41 @@ impl ScrollDelta {
}
/// Combines two scroll deltas into one.
/// If the signs of the deltas are the same (both positive or both negative),
/// the deltas are added together. If the signs are opposite, the second delta
/// (other) is used, effectively overriding the first delta.
pub fn coalesce(self, other: ScrollDelta) -> ScrollDelta {
match (self, other) {
(ScrollDelta::Pixels(px_a), ScrollDelta::Pixels(px_b)) => {
ScrollDelta::Pixels(px_a + px_b)
(ScrollDelta::Pixels(a), ScrollDelta::Pixels(b)) => {
let x = if a.x.signum() * b.x.signum() >= 0. {
a.x + b.x
} else {
b.x
};
let y = if a.y.signum() * b.y.signum() >= 0. {
a.y + b.y
} else {
b.y
};
ScrollDelta::Pixels(point(x, y))
}
(ScrollDelta::Lines(lines_a), ScrollDelta::Lines(lines_b)) => {
ScrollDelta::Lines(lines_a + lines_b)
(ScrollDelta::Lines(a), ScrollDelta::Lines(b)) => {
let x = if a.x.signum() * b.x.signum() >= 0. {
a.x + b.x
} else {
b.x
};
let y = if a.y.signum() * b.y.signum() >= 0. {
a.y + b.y
} else {
b.y
};
ScrollDelta::Lines(point(x, y))
}
_ => other,

View File

@@ -210,6 +210,93 @@ impl Debug for DisplayId {
unsafe impl Send for DisplayId {}
/// Which part of the window to resize
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResizeEdge {
/// The top edge
Top,
/// The top right corner
TopRight,
/// The right edge
Right,
/// The bottom right corner
BottomRight,
/// The bottom edge
Bottom,
/// The bottom left corner
BottomLeft,
/// The left edge
Left,
/// The top left corner
TopLeft,
}
/// A type to describe the appearance of a window
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
pub enum WindowDecorations {
#[default]
/// Server side decorations
Server,
/// Client side decorations
Client,
}
/// A type to describe how this window is currently configured
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
pub enum Decorations {
/// The window is configured to use server side decorations
#[default]
Server,
/// The window is configured to use client side decorations
Client {
/// The edge tiling state
tiling: Tiling,
},
}
/// What window controls this platform supports
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
pub struct WindowControls {
/// Whether this platform supports fullscreen
pub fullscreen: bool,
/// Whether this platform supports maximize
pub maximize: bool,
/// Whether this platform supports minimize
pub minimize: bool,
/// Whether this platform supports a window menu
pub window_menu: bool,
}
/// A type to describe which sides of the window are currently tiled in some way
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
pub struct Tiling {
/// Whether the top edge is tiled
pub top: bool,
/// Whether the left edge is tiled
pub left: bool,
/// Whether the right edge is tiled
pub right: bool,
/// Whether the bottom edge is tiled
pub bottom: bool,
}
impl Tiling {
/// Initializes a [`Tiling`] type with all sides tiled
pub fn tiled() -> Self {
Self {
top: true,
left: true,
right: true,
bottom: true,
}
}
/// Whether any edge is tiled
pub fn is_tiled(&self) -> bool {
self.top || self.left || self.right || self.bottom
}
}
pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn bounds(&self) -> Bounds<Pixels>;
fn is_maximized(&self) -> bool;
@@ -232,10 +319,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn activate(&self);
fn is_active(&self) -> bool;
fn set_title(&mut self, title: &str);
fn set_app_id(&mut self, app_id: &str);
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance);
fn set_edited(&mut self, edited: bool);
fn show_character_palette(&self);
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
fn minimize(&self);
fn zoom(&self);
fn toggle_fullscreen(&self);
@@ -252,12 +336,31 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn completed_frame(&self) {}
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
// macOS specific methods
fn set_edited(&mut self, _edited: bool) {}
fn show_character_palette(&self) {}
#[cfg(target_os = "windows")]
fn get_raw_handle(&self) -> windows::HWND;
fn show_window_menu(&self, position: Point<Pixels>);
fn start_system_move(&self);
fn should_render_window_controls(&self) -> bool;
// Linux specific methods
fn request_decorations(&self, _decorations: WindowDecorations) {}
fn show_window_menu(&self, _position: Point<Pixels>) {}
fn start_window_move(&self) {}
fn start_window_resize(&self, _edge: ResizeEdge) {}
fn window_decorations(&self) -> Decorations {
Decorations::Server
}
fn set_app_id(&mut self, _app_id: &str) {}
fn window_controls(&self) -> WindowControls {
WindowControls {
fullscreen: true,
maximize: true,
minimize: true,
window_menu: false,
}
}
fn set_client_inset(&self, _inset: Pixels) {}
#[cfg(any(test, feature = "test-support"))]
fn as_test(&mut self) -> Option<&mut TestWindow> {
@@ -570,6 +673,10 @@ pub struct WindowOptions {
/// Window minimum size
pub window_min_size: Option<Size<Pixels>>,
/// Whether to use client or server side decorations. Wayland only
/// Note that this may be ignored.
pub window_decorations: Option<WindowDecorations>,
}
/// The variables that can be configured when creating a new window
@@ -596,8 +703,6 @@ pub(crate) struct WindowParams {
pub display_id: Option<DisplayId>,
pub window_background: WindowBackgroundAppearance,
#[cfg_attr(target_os = "linux", allow(dead_code))]
pub window_min_size: Option<Size<Pixels>>,
}
@@ -649,6 +754,7 @@ impl Default for WindowOptions {
window_background: WindowBackgroundAppearance::default(),
app_id: None,
window_min_size: None,
window_decorations: None,
}
}
}
@@ -659,7 +765,7 @@ pub struct TitlebarOptions {
/// The initial title of the window
pub title: Option<SharedString>,
/// Whether the titlebar should appear transparent
/// Whether the titlebar should appear transparent (macOS only)
pub appears_transparent: bool,
/// The position of the macOS traffic light buttons
@@ -805,6 +911,14 @@ pub enum CursorStyle {
/// corresponds to the CSS cursor value `ns-resize`
ResizeUpDown,
/// A resize cursor directing up-left and down-right
/// corresponds to the CSS cursor value `nesw-resize`
ResizeUpLeftDownRight,
/// A resize cursor directing up-right and down-left
/// corresponds to the CSS cursor value `nwse-resize`
ResizeUpRightDownLeft,
/// A cursor indicating that the item/column can be resized horizontally.
/// corresponds to the CSS curosr value `col-resize`
ResizeColumn,

View File

@@ -5,10 +5,12 @@ use calloop::{
timer::TimeoutAction,
EventLoop,
};
use mio::Waker;
use parking::{Parker, Unparker};
use parking_lot::Mutex;
use std::{sync::Arc, thread, time::Duration};
use std::{
thread,
time::{Duration, Instant},
};
use util::ResultExt;
struct TimerAfter {
@@ -19,7 +21,6 @@ struct TimerAfter {
pub(crate) struct LinuxDispatcher {
parker: Mutex<Parker>,
main_sender: Sender<Runnable>,
main_waker: Option<Arc<Waker>>,
timer_sender: Sender<TimerAfter>,
background_sender: flume::Sender<Runnable>,
_background_threads: Vec<thread::JoinHandle<()>>,
@@ -27,18 +28,26 @@ pub(crate) struct LinuxDispatcher {
}
impl LinuxDispatcher {
pub fn new(main_sender: Sender<Runnable>, main_waker: Option<Arc<Waker>>) -> Self {
pub fn new(main_sender: Sender<Runnable>) -> Self {
let (background_sender, background_receiver) = flume::unbounded::<Runnable>();
let thread_count = std::thread::available_parallelism()
.map(|i| i.get())
.unwrap_or(1);
let mut background_threads = (0..thread_count)
.map(|_| {
.map(|i| {
let receiver = background_receiver.clone();
std::thread::spawn(move || {
for runnable in receiver {
let start = Instant::now();
runnable.run();
log::trace!(
"background thread {}: ran runnable. took: {:?}",
i,
start.elapsed()
);
}
})
})
@@ -79,7 +88,6 @@ impl LinuxDispatcher {
Self {
parker: Mutex::new(Parker::new()),
main_sender,
main_waker,
timer_sender,
background_sender,
_background_threads: background_threads,
@@ -99,9 +107,6 @@ impl PlatformDispatcher for LinuxDispatcher {
fn dispatch_on_main_thread(&self, runnable: Runnable) {
self.main_sender.send(runnable).ok();
if let Some(main_waker) = self.main_waker.as_ref() {
main_waker.wake().ok();
}
}
fn dispatch_after(&self, duration: Duration, runnable: Runnable) {

View File

@@ -22,7 +22,7 @@ impl HeadlessClient {
pub(crate) fn new() -> Self {
let event_loop = EventLoop::try_new().unwrap();
let (common, main_receiver) = LinuxCommon::new(Box::new(event_loop.get_signal()), None);
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
let handle = event_loop.handle();
@@ -81,6 +81,8 @@ impl LinuxClient for HeadlessClient {
fn open_uri(&self, _uri: &str) {}
fn reveal_path(&self, _path: std::path::PathBuf) {}
fn write_to_primary(&self, _item: crate::ClipboardItem) {}
fn write_to_clipboard(&self, _item: crate::ClipboardItem) {}

View File

@@ -7,7 +7,7 @@ use std::ffi::OsString;
use std::fs::File;
use std::io::Read;
use std::ops::{Deref, DerefMut};
use std::os::fd::{AsRawFd, FromRawFd};
use std::os::fd::{AsFd, AsRawFd, FromRawFd};
use std::panic::Location;
use std::rc::Weak;
use std::{
@@ -20,13 +20,14 @@ use std::{
use anyhow::anyhow;
use ashpd::desktop::file_chooser::{OpenFileRequest, SaveFileRequest};
use ashpd::desktop::open_uri::{OpenDirectoryRequest, OpenFileRequest as OpenUriRequest};
use ashpd::{url, ActivationToken};
use async_task::Runnable;
use calloop::channel::Channel;
use calloop::{EventLoop, LoopHandle, LoopSignal};
use filedescriptor::FileDescriptor;
use flume::{Receiver, Sender};
use futures::channel::oneshot;
use mio::Waker;
use parking_lot::Mutex;
use time::UtcOffset;
use util::ResultExt;
@@ -67,6 +68,7 @@ pub trait LinuxClient {
) -> anyhow::Result<Box<dyn PlatformWindow>>;
fn set_cursor_style(&self, style: CursorStyle);
fn open_uri(&self, uri: &str);
fn reveal_path(&self, path: PathBuf);
fn write_to_primary(&self, item: ClipboardItem);
fn write_to_clipboard(&self, item: ClipboardItem);
fn read_from_primary(&self) -> Option<ClipboardItem>;
@@ -85,16 +87,6 @@ pub(crate) struct PlatformHandlers {
pub(crate) validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
}
pub trait QuitSignal {
fn quit(&mut self);
}
impl QuitSignal for LoopSignal {
fn quit(&mut self) {
self.stop();
}
}
pub(crate) struct LinuxCommon {
pub(crate) background_executor: BackgroundExecutor,
pub(crate) foreground_executor: ForegroundExecutor,
@@ -102,20 +94,17 @@ pub(crate) struct LinuxCommon {
pub(crate) appearance: WindowAppearance,
pub(crate) auto_hide_scrollbars: bool,
pub(crate) callbacks: PlatformHandlers,
pub(crate) quit_signal: Box<dyn QuitSignal>,
pub(crate) signal: LoopSignal,
pub(crate) menus: Vec<OwnedMenu>,
}
impl LinuxCommon {
pub fn new(
quit_signal: Box<dyn QuitSignal>,
main_waker: Option<Arc<Waker>>,
) -> (Self, Channel<Runnable>) {
pub fn new(signal: LoopSignal) -> (Self, Channel<Runnable>) {
let (main_sender, main_receiver) = calloop::channel::channel::<Runnable>();
let text_system = Arc::new(CosmicTextSystem::new());
let callbacks = PlatformHandlers::default();
let dispatcher = Arc::new(LinuxDispatcher::new(main_sender.clone(), main_waker));
let dispatcher = Arc::new(LinuxDispatcher::new(main_sender.clone()));
let background_executor = BackgroundExecutor::new(dispatcher.clone());
@@ -126,7 +115,7 @@ impl LinuxCommon {
appearance: WindowAppearance::Light,
auto_hide_scrollbars: false,
callbacks,
quit_signal,
signal,
menus: Vec::new(),
};
@@ -160,7 +149,7 @@ impl<P: LinuxClient + 'static> Platform for P {
}
fn quit(&self) {
self.with_common(|common| common.quit_signal.quit());
self.with_common(|common| common.signal.stop());
}
fn compositor_name(&self) -> &'static str {
@@ -314,20 +303,27 @@ impl<P: LinuxClient + 'static> Platform for P {
let directory = directory.to_owned();
self.foreground_executor()
.spawn(async move {
let result = SaveFileRequest::default()
let request = SaveFileRequest::default()
.modal(true)
.title("Select new path")
.accept_label("Accept")
.send()
.await
.ok()
.and_then(|request| request.response().ok())
.and_then(|response| {
response
.uris()
.first()
.and_then(|uri| uri.to_file_path().ok())
});
.current_folder(directory);
let result = if let Ok(request) = request {
request
.send()
.await
.ok()
.and_then(|request| request.response().ok())
.and_then(|response| {
response
.uris()
.first()
.and_then(|uri| uri.to_file_path().ok())
})
} else {
None
};
done_tx.send(result);
})
@@ -337,13 +333,7 @@ impl<P: LinuxClient + 'static> Platform for P {
}
fn reveal_path(&self, path: &Path) {
if path.is_dir() {
open::that_detached(path);
return;
}
// If `path` is a file, the system may try to open it in a text editor
let dir = path.parent().unwrap_or(Path::new(""));
open::that_detached(dir);
self.reveal_path(path.to_owned());
}
fn on_quit(&self, callback: Box<dyn FnMut()>) {
@@ -504,18 +494,40 @@ impl<P: LinuxClient + 'static> Platform for P {
fn add_recent_document(&self, _path: &Path) {}
}
pub(super) fn open_uri_internal(uri: &str, activation_token: Option<&str>) {
let mut last_err = None;
for mut command in open::commands(uri) {
if let Some(token) = activation_token {
command.env("XDG_ACTIVATION_TOKEN", token);
}
match command.spawn() {
Ok(_) => return,
Err(err) => last_err = Some(err),
}
pub(super) fn open_uri_internal(
executor: BackgroundExecutor,
uri: &str,
activation_token: Option<String>,
) {
if let Some(uri) = url::Url::parse(uri).log_err() {
executor
.spawn(async move {
OpenUriRequest::default()
.activation_token(activation_token.map(ActivationToken::from))
.send_uri(&uri)
.await
.log_err();
})
.detach();
}
log::error!("failed to open uri: {uri:?}, last error: {last_err:?}");
}
pub(super) fn reveal_path_internal(
executor: BackgroundExecutor,
path: PathBuf,
activation_token: Option<String>,
) {
executor
.spawn(async move {
if let Some(dir) = File::open(path).log_err() {
OpenDirectoryRequest::default()
.activation_token(activation_token.map(ActivationToken::from))
.send(&dir.as_fd())
.await
.log_err();
}
})
.detach();
}
pub(super) fn is_within_click_distance(a: Point<Pixels>, b: Point<Pixels>) -> bool {
@@ -572,6 +584,8 @@ impl CursorStyle {
CursorStyle::ResizeUp => Shape::NResize,
CursorStyle::ResizeDown => Shape::SResize,
CursorStyle::ResizeUpDown => Shape::NsResize,
CursorStyle::ResizeUpLeftDownRight => Shape::NwseResize,
CursorStyle::ResizeUpRightDownLeft => Shape::NeswResize,
CursorStyle::ResizeColumn => Shape::ColResize,
CursorStyle::ResizeRow => Shape::RowResize,
CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText,
@@ -599,6 +613,8 @@ impl CursorStyle {
CursorStyle::ResizeUp => "n-resize",
CursorStyle::ResizeDown => "s-resize",
CursorStyle::ResizeUpDown => "ns-resize",
CursorStyle::ResizeUpLeftDownRight => "nwse-resize",
CursorStyle::ResizeUpRightDownLeft => "nesw-resize",
CursorStyle::ResizeColumn => "col-resize",
CursorStyle::ResizeRow => "row-resize",
CursorStyle::IBeamCursorForVerticalLayout => "vertical-text",

View File

@@ -61,7 +61,6 @@ use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blu
use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
use xkbcommon::xkb::{self, Keycode, KEYMAP_COMPILE_NO_FLAGS};
use super::super::{open_uri_internal, read_fd, DOUBLE_CLICK_INTERVAL};
use super::display::WaylandDisplay;
use super::window::{ImeInput, WaylandWindowStatePtr};
use crate::platform::linux::wayland::clipboard::{
@@ -72,11 +71,14 @@ use crate::platform::linux::wayland::serial::{SerialKind, SerialTracker};
use crate::platform::linux::wayland::window::WaylandWindow;
use crate::platform::linux::xdg_desktop_portal::{Event as XDPEvent, XDPEventSource};
use crate::platform::linux::LinuxClient;
use crate::platform::linux::{get_xkb_compose_state, is_within_click_distance};
use crate::platform::linux::{
get_xkb_compose_state, is_within_click_distance, open_uri_internal, read_fd,
reveal_path_internal,
};
use crate::platform::PlatformWindow;
use crate::{
point, px, size, Bounds, DevicePixels, FileDropEvent, ForegroundExecutor, MouseExitEvent, Size,
SCROLL_LINES,
DOUBLE_CLICK_INTERVAL, SCROLL_LINES,
};
use crate::{
AnyWindowHandle, CursorStyle, DisplayId, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers,
@@ -138,7 +140,7 @@ impl Globals {
primary_selection_manager: globals.bind(&qh, 1..=1, ()).ok(),
shm: globals.bind(&qh, 1..=1, ()).unwrap(),
seat,
wm_base: globals.bind(&qh, 1..=1, ()).unwrap(),
wm_base: globals.bind(&qh, 2..=5, ()).unwrap(),
viewporter: globals.bind(&qh, 1..=1, ()).ok(),
fractional_scale_manager: globals.bind(&qh, 1..=1, ()).ok(),
decoration_manager: globals.bind(&qh, 1..=1, ()).ok(),
@@ -220,7 +222,7 @@ pub(crate) struct WaylandClientState {
data_offers: Vec<DataOffer<WlDataOffer>>,
primary_data_offer: Option<DataOffer<ZwpPrimarySelectionOfferV1>>,
cursor: Cursor,
pending_open_uri: Option<String>,
pending_activation: Option<PendingActivation>,
event_loop: Option<EventLoop<'static, WaylandClientStatePtr>>,
common: LinuxCommon,
}
@@ -244,6 +246,15 @@ pub(crate) struct KeyRepeat {
current_keycode: Option<xkb::Keycode>,
}
pub(crate) enum PendingActivation {
/// URI to open in the web browser.
Uri(String),
/// Path to open in the file explorer.
Path(PathBuf),
/// A window from ourselves to raise.
Window(ObjectId),
}
/// This struct is required to conform to Rust's orphan rules, so we can dispatch on the state but hand the
/// window to GPUI.
#[derive(Clone)]
@@ -260,6 +271,11 @@ impl WaylandClientStatePtr {
self.0.upgrade().unwrap().borrow().serial_tracker.get(kind)
}
pub fn set_pending_activation(&self, window: ObjectId) {
self.0.upgrade().unwrap().borrow_mut().pending_activation =
Some(PendingActivation::Window(window));
}
pub fn enable_ime(&self) {
let client = self.get_client();
let mut state = client.borrow_mut();
@@ -310,7 +326,7 @@ impl WaylandClientStatePtr {
}
}
if state.windows.is_empty() {
state.common.quit_signal.quit();
state.common.signal.stop();
}
}
}
@@ -406,7 +422,7 @@ impl WaylandClient {
let event_loop = EventLoop::<WaylandClientStatePtr>::try_new().unwrap();
let (common, main_receiver) = LinuxCommon::new(Box::new(event_loop.get_signal()), None);
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
let handle = event_loop.handle();
handle
@@ -443,7 +459,7 @@ impl WaylandClient {
let mut cursor = Cursor::new(&conn, &globals, 24);
handle
.insert_source(XDPEventSource::new(&common.background_executor, None), {
.insert_source(XDPEventSource::new(&common.background_executor), {
move |event, _, client| match event {
XDPEvent::WindowAppearance(appearance) => {
if let Some(client) = client.0.upgrade() {
@@ -530,7 +546,7 @@ impl WaylandClient {
data_offers: Vec::new(),
primary_data_offer: None,
cursor,
pending_open_uri: None,
pending_activation: None,
event_loop: Some(event_loop),
}));
@@ -629,14 +645,33 @@ impl LinuxClient for WaylandClient {
state.globals.activation.clone(),
state.mouse_focused_window.clone(),
) {
state.pending_open_uri = Some(uri.to_owned());
state.pending_activation = Some(PendingActivation::Uri(uri.to_string()));
let token = activation.get_activation_token(&state.globals.qh, ());
let serial = state.serial_tracker.get(SerialKind::MousePress);
token.set_serial(serial, &state.wl_seat);
token.set_surface(&window.surface());
token.commit();
} else {
open_uri_internal(uri, None);
let executor = state.common.background_executor.clone();
open_uri_internal(executor, uri, None);
}
}
fn reveal_path(&self, path: PathBuf) {
let mut state = self.0.borrow_mut();
if let (Some(activation), Some(window)) = (
state.globals.activation.clone(),
state.mouse_focused_window.clone(),
) {
state.pending_activation = Some(PendingActivation::Path(path));
let token = activation.get_activation_token(&state.globals.qh, ());
let serial = state.serial_tracker.get(SerialKind::MousePress);
token.set_serial(serial, &state.wl_seat);
token.set_surface(&window.surface());
token.commit();
} else {
let executor = state.common.background_executor.clone();
reveal_path_internal(executor, path, None);
}
}
@@ -809,7 +844,7 @@ impl Dispatch<WlCallback, ObjectId> for WaylandClientStatePtr {
match event {
wl_callback::Event::Done { .. } => {
window.frame(true);
window.frame();
}
_ => {}
}
@@ -954,13 +989,25 @@ impl Dispatch<xdg_activation_token_v1::XdgActivationTokenV1, ()> for WaylandClie
) {
let client = this.get_client();
let mut state = client.borrow_mut();
if let xdg_activation_token_v1::Event::Done { token } = event {
if let Some(uri) = state.pending_open_uri.take() {
open_uri_internal(&uri, Some(&token));
} else {
log::error!("called while pending_open_uri is None");
let executor = state.common.background_executor.clone();
match state.pending_activation.take() {
Some(PendingActivation::Uri(uri)) => open_uri_internal(executor, &uri, Some(token)),
Some(PendingActivation::Path(path)) => {
reveal_path_internal(executor, path, Some(token))
}
Some(PendingActivation::Window(window)) => {
let Some(window) = get_window(&mut state, &window) else {
return;
};
let activation = state.globals.activation.as_ref().unwrap();
activation.activate(token, &window.surface());
}
None => log::error!("activation token received with no pending activation"),
}
}
token.destroy();
}
}
@@ -1194,7 +1241,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
&& state.repeat.current_keycode.is_some()
&& state.keyboard_focused_window.is_some();
if !is_repeating {
if !is_repeating || rate == 0 {
return TimeoutAction::Drop;
}
@@ -1508,6 +1555,11 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
if state.axis_source == AxisSource::Wheel {
return;
}
let axis = if state.modifiers.shift {
wl_pointer::Axis::HorizontalScroll
} else {
axis
};
let axis_modifier = match axis {
wl_pointer::Axis::VerticalScroll => state.vertical_modifier,
wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier,
@@ -1533,6 +1585,11 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
discrete,
} => {
state.scroll_event_received = true;
let axis = if state.modifiers.shift {
wl_pointer::Axis::HorizontalScroll
} else {
axis
};
let axis_modifier = match axis {
wl_pointer::Axis::VerticalScroll => state.vertical_modifier,
wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier,
@@ -1555,6 +1612,11 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
value120,
} => {
state.scroll_event_received = true;
let axis = if state.modifiers.shift {
wl_pointer::Axis::HorizontalScroll
} else {
axis
};
let axis_modifier = match axis {
wl_pointer::Axis::VerticalScroll => state.vertical_modifier,
wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier,

View File

@@ -25,9 +25,10 @@ use crate::platform::linux::wayland::serial::SerialKind;
use crate::platform::{PlatformAtlas, PlatformInputHandler, PlatformWindow};
use crate::scene::Scene;
use crate::{
px, size, AnyWindowHandle, Bounds, Globals, Modifiers, Output, Pixels, PlatformDisplay,
PlatformInput, Point, PromptLevel, Size, WaylandClientStatePtr, WindowAppearance,
WindowBackgroundAppearance, WindowBounds, WindowParams,
px, size, AnyWindowHandle, Bounds, Decorations, Globals, Modifiers, Output, Pixels,
PlatformDisplay, PlatformInput, Point, PromptLevel, ResizeEdge, Size, Tiling,
WaylandClientStatePtr, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
WindowControls, WindowDecorations, WindowParams,
};
#[derive(Default)]
@@ -62,10 +63,12 @@ impl rwh::HasDisplayHandle for RawWindow {
}
}
#[derive(Debug)]
struct InProgressConfigure {
size: Option<Size<Pixels>>,
fullscreen: bool,
maximized: bool,
tiling: Tiling,
}
pub struct WaylandWindowState {
@@ -73,6 +76,7 @@ pub struct WaylandWindowState {
acknowledged_first_configure: bool,
pub surface: wl_surface::WlSurface,
decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
app_id: Option<String>,
appearance: WindowAppearance,
blur: Option<org_kde_kwin_blur::OrgKdeKwinBlur>,
toplevel: xdg_toplevel::XdgToplevel,
@@ -84,14 +88,19 @@ pub struct WaylandWindowState {
bounds: Bounds<Pixels>,
scale: f32,
input_handler: Option<PlatformInputHandler>,
decoration_state: WaylandDecorationState,
decorations: WindowDecorations,
background_appearance: WindowBackgroundAppearance,
fullscreen: bool,
maximized: bool,
windowed_bounds: Bounds<Pixels>,
tiling: Tiling,
window_bounds: Bounds<Pixels>,
client: WaylandClientStatePtr,
handle: AnyWindowHandle,
active: bool,
in_progress_configure: Option<InProgressConfigure>,
in_progress_window_controls: Option<WindowControls>,
window_controls: WindowControls,
inset: Option<Pixels>,
}
#[derive(Clone)]
@@ -142,7 +151,7 @@ impl WaylandWindowState {
height: options.bounds.size.height.0 as u32,
depth: 1,
},
transparent: options.window_background != WindowBackgroundAppearance::Opaque,
transparent: true,
};
Ok(Self {
@@ -150,6 +159,7 @@ impl WaylandWindowState {
acknowledged_first_configure: false,
surface,
decoration,
app_id: None,
blur: None,
toplevel,
viewport,
@@ -160,17 +170,33 @@ impl WaylandWindowState {
bounds: options.bounds,
scale: 1.0,
input_handler: None,
decoration_state: WaylandDecorationState::Client,
decorations: WindowDecorations::Client,
background_appearance: WindowBackgroundAppearance::Opaque,
fullscreen: false,
maximized: false,
windowed_bounds: options.bounds,
tiling: Tiling::default(),
window_bounds: options.bounds,
in_progress_configure: None,
client,
appearance,
handle,
active: false,
in_progress_window_controls: None,
// Assume that we can do anything, unless told otherwise
window_controls: WindowControls {
fullscreen: true,
maximize: true,
minimize: true,
window_menu: true,
},
inset: None,
})
}
pub fn is_transparent(&self) -> bool {
self.decorations == WindowDecorations::Client
|| self.background_appearance != WindowBackgroundAppearance::Opaque
}
}
pub(crate) struct WaylandWindow(pub WaylandWindowStatePtr);
@@ -235,7 +261,7 @@ impl WaylandWindow {
.wm_base
.get_xdg_surface(&surface, &globals.qh, surface.id());
let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id());
toplevel.set_min_size(200, 200);
toplevel.set_min_size(50, 50);
if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() {
fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id());
@@ -246,13 +272,7 @@ impl WaylandWindow {
.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
decoration_manager.get_toplevel_decoration(&toplevel, &globals.qh, surface.id())
});
let viewport = globals
@@ -296,12 +316,11 @@ impl WaylandWindowStatePtr {
Rc::ptr_eq(&self.state, &other.state)
}
pub fn frame(&self, request_frame_callback: bool) {
if request_frame_callback {
let state = self.state.borrow_mut();
state.surface.frame(&state.globals.qh, state.surface.id());
drop(state);
}
pub fn frame(&self) {
let mut state = self.state.borrow_mut();
state.surface.frame(&state.globals.qh, state.surface.id());
drop(state);
let mut cb = self.callbacks.borrow_mut();
if let Some(fun) = cb.request_frame.as_mut() {
fun();
@@ -311,6 +330,18 @@ impl WaylandWindowStatePtr {
pub fn handle_xdg_surface_event(&self, event: xdg_surface::Event) {
match event {
xdg_surface::Event::Configure { serial } => {
{
let mut state = self.state.borrow_mut();
if let Some(window_controls) = state.in_progress_window_controls.take() {
state.window_controls = window_controls;
drop(state);
let mut callbacks = self.callbacks.borrow_mut();
if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() {
appearance_changed();
}
}
}
{
let mut state = self.state.borrow_mut();
@@ -318,18 +349,20 @@ impl WaylandWindowStatePtr {
let got_unmaximized = state.maximized && !configure.maximized;
state.fullscreen = configure.fullscreen;
state.maximized = configure.maximized;
if got_unmaximized {
configure.size = Some(state.windowed_bounds.size);
} else if !configure.fullscreen && !configure.maximized {
state.tiling = configure.tiling;
if !configure.fullscreen && !configure.maximized {
configure.size = if got_unmaximized {
Some(state.window_bounds.size)
} else {
compute_outer_size(state.inset, configure.size, state.tiling)
};
if let Some(size) = configure.size {
state.windowed_bounds = Bounds {
state.window_bounds = Bounds {
origin: Point::default(),
size,
};
}
}
drop(state);
if let Some(size) = configure.size {
self.resize(size);
@@ -338,10 +371,28 @@ impl WaylandWindowStatePtr {
}
let mut state = self.state.borrow_mut();
state.xdg_surface.ack_configure(serial);
let window_geometry = inset_by_tiling(
state.bounds.map_origin(|_| px(0.0)),
state.inset.unwrap_or(px(0.0)),
state.tiling,
)
.map(|v| v.0 as i32)
.map_size(|v| if v <= 0 { 1 } else { v });
state.xdg_surface.set_window_geometry(
window_geometry.origin.x,
window_geometry.origin.y,
window_geometry.size.width,
window_geometry.size.height,
);
let request_frame_callback = !state.acknowledged_first_configure;
state.acknowledged_first_configure = true;
drop(state);
self.frame(request_frame_callback);
if request_frame_callback {
state.acknowledged_first_configure = true;
drop(state);
self.frame();
}
}
_ => {}
}
@@ -351,10 +402,21 @@ impl WaylandWindowStatePtr {
match event {
zxdg_toplevel_decoration_v1::Event::Configure { mode } => match mode {
WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ServerSide) => {
self.set_decoration_state(WaylandDecorationState::Server)
self.state.borrow_mut().decorations = WindowDecorations::Server;
if let Some(mut appearance_changed) =
self.callbacks.borrow_mut().appearance_changed.as_mut()
{
appearance_changed();
}
}
WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ClientSide) => {
self.set_decoration_state(WaylandDecorationState::Client)
self.state.borrow_mut().decorations = WindowDecorations::Client;
// Update background to be transparent
if let Some(mut appearance_changed) =
self.callbacks.borrow_mut().appearance_changed.as_mut()
{
appearance_changed();
}
}
WEnum::Value(_) => {
log::warn!("Unknown decoration mode");
@@ -389,14 +451,48 @@ impl WaylandWindowStatePtr {
Some(size(px(width as f32), px(height as f32)))
};
let fullscreen = states.contains(&(xdg_toplevel::State::Fullscreen as u8));
let maximized = states.contains(&(xdg_toplevel::State::Maximized as u8));
let states = extract_states::<xdg_toplevel::State>(&states);
let mut tiling = Tiling::default();
let mut fullscreen = false;
let mut maximized = false;
for state in states {
match state {
xdg_toplevel::State::Maximized => {
maximized = true;
}
xdg_toplevel::State::Fullscreen => {
fullscreen = true;
}
xdg_toplevel::State::TiledTop => {
tiling.top = true;
}
xdg_toplevel::State::TiledLeft => {
tiling.left = true;
}
xdg_toplevel::State::TiledRight => {
tiling.right = true;
}
xdg_toplevel::State::TiledBottom => {
tiling.bottom = true;
}
_ => {
// noop
}
}
}
if fullscreen || maximized {
tiling = Tiling::tiled();
}
let mut state = self.state.borrow_mut();
state.in_progress_configure = Some(InProgressConfigure {
size,
fullscreen,
maximized,
tiling,
});
false
@@ -415,6 +511,33 @@ impl WaylandWindowStatePtr {
true
}
}
xdg_toplevel::Event::WmCapabilities { capabilities } => {
let mut window_controls = WindowControls::default();
let states = extract_states::<xdg_toplevel::WmCapabilities>(&capabilities);
for state in states {
match state {
xdg_toplevel::WmCapabilities::Maximize => {
window_controls.maximize = true;
}
xdg_toplevel::WmCapabilities::Minimize => {
window_controls.minimize = true;
}
xdg_toplevel::WmCapabilities::Fullscreen => {
window_controls.fullscreen = true;
}
xdg_toplevel::WmCapabilities::WindowMenu => {
window_controls.window_menu = true;
}
_ => {}
}
}
let mut state = self.state.borrow_mut();
state.in_progress_window_controls = Some(window_controls);
false
}
_ => false,
}
}
@@ -545,18 +668,6 @@ impl WaylandWindowStatePtr {
self.set_size_and_scale(None, Some(scale));
}
/// Notifies the window of the state of the decorations.
///
/// # Note
///
/// This API is indirectly called by the wayland compositor and
/// not meant to be called by a user who wishes to change the state
/// of the decorations. This is because the state of the decorations
/// is managed by the compositor and not the client.
pub fn set_decoration_state(&self, state: WaylandDecorationState) {
self.state.borrow_mut().decoration_state = state;
}
pub fn close(&self) {
let mut callbacks = self.callbacks.borrow_mut();
if let Some(fun) = callbacks.close.take() {
@@ -599,6 +710,17 @@ impl WaylandWindowStatePtr {
}
}
fn extract_states<'a, S: TryFrom<u32> + 'a>(states: &'a [u8]) -> impl Iterator<Item = S> + 'a
where
<S as TryFrom<u32>>::Error: 'a,
{
states
.chunks_exact(4)
.flat_map(TryInto::<[u8; 4]>::try_into)
.map(u32::from_ne_bytes)
.flat_map(S::try_from)
}
fn primary_output_scale(state: &mut RefMut<WaylandWindowState>) -> i32 {
let mut scale = 1;
let mut current_output = state.display.take();
@@ -639,9 +761,9 @@ impl PlatformWindow for WaylandWindow {
fn window_bounds(&self) -> WindowBounds {
let state = self.borrow();
if state.fullscreen {
WindowBounds::Fullscreen(state.windowed_bounds)
WindowBounds::Fullscreen(state.window_bounds)
} else if state.maximized {
WindowBounds::Maximized(state.windowed_bounds)
WindowBounds::Maximized(state.window_bounds)
} else {
drop(state);
WindowBounds::Windowed(self.bounds())
@@ -703,7 +825,20 @@ impl PlatformWindow for WaylandWindow {
}
fn activate(&self) {
log::info!("Wayland does not support this API");
// Try to request an activation token. Even though the activation is likely going to be rejected,
// KWin and Mutter can use the app_id to visually indicate we're requesting attention.
let state = self.borrow();
if let (Some(activation), Some(app_id)) = (&state.globals.activation, state.app_id.clone())
{
state.client.set_pending_activation(state.surface.id());
let token = activation.get_activation_token(&state.globals.qh, ());
// The serial isn't exactly important here, since the activation is probably going to be rejected anyway.
let serial = state.client.get_serial(SerialKind::MousePress);
token.set_app_id(app_id);
token.set_serial(serial, &state.globals.seat);
token.set_surface(&state.surface);
token.commit();
}
}
fn is_active(&self) -> bool {
@@ -715,55 +850,15 @@ impl PlatformWindow for WaylandWindow {
}
fn set_app_id(&mut self, app_id: &str) {
self.borrow().toplevel.set_app_id(app_id.to_owned());
}
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
let opaque = background_appearance == WindowBackgroundAppearance::Opaque;
let mut state = self.borrow_mut();
state.renderer.update_transparency(!opaque);
let region = state
.globals
.compositor
.create_region(&state.globals.qh, ());
region.add(0, 0, i32::MAX, i32::MAX);
if opaque {
// Promise the compositor that this region of the window surface
// contains no transparent pixels. This allows the compositor to
// do skip whatever is behind the surface for better performance.
state.surface.set_opaque_region(Some(&region));
} else {
state.surface.set_opaque_region(None);
}
if let Some(ref blur_manager) = state.globals.blur_manager {
if background_appearance == WindowBackgroundAppearance::Blurred {
if state.blur.is_none() {
let blur = blur_manager.create(&state.surface, &state.globals.qh, ());
blur.set_region(Some(&region));
state.blur = Some(blur);
}
state.blur.as_ref().unwrap().commit();
} else {
// It probably doesn't hurt to clear the blur for opaque windows
blur_manager.unset(&state.surface);
if let Some(b) = state.blur.take() {
b.release()
}
}
}
region.destroy();
state.toplevel.set_app_id(app_id.to_owned());
state.app_id = Some(app_id.to_owned());
}
fn set_edited(&mut self, _edited: bool) {
log::info!("ignoring macOS specific set_edited");
}
fn show_character_palette(&self) {
log::info!("ignoring macOS specific show_character_palette");
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
let mut state = self.borrow_mut();
state.background_appearance = background_appearance;
update_window(state);
}
fn minimize(&self) {
@@ -830,7 +925,7 @@ impl PlatformWindow for WaylandWindow {
}
fn completed_frame(&self) {
let mut state = self.borrow_mut();
let state = self.borrow();
state.surface.commit();
}
@@ -850,22 +945,173 @@ impl PlatformWindow for WaylandWindow {
);
}
fn start_system_move(&self) {
fn start_window_move(&self) {
let state = self.borrow();
let serial = state.client.get_serial(SerialKind::MousePress);
state.toplevel._move(&state.globals.seat, serial);
}
fn should_render_window_controls(&self) -> bool {
self.borrow().decoration_state == WaylandDecorationState::Client
fn start_window_resize(&self, edge: crate::ResizeEdge) {
let state = self.borrow();
state.toplevel.resize(
&state.globals.seat,
state.client.get_serial(SerialKind::MousePress),
edge.to_xdg(),
)
}
fn window_decorations(&self) -> Decorations {
let state = self.borrow();
match state.decorations {
WindowDecorations::Server => Decorations::Server,
WindowDecorations::Client => Decorations::Client {
tiling: state.tiling,
},
}
}
fn request_decorations(&self, decorations: WindowDecorations) {
let mut state = self.borrow_mut();
state.decorations = decorations;
if let Some(decoration) = state.decoration.as_ref() {
decoration.set_mode(decorations.to_xdg());
update_window(state);
}
}
fn window_controls(&self) -> WindowControls {
self.borrow().window_controls
}
fn set_client_inset(&self, inset: Pixels) {
let mut state = self.borrow_mut();
if Some(inset) != state.inset {
state.inset = Some(inset);
update_window(state);
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum WaylandDecorationState {
/// Decorations are to be provided by the client
Client,
fn update_window(mut state: RefMut<WaylandWindowState>) {
let opaque = !state.is_transparent();
/// Decorations are provided by the server
Server,
state.renderer.update_transparency(!opaque);
let mut opaque_area = state.window_bounds.map(|v| v.0 as i32);
if let Some(inset) = state.inset {
opaque_area.inset(inset.0 as i32);
}
let region = state
.globals
.compositor
.create_region(&state.globals.qh, ());
region.add(
opaque_area.origin.x,
opaque_area.origin.y,
opaque_area.size.width,
opaque_area.size.height,
);
// Note that rounded corners make this rectangle API hard to work with.
// As this is common when using CSD, let's just disable this API.
if state.background_appearance == WindowBackgroundAppearance::Opaque
&& state.decorations == WindowDecorations::Server
{
// Promise the compositor that this region of the window surface
// contains no transparent pixels. This allows the compositor to
// do skip whatever is behind the surface for better performance.
state.surface.set_opaque_region(Some(&region));
} else {
state.surface.set_opaque_region(None);
}
if let Some(ref blur_manager) = state.globals.blur_manager {
if state.background_appearance == WindowBackgroundAppearance::Blurred {
if state.blur.is_none() {
let blur = blur_manager.create(&state.surface, &state.globals.qh, ());
blur.set_region(Some(&region));
state.blur = Some(blur);
}
state.blur.as_ref().unwrap().commit();
} else {
// It probably doesn't hurt to clear the blur for opaque windows
blur_manager.unset(&state.surface);
if let Some(b) = state.blur.take() {
b.release()
}
}
}
region.destroy();
}
impl WindowDecorations {
fn to_xdg(&self) -> zxdg_toplevel_decoration_v1::Mode {
match self {
WindowDecorations::Client => zxdg_toplevel_decoration_v1::Mode::ClientSide,
WindowDecorations::Server => zxdg_toplevel_decoration_v1::Mode::ServerSide,
}
}
}
impl ResizeEdge {
fn to_xdg(&self) -> xdg_toplevel::ResizeEdge {
match self {
ResizeEdge::Top => xdg_toplevel::ResizeEdge::Top,
ResizeEdge::TopRight => xdg_toplevel::ResizeEdge::TopRight,
ResizeEdge::Right => xdg_toplevel::ResizeEdge::Right,
ResizeEdge::BottomRight => xdg_toplevel::ResizeEdge::BottomRight,
ResizeEdge::Bottom => xdg_toplevel::ResizeEdge::Bottom,
ResizeEdge::BottomLeft => xdg_toplevel::ResizeEdge::BottomLeft,
ResizeEdge::Left => xdg_toplevel::ResizeEdge::Left,
ResizeEdge::TopLeft => xdg_toplevel::ResizeEdge::TopLeft,
}
}
}
/// The configuration event is in terms of the window geometry, which we are constantly
/// updating to account for the client decorations. But that's not the area we want to render
/// to, due to our intrusize CSD. So, here we calculate the 'actual' size, by adding back in the insets
fn compute_outer_size(
inset: Option<Pixels>,
new_size: Option<Size<Pixels>>,
tiling: Tiling,
) -> Option<Size<Pixels>> {
let Some(inset) = inset else { return new_size };
new_size.map(|mut new_size| {
if !tiling.top {
new_size.height += inset;
}
if !tiling.bottom {
new_size.height += inset;
}
if !tiling.left {
new_size.width += inset;
}
if !tiling.right {
new_size.width += inset;
}
new_size
})
}
fn inset_by_tiling(mut bounds: Bounds<Pixels>, inset: Pixels, tiling: Tiling) -> Bounds<Pixels> {
if !tiling.top {
bounds.origin.y += inset;
bounds.size.height -= inset;
}
if !tiling.bottom {
bounds.size.height -= inset;
}
if !tiling.left {
bounds.origin.x += inset;
bounds.size.width -= inset;
}
if !tiling.right {
bounds.size.width -= inset;
}
bounds
}

View File

@@ -1,26 +1,23 @@
use std::cell::RefCell;
use std::collections::HashSet;
use std::ops::Deref;
use std::os::fd::AsRawFd;
use std::path::PathBuf;
use std::rc::{Rc, Weak};
use std::sync::Arc;
use std::time::{Duration, Instant};
use anyhow::Context;
use async_task::Runnable;
use calloop::channel::Channel;
use calloop::generic::{FdWrapper, Generic};
use calloop::{EventLoop, LoopHandle, RegistrationToken};
use collections::HashMap;
use futures::channel::oneshot;
use mio::{Interest, Token, Waker};
use util::ResultExt;
use x11rb::connection::{Connection, RequestConnection};
use x11rb::cursor;
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::{ChangeWindowAttributesAux, ConnectionExt as _};
use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _, KeyPressEvent};
use x11rb::protocol::{randr, render, xinput, xkb, xproto, Event};
use x11rb::resource_manager::Database;
use x11rb::xcb_ffi::XCBConnection;
@@ -33,24 +30,24 @@ use crate::platform::linux::LinuxClient;
use crate::platform::{LinuxCommon, PlatformWindow};
use crate::{
modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, ClipboardItem, CursorStyle,
DisplayId, Keystroke, Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay, PlatformInput,
Point, QuitSignal, ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
DisplayId, Keystroke, Modifiers, ModifiersChangedEvent, Pixels, Platform, PlatformDisplay,
PlatformInput, Point, ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
};
use super::{
super::{get_xkb_compose_state, open_uri_internal, SCROLL_LINES},
X11Display, X11WindowStatePtr, XcbAtoms,
};
use super::{button_of_key, modifiers_from_state, pressed_button_from_mask};
use super::{X11Display, X11WindowStatePtr, XcbAtoms};
use super::{XimCallbackEvent, XimHandler};
use crate::platform::linux::is_within_click_distance;
use crate::platform::linux::platform::DOUBLE_CLICK_INTERVAL;
use crate::platform::linux::platform::{DOUBLE_CLICK_INTERVAL, SCROLL_LINES};
use crate::platform::linux::xdg_desktop_portal::{Event as XDPEvent, XDPEventSource};
use crate::platform::linux::{
get_xkb_compose_state, is_within_click_distance, open_uri_internal, reveal_path_internal,
};
pub(super) const XINPUT_MASTER_DEVICE: u16 = 1;
pub(crate) struct WindowRef {
window: X11WindowStatePtr,
refresh_event_token: RegistrationToken,
}
impl WindowRef {
@@ -98,19 +95,17 @@ impl From<xim::ClientError> for EventHandlerError {
}
pub struct X11ClientState {
/// poll is in an Option so we can take it out in `run()` without
/// mutating self.
poll: Option<mio::Poll>,
quit_signal_rx: oneshot::Receiver<()>,
runnables: Channel<Runnable>,
xdp_event_source: XDPEventSource,
pub(crate) loop_handle: LoopHandle<'static, X11Client>,
pub(crate) event_loop: Option<calloop::EventLoop<'static, X11Client>>,
pub(crate) last_click: Instant,
pub(crate) last_location: Point<Pixels>,
pub(crate) current_count: usize,
pub(crate) scale_factor: f32,
pub(crate) xcb_connection: Rc<XCBConnection>,
client_side_decorations_supported: bool,
pub(crate) x_root_index: usize,
pub(crate) _resource_database: Database,
pub(crate) atoms: XcbAtoms,
@@ -124,6 +119,7 @@ pub struct X11ClientState {
pub(crate) compose_state: Option<xkbc::compose::State>,
pub(crate) pre_edit_text: Option<String>,
pub(crate) composing: bool,
pub(crate) pre_ime_key_down: Option<Keystroke>,
pub(crate) cursor_handle: cursor::Handle,
pub(crate) cursor_styles: HashMap<xproto::Window, CursorStyle>,
pub(crate) cursor_cache: HashMap<CursorStyle, xproto::Cursor>,
@@ -145,46 +141,14 @@ impl X11ClientStatePtr {
let client = X11Client(self.0.upgrade().expect("client already dropped"));
let mut state = client.0.borrow_mut();
if state.windows.remove(&x_window).is_none() {
log::warn!(
"failed to remove X window {} from client state, does not exist",
x_window
);
if let Some(window_ref) = state.windows.remove(&x_window) {
state.loop_handle.remove(window_ref.refresh_event_token);
}
state.cursor_styles.remove(&x_window);
if state.windows.is_empty() {
state.common.quit_signal.quit();
}
}
}
struct ChannelQuitSignal {
tx: Option<oneshot::Sender<()>>,
waker: Option<Arc<Waker>>,
}
impl ChannelQuitSignal {
fn new(waker: Option<Arc<Waker>>) -> (Self, oneshot::Receiver<()>) {
let (tx, rx) = oneshot::channel::<()>();
let quit_signal = ChannelQuitSignal {
tx: Some(tx),
waker,
};
(quit_signal, rx)
}
}
impl QuitSignal for ChannelQuitSignal {
fn quit(&mut self) {
if let Some(tx) = self.tx.take() {
tx.send(()).log_err();
if let Some(waker) = self.waker.as_ref() {
waker.wake().ok();
}
state.common.signal.stop();
}
}
}
@@ -194,12 +158,27 @@ pub(crate) struct X11Client(Rc<RefCell<X11ClientState>>);
impl X11Client {
pub(crate) fn new() -> Self {
let mut poll = mio::Poll::new().unwrap();
let event_loop = EventLoop::try_new().unwrap();
let waker = Arc::new(Waker::new(poll.registry(), WAKER_TOKEN).unwrap());
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
let (quit_signal, quit_signal_rx) = ChannelQuitSignal::new(Some(waker.clone()));
let (common, runnables) = LinuxCommon::new(Box::new(quit_signal), Some(waker.clone()));
let handle = event_loop.handle();
handle
.insert_source(main_receiver, {
let handle = handle.clone();
move |event, _, _: &mut X11Client| {
if let calloop::channel::Event::Msg(runnable) = event {
// Insert the runnables as idle callbacks, so we make sure that user-input and X11
// events have higher priority and runnables are only worked off after the event
// callbacks.
handle.insert_idle(|_| {
runnable.run();
});
}
}
})
.unwrap();
let (xcb_connection, x_root_index) = XCBConnection::connect(None).unwrap();
xcb_connection
@@ -241,13 +220,25 @@ impl X11Client {
.map(|class| *class)
.collect::<Vec<_>>();
let atoms = XcbAtoms::new(&xcb_connection).unwrap();
let atoms = XcbAtoms::new(&xcb_connection).unwrap().reply().unwrap();
let root = xcb_connection.setup().roots[0].root;
let compositor_present = check_compositor_present(&xcb_connection, root);
let gtk_frame_extents_supported =
check_gtk_frame_extents_supported(&xcb_connection, &atoms, root);
let client_side_decorations_supported = compositor_present && gtk_frame_extents_supported;
log::info!(
"x11: compositor present: {}, gtk_frame_extents_supported: {}",
compositor_present,
gtk_frame_extents_supported
);
let xkb = xcb_connection
.xkb_use_extension(XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION)
.unwrap()
.reply()
.unwrap();
let atoms = atoms.reply().unwrap();
let xkb = xkb.reply().unwrap();
let events = xkb::EventType::STATE_NOTIFY;
xcb_connection
.xkb_select_events(
@@ -298,24 +289,54 @@ impl X11Client {
None
};
let xdp_event_source =
XDPEventSource::new(&common.background_executor, Some(waker.clone()));
// Safety: Safe if xcb::Connection always returns a valid fd
let fd = unsafe { FdWrapper::new(Rc::clone(&xcb_connection)) };
handle
.insert_source(
Generic::new_with_error::<EventHandlerError>(
fd,
calloop::Interest::READ,
calloop::Mode::Level,
),
{
let xcb_connection = xcb_connection.clone();
move |_readiness, _, client| {
client.process_x11_events(&xcb_connection)?;
Ok(calloop::PostAction::Continue)
}
},
)
.expect("Failed to initialize x11 event source");
handle
.insert_source(XDPEventSource::new(&common.background_executor), {
move |event, _, client| match event {
XDPEvent::WindowAppearance(appearance) => {
client.with_common(|common| common.appearance = appearance);
for (_, window) in &mut client.0.borrow_mut().windows {
window.window.set_appearance(appearance);
}
}
XDPEvent::CursorTheme(_) | XDPEvent::CursorSize(_) => {
// noop, X11 manages this for us.
}
}
})
.unwrap();
X11Client(Rc::new(RefCell::new(X11ClientState {
poll: Some(poll),
runnables,
xdp_event_source,
quit_signal_rx,
common,
modifiers: Modifiers::default(),
event_loop: Some(event_loop),
loop_handle: handle,
common,
last_click: Instant::now(),
last_location: Point::new(px(0.0), px(0.0)),
current_count: 0,
scale_factor,
xcb_connection,
client_side_decorations_supported,
x_root_index,
_resource_database: resource_database,
atoms,
@@ -327,6 +348,7 @@ impl X11Client {
compose_state,
pre_edit_text: None,
pre_ime_key_down: None,
composing: false,
cursor_handle,
@@ -342,6 +364,125 @@ impl X11Client {
})))
}
pub fn process_x11_events(
&self,
xcb_connection: &XCBConnection,
) -> Result<(), EventHandlerError> {
loop {
let mut events = Vec::new();
let mut windows_to_refresh = HashSet::new();
let mut last_key_release = None;
let mut last_key_press: Option<KeyPressEvent> = None;
loop {
match xcb_connection.poll_for_event() {
Ok(Some(event)) => {
match event {
Event::Expose(expose_event) => {
windows_to_refresh.insert(expose_event.window);
}
Event::KeyRelease(_) => {
last_key_release = Some(event);
}
Event::KeyPress(key_press) => {
if let Some(last_press) = last_key_press.as_ref() {
if last_press.detail == key_press.detail {
continue;
}
}
if let Some(Event::KeyRelease(key_release)) =
last_key_release.take()
{
// We ignore that last KeyRelease if it's too close to this KeyPress,
// suggesting that it's auto-generated by X11 as a key-repeat event.
if key_release.detail != key_press.detail
|| key_press.time.saturating_sub(key_release.time) > 20
{
events.push(Event::KeyRelease(key_release));
}
}
events.push(Event::KeyPress(key_press));
last_key_press = Some(key_press);
}
_ => {
if let Some(release_event) = last_key_release.take() {
events.push(release_event);
}
events.push(event);
}
}
}
Ok(None) => {
// Add any remaining stored KeyRelease event
if let Some(release_event) = last_key_release.take() {
events.push(release_event);
}
break;
}
Err(e) => {
log::warn!("error polling for X11 events: {e:?}");
break;
}
}
}
if events.is_empty() && windows_to_refresh.is_empty() {
break;
}
for window in windows_to_refresh.into_iter() {
if let Some(window) = self.get_window(window) {
window.refresh();
}
}
for event in events.into_iter() {
let mut state = self.0.borrow_mut();
if state.ximc.is_none() || state.xim_handler.is_none() {
drop(state);
self.handle_event(event);
continue;
}
let mut ximc = state.ximc.take().unwrap();
let mut xim_handler = state.xim_handler.take().unwrap();
let xim_connected = xim_handler.connected;
drop(state);
let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) {
Ok(handled) => handled,
Err(err) => {
log::error!("XIMClientError: {}", err);
false
}
};
let xim_callback_event = xim_handler.last_callback_event.take();
let mut state = self.0.borrow_mut();
state.ximc = Some(ximc);
state.xim_handler = Some(xim_handler);
drop(state);
if let Some(event) = xim_callback_event {
self.handle_xim_callback_event(event);
}
if xim_filtered {
continue;
}
if xim_connected {
self.xim_handle_event(event);
} else {
self.handle_event(event);
}
}
}
Ok(())
}
pub fn enable_ime(&self) {
let mut state = self.0.borrow_mut();
if state.ximc.is_none() {
@@ -404,115 +545,11 @@ impl X11Client {
.map(|window_reference| window_reference.window.clone())
}
fn read_x11_events(&self) -> (HashSet<u32>, Vec<Event>) {
let mut events = Vec::new();
let mut windows_to_refresh = HashSet::new();
let mut state = self.0.borrow_mut();
let mut last_key_release: Option<Event> = None;
loop {
match state.xcb_connection.poll_for_event() {
Ok(Some(event)) => {
if let Event::Expose(expose_event) = event {
windows_to_refresh.insert(expose_event.window);
} else {
match event {
Event::KeyRelease(_) => {
last_key_release = Some(event);
}
Event::KeyPress(key_press) => {
if let Some(Event::KeyRelease(key_release)) =
last_key_release.take()
{
// We ignore that last KeyRelease if it's too close to this KeyPress,
// suggesting that it's auto-generated by X11 as a key-repeat event.
if key_release.detail != key_press.detail
|| key_press.time.wrapping_sub(key_release.time) > 20
{
events.push(Event::KeyRelease(key_release));
}
}
events.push(Event::KeyPress(key_press));
}
_ => {
if let Some(release_event) = last_key_release.take() {
events.push(release_event);
}
events.push(event);
}
}
}
}
Ok(None) => {
// Add any remaining stored KeyRelease event
if let Some(release_event) = last_key_release.take() {
events.push(release_event);
}
break;
}
Err(e) => {
log::warn!("error polling for X11 events: {e:?}");
break;
}
}
}
(windows_to_refresh, events)
}
fn process_x11_events(&self, events: Vec<Event>) {
for event in events.into_iter() {
let mut state = self.0.borrow_mut();
if state.ximc.is_none() || state.xim_handler.is_none() {
drop(state);
self.handle_event(event);
continue;
}
let mut ximc = state.ximc.take().unwrap();
let mut xim_handler = state.xim_handler.take().unwrap();
let xim_connected = xim_handler.connected;
drop(state);
// let xim_filtered = false;
let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) {
Ok(handled) => handled,
Err(err) => {
log::error!("XIMClientError: {}", err);
false
}
};
let xim_callback_event = xim_handler.last_callback_event.take();
let mut state = self.0.borrow_mut();
state.ximc = Some(ximc);
state.xim_handler = Some(xim_handler);
if let Some(event) = xim_callback_event {
drop(state);
self.handle_xim_callback_event(event);
} else {
drop(state);
}
if xim_filtered {
continue;
}
if xim_connected {
self.xim_handle_event(event);
} else {
self.handle_event(event);
}
}
}
fn handle_event(&self, event: Event) -> Option<()> {
match event {
Event::ClientMessage(event) => {
let window = self.get_window(event.window)?;
let [atom, ..] = event.data.as_data32();
let [atom, _arg1, arg2, arg3, _arg4] = event.data.as_data32();
let mut state = self.0.borrow_mut();
if atom == state.atoms.WM_DELETE_WINDOW {
@@ -521,6 +558,12 @@ impl X11Client {
// Rest of the close logic is handled in drop_window()
window.close();
}
} else if atom == state.atoms._NET_WM_SYNC_REQUEST {
window.state.borrow_mut().last_sync_counter =
Some(x11rb::protocol::sync::Int64 {
lo: arg2,
hi: arg3 as i32,
})
}
}
Event::ConfigureNotify(event) => {
@@ -537,9 +580,9 @@ impl X11Client {
let window = self.get_window(event.window)?;
window.configure(bounds);
}
Event::Expose(event) => {
Event::PropertyNotify(event) => {
let window = self.get_window(event.window)?;
window.refresh();
window.property_notify(event);
}
Event::FocusIn(event) => {
let window = self.get_window(event.event)?;
@@ -568,8 +611,8 @@ impl X11Client {
event.base_mods.into(),
event.latched_mods.into(),
event.locked_mods.into(),
0,
0,
event.base_group as u32,
event.latched_group as u32,
event.locked_group.into(),
);
@@ -593,6 +636,7 @@ impl X11Client {
let modifiers = modifiers_from_state(event.state);
state.modifiers = modifiers;
state.pre_ime_key_down.take();
let keystroke = {
let code = event.detail.into();
@@ -807,10 +851,15 @@ impl X11Client {
if let Some(old_scroll) = old_scroll {
let delta_scroll = old_scroll - new_scroll;
let (x, y) = if !modifiers.shift {
(0.0, delta_scroll)
} else {
(delta_scroll, 0.0)
};
window.handle_input(PlatformInput::ScrollWheel(
crate::ScrollWheelEvent {
position,
delta: ScrollDelta::Lines(Point::new(0.0, delta_scroll)),
delta: ScrollDelta::Lines(Point::new(x, y)),
modifiers,
touch_phase: TouchPhase::default(),
},
@@ -867,6 +916,11 @@ impl X11Client {
match event {
Event::KeyPress(event) | Event::KeyRelease(event) => {
let mut state = self.0.borrow_mut();
state.pre_ime_key_down = Some(Keystroke::from_xkb(
&state.xkb,
state.modifiers,
event.detail.into(),
));
let mut ximc = state.ximc.take().unwrap();
let mut xim_handler = state.xim_handler.take().unwrap();
drop(state);
@@ -893,6 +947,16 @@ impl X11Client {
fn xim_handle_commit(&self, window: xproto::Window, text: String) -> Option<()> {
let window = self.get_window(window).unwrap();
let mut state = self.0.borrow_mut();
if !state.composing {
if let Some(keystroke) = state.pre_ime_key_down.take() {
drop(state);
window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent {
keystroke,
is_held: false,
}));
return Some(());
}
}
state.composing = false;
drop(state);
@@ -942,13 +1006,11 @@ impl X11Client {
}
}
const XCB_CONNECTION_TOKEN: Token = Token(0);
const WAKER_TOKEN: Token = Token(1);
impl LinuxClient for X11Client {
fn compositor_name(&self) -> &'static str {
"X11"
}
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R {
f(&mut self.0.borrow_mut().common)
}
@@ -1007,6 +1069,7 @@ impl LinuxClient for X11Client {
state.common.foreground_executor.clone(),
params,
&state.xcb_connection,
state.client_side_decorations_supported,
state.x_root_index,
x_window,
&state.atoms,
@@ -1014,8 +1077,61 @@ impl LinuxClient for X11Client {
state.common.appearance,
)?;
let screen_resources = state
.xcb_connection
.randr_get_screen_resources(x_window)
.unwrap()
.reply()
.expect("Could not find available screens");
let mode = screen_resources
.crtcs
.iter()
.find_map(|crtc| {
let crtc_info = state
.xcb_connection
.randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME)
.ok()?
.reply()
.ok()?;
screen_resources
.modes
.iter()
.find(|m| m.id == crtc_info.mode)
})
.expect("Unable to find screen refresh rate");
let refresh_event_token = state
.loop_handle
.insert_source(calloop::timer::Timer::immediate(), {
let refresh_duration = mode_refresh_rate(mode);
move |mut instant, (), client| {
let xcb_connection = {
let state = client.0.borrow_mut();
let xcb_connection = state.xcb_connection.clone();
if let Some(window) = state.windows.get(&x_window) {
let window = window.window.clone();
drop(state);
window.refresh();
}
xcb_connection
};
client.process_x11_events(&xcb_connection).log_err();
// Take into account that some frames have been skipped
let now = Instant::now();
while instant < now {
instant += refresh_duration;
}
calloop::timer::TimeoutAction::ToInstant(instant)
}
})
.expect("Failed to initialize refresh timer");
let window_ref = WindowRef {
window: window.0.clone(),
refresh_event_token,
};
state.windows.insert(x_window, window_ref);
@@ -1057,11 +1173,17 @@ impl LinuxClient for X11Client {
..Default::default()
},
)
.expect("failed to change window cursor");
.expect("failed to change window cursor")
.check()
.unwrap();
}
fn open_uri(&self, uri: &str) {
open_uri_internal(uri, None);
open_uri_internal(self.background_executor(), uri, None);
}
fn reveal_path(&self, path: PathBuf) {
reveal_path_internal(self.background_executor(), path, None);
}
fn write_to_primary(&self, item: crate::ClipboardItem) {
@@ -1138,123 +1260,14 @@ impl LinuxClient for X11Client {
}
fn run(&self) {
let mut poll = self
let mut event_loop = self
.0
.borrow_mut()
.poll
.event_loop
.take()
.context("no poll set on X11Client. calling run more than once is not possible")
.unwrap();
.expect("App is already running");
let xcb_fd = self.0.borrow().xcb_connection.as_raw_fd();
let mut xcb_source = mio::unix::SourceFd(&xcb_fd);
poll.registry()
.register(&mut xcb_source, XCB_CONNECTION_TOKEN, Interest::READABLE)
.unwrap();
let mut events = mio::Events::with_capacity(1024);
let mut next_refresh_needed = Instant::now();
'run_loop: loop {
let poll_timeout = next_refresh_needed - Instant::now();
// We rounding the poll_timeout down so `mio` doesn't round it up to the next higher milliseconds
let poll_timeout = Duration::from_millis(poll_timeout.as_millis() as u64);
if poll_timeout >= Duration::from_millis(1) {
let _ = poll.poll(&mut events, Some(poll_timeout));
};
let mut state = self.0.borrow_mut();
// Check if we need to quit
if let Ok(Some(())) = state.quit_signal_rx.try_recv() {
return;
}
// Redraw windows
let now = Instant::now();
if now > next_refresh_needed {
// This will be pulled down to 16ms (or less) if a window is open
let mut frame_length = Duration::from_millis(100);
let mut windows = vec![];
for (_, window_ref) in state.windows.iter() {
if !window_ref.window.state.borrow().destroyed {
frame_length = frame_length.min(window_ref.window.refresh_rate());
windows.push(window_ref.window.clone());
}
}
drop(state);
for window in windows {
window.refresh();
}
state = self.0.borrow_mut();
// In the case that we're looping a bit too fast, slow down
next_refresh_needed = now.max(next_refresh_needed) + frame_length;
}
// X11 events
drop(state);
loop {
let (x_windows, events) = self.read_x11_events();
for x_window in x_windows {
if let Some(window) = self.get_window(x_window) {
window.refresh();
}
}
if events.len() == 0 {
break;
}
self.process_x11_events(events);
// When X11 is sending us events faster than we can handle we'll
// let the frame rate drop to 10fps to try and avoid getting too behind.
if Instant::now() > next_refresh_needed + Duration::from_millis(80) {
continue 'run_loop;
}
}
state = self.0.borrow_mut();
// Runnables
while let Ok(runnable) = state.runnables.try_recv() {
drop(state);
runnable.run();
state = self.0.borrow_mut();
if Instant::now() + Duration::from_millis(1) >= next_refresh_needed {
continue 'run_loop;
}
}
// XDG events
if let Ok(event) = state.xdp_event_source.try_recv() {
match event {
XDPEvent::WindowAppearance(appearance) => {
let mut windows = state
.windows
.values()
.map(|window| window.window.clone())
.collect::<Vec<_>>();
drop(state);
self.with_common(|common| common.appearance = appearance);
for mut window in windows {
window.set_appearance(appearance);
}
}
XDPEvent::CursorTheme(_) | XDPEvent::CursorSize(_) => {
// noop, X11 manages this for us.
}
};
};
}
event_loop.run(None, &mut self.clone(), |_| {}).log_err();
}
fn active_window(&self) -> Option<AnyWindowHandle> {
@@ -1268,6 +1281,120 @@ impl LinuxClient for X11Client {
}
}
// Adatpted from:
// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111
pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
if mode.dot_clock == 0 || mode.htotal == 0 || mode.vtotal == 0 {
return Duration::from_millis(16);
}
let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64);
let micros = 1_000_000_000 / millihertz;
log::info!("Refreshing at {} micros", micros);
Duration::from_micros(micros)
}
fn fp3232_to_f32(value: xinput::Fp3232) -> f32 {
value.integral as f32 + value.frac as f32 / u32::MAX as f32
}
fn check_compositor_present(xcb_connection: &XCBConnection, root: u32) -> bool {
// Method 1: Check for _NET_WM_CM_S{root}
let atom_name = format!("_NET_WM_CM_S{}", root);
let atom = xcb_connection
.intern_atom(false, atom_name.as_bytes())
.unwrap()
.reply()
.map(|reply| reply.atom)
.unwrap_or(0);
let method1 = if atom != 0 {
xcb_connection
.get_selection_owner(atom)
.unwrap()
.reply()
.map(|reply| reply.owner != 0)
.unwrap_or(false)
} else {
false
};
// Method 2: Check for _NET_WM_CM_OWNER
let atom_name = "_NET_WM_CM_OWNER";
let atom = xcb_connection
.intern_atom(false, atom_name.as_bytes())
.unwrap()
.reply()
.map(|reply| reply.atom)
.unwrap_or(0);
let method2 = if atom != 0 {
xcb_connection
.get_property(false, root, atom, xproto::AtomEnum::WINDOW, 0, 1)
.unwrap()
.reply()
.map(|reply| reply.value_len > 0)
.unwrap_or(false)
} else {
false
};
// Method 3: Check for _NET_SUPPORTING_WM_CHECK
let atom_name = "_NET_SUPPORTING_WM_CHECK";
let atom = xcb_connection
.intern_atom(false, atom_name.as_bytes())
.unwrap()
.reply()
.map(|reply| reply.atom)
.unwrap_or(0);
let method3 = if atom != 0 {
xcb_connection
.get_property(false, root, atom, xproto::AtomEnum::WINDOW, 0, 1)
.unwrap()
.reply()
.map(|reply| reply.value_len > 0)
.unwrap_or(false)
} else {
false
};
// TODO: Remove this
log::info!(
"Compositor detection: _NET_WM_CM_S?={}, _NET_WM_CM_OWNER={}, _NET_SUPPORTING_WM_CHECK={}",
method1,
method2,
method3
);
method1 || method2 || method3
}
fn check_gtk_frame_extents_supported(
xcb_connection: &XCBConnection,
atoms: &XcbAtoms,
root: xproto::Window,
) -> bool {
let supported_atoms = xcb_connection
.get_property(
false,
root,
atoms._NET_SUPPORTED,
xproto::AtomEnum::ATOM,
0,
1024,
)
.unwrap()
.reply()
.map(|reply| {
// Convert Vec<u8> to Vec<u32>
reply
.value
.chunks_exact(4)
.map(|chunk| u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
.collect::<Vec<u32>>()
})
.unwrap_or_default();
supported_atoms.contains(&atoms._GTK_FRAME_EXTENTS)
}

View File

@@ -2,10 +2,11 @@ use anyhow::Context;
use crate::{
platform::blade::{BladeRenderer, BladeSurfaceConfig},
px, size, AnyWindowHandle, Bounds, DevicePixels, ForegroundExecutor, Modifiers, Pixels,
PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
PromptLevel, Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
WindowKind, WindowParams, X11ClientStatePtr,
px, size, AnyWindowHandle, Bounds, Decorations, DevicePixels, ForegroundExecutor, Modifiers,
Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow,
Point, PromptLevel, ResizeEdge, Scene, Size, Tiling, WindowAppearance,
WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowParams,
X11ClientStatePtr,
};
use blade_graphics as gpu;
@@ -14,25 +15,17 @@ use util::{maybe, ResultExt};
use x11rb::{
connection::Connection,
protocol::{
randr::{self, ConnectionExt as _},
sync,
xinput::{self, ConnectionExt as _},
xproto::{
self, ClientMessageEvent, ConnectionExt as _, EventMask, TranslateCoordinatesReply,
},
xproto::{self, ClientMessageEvent, ConnectionExt, EventMask, TranslateCoordinatesReply},
},
wrapper::ConnectionExt as _,
xcb_ffi::XCBConnection,
};
use std::{
cell::RefCell,
ffi::c_void,
num::NonZeroU32,
ops::Div,
ptr::NonNull,
rc::Rc,
sync::{self, Arc},
time::Duration,
cell::RefCell, ffi::c_void, mem::size_of, num::NonZeroU32, ops::Div, ptr::NonNull, rc::Rc,
sync::Arc,
};
use super::{X11Display, XINPUT_MASTER_DEVICE};
@@ -50,10 +43,18 @@ x11rb::atom_manager! {
_NET_WM_STATE_HIDDEN,
_NET_WM_STATE_FOCUSED,
_NET_ACTIVE_WINDOW,
_NET_WM_SYNC_REQUEST,
_NET_WM_SYNC_REQUEST_COUNTER,
_NET_WM_BYPASS_COMPOSITOR,
_NET_WM_MOVERESIZE,
_NET_WM_WINDOW_TYPE,
_NET_WM_WINDOW_TYPE_NOTIFICATION,
_NET_WM_SYNC,
_NET_SUPPORTED,
_MOTIF_WM_HINTS,
_GTK_SHOW_WINDOW_MENU,
_GTK_FRAME_EXTENTS,
_GTK_EDGE_CONSTRAINTS,
}
}
@@ -70,6 +71,64 @@ fn query_render_extent(xcb_connection: &XCBConnection, x_window: xproto::Window)
}
}
impl ResizeEdge {
fn to_moveresize(&self) -> u32 {
match self {
ResizeEdge::TopLeft => 0,
ResizeEdge::Top => 1,
ResizeEdge::TopRight => 2,
ResizeEdge::Right => 3,
ResizeEdge::BottomRight => 4,
ResizeEdge::Bottom => 5,
ResizeEdge::BottomLeft => 6,
ResizeEdge::Left => 7,
}
}
}
#[derive(Debug)]
struct EdgeConstraints {
top_tiled: bool,
#[allow(dead_code)]
top_resizable: bool,
right_tiled: bool,
#[allow(dead_code)]
right_resizable: bool,
bottom_tiled: bool,
#[allow(dead_code)]
bottom_resizable: bool,
left_tiled: bool,
#[allow(dead_code)]
left_resizable: bool,
}
impl EdgeConstraints {
fn from_atom(atom: u32) -> Self {
EdgeConstraints {
top_tiled: (atom & (1 << 0)) != 0,
top_resizable: (atom & (1 << 1)) != 0,
right_tiled: (atom & (1 << 2)) != 0,
right_resizable: (atom & (1 << 3)) != 0,
bottom_tiled: (atom & (1 << 4)) != 0,
bottom_resizable: (atom & (1 << 5)) != 0,
left_tiled: (atom & (1 << 6)) != 0,
left_resizable: (atom & (1 << 7)) != 0,
}
}
fn to_tiling(&self) -> Tiling {
Tiling {
top: self.top_tiled,
right: self.right_tiled,
bottom: self.bottom_tiled,
left: self.left_tiled,
}
}
}
#[derive(Debug)]
struct Visual {
id: xproto::Visualid,
@@ -161,11 +220,12 @@ pub struct Callbacks {
pub struct X11WindowState {
pub destroyed: bool,
refresh_rate: Duration,
client: X11ClientStatePtr,
executor: ForegroundExecutor,
atoms: XcbAtoms,
x_root_window: xproto::Window,
pub(crate) counter_id: sync::Counter,
pub(crate) last_sync_counter: Option<sync::Int64>,
_raw: RawWindow,
bounds: Bounds<Pixels>,
scale_factor: f32,
@@ -173,7 +233,23 @@ pub struct X11WindowState {
display: Rc<dyn PlatformDisplay>,
input_handler: Option<PlatformInputHandler>,
appearance: WindowAppearance,
background_appearance: WindowBackgroundAppearance,
maximized_vertical: bool,
maximized_horizontal: bool,
hidden: bool,
active: bool,
fullscreen: bool,
client_side_decorations_supported: bool,
decorations: WindowDecorations,
edge_constraints: Option<EdgeConstraints>,
pub handle: AnyWindowHandle,
last_insets: [u32; 4],
}
impl X11WindowState {
fn is_transparent(&self) -> bool {
self.background_appearance != WindowBackgroundAppearance::Opaque
}
}
#[derive(Clone)]
@@ -181,7 +257,7 @@ pub(crate) struct X11WindowStatePtr {
pub state: Rc<RefCell<X11WindowState>>,
pub(crate) callbacks: Rc<RefCell<Callbacks>>,
xcb_connection: Rc<XCBConnection>,
pub x_window: xproto::Window,
x_window: xproto::Window,
}
impl rwh::HasWindowHandle for RawWindow {
@@ -219,6 +295,7 @@ impl X11WindowState {
executor: ForegroundExecutor,
params: WindowParams,
xcb_connection: &Rc<XCBConnection>,
client_side_decorations_supported: bool,
x_main_screen_index: usize,
x_window: xproto::Window,
atoms: &XcbAtoms,
@@ -230,19 +307,11 @@ impl X11WindowState {
.map_or(x_main_screen_index, |did| did.0 as usize);
let visual_set = find_visuals(&xcb_connection, x_screen_index);
let visual_maybe = match params.window_background {
WindowBackgroundAppearance::Opaque => visual_set.opaque,
WindowBackgroundAppearance::Transparent | WindowBackgroundAppearance::Blurred => {
visual_set.transparent
}
};
let visual = match visual_maybe {
let visual = match visual_set.transparent {
Some(visual) => visual,
None => {
log::warn!(
"Unable to find a matching visual for {:?}",
params.window_background
);
log::warn!("Unable to find a transparent visual",);
visual_set.inherit
}
};
@@ -269,7 +338,8 @@ impl X11WindowState {
| xproto::EventMask::STRUCTURE_NOTIFY
| xproto::EventMask::FOCUS_CHANGE
| xproto::EventMask::KEY_PRESS
| xproto::EventMask::KEY_RELEASE,
| xproto::EventMask::KEY_RELEASE
| EventMask::PROPERTY_CHANGE,
);
let mut bounds = params.bounds.to_device_pixels(scale_factor);
@@ -349,7 +419,26 @@ impl X11WindowState {
x_window,
atoms.WM_PROTOCOLS,
xproto::AtomEnum::ATOM,
&[atoms.WM_DELETE_WINDOW],
&[atoms.WM_DELETE_WINDOW, atoms._NET_WM_SYNC_REQUEST],
)
.unwrap();
sync::initialize(xcb_connection, 3, 1).unwrap();
let sync_request_counter = xcb_connection.generate_id().unwrap();
sync::create_counter(
xcb_connection,
sync_request_counter,
sync::Int64 { lo: 0, hi: 0 },
)
.unwrap();
xcb_connection
.change_property32(
xproto::PropMode::REPLACE,
x_window,
atoms._NET_WM_SYNC_REQUEST_COUNTER,
xproto::AtomEnum::CARDINAL,
&[sync_request_counter],
)
.unwrap();
@@ -396,35 +485,14 @@ impl X11WindowState {
// Note: this has to be done after the GPU init, or otherwise
// the sizes are immediately invalidated.
size: query_render_extent(xcb_connection, x_window),
transparent: params.window_background != WindowBackgroundAppearance::Opaque,
// We set it to transparent by default, even if we have client-side
// decorations, since those seem to work on X11 even without `true` here.
// If the window appearance changes, then the renderer will get updated
// too
transparent: false,
};
xcb_connection.map_window(x_window).unwrap();
let screen_resources = xcb_connection
.randr_get_screen_resources(x_window)
.unwrap()
.reply()
.expect("Could not find available screens");
let mode = screen_resources
.crtcs
.iter()
.find_map(|crtc| {
let crtc_info = xcb_connection
.randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME)
.ok()?
.reply()
.ok()?;
screen_resources
.modes
.iter()
.find(|m| m.id == crtc_info.mode)
})
.expect("Unable to find screen refresh rate");
let refresh_rate = mode_refresh_rate(&mode);
Ok(Self {
client,
executor,
@@ -438,10 +506,21 @@ impl X11WindowState {
renderer: BladeRenderer::new(gpu, config),
atoms: *atoms,
input_handler: None,
active: false,
fullscreen: false,
maximized_vertical: false,
maximized_horizontal: false,
hidden: false,
appearance,
handle,
background_appearance: WindowBackgroundAppearance::Opaque,
destroyed: false,
refresh_rate,
client_side_decorations_supported,
decorations: WindowDecorations::Server,
last_insets: [0, 0, 0, 0],
edge_constraints: None,
counter_id: sync_request_counter,
last_sync_counter: None,
})
}
@@ -505,19 +584,21 @@ impl X11Window {
executor: ForegroundExecutor,
params: WindowParams,
xcb_connection: &Rc<XCBConnection>,
client_side_decorations_supported: bool,
x_main_screen_index: usize,
x_window: xproto::Window,
atoms: &XcbAtoms,
scale_factor: f32,
appearance: WindowAppearance,
) -> anyhow::Result<Self> {
Ok(Self(X11WindowStatePtr {
let ptr = X11WindowStatePtr {
state: Rc::new(RefCell::new(X11WindowState::new(
handle,
client,
executor,
params,
xcb_connection,
client_side_decorations_supported,
x_main_screen_index,
x_window,
atoms,
@@ -527,7 +608,12 @@ impl X11Window {
callbacks: Rc::new(RefCell::new(Callbacks::default())),
xcb_connection: xcb_connection.clone(),
x_window,
}))
};
let state = ptr.state.borrow_mut();
ptr.set_wm_properties(state);
Ok(Self(ptr))
}
fn set_wm_hints(&self, wm_hint_property_state: WmHintPropertyState, prop1: u32, prop2: u32) {
@@ -546,30 +632,9 @@ impl X11Window {
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
message,
)
.unwrap();
}
fn get_wm_hints(&self) -> Vec<u32> {
let reply = self
.0
.xcb_connection
.get_property(
false,
self.0.x_window,
self.0.state.borrow().atoms._NET_WM_STATE,
xproto::AtomEnum::ATOM,
0,
u32::MAX,
)
.unwrap()
.reply()
.check()
.unwrap();
// Reply is in u8 but atoms are represented as u32
reply
.value
.chunks_exact(4)
.map(|chunk| u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
.collect()
}
fn get_root_position(&self, position: Point<Pixels>) -> TranslateCoordinatesReply {
@@ -586,6 +651,48 @@ impl X11Window {
.reply()
.unwrap()
}
fn send_moveresize(&self, flag: u32) {
let state = self.0.state.borrow();
self.0
.xcb_connection
.ungrab_pointer(x11rb::CURRENT_TIME)
.unwrap()
.check()
.unwrap();
let pointer = self
.0
.xcb_connection
.query_pointer(self.0.x_window)
.unwrap()
.reply()
.unwrap();
let message = ClientMessageEvent::new(
32,
self.0.x_window,
state.atoms._NET_WM_MOVERESIZE,
[
pointer.root_x as u32,
pointer.root_y as u32,
flag,
0, // Left mouse button
0,
],
);
self.0
.xcb_connection
.send_event(
false,
state.x_root_window,
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
message,
)
.unwrap();
self.0.xcb_connection.flush().unwrap();
}
}
impl X11WindowStatePtr {
@@ -600,6 +707,78 @@ impl X11WindowStatePtr {
}
}
pub fn property_notify(&self, event: xproto::PropertyNotifyEvent) {
let mut state = self.state.borrow_mut();
if event.atom == state.atoms._NET_WM_STATE {
self.set_wm_properties(state);
} else if event.atom == state.atoms._GTK_EDGE_CONSTRAINTS {
self.set_edge_constraints(state);
}
}
fn set_edge_constraints(&self, mut state: std::cell::RefMut<X11WindowState>) {
let reply = self
.xcb_connection
.get_property(
false,
self.x_window,
state.atoms._GTK_EDGE_CONSTRAINTS,
xproto::AtomEnum::CARDINAL,
0,
4,
)
.unwrap()
.reply()
.unwrap();
if reply.value_len != 0 {
let atom = u32::from_ne_bytes(reply.value[0..4].try_into().unwrap());
let edge_constraints = EdgeConstraints::from_atom(atom);
state.edge_constraints.replace(edge_constraints);
}
}
fn set_wm_properties(&self, mut state: std::cell::RefMut<X11WindowState>) {
let reply = self
.xcb_connection
.get_property(
false,
self.x_window,
state.atoms._NET_WM_STATE,
xproto::AtomEnum::ATOM,
0,
u32::MAX,
)
.unwrap()
.reply()
.unwrap();
let atoms = reply
.value
.chunks_exact(4)
.map(|chunk| u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
state.active = false;
state.fullscreen = false;
state.maximized_vertical = false;
state.maximized_horizontal = false;
state.hidden = true;
for atom in atoms {
if atom == state.atoms._NET_WM_STATE_FOCUSED {
state.active = true;
} else if atom == state.atoms._NET_WM_STATE_FULLSCREEN {
state.fullscreen = true;
} else if atom == state.atoms._NET_WM_STATE_MAXIMIZED_VERT {
state.maximized_vertical = true;
} else if atom == state.atoms._NET_WM_STATE_MAXIMIZED_HORZ {
state.maximized_horizontal = true;
} else if atom == state.atoms._NET_WM_STATE_HIDDEN {
state.hidden = true;
}
}
}
pub fn close(&self) {
let mut callbacks = self.callbacks.borrow_mut();
if let Some(fun) = callbacks.close.take() {
@@ -715,6 +894,9 @@ impl X11WindowStatePtr {
));
resize_args = Some((state.content_size(), state.scale_factor));
}
if let Some(value) = state.last_sync_counter.take() {
sync::set_counter(&self.xcb_connection, state.counter_id, value).unwrap();
}
}
let mut callbacks = self.callbacks.borrow_mut();
@@ -737,17 +919,17 @@ impl X11WindowStatePtr {
}
pub fn set_appearance(&mut self, appearance: WindowAppearance) {
self.state.borrow_mut().appearance = appearance;
let mut state = self.state.borrow_mut();
state.appearance = appearance;
let is_transparent = state.is_transparent();
state.renderer.update_transparency(is_transparent);
state.appearance = appearance;
drop(state);
let mut callbacks = self.callbacks.borrow_mut();
if let Some(ref mut fun) = callbacks.appearance_changed {
(fun)()
}
}
pub fn refresh_rate(&self) -> Duration {
self.state.borrow().refresh_rate
}
}
impl PlatformWindow for X11Window {
@@ -757,11 +939,9 @@ impl PlatformWindow for X11Window {
fn is_maximized(&self) -> bool {
let state = self.0.state.borrow();
let wm_hints = self.get_wm_hints();
// A maximized window that gets minimized will still retain its maximized state.
!wm_hints.contains(&state.atoms._NET_WM_STATE_HIDDEN)
&& wm_hints.contains(&state.atoms._NET_WM_STATE_MAXIMIZED_VERT)
&& wm_hints.contains(&state.atoms._NET_WM_STATE_MAXIMIZED_HORZ)
!state.hidden && state.maximized_vertical && state.maximized_horizontal
}
fn window_bounds(&self) -> WindowBounds {
@@ -859,12 +1039,11 @@ impl PlatformWindow for X11Window {
xproto::Time::CURRENT_TIME,
)
.log_err();
self.0.xcb_connection.flush().unwrap();
}
fn is_active(&self) -> bool {
let state = self.0.state.borrow();
self.get_wm_hints()
.contains(&state.atoms._NET_WM_STATE_FOCUSED)
self.0.state.borrow().active
}
fn set_title(&mut self, title: &str) {
@@ -889,6 +1068,7 @@ impl PlatformWindow for X11Window {
title.as_bytes(),
)
.unwrap();
self.0.xcb_connection.flush().unwrap();
}
fn set_app_id(&mut self, app_id: &str) {
@@ -906,6 +1086,8 @@ impl PlatformWindow for X11Window {
xproto::AtomEnum::STRING,
&data,
)
.unwrap()
.check()
.unwrap();
}
@@ -913,10 +1095,11 @@ impl PlatformWindow for X11Window {
log::info!("ignoring macOS specific set_edited");
}
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
let mut inner = self.0.state.borrow_mut();
let transparent = background_appearance != WindowBackgroundAppearance::Opaque;
inner.renderer.update_transparency(transparent);
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
let mut state = self.0.state.borrow_mut();
state.background_appearance = background_appearance;
let transparent = state.is_transparent();
state.renderer.update_transparency(transparent);
}
fn show_character_palette(&self) {
@@ -940,6 +1123,8 @@ impl PlatformWindow for X11Window {
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
message,
)
.unwrap()
.check()
.unwrap();
}
@@ -962,9 +1147,7 @@ impl PlatformWindow for X11Window {
}
fn is_fullscreen(&self) -> bool {
let state = self.0.state.borrow();
self.get_wm_hints()
.contains(&state.atoms._NET_WM_STATE_FULLSCREEN)
self.0.state.borrow().fullscreen
}
fn on_request_frame(&self, callback: Box<dyn FnMut()>) {
@@ -1004,7 +1187,7 @@ impl PlatformWindow for X11Window {
inner.renderer.draw(scene);
}
fn sprite_atlas(&self) -> sync::Arc<dyn PlatformAtlas> {
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
let inner = self.0.state.borrow();
inner.renderer.sprite_atlas().clone()
}
@@ -1032,56 +1215,144 @@ impl PlatformWindow for X11Window {
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
message,
)
.unwrap()
.check()
.unwrap();
}
fn start_system_move(&self) {
let state = self.0.state.borrow();
let pointer = self
.0
.xcb_connection
.query_pointer(self.0.x_window)
.unwrap()
.reply()
.unwrap();
fn start_window_move(&self) {
const MOVERESIZE_MOVE: u32 = 8;
let message = ClientMessageEvent::new(
32,
self.0.x_window,
state.atoms._NET_WM_MOVERESIZE,
[
pointer.root_x as u32,
pointer.root_y as u32,
MOVERESIZE_MOVE,
1, // Left mouse button
1,
],
);
self.send_moveresize(MOVERESIZE_MOVE);
}
fn start_window_resize(&self, edge: ResizeEdge) {
self.send_moveresize(edge.to_moveresize());
}
fn window_decorations(&self) -> crate::Decorations {
let state = self.0.state.borrow();
// Client window decorations require compositor support
if !state.client_side_decorations_supported {
return Decorations::Server;
}
match state.decorations {
WindowDecorations::Server => Decorations::Server,
WindowDecorations::Client => {
let tiling = if let Some(edge_constraints) = &state.edge_constraints {
edge_constraints.to_tiling()
} else {
// https://source.chromium.org/chromium/chromium/src/+/main:ui/ozone/platform/x11/x11_window.cc;l=2519;drc=1f14cc876cc5bf899d13284a12c451498219bb2d
Tiling {
top: state.maximized_vertical,
bottom: state.maximized_vertical,
left: state.maximized_horizontal,
right: state.maximized_horizontal,
}
};
Decorations::Client { tiling }
}
}
}
fn set_client_inset(&self, inset: Pixels) {
let mut state = self.0.state.borrow_mut();
let dp = (inset.0 * state.scale_factor) as u32;
let insets = if let Some(edge_constraints) = &state.edge_constraints {
let left = if edge_constraints.left_tiled { 0 } else { dp };
let top = if edge_constraints.top_tiled { 0 } else { dp };
let right = if edge_constraints.right_tiled { 0 } else { dp };
let bottom = if edge_constraints.bottom_tiled { 0 } else { dp };
[left, right, top, bottom]
} else {
let (left, right) = if state.maximized_horizontal {
(0, 0)
} else {
(dp, dp)
};
let (top, bottom) = if state.maximized_vertical {
(0, 0)
} else {
(dp, dp)
};
[left, right, top, bottom]
};
if state.last_insets != insets {
state.last_insets = insets;
self.0
.xcb_connection
.change_property(
xproto::PropMode::REPLACE,
self.0.x_window,
state.atoms._GTK_FRAME_EXTENTS,
xproto::AtomEnum::CARDINAL,
size_of::<u32>() as u8 * 8,
4,
bytemuck::cast_slice::<u32, u8>(&insets),
)
.unwrap()
.check()
.unwrap();
}
}
fn request_decorations(&self, mut decorations: crate::WindowDecorations) {
let mut state = self.0.state.borrow_mut();
if matches!(decorations, crate::WindowDecorations::Client)
&& !state.client_side_decorations_supported
{
log::info!(
"x11: no compositor present, falling back to server-side window decorations"
);
decorations = crate::WindowDecorations::Server;
}
// https://github.com/rust-windowing/winit/blob/master/src/platform_impl/linux/x11/util/hint.rs#L53-L87
let hints_data: [u32; 5] = match decorations {
WindowDecorations::Server => [1 << 1, 0, 1, 0, 0],
WindowDecorations::Client => [1 << 1, 0, 0, 0, 0],
};
self.0
.xcb_connection
.send_event(
false,
state.x_root_window,
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
message,
.change_property(
xproto::PropMode::REPLACE,
self.0.x_window,
state.atoms._MOTIF_WM_HINTS,
state.atoms._MOTIF_WM_HINTS,
std::mem::size_of::<u32>() as u8 * 8,
5,
bytemuck::cast_slice::<u32, u8>(&hints_data),
)
.unwrap()
.check()
.unwrap();
}
fn should_render_window_controls(&self) -> bool {
false
match decorations {
WindowDecorations::Server => {
state.decorations = WindowDecorations::Server;
let is_transparent = state.is_transparent();
state.renderer.update_transparency(is_transparent);
}
WindowDecorations::Client => {
state.decorations = WindowDecorations::Client;
let is_transparent = state.is_transparent();
state.renderer.update_transparency(is_transparent);
}
}
drop(state);
let mut callbacks = self.0.callbacks.borrow_mut();
if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() {
appearance_changed();
}
}
}
// Adapted from:
// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111
pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
if mode.dot_clock == 0 || mode.htotal == 0 || mode.vtotal == 0 {
return Duration::from_millis(16);
}
let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64);
let micros = 1_000_000_000 / millihertz;
log::info!("Refreshing at {} micros", micros);
Duration::from_micros(micros)
}

View File

@@ -2,13 +2,9 @@
//!
//! This module uses the [ashpd] crate
use std::sync::Arc;
use anyhow::anyhow;
use ashpd::desktop::settings::{ColorScheme, Settings};
use calloop::channel::{Channel, Sender};
use calloop::channel::Channel;
use calloop::{EventSource, Poll, PostAction, Readiness, Token, TokenFactory};
use mio::Waker;
use smol::stream::StreamExt;
use crate::{BackgroundExecutor, WindowAppearance};
@@ -24,45 +20,31 @@ pub struct XDPEventSource {
}
impl XDPEventSource {
pub fn new(executor: &BackgroundExecutor, waker: Option<Arc<Waker>>) -> Self {
pub fn new(executor: &BackgroundExecutor) -> Self {
let (sender, channel) = calloop::channel::channel();
let background = executor.clone();
executor
.spawn(async move {
fn send_event<T>(
sender: &Sender<T>,
waker: &Option<Arc<Waker>>,
event: T,
) -> Result<(), std::sync::mpsc::SendError<T>> {
sender.send(event)?;
if let Some(waker) = waker {
waker.wake().ok();
};
Ok(())
}
let settings = Settings::new().await?;
if let Ok(initial_appearance) = settings.color_scheme().await {
send_event(
&sender,
&waker,
Event::WindowAppearance(WindowAppearance::from_native(initial_appearance)),
)?;
sender.send(Event::WindowAppearance(WindowAppearance::from_native(
initial_appearance,
)))?;
}
if let Ok(initial_theme) = settings
.read::<String>("org.gnome.desktop.interface", "cursor-theme")
.await
{
send_event(&sender, &waker, Event::CursorTheme(initial_theme))?;
sender.send(Event::CursorTheme(initial_theme))?;
}
if let Ok(initial_size) = settings
.read::<u32>("org.gnome.desktop.interface", "cursor-size")
.await
{
send_event(&sender, &waker, Event::CursorSize(initial_size))?;
sender.send(Event::CursorSize(initial_size))?;
}
if let Ok(mut cursor_theme_changed) = settings
@@ -73,12 +55,11 @@ impl XDPEventSource {
.await
{
let sender = sender.clone();
let waker = waker.clone();
background
.spawn(async move {
while let Some(theme) = cursor_theme_changed.next().await {
let theme = theme?;
send_event(&sender, &waker, Event::CursorTheme(theme))?;
sender.send(Event::CursorTheme(theme))?;
}
anyhow::Ok(())
})
@@ -93,12 +74,11 @@ impl XDPEventSource {
.await
{
let sender = sender.clone();
let waker = waker.clone();
background
.spawn(async move {
while let Some(size) = cursor_size_changed.next().await {
let size = size?;
send_event(&sender, &waker, Event::CursorSize(size))?;
sender.send(Event::CursorSize(size))?;
}
anyhow::Ok(())
})
@@ -107,11 +87,9 @@ impl XDPEventSource {
let mut appearance_changed = settings.receive_color_scheme_changed().await?;
while let Some(scheme) = appearance_changed.next().await {
send_event(
&sender,
&waker,
Event::WindowAppearance(WindowAppearance::from_native(scheme)),
)?;
sender.send(Event::WindowAppearance(WindowAppearance::from_native(
scheme,
)))?;
}
anyhow::Ok(())
@@ -120,12 +98,6 @@ impl XDPEventSource {
Self { channel }
}
pub fn try_recv(&self) -> anyhow::Result<Event> {
self.channel
.try_recv()
.map_err(|error| anyhow!("{}", error))
}
}
impl EventSource for XDPEventSource {

View File

@@ -796,14 +796,24 @@ impl Platform for MacPlatform {
CursorStyle::ClosedHand => msg_send![class!(NSCursor), closedHandCursor],
CursorStyle::OpenHand => msg_send![class!(NSCursor), openHandCursor],
CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), verticalResizeCursor],
CursorStyle::ResizeLeft => msg_send![class!(NSCursor), resizeLeftCursor],
CursorStyle::ResizeRight => msg_send![class!(NSCursor), resizeRightCursor],
CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
CursorStyle::ResizeColumn => msg_send![class!(NSCursor), resizeLeftRightCursor],
CursorStyle::ResizeRow => msg_send![class!(NSCursor), resizeUpDownCursor],
CursorStyle::ResizeUp => msg_send![class!(NSCursor), resizeUpCursor],
CursorStyle::ResizeDown => msg_send![class!(NSCursor), resizeDownCursor],
CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
CursorStyle::ResizeRow => msg_send![class!(NSCursor), resizeUpDownCursor],
// Undocumented, private class methods:
// https://stackoverflow.com/questions/27242353/cocoa-predefined-resize-mouse-cursor
CursorStyle::ResizeUpLeftDownRight => {
msg_send![class!(NSCursor), _windowResizeNorthWestSouthEastCursor]
}
CursorStyle::ResizeUpRightDownLeft => {
msg_send![class!(NSCursor), _windowResizeNorthEastSouthWestCursor]
}
CursorStyle::IBeamCursorForVerticalLayout => {
msg_send![class!(NSCursor), IBeamCursorForVerticalLayout]
}

View File

@@ -497,7 +497,6 @@ impl MacWindow {
pub fn open(
handle: AnyWindowHandle,
WindowParams {
window_background,
bounds,
titlebar,
kind,
@@ -603,7 +602,7 @@ impl MacWindow {
native_window as *mut _,
native_view as *mut _,
bounds.size.map(|pixels| pixels.0),
window_background != WindowBackgroundAppearance::Opaque,
false,
),
request_frame_callback: None,
event_callback: None,
@@ -676,8 +675,6 @@ impl MacWindow {
native_window.setContentView_(native_view.autorelease());
native_window.makeFirstResponder_(native_view);
window.set_background_appearance(window_background);
match kind {
WindowKind::Normal => {
native_window.setLevel_(NSNormalWindowLevel);
@@ -956,7 +953,7 @@ impl PlatformWindow for MacWindow {
fn set_app_id(&mut self, _app_id: &str) {}
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
let mut this = self.0.as_ref().lock();
this.renderer
.update_transparency(background_appearance != WindowBackgroundAppearance::Opaque);
@@ -1092,14 +1089,6 @@ impl PlatformWindow for MacWindow {
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
self.0.lock().renderer.sprite_atlas().clone()
}
fn show_window_menu(&self, _position: Point<Pixels>) {}
fn start_system_move(&self) {}
fn should_render_window_controls(&self) -> bool {
false
}
}
impl rwh::HasWindowHandle for MacWindow {

View File

@@ -188,9 +188,7 @@ impl PlatformWindow for TestWindow {
fn set_app_id(&mut self, _app_id: &str) {}
fn set_background_appearance(&mut self, _background: WindowBackgroundAppearance) {
unimplemented!()
}
fn set_background_appearance(&self, _background: WindowBackgroundAppearance) {}
fn set_edited(&mut self, edited: bool) {
self.0.lock().edited = edited;
@@ -262,13 +260,9 @@ impl PlatformWindow for TestWindow {
unimplemented!()
}
fn start_system_move(&self) {
fn start_window_move(&self) {
unimplemented!()
}
fn should_render_window_controls(&self) -> bool {
false
}
}
pub(crate) struct TestAtlasState {

View File

@@ -274,7 +274,7 @@ impl WindowsWindow {
handle,
hide_title_bar,
display,
transparent: params.window_background != WindowBackgroundAppearance::Opaque,
transparent: true,
executor,
current_cursor,
};
@@ -511,9 +511,7 @@ impl PlatformWindow for WindowsWindow {
.ok();
}
fn set_app_id(&mut self, _app_id: &str) {}
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
self.0
.state
.borrow_mut()
@@ -521,12 +519,6 @@ impl PlatformWindow for WindowsWindow {
.update_transparency(background_appearance != WindowBackgroundAppearance::Opaque);
}
// todo(windows)
fn set_edited(&mut self, _edited: bool) {}
// todo(windows)
fn show_character_palette(&self) {}
fn minimize(&self) {
unsafe { ShowWindowAsync(self.0.hwnd, SW_MINIMIZE).ok().log_err() };
}
@@ -645,14 +637,6 @@ impl PlatformWindow for WindowsWindow {
fn get_raw_handle(&self) -> HWND {
self.0.hwnd
}
fn show_window_menu(&self, _position: Point<Pixels>) {}
fn start_system_move(&self) {}
fn should_render_window_controls(&self) -> bool {
false
}
}
#[implement(IDropTarget)]

View File

@@ -1,19 +1,20 @@
use crate::{
hash, point, prelude::*, px, size, transparent_black, Action, AnyDrag, AnyElement, AnyTooltip,
AnyView, AppContext, Arena, Asset, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow,
Context, Corners, CursorStyle, DevicePixels, DispatchActionListener, DispatchNodeId,
DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten,
FontId, Global, GlobalElementId, GlyphId, Hsla, ImageData, InputHandler, IsZero, KeyBinding,
KeyContext, KeyDownEvent, KeyEvent, KeyMatch, KeymatchResult, Keystroke, KeystrokeEvent,
LayoutId, LineLayoutIndex, Model, ModelContext, Modifiers, ModifiersChangedEvent,
MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels,
PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams,
RenderSvgParams, ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style,
SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement,
TransformationMatrix, Underline, UnderlineStyle, View, VisualContext, WeakView,
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowOptions, WindowParams,
WindowTextSystem, SUBPIXEL_VARIANTS,
Context, Corners, CursorStyle, Decorations, DevicePixels, DispatchActionListener,
DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter,
FileDropEvent, Flatten, FontId, Global, GlobalElementId, GlyphId, Hsla, ImageData,
InputHandler, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, KeyMatch, KeymatchResult,
Keystroke, KeystrokeEvent, LayoutId, LineLayoutIndex, Model, ModelContext, Modifiers,
ModifiersChangedEvent, MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent,
Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler,
PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams,
RenderImageParams, RenderSvgParams, ResizeEdge, ScaledPixels, Scene, Shadow, SharedString,
Size, StrikethroughStyle, Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task,
TextStyle, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, View,
VisualContext, WeakView, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem,
SUBPIXEL_VARIANTS,
};
use anyhow::{anyhow, Context as _, Result};
use collections::{FxHashMap, FxHashSet};
@@ -610,7 +611,10 @@ fn default_bounds(display_id: Option<DisplayId>, cx: &mut AppContext) -> Bounds<
cx.active_window()
.and_then(|w| w.update(cx, |_, cx| cx.bounds()).ok())
.map(|bounds| bounds.map_origin(|origin| origin + DEFAULT_WINDOW_OFFSET))
.map(|mut bounds| {
bounds.origin += DEFAULT_WINDOW_OFFSET;
bounds
})
.unwrap_or_else(|| {
let display = display_id
.map(|id| cx.find_display(id))
@@ -639,6 +643,7 @@ impl Window {
window_background,
app_id,
window_min_size,
window_decorations,
} = options;
let bounds = window_bounds
@@ -654,7 +659,6 @@ impl Window {
focus,
show,
display_id,
window_background,
window_min_size,
},
)?;
@@ -672,6 +676,10 @@ impl Window {
let next_frame_callbacks: Rc<RefCell<Vec<FrameCallback>>> = Default::default();
let last_input_timestamp = Rc::new(Cell::new(Instant::now()));
platform_window
.request_decorations(window_decorations.unwrap_or(WindowDecorations::Server));
platform_window.set_background_appearance(window_background);
if let Some(ref window_open_state) = window_bounds {
match window_open_state {
WindowBounds::Fullscreen(_) => platform_window.toggle_fullscreen(),
@@ -990,6 +998,16 @@ impl<'a> WindowContext<'a> {
self.window.platform_window.is_maximized()
}
/// request a certain window decoration (Wayland)
pub fn request_decorations(&self, decorations: WindowDecorations) {
self.window.platform_window.request_decorations(decorations);
}
/// Start a window resize operation (Wayland)
pub fn start_window_resize(&self, edge: ResizeEdge) {
self.window.platform_window.start_window_resize(edge);
}
/// Return the `WindowBounds` to indicate that how a window should be opened
/// after it has been closed
pub fn window_bounds(&self) -> WindowBounds {
@@ -1217,13 +1235,23 @@ impl<'a> WindowContext<'a> {
/// Tells the compositor to take control of window movement (Wayland and X11)
///
/// Events may not be received during a move operation.
pub fn start_system_move(&self) {
self.window.platform_window.start_system_move()
pub fn start_window_move(&self) {
self.window.platform_window.start_window_move()
}
/// When using client side decorations, set this to the width of the invisible decorations (Wayland and X11)
pub fn set_client_inset(&self, inset: Pixels) {
self.window.platform_window.set_client_inset(inset);
}
/// Returns whether the title bar window controls need to be rendered by the application (Wayland and X11)
pub fn should_render_window_controls(&self) -> bool {
self.window.platform_window.should_render_window_controls()
pub fn window_decorations(&self) -> Decorations {
self.window.platform_window.window_decorations()
}
/// Returns which window controls are currently visible (Wayland)
pub fn window_controls(&self) -> WindowControls {
self.window.platform_window.window_controls()
}
/// Updates the window's title at the platform level.
@@ -1237,7 +1265,7 @@ impl<'a> WindowContext<'a> {
}
/// Sets the window background appearance.
pub fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
pub fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
self.window
.platform_window
.set_background_appearance(background_appearance);

View File

@@ -28,7 +28,8 @@ pub use settings::*;
pub use styles::*;
use gpui::{
AppContext, AssetSource, Hsla, SharedString, WindowAppearance, WindowBackgroundAppearance,
px, AppContext, AssetSource, Hsla, Pixels, SharedString, WindowAppearance,
WindowBackgroundAppearance,
};
use serde::Deserialize;
@@ -38,6 +39,9 @@ pub enum Appearance {
Dark,
}
pub const CLIENT_SIDE_DECORATION_ROUNDING: Pixels = px(10.0);
pub const CLIENT_SIDE_DECORATION_SHADOW: Pixels = px(10.0);
impl Appearance {
pub fn is_light(&self) -> bool {
match self {

View File

@@ -1,4 +1,3 @@
pub mod platform_generic;
pub mod platform_linux;
pub mod platform_mac;
pub mod platform_windows;

View File

@@ -1,47 +0,0 @@
use gpui::{prelude::*, Action};
use ui::prelude::*;
use crate::window_controls::{WindowControl, WindowControlType};
#[derive(IntoElement)]
pub struct GenericWindowControls {
close_window_action: Box<dyn Action>,
}
impl GenericWindowControls {
pub fn new(close_action: Box<dyn Action>) -> Self {
Self {
close_window_action: close_action,
}
}
}
impl RenderOnce for GenericWindowControls {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
h_flex()
.id("generic-window-controls")
.px_3()
.gap_1p5()
.child(WindowControl::new(
"minimize",
WindowControlType::Minimize,
cx,
))
.child(WindowControl::new(
"maximize-or-restore",
if cx.is_maximized() {
WindowControlType::Restore
} else {
WindowControlType::Maximize
},
cx,
))
.child(WindowControl::new_close(
"close",
WindowControlType::Close,
self.close_window_action,
cx,
))
}
}

View File

@@ -1,8 +1,8 @@
use gpui::{prelude::*, Action};
use gpui::{prelude::*, Action, MouseButton};
use ui::prelude::*;
use super::platform_generic::GenericWindowControls;
use crate::window_controls::{WindowControl, WindowControlType};
#[derive(IntoElement)]
pub struct LinuxWindowControls {
@@ -18,7 +18,31 @@ impl LinuxWindowControls {
}
impl RenderOnce for LinuxWindowControls {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
GenericWindowControls::new(self.close_window_action.boxed_clone()).into_any_element()
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
h_flex()
.id("generic-window-controls")
.px_3()
.gap_3()
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
.child(WindowControl::new(
"minimize",
WindowControlType::Minimize,
cx,
))
.child(WindowControl::new(
"maximize-or-restore",
if cx.is_maximized() {
WindowControlType::Restore
} else {
WindowControlType::Maximize
},
cx,
))
.child(WindowControl::new_close(
"close",
WindowControlType::Close,
self.close_window_action,
cx,
))
}
}

View File

@@ -9,9 +9,9 @@ use call::{ActiveCall, ParticipantLocation};
use client::{Client, UserStore};
use collab::render_color_ribbon;
use gpui::{
actions, div, px, Action, AnyElement, AppContext, Element, InteractiveElement, Interactivity,
IntoElement, Model, ParentElement, Render, Stateful, StatefulInteractiveElement, Styled,
Subscription, ViewContext, VisualContext, WeakView,
actions, div, px, Action, AnyElement, AppContext, Decorations, Element, InteractiveElement,
Interactivity, IntoElement, Model, MouseButton, ParentElement, Render, Stateful,
StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, WeakView,
};
use project::{Project, RepositoryEntry};
use recent_projects::RecentProjects;
@@ -58,6 +58,7 @@ pub struct TitleBar {
user_store: Model<UserStore>,
client: Arc<Client>,
workspace: WeakView<Workspace>,
should_move: bool,
_subscriptions: Vec<Subscription>,
}
@@ -73,8 +74,10 @@ impl Render for TitleBar {
let platform_supported = cfg!(target_os = "macos");
let height = Self::height(cx);
let supported_controls = cx.window_controls();
let decorations = cx.window_decorations();
let mut title_bar = h_flex()
h_flex()
.id("titlebar")
.w_full()
.pt(Self::top_padding(cx))
@@ -88,6 +91,16 @@ impl Render for TitleBar {
this.pl_2()
}
})
.map(|el| {
match decorations {
Decorations::Server => el,
Decorations::Client { tiling, .. } => el
.when(!(tiling.top || tiling.right), |el| {
el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.top || tiling.left), |el| el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING))
}
})
.bg(cx.theme().colors().title_bar_background)
.content_stretch()
.child(
@@ -113,7 +126,7 @@ impl Render for TitleBar {
.children(self.render_project_host(cx))
.child(self.render_project_name(cx))
.children(self.render_project_branch(cx))
.on_mouse_move(|_, cx| cx.stop_propagation()),
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
)
.child(
h_flex()
@@ -145,7 +158,7 @@ impl Render for TitleBar {
this.children(current_user_face_pile.map(|face_pile| {
v_flex()
.on_mouse_move(|_, cx| cx.stop_propagation())
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
.child(face_pile)
.child(render_color_ribbon(player_colors.local().cursor))
}))
@@ -208,7 +221,7 @@ impl Render for TitleBar {
h_flex()
.gap_1()
.pr_1()
.on_mouse_move(|_, cx| cx.stop_propagation())
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
.when_some(room, |this, room| {
let room = room.read(cx);
let project = self.project.read(cx);
@@ -373,34 +386,38 @@ impl Render for TitleBar {
}
}),
)
);
// Windows Window Controls
title_bar = title_bar.when(
).when(
self.platform_style == PlatformStyle::Windows && !cx.is_fullscreen(),
|title_bar| title_bar.child(platform_windows::WindowsWindowControls::new(height)),
);
// Linux Window Controls
title_bar = title_bar.when(
).when(
self.platform_style == PlatformStyle::Linux
&& !cx.is_fullscreen()
&& cx.should_render_window_controls(),
&& matches!(decorations, Decorations::Client { .. }),
|title_bar| {
title_bar
.child(platform_linux::LinuxWindowControls::new(close_action))
.on_mouse_down(gpui::MouseButton::Right, move |ev, cx| {
cx.show_window_menu(ev.position)
.when(supported_controls.window_menu, |titlebar| {
titlebar.on_mouse_down(gpui::MouseButton::Right, move |ev, cx| {
cx.show_window_menu(ev.position)
})
})
.on_mouse_move(move |ev, cx| {
if ev.dragging() {
cx.start_system_move();
}
})
},
);
title_bar
.on_mouse_move(cx.listener(move |this, _ev, cx| {
if this.should_move {
this.should_move = false;
cx.start_window_move();
}
}))
.on_mouse_down_out(cx.listener(move |this, _ev, _cx| {
this.should_move = false;
}))
.on_mouse_down(gpui::MouseButton::Left, cx.listener(move |this, _ev, _cx| {
this.should_move = true;
}))
},
)
}
}
@@ -430,6 +447,7 @@ impl TitleBar {
content: div().id(id.into()),
children: SmallVec::new(),
workspace: workspace.weak_handle(),
should_move: false,
project,
user_store,
client,

View File

@@ -38,7 +38,7 @@ impl WindowControlStyle {
Self {
background: colors.ghost_element_background,
background_hover: colors.ghost_element_background,
background_hover: colors.ghost_element_hover,
icon: colors.icon,
icon_hover: colors.icon_muted,
}
@@ -127,7 +127,7 @@ impl WindowControl {
impl RenderOnce for WindowControl {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
let icon = svg()
.size_5()
.size_4()
.flex_none()
.path(self.icon.icon().path())
.text_color(self.style.icon)
@@ -139,7 +139,7 @@ impl RenderOnce for WindowControl {
.cursor_pointer()
.justify_center()
.content_center()
.rounded_md()
.rounded_2xl()
.w_5()
.h_5()
.hover(|this| this.bg(self.style.background_hover))

View File

@@ -32,6 +32,7 @@ impl Display for BaseKeymap {
}
impl BaseKeymap {
#[cfg(target_os = "macos")]
pub const OPTIONS: [(&'static str, Self); 5] = [
("VSCode (Default)", Self::VSCode),
("Atom", Self::Atom),
@@ -40,12 +41,31 @@ impl BaseKeymap {
("TextMate", Self::TextMate),
];
#[cfg(not(target_os = "macos"))]
pub const OPTIONS: [(&'static str, Self); 4] = [
("VSCode (Default)", Self::VSCode),
("Atom", Self::Atom),
("JetBrains", Self::JetBrains),
("Sublime Text", Self::SublimeText),
];
pub fn asset_path(&self) -> Option<&'static str> {
#[cfg(target_os = "macos")]
match self {
BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"),
BaseKeymap::SublimeText => Some("keymaps/sublime_text.json"),
BaseKeymap::Atom => Some("keymaps/atom.json"),
BaseKeymap::TextMate => Some("keymaps/textmate.json"),
BaseKeymap::JetBrains => Some("keymaps/macos/jetbrains.json"),
BaseKeymap::SublimeText => Some("keymaps/macos/sublime_text.json"),
BaseKeymap::Atom => Some("keymaps/macos/atom.json"),
BaseKeymap::TextMate => Some("keymaps/macos/textmate.json"),
BaseKeymap::VSCode => None,
BaseKeymap::None => None,
}
#[cfg(not(target_os = "macos"))]
match self {
BaseKeymap::JetBrains => Some("keymaps/linux/jetbrains.json"),
BaseKeymap::SublimeText => Some("keymaps/linux/sublime_text.json"),
BaseKeymap::Atom => Some("keymaps/linux/atom.json"),
BaseKeymap::TextMate => None,
BaseKeymap::VSCode => None,
BaseKeymap::None => None,
}

View File

@@ -1,9 +1,10 @@
use crate::{ItemHandle, Pane};
use gpui::{
AnyView, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext,
WindowContext,
AnyView, Decorations, IntoElement, ParentElement, Render, Styled, Subscription, View,
ViewContext, WindowContext,
};
use std::any::TypeId;
use theme::CLIENT_SIDE_DECORATION_ROUNDING;
use ui::{h_flex, prelude::*};
use util::ResultExt;
@@ -40,8 +41,17 @@ impl Render for StatusBar {
.gap(Spacing::Large.rems(cx))
.py(Spacing::Small.rems(cx))
.px(Spacing::Large.rems(cx))
// .h_8()
.bg(cx.theme().colors().status_bar_background)
.map(|el| match cx.window_decorations() {
Decorations::Server => el,
Decorations::Client { tiling, .. } => el
.when(!(tiling.bottom || tiling.right), |el| {
el.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.bottom || tiling.left), |el| {
el.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING)
}),
})
.child(self.render_left_tools(cx))
.child(self.render_right_tools(cx))
}

View File

@@ -27,11 +27,13 @@ use futures::{
Future, FutureExt, StreamExt,
};
use gpui::{
action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size, Action,
AnyElement, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds,
DragMoveEvent, Entity as _, EntityId, EventEmitter, FocusHandle, FocusableView, Global,
KeyContext, Keystroke, ManagedView, Model, ModelContext, PathPromptOptions, Point, PromptLevel,
Render, Size, Subscription, Task, View, WeakView, WindowBounds, WindowHandle, WindowOptions,
action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size,
transparent_black, Action, AnyElement, AnyView, AnyWeakView, AppContext, AsyncAppContext,
AsyncWindowContext, Bounds, CursorStyle, Decorations, DragMoveEvent, Entity as _, EntityId,
EventEmitter, FocusHandle, FocusableView, Global, Hsla, KeyContext, Keystroke, ManagedView,
Model, ModelContext, MouseButton, PathPromptOptions, Point, PromptLevel, Render, ResizeEdge,
Size, Stateful, Subscription, Task, Tiling, View, WeakView, WindowBounds, WindowHandle,
WindowOptions,
};
use item::{
FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
@@ -4165,156 +4167,162 @@ impl Render for Workspace {
let theme = cx.theme().clone();
let colors = theme.colors();
self.actions(div(), cx)
.key_context(context)
.relative()
.size_full()
.flex()
.flex_col()
.font(ui_font)
.gap_0()
.justify_start()
.items_start()
.text_color(colors.text)
.bg(colors.background)
.children(self.titlebar_item.clone())
.child(
div()
.id("workspace")
.relative()
.flex_1()
.w_full()
.flex()
.flex_col()
.overflow_hidden()
.border_t_1()
.border_b_1()
.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()
})
.when(self.zoomed.is_none(), |this| {
this.on_drag_move(cx.listener(
|workspace, e: &DragMoveEvent<DraggedDock>, cx| match e.drag(cx).0 {
DockPosition::Left => {
let size = workspace.bounds.left() + e.event.position.x;
workspace.left_dock.update(cx, |left_dock, cx| {
left_dock.resize_active_panel(Some(size), cx);
});
}
DockPosition::Right => {
let size = workspace.bounds.right() - e.event.position.x;
workspace.right_dock.update(cx, |right_dock, cx| {
right_dock.resize_active_panel(Some(size), cx);
});
}
DockPosition::Bottom => {
let size = workspace.bounds.bottom() - e.event.position.y;
workspace.bottom_dock.update(cx, |bottom_dock, cx| {
bottom_dock.resize_active_panel(Some(size), cx);
});
}
},
))
})
.child(
div()
.flex()
.flex_row()
.h_full()
// Left Dock
.children(self.zoomed_position.ne(&Some(DockPosition::Left)).then(
|| {
div()
.flex()
.flex_none()
.overflow_hidden()
.child(self.left_dock.clone())
client_side_decorations(
self.actions(div(), cx)
.key_context(context)
.relative()
.size_full()
.flex()
.flex_col()
.font(ui_font)
.gap_0()
.justify_start()
.items_start()
.text_color(colors.text)
.overflow_hidden()
.children(self.titlebar_item.clone())
.child(
div()
.id("workspace")
.bg(colors.background)
.relative()
.flex_1()
.w_full()
.flex()
.flex_col()
.overflow_hidden()
.border_t_1()
.border_b_1()
.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()
})
.when(self.zoomed.is_none(), |this| {
this.on_drag_move(cx.listener(
|workspace, e: &DragMoveEvent<DraggedDock>, cx| match e.drag(cx).0 {
DockPosition::Left => {
let size = e.event.position.x - workspace.bounds.left();
workspace.left_dock.update(cx, |left_dock, cx| {
left_dock.resize_active_panel(Some(size), cx);
});
}
DockPosition::Right => {
let size = workspace.bounds.right() - e.event.position.x;
workspace.right_dock.update(cx, |right_dock, cx| {
right_dock.resize_active_panel(Some(size), cx);
});
}
DockPosition::Bottom => {
let size = workspace.bounds.bottom() - e.event.position.y;
workspace.bottom_dock.update(cx, |bottom_dock, cx| {
bottom_dock.resize_active_panel(Some(size), cx);
});
}
},
))
// Panes
.child(
div()
.flex()
.flex_col()
.flex_1()
.overflow_hidden()
.child(
h_flex()
.flex_1()
.when_some(paddings.0, |this, p| {
this.child(p.border_r_1())
})
.child(self.center.render(
&self.project,
&self.follower_states,
self.active_call(),
&self.active_pane,
self.zoomed.as_ref(),
&self.app_state,
cx,
))
.when_some(paddings.1, |this, p| {
this.child(p.border_l_1())
}),
)
.children(
self.zoomed_position
.ne(&Some(DockPosition::Bottom))
.then(|| self.bottom_dock.clone()),
),
)
// Right Dock
.children(self.zoomed_position.ne(&Some(DockPosition::Right)).then(
|| {
})
.child(
div()
.flex()
.flex_row()
.h_full()
// Left Dock
.children(self.zoomed_position.ne(&Some(DockPosition::Left)).then(
|| {
div()
.flex()
.flex_none()
.overflow_hidden()
.child(self.left_dock.clone())
},
))
// Panes
.child(
div()
.flex()
.flex_none()
.flex_col()
.flex_1()
.overflow_hidden()
.child(self.right_dock.clone())
},
)),
)
.children(self.zoomed.as_ref().and_then(|view| {
let zoomed_view = view.upgrade()?;
let div = div()
.occlude()
.absolute()
.overflow_hidden()
.border_color(colors.border)
.bg(colors.background)
.child(zoomed_view)
.inset_0()
.shadow_lg();
.child(
h_flex()
.flex_1()
.when_some(paddings.0, |this, p| {
this.child(p.border_r_1())
})
.child(self.center.render(
&self.project,
&self.follower_states,
self.active_call(),
&self.active_pane,
self.zoomed.as_ref(),
&self.app_state,
cx,
))
.when_some(paddings.1, |this, p| {
this.child(p.border_l_1())
}),
)
.children(
self.zoomed_position
.ne(&Some(DockPosition::Bottom))
.then(|| self.bottom_dock.clone()),
),
)
// Right Dock
.children(
self.zoomed_position.ne(&Some(DockPosition::Right)).then(
|| {
div()
.flex()
.flex_none()
.overflow_hidden()
.child(self.right_dock.clone())
},
),
),
)
.children(self.zoomed.as_ref().and_then(|view| {
let zoomed_view = view.upgrade()?;
let div = div()
.occlude()
.absolute()
.overflow_hidden()
.border_color(colors.border)
.bg(colors.background)
.child(zoomed_view)
.inset_0()
.shadow_lg();
Some(match self.zoomed_position {
Some(DockPosition::Left) => div.right_2().border_r_1(),
Some(DockPosition::Right) => div.left_2().border_l_1(),
Some(DockPosition::Bottom) => div.top_2().border_t_1(),
None => div.top_2().bottom_2().left_2().right_2().border_1(),
})
}))
.child(self.modal_layer.clone())
.children(self.render_notifications(cx)),
)
.child(self.status_bar.clone())
.children(if self.project.read(cx).is_disconnected() {
if let Some(render) = self.render_disconnected_overlay.take() {
let result = render(self, cx);
self.render_disconnected_overlay = Some(render);
Some(result)
Some(match self.zoomed_position {
Some(DockPosition::Left) => div.right_2().border_r_1(),
Some(DockPosition::Right) => div.left_2().border_l_1(),
Some(DockPosition::Bottom) => div.top_2().border_t_1(),
None => div.top_2().bottom_2().left_2().right_2().border_1(),
})
}))
.child(self.modal_layer.clone())
.children(self.render_notifications(cx)),
)
.child(self.status_bar.clone())
.children(if self.project.read(cx).is_disconnected() {
if let Some(render) = self.render_disconnected_overlay.take() {
let result = render(self, cx);
self.render_disconnected_overlay = Some(render);
Some(result)
} else {
None
}
} else {
None
}
} else {
None
})
}),
cx,
)
}
}
@@ -6474,3 +6482,222 @@ mod tests {
});
}
}
pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext) -> Stateful<Div> {
const BORDER_SIZE: Pixels = px(1.0);
let decorations = cx.window_decorations();
if matches!(decorations, Decorations::Client { .. }) {
cx.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
}
struct GlobalResizeEdge(ResizeEdge);
impl Global for GlobalResizeEdge {}
div()
.id("window-backdrop")
.bg(transparent_black())
.map(|div| match decorations {
Decorations::Server => div,
Decorations::Client { tiling, .. } => div
.when(!(tiling.top || tiling.right), |div| {
div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.top || tiling.left), |div| {
div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.bottom || tiling.right), |div| {
div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.bottom || tiling.left), |div| {
div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!tiling.top, |div| {
div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
})
.when(!tiling.bottom, |div| {
div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
})
.when(!tiling.left, |div| {
div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
})
.when(!tiling.right, |div| {
div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
})
.on_mouse_move(move |e, cx| {
let size = cx.window_bounds().get_bounds().size;
let pos = e.position;
let new_edge =
resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
let edge = cx.try_global::<GlobalResizeEdge>();
if new_edge != edge.map(|edge| edge.0) {
cx.window_handle()
.update(cx, |workspace, cx| cx.notify(workspace.entity_id()))
.ok();
}
})
.on_mouse_down(MouseButton::Left, move |e, cx| {
let size = cx.window_bounds().get_bounds().size;
let pos = e.position;
let edge = match resize_edge(
pos,
theme::CLIENT_SIDE_DECORATION_SHADOW,
size,
tiling,
) {
Some(value) => value,
None => return,
};
cx.start_window_resize(edge);
}),
})
.size_full()
.child(
div()
.cursor(CursorStyle::Arrow)
.map(|div| match decorations {
Decorations::Server => div,
Decorations::Client { tiling } => div
.border_color(cx.theme().colors().border)
.when(!(tiling.top || tiling.right), |div| {
div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.top || tiling.left), |div| {
div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.bottom || tiling.right), |div| {
div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.bottom || tiling.left), |div| {
div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!tiling.top, |div| div.border_t(BORDER_SIZE))
.when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
.when(!tiling.left, |div| div.border_l(BORDER_SIZE))
.when(!tiling.right, |div| div.border_r(BORDER_SIZE))
.when(!tiling.is_tiled(), |div| {
div.shadow(smallvec::smallvec![gpui::BoxShadow {
color: Hsla {
h: 0.,
s: 0.,
l: 0.,
a: 0.4,
},
blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
spread_radius: px(0.),
offset: point(px(0.0), px(0.0)),
}])
}),
})
.on_mouse_move(|_e, cx| {
cx.stop_propagation();
})
.bg(cx.theme().colors().border)
.size_full()
.child(element),
)
.map(|div| match decorations {
Decorations::Server => div,
Decorations::Client { tiling, .. } => div.child(
canvas(
|_bounds, cx| {
cx.insert_hitbox(
Bounds::new(
point(px(0.0), px(0.0)),
cx.window_bounds().get_bounds().size,
),
false,
)
},
move |_bounds, hitbox, cx| {
let mouse = cx.mouse_position();
let size = cx.window_bounds().get_bounds().size;
let Some(edge) =
resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
else {
return;
};
cx.set_global(GlobalResizeEdge(edge));
cx.set_cursor_style(
match edge {
ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
ResizeEdge::Left | ResizeEdge::Right => {
CursorStyle::ResizeLeftRight
}
ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
CursorStyle::ResizeUpLeftDownRight
}
ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
CursorStyle::ResizeUpRightDownLeft
}
},
&hitbox,
);
},
)
.size_full()
.absolute(),
),
})
}
fn resize_edge(
pos: Point<Pixels>,
shadow_size: Pixels,
window_size: Size<Pixels>,
tiling: Tiling,
) -> Option<ResizeEdge> {
let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
if bounds.contains(&pos) {
return None;
}
let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
if top_left_bounds.contains(&pos) {
return Some(ResizeEdge::TopLeft);
}
let top_right_bounds = Bounds::new(
Point::new(window_size.width - corner_size.width, px(0.)),
corner_size,
);
if top_right_bounds.contains(&pos) {
return Some(ResizeEdge::TopRight);
}
let bottom_left_bounds = Bounds::new(
Point::new(px(0.), window_size.height - corner_size.height),
corner_size,
);
if bottom_left_bounds.contains(&pos) {
return Some(ResizeEdge::BottomLeft);
}
let bottom_right_bounds = Bounds::new(
Point::new(
window_size.width - corner_size.width,
window_size.height - corner_size.height,
),
corner_size,
);
if bottom_right_bounds.contains(&pos) {
return Some(ResizeEdge::BottomRight);
}
if !tiling.top && pos.y < shadow_size {
Some(ResizeEdge::Top)
} else if !tiling.bottom && pos.y > window_size.height - shadow_size {
Some(ResizeEdge::Bottom)
} else if !tiling.left && pos.x < shadow_size {
Some(ResizeEdge::Left)
} else if !tiling.right && pos.x > window_size.width - shadow_size {
Some(ResizeEdge::Right)
} else {
None
}
}

View File

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

View File

@@ -1 +1 @@
dev
preview

View File

@@ -6,9 +6,12 @@ GenericName=Text Editor
Comment=A high-performance, multiplayer code editor.
TryExec=$APP_CLI
StartupNotify=$DO_STARTUP_NOTIFY
StartupWMClass=$APP_ID
Exec=$APP_CLI $APP_ARGS
Icon=$APP_ICON
Categories=Utility;TextEditor;Development;IDE;
Keywords=zed;
MimeType=text/plain;inode/directory;
[Desktop Action NewWorkspace]
Exec=$APP_CLI --new $APP_ARGS
Name=Open a new workspace

View File

@@ -90,6 +90,11 @@ pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut AppContext) ->
.find(|display| display.uuid().ok() == Some(uuid))
});
let app_id = ReleaseChannel::global(cx).app_id();
let window_decorations = match std::env::var("ZED_WINDOW_DECORATIONS") {
Ok(val) if val == "server" => gpui::WindowDecorations::Server,
Ok(val) if val == "client" => gpui::WindowDecorations::Client,
_ => gpui::WindowDecorations::Client,
};
WindowOptions {
titlebar: Some(TitlebarOptions {
@@ -105,6 +110,7 @@ pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut AppContext) ->
display_id: display.map(|display| display.id()),
window_background: cx.theme().window_background_appearance(),
app_id: Some(app_id.to_owned()),
window_decorations: Some(window_decorations),
window_min_size: Some(gpui::Size {
width: px(360.0),
height: px(240.0),

View File

@@ -1,7 +1,7 @@
use gpui::{
div, opaque_grey, AppContext, EventEmitter, FocusHandle, FocusableView, FontWeight,
InteractiveElement, IntoElement, ParentElement, PromptHandle, PromptLevel, PromptResponse,
Render, RenderablePromptHandle, Styled, ViewContext, VisualContext, WindowContext,
div, AppContext, EventEmitter, FocusHandle, FocusableView, FontWeight, InteractiveElement,
IntoElement, ParentElement, PromptHandle, PromptLevel, PromptResponse, Render,
RenderablePromptHandle, Styled, ViewContext, VisualContext, WindowContext,
};
use settings::Settings;
use theme::ThemeSettings;
@@ -31,6 +31,7 @@ pub fn fallback_prompt_renderer(
detail: detail.map(ToString::to_string),
actions: actions.iter().map(ToString::to_string).collect(),
focus: cx.focus_handle(),
active_action_id: actions.len() - 1,
}
});
@@ -44,10 +45,12 @@ pub struct FallbackPromptRenderer {
detail: Option<String>,
actions: Vec<String>,
focus: FocusHandle,
active_action_id: usize,
}
impl FallbackPromptRenderer {
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
cx.emit(PromptResponse(0));
cx.emit(PromptResponse(self.active_action_id));
}
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
@@ -55,7 +58,32 @@ impl FallbackPromptRenderer {
cx.emit(PromptResponse(ix));
}
}
fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext<Self>) {
self.active_action_id = 0;
cx.notify();
}
fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
self.active_action_id = self.actions.len().saturating_sub(1);
cx.notify();
}
fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
self.active_action_id = (self.active_action_id + 1) % self.actions.len();
cx.notify();
}
fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
if self.active_action_id > 0 {
self.active_action_id -= 1;
} else {
self.active_action_id = self.actions.len().saturating_sub(1);
}
cx.notify();
}
}
impl Render for FallbackPromptRenderer {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
@@ -66,6 +94,10 @@ impl Render for FallbackPromptRenderer {
.track_focus(&self.focus)
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_prev))
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_last))
.elevation_3(cx)
.w_72()
.overflow_hidden()
@@ -87,11 +119,11 @@ impl Render for FallbackPromptRenderer {
.child(detail)
}))
.child(h_flex().justify_end().gap_2().children(
self.actions.iter().enumerate().rev().map(|(ix, action)| {
self.actions.iter().rev().enumerate().map(|(ix, action)| {
ui::Button::new(ix, action.clone())
.label_size(LabelSize::Large)
.style(ButtonStyle::Filled)
.when(ix == 0, |el| {
.when(ix == self.active_action_id, |el| {
el.style(ButtonStyle::Tinted(TintColor::Accent))
})
.layer(ElevationIndex::ModalSurface)
@@ -101,35 +133,24 @@ impl Render for FallbackPromptRenderer {
}),
));
div()
.size_full()
.occlude()
.child(
div()
.size_full()
.bg(opaque_grey(0.5, 0.6))
.absolute()
.top_0()
.left_0(),
)
.child(
div()
.size_full()
.absolute()
.top_0()
.left_0()
.flex()
.flex_col()
.justify_around()
.child(
div()
.w_full()
.flex()
.flex_row()
.justify_around()
.child(prompt),
),
)
div().size_full().occlude().child(
div()
.size_full()
.absolute()
.top_0()
.left_0()
.flex()
.flex_col()
.justify_around()
.child(
div()
.w_full()
.flex()
.flex_row()
.justify_around()
.child(prompt),
),
)
}
}

View File

@@ -82,6 +82,7 @@ cp "crates/zed/resources/app-icon$suffix@2x.png" "${zed_dir}/share/icons/hicolor
export DO_STARTUP_NOTIFY="true"
export APP_CLI="zed"
export APP_ICON="zed"
export APP_ARGS="%U"
if [[ "$channel" == "preview" ]]; then
export APP_NAME="Zed Preview"
elif [[ "$channel" == "nightly" ]]; then