Compare commits

...

33 Commits

Author SHA1 Message Date
Max Brunsfeld
dc78995fee zed 0.121.7 2024-02-08 12:16:49 -08:00
Antonio Scandurra
59b26719b0 Replace CADisplayLink with CVDisplayLink (#7583)
Release Notes:

- Fixed a bug that caused Zed to render at 60fps even on ProMotion
displays.
- Fixed a bug that could saturate the main thread event loop in certain
circumstances.

---------

Co-authored-by: Thorsten <thorsten@zed.dev>
Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Max <max@zed.dev>
2024-02-08 12:10:21 -08:00
Kirill Bulatov
d5a3370d7c Fix gopls langserver downloads (#7571)
Fixes https://github.com/zed-industries/zed/issues/7534 by not requiring
assets for gopls and vscode-eslint langservers — those two are the only
ones in Zed that do not use assets directly when determining langserver
version and retrieving those.
All other servers deal with assets, hence require those to be present.

The problem with https://github.com/tamasfe/taplo/releases is that they
host multiple binary releases in the same release list, so for now the
code works because only the langserver has assets — but as soon as
another release there gets assets, it will break again.
We could filter out those by names also, but they also tend to change
(and can be edited manually), so keeping it as is for now.

Release Notes:

- Fixed gopls language server downloads
([7534](https://github.com/zed-industries/zed/issues/7534))
2024-02-08 21:22:17 +02:00
Conrad Irwin
0a9e34eb1e Testing buf breaking (#7475)
Release Notes:

- N/A
2024-02-08 09:11:42 -07:00
Conrad Irwin
7894f57171 zed 0.121.6 2024-02-07 21:46:56 -07:00
Conrad Irwin
d1c4fc38ee Fix panic! caused by bad utf16 clipping (#7530)
Release Notes:

- Fixed a panic in diagnostics with emojis

**or**

- N/A
2024-02-07 21:46:16 -07:00
Conrad Irwin
c6ff5c4c1f Go back to an alacritty release (#7474)
Release Notes:

- N/A
2024-02-07 16:59:56 -07:00
Conrad Irwin
85dfbe3349 Bump alacritty to fix some panics (#7313)
Release Notes:

- Fixed some panics in the Terminal
([#6835](https://github.com/zed-industries/zed/issues/6835)).
2024-02-07 16:59:51 -07:00
Conrad Irwin
d10588d871 Prevent terminal being a single column wide (#7471)
Fixes: #2750
Fixes: #7457



Release Notes:

- Fixed a hang/panic that could happen rendering a double-width
character in a single-width terminal
([#2750](https://github.com/zed-industries/zed/issues/2750),
[#7457](https://github.com/zed-industries/zed/issues/7457)).
2024-02-07 16:58:48 -07:00
Conrad Irwin
3b28b37866 zed 0.121.5 2024-02-07 11:49:55 -07:00
Antonio Scandurra
c544ce0d47 Stop display link when window is occluded (#7511)
Release Notes:

- Fixed a bug that caused the window to become unresponsive after
foregrounding.

---------

Co-authored-by: Conrad <conrad@zed.dev>
2024-02-07 11:49:06 -07:00
Joseph T. Lyons
a3a7ae455b v0.121.x stable 2024-02-07 12:19:28 -05:00
Conrad Irwin
ac85440c82 Attempt to fix random lag (#7506)
Co-Authored-By: Antonio <antonio@zed.dev>
Co-Authored-By: Thorsten <thorsten@zed.dev>
Co-Authored-By: Mikayla <mikayla@zed.dev>

Release Notes:

- N/A

**or**

- N/A

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Thorsten <thorsten@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.dev>
2024-02-07 12:16:25 -05:00
Max Brunsfeld
f28a4c7cf9 zed 0.121.4 2024-02-05 15:01:50 -08:00
Mikayla Maki
824e7e421a Disable extra frames for ProMotion when screen is not active. (#7410)
This was causing an issue where windows about 1/10 of the way across the
display would hang for a fully second after being deactivated.

Release Notes:

- N/A

Co-authored-by: max <max@zed.dev>
Co-authored-by: nathan <nathan@zed.dev>
Co-authored-by: antonio <antonio@zed.dev>
2024-02-05 15:00:23 -08:00
Antonio Scandurra
5721dec2ec zed 0.121.3 2024-02-05 11:22:47 -07:00
Thorsten Ball
9f90ebd509 Fix cmd+k in terminal and fix sporadic keybind misses (#7388)
This fixes `cmd+k` in the terminal taking 1s to have an effect. It is
now immediate.

It also fixes #7270 by ensuring that we don't set a bad state when
matching keybindings.

It matches keybindings per context and if it finds a match on a lower
context it doesn't keep pending keystrokes. If it finds two matches on
the same context level, requiring more keystrokes, then it waits.

Release Notes:

- Fixed `cmd-k` in terminal taking 1s to have an effect. Also fixed
sporadic non-matching of keybindings if there are overlapping
keybindings.
([#7270](https://github.com/zed-industries/zed/issues/7270)).

---------

Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
# Conflicts:
#	crates/gpui/src/window.rs
2024-02-05 11:21:54 -07:00
Antonio Scandurra
17a529c0f5 Scope line layout cache to each window (#7235)
This improves a performance problem we were observing when having
multiple windows updating at the same time, where each window would
invalidate the other window's layout cache.

Release Notes:

- Improved performance when having multiple Zed windows open.

Co-authored-by: Max Brunsfeld <max@zed.dev>
# Conflicts:
#	crates/gpui/src/window.rs
2024-02-05 11:19:21 -07:00
Antonio Scandurra
3a0fb0d322 Mark the window as dirty when first opening it (#7384)
Otherwise we won't display anything if the window never notifies.

Release Notes:

- N/A

Co-authored-by: Nathan <nathan@zed.dev>
2024-02-05 11:15:36 -07:00
Julia
f8bc7fbe0e Use window's screen rather than window itself to start display link
Co-Authored-By: Antonio Scandurra <antonio@zed.dev>
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2024-02-05 11:15:25 -07:00
Antonio Scandurra
a413729be9 Reintroduce ProMotion support (#7347)
This re-introduces the changes of #7305 but this time we create a
display link using the `NSScreen` associated with the window. We're
hoping we'll get these frame requests more reliably, and this seems
supported by the fact that awakening my laptop restores the frame
requests.

Release Notes:

- See #7305.

Co-authored-by: Nathan <nathan@zed.dev>
# Conflicts:
#	crates/gpui/src/window.rs
2024-02-05 11:15:05 -07:00
Antonio Scandurra
441a21ed0b Remove unnecessary focus_invalidated field (#7320)
I believe at some point this was used for tests but it doesn't seem
necessary anymore.

Release Notes:

- N/A
# Conflicts:
#	crates/gpui/src/app.rs
2024-02-05 11:13:28 -07:00
Antonio Scandurra
234673a1b9 Reduce GPU memory usage (#7319)
This pull request decreases the size of each instance buffer and shares
instance buffers across windows.

Release Notes:

- Improved GPU memory usage.

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
# Conflicts:
#	crates/gpui/src/platform/mac/window.rs
2024-02-05 11:12:37 -07:00
Antonio Scandurra
34e846b353 Use Mutex instead of a RefCell to acquire/release instance buffers (#7291)
This fixes a panic happening when releasing an instance buffer.
Releasing the buffer happens on a different thread but the borrow
checker was not catching it because the metal buffer completion handler
API doesn't have a `Send` marker on it.

Release Notes:

- N/A
2024-02-02 11:53:26 -05:00
Thorsten Ball
889a6e2bc3 Use command_buffer.wait_until_scheduled in metal renderer (#7283)
This commit goes back to using `wait_until_scheduled` as opposed to
`wait_until_completed`. What this means, however, is that another draw
could take place before the previous one finished. When that happens we
don't want to reuse the same instance buffer because the GPU is actively
reading from it, so we use a pool instead.

Release Notes:

- Fixed a bug that caused inconsistent frame rate when scrolling on
certain hardware.

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Antonio <antonio@zed.dev>
2024-02-02 11:53:13 -05:00
Joseph T. Lyons
21b755abae zed 0.121.2 2024-02-02 11:38:33 -05:00
Antonio Scandurra
0bf444d39d Introduce a fast path for drawing quads with no borders / corner radii (#7231)
This will introduce an extra conditional but saves us from doing a bunch
of math in the simple case of drawing simple rectangles that aren't
rounded or don't have borders.


![Figure_1](https://github.com/zed-industries/zed/assets/482957/cba95ce2-2d9a-46ab-a142-35368334eb75)

Release Notes:

- Improved rendering performance.
2024-02-01 18:17:21 -05:00
Thorsten Ball
a745e9b58b zed 0.121.1 2024-02-01 16:53:24 +01:00
Ares Andrew
958fbacc2b Filter LSP github releases that have no assets to properly download LSP servers (#7189)
Fixes https://github.com/zed-industries/zed/issues/7183

Release Notes:

- Filter lsp github releases that have no assets ([7189](https://github.com/zed-industries/zed/issues/7183))
2024-02-01 16:50:14 +01:00
Thorsten Ball
bb6c06e204 assistant: render api key editor if no credentials are set (#7197)
This hopefully reduces confusion for new users. I updated the docs just
this morning, but I figured it's probably better to fix the issue
itself.

So what this does is to render the API key editor whenever the assistant
panel is opened/focused and no credentials can be found.

See: https://github.com/zed-industries/zed/discussions/6943

Release Notes:

- Fixed assistant panel not showing dialog to enter API key when opened
without saved credentials.

---------

Co-authored-by: Piotr <piotr@zed.dev>
2024-02-01 16:44:38 +01:00
Marshall Bowers
2e9f665c77 Watch the themes directory for changes (#7173)
This PR makes Zed watch the themes directory for changes.

When theme files are added or modified, we reload the theme and apply
any changes to Zed.

Release Notes:

- Added live reloading for the themes directory.
2024-01-31 18:30:23 -05:00
Conrad Irwin
3fdccaacbc disallow opening private files (#7165)
- Disallow sharing gitignored files through collab
- Show errors when failing to open files
- Show a warning to followers when view is unshared

/cc @mikaylamaki, let's update this to use your `private_files` config
before merge.


Release Notes:

- Added the ability to prevent sharing private files over collab.

---------

Co-authored-by: Piotr <piotr@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.dev>
2024-01-31 12:08:09 -08:00
Joseph T. Lyons
7dd7ecd07f v0.121.x preview 2024-01-31 14:44:02 -05:00
54 changed files with 929 additions and 661 deletions

View File

@@ -23,8 +23,3 @@ runs:
export SQUAWK_GITHUB_TOKEN=${{ github.token }}
. ./script/squawk
- uses: bufbuild/buf-setup-action@v1
- uses: bufbuild/buf-breaking-action@v1
with:
input: "crates/rpc/proto/"
against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=main,subdir=crates/rpc/proto/"

View File

@@ -48,6 +48,18 @@ jobs:
- name: Run style checks
uses: ./.github/actions/check_style
- name: Ensure fresh merge
run: |
git checkout -B temp
git merge -q origin/main -m "merge main into temp"
- uses: bufbuild/buf-setup-action@v1
- uses: bufbuild/buf-breaking-action@v1
with:
input: "crates/rpc/proto/"
against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=main,subdir=crates/rpc/proto/"
tests:
name: Run tests
runs-on:

6
Cargo.lock generated
View File

@@ -103,9 +103,9 @@ dependencies = [
[[package]]
name = "alacritty_terminal"
version = "0.21.0"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35229555d7cc7e83392dfc27c96bec560b1076d756184893296cd60125f4a264"
checksum = "cc7ceabf6fc76511f616ca216b51398a2511f19ba9f71bcbd977999edff1b0d1"
dependencies = [
"base64 0.21.4",
"bitflags 2.4.1",
@@ -10261,7 +10261,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.121.0"
version = "0.121.7"
dependencies = [
"activity_indicator",
"ai",

View File

@@ -415,7 +415,15 @@
"cmd-?": "assistant::ToggleFocus",
"cmd-alt-s": "workspace::SaveAll",
"cmd-k m": "language_selector::Toggle",
"escape": "workspace::Unfollow"
"escape": "workspace::Unfollow",
"cmd-k cmd-left": ["workspace::ActivatePaneInDirection", "Left"],
"cmd-k cmd-right": ["workspace::ActivatePaneInDirection", "Right"],
"cmd-k cmd-up": ["workspace::ActivatePaneInDirection", "Up"],
"cmd-k cmd-down": ["workspace::ActivatePaneInDirection", "Down"],
"cmd-k shift-left": ["workspace::SwapPaneInDirection", "Left"],
"cmd-k shift-right": ["workspace::SwapPaneInDirection", "Right"],
"cmd-k shift-up": ["workspace::SwapPaneInDirection", "Up"],
"cmd-k shift-down": ["workspace::SwapPaneInDirection", "Down"]
}
},
// Bindings from Sublime Text
@@ -441,18 +449,6 @@
"ctrl-alt-shift-f": "editor::SelectToNextSubwordEnd"
}
},
{
"bindings": {
"cmd-k cmd-left": ["workspace::ActivatePaneInDirection", "Left"],
"cmd-k cmd-right": ["workspace::ActivatePaneInDirection", "Right"],
"cmd-k cmd-up": ["workspace::ActivatePaneInDirection", "Up"],
"cmd-k cmd-down": ["workspace::ActivatePaneInDirection", "Down"],
"cmd-k shift-left": ["workspace::SwapPaneInDirection", "Left"],
"cmd-k shift-right": ["workspace::SwapPaneInDirection", "Right"],
"cmd-k shift-up": ["workspace::SwapPaneInDirection", "Up"],
"cmd-k shift-down": ["workspace::SwapPaneInDirection", "Down"]
}
},
// Bindings from Atom
{
"context": "Pane",

View File

@@ -199,9 +199,13 @@ impl AssistantPanel {
.update(cx, |toolbar, cx| toolbar.focus_changed(true, cx));
cx.notify();
if self.focus_handle.is_focused(cx) {
if let Some(editor) = self.active_editor() {
cx.focus_view(editor);
} else if let Some(api_key_editor) = self.api_key_editor.as_ref() {
if self.has_credentials() {
if let Some(editor) = self.active_editor() {
cx.focus_view(editor);
}
}
if let Some(api_key_editor) = self.api_key_editor.as_ref() {
cx.focus_view(api_key_editor);
}
}
@@ -777,6 +781,10 @@ impl AssistantPanel {
});
}
fn build_api_key_editor(&mut self, cx: &mut WindowContext<'_>) {
self.api_key_editor = Some(build_api_key_editor(cx));
}
fn new_conversation(&mut self, cx: &mut ViewContext<Self>) -> View<ConversationEditor> {
let editor = cx.new_view(|cx| {
ConversationEditor::new(
@@ -870,7 +878,7 @@ impl AssistantPanel {
cx.update(|cx| completion_provider.delete_credentials(cx))?
.await;
this.update(&mut cx, |this, cx| {
this.api_key_editor = Some(build_api_key_editor(cx));
this.build_api_key_editor(cx);
this.focus_handle.focus(cx);
cx.notify();
})
@@ -1136,7 +1144,7 @@ impl AssistantPanel {
}
}
fn build_api_key_editor(cx: &mut ViewContext<AssistantPanel>) -> View<Editor> {
fn build_api_key_editor(cx: &mut WindowContext) -> View<Editor> {
cx.new_view(|cx| {
let mut editor = Editor::single_line(cx);
editor.set_placeholder_text("sk-000000000000000000000000000000000000000000000000", cx);
@@ -1147,9 +1155,10 @@ fn build_api_key_editor(cx: &mut ViewContext<AssistantPanel>) -> View<Editor> {
impl Render for AssistantPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
if let Some(api_key_editor) = self.api_key_editor.clone() {
const INSTRUCTIONS: [&'static str; 5] = [
const INSTRUCTIONS: [&'static str; 6] = [
"To use the assistant panel or inline assistant, you need to add your OpenAI API key.",
" - You can create an API key at: platform.openai.com/api-keys",
" - Make sure your OpenAI account has credits",
" - Having a subscription for another service like GitHub Copilot won't work.",
" ",
"Paste your OpenAI API key and press Enter to use the assistant:"
@@ -1342,7 +1351,9 @@ impl Panel for AssistantPanel {
cx.spawn(|this, mut cx| async move {
load_credentials.await;
this.update(&mut cx, |this, cx| {
if this.editors.is_empty() {
if !this.has_credentials() {
this.build_api_key_editor(cx);
} else if this.editors.is_empty() {
this.new_conversation(cx);
}
})

View File

@@ -28,6 +28,9 @@ async fn main() -> Result<()> {
Some("version") => {
println!("collab v{VERSION}");
}
Some("migrate") => {
run_migrations().await?;
}
Some("serve") => {
let config = envy::from_env::<Config>().expect("error loading config");
init_tracing(&config);

View File

@@ -5967,6 +5967,6 @@ async fn test_cmd_k_left(cx: &mut TestAppContext) {
cx.executor().advance_clock(Duration::from_secs(2));
cx.simulate_keystrokes("left");
workspace.update(cx, |workspace, cx| {
assert!(workspace.items(cx).collect::<Vec<_>>().len() == 3);
assert!(workspace.items(cx).collect::<Vec<_>>().len() == 2);
});
}

View File

@@ -976,7 +976,8 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
///Check for the latest copilot language server and download it if we haven't already
async fn fetch_latest(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
let release = latest_github_release("zed-industries/copilot", false, http.clone()).await?;
let release =
latest_github_release("zed-industries/copilot", true, false, http.clone()).await?;
let version_dir = &*paths::COPILOT_DIR.join(format!("copilot-{}", release.name));
@@ -997,7 +998,7 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
let mut response = http
.get(url, Default::default(), true)
.await
.map_err(|err| anyhow!("error downloading copilot release: {}", err))?;
.context("error downloading copilot release")?;
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
let archive = Archive::new(decompressed_bytes);
archive.unpack(dist_dir).await?;

View File

@@ -185,6 +185,14 @@ impl FollowableItem for Editor {
fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
let buffer = self.buffer.read(cx);
if buffer
.as_singleton()
.and_then(|buffer| buffer.read(cx).file())
.map_or(false, |file| file.is_private())
{
return None;
}
let scroll_anchor = self.scroll_manager.anchor();
let excerpts = buffer
.read(cx)

View File

@@ -3,7 +3,7 @@
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint};
use gpui::{px, Pixels, TextSystem};
use gpui::{px, Pixels, WindowTextSystem};
use language::Point;
use std::{ops::Range, sync::Arc};
@@ -22,7 +22,7 @@ pub enum FindRange {
/// TextLayoutDetails encompasses everything we need to move vertically
/// taking into account variable width characters.
pub struct TextLayoutDetails {
pub(crate) text_system: Arc<TextSystem>,
pub(crate) text_system: Arc<WindowTextSystem>,
pub(crate) editor_style: EditorStyle,
pub(crate) rem_size: Pixels,
pub anchor: Anchor,

View File

@@ -21,12 +21,21 @@ fn generate_dispatch_bindings() {
let bindings = bindgen::Builder::default()
.header("src/platform/mac/dispatch.h")
.allowlist_var("_dispatch_main_q")
.allowlist_var("_dispatch_source_type_data_add")
.allowlist_var("DISPATCH_QUEUE_PRIORITY_DEFAULT")
.allowlist_var("DISPATCH_QUEUE_PRIORITY_HIGH")
.allowlist_var("DISPATCH_TIME_NOW")
.allowlist_function("dispatch_get_global_queue")
.allowlist_function("dispatch_async_f")
.allowlist_function("dispatch_after_f")
.allowlist_function("dispatch_time")
.allowlist_function("dispatch_source_merge_data")
.allowlist_function("dispatch_source_create")
.allowlist_function("dispatch_source_set_event_handler_f")
.allowlist_function("dispatch_resume")
.allowlist_function("dispatch_suspend")
.allowlist_function("dispatch_source_cancel")
.allowlist_function("dispatch_set_context")
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
.layout_tests(false)
.generate()

View File

@@ -17,8 +17,8 @@ use time::UtcOffset;
use crate::{
current_platform, image_cache::ImageCache, init_app_menus, Action, ActionRegistry, Any,
AnyView, AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
DispatchPhase, DisplayId, Entity, EventEmitter, ForegroundExecutor, Global, KeyBinding, Keymap,
Keystroke, LayoutId, Menu, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point, Render,
DispatchPhase, Entity, EventEmitter, ForegroundExecutor, Global, KeyBinding, Keymap, Keystroke,
LayoutId, Menu, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point, Render,
SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement,
TextSystem, View, ViewContext, Window, WindowContext, WindowHandle, WindowId,
};
@@ -192,7 +192,6 @@ impl App {
}
}
pub(crate) type FrameCallback = Box<dyn FnOnce(&mut AppContext)>;
type Handler = Box<dyn FnMut(&mut AppContext) -> bool + 'static>;
type Listener = Box<dyn FnMut(&dyn Any, &mut AppContext) -> bool + 'static>;
type KeystrokeObserver = Box<dyn FnMut(&KeystrokeEvent, &mut WindowContext) + 'static>;
@@ -212,8 +211,6 @@ pub struct AppContext {
pending_updates: usize,
pub(crate) actions: Rc<ActionRegistry>,
pub(crate) active_drag: Option<AnyDrag>,
pub(crate) next_frame_callbacks: FxHashMap<DisplayId, Vec<FrameCallback>>,
pub(crate) frame_consumers: FxHashMap<DisplayId, Task<()>>,
pub(crate) background_executor: BackgroundExecutor,
pub(crate) foreground_executor: ForegroundExecutor,
pub(crate) svg_renderer: SvgRenderer,
@@ -274,8 +271,6 @@ impl AppContext {
flushing_effects: false,
pending_updates: 0,
active_drag: None,
next_frame_callbacks: FxHashMap::default(),
frame_consumers: FxHashMap::default(),
background_executor: executor,
foreground_executor,
svg_renderer: SvgRenderer::new(asset_source.clone()),
@@ -652,27 +647,20 @@ impl AppContext {
}
}
} else {
for window in self.windows.values() {
if let Some(window) = window.as_ref() {
if window.dirty {
window.platform_window.invalidate();
}
}
}
#[cfg(any(test, feature = "test-support"))]
for window in self
.windows
.values()
.filter_map(|window| {
let window = window.as_ref()?;
(window.dirty || window.focus_invalidated).then_some(window.handle)
window.dirty.get().then_some(window.handle)
})
.collect::<Vec<_>>()
{
self.update_window(window, |_, cx| cx.draw()).unwrap();
}
#[allow(clippy::collapsible_else_if)]
if self.pending_effects.is_empty() {
break;
}
@@ -749,7 +737,7 @@ impl AppContext {
fn apply_refresh_effect(&mut self) {
for window in self.windows.values_mut() {
if let Some(window) = window.as_mut() {
window.dirty = true;
window.dirty.set(true);
}
}
}

View File

@@ -62,16 +62,6 @@ use std::{
rc::Rc,
};
/// KeymatchMode controls how keybindings are resolved in the case of conflicting pending keystrokes.
/// When `Sequenced`, gpui will wait for 1s for sequences to complete.
/// When `Immediate`, gpui will immediately resolve the keybinding.
#[derive(Default, PartialEq)]
pub enum KeymatchMode {
#[default]
Sequenced,
Immediate,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub(crate) struct DispatchNodeId(usize);
@@ -84,7 +74,6 @@ pub(crate) struct DispatchTree {
keystroke_matchers: FxHashMap<SmallVec<[KeyContext; 4]>, KeystrokeMatcher>,
keymap: Rc<RefCell<Keymap>>,
action_registry: Rc<ActionRegistry>,
pub(crate) keymatch_mode: KeymatchMode,
}
#[derive(Default)]
@@ -116,7 +105,6 @@ impl DispatchTree {
keystroke_matchers: FxHashMap::default(),
keymap,
action_registry,
keymatch_mode: KeymatchMode::Sequenced,
}
}
@@ -127,7 +115,6 @@ impl DispatchTree {
self.focusable_node_ids.clear();
self.view_node_ids.clear();
self.keystroke_matchers.clear();
self.keymatch_mode = KeymatchMode::Sequenced;
}
pub fn push_node(
@@ -335,7 +322,7 @@ impl DispatchTree {
.collect()
}
// dispatch_key pushses the next keystroke into any key binding matchers.
// dispatch_key pushes the next keystroke into any key binding matchers.
// any matching bindings are returned in the order that they should be dispatched:
// * First by length of binding (so if you have a binding for "b" and "ab", the "ab" binding fires first)
// * Secondly by depth in the tree (so if Editor has a binding for "b" and workspace a
@@ -364,6 +351,11 @@ impl DispatchTree {
.or_insert_with(|| KeystrokeMatcher::new(self.keymap.clone()));
let result = keystroke_matcher.match_keystroke(keystroke, &context_stack);
if result.pending && !pending && !bindings.is_empty() {
context_stack.pop();
continue;
}
pending = result.pending || pending;
for new_binding in result.bindings {
match bindings

View File

@@ -67,14 +67,6 @@ pub(crate) trait Platform: 'static {
options: WindowOptions,
) -> Box<dyn PlatformWindow>;
fn set_display_link_output_callback(
&self,
display_id: DisplayId,
callback: Box<dyn FnMut() + Send>,
);
fn start_display_link(&self, display_id: DisplayId);
fn stop_display_link(&self, display_id: DisplayId);
fn open_url(&self, url: &str);
fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
fn prompt_for_paths(
@@ -175,7 +167,6 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn on_close(&self, callback: Box<dyn FnOnce()>);
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
fn is_topmost_for_position(&self, position: Point<Pixels>) -> bool;
fn invalidate(&self);
fn draw(&self, scene: &Scene);
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;

View File

@@ -2,7 +2,7 @@
//! an origin at the bottom left of the main display.
mod dispatcher;
mod display;
mod display_linker;
mod display_link;
mod events;
mod metal_atlas;
mod metal_renderer;
@@ -23,7 +23,7 @@ use std::ops::Range;
pub(crate) use dispatcher::*;
pub(crate) use display::*;
pub(crate) use display_linker::*;
pub(crate) use display_link::*;
pub(crate) use metal_atlas::*;
pub(crate) use platform::*;
pub(crate) use text_system::*;

View File

@@ -1 +1,2 @@
#include <dispatch/dispatch.h>
#include <dispatch/source.h>

View File

@@ -51,7 +51,7 @@ impl PlatformDispatcher for MacDispatcher {
fn dispatch(&self, runnable: Runnable, _: Option<TaskLabel>) {
unsafe {
dispatch_async_f(
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.try_into().unwrap(), 0),
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH.try_into().unwrap(), 0),
runnable.into_raw().as_ptr() as *mut c_void,
Some(trampoline),
);

View File

@@ -1,93 +1,96 @@
use std::{
ffi::c_void,
mem,
sync::{Arc, Weak},
use crate::{
dispatch_get_main_queue,
dispatch_sys::{
_dispatch_source_type_data_add, dispatch_resume, dispatch_set_context,
dispatch_source_cancel, dispatch_source_create, dispatch_source_merge_data,
dispatch_source_set_event_handler_f, dispatch_source_t, dispatch_suspend,
},
};
use anyhow::Result;
use core_graphics::display::CGDirectDisplayID;
use std::ffi::c_void;
use util::ResultExt;
use crate::DisplayId;
use collections::HashMap;
use parking_lot::Mutex;
pub(crate) struct MacDisplayLinker {
links: HashMap<DisplayId, MacDisplayLink>,
pub struct DisplayLink {
display_link: sys::DisplayLink,
frame_requests: dispatch_source_t,
}
struct MacDisplayLink {
system_link: sys::DisplayLink,
_output_callback: Arc<OutputCallback>,
}
impl MacDisplayLinker {
pub fn new() -> Self {
MacDisplayLinker {
links: Default::default(),
impl DisplayLink {
pub fn new(
display_id: CGDirectDisplayID,
data: *mut c_void,
callback: unsafe extern "C" fn(*mut c_void),
) -> Result<DisplayLink> {
unsafe extern "C" fn display_link_callback(
_display_link_out: *mut sys::CVDisplayLink,
_current_time: *const sys::CVTimeStamp,
_output_time: *const sys::CVTimeStamp,
_flags_in: i64,
_flags_out: *mut i64,
frame_requests: *mut c_void,
) -> i32 {
let frame_requests = frame_requests as dispatch_source_t;
dispatch_source_merge_data(frame_requests, 1);
0
}
}
}
type OutputCallback = Mutex<Box<dyn FnMut() + Send>>;
impl MacDisplayLinker {
pub fn set_output_callback(
&mut self,
display_id: DisplayId,
output_callback: Box<dyn FnMut() + Send>,
) {
if let Some(mut system_link) = unsafe { sys::DisplayLink::on_display(display_id.0) } {
let callback = Arc::new(Mutex::new(output_callback));
let weak_callback_ptr: *const OutputCallback = Arc::downgrade(&callback).into_raw();
unsafe { system_link.set_output_callback(trampoline, weak_callback_ptr as *mut c_void) }
self.links.insert(
display_id,
MacDisplayLink {
_output_callback: callback,
system_link,
},
unsafe {
let frame_requests = dispatch_source_create(
&_dispatch_source_type_data_add,
0,
0,
dispatch_get_main_queue(),
);
} else {
log::warn!("DisplayLink could not be obtained for {:?}", display_id);
dispatch_set_context(
crate::dispatch_sys::dispatch_object_t {
_ds: frame_requests,
},
data,
);
dispatch_source_set_event_handler_f(frame_requests, Some(callback));
let display_link = sys::DisplayLink::new(
display_id,
display_link_callback,
frame_requests as *mut c_void,
)?;
Ok(Self {
display_link,
frame_requests,
})
}
}
pub fn start(&mut self, display_id: DisplayId) {
if let Some(link) = self.links.get_mut(&display_id) {
unsafe {
link.system_link.start();
}
} else {
log::warn!("No DisplayLink callback registered for {:?}", display_id)
pub fn start(&mut self) -> Result<()> {
unsafe {
dispatch_resume(crate::dispatch_sys::dispatch_object_t {
_ds: self.frame_requests,
});
self.display_link.start()?;
}
Ok(())
}
pub fn stop(&mut self, display_id: DisplayId) {
if let Some(link) = self.links.get_mut(&display_id) {
unsafe {
link.system_link.stop();
}
} else {
log::warn!("No DisplayLink callback registered for {:?}", display_id)
pub fn stop(&mut self) -> Result<()> {
unsafe {
dispatch_suspend(crate::dispatch_sys::dispatch_object_t {
_ds: self.frame_requests,
});
self.display_link.stop()?;
}
Ok(())
}
}
unsafe extern "C" fn trampoline(
_display_link_out: *mut sys::CVDisplayLink,
current_time: *const sys::CVTimeStamp,
output_time: *const sys::CVTimeStamp,
_flags_in: i64,
_flags_out: *mut i64,
user_data: *mut c_void,
) -> i32 {
if let Some((_current_time, _output_time)) = current_time.as_ref().zip(output_time.as_ref()) {
let output_callback: Weak<OutputCallback> =
Weak::from_raw(user_data as *mut OutputCallback);
if let Some(output_callback) = output_callback.upgrade() {
(output_callback.lock())()
impl Drop for DisplayLink {
fn drop(&mut self) {
self.stop().log_err();
unsafe {
dispatch_source_cancel(self.frame_requests);
}
mem::forget(output_callback);
}
0
}
mod sys {
@@ -96,10 +99,12 @@ mod sys {
//! Apple docs: [CVDisplayLink](https://developer.apple.com/documentation/corevideo/cvdisplaylinkoutputcallback?language=objc)
#![allow(dead_code, non_upper_case_globals)]
use anyhow::Result;
use core_graphics::display::CGDirectDisplayID;
use foreign_types::{foreign_type, ForeignType};
use std::{
ffi::c_void,
fmt::{Debug, Formatter, Result},
fmt::{self, Debug, Formatter},
};
#[derive(Debug)]
@@ -114,7 +119,7 @@ mod sys {
}
impl Debug for DisplayLink {
fn fmt(&self, formatter: &mut Formatter) -> Result {
fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
formatter
.debug_tuple("DisplayLink")
.field(&self.as_ptr())
@@ -201,19 +206,15 @@ mod sys {
pub fn CVDisplayLinkCreateWithActiveCGDisplays(
display_link_out: *mut *mut CVDisplayLink,
) -> i32;
pub fn CVDisplayLinkCreateWithCGDisplay(
pub fn CVDisplayLinkSetCurrentCGDisplay(
display_link: &mut DisplayLinkRef,
display_id: u32,
display_link_out: *mut *mut CVDisplayLink,
) -> i32;
pub fn CVDisplayLinkSetOutputCallback(
display_link: &mut DisplayLinkRef,
callback: CVDisplayLinkOutputCallback,
user_info: *mut c_void,
) -> i32;
pub fn CVDisplayLinkSetCurrentCGDisplay(
display_link: &mut DisplayLinkRef,
display_id: u32,
) -> i32;
pub fn CVDisplayLinkStart(display_link: &mut DisplayLinkRef) -> i32;
pub fn CVDisplayLinkStop(display_link: &mut DisplayLinkRef) -> i32;
pub fn CVDisplayLinkRelease(display_link: *mut CVDisplayLink);
@@ -221,52 +222,46 @@ mod sys {
}
impl DisplayLink {
/// Apple docs: [CVDisplayLinkCreateWithActiveCGDisplays](https://developer.apple.com/documentation/corevideo/1456863-cvdisplaylinkcreatewithactivecgd?language=objc)
pub unsafe fn new() -> Option<Self> {
let mut display_link: *mut CVDisplayLink = 0 as _;
let code = CVDisplayLinkCreateWithActiveCGDisplays(&mut display_link);
if code == 0 {
Some(DisplayLink::from_ptr(display_link))
} else {
None
}
}
/// Apple docs: [CVDisplayLinkCreateWithCGDisplay](https://developer.apple.com/documentation/corevideo/1456981-cvdisplaylinkcreatewithcgdisplay?language=objc)
pub unsafe fn on_display(display_id: u32) -> Option<Self> {
pub unsafe fn new(
display_id: CGDirectDisplayID,
callback: CVDisplayLinkOutputCallback,
user_info: *mut c_void,
) -> Result<Self> {
let mut display_link: *mut CVDisplayLink = 0 as _;
let code = CVDisplayLinkCreateWithCGDisplay(display_id, &mut display_link);
if code == 0 {
Some(DisplayLink::from_ptr(display_link))
} else {
None
}
let code = CVDisplayLinkCreateWithActiveCGDisplays(&mut display_link);
anyhow::ensure!(code == 0, "could not create display link, code: {}", code);
let mut display_link = DisplayLink::from_ptr(display_link);
let code = CVDisplayLinkSetOutputCallback(&mut display_link, callback, user_info);
anyhow::ensure!(code == 0, "could not set output callback, code: {}", code);
let code = CVDisplayLinkSetCurrentCGDisplay(&mut display_link, display_id);
anyhow::ensure!(
code == 0,
"could not assign display to display link, code: {}",
code
);
Ok(display_link)
}
}
impl DisplayLinkRef {
/// Apple docs: [CVDisplayLinkSetOutputCallback](https://developer.apple.com/documentation/corevideo/1457096-cvdisplaylinksetoutputcallback?language=objc)
pub unsafe fn set_output_callback(
&mut self,
callback: CVDisplayLinkOutputCallback,
user_info: *mut c_void,
) {
assert_eq!(CVDisplayLinkSetOutputCallback(self, callback, user_info), 0);
}
/// Apple docs: [CVDisplayLinkSetCurrentCGDisplay](https://developer.apple.com/documentation/corevideo/1456768-cvdisplaylinksetcurrentcgdisplay?language=objc)
pub unsafe fn set_current_display(&mut self, display_id: u32) {
assert_eq!(CVDisplayLinkSetCurrentCGDisplay(self, display_id), 0);
}
/// Apple docs: [CVDisplayLinkStart](https://developer.apple.com/documentation/corevideo/1457193-cvdisplaylinkstart?language=objc)
pub unsafe fn start(&mut self) {
assert_eq!(CVDisplayLinkStart(self), 0);
pub unsafe fn start(&mut self) -> Result<()> {
let code = CVDisplayLinkStart(self);
anyhow::ensure!(code == 0, "could not start display link, code: {}", code);
Ok(())
}
/// Apple docs: [CVDisplayLinkStop](https://developer.apple.com/documentation/corevideo/1457281-cvdisplaylinkstop?language=objc)
pub unsafe fn stop(&mut self) {
assert_eq!(CVDisplayLinkStop(self), 0);
pub unsafe fn stop(&mut self) -> Result<()> {
let code = CVDisplayLinkStop(self);
anyhow::ensure!(code == 0, "could not stop display link, code: {}", code);
Ok(())
}
}
}

View File

@@ -3,6 +3,7 @@ use crate::{
Hsla, MetalAtlas, MonochromeSprite, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch,
Quad, ScaledPixels, Scene, Shadow, Size, Surface, Underline,
};
use block::ConcreteBlock;
use cocoa::{
base::{NO, YES},
foundation::NSUInteger,
@@ -14,18 +15,21 @@ use foreign_types::ForeignType;
use media::core_video::CVMetalTextureCache;
use metal::{CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange};
use objc::{self, msg_send, sel, sel_impl};
use parking_lot::Mutex;
use smallvec::SmallVec;
use std::{ffi::c_void, mem, ptr, sync::Arc};
use std::{cell::Cell, ffi::c_void, mem, ptr, sync::Arc};
#[cfg(not(feature = "runtime_shaders"))]
const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib"));
#[cfg(feature = "runtime_shaders")]
const SHADERS_SOURCE_FILE: &'static str =
include_str!(concat!(env!("OUT_DIR"), "/stitched_shaders.metal"));
const INSTANCE_BUFFER_SIZE: usize = 32 * 1024 * 1024; // This is an arbitrary decision. There's probably a more optimal value (maybe even we could adjust dynamically...)
const INSTANCE_BUFFER_SIZE: usize = 2 * 1024 * 1024; // This is an arbitrary decision. There's probably a more optimal value (maybe even we could adjust dynamically...)
pub(crate) struct MetalRenderer {
device: metal::Device,
layer: metal::MetalLayer,
presents_with_transaction: bool,
command_queue: CommandQueue,
paths_rasterization_pipeline_state: metal::RenderPipelineState,
path_sprites_pipeline_state: metal::RenderPipelineState,
@@ -36,13 +40,14 @@ pub(crate) struct MetalRenderer {
polychrome_sprites_pipeline_state: metal::RenderPipelineState,
surfaces_pipeline_state: metal::RenderPipelineState,
unit_vertices: metal::Buffer,
instances: metal::Buffer,
#[allow(clippy::arc_with_non_send_sync)]
instance_buffer_pool: Arc<Mutex<Vec<metal::Buffer>>>,
sprite_atlas: Arc<MetalAtlas>,
core_video_texture_cache: CVMetalTextureCache,
}
impl MetalRenderer {
pub fn new(is_opaque: bool) -> Self {
pub fn new(instance_buffer_pool: Arc<Mutex<Vec<metal::Buffer>>>) -> Self {
let device: metal::Device = if let Some(device) = metal::Device::system_default() {
device
} else {
@@ -53,8 +58,8 @@ impl MetalRenderer {
let layer = metal::MetalLayer::new();
layer.set_device(&device);
layer.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
layer.set_presents_with_transaction(true);
layer.set_opaque(is_opaque);
layer.set_opaque(true);
layer.set_maximum_drawable_count(3);
unsafe {
let _: () = msg_send![&*layer, setAllowsNextDrawableTimeout: NO];
let _: () = msg_send![&*layer, setNeedsDisplayOnBoundsChange: YES];
@@ -93,10 +98,6 @@ impl MetalRenderer {
mem::size_of_val(&unit_vertices) as u64,
MTLResourceOptions::StorageModeManaged,
);
let instances = device.new_buffer(
INSTANCE_BUFFER_SIZE as u64,
MTLResourceOptions::StorageModeManaged,
);
let paths_rasterization_pipeline_state = build_path_rasterization_pipeline_state(
&device,
@@ -165,9 +166,13 @@ impl MetalRenderer {
let command_queue = device.new_command_queue();
let sprite_atlas = Arc::new(MetalAtlas::new(device.clone()));
let core_video_texture_cache =
unsafe { CVMetalTextureCache::new(device.as_ptr()).unwrap() };
Self {
device,
layer,
presents_with_transaction: false,
command_queue,
paths_rasterization_pipeline_state,
path_sprites_pipeline_state,
@@ -178,9 +183,9 @@ impl MetalRenderer {
polychrome_sprites_pipeline_state,
surfaces_pipeline_state,
unit_vertices,
instances,
instance_buffer_pool,
sprite_atlas,
core_video_texture_cache: unsafe { CVMetalTextureCache::new(device.as_ptr()).unwrap() },
core_video_texture_cache,
}
}
@@ -192,6 +197,12 @@ impl MetalRenderer {
&self.sprite_atlas
}
pub fn set_presents_with_transaction(&mut self, presents_with_transaction: bool) {
self.presents_with_transaction = presents_with_transaction;
self.layer
.set_presents_with_transaction(presents_with_transaction);
}
pub fn draw(&mut self, scene: &Scene) {
let layer = self.layer.clone();
let viewport_size = layer.drawable_size();
@@ -208,14 +219,24 @@ impl MetalRenderer {
);
return;
};
let mut instance_buffer = self.instance_buffer_pool.lock().pop().unwrap_or_else(|| {
self.device.new_buffer(
INSTANCE_BUFFER_SIZE as u64,
MTLResourceOptions::StorageModeManaged,
)
});
let command_queue = self.command_queue.clone();
let command_buffer = command_queue.new_command_buffer();
let mut instance_offset = 0;
let Some(path_tiles) =
self.rasterize_paths(scene.paths(), &mut instance_offset, command_buffer)
else {
panic!("failed to rasterize {} paths", scene.paths().len());
let Some(path_tiles) = self.rasterize_paths(
scene.paths(),
&mut instance_buffer,
&mut instance_offset,
command_buffer,
) else {
log::error!("failed to rasterize {} paths", scene.paths().len());
return;
};
let render_pass_descriptor = metal::RenderPassDescriptor::new();
@@ -243,22 +264,29 @@ impl MetalRenderer {
let ok = match batch {
PrimitiveBatch::Shadows(shadows) => self.draw_shadows(
shadows,
&mut instance_buffer,
&mut instance_offset,
viewport_size,
command_encoder,
),
PrimitiveBatch::Quads(quads) => self.draw_quads(
quads,
&mut instance_buffer,
&mut instance_offset,
viewport_size,
command_encoder,
),
PrimitiveBatch::Quads(quads) => {
self.draw_quads(quads, &mut instance_offset, viewport_size, command_encoder)
}
PrimitiveBatch::Paths(paths) => self.draw_paths(
paths,
&path_tiles,
&mut instance_buffer,
&mut instance_offset,
viewport_size,
command_encoder,
),
PrimitiveBatch::Underlines(underlines) => self.draw_underlines(
underlines,
&mut instance_buffer,
&mut instance_offset,
viewport_size,
command_encoder,
@@ -269,6 +297,7 @@ impl MetalRenderer {
} => self.draw_monochrome_sprites(
texture_id,
sprites,
&mut instance_buffer,
&mut instance_offset,
viewport_size,
command_encoder,
@@ -279,12 +308,14 @@ impl MetalRenderer {
} => self.draw_polychrome_sprites(
texture_id,
sprites,
&mut instance_buffer,
&mut instance_offset,
viewport_size,
command_encoder,
),
PrimitiveBatch::Surfaces(surfaces) => self.draw_surfaces(
surfaces,
&mut instance_buffer,
&mut instance_offset,
viewport_size,
command_encoder,
@@ -292,7 +323,7 @@ impl MetalRenderer {
};
if !ok {
panic!("scene too large: {} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces",
log::error!("scene too large: {} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces",
scene.paths.len(),
scene.shadows.len(),
scene.quads.len(),
@@ -300,28 +331,45 @@ impl MetalRenderer {
scene.monochrome_sprites.len(),
scene.polychrome_sprites.len(),
scene.surfaces.len(),
)
);
break;
}
}
command_encoder.end_encoding();
self.instances.did_modify_range(NSRange {
instance_buffer.did_modify_range(NSRange {
location: 0,
length: instance_offset as NSUInteger,
});
command_buffer.commit();
let instance_buffer_pool = self.instance_buffer_pool.clone();
let instance_buffer = Cell::new(Some(instance_buffer));
let block = ConcreteBlock::new(move |_| {
if let Some(instance_buffer) = instance_buffer.take() {
instance_buffer_pool.lock().push(instance_buffer);
}
});
let block = block.copy();
command_buffer.add_completed_handler(&block);
self.sprite_atlas.clear_textures(AtlasTextureKind::Path);
command_buffer.wait_until_completed();
drawable.present();
if self.presents_with_transaction {
command_buffer.commit();
command_buffer.wait_until_scheduled();
drawable.present();
} else {
command_buffer.present_drawable(drawable);
command_buffer.commit();
}
}
fn rasterize_paths(
&mut self,
paths: &[Path<ScaledPixels>],
offset: &mut usize,
instance_buffer: &mut metal::Buffer,
instance_offset: &mut usize,
command_buffer: &metal::CommandBufferRef,
) -> Option<HashMap<PathId, AtlasTile>> {
let mut tiles = HashMap::default();
@@ -347,9 +395,9 @@ impl MetalRenderer {
}
for (texture_id, vertices) in vertices_by_texture_id {
align_offset(offset);
align_offset(instance_offset);
let vertices_bytes_len = mem::size_of_val(vertices.as_slice());
let next_offset = *offset + vertices_bytes_len;
let next_offset = *instance_offset + vertices_bytes_len;
if next_offset > INSTANCE_BUFFER_SIZE {
return None;
}
@@ -369,8 +417,8 @@ impl MetalRenderer {
command_encoder.set_render_pipeline_state(&self.paths_rasterization_pipeline_state);
command_encoder.set_vertex_buffer(
PathRasterizationInputIndex::Vertices as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
let texture_size = Size {
width: DevicePixels::from(texture.width()),
@@ -382,7 +430,8 @@ impl MetalRenderer {
&texture_size as *const Size<DevicePixels> as *const _,
);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
let buffer_contents =
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
unsafe {
ptr::copy_nonoverlapping(
vertices.as_ptr() as *const u8,
@@ -397,7 +446,7 @@ impl MetalRenderer {
vertices.len() as u64,
);
command_encoder.end_encoding();
*offset = next_offset;
*instance_offset = next_offset;
}
Some(tiles)
@@ -406,14 +455,15 @@ impl MetalRenderer {
fn draw_shadows(
&mut self,
shadows: &[Shadow],
offset: &mut usize,
instance_buffer: &mut metal::Buffer,
instance_offset: &mut usize,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) -> bool {
if shadows.is_empty() {
return true;
}
align_offset(offset);
align_offset(instance_offset);
command_encoder.set_render_pipeline_state(&self.shadows_pipeline_state);
command_encoder.set_vertex_buffer(
@@ -423,13 +473,13 @@ impl MetalRenderer {
);
command_encoder.set_vertex_buffer(
ShadowInputIndex::Shadows as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_fragment_buffer(
ShadowInputIndex::Shadows as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_vertex_bytes(
@@ -439,9 +489,10 @@ impl MetalRenderer {
);
let shadow_bytes_len = mem::size_of_val(shadows);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
let buffer_contents =
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
let next_offset = *offset + shadow_bytes_len;
let next_offset = *instance_offset + shadow_bytes_len;
if next_offset > INSTANCE_BUFFER_SIZE {
return false;
}
@@ -460,21 +511,22 @@ impl MetalRenderer {
6,
shadows.len() as u64,
);
*offset = next_offset;
*instance_offset = next_offset;
true
}
fn draw_quads(
&mut self,
quads: &[Quad],
offset: &mut usize,
instance_buffer: &mut metal::Buffer,
instance_offset: &mut usize,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) -> bool {
if quads.is_empty() {
return true;
}
align_offset(offset);
align_offset(instance_offset);
command_encoder.set_render_pipeline_state(&self.quads_pipeline_state);
command_encoder.set_vertex_buffer(
@@ -484,13 +536,13 @@ impl MetalRenderer {
);
command_encoder.set_vertex_buffer(
QuadInputIndex::Quads as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_fragment_buffer(
QuadInputIndex::Quads as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_vertex_bytes(
@@ -500,9 +552,10 @@ impl MetalRenderer {
);
let quad_bytes_len = mem::size_of_val(quads);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
let buffer_contents =
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
let next_offset = *offset + quad_bytes_len;
let next_offset = *instance_offset + quad_bytes_len;
if next_offset > INSTANCE_BUFFER_SIZE {
return false;
}
@@ -517,7 +570,7 @@ impl MetalRenderer {
6,
quads.len() as u64,
);
*offset = next_offset;
*instance_offset = next_offset;
true
}
@@ -525,7 +578,8 @@ impl MetalRenderer {
&mut self,
paths: &[Path<ScaledPixels>],
tiles_by_path_id: &HashMap<PathId, AtlasTile>,
offset: &mut usize,
instance_buffer: &mut metal::Buffer,
instance_offset: &mut usize,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) -> bool {
@@ -573,7 +627,7 @@ impl MetalRenderer {
if sprites.is_empty() {
break;
} else {
align_offset(offset);
align_offset(instance_offset);
let texture_id = prev_texture_id.take().unwrap();
let texture: metal::Texture = self.sprite_atlas.metal_texture(texture_id);
let texture_size = size(
@@ -583,8 +637,8 @@ impl MetalRenderer {
command_encoder.set_vertex_buffer(
SpriteInputIndex::Sprites as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_vertex_bytes(
SpriteInputIndex::AtlasTextureSize as u64,
@@ -593,20 +647,20 @@ impl MetalRenderer {
);
command_encoder.set_fragment_buffer(
SpriteInputIndex::Sprites as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder
.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
let sprite_bytes_len = mem::size_of_val(sprites.as_slice());
let next_offset = *offset + sprite_bytes_len;
let next_offset = *instance_offset + sprite_bytes_len;
if next_offset > INSTANCE_BUFFER_SIZE {
return false;
}
let buffer_contents =
unsafe { (self.instances.contents() as *mut u8).add(*offset) };
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
unsafe {
ptr::copy_nonoverlapping(
@@ -622,7 +676,7 @@ impl MetalRenderer {
6,
sprites.len() as u64,
);
*offset = next_offset;
*instance_offset = next_offset;
sprites.clear();
}
}
@@ -632,14 +686,15 @@ impl MetalRenderer {
fn draw_underlines(
&mut self,
underlines: &[Underline],
offset: &mut usize,
instance_buffer: &mut metal::Buffer,
instance_offset: &mut usize,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) -> bool {
if underlines.is_empty() {
return true;
}
align_offset(offset);
align_offset(instance_offset);
command_encoder.set_render_pipeline_state(&self.underlines_pipeline_state);
command_encoder.set_vertex_buffer(
@@ -649,13 +704,13 @@ impl MetalRenderer {
);
command_encoder.set_vertex_buffer(
UnderlineInputIndex::Underlines as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_fragment_buffer(
UnderlineInputIndex::Underlines as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_vertex_bytes(
@@ -665,9 +720,10 @@ impl MetalRenderer {
);
let underline_bytes_len = mem::size_of_val(underlines);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
let buffer_contents =
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
let next_offset = *offset + underline_bytes_len;
let next_offset = *instance_offset + underline_bytes_len;
if next_offset > INSTANCE_BUFFER_SIZE {
return false;
}
@@ -686,7 +742,7 @@ impl MetalRenderer {
6,
underlines.len() as u64,
);
*offset = next_offset;
*instance_offset = next_offset;
true
}
@@ -694,14 +750,15 @@ impl MetalRenderer {
&mut self,
texture_id: AtlasTextureId,
sprites: &[MonochromeSprite],
offset: &mut usize,
instance_buffer: &mut metal::Buffer,
instance_offset: &mut usize,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) -> bool {
if sprites.is_empty() {
return true;
}
align_offset(offset);
align_offset(instance_offset);
let texture = self.sprite_atlas.metal_texture(texture_id);
let texture_size = size(
@@ -716,8 +773,8 @@ impl MetalRenderer {
);
command_encoder.set_vertex_buffer(
SpriteInputIndex::Sprites as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_vertex_bytes(
SpriteInputIndex::ViewportSize as u64,
@@ -731,15 +788,16 @@ impl MetalRenderer {
);
command_encoder.set_fragment_buffer(
SpriteInputIndex::Sprites as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
let sprite_bytes_len = mem::size_of_val(sprites);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
let buffer_contents =
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
let next_offset = *offset + sprite_bytes_len;
let next_offset = *instance_offset + sprite_bytes_len;
if next_offset > INSTANCE_BUFFER_SIZE {
return false;
}
@@ -758,7 +816,7 @@ impl MetalRenderer {
6,
sprites.len() as u64,
);
*offset = next_offset;
*instance_offset = next_offset;
true
}
@@ -766,14 +824,15 @@ impl MetalRenderer {
&mut self,
texture_id: AtlasTextureId,
sprites: &[PolychromeSprite],
offset: &mut usize,
instance_buffer: &mut metal::Buffer,
instance_offset: &mut usize,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) -> bool {
if sprites.is_empty() {
return true;
}
align_offset(offset);
align_offset(instance_offset);
let texture = self.sprite_atlas.metal_texture(texture_id);
let texture_size = size(
@@ -788,8 +847,8 @@ impl MetalRenderer {
);
command_encoder.set_vertex_buffer(
SpriteInputIndex::Sprites as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_vertex_bytes(
SpriteInputIndex::ViewportSize as u64,
@@ -803,15 +862,16 @@ impl MetalRenderer {
);
command_encoder.set_fragment_buffer(
SpriteInputIndex::Sprites as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
let sprite_bytes_len = mem::size_of_val(sprites);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
let buffer_contents =
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
let next_offset = *offset + sprite_bytes_len;
let next_offset = *instance_offset + sprite_bytes_len;
if next_offset > INSTANCE_BUFFER_SIZE {
return false;
}
@@ -830,14 +890,15 @@ impl MetalRenderer {
6,
sprites.len() as u64,
);
*offset = next_offset;
*instance_offset = next_offset;
true
}
fn draw_surfaces(
&mut self,
surfaces: &[Surface],
offset: &mut usize,
instance_buffer: &mut metal::Buffer,
instance_offset: &mut usize,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) -> bool {
@@ -889,16 +950,16 @@ impl MetalRenderer {
.unwrap()
};
align_offset(offset);
let next_offset = *offset + mem::size_of::<Surface>();
align_offset(instance_offset);
let next_offset = *instance_offset + mem::size_of::<Surface>();
if next_offset > INSTANCE_BUFFER_SIZE {
return false;
}
command_encoder.set_vertex_buffer(
SurfaceInputIndex::Surfaces as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_vertex_bytes(
SurfaceInputIndex::TextureSize as u64,
@@ -915,8 +976,8 @@ impl MetalRenderer {
);
unsafe {
let buffer_contents =
(self.instances.contents() as *mut u8).add(*offset) as *mut SurfaceBounds;
let buffer_contents = (instance_buffer.contents() as *mut u8).add(*instance_offset)
as *mut SurfaceBounds;
ptr::write(
buffer_contents,
SurfaceBounds {
@@ -927,7 +988,7 @@ impl MetalRenderer {
}
command_encoder.draw_primitives(metal::MTLPrimitiveType::Triangle, 0, 6);
*offset = next_offset;
*instance_offset = next_offset;
}
true
}

View File

@@ -1,9 +1,9 @@
use super::{events::key_to_native, BoolExt};
use crate::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
ForegroundExecutor, Keymap, MacDispatcher, MacDisplay, MacDisplayLinker, MacTextSystem,
MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, PlatformInput,
PlatformTextSystem, PlatformWindow, Result, SemanticVersion, Task, WindowOptions,
ForegroundExecutor, Keymap, MacDispatcher, MacDisplay, MacTextSystem, MacWindow, Menu,
MenuItem, PathPromptOptions, Platform, PlatformDisplay, PlatformInput, PlatformTextSystem,
PlatformWindow, Result, SemanticVersion, Task, WindowOptions,
};
use anyhow::anyhow;
use block::ConcreteBlock;
@@ -145,7 +145,7 @@ pub(crate) struct MacPlatformState {
background_executor: BackgroundExecutor,
foreground_executor: ForegroundExecutor,
text_system: Arc<MacTextSystem>,
display_linker: MacDisplayLinker,
instance_buffer_pool: Arc<Mutex<Vec<metal::Buffer>>>,
pasteboard: id,
text_hash_pasteboard_type: id,
metadata_pasteboard_type: id,
@@ -175,7 +175,7 @@ impl MacPlatform {
background_executor: BackgroundExecutor::new(dispatcher.clone()),
foreground_executor: ForegroundExecutor::new(dispatcher),
text_system: Arc::new(MacTextSystem::new()),
display_linker: MacDisplayLinker::new(),
instance_buffer_pool: Arc::default(),
pasteboard: unsafe { NSPasteboard::generalPasteboard(nil) },
text_hash_pasteboard_type: unsafe { ns_string("zed-text-hash") },
metadata_pasteboard_type: unsafe { ns_string("zed-metadata") },
@@ -494,26 +494,13 @@ impl Platform for MacPlatform {
handle: AnyWindowHandle,
options: WindowOptions,
) -> Box<dyn PlatformWindow> {
Box::new(MacWindow::open(handle, options, self.foreground_executor()))
}
fn set_display_link_output_callback(
&self,
display_id: DisplayId,
callback: Box<dyn FnMut() + Send>,
) {
self.0
.lock()
.display_linker
.set_output_callback(display_id, callback);
}
fn start_display_link(&self, display_id: DisplayId) {
self.0.lock().display_linker.start(display_id);
}
fn stop_display_link(&self, display_id: DisplayId) {
self.0.lock().display_linker.stop(display_id);
let instance_buffer_pool = self.0.lock().instance_buffer_pool.clone();
Box::new(MacWindow::open(
handle,
options,
self.foreground_executor(),
instance_buffer_pool,
))
}
fn open_url(&self, url: &str) {

View File

@@ -61,6 +61,16 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
constant Quad *quads
[[buffer(QuadInputIndex_Quads)]]) {
Quad quad = quads[input.quad_id];
// Fast path when the quad is not rounded and doesn't have any border.
if (quad.corner_radii.top_left == 0. && quad.corner_radii.bottom_left == 0. &&
quad.corner_radii.top_right == 0. &&
quad.corner_radii.bottom_right == 0. && quad.border_widths.top == 0. &&
quad.border_widths.left == 0. && quad.border_widths.right == 0. &&
quad.border_widths.bottom == 0.) {
return input.background_color;
}
float2 half_size =
float2(quad.bounds.size.width, quad.bounds.size.height) / 2.;
float2 center =

View File

@@ -1,10 +1,11 @@
use super::{global_bounds_from_ns_rect, ns_string, MacDisplay, MetalRenderer, NSRange};
use crate::{
global_bounds_to_ns_rect, platform::PlatformInputHandler, point, px, size, AnyWindowHandle,
Bounds, ExternalPaths, FileDropEvent, ForegroundExecutor, GlobalPixels, KeyDownEvent,
Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent,
MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point,
PromptLevel, Size, Timer, WindowAppearance, WindowBounds, WindowKind, WindowOptions,
Bounds, DisplayLink, ExternalPaths, FileDropEvent, ForegroundExecutor, GlobalPixels,
KeyDownEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent,
MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
PlatformWindow, Point, PromptLevel, Size, Timer, WindowAppearance, WindowBounds, WindowKind,
WindowOptions,
};
use block::ConcreteBlock;
use cocoa::{
@@ -12,7 +13,7 @@ use cocoa::{
CGPoint, NSApplication, NSBackingStoreBuffered, NSEventModifierFlags,
NSFilenamesPboardType, NSPasteboard, NSScreen, NSView, NSViewHeightSizable,
NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowCollectionBehavior,
NSWindowStyleMask, NSWindowTitleVisibility,
NSWindowOcclusionState, NSWindowStyleMask, NSWindowTitleVisibility,
},
base::{id, nil},
foundation::{
@@ -20,7 +21,7 @@ use cocoa::{
NSSize, NSString, NSUInteger,
},
};
use core_graphics::display::CGRect;
use core_graphics::display::{CGDirectDisplayID, CGRect};
use ctor::ctor;
use foreign_types::ForeignTypeRef;
use futures::channel::oneshot;
@@ -50,6 +51,7 @@ use std::{
sync::{Arc, Weak},
time::Duration,
};
use util::ResultExt;
const WINDOW_STATE_IVAR: &str = "windowState";
@@ -248,6 +250,10 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
sel!(windowDidResize:),
window_did_resize as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowDidChangeOcclusionState:),
window_did_change_occlusion_state as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowWillEnterFullScreen:),
window_will_enter_fullscreen as extern "C" fn(&Object, Sel, id),
@@ -260,6 +266,10 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
sel!(windowDidMove:),
window_did_move as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowDidChangeScreen:),
window_did_change_screen as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowDidBecomeKey:),
window_did_change_key_status as extern "C" fn(&Object, Sel, id),
@@ -320,7 +330,8 @@ struct MacWindowState {
handle: AnyWindowHandle,
executor: ForegroundExecutor,
native_window: id,
native_view: NonNull<id>,
native_view: NonNull<Object>,
display_link: Option<DisplayLink>,
renderer: MetalRenderer,
kind: WindowKind,
request_frame_callback: Option<Box<dyn FnMut()>>,
@@ -392,6 +403,21 @@ impl MacWindowState {
}
}
fn start_display_link(&mut self) {
self.stop_display_link();
let display_id = unsafe { display_id_for_screen(self.native_window.screen()) };
if let Some(mut display_link) =
DisplayLink::new(display_id, self.native_view.as_ptr() as *mut c_void, step).log_err()
{
display_link.start().log_err();
self.display_link = Some(display_link);
}
}
fn stop_display_link(&mut self) {
self.display_link = None;
}
fn is_fullscreen(&self) -> bool {
unsafe {
let style_mask = self.native_window.styleMask();
@@ -458,6 +484,7 @@ impl MacWindow {
handle: AnyWindowHandle,
options: WindowOptions,
executor: ForegroundExecutor,
instance_buffer_pool: Arc<Mutex<Vec<metal::Buffer>>>,
) -> Self {
unsafe {
let pool = NSAutoreleasePool::new(nil);
@@ -495,11 +522,8 @@ impl MacWindow {
let count: u64 = cocoa::foundation::NSArray::count(screens);
for i in 0..count {
let screen = cocoa::foundation::NSArray::objectAtIndex(screens, i);
let device_description = NSScreen::deviceDescription(screen);
let screen_number_key: id = NSString::alloc(nil).init_str("NSScreenNumber");
let screen_number = device_description.objectForKey_(screen_number_key);
let screen_number: NSUInteger = msg_send![screen_number, unsignedIntegerValue];
if screen_number as u32 == display.id().0 {
let display_id = display_id_for_screen(screen);
if display_id == display.id().0 {
target_screen = screen;
break;
}
@@ -521,15 +545,15 @@ impl MacWindow {
let native_view: id = msg_send![VIEW_CLASS, alloc];
let native_view = NSView::init(native_view);
assert!(!native_view.is_null());
let window = Self(Arc::new(Mutex::new(MacWindowState {
handle,
executor,
native_window,
native_view: NonNull::new_unchecked(native_view as *mut _),
renderer: MetalRenderer::new(true),
native_view: NonNull::new_unchecked(native_view),
display_link: None,
renderer: MetalRenderer::new(instance_buffer_pool),
kind: options.kind,
request_frame_callback: None,
event_callback: None,
@@ -663,6 +687,7 @@ impl MacWindow {
}
window.0.lock().move_traffic_light();
pool.drain();
window
@@ -685,8 +710,9 @@ impl MacWindow {
impl Drop for MacWindow {
fn drop(&mut self) {
let this = self.0.lock();
let mut this = self.0.lock();
let window = this.native_window;
this.display_link.take();
this.executor
.spawn(async move {
unsafe {
@@ -1000,13 +1026,6 @@ impl PlatformWindow for MacWindow {
}
}
fn invalidate(&self) {
let this = self.0.lock();
unsafe {
let _: () = msg_send![this.native_window.contentView(), setNeedsDisplay: YES];
}
}
fn draw(&self, scene: &crate::Scene) {
let mut this = self.0.lock();
this.renderer.draw(scene);
@@ -1320,6 +1339,22 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) {
}
}
extern "C" fn window_did_change_occlusion_state(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
let lock = &mut *window_state.lock();
unsafe {
if lock
.native_window
.occlusionState()
.contains(NSWindowOcclusionState::NSWindowOcclusionStateVisible)
{
lock.start_display_link();
} else {
lock.stop_display_link();
}
}
}
extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
window_state.as_ref().lock().move_traffic_light();
@@ -1353,6 +1388,12 @@ extern "C" fn window_did_move(this: &Object, _: Sel, _: id) {
}
}
extern "C" fn window_did_change_screen(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
lock.start_display_link();
}
extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
let lock = window_state.lock();
@@ -1495,6 +1536,24 @@ extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) {
extern "C" fn display_layer(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.lock();
if let Some(mut callback) = lock.request_frame_callback.take() {
lock.renderer.set_presents_with_transaction(true);
lock.stop_display_link();
drop(lock);
callback();
let mut lock = window_state.lock();
lock.request_frame_callback = Some(callback);
lock.renderer.set_presents_with_transaction(false);
lock.start_display_link();
}
}
unsafe extern "C" fn step(view: *mut c_void) {
let view = view as id;
let window_state = unsafe { get_window_state(&*view) };
let mut lock = window_state.lock();
if let Some(mut callback) = lock.request_frame_callback.take() {
drop(lock);
callback();
@@ -1821,3 +1880,11 @@ where
None
}
}
unsafe fn display_id_for_screen(screen: id) -> CGDirectDisplayID {
let device_description = NSScreen::deviceDescription(screen);
let screen_number_key: id = NSString::alloc(nil).init_str("NSScreenNumber");
let screen_number = device_description.objectForKey_(screen_number_key);
let screen_number: NSUInteger = msg_send![screen_number, unsignedIntegerValue];
screen_number as CGDirectDisplayID
}

View File

@@ -176,19 +176,7 @@ impl Platform for TestPlatform {
Box::new(window)
}
fn set_display_link_output_callback(
&self,
_display_id: DisplayId,
mut callback: Box<dyn FnMut() + Send>,
) {
callback()
}
fn start_display_link(&self, _display_id: DisplayId) {}
fn stop_display_link(&self, _display_id: DisplayId) {}
fn open_url(&self, _url: &str) {
fn open_url(&self, _: &str) {
unimplemented!()
}

View File

@@ -284,8 +284,6 @@ impl PlatformWindow for TestWindow {
unimplemented!()
}
fn invalidate(&self) {}
fn draw(&self, _scene: &crate::Scene) {}
fn sprite_atlas(&self) -> sync::Arc<dyn crate::PlatformAtlas> {

View File

@@ -15,6 +15,7 @@ use crate::{
use anyhow::anyhow;
use collections::{BTreeSet, FxHashMap, FxHashSet};
use core::fmt;
use derive_more::Deref;
use itertools::Itertools;
use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
use smallvec::{smallvec, SmallVec};
@@ -38,9 +39,8 @@ pub struct FontFamilyId(pub usize);
pub(crate) const SUBPIXEL_VARIANTS: u8 = 4;
/// The GPUI text layout and rendering sub system.
/// The GPUI text rendering sub system.
pub struct TextSystem {
line_layout_cache: Arc<LineLayoutCache>,
platform_text_system: Arc<dyn PlatformTextSystem>,
font_ids_by_font: RwLock<FxHashMap<Font, Result<FontId>>>,
font_metrics: RwLock<FxHashMap<FontId, FontMetrics>>,
@@ -53,7 +53,6 @@ pub struct TextSystem {
impl TextSystem {
pub(crate) fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
TextSystem {
line_layout_cache: Arc::new(LineLayoutCache::new(platform_text_system.clone())),
platform_text_system,
font_metrics: RwLock::default(),
raster_bounds: RwLock::default(),
@@ -234,43 +233,66 @@ impl TextSystem {
}
}
pub(crate) fn with_view<R>(&self, view_id: EntityId, f: impl FnOnce() -> R) -> R {
self.line_layout_cache.with_view(view_id, f)
/// Returns a handle to a line wrapper, for the given font and font size.
pub fn line_wrapper(self: &Arc<Self>, font: Font, font_size: Pixels) -> LineWrapperHandle {
let lock = &mut self.wrapper_pool.lock();
let font_id = self.resolve_font(&font);
let wrappers = lock
.entry(FontIdWithSize { font_id, font_size })
.or_default();
let wrapper = wrappers.pop().unwrap_or_else(|| {
LineWrapper::new(font_id, font_size, self.platform_text_system.clone())
});
LineWrapperHandle {
wrapper: Some(wrapper),
text_system: self.clone(),
}
}
/// Layout the given line of text, at the given font_size.
/// Subsets of the line can be styled independently with the `runs` parameter.
/// Generally, you should prefer to use `TextLayout::shape_line` instead, which
/// can be painted directly.
pub fn layout_line(
&self,
text: &str,
font_size: Pixels,
runs: &[TextRun],
) -> Result<Arc<LineLayout>> {
let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
for run in runs.iter() {
let font_id = self.resolve_font(&run.font);
if let Some(last_run) = font_runs.last_mut() {
if last_run.font_id == font_id {
last_run.len += run.len;
continue;
}
}
font_runs.push(FontRun {
len: run.len,
font_id,
});
/// Get the rasterized size and location of a specific, rendered glyph.
pub(crate) fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
let raster_bounds = self.raster_bounds.upgradable_read();
if let Some(bounds) = raster_bounds.get(params) {
Ok(*bounds)
} else {
let mut raster_bounds = RwLockUpgradableReadGuard::upgrade(raster_bounds);
let bounds = self.platform_text_system.glyph_raster_bounds(params)?;
raster_bounds.insert(params.clone(), bounds);
Ok(bounds)
}
}
let layout = self
.line_layout_cache
.layout_line(text, font_size, &font_runs);
pub(crate) fn rasterize_glyph(
&self,
params: &RenderGlyphParams,
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
let raster_bounds = self.raster_bounds(params)?;
self.platform_text_system
.rasterize_glyph(params, raster_bounds)
}
}
font_runs.clear();
self.font_runs_pool.lock().push(font_runs);
/// The GPUI text layout subsystem.
#[derive(Deref)]
pub struct WindowTextSystem {
line_layout_cache: Arc<LineLayoutCache>,
#[deref]
text_system: Arc<TextSystem>,
}
Ok(layout)
impl WindowTextSystem {
pub(crate) fn new(text_system: Arc<TextSystem>) -> Self {
Self {
line_layout_cache: Arc::new(LineLayoutCache::new(
text_system.platform_text_system.clone(),
)),
text_system,
}
}
pub(crate) fn with_view<R>(&self, view_id: EntityId, f: impl FnOnce() -> R) -> R {
self.line_layout_cache.with_view(view_id, f)
}
/// Shape the given line, at the given font_size, for painting to the screen.
@@ -429,43 +451,39 @@ impl TextSystem {
self.line_layout_cache.finish_frame(reused_views)
}
/// Returns a handle to a line wrapper, for the given font and font size.
pub fn line_wrapper(self: &Arc<Self>, font: Font, font_size: Pixels) -> LineWrapperHandle {
let lock = &mut self.wrapper_pool.lock();
let font_id = self.resolve_font(&font);
let wrappers = lock
.entry(FontIdWithSize { font_id, font_size })
.or_default();
let wrapper = wrappers.pop().unwrap_or_else(|| {
LineWrapper::new(font_id, font_size, self.platform_text_system.clone())
});
LineWrapperHandle {
wrapper: Some(wrapper),
text_system: self.clone(),
}
}
/// Get the rasterized size and location of a specific, rendered glyph.
pub(crate) fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
let raster_bounds = self.raster_bounds.upgradable_read();
if let Some(bounds) = raster_bounds.get(params) {
Ok(*bounds)
} else {
let mut raster_bounds = RwLockUpgradableReadGuard::upgrade(raster_bounds);
let bounds = self.platform_text_system.glyph_raster_bounds(params)?;
raster_bounds.insert(params.clone(), bounds);
Ok(bounds)
}
}
pub(crate) fn rasterize_glyph(
/// Layout the given line of text, at the given font_size.
/// Subsets of the line can be styled independently with the `runs` parameter.
/// Generally, you should prefer to use `TextLayout::shape_line` instead, which
/// can be painted directly.
pub fn layout_line(
&self,
params: &RenderGlyphParams,
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
let raster_bounds = self.raster_bounds(params)?;
self.platform_text_system
.rasterize_glyph(params, raster_bounds)
text: &str,
font_size: Pixels,
runs: &[TextRun],
) -> Result<Arc<LineLayout>> {
let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
for run in runs.iter() {
let font_id = self.resolve_font(&run.font);
if let Some(last_run) = font_runs.last_mut() {
if last_run.font_id == font_id {
last_run.len += run.len;
continue;
}
}
font_runs.push(FontRun {
len: run.len,
font_id,
});
}
let layout = self
.line_layout_cache
.layout_line(text, font_size, &font_runs);
font_runs.clear();
self.font_runs_pool.lock().push(font_runs);
Ok(layout)
}
}

View File

@@ -143,7 +143,7 @@ impl Boundary {
#[cfg(test)]
mod tests {
use super::*;
use crate::{font, TestAppContext, TestDispatcher, TextRun, WrapBoundary};
use crate::{font, TestAppContext, TestDispatcher, TextRun, WindowTextSystem, WrapBoundary};
use rand::prelude::*;
#[test]
@@ -218,7 +218,7 @@ mod tests {
#[crate::test]
fn test_wrap_shaped_line(cx: &mut TestAppContext) {
cx.update(|cx| {
let text_system = cx.text_system().clone();
let text_system = WindowTextSystem::new(cx.text_system().clone());
let normal = TextRun {
len: 0,

View File

@@ -2,28 +2,24 @@ use crate::{
px, size, transparent_black, Action, AnyDrag, AnyView, AppContext, Arena, AsyncWindowContext,
AvailableSpace, Bounds, Context, Corners, CursorStyle, DispatchActionListener, DispatchNodeId,
DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten,
Global, GlobalElementId, Hsla, KeyBinding, KeyContext, KeyDownEvent, KeyMatch, KeymatchMode,
KeymatchResult, Keystroke, KeystrokeEvent, Model, ModelContext, Modifiers, MouseButton,
MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
PlatformWindow, Point, PromptLevel, Render, ScaledPixels, SharedString, Size, SubscriberSet,
Subscription, TaffyLayoutEngine, Task, View, VisualContext, WeakView, WindowBounds,
WindowOptions,
Global, GlobalElementId, Hsla, KeyBinding, KeyContext, KeyDownEvent, KeyMatch, KeymatchResult,
Keystroke, KeystrokeEvent, Model, ModelContext, Modifiers, MouseButton, MouseMoveEvent,
MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point,
PromptLevel, Render, ScaledPixels, SharedString, Size, SubscriberSet, Subscription,
TaffyLayoutEngine, Task, View, VisualContext, WeakView, WindowBounds, WindowOptions,
WindowTextSystem,
};
use anyhow::{anyhow, Context as _, Result};
use collections::FxHashSet;
use derive_more::{Deref, DerefMut};
use futures::{
channel::{mpsc, oneshot},
StreamExt,
};
use futures::channel::oneshot;
use parking_lot::RwLock;
use slotmap::SlotMap;
use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
borrow::{Borrow, BorrowMut},
cell::RefCell,
collections::hash_map::Entry,
cell::{Cell, RefCell},
fmt::{Debug, Display},
future::Future,
hash::{Hash, Hasher},
@@ -34,7 +30,7 @@ use std::{
atomic::{AtomicUsize, Ordering::SeqCst},
Arc,
},
time::Duration,
time::{Duration, Instant},
};
use util::{measure, ResultExt};
@@ -243,6 +239,8 @@ impl<M: FocusableView + EventEmitter<DismissEvent>> ManagedView for M {}
/// Emitted by implementers of [`ManagedView`] to indicate the view should be dismissed, such as when a view is presented as a modal.
pub struct DismissEvent;
type FrameCallback = Box<dyn FnOnce(&mut WindowContext)>;
// Holds the state for a specific window.
#[doc(hidden)]
pub struct Window {
@@ -251,6 +249,7 @@ pub struct Window {
pub(crate) platform_window: Box<dyn PlatformWindow>,
display_id: DisplayId,
sprite_atlas: Arc<dyn PlatformAtlas>,
text_system: Arc<WindowTextSystem>,
pub(crate) rem_size: Pixels,
pub(crate) viewport_size: Size<Pixels>,
layout_engine: Option<TaffyLayoutEngine>,
@@ -258,6 +257,7 @@ pub struct Window {
pub(crate) element_id_stack: GlobalElementId,
pub(crate) rendered_frame: Frame,
pub(crate) next_frame: Frame,
next_frame_callbacks: Rc<RefCell<Vec<FrameCallback>>>,
pub(crate) dirty_views: FxHashSet<EntityId>,
pub(crate) focus_handles: Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>,
focus_listeners: SubscriberSet<(), AnyWindowFocusListener>,
@@ -268,17 +268,15 @@ pub struct Window {
scale_factor: f32,
bounds: WindowBounds,
bounds_observers: SubscriberSet<(), AnyObserver>,
active: bool,
pub(crate) dirty: bool,
active: Rc<Cell<bool>>,
pub(crate) dirty: Rc<Cell<bool>>,
pub(crate) last_input_timestamp: Rc<Cell<Instant>>,
pub(crate) refreshing: bool,
pub(crate) drawing: bool,
activation_observers: SubscriberSet<(), AnyObserver>,
pub(crate) focus: Option<FocusId>,
focus_enabled: bool,
pending_input: Option<PendingInput>,
#[cfg(any(test, feature = "test-support"))]
pub(crate) focus_invalidated: bool,
}
#[derive(Default, Debug)]
@@ -290,10 +288,6 @@ struct PendingInput {
}
impl PendingInput {
fn is_noop(&self) -> bool {
self.bindings.is_empty() && (self.keystrokes.iter().all(|k| k.ime_key.is_none()))
}
fn input(&self) -> String {
self.keystrokes
.iter()
@@ -337,13 +331,47 @@ impl Window {
let content_size = platform_window.content_size();
let scale_factor = platform_window.scale_factor();
let bounds = platform_window.bounds();
let text_system = Arc::new(WindowTextSystem::new(cx.text_system().clone()));
let dirty = Rc::new(Cell::new(true));
let active = Rc::new(Cell::new(false));
let next_frame_callbacks: Rc<RefCell<Vec<FrameCallback>>> = Default::default();
let last_input_timestamp = Rc::new(Cell::new(Instant::now()));
platform_window.on_request_frame(Box::new({
let mut cx = cx.to_async();
let dirty = dirty.clone();
let active = active.clone();
let next_frame_callbacks = next_frame_callbacks.clone();
let last_input_timestamp = last_input_timestamp.clone();
move || {
measure("frame duration", || {
handle.update(&mut cx, |_, cx| cx.draw()).log_err();
})
let next_frame_callbacks = next_frame_callbacks.take();
if !next_frame_callbacks.is_empty() {
handle
.update(&mut cx, |_, cx| {
for callback in next_frame_callbacks {
callback(cx);
}
})
.log_err();
}
if dirty.get() {
measure("frame duration", || {
handle
.update(&mut cx, |_, cx| {
cx.draw();
cx.present();
})
.log_err();
})
}
// Keep presenting the current scene for 1 extra second since the
// last input to prevent the display from underclocking the refresh rate.
else if active.get()
&& last_input_timestamp.get().elapsed() < Duration::from_secs(1)
{
handle.update(&mut cx, |_, cx| cx.present()).log_err();
}
}
}));
platform_window.on_resize(Box::new({
@@ -367,7 +395,7 @@ impl Window {
move |active| {
handle
.update(&mut cx, |_, cx| {
cx.window.active = active;
cx.window.active.set(active);
cx.window
.activation_observers
.clone()
@@ -393,6 +421,7 @@ impl Window {
platform_window,
display_id,
sprite_atlas,
text_system,
rem_size: px(16.),
viewport_size: content_size,
layout_engine: Some(TaffyLayoutEngine::new()),
@@ -400,6 +429,7 @@ impl Window {
element_id_stack: GlobalElementId::default(),
rendered_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
next_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
next_frame_callbacks,
dirty_views: FxHashSet::default(),
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
focus_listeners: SubscriberSet::new(),
@@ -410,17 +440,15 @@ impl Window {
scale_factor,
bounds,
bounds_observers: SubscriberSet::new(),
active: false,
dirty: false,
active,
dirty,
last_input_timestamp,
refreshing: false,
drawing: false,
activation_observers: SubscriberSet::new(),
focus: None,
focus_enabled: true,
pending_input: None,
#[cfg(any(test, feature = "test-support"))]
focus_invalidated: false,
}
}
}
@@ -472,7 +500,7 @@ impl<'a> WindowContext<'a> {
pub fn refresh(&mut self) {
if !self.window.drawing {
self.window.refreshing = true;
self.window.dirty = true;
self.window.dirty.set(true);
}
}
@@ -505,12 +533,6 @@ impl<'a> WindowContext<'a> {
.rendered_frame
.dispatch_tree
.clear_pending_keystrokes();
#[cfg(any(test, feature = "test-support"))]
{
self.window.focus_invalidated = true;
}
self.refresh();
}
@@ -530,6 +552,11 @@ impl<'a> WindowContext<'a> {
self.window.focus_enabled = false;
}
/// Accessor for the text system.
pub fn text_system(&self) -> &Arc<WindowTextSystem> {
&self.window.text_system
}
/// Dispatch the given action on the currently focused element.
pub fn dispatch_action(&mut self, action: Box<dyn Action>) {
let focus_handle = self.focused();
@@ -639,57 +666,7 @@ impl<'a> WindowContext<'a> {
/// Schedule the given closure to be run directly after the current frame is rendered.
pub fn on_next_frame(&mut self, callback: impl FnOnce(&mut WindowContext) + 'static) {
let handle = self.window.handle;
let display_id = self.window.display_id;
let mut frame_consumers = std::mem::take(&mut self.app.frame_consumers);
if let Entry::Vacant(e) = frame_consumers.entry(display_id) {
let (tx, mut rx) = mpsc::unbounded::<()>();
self.platform.set_display_link_output_callback(
display_id,
Box::new(move || _ = tx.unbounded_send(())),
);
let consumer_task = self.app.spawn(|cx| async move {
while rx.next().await.is_some() {
cx.update(|cx| {
for callback in cx
.next_frame_callbacks
.get_mut(&display_id)
.unwrap()
.drain(..)
.collect::<SmallVec<[_; 32]>>()
{
callback(cx);
}
})
.ok();
// Flush effects, then stop the display link if no new next_frame_callbacks have been added.
cx.update(|cx| {
if cx.next_frame_callbacks.is_empty() {
cx.platform.stop_display_link(display_id);
}
})
.ok();
}
});
e.insert(consumer_task);
}
debug_assert!(self.app.frame_consumers.is_empty());
self.app.frame_consumers = frame_consumers;
if self.next_frame_callbacks.is_empty() {
self.platform.start_display_link(display_id);
}
self.next_frame_callbacks
.entry(display_id)
.or_default()
.push(Box::new(move |cx: &mut AppContext| {
cx.update_window(handle, |_root_view, cx| callback(cx)).ok();
}));
RefCell::borrow_mut(&self.window.next_frame_callbacks).push(Box::new(callback));
}
/// Spawn the future returned by the given closure on the application thread pool.
@@ -741,7 +718,7 @@ impl<'a> WindowContext<'a> {
/// Returns whether this window is focused by the operating system (receiving key events).
pub fn is_window_active(&self) -> bool {
self.window.active
self.window.active.get()
}
/// Toggle zoom on the window.
@@ -927,16 +904,12 @@ impl<'a> WindowContext<'a> {
&self.window.next_frame.z_index_stack
}
/// Draw pixels to the display for this window based on the contents of its scene.
/// Produces a new frame and assigns it to `rendered_frame`. To actually show
/// the contents of the new [Scene], use [present].
pub(crate) fn draw(&mut self) {
self.window.dirty = false;
self.window.dirty.set(false);
self.window.drawing = true;
#[cfg(any(test, feature = "test-support"))]
{
self.window.focus_invalidated = false;
}
if let Some(requested_handler) = self.window.rendered_frame.requested_input_handler.as_mut()
{
let input_handler = self.window.platform_window.take_input_handler();
@@ -1001,7 +974,7 @@ impl<'a> WindowContext<'a> {
self.window.focus,
);
self.window.next_frame.focus = self.window.focus;
self.window.next_frame.window_active = self.window.active;
self.window.next_frame.window_active = self.window.active.get();
self.window.root_view = Some(root_view);
// Set the cursor only if we're the active window.
@@ -1070,16 +1043,19 @@ impl<'a> WindowContext<'a> {
.clone()
.retain(&(), |listener| listener(&event, self));
}
self.window
.platform_window
.draw(&self.window.rendered_frame.scene);
self.window.refreshing = false;
self.window.drawing = false;
}
fn present(&self) {
self.window
.platform_window
.draw(&self.window.rendered_frame.scene);
}
/// Dispatch a mouse or keyboard event on the window.
pub fn dispatch_event(&mut self, event: PlatformInput) -> bool {
self.window.last_input_timestamp.set(Instant::now());
// Handlers may set this to false by calling `stop_propagation`.
self.app.propagate_event = true;
// Handlers may set this to true by calling `prevent_default`.
@@ -1243,21 +1219,12 @@ impl<'a> WindowContext<'a> {
.dispatch_path(node_id);
if let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() {
let KeymatchResult {
bindings,
mut pending,
} = self
let KeymatchResult { bindings, pending } = self
.window
.rendered_frame
.dispatch_tree
.dispatch_key(&key_down_event.keystroke, &dispatch_path);
if self.window.rendered_frame.dispatch_tree.keymatch_mode == KeymatchMode::Immediate
&& !bindings.is_empty()
{
pending = false;
}
if pending {
let mut currently_pending = self.window.pending_input.take().unwrap_or_default();
if currently_pending.focus.is_some() && currently_pending.focus != self.window.focus
@@ -1272,22 +1239,17 @@ impl<'a> WindowContext<'a> {
currently_pending.bindings.push(binding);
}
// for vim compatibility, we also should check "is input handler enabled"
if !currently_pending.is_noop() {
currently_pending.timer = Some(self.spawn(|mut cx| async move {
cx.background_executor.timer(Duration::from_secs(1)).await;
cx.update(move |cx| {
cx.clear_pending_keystrokes();
let Some(currently_pending) = cx.window.pending_input.take() else {
return;
};
cx.replay_pending_input(currently_pending)
})
.log_err();
}));
} else {
currently_pending.timer = None;
}
currently_pending.timer = Some(self.spawn(|mut cx| async move {
cx.background_executor.timer(Duration::from_secs(1)).await;
cx.update(move |cx| {
cx.clear_pending_keystrokes();
let Some(currently_pending) = cx.window.pending_input.take() else {
return;
};
cx.replay_pending_input(currently_pending)
})
.log_err();
}));
self.window.pending_input = Some(currently_pending);
self.propagate_event = false;
@@ -1315,8 +1277,21 @@ impl<'a> WindowContext<'a> {
}
}
self.dispatch_key_down_up_event(event, &dispatch_path);
if !self.propagate_event {
return;
}
self.dispatch_keystroke_observers(event, None);
}
fn dispatch_key_down_up_event(
&mut self,
event: &dyn Any,
dispatch_path: &SmallVec<[DispatchNodeId; 32]>,
) {
// Capture phase
for node_id in &dispatch_path {
for node_id in dispatch_path {
let node = self.window.rendered_frame.dispatch_tree.node(*node_id);
for key_listener in node.key_listeners.clone() {
@@ -1342,8 +1317,6 @@ impl<'a> WindowContext<'a> {
}
}
}
self.dispatch_keystroke_observers(event, None);
}
/// Determine whether a potential multi-stroke key binding is in progress on this window.
@@ -1380,6 +1353,24 @@ impl<'a> WindowContext<'a> {
}
}
let dispatch_path = self
.window
.rendered_frame
.dispatch_tree
.dispatch_path(node_id);
for keystroke in currently_pending.keystrokes {
let event = KeyDownEvent {
keystroke,
is_held: false,
};
self.dispatch_key_down_up_event(&event, &dispatch_path);
if !self.propagate_event {
return;
}
}
if !input.is_empty() {
if let Some(mut input_handler) = self.window.platform_window.take_input_handler() {
input_handler.flush_pending_input(&input, self);
@@ -2023,7 +2014,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
}
if !self.window.drawing {
self.window_cx.window.dirty = true;
self.window_cx.window.dirty.set(true);
self.window_cx.app.push_effect(Effect::Notify {
emitter: self.view.model.entity_id,
});
@@ -2489,7 +2480,7 @@ impl<V: 'static + Render> WindowHandle<V> {
pub fn is_active(&self, cx: &AppContext) -> Option<bool> {
cx.windows
.get(self.id)
.and_then(|window| window.as_ref().map(|window| window.active))
.and_then(|window| window.as_ref().map(|window| window.active.get()))
}
}

View File

@@ -31,11 +31,11 @@ use crate::{
prelude::*, size, AnyTooltip, AppContext, AvailableSpace, Bounds, BoxShadow, ContentMask,
Corners, CursorStyle, DevicePixels, DispatchPhase, DispatchTree, ElementId, ElementStateBox,
EntityId, FocusHandle, FocusId, FontId, GlobalElementId, GlyphId, Hsla, ImageData,
InputHandler, IsZero, KeyContext, KeyEvent, KeymatchMode, LayoutId, MonochromeSprite,
MouseEvent, PaintQuad, Path, Pixels, PlatformInputHandler, Point, PolychromeSprite, Quad,
RenderGlyphParams, RenderImageParams, RenderSvgParams, Scene, Shadow, SharedString, Size,
StackingContext, StackingOrder, Style, Surface, TextStyleRefinement, Underline, UnderlineStyle,
Window, WindowContext, SUBPIXEL_VARIANTS,
InputHandler, IsZero, KeyContext, KeyEvent, LayoutId, MonochromeSprite, MouseEvent, PaintQuad,
Path, Pixels, PlatformInputHandler, Point, PolychromeSprite, Quad, RenderGlyphParams,
RenderImageParams, RenderSvgParams, Scene, Shadow, SharedString, Size, StackingContext,
StackingOrder, Style, Surface, TextStyleRefinement, Underline, UnderlineStyle, Window,
WindowContext, SUBPIXEL_VARIANTS,
};
type AnyMouseListener = Box<dyn FnMut(&dyn Any, DispatchPhase, &mut ElementContext) + 'static>;
@@ -1143,15 +1143,6 @@ impl<'a> ElementContext<'a> {
}
}
/// keymatch mode immediate instructs GPUI to prefer shorter action bindings.
/// In the case that you have a keybinding of `"cmd-k": "terminal::Clear"` and
/// `"cmd-k left": "workspace::MoveLeft"`, GPUI will by default wait for 1s after
/// you type cmd-k to see if you're going to type left.
/// This is problematic in the terminal
pub fn keymatch_mode_immediate(&mut self) {
self.window.next_frame.dispatch_tree.keymatch_mode = KeymatchMode::Immediate;
}
/// Register a mouse event listener on the window for the next frame. The type of event
/// is determined by the first parameter of the given listener. When the next frame is rendered
/// the listener will be cleared.

View File

@@ -384,7 +384,7 @@ pub trait File: Send + Sync {
/// Converts this file into a protobuf message.
fn to_proto(&self) -> rpc::proto::File;
/// Return whether Zed considers this to be a dotenv file.
/// Return whether Zed considers this to be a private file.
fn is_private(&self) -> bool;
}
@@ -406,6 +406,11 @@ pub trait LocalFile: File {
mtime: SystemTime,
cx: &mut AppContext,
);
/// Returns true if the file should not be shared with collaborators.
fn is_private(&self, _: &AppContext) -> bool {
false
}
}
/// The auto-indent behavior associated with an editing operation.

View File

@@ -56,6 +56,7 @@ use postage::watch;
use prettier_support::{DefaultPrettier, PrettierInstance};
use project_settings::{LspSettings, ProjectSettings};
use rand::prelude::*;
use rpc::{ErrorCode, ErrorExt};
use search::SearchQuery;
use serde::Serialize;
use settings::{Settings, SettingsStore};
@@ -1760,7 +1761,7 @@ impl Project {
cx.background_executor().spawn(async move {
wait_for_loading_buffer(loading_watch)
.await
.map_err(|error| anyhow!("{project_path:?} opening failure: {error:#}"))
.map_err(|e| e.cloned())
})
}
@@ -3984,7 +3985,7 @@ impl Project {
range.end = snapshot.clip_point_utf16(Unclipped(range.end), Bias::Right);
if range.start == range.end && range.end.column > 0 {
range.start.column -= 1;
range.end = snapshot.clip_point_utf16(Unclipped(range.end), Bias::Left);
range.start = snapshot.clip_point_utf16(Unclipped(range.start), Bias::Left);
}
}
@@ -8011,11 +8012,20 @@ impl Project {
.update(&mut cx, |this, cx| this.open_buffer_for_symbol(&symbol, cx))?
.await?;
Ok(proto::OpenBufferForSymbolResponse {
buffer_id: this.update(&mut cx, |this, cx| {
this.create_buffer_for_peer(&buffer, peer_id, cx).into()
})?,
})
this.update(&mut cx, |this, cx| {
let is_private = buffer
.read(cx)
.file()
.map(|f| f.is_private())
.unwrap_or_default();
if is_private {
Err(anyhow!(ErrorCode::UnsharedItem))
} else {
Ok(proto::OpenBufferForSymbolResponse {
buffer_id: this.create_buffer_for_peer(&buffer, peer_id, cx).into(),
})
}
})?
}
fn symbol_signature(&self, project_path: &ProjectPath) -> [u8; 32] {
@@ -8037,11 +8047,7 @@ impl Project {
let buffer = this
.update(&mut cx, |this, cx| this.open_buffer_by_id(buffer_id, cx))?
.await?;
this.update(&mut cx, |this, cx| {
Ok(proto::OpenBufferResponse {
buffer_id: this.create_buffer_for_peer(&buffer, peer_id, cx).into(),
})
})?
Project::respond_to_open_buffer_request(this, buffer, peer_id, &mut cx)
}
async fn handle_open_buffer_by_path(
@@ -8063,10 +8069,28 @@ impl Project {
})?;
let buffer = open_buffer.await?;
this.update(&mut cx, |this, cx| {
Ok(proto::OpenBufferResponse {
buffer_id: this.create_buffer_for_peer(&buffer, peer_id, cx).into(),
})
Project::respond_to_open_buffer_request(this, buffer, peer_id, &mut cx)
}
fn respond_to_open_buffer_request(
this: Model<Self>,
buffer: Model<Buffer>,
peer_id: proto::PeerId,
cx: &mut AsyncAppContext,
) -> Result<proto::OpenBufferResponse> {
this.update(cx, |this, cx| {
let is_private = buffer
.read(cx)
.file()
.map(|f| f.is_private())
.unwrap_or_default();
if is_private {
Err(anyhow!(ErrorCode::UnsharedItem))
} else {
Ok(proto::OpenBufferResponse {
buffer_id: this.create_buffer_for_peer(&buffer, peer_id, cx).into(),
})
}
})?
}

View File

@@ -31,6 +31,7 @@ theme = { path = "../theme" }
ui = { path = "../ui" }
unicase = "2.6"
util = { path = "../util" }
client = { path = "../client" }
workspace = { path = "../workspace", package = "workspace" }
[dev-dependencies]

View File

@@ -1,5 +1,6 @@
pub mod file_associations;
mod project_panel_settings;
use client::{ErrorCode, ErrorExt};
use settings::Settings;
use db::kvp::KEY_VALUE_STORE;
@@ -35,6 +36,7 @@ use unicase::UniCase;
use util::{maybe, ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
notifications::DetachAndPromptErr,
Workspace,
};
@@ -259,6 +261,7 @@ impl ProjectPanel {
} => {
if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
let file_path = entry.path.clone();
workspace
.open_path(
ProjectPath {
@@ -269,7 +272,15 @@ impl ProjectPanel {
focus_opened_item,
cx,
)
.detach_and_log_err(cx);
.detach_and_prompt_err("Failed to open file", cx, move |e, _| {
match e.error_code() {
ErrorCode::UnsharedItem => Some(format!(
"{} is not shared by the host. This could be because it has been marked as `private`",
file_path.display()
)),
_ => None,
}
});
if !focus_opened_item {
if let Some(project_panel) = project_panel.upgrade() {
let focus_handle = project_panel.read(cx).focus_handle.clone();

View File

@@ -216,6 +216,7 @@ enum ErrorCode {
BadPublicNesting = 9;
CircularNesting = 10;
WrongMoveTarget = 11;
UnsharedItem = 12;
}
message Test {

View File

@@ -80,6 +80,8 @@ pub trait ErrorExt {
fn error_tag(&self, k: &str) -> Option<&str>;
/// to_proto() converts the error into a proto::Error
fn to_proto(&self) -> proto::Error;
///
fn cloned(&self) -> anyhow::Error;
}
impl ErrorExt for anyhow::Error {
@@ -106,6 +108,14 @@ impl ErrorExt for anyhow::Error {
ErrorCode::Internal.message(format!("{}", self)).to_proto()
}
}
fn cloned(&self) -> anyhow::Error {
if let Some(rpc_error) = self.downcast_ref::<RpcError>() {
rpc_error.cloned()
} else {
anyhow::anyhow!("{}", self)
}
}
}
impl From<proto::ErrorCode> for anyhow::Error {
@@ -189,6 +199,10 @@ impl ErrorExt for RpcError {
tags: self.tags.clone(),
}
}
fn cloned(&self) -> anyhow::Error {
self.clone().into()
}
}
impl std::error::Error for RpcError {

View File

@@ -11,7 +11,7 @@ doctest = false
[dependencies]
alacritty_terminal = "0.21"
alacritty_terminal = "0.22.0"
anyhow.workspace = true
db = { path = "../db" }
dirs = "4.0.0"

View File

@@ -364,7 +364,7 @@ impl TerminalBuilder {
pty,
pty_options.hold,
false,
);
)?;
//Kick things off
let pty_tx = event_loop.channel();

View File

@@ -4,8 +4,8 @@ use gpui::{
ElementContext, ElementId, FocusHandle, Font, FontStyle, FontWeight, HighlightStyle, Hsla,
InputHandler, InteractiveBounds, InteractiveElement, InteractiveElementState, Interactivity,
IntoElement, LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, MouseMoveEvent,
Pixels, Point, ShapedLine, StatefulInteractiveElement, Styled, TextRun, TextStyle, TextSystem,
UnderlineStyle, WeakView, WhiteSpace, WindowContext,
Pixels, Point, ShapedLine, StatefulInteractiveElement, Styled, TextRun, TextStyle,
UnderlineStyle, WeakView, WhiteSpace, WindowContext, WindowTextSystem,
};
use itertools::Itertools;
use language::CursorShape;
@@ -185,7 +185,7 @@ impl TerminalElement {
grid: &Vec<IndexedCell>,
text_style: &TextStyle,
// terminal_theme: &TerminalStyle,
text_system: &TextSystem,
text_system: &WindowTextSystem,
hyperlink: Option<(HighlightStyle, &RangeInclusive<AlacPoint>)>,
cx: &WindowContext<'_>,
) -> (Vec<LayoutCell>, Vec<LayoutRect>) {
@@ -450,6 +450,13 @@ impl TerminalElement {
let mut size = bounds.size.clone();
size.width -= gutter;
// https://github.com/zed-industries/zed/issues/2750
// if the terminal is one column wide, rendering 🦀
// causes alacritty to misbehave.
if size.width < cell_width * 2.0 {
size.width = cell_width * 2.0;
}
TerminalSize::new(line_height, cell_width, size)
};
@@ -776,7 +783,6 @@ impl Element for TerminalElement {
self.interactivity
.paint(bounds, bounds.size, state, cx, |_, _, cx| {
cx.handle_input(&self.focus, terminal_input_handler);
cx.keymatch_mode_immediate();
cx.on_key_event({
let this = self.terminal.clone();

View File

@@ -255,19 +255,23 @@ impl ThemeRegistry {
continue;
};
let Some(reader) = fs.open_sync(&theme_path).await.log_err() else {
continue;
};
let Some(theme) = serde_json_lenient::from_reader(reader).log_err() else {
continue;
};
self.insert_user_theme_families([theme]);
self.load_user_theme(&theme_path, fs.clone())
.await
.log_err();
}
Ok(())
}
/// Loads the user theme from the specified path and adds it to the registry.
pub async fn load_user_theme(&self, theme_path: &Path, fs: Arc<dyn Fs>) -> Result<()> {
let reader = fs.open_sync(&theme_path).await?;
let theme = serde_json_lenient::from_reader(reader)?;
self.insert_user_theme_families([theme]);
Ok(())
}
}
impl Default for ThemeRegistry {

View File

@@ -27,6 +27,7 @@ pub struct GithubReleaseAsset {
pub async fn latest_github_release(
repo_name_with_owner: &str,
require_assets: bool,
pre_release: bool,
http: Arc<dyn HttpClient>,
) -> Result<GithubRelease, anyhow::Error> {
@@ -68,6 +69,7 @@ pub async fn latest_github_release(
releases
.into_iter()
.filter(|release| !require_assets || !release.assets.is_empty())
.find(|release| release.pre_release == pre_release)
.ok_or(anyhow!("Failed to find a release"))
}

View File

@@ -176,11 +176,19 @@ impl Member {
return div().into_any();
}
let leader = follower_states.get(pane).and_then(|state| {
let follower_state = follower_states.get(pane);
let leader = follower_state.and_then(|state| {
let room = active_call?.read(cx).room()?.read(cx);
room.remote_participant_for_peer_id(state.leader_id)
});
let is_in_unshared_view = follower_state.map_or(false, |state| {
state.active_view_id.is_some_and(|view_id| {
!state.items_by_leader_view_id.contains_key(&view_id)
})
});
let mut leader_border = None;
let mut leader_status_box = None;
let mut leader_join_data = None;
@@ -198,7 +206,14 @@ impl Member {
project_id: leader_project_id,
} => {
if Some(leader_project_id) == project.read(cx).remote_id() {
None
if is_in_unshared_view {
Some(Label::new(format!(
"{} is in an unshared pane",
leader.user.github_login
)))
} else {
None
}
} else {
leader_join_data = Some((leader_project_id, leader.user.id));
Some(Label::new(format!(

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.121.0"
version = "0.121.7"
publish = false
license = "GPL-3.0-or-later"

View File

@@ -1 +1 @@
dev
stable

View File

@@ -28,7 +28,8 @@ impl super::LspAdapter for CLspAdapter {
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
let release = latest_github_release("clangd/clangd", false, delegate.http_client()).await?;
let release =
latest_github_release("clangd/clangd", true, false, delegate.http_client()).await?;
let asset_name = format!("clangd-mac-{}.zip", release.name);
let asset = release
.assets

View File

@@ -29,10 +29,6 @@ impl super::LspAdapter for OmniSharpAdapter {
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
let release =
latest_github_release("OmniSharp/omnisharp-roslyn", false, delegate.http_client())
.await?;
let mapped_arch = match ARCH {
"aarch64" => Some("arm64"),
"x86_64" => Some("x64"),
@@ -42,6 +38,13 @@ impl super::LspAdapter for OmniSharpAdapter {
match mapped_arch {
None => Ok(Box::new(())),
Some(arch) => {
let release = latest_github_release(
"OmniSharp/omnisharp-roslyn",
true,
false,
delegate.http_client(),
)
.await?;
let asset_name = format!("omnisharp-osx-{}-net6.0.tar.gz", arch);
let asset = release
.assets

View File

@@ -70,7 +70,8 @@ impl LspAdapter for DenoLspAdapter {
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
let release = latest_github_release("denoland/deno", false, delegate.http_client()).await?;
let release =
latest_github_release("denoland/deno", true, false, delegate.http_client()).await?;
let asset_name = format!("deno-{}-apple-darwin.zip", consts::ARCH);
let asset = release
.assets

View File

@@ -111,19 +111,19 @@ impl LspAdapter for ElixirLspAdapter {
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
let http = delegate.http_client();
let release = latest_github_release("elixir-lsp/elixir-ls", false, http).await?;
let release = latest_github_release("elixir-lsp/elixir-ls", true, false, http).await?;
let version_name = release
.name
.strip_prefix("Release ")
.context("Elixir-ls release name does not start with prefix")?
.to_owned();
let asset_name = format!("elixir-ls-{}.zip", &version_name);
let asset_name = format!("elixir-ls-{version_name}.zip");
let asset = release
.assets
.iter()
.find(|asset| asset.name == asset_name)
.ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
.ok_or_else(|| anyhow!("no asset found matching {asset_name:?}"))?;
let version = GitHubLspBinaryVersion {
name: version_name,
@@ -313,20 +313,21 @@ impl LspAdapter for NextLspAdapter {
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
let release =
latest_github_release("elixir-tools/next-ls", false, delegate.http_client()).await?;
let version = release.name.clone();
let platform = match consts::ARCH {
"x86_64" => "darwin_amd64",
"aarch64" => "darwin_arm64",
other => bail!("Running on unsupported platform: {other}"),
};
let asset_name = format!("next_ls_{}", platform);
let release =
latest_github_release("elixir-tools/next-ls", true, false, delegate.http_client())
.await?;
let version = release.name;
let asset_name = format!("next_ls_{platform}");
let asset = release
.assets
.iter()
.find(|asset| asset.name == asset_name)
.ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
.with_context(|| format!("no asset found matching {asset_name:?}"))?;
let version = GitHubLspBinaryVersion {
name: version,
url: asset.browser_download_url.clone(),

View File

@@ -35,7 +35,7 @@ impl LspAdapter for GleamLspAdapter {
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
let release =
latest_github_release("gleam-lang/gleam", false, delegate.http_client()).await?;
latest_github_release("gleam-lang/gleam", true, false, delegate.http_client()).await?;
let asset_name = format!(
"gleam-{version}-{arch}-apple-darwin.tar.gz",

View File

@@ -45,7 +45,8 @@ impl super::LspAdapter for GoLspAdapter {
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
let release = latest_github_release("golang/tools", false, delegate.http_client()).await?;
let release =
latest_github_release("golang/tools", false, false, delegate.http_client()).await?;
let version: Option<String> = release.name.strip_prefix("gopls/v").map(str::to_string);
if version.is_none() {
log::warn!(

View File

@@ -30,15 +30,19 @@ impl super::LspAdapter for LuaLspAdapter {
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
let release =
latest_github_release("LuaLS/lua-language-server", false, delegate.http_client())
.await?;
let version = release.name.clone();
let platform = match consts::ARCH {
"x86_64" => "x64",
"aarch64" => "arm64",
other => bail!("Running on unsupported platform: {other}"),
};
let release = latest_github_release(
"LuaLS/lua-language-server",
true,
false,
delegate.http_client(),
)
.await?;
let version = &release.name;
let asset_name = format!("lua-language-server-{version}-darwin-{platform}.tar.gz");
let asset = release
.assets
@@ -46,7 +50,7 @@ impl super::LspAdapter for LuaLspAdapter {
.find(|asset| asset.name == asset_name)
.ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
let version = GitHubLspBinaryVersion {
name: release.name.clone(),
name: release.name,
url: asset.browser_download_url.clone(),
};
Ok(Box::new(version) as Box<_>)

View File

@@ -31,8 +31,13 @@ impl LspAdapter for RustLspAdapter {
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
let release =
latest_github_release("rust-lang/rust-analyzer", false, delegate.http_client()).await?;
let release = latest_github_release(
"rust-lang/rust-analyzer",
true,
false,
delegate.http_client(),
)
.await?;
let asset_name = format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH);
let asset = release
.assets

View File

@@ -26,7 +26,8 @@ impl LspAdapter for TaploLspAdapter {
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
let release = latest_github_release("tamasfe/taplo", false, delegate.http_client()).await?;
let release =
latest_github_release("tamasfe/taplo", true, false, delegate.http_client()).await?;
let asset_name = format!("taplo-full-darwin-{arch}.gz", arch = std::env::consts::ARCH);
let asset = release

View File

@@ -246,8 +246,13 @@ impl LspAdapter for EsLintLspAdapter {
// At the time of writing the latest vscode-eslint release was released in 2020 and requires
// special custom LSP protocol extensions be handled to fully initialize. Download the latest
// prerelease instead to sidestep this issue
let release =
latest_github_release("microsoft/vscode-eslint", true, delegate.http_client()).await?;
let release = latest_github_release(
"microsoft/vscode-eslint",
false,
false,
delegate.http_client(),
)
.await?;
Ok(Box::new(GitHubLspBinaryVersion {
name: release.name,
url: release.tarball_url,

View File

@@ -28,8 +28,9 @@ impl LspAdapter for ZlsAdapter {
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
let release = latest_github_release("zigtools/zls", false, delegate.http_client()).await?;
let asset_name = format!("zls-{}-macos.tar.gz", ARCH);
let release =
latest_github_release("zigtools/zls", true, false, delegate.http_client()).await?;
let asset_name = format!("zls-{ARCH}-macos.tar.gz");
let asset = release
.assets
.iter()

View File

@@ -11,6 +11,7 @@ use db::kvp::KEY_VALUE_STORE;
use editor::Editor;
use env_logger::Builder;
use fs::RealFs;
use fsevent::StreamFlags;
use futures::StreamExt;
use gpui::{App, AppContext, AsyncAppContext, Context, SemanticVersion, Task};
use isahc::{prelude::Configurable, Request};
@@ -171,35 +172,8 @@ fn main() {
);
assistant::init(cx);
// TODO: Should we be loading the themes in a different spot?
cx.spawn({
let fs = fs.clone();
|cx| async move {
if let Some(theme_registry) =
cx.update(|cx| ThemeRegistry::global(cx).clone()).log_err()
{
if let Some(()) = theme_registry
.load_user_themes(&paths::THEMES_DIR.clone(), fs)
.await
.log_err()
{
cx.update(|cx| {
let mut theme_settings = ThemeSettings::get_global(cx).clone();
if let Some(requested_theme) = theme_settings.requested_theme.clone() {
if let Some(_theme) =
theme_settings.switch_theme(&requested_theme, cx)
{
ThemeSettings::override_global(theme_settings, cx);
}
}
})
.log_err();
}
}
}
})
.detach();
load_user_themes_in_background(fs.clone(), cx);
watch_themes(fs.clone(), cx);
cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))
.detach();
@@ -899,6 +873,81 @@ fn load_embedded_fonts(cx: &AppContext) {
.unwrap();
}
/// Spawns a background task to load the user themes from the themes directory.
fn load_user_themes_in_background(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
cx.spawn({
let fs = fs.clone();
|cx| async move {
if let Some(theme_registry) =
cx.update(|cx| ThemeRegistry::global(cx).clone()).log_err()
{
if let Some(()) = theme_registry
.load_user_themes(&paths::THEMES_DIR.clone(), fs)
.await
.log_err()
{
cx.update(|cx| {
let mut theme_settings = ThemeSettings::get_global(cx).clone();
if let Some(requested_theme) = theme_settings.requested_theme.clone() {
if let Some(_theme) = theme_settings.switch_theme(&requested_theme, cx)
{
ThemeSettings::override_global(theme_settings, cx);
}
}
})
.log_err();
}
}
}
})
.detach();
}
/// Spawns a background task to watch the themes directory for changes.
fn watch_themes(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
cx.spawn(|cx| async move {
let mut events = fs
.watch(&paths::THEMES_DIR.clone(), Duration::from_millis(100))
.await;
while let Some(events) = events.next().await {
for event in events {
if event.flags.contains(StreamFlags::ITEM_REMOVED) {
// Theme was removed, don't need to reload.
// We may want to remove the theme from the registry, in this case.
} else {
if let Some(theme_registry) =
cx.update(|cx| ThemeRegistry::global(cx).clone()).log_err()
{
if let Some(()) = theme_registry
.load_user_theme(&event.path, fs.clone())
.await
.log_err()
{
cx.update(|cx| {
let mut theme_settings = ThemeSettings::get_global(cx).clone();
if let Some(requested_theme) =
theme_settings.requested_theme.clone()
{
if let Some(_theme) =
theme_settings.switch_theme(&requested_theme, cx)
{
ThemeSettings::override_global(theme_settings, cx);
}
}
})
.log_err();
}
}
}
}
}
})
.detach()
}
async fn watch_languages(fs: Arc<dyn fs::Fs>, languages: Arc<LanguageRegistry>) {
let reload_debounce = Duration::from_millis(250);