Merge branch 'main' into chat-mentions-for-newly-joined-users

This commit is contained in:
Conrad Irwin 2024-01-24 11:31:27 -07:00
commit 9693e14809
50 changed files with 1073 additions and 623 deletions

View file

@ -0,0 +1,24 @@
# This is just a temporary template - we will delete this once we move preview to stable
name: Parity Report
description: "Report that a regression occurred in the transition between gpui-1 Zed and gpui-2 Zed"
labels: ["admin read", "triage", "enhancement", "defect", "parity"]
body:
- type: checkboxes
attributes:
label: Check for existing issues
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
options:
- label: Completed
required: true
- type: textarea
attributes:
label: Describe the regression
validations:
required: true
- type: textarea
attributes:
label: |
If necessary, add screenshots of gpui-1 Zed to show the missing feature
description: Drag images into the text input below
validations:
required: false

View file

@ -0,0 +1,24 @@
name: Feature Request
description: "Tip: open this issue template from within Zed with the `request feature` command palette action"
labels: ["admin read", "triage", "enhancement"]
body:
- type: checkboxes
attributes:
label: Check for existing issues
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
options:
- label: Completed
required: true
- type: textarea
attributes:
label: Describe the feature
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: |
If applicable, add mockups / screenshots to help present your vision of the feature
description: Drag images into the text input below
validations:
required: false

View file

@ -0,0 +1,47 @@
name: Language Support
description: Request language support
title: "<name_of_language> support"
labels:
[
"admin read",
"triage",
"enhancement",
"language",
"unsupported language",
"potential plugin",
]
body:
- type: checkboxes
attributes:
label: Check for existing issues
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
options:
- label: Completed
required: true
- type: input
attributes:
label: Language
description: What language do you want support for?
placeholder: HTML
validations:
required: true
- type: input
attributes:
label: Tree Sitter parser link
description: If applicable, provide a link to the appropriate tree sitter parser. Look here first - https://tree-sitter.github.io/tree-sitter/#available-parsers
placeholder: https://github.com/tree-sitter/tree-sitter-html
validations:
required: false
- type: input
attributes:
label: Language server link
description: If applicable, provide a link to the appropriate language server. Look here first - https://microsoft.github.io/language-server-protocol/implementors/servers/
placeholder: https://github.com/Microsoft/vscode/tree/main/extensions/html-language-features/server
validations:
required: false
- type: textarea
attributes:
label: Misc notes
description: Provide any additional things the team should consider when adding support for this language
validations:
required: false

38
.github/ISSUE_TEMPLATE/2_bug_report.yml vendored Normal file
View file

@ -0,0 +1,38 @@
name: Bug Report
description: "Tip: open this issue template from within Zed with the `file bug report` command palette action"
labels: ["admin read", "triage", "defect"]
body:
- type: checkboxes
attributes:
label: Check for existing issues
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
options:
- label: Completed
required: true
- type: textarea
attributes:
label: Describe the bug / provide steps to reproduce it
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
id: environment
attributes:
label: Environment
description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below.
validations:
required: true
- type: textarea
attributes:
label: If applicable, add mockups / screenshots to help explain present your vision of the feature
description: Drag issues into the text input below
validations:
required: false
- type: textarea
attributes:
label: |
If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
description: Drag Zed.log into the text input below
validations:
required: false

13
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1,13 @@
contact_links:
- name: Open Source Checklist
url: https://github.com/zed-industries/community/issues/2197
about: An issue tracking our progress towards open sourcing Zed
- name: Top-Ranking Issues
url: https://github.com/zed-industries/community/issues/52
about: See an overview of the most popular Zed issues
- name: Platform Support
url: https://github.com/zed-industries/community/issues/174
about: A quick note on platform support
- name: Postive Feedback
url: https://github.com/zed-industries/community/discussions/2337
about: A central location for kind words about Zed

View file

@ -0,0 +1,17 @@
on:
schedule:
- cron: "0 */12 * * *"
workflow_dispatch:
jobs:
update_top_ranking_issues:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.10.5"
architecture: "x64"
cache: "pip"
- run: pip install -r scripts/update_top_ranking_issues/requirements.txt
- run: python scripts/update_top_ranking_issues/main.py --github-token ${{ secrets.GITHUB_TOKEN }} --prod

3
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,3 @@
# Code of Conduct
The Code of Conduct for this repository can be found online at [zed.dev/docs/code-of-conduct](https://zed.dev/docs/code-of-conduct).

2
Cargo.lock generated
View file

@ -9704,7 +9704,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.120.0"
version = "0.121.0"
dependencies = [
"activity_indicator",
"ai",

View file

@ -1,10 +1,3 @@
# 🚧 TODO 🚧
- [ ] Add intro
- [ ] Add link to contributing guide
- [ ] Add barebones running zed from source instructions
- [ ] Link out to further dev docs
# Zed
[![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
@ -16,7 +9,11 @@ Welcome to Zed, a high-performance, multiplayer code editor from the creators of
- [Building Zed](./docs/src/developing_zed__building_zed.md)
- [Running Collaboration Locally](./docs/src/developing_zed__local_collaboration.md)
### Licensing
## Contributing
See [CONTRIBUTING.md](./CONTRIBUTING.md) for ways you can contribute to Zed.
## Licensing
License information for third party dependencies must be correctly provided for CI to pass.

View file

@ -1,93 +0,0 @@
Copyright © 2017 IBM Corp. with Reserved Font Name "Plex"
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View file

@ -79,5 +79,11 @@
"cmd-1": "workspace::ToggleLeftDock",
"cmd-6": "diagnostics::Deploy"
}
},
{
"context": "ProjectPanel",
"bindings": {
"enter": "project_panel::Open"
}
}
]

View file

@ -1,3 +1,4 @@
use futures::future::BoxFuture;
use gpui::AppContext;
#[derive(Clone, Debug)]
@ -9,7 +10,14 @@ pub enum ProviderCredential {
pub trait CredentialProvider: Send + Sync {
fn has_credentials(&self) -> bool;
fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential;
fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential);
fn delete_credentials(&self, cx: &mut AppContext);
#[must_use]
fn retrieve_credentials(&self, cx: &mut AppContext) -> BoxFuture<ProviderCredential>;
#[must_use]
fn save_credentials(
&self,
cx: &mut AppContext,
credential: ProviderCredential,
) -> BoxFuture<()>;
#[must_use]
fn delete_credentials(&self, cx: &mut AppContext) -> BoxFuture<()>;
}

View file

@ -201,8 +201,10 @@ pub struct OpenAICompletionProvider {
}
impl OpenAICompletionProvider {
pub fn new(model_name: &str, executor: BackgroundExecutor) -> Self {
let model = OpenAILanguageModel::load(model_name);
pub async fn new(model_name: String, executor: BackgroundExecutor) -> Self {
let model = executor
.spawn(async move { OpenAILanguageModel::load(&model_name) })
.await;
let credential = Arc::new(RwLock::new(ProviderCredential::NoCredentials));
Self {
model,
@ -220,45 +222,70 @@ impl CredentialProvider for OpenAICompletionProvider {
}
}
fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential {
fn retrieve_credentials(&self, cx: &mut AppContext) -> BoxFuture<ProviderCredential> {
let existing_credential = self.credential.read().clone();
let retrieved_credential = match existing_credential {
ProviderCredential::Credentials { .. } => existing_credential.clone(),
ProviderCredential::Credentials { .. } => {
return async move { existing_credential }.boxed()
}
_ => {
if let Some(api_key) = env::var("OPENAI_API_KEY").log_err() {
ProviderCredential::Credentials { api_key }
} else if let Some(Some((_, api_key))) =
cx.read_credentials(OPENAI_API_URL).log_err()
{
if let Some(api_key) = String::from_utf8(api_key).log_err() {
ProviderCredential::Credentials { api_key }
} else {
ProviderCredential::NoCredentials
}
async move { ProviderCredential::Credentials { api_key } }.boxed()
} else {
ProviderCredential::NoCredentials
let credentials = cx.read_credentials(OPENAI_API_URL);
async move {
if let Some(Some((_, api_key))) = credentials.await.log_err() {
if let Some(api_key) = String::from_utf8(api_key).log_err() {
ProviderCredential::Credentials { api_key }
} else {
ProviderCredential::NoCredentials
}
} else {
ProviderCredential::NoCredentials
}
}
.boxed()
}
}
};
*self.credential.write() = retrieved_credential.clone();
retrieved_credential
async move {
let retrieved_credential = retrieved_credential.await;
*self.credential.write() = retrieved_credential.clone();
retrieved_credential
}
.boxed()
}
fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential) {
fn save_credentials(
&self,
cx: &mut AppContext,
credential: ProviderCredential,
) -> BoxFuture<()> {
*self.credential.write() = credential.clone();
let credential = credential.clone();
match credential {
let write_credentials = match credential {
ProviderCredential::Credentials { api_key } => {
cx.write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes())
.log_err();
Some(cx.write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes()))
}
_ => None,
};
async move {
if let Some(write_credentials) = write_credentials {
write_credentials.await.log_err();
}
_ => {}
}
.boxed()
}
fn delete_credentials(&self, cx: &mut AppContext) {
cx.delete_credentials(OPENAI_API_URL).log_err();
fn delete_credentials(&self, cx: &mut AppContext) -> BoxFuture<()> {
*self.credential.write() = ProviderCredential::NoCredentials;
let delete_credentials = cx.delete_credentials(OPENAI_API_URL);
async move {
delete_credentials.await.log_err();
}
.boxed()
}
}

View file

@ -1,6 +1,8 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use futures::future::BoxFuture;
use futures::AsyncReadExt;
use futures::FutureExt;
use gpui::AppContext;
use gpui::BackgroundExecutor;
use isahc::http::StatusCode;
@ -67,11 +69,14 @@ struct OpenAIEmbeddingUsage {
}
impl OpenAIEmbeddingProvider {
pub fn new(client: Arc<dyn HttpClient>, executor: BackgroundExecutor) -> Self {
pub async fn new(client: Arc<dyn HttpClient>, executor: BackgroundExecutor) -> Self {
let (rate_limit_count_tx, rate_limit_count_rx) = watch::channel_with(None);
let rate_limit_count_tx = Arc::new(Mutex::new(rate_limit_count_tx));
let model = OpenAILanguageModel::load("text-embedding-ada-002");
// Loading the model is expensive, so ensure this runs off the main thread.
let model = executor
.spawn(async move { OpenAILanguageModel::load("text-embedding-ada-002") })
.await;
let credential = Arc::new(RwLock::new(ProviderCredential::NoCredentials));
OpenAIEmbeddingProvider {
@ -154,46 +159,71 @@ impl CredentialProvider for OpenAIEmbeddingProvider {
_ => false,
}
}
fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential {
let existing_credential = self.credential.read().clone();
fn retrieve_credentials(&self, cx: &mut AppContext) -> BoxFuture<ProviderCredential> {
let existing_credential = self.credential.read().clone();
let retrieved_credential = match existing_credential {
ProviderCredential::Credentials { .. } => existing_credential.clone(),
ProviderCredential::Credentials { .. } => {
return async move { existing_credential }.boxed()
}
_ => {
if let Some(api_key) = env::var("OPENAI_API_KEY").log_err() {
ProviderCredential::Credentials { api_key }
} else if let Some(Some((_, api_key))) =
cx.read_credentials(OPENAI_API_URL).log_err()
{
if let Some(api_key) = String::from_utf8(api_key).log_err() {
ProviderCredential::Credentials { api_key }
} else {
ProviderCredential::NoCredentials
}
async move { ProviderCredential::Credentials { api_key } }.boxed()
} else {
ProviderCredential::NoCredentials
let credentials = cx.read_credentials(OPENAI_API_URL);
async move {
if let Some(Some((_, api_key))) = credentials.await.log_err() {
if let Some(api_key) = String::from_utf8(api_key).log_err() {
ProviderCredential::Credentials { api_key }
} else {
ProviderCredential::NoCredentials
}
} else {
ProviderCredential::NoCredentials
}
}
.boxed()
}
}
};
*self.credential.write() = retrieved_credential.clone();
retrieved_credential
}
fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential) {
*self.credential.write() = credential.clone();
match credential {
ProviderCredential::Credentials { api_key } => {
cx.write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes())
.log_err();
}
_ => {}
async move {
let retrieved_credential = retrieved_credential.await;
*self.credential.write() = retrieved_credential.clone();
retrieved_credential
}
.boxed()
}
fn delete_credentials(&self, cx: &mut AppContext) {
cx.delete_credentials(OPENAI_API_URL).log_err();
fn save_credentials(
&self,
cx: &mut AppContext,
credential: ProviderCredential,
) -> BoxFuture<()> {
*self.credential.write() = credential.clone();
let credential = credential.clone();
let write_credentials = match credential {
ProviderCredential::Credentials { api_key } => {
Some(cx.write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes()))
}
_ => None,
};
async move {
if let Some(write_credentials) = write_credentials {
write_credentials.await.log_err();
}
}
.boxed()
}
fn delete_credentials(&self, cx: &mut AppContext) -> BoxFuture<()> {
*self.credential.write() = ProviderCredential::NoCredentials;
let delete_credentials = cx.delete_credentials(OPENAI_API_URL);
async move {
delete_credentials.await.log_err();
}
.boxed()
}
}

View file

@ -104,11 +104,22 @@ impl CredentialProvider for FakeEmbeddingProvider {
fn has_credentials(&self) -> bool {
true
}
fn retrieve_credentials(&self, _cx: &mut AppContext) -> ProviderCredential {
ProviderCredential::NotNeeded
fn retrieve_credentials(&self, _cx: &mut AppContext) -> BoxFuture<ProviderCredential> {
async { ProviderCredential::NotNeeded }.boxed()
}
fn save_credentials(
&self,
_cx: &mut AppContext,
_credential: ProviderCredential,
) -> BoxFuture<()> {
async {}.boxed()
}
fn delete_credentials(&self, _cx: &mut AppContext) -> BoxFuture<()> {
async {}.boxed()
}
fn save_credentials(&self, _cx: &mut AppContext, _credential: ProviderCredential) {}
fn delete_credentials(&self, _cx: &mut AppContext) {}
}
#[async_trait]
@ -165,11 +176,22 @@ impl CredentialProvider for FakeCompletionProvider {
fn has_credentials(&self) -> bool {
true
}
fn retrieve_credentials(&self, _cx: &mut AppContext) -> ProviderCredential {
ProviderCredential::NotNeeded
fn retrieve_credentials(&self, _cx: &mut AppContext) -> BoxFuture<ProviderCredential> {
async { ProviderCredential::NotNeeded }.boxed()
}
fn save_credentials(
&self,
_cx: &mut AppContext,
_credential: ProviderCredential,
) -> BoxFuture<()> {
async {}.boxed()
}
fn delete_credentials(&self, _cx: &mut AppContext) -> BoxFuture<()> {
async {}.boxed()
}
fn save_credentials(&self, _cx: &mut AppContext, _credential: ProviderCredential) {}
fn delete_credentials(&self, _cx: &mut AppContext) {}
}
impl CompletionProvider for FakeCompletionProvider {

View file

@ -6,14 +6,12 @@ use crate::{
NewConversation, QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata,
SavedMessage, Split, ToggleFocus, ToggleIncludeConversation, ToggleRetrieveContext,
};
use ai::prompts::repository_context::PromptCodeSnippet;
use ai::{
auth::ProviderCredential,
completion::{CompletionProvider, CompletionRequest},
providers::open_ai::{OpenAICompletionProvider, OpenAIRequest, RequestMessage},
};
use ai::prompts::repository_context::PromptCodeSnippet;
use anyhow::{anyhow, Result};
use chrono::{DateTime, Local};
use client::telemetry::AssistantKind;
@ -31,9 +29,9 @@ use fs::Fs;
use futures::StreamExt;
use gpui::{
canvas, div, point, relative, rems, uniform_list, Action, AnyElement, AppContext,
AsyncWindowContext, AvailableSpace, ClipboardItem, Context, EventEmitter, FocusHandle,
FocusableView, FontStyle, FontWeight, HighlightStyle, InteractiveElement, IntoElement, Model,
ModelContext, ParentElement, Pixels, PromptLevel, Render, SharedString,
AsyncAppContext, AsyncWindowContext, AvailableSpace, ClipboardItem, Context, EventEmitter,
FocusHandle, FocusableView, FontStyle, FontWeight, HighlightStyle, InteractiveElement,
IntoElement, Model, ModelContext, ParentElement, Pixels, PromptLevel, Render, SharedString,
StatefulInteractiveElement, Styled, Subscription, Task, TextStyle, UniformListScrollHandle,
View, ViewContext, VisualContext, WeakModel, WeakView, WhiteSpace, WindowContext,
};
@ -123,6 +121,10 @@ impl AssistantPanel {
.await
.log_err()
.unwrap_or_default();
// Defaulting currently to GPT4, allow for this to be set via config.
let completion_provider =
OpenAICompletionProvider::new("gpt-4".into(), cx.background_executor().clone())
.await;
// TODO: deserialize state.
let workspace_handle = workspace.clone();
@ -156,11 +158,6 @@ impl AssistantPanel {
});
let semantic_index = SemanticIndex::global(cx);
// Defaulting currently to GPT4, allow for this to be set via config.
let completion_provider = Arc::new(OpenAICompletionProvider::new(
"gpt-4",
cx.background_executor().clone(),
));
let focus_handle = cx.focus_handle();
cx.on_focus_in(&focus_handle, Self::focus_in).detach();
@ -176,7 +173,7 @@ impl AssistantPanel {
zoomed: false,
focus_handle,
toolbar,
completion_provider,
completion_provider: Arc::new(completion_provider),
api_key_editor: None,
languages: workspace.app_state().languages.clone(),
fs: workspace.app_state().fs.clone(),
@ -221,23 +218,9 @@ impl AssistantPanel {
_: &InlineAssist,
cx: &mut ViewContext<Workspace>,
) {
let this = if let Some(this) = workspace.panel::<AssistantPanel>(cx) {
if this.update(cx, |assistant, cx| {
if !assistant.has_credentials() {
assistant.load_credentials(cx);
};
assistant.has_credentials()
}) {
this
} else {
workspace.focus_panel::<AssistantPanel>(cx);
return;
}
} else {
let Some(assistant) = workspace.panel::<AssistantPanel>(cx) else {
return;
};
let active_editor = if let Some(active_editor) = workspace
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))
@ -246,12 +229,32 @@ impl AssistantPanel {
} else {
return;
};
let project = workspace.project().clone();
let project = workspace.project();
if assistant.update(cx, |assistant, _| assistant.has_credentials()) {
assistant.update(cx, |assistant, cx| {
assistant.new_inline_assist(&active_editor, cx, &project)
});
} else {
let assistant = assistant.downgrade();
cx.spawn(|workspace, mut cx| async move {
assistant
.update(&mut cx, |assistant, cx| assistant.load_credentials(cx))?
.await;
if assistant.update(&mut cx, |assistant, _| assistant.has_credentials())? {
assistant.update(&mut cx, |assistant, cx| {
assistant.new_inline_assist(&active_editor, cx, &project)
})?;
} else {
workspace.update(&mut cx, |workspace, cx| {
workspace.focus_panel::<AssistantPanel>(cx)
})?;
}
this.update(cx, |assistant, cx| {
assistant.new_inline_assist(&active_editor, cx, project)
});
anyhow::Ok(())
})
.detach_and_log_err(cx)
}
}
fn new_inline_assist(
@ -291,9 +294,6 @@ impl AssistantPanel {
let inline_assist_id = post_inc(&mut self.next_inline_assist_id);
let provider = self.completion_provider.clone();
// Retrieve Credentials Authenticates the Provider
provider.retrieve_credentials(cx);
let codegen = cx.new_model(|cx| {
Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx)
});
@ -846,11 +846,18 @@ impl AssistantPanel {
api_key: api_key.clone(),
};
self.completion_provider.save_credentials(cx, credential);
let completion_provider = self.completion_provider.clone();
cx.spawn(|this, mut cx| async move {
cx.update(|cx| completion_provider.save_credentials(cx, credential))?
.await;
self.api_key_editor.take();
self.focus_handle.focus(cx);
cx.notify();
this.update(&mut cx, |this, cx| {
this.api_key_editor.take();
this.focus_handle.focus(cx);
cx.notify();
})
})
.detach_and_log_err(cx);
}
} else {
cx.propagate();
@ -858,10 +865,17 @@ impl AssistantPanel {
}
fn reset_credentials(&mut self, _: &ResetKey, cx: &mut ViewContext<Self>) {
self.completion_provider.delete_credentials(cx);
self.api_key_editor = Some(build_api_key_editor(cx));
self.focus_handle.focus(cx);
cx.notify();
let completion_provider = self.completion_provider.clone();
cx.spawn(|this, mut cx| async move {
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.focus_handle.focus(cx);
cx.notify();
})
})
.detach_and_log_err(cx);
}
fn toggle_zoom(&mut self, _: &workspace::ToggleZoom, cx: &mut ViewContext<Self>) {
@ -1079,9 +1093,9 @@ impl AssistantPanel {
cx.spawn(|this, mut cx| async move {
let saved_conversation = fs.load(&path).await?;
let saved_conversation = serde_json::from_str(&saved_conversation)?;
let conversation = cx.new_model(|cx| {
Conversation::deserialize(saved_conversation, path.clone(), languages, cx)
})?;
let conversation =
Conversation::deserialize(saved_conversation, path.clone(), languages, &mut cx)
.await?;
this.update(&mut cx, |this, cx| {
// If, by the time we've loaded the conversation, the user has already opened
// the same conversation, we don't want to open it again.
@ -1108,8 +1122,16 @@ impl AssistantPanel {
self.completion_provider.has_credentials()
}
fn load_credentials(&mut self, cx: &mut ViewContext<Self>) {
self.completion_provider.retrieve_credentials(cx);
fn load_credentials(&mut self, cx: &mut ViewContext<Self>) -> Task<()> {
let completion_provider = self.completion_provider.clone();
cx.spawn(|_, mut cx| async move {
if let Some(retrieve_credentials) = cx
.update(|cx| completion_provider.retrieve_credentials(cx))
.log_err()
{
retrieve_credentials.await;
}
})
}
}
@ -1315,11 +1337,16 @@ impl Panel for AssistantPanel {
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
if active {
self.load_credentials(cx);
if self.editors.is_empty() {
self.new_conversation(cx);
}
let load_credentials = self.load_credentials(cx);
cx.spawn(|this, mut cx| async move {
load_credentials.await;
this.update(&mut cx, |this, cx| {
if this.editors.is_empty() {
this.new_conversation(cx);
}
})
})
.detach_and_log_err(cx);
}
}
@ -1462,21 +1489,25 @@ impl Conversation {
}
}
fn deserialize(
async fn deserialize(
saved_conversation: SavedConversation,
path: PathBuf,
language_registry: Arc<LanguageRegistry>,
cx: &mut ModelContext<Self>,
) -> Self {
cx: &mut AsyncAppContext,
) -> Result<Model<Self>> {
let id = match saved_conversation.id {
Some(id) => Some(id),
None => Some(Uuid::new_v4().to_string()),
};
let model = saved_conversation.model;
let completion_provider: Arc<dyn CompletionProvider> = Arc::new(
OpenAICompletionProvider::new(model.full_name(), cx.background_executor().clone()),
OpenAICompletionProvider::new(
model.full_name().into(),
cx.background_executor().clone(),
)
.await,
);
completion_provider.retrieve_credentials(cx);
cx.update(|cx| completion_provider.retrieve_credentials(cx))?;
let markdown = language_registry.language_for_name("Markdown");
let mut message_anchors = Vec::new();
let mut next_message_id = MessageId(0);
@ -1499,32 +1530,34 @@ impl Conversation {
})
.detach_and_log_err(cx);
buffer
});
})?;
let mut this = Self {
id,
message_anchors,
messages_metadata: saved_conversation.message_metadata,
next_message_id,
summary: Some(Summary {
text: saved_conversation.summary,
done: true,
}),
pending_summary: Task::ready(None),
completion_count: Default::default(),
pending_completions: Default::default(),
token_count: None,
max_token_count: tiktoken_rs::model::get_context_size(&model.full_name()),
pending_token_count: Task::ready(None),
model,
_subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
pending_save: Task::ready(Ok(())),
path: Some(path),
buffer,
completion_provider,
};
this.count_remaining_tokens(cx);
this
cx.new_model(|cx| {
let mut this = Self {
id,
message_anchors,
messages_metadata: saved_conversation.message_metadata,
next_message_id,
summary: Some(Summary {
text: saved_conversation.summary,
done: true,
}),
pending_summary: Task::ready(None),
completion_count: Default::default(),
pending_completions: Default::default(),
token_count: None,
max_token_count: tiktoken_rs::model::get_context_size(&model.full_name()),
pending_token_count: Task::ready(None),
model,
_subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
pending_save: Task::ready(Ok(())),
path: Some(path),
buffer,
completion_provider,
};
this.count_remaining_tokens(cx);
this
})
}
fn handle_buffer_event(
@ -3169,7 +3202,7 @@ mod tests {
use super::*;
use crate::MessageId;
use ai::test::FakeCompletionProvider;
use gpui::AppContext;
use gpui::{AppContext, TestAppContext};
use settings::SettingsStore;
#[gpui::test]
@ -3487,16 +3520,17 @@ mod tests {
}
#[gpui::test]
fn test_serialization(cx: &mut AppContext) {
let settings_store = SettingsStore::test(cx);
async fn test_serialization(cx: &mut TestAppContext) {
let settings_store = cx.update(SettingsStore::test);
cx.set_global(settings_store);
init(cx);
cx.update(init);
let registry = Arc::new(LanguageRegistry::test());
let completion_provider = Arc::new(FakeCompletionProvider::new());
let conversation =
cx.new_model(|cx| Conversation::new(registry.clone(), cx, completion_provider));
let buffer = conversation.read(cx).buffer.clone();
let message_0 = conversation.read(cx).message_anchors[0].id;
let buffer = conversation.read_with(cx, |conversation, _| conversation.buffer.clone());
let message_0 =
conversation.read_with(cx, |conversation, _| conversation.message_anchors[0].id);
let message_1 = conversation.update(cx, |conversation, cx| {
conversation
.insert_message_after(message_0, Role::Assistant, MessageStatus::Done, cx)
@ -3517,9 +3551,9 @@ mod tests {
.unwrap()
});
buffer.update(cx, |buffer, cx| buffer.undo(cx));
assert_eq!(buffer.read(cx).text(), "a\nb\nc\n");
assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "a\nb\nc\n");
assert_eq!(
messages(&conversation, cx),
cx.read(|cx| messages(&conversation, cx)),
[
(message_0, Role::User, 0..2),
(message_1.id, Role::Assistant, 2..6),
@ -3527,18 +3561,22 @@ mod tests {
]
);
let deserialized_conversation = cx.new_model(|cx| {
Conversation::deserialize(
conversation.read(cx).serialize(cx),
Default::default(),
registry.clone(),
cx,
)
});
let deserialized_buffer = deserialized_conversation.read(cx).buffer.clone();
assert_eq!(deserialized_buffer.read(cx).text(), "a\nb\nc\n");
let deserialized_conversation = Conversation::deserialize(
conversation.read_with(cx, |conversation, cx| conversation.serialize(cx)),
Default::default(),
registry.clone(),
&mut cx.to_async(),
)
.await
.unwrap();
let deserialized_buffer =
deserialized_conversation.read_with(cx, |conversation, _| conversation.buffer.clone());
assert_eq!(
messages(&deserialized_conversation, cx),
deserialized_buffer.read_with(cx, |buffer, _| buffer.text()),
"a\nb\nc\n"
);
assert_eq!(
cx.read(|cx| messages(&deserialized_conversation, cx)),
[
(message_0, Role::User, 0..2),
(message_1.id, Role::Assistant, 2..6),

View file

@ -700,8 +700,8 @@ impl Client {
}
}
pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
read_credentials_from_keychain(cx).is_some()
pub async fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
read_credentials_from_keychain(cx).await.is_some()
}
#[async_recursion(?Send)]
@ -732,7 +732,7 @@ impl Client {
let mut read_from_keychain = false;
let mut credentials = self.state.read().credentials.clone();
if credentials.is_none() && try_keychain {
credentials = read_credentials_from_keychain(cx);
credentials = read_credentials_from_keychain(cx).await;
read_from_keychain = credentials.is_some();
}
if credentials.is_none() {
@ -770,7 +770,7 @@ impl Client {
Ok(conn) => {
self.state.write().credentials = Some(credentials.clone());
if !read_from_keychain && IMPERSONATE_LOGIN.is_none() {
write_credentials_to_keychain(credentials, cx).log_err();
write_credentials_to_keychain(credentials, cx).await.log_err();
}
futures::select_biased! {
@ -784,7 +784,7 @@ impl Client {
Err(EstablishConnectionError::Unauthorized) => {
self.state.write().credentials.take();
if read_from_keychain {
delete_credentials_from_keychain(cx).log_err();
delete_credentials_from_keychain(cx).await.log_err();
self.set_status(Status::SignedOut, cx);
self.authenticate_and_connect(false, cx).await
} else {
@ -1350,14 +1350,16 @@ impl Client {
}
}
fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
async fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
if IMPERSONATE_LOGIN.is_some() {
return None;
}
let (user_id, access_token) = cx
.update(|cx| cx.read_credentials(&ZED_SERVER_URL).log_err().flatten())
.ok()??;
.update(|cx| cx.read_credentials(&ZED_SERVER_URL))
.log_err()?
.await
.log_err()??;
Some(Credentials {
user_id: user_id.parse().ok()?,
@ -1365,7 +1367,10 @@ fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
})
}
fn write_credentials_to_keychain(credentials: Credentials, cx: &AsyncAppContext) -> Result<()> {
async fn write_credentials_to_keychain(
credentials: Credentials,
cx: &AsyncAppContext,
) -> Result<()> {
cx.update(move |cx| {
cx.write_credentials(
&ZED_SERVER_URL,
@ -1373,10 +1378,12 @@ fn write_credentials_to_keychain(credentials: Credentials, cx: &AsyncAppContext)
credentials.access_token.as_bytes(),
)
})?
.await
}
fn delete_credentials_from_keychain(cx: &AsyncAppContext) -> Result<()> {
async fn delete_credentials_from_keychain(cx: &AsyncAppContext) -> Result<()> {
cx.update(move |cx| cx.delete_credentials(&ZED_SERVER_URL))?
.await
}
const WORKTREE_URL_PREFIX: &str = "zed://worktrees/";

View file

@ -126,6 +126,7 @@ const MAX_LINE_LEN: usize = 1024;
const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
const MAX_SELECTION_HISTORY_LEN: usize = 1024;
const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
pub(crate) const CURSORS_VISIBLE_FOR: Duration = Duration::from_millis(2000);
#[doc(hidden)]
pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250);
#[doc(hidden)]
@ -369,7 +370,7 @@ pub struct Editor {
collaboration_hub: Option<Box<dyn CollaborationHub>>,
blink_manager: Model<BlinkManager>,
show_cursor_names: bool,
hovered_cursor: Option<HoveredCursor>,
hovered_cursors: HashMap<HoveredCursor, Task<()>>,
pub show_local_selections: bool,
mode: EditorMode,
show_gutter: bool,
@ -463,6 +464,7 @@ enum SelectionHistoryMode {
Redoing,
}
#[derive(Clone, PartialEq, Eq, Hash)]
struct HoveredCursor {
replica_id: u16,
selection_id: usize,
@ -1440,7 +1442,7 @@ impl Editor {
gutter_width: Default::default(),
style: None,
show_cursor_names: false,
hovered_cursor: Default::default(),
hovered_cursors: Default::default(),
editor_actions: Default::default(),
show_copilot_suggestions: mode == EditorMode::Full,
_subscriptions: vec![
@ -3741,7 +3743,7 @@ impl Editor {
self.show_cursor_names = true;
cx.notify();
cx.spawn(|this, mut cx| async move {
cx.background_executor().timer(Duration::from_secs(3)).await;
cx.background_executor().timer(CURSORS_VISIBLE_FOR).await;
this.update(&mut cx, |this, cx| {
this.show_cursor_names = false;
cx.notify()

View file

@ -17,8 +17,8 @@ use crate::{
mouse_context_menu,
scroll::scroll_amount::ScrollAmount,
CursorShape, DisplayPoint, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle,
HalfPageDown, HalfPageUp, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, SelectPhase,
Selection, SoftWrap, ToPoint, MAX_LINE_LEN,
HalfPageDown, HalfPageUp, HoveredCursor, LineDown, LineUp, OpenExcerpts, PageDown, PageUp,
Point, SelectPhase, Selection, SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
};
use anyhow::Result;
use collections::{BTreeMap, HashMap};
@ -612,13 +612,24 @@ impl EditorElement {
.anchor_at(range.end.to_point(&snapshot.display_snapshot), Bias::Right);
let Some(selection) = snapshot.remote_selections_in_range(&range, hub, cx).next() else {
editor.hovered_cursor.take();
return;
};
editor.hovered_cursor.replace(crate::HoveredCursor {
let key = crate::HoveredCursor {
replica_id: selection.replica_id,
selection_id: selection.selection.id,
});
};
editor.hovered_cursors.insert(
key.clone(),
cx.spawn(|editor, mut cx| async move {
cx.background_executor().timer(CURSORS_VISIBLE_FOR).await;
editor
.update(&mut cx, |editor, cx| {
editor.hovered_cursors.remove(&key);
cx.notify();
})
.ok();
}),
);
cx.notify()
}
@ -1986,7 +1997,9 @@ impl EditorElement {
if Some(selection.peer_id) == editor.leader_peer_id {
continue;
}
let is_shown = editor.show_cursor_names || editor.hovered_cursor.as_ref().is_some_and(|c| c.replica_id == selection.replica_id && c.selection_id == selection.selection.id);
let key = HoveredCursor{replica_id: selection.replica_id, selection_id: selection.selection.id};
let is_shown = editor.show_cursor_names || editor.hovered_cursors.contains_key(&key);
remote_selections
.entry(selection.replica_id)

View file

@ -1,3 +1,40 @@
# GPUI
# Welcome to GPUI!
A fast, productive UI framework from the creators of [https://zed.dev].
GPUI is a hybrid immediate and retained mode, GPU accelerated, UI framework
for Rust, designed to support a wide variety of applications.
## Getting Started
GPUI is still in active development as we work on the Zed code editor and isn't yet on crates.io. You'll also need to use the latest version of stable rust and be on macOS. Add the following to your Cargo.toml:
```
gpui = { git = "https://github.com/zed-industries/zed" }
```
Everything in GPUI starts with an `App`. You can create one with `App::new()`, and kick off your application by passing a callback to `App::run()`. Inside this callback, you can create a new window with `AppContext::open_window()`, and register your first root view. See [gpui.rs](https://www.gpui.rs/) for a complete example.
## The Big Picture
GPUI offers three different registers(https://en.wikipedia.org/wiki/Register_(sociolinguistics)) depending on your needs:
- State management and communication with Models. Whenever you need to store application state that communicates between different parts of your application, you'll want to use GPUI's models. Models are owned by GPUI and are only accessible through an owned smart pointer similar to an `Rc`. See the `app::model_context` module for more information.
- High level, declarative UI with Views. All UI in GPUI starts with a View. A view is simply a model that can be rendered, via the `Render` trait. At the start of each frame, GPUI will call this render method on the root view of a given window. Views build a tree of `elements`, lay them out and style them with a tailwind-style API, and then give them to GPUI to turn into pixels. See the `div` element for an all purpose swiss-army knife of rendering.
- Low level, imperative UI with Elements. Elements are the building blocks of UI in GPUI, and they provide a nice wrapper around an imperative API that provides as much flexibility and control as you need. Elements have total control over how they and their child elements are rendered and and can be used for making efficient views into large lists, implement custom layouting for a code editor, and anything else you can think of. See the `element` module for more information.
Each of these registers has one or more corresponding contexts that can be accessed from all GPUI services. This context is your main interface to GPUI, and is used extensively throughout the framework.
## Other Resources
In addition to the systems above, GPUI provides a range of smaller services that are useful for building complex applications:
- Actions are user-defined structs that are used for converting keystrokes into logical operations in your UI. Use this for implementing keyboard shortcuts, such as cmd-q. See the `action` module for more information.
- Platform services, such as `quit the app` or `open a URL` are available as methods on the `app::AppContext`.
- An async executor that is integrated with the platform's event loop. See the `executor` module for more information.,
- The `[gpui::test]` macro provides a convenient way to write tests for your GPUI applications. Tests also have their own kind of context, a `TestAppContext` which provides ways of simulating common platform input. See `app::test_context` and `test` modules for more details.
Currently, the best way to learn about these APIs is to read the Zed source code, ask us about it at a fireside hack, or drop a question in the [Zed Discord](https://discord.gg/U4qhCEhMXP). We're working on improving the documentation, creating more examples, and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog).

View file

@ -523,17 +523,22 @@ impl AppContext {
}
/// Writes credentials to the platform keychain.
pub fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()> {
pub fn write_credentials(
&self,
url: &str,
username: &str,
password: &[u8],
) -> Task<Result<()>> {
self.platform.write_credentials(url, username, password)
}
/// Reads credentials from the platform keychain.
pub fn read_credentials(&self, url: &str) -> Result<Option<(String, Vec<u8>)>> {
pub fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
self.platform.read_credentials(url)
}
/// Deletes credentials from the platform keychain.
pub fn delete_credentials(&self, url: &str) -> Result<()> {
pub fn delete_credentials(&self, url: &str) -> Task<Result<()>> {
self.platform.delete_credentials(url)
}

View file

@ -154,6 +154,7 @@ impl AsyncAppContext {
}
/// Reads the global state of the specified type, passing it to the given callback.
///
/// Panics if no global state of the specified type has been assigned.
/// Returns an error if the `AppContext` has been dropped.
pub fn read_global<G: 'static, R>(&self, read: impl FnOnce(&G, &AppContext) -> R) -> Result<R> {
@ -166,7 +167,10 @@ impl AsyncAppContext {
}
/// Reads the global state of the specified type, passing it to the given callback.
/// Similar to [read_global], but returns an error instead of panicking if no state of the specified type has been assigned.
///
/// Similar to [`AsyncAppContext::read_global`], but returns an error instead of panicking
/// if no state of the specified type has been assigned.
///
/// Returns an error if no state of the specified type has been assigned the `AppContext` has been dropped.
pub fn try_read_global<G: 'static, R>(
&self,
@ -212,12 +216,12 @@ impl AsyncWindowContext {
self.window
}
/// A convenience method for [WindowContext::update()]
/// A convenience method for [`AppContext::update_window`].
pub fn update<R>(&mut self, update: impl FnOnce(&mut WindowContext) -> R) -> Result<R> {
self.app.update_window(self.window, |_, cx| update(cx))
}
/// A convenience method for [WindowContext::update()]
/// A convenience method for [`AppContext::update_window`].
pub fn update_root<R>(
&mut self,
update: impl FnOnce(AnyView, &mut WindowContext) -> R,
@ -225,12 +229,12 @@ impl AsyncWindowContext {
self.app.update_window(self.window, update)
}
/// A convenience method for [WindowContext::on_next_frame()]
/// A convenience method for [`WindowContext::on_next_frame`].
pub fn on_next_frame(&mut self, f: impl FnOnce(&mut WindowContext) + 'static) {
self.window.update(self, |_, cx| cx.on_next_frame(f)).ok();
}
/// A convenience method for [AppContext::global()]
/// A convenience method for [`AppContext::global`].
pub fn read_global<G: 'static, R>(
&mut self,
read: impl FnOnce(&G, &WindowContext) -> R,
@ -238,7 +242,7 @@ impl AsyncWindowContext {
self.window.update(self, |_, cx| read(cx.global(), cx))
}
/// A convenience method for [AppContext::update_global()]
/// A convenience method for [`AppContext::update_global`].
/// for updating the global state of the specified type.
pub fn update_global<G, R>(
&mut self,

View file

@ -42,7 +42,8 @@ impl<'a, T: 'static> ModelContext<'a, T> {
self.model_state.clone()
}
/// Arranges for the given function to be called whenever [ModelContext::notify] or [ViewContext::notify] is called with the given model or view.
/// Arranges for the given function to be called whenever [`ModelContext::notify`] or
/// [`ViewContext::notify`](crate::ViewContext::notify) is called with the given model or view.
pub fn observe<W, E>(
&mut self,
entity: &E,
@ -150,7 +151,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
}
/// Arrange for the given function to be invoked whenever the application is quit.
/// The future returned from this callback will be polled for up to [gpui::SHUTDOWN_TIMEOUT] until the app fully quits.
/// The future returned from this callback will be polled for up to [crate::SHUTDOWN_TIMEOUT] until the app fully quits.
pub fn on_app_quit<Fut>(
&mut self,
mut on_quit: impl FnMut(&mut T, &mut ModelContext<T>) -> Fut + 'static,

View file

@ -44,6 +44,14 @@ impl Arena {
}
}
pub fn len(&self) -> usize {
self.offset as usize - self.start as usize
}
pub fn capacity(&self) -> usize {
self.end as usize - self.start as usize
}
pub fn clear(&mut self) {
self.valid.set(false);
self.valid = Rc::new(Cell::new(true));

View file

@ -3,11 +3,11 @@ use serde::de::{self, Deserialize, Deserializer, Visitor};
use std::fmt;
/// Convert an RGB hex color code number to a color type
pub fn rgb<C: From<Rgba>>(hex: u32) -> C {
pub fn rgb(hex: u32) -> Rgba {
let r = ((hex >> 16) & 0xFF) as f32 / 255.0;
let g = ((hex >> 8) & 0xFF) as f32 / 255.0;
let b = (hex & 0xFF) as f32 / 255.0;
Rgba { r, g, b, a: 1.0 }.into()
Rgba { r, g, b, a: 1.0 }
}
/// Convert an RGBA hex color code number to [`Rgba`]
@ -40,7 +40,6 @@ impl fmt::Debug for Rgba {
impl Rgba {
/// Create a new [`Rgba`] color by blending this and another color together
/// TODO!(docs): find the source for this algorithm
pub fn blend(&self, other: Rgba) -> Self {
if other.a >= 1.0 {
other

View file

@ -136,6 +136,25 @@ impl Render for () {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {}
}
/// A quick way to create a [`Render`]able view without having to define a new type.
#[cfg(any(test, feature = "test-support"))]
pub struct TestView(Box<dyn FnMut(&mut ViewContext<TestView>) -> AnyElement>);
#[cfg(any(test, feature = "test-support"))]
impl TestView {
/// Construct a TestView from a render closure.
pub fn new<F: FnMut(&mut ViewContext<TestView>) -> AnyElement + 'static>(f: F) -> Self {
Self(Box::new(f))
}
}
#[cfg(any(test, feature = "test-support"))]
impl Render for TestView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
(self.0)(cx)
}
}
/// You can derive [`IntoElement`] on any type that implements this trait.
/// It is used to construct reusable `components` out of plain data. Think of
/// components as a recipe for a certain pattern of elements. RenderOnce allows

View file

@ -1,7 +1,7 @@
//! Div is the central, reusable element that most GPUI trees will be built from.
//! It functions as a container for other elements, and provides a number of
//! useful features for laying out and styling its children as well as binding
//! mouse events and action handlers. It is meant to be similar to the HTML <div>
//! mouse events and action handlers. It is meant to be similar to the HTML `<div>`
//! element, but for GPUI.
//!
//! # Build your own div
@ -14,13 +14,6 @@
//! as several associated traits. Together, these provide the full suite of Dom-like events
//! and Tailwind-like styling that you can use to build your own custom elements. Div is
//! constructed by combining these two systems into an all-in-one element.
//!
//! # Capturing and bubbling
//!
//! Note that while event dispatch in GPUI uses similar names and concepts to the web
//! even API, the details are very different. See the documentation in [TODO!(docs)
//! DOCUMENT EVENT DISPATCH SOMEWHERE IN WINDOW CONTEXT] for more details
//!
use crate::{
point, px, size, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, Bounds,
@ -85,7 +78,7 @@ impl Interactivity {
/// Bind the given callback to the mouse down event for the given mouse button, during the bubble phase
/// The imperative API equivalent of [`InteractiveElement::on_mouse_down`]
///
/// See [`ViewContext::listener()`] to get access to the view state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to the view state from this callback.
pub fn on_mouse_down(
&mut self,
button: MouseButton,
@ -105,7 +98,7 @@ impl Interactivity {
/// Bind the given callback to the mouse down event for any button, during the capture phase
/// The imperative API equivalent of [`InteractiveElement::capture_any_mouse_down`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
pub fn capture_any_mouse_down(
&mut self,
listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
@ -119,9 +112,9 @@ impl Interactivity {
}
/// Bind the given callback to the mouse down event for any button, during the bubble phase
/// the imperative API equivalent to [`InteractiveElement::on_any_mouse_down()`]
/// the imperative API equivalent to [`InteractiveElement::on_any_mouse_down`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
pub fn on_any_mouse_down(
&mut self,
listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
@ -135,9 +128,9 @@ impl Interactivity {
}
/// Bind the given callback to the mouse up event for the given button, during the bubble phase
/// the imperative API equivalent to [`InteractiveElement::on_mouse_up()`]
/// the imperative API equivalent to [`InteractiveElement::on_mouse_up`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
pub fn on_mouse_up(
&mut self,
button: MouseButton,
@ -155,9 +148,9 @@ impl Interactivity {
}
/// Bind the given callback to the mouse up event for any button, during the capture phase
/// the imperative API equivalent to [`InteractiveElement::capture_any_mouse_up()`]
/// the imperative API equivalent to [`InteractiveElement::capture_any_mouse_up`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
pub fn capture_any_mouse_up(
&mut self,
listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static,
@ -171,9 +164,9 @@ impl Interactivity {
}
/// Bind the given callback to the mouse up event for any button, during the bubble phase
/// the imperative API equivalent to [`InteractiveElement::on_any_mouse_up()`]
/// the imperative API equivalent to [`Interactivity::on_any_mouse_up`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
pub fn on_any_mouse_up(
&mut self,
listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static,
@ -188,9 +181,9 @@ impl Interactivity {
/// Bind the given callback to the mouse down event, on any button, during the capture phase,
/// when the mouse is outside of the bounds of this element.
/// The imperative API equivalent to [`InteractiveElement::on_mouse_down_out()`]
/// The imperative API equivalent to [`InteractiveElement::on_mouse_down_out`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
pub fn on_mouse_down_out(
&mut self,
listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
@ -206,9 +199,9 @@ impl Interactivity {
/// Bind the given callback to the mouse up event, for the given button, during the capture phase,
/// when the mouse is outside of the bounds of this element.
/// The imperative API equivalent to [`InteractiveElement::on_mouse_up_out()`]
/// The imperative API equivalent to [`InteractiveElement::on_mouse_up_out`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
pub fn on_mouse_up_out(
&mut self,
button: MouseButton,
@ -226,9 +219,9 @@ impl Interactivity {
}
/// Bind the given callback to the mouse move event, during the bubble phase
/// The imperative API equivalent to [`InteractiveElement::on_mouse_move()`]
/// The imperative API equivalent to [`InteractiveElement::on_mouse_move`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
pub fn on_mouse_move(
&mut self,
listener: impl Fn(&MouseMoveEvent, &mut WindowContext) + 'static,
@ -245,9 +238,9 @@ impl Interactivity {
/// will be called for all move events, inside or outside of this element, as long as the
/// drag was started with this element under the mouse. Useful for implementing draggable
/// UIs that don't conform to a drag and drop style interaction, like resizing.
/// The imperative API equivalent to [`InteractiveElement::on_drag_move()`]
/// The imperative API equivalent to [`InteractiveElement::on_drag_move`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
pub fn on_drag_move<T>(
&mut self,
listener: impl Fn(&DragMoveEvent<T>, &mut WindowContext) + 'static,
@ -275,9 +268,9 @@ impl Interactivity {
}
/// Bind the given callback to scroll wheel events during the bubble phase
/// The imperative API equivalent to [`InteractiveElement::on_scroll_wheel()`]
/// The imperative API equivalent to [`InteractiveElement::on_scroll_wheel`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
pub fn on_scroll_wheel(
&mut self,
listener: impl Fn(&ScrollWheelEvent, &mut WindowContext) + 'static,
@ -291,9 +284,9 @@ impl Interactivity {
}
/// Bind the given callback to an action dispatch during the capture phase
/// The imperative API equivalent to [`InteractiveElement::capture_action()`]
/// The imperative API equivalent to [`InteractiveElement::capture_action`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
pub fn capture_action<A: Action>(
&mut self,
listener: impl Fn(&A, &mut WindowContext) + 'static,
@ -310,9 +303,9 @@ impl Interactivity {
}
/// Bind the given callback to an action dispatch during the bubble phase
/// The imperative API equivalent to [`InteractiveElement::on_action()`]
/// The imperative API equivalent to [`InteractiveElement::on_action`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
pub fn on_action<A: Action>(&mut self, listener: impl Fn(&A, &mut WindowContext) + 'static) {
self.action_listeners.push((
TypeId::of::<A>(),
@ -328,9 +321,9 @@ impl Interactivity {
/// Bind the given callback to an action dispatch, based on a dynamic action parameter
/// instead of a type parameter. Useful for component libraries that want to expose
/// action bindings to their users.
/// The imperative API equivalent to [`InteractiveElement::on_boxed_action()`]
/// The imperative API equivalent to [`InteractiveElement::on_boxed_action`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
pub fn on_boxed_action(
&mut self,
action: &dyn Action,
@ -348,9 +341,9 @@ impl Interactivity {
}
/// Bind the given callback to key down events during the bubble phase
/// The imperative API equivalent to [`InteractiveElement::on_key_down()`]
/// The imperative API equivalent to [`InteractiveElement::on_key_down`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
pub fn on_key_down(&mut self, listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static) {
self.key_down_listeners
.push(Box::new(move |event, phase, cx| {
@ -361,9 +354,9 @@ impl Interactivity {
}
/// Bind the given callback to key down events during the capture phase
/// The imperative API equivalent to [`InteractiveElement::capture_key_down()`]
/// The imperative API equivalent to [`InteractiveElement::capture_key_down`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
pub fn capture_key_down(
&mut self,
listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static,
@ -377,9 +370,9 @@ impl Interactivity {
}
/// Bind the given callback to key up events during the bubble phase
/// The imperative API equivalent to [`InteractiveElement::on_key_up()`]
/// The imperative API equivalent to [`InteractiveElement::on_key_up`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
pub fn on_key_up(&mut self, listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static) {
self.key_up_listeners
.push(Box::new(move |event, phase, cx| {
@ -390,9 +383,9 @@ impl Interactivity {
}
/// Bind the given callback to key up events during the capture phase
/// The imperative API equivalent to [`InteractiveElement::on_key_up()`]
/// The imperative API equivalent to [`InteractiveElement::on_key_up`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
pub fn capture_key_up(&mut self, listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static) {
self.key_up_listeners
.push(Box::new(move |event, phase, cx| {
@ -403,9 +396,9 @@ impl Interactivity {
}
/// Bind the given callback to drop events of the given type, whether or not the drag started on this element
/// The imperative API equivalent to [`InteractiveElement::on_drop()`]
/// The imperative API equivalent to [`InteractiveElement::on_drop`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
pub fn on_drop<T: 'static>(&mut self, listener: impl Fn(&T, &mut WindowContext) + 'static) {
self.drop_listeners.push((
TypeId::of::<T>(),
@ -416,15 +409,15 @@ impl Interactivity {
}
/// Use the given predicate to determine whether or not a drop event should be dispatched to this element
/// The imperative API equivalent to [`InteractiveElement::can_drop()`]
/// The imperative API equivalent to [`InteractiveElement::can_drop`]
pub fn can_drop(&mut self, predicate: impl Fn(&dyn Any, &mut WindowContext) -> bool + 'static) {
self.can_drop_predicate = Some(Box::new(predicate));
}
/// Bind the given callback to click events of this element
/// The imperative API equivalent to [`InteractiveElement::on_click()`]
/// The imperative API equivalent to [`StatefulInteractiveElement::on_click`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
pub fn on_click(&mut self, listener: impl Fn(&ClickEvent, &mut WindowContext) + 'static)
where
Self: Sized,
@ -435,10 +428,10 @@ impl Interactivity {
/// On drag initiation, this callback will be used to create a new view to render the dragged value for a
/// drag and drop operation. This API should also be used as the equivalent of 'on drag start' with
/// the [`Self::on_drag_move()`] API
/// The imperative API equivalent to [`InteractiveElement::on_drag()`]
/// the [`Self::on_drag_move`] API
/// The imperative API equivalent to [`StatefulInteractiveElement::on_drag`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
pub fn on_drag<T, W>(
&mut self,
value: T,
@ -460,9 +453,9 @@ impl Interactivity {
/// Bind the given callback on the hover start and end events of this element. Note that the boolean
/// passed to the callback is true when the hover starts and false when it ends.
/// The imperative API equivalent to [`InteractiveElement::on_drag()`]
/// The imperative API equivalent to [`StatefulInteractiveElement::on_drag`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
pub fn on_hover(&mut self, listener: impl Fn(&bool, &mut WindowContext) + 'static)
where
Self: Sized,
@ -475,7 +468,7 @@ impl Interactivity {
}
/// Use the given callback to construct a new tooltip view when the mouse hovers over this element.
/// The imperative API equivalent to [`InteractiveElement::tooltip()`]
/// The imperative API equivalent to [`InteractiveElement::tooltip`]
pub fn tooltip(&mut self, build_tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static)
where
Self: Sized,
@ -488,7 +481,7 @@ impl Interactivity {
}
/// Block the mouse from interacting with this element or any of it's children
/// The imperative API equivalent to [`InteractiveElement::block_mouse()`]
/// The imperative API equivalent to [`InteractiveElement::block_mouse`]
pub fn block_mouse(&mut self) {
self.block_mouse = true;
}
@ -559,9 +552,9 @@ pub trait InteractiveElement: Sized {
}
/// Bind the given callback to the mouse down event for the given mouse button,
/// the fluent API equivalent to [`Interactivity::on_mouse_down()`]
/// the fluent API equivalent to [`Interactivity::on_mouse_down`]
///
/// See [`ViewContext::listener()`] to get access to the view state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to the view state from this callback.
fn on_mouse_down(
mut self,
button: MouseButton,
@ -573,7 +566,7 @@ pub trait InteractiveElement: Sized {
#[cfg(any(test, feature = "test-support"))]
/// Set a key that can be used to look up this element's bounds
/// in the [`VisualTestContext::debug_bounds()`] map
/// in the [`VisualTestContext::debug_bounds`] map
/// This is a noop in release builds
fn debug_selector(mut self, f: impl FnOnce() -> String) -> Self {
self.interactivity().debug_selector = Some(f());
@ -582,7 +575,7 @@ pub trait InteractiveElement: Sized {
#[cfg(not(any(test, feature = "test-support")))]
/// Set a key that can be used to look up this element's bounds
/// in the [`VisualTestContext::debug_bounds()`] map
/// in the [`VisualTestContext::debug_bounds`] map
/// This is a noop in release builds
#[inline]
fn debug_selector(self, _: impl FnOnce() -> String) -> Self {
@ -590,9 +583,9 @@ pub trait InteractiveElement: Sized {
}
/// Bind the given callback to the mouse down event for any button, during the capture phase
/// the fluent API equivalent to [`Interactivity::capture_any_mouse_down()`]
/// the fluent API equivalent to [`Interactivity::capture_any_mouse_down`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
fn capture_any_mouse_down(
mut self,
listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
@ -602,9 +595,9 @@ pub trait InteractiveElement: Sized {
}
/// Bind the given callback to the mouse down event for any button, during the capture phase
/// the fluent API equivalent to [`Interactivity::on_any_mouse_down()`]
/// the fluent API equivalent to [`Interactivity::on_any_mouse_down`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
fn on_any_mouse_down(
mut self,
listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
@ -614,9 +607,9 @@ pub trait InteractiveElement: Sized {
}
/// Bind the given callback to the mouse up event for the given button, during the bubble phase
/// the fluent API equivalent to [`Interactivity::on_mouse_up()`]
/// the fluent API equivalent to [`Interactivity::on_mouse_up`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
fn on_mouse_up(
mut self,
button: MouseButton,
@ -627,9 +620,9 @@ pub trait InteractiveElement: Sized {
}
/// Bind the given callback to the mouse up event for any button, during the capture phase
/// the fluent API equivalent to [`Interactivity::capture_any_mouse_up()`]
/// the fluent API equivalent to [`Interactivity::capture_any_mouse_up`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
fn capture_any_mouse_up(
mut self,
listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static,
@ -640,9 +633,9 @@ pub trait InteractiveElement: Sized {
/// Bind the given callback to the mouse down event, on any button, during the capture phase,
/// when the mouse is outside of the bounds of this element.
/// The fluent API equivalent to [`Interactivity::on_mouse_down_out()`]
/// The fluent API equivalent to [`Interactivity::on_mouse_down_out`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
fn on_mouse_down_out(
mut self,
listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
@ -653,9 +646,9 @@ pub trait InteractiveElement: Sized {
/// Bind the given callback to the mouse up event, for the given button, during the capture phase,
/// when the mouse is outside of the bounds of this element.
/// The fluent API equivalent to [`Interactivity::on_mouse_up_out()`]
/// The fluent API equivalent to [`Interactivity::on_mouse_up_out`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
fn on_mouse_up_out(
mut self,
button: MouseButton,
@ -666,9 +659,9 @@ pub trait InteractiveElement: Sized {
}
/// Bind the given callback to the mouse move event, during the bubble phase
/// The fluent API equivalent to [`Interactivity::on_mouse_move()`]
/// The fluent API equivalent to [`Interactivity::on_mouse_move`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
fn on_mouse_move(
mut self,
listener: impl Fn(&MouseMoveEvent, &mut WindowContext) + 'static,
@ -681,9 +674,9 @@ pub trait InteractiveElement: Sized {
/// will be called for all move events, inside or outside of this element, as long as the
/// drag was started with this element under the mouse. Useful for implementing draggable
/// UIs that don't conform to a drag and drop style interaction, like resizing.
/// The fluent API equivalent to [`Interactivity::on_drag_move()`]
/// The fluent API equivalent to [`Interactivity::on_drag_move`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
fn on_drag_move<T: 'static>(
mut self,
listener: impl Fn(&DragMoveEvent<T>, &mut WindowContext) + 'static,
@ -696,9 +689,9 @@ pub trait InteractiveElement: Sized {
}
/// Bind the given callback to scroll wheel events during the bubble phase
/// The fluent API equivalent to [`Interactivity::on_scroll_wheel()`]
/// The fluent API equivalent to [`Interactivity::on_scroll_wheel`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
fn on_scroll_wheel(
mut self,
listener: impl Fn(&ScrollWheelEvent, &mut WindowContext) + 'static,
@ -708,9 +701,9 @@ pub trait InteractiveElement: Sized {
}
/// Capture the given action, before normal action dispatch can fire
/// The fluent API equivalent to [`Interactivity::on_scroll_wheel()`]
/// The fluent API equivalent to [`Interactivity::on_scroll_wheel`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
fn capture_action<A: Action>(
mut self,
listener: impl Fn(&A, &mut WindowContext) + 'static,
@ -720,9 +713,9 @@ pub trait InteractiveElement: Sized {
}
/// Bind the given callback to an action dispatch during the bubble phase
/// The fluent API equivalent to [`Interactivity::on_action()`]
/// The fluent API equivalent to [`Interactivity::on_action`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
fn on_action<A: Action>(mut self, listener: impl Fn(&A, &mut WindowContext) + 'static) -> Self {
self.interactivity().on_action(listener);
self
@ -731,9 +724,9 @@ pub trait InteractiveElement: Sized {
/// Bind the given callback to an action dispatch, based on a dynamic action parameter
/// instead of a type parameter. Useful for component libraries that want to expose
/// action bindings to their users.
/// The fluent API equivalent to [`Interactivity::on_boxed_action()`]
/// The fluent API equivalent to [`Interactivity::on_boxed_action`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
fn on_boxed_action(
mut self,
action: &dyn Action,
@ -744,9 +737,9 @@ pub trait InteractiveElement: Sized {
}
/// Bind the given callback to key down events during the bubble phase
/// The fluent API equivalent to [`Interactivity::on_key_down()`]
/// The fluent API equivalent to [`Interactivity::on_key_down`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
fn on_key_down(
mut self,
listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static,
@ -756,9 +749,9 @@ pub trait InteractiveElement: Sized {
}
/// Bind the given callback to key down events during the capture phase
/// The fluent API equivalent to [`Interactivity::capture_key_down()`]
/// The fluent API equivalent to [`Interactivity::capture_key_down`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
fn capture_key_down(
mut self,
listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static,
@ -768,18 +761,18 @@ pub trait InteractiveElement: Sized {
}
/// Bind the given callback to key up events during the bubble phase
/// The fluent API equivalent to [`Interactivity::on_key_up()`]
/// The fluent API equivalent to [`Interactivity::on_key_up`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
fn on_key_up(mut self, listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static) -> Self {
self.interactivity().on_key_up(listener);
self
}
/// Bind the given callback to key up events during the capture phase
/// The fluent API equivalent to [`Interactivity::capture_key_up()`]
/// The fluent API equivalent to [`Interactivity::capture_key_up`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
fn capture_key_up(
mut self,
listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static,
@ -813,16 +806,16 @@ pub trait InteractiveElement: Sized {
}
/// Bind the given callback to drop events of the given type, whether or not the drag started on this element
/// The fluent API equivalent to [`Interactivity::on_drop()`]
/// The fluent API equivalent to [`Interactivity::on_drop`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
fn on_drop<T: 'static>(mut self, listener: impl Fn(&T, &mut WindowContext) + 'static) -> Self {
self.interactivity().on_drop(listener);
self
}
/// Use the given predicate to determine whether or not a drop event should be dispatched to this element
/// The fluent API equivalent to [`Interactivity::can_drop()`]
/// The fluent API equivalent to [`Interactivity::can_drop`]
fn can_drop(
mut self,
predicate: impl Fn(&dyn Any, &mut WindowContext) -> bool + 'static,
@ -832,7 +825,7 @@ pub trait InteractiveElement: Sized {
}
/// Block the mouse from interacting with this element or any of it's children
/// The fluent API equivalent to [`Interactivity::block_mouse()`]
/// The fluent API equivalent to [`Interactivity::block_mouse`]
fn block_mouse(mut self) -> Self {
self.interactivity().block_mouse();
self
@ -899,9 +892,9 @@ pub trait StatefulInteractiveElement: InteractiveElement {
}
/// Bind the given callback to click events of this element
/// The fluent API equivalent to [`Interactivity::on_click()`]
/// The fluent API equivalent to [`Interactivity::on_click`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
fn on_click(mut self, listener: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self
where
Self: Sized,
@ -912,10 +905,10 @@ pub trait StatefulInteractiveElement: InteractiveElement {
/// On drag initiation, this callback will be used to create a new view to render the dragged value for a
/// drag and drop operation. This API should also be used as the equivalent of 'on drag start' with
/// the [`Self::on_drag_move()`] API
/// The fluent API equivalent to [`Interactivity::on_drag()`]
/// the [`Self::on_drag_move`] API
/// The fluent API equivalent to [`Interactivity::on_drag`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
fn on_drag<T, W>(
mut self,
value: T,
@ -932,9 +925,9 @@ pub trait StatefulInteractiveElement: InteractiveElement {
/// Bind the given callback on the hover start and end events of this element. Note that the boolean
/// passed to the callback is true when the hover starts and false when it ends.
/// The fluent API equivalent to [`Interactivity::on_hover()`]
/// The fluent API equivalent to [`Interactivity::on_hover`]
///
/// See [`ViewContext::listener()`] to get access to a view's state from this callback
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
fn on_hover(mut self, listener: impl Fn(&bool, &mut WindowContext) + 'static) -> Self
where
Self: Sized,
@ -944,7 +937,7 @@ pub trait StatefulInteractiveElement: InteractiveElement {
}
/// Use the given callback to construct a new tooltip view when the mouse hovers over this element.
/// The fluent API equivalent to [`Interactivity::tooltip()`]
/// The fluent API equivalent to [`Interactivity::tooltip`]
fn tooltip(mut self, build_tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self
where
Self: Sized,

View file

@ -104,7 +104,7 @@ impl Element for Img {
cx.with_z_index(1, |cx| {
match source {
ImageSource::Uri(uri) => {
let image_future = cx.image_cache.get(uri.clone());
let image_future = cx.image_cache.get(uri.clone(), cx);
if let Some(data) = image_future
.clone()
.now_or_never()

View file

@ -46,6 +46,18 @@ impl IntoElement for &'static str {
}
}
impl IntoElement for String {
type Element = SharedString;
fn element_id(&self) -> Option<ElementId> {
None
}
fn into_element(self) -> Self::Element {
self.into()
}
}
impl Element for SharedString {
type State = TextState;

View file

@ -1,30 +1,64 @@
//! # Welcome to GPUI!
//!
//! GPUI is a hybrid immediate and retained mode, GPU accelerated, UI framework
//! for Rust, designed to support a wide variety of applications. GPUI is currently
//! being actively developed and improved for the [Zed code editor](https://zed.dev/), and new versions
//! will have breaking changes. You'll probably need to use the latest stable version
//! of rust to use GPUI.
//! for Rust, designed to support a wide variety of applications.
//!
//! # Getting started with GPUI
//! ## Getting Started
//!
//! TODO!(docs): Write a code sample showing how to create a window and render a simple
//! div
//! GPUI is still in active development as we work on the Zed code editor and isn't yet on crates.io.
//! You'll also need to use the latest version of stable rust and be on macOS. Add the following to your
//! Cargo.toml:
//!
//! # Drawing interesting things
//! ```
//! gpui = { git = "https://github.com/zed-industries/zed" }
//! ```
//!
//! TODO!(docs): Expand demo to show how to draw a more interesting scene, with
//! a counter to store state and a button to increment it.
//! Everything in GPUI starts with an [`App`]. You can create one with [`App::new`], and
//! kick off your application by passing a callback to [`App::run`]. Inside this callback,
//! you can create a new window with [`AppContext::open_window`], and register your first root
//! view. See [gpui.rs](https://www.gpui.rs/) for a complete example.
//!
//! # Interacting with your application state
//! ## The Big Picture
//!
//! TODO!(docs): Expand demo to show GPUI entity interactions, like subscriptions and entities
//! maybe make a network request to show async stuff?
//! GPUI offers three different [registers](https://en.wikipedia.org/wiki/Register_(sociolinguistics)) depending on your needs:
//!
//! # Conclusion
//! - State management and communication with Models. Whenever you need to store application state
//! that communicates between different parts of your application, you'll want to use GPUI's
//! models. Models are owned by GPUI and are only accessible through an owned smart pointer
//! similar to an [`Rc`]. See the [`app::model_context`] module for more information.
//!
//! TODO!(docs): Wrap up with a conclusion and links to other places? Zed / GPUI website?
//! Discord for chatting about it? Other tutorials or references?
//! - High level, declarative UI with Views. All UI in GPUI starts with a View. A view is simply
//! a model that can be rendered, via the [`Render`] trait. At the start of each frame, GPUI
//! will call this render method on the root view of a given window. Views build a tree of
//! `elements`, lay them out and style them with a tailwind-style API, and then give them to
//! GPUI to turn into pixels. See the [`elements::Div`] element for an all purpose swiss-army
//! knife for UI.
//!
//! - Low level, imperative UI with Elements. Elements are the building blocks of UI in GPUI, and they
//! provide a nice wrapper around an imperative API that provides as much flexibility and control as
//! you need. Elements have total control over how they and their child elements are rendered and and
//! can be used for making efficient views into large lists, implement custom layouting for a code editor,
//! and anything else you can think of. See the [`element`] module for more information.
//!
//! Each of these registers has one or more corresponding contexts that can be accessed from all GPUI services.
//! This context is your main interface to GPUI, and is used extensively throughout the framework.
//!
//! ## Other Resources
//!
//! In addition to the systems above, GPUI provides a range of smaller services that are useful for building
//! complex applications:
//!
//! - Actions are user-defined structs that are used for converting keystrokes into logical operations in your UI.
//! Use this for implementing keyboard shortcuts, such as cmd-q. See the [`action`] module for more information.
//! - Platform services, such as `quit the app` or `open a URL` are available as methods on the [`app::AppContext`].
//! - An async executor that is integrated with the platform's event loop. See the [`executor`] module for more information.,
//! - The [gpui::test] macro provides a convenient way to write tests for your GPUI applications. Tests also have their
//! own kind of context, a [`TestAppContext`] which provides ways of simulating common platform input. See [`app::test_context`]
//! and [`test`] modules for more details.
//!
//! Currently, the best way to learn about these APIs is to read the Zed source code, ask us about it at a fireside hack, or drop
//! a question in the [Zed Discord](https://discord.gg/U4qhCEhMXP). We're working on improving the documentation, creating more examples,
//! and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog).
#![deny(missing_docs)]
#![allow(clippy::type_complexity)]

View file

@ -1,9 +1,6 @@
use crate::{ImageData, ImageId, SharedUrl};
use crate::{AppContext, ImageData, ImageId, SharedUrl, Task};
use collections::HashMap;
use futures::{
future::{BoxFuture, Shared},
AsyncReadExt, FutureExt, TryFutureExt,
};
use futures::{future::Shared, AsyncReadExt, FutureExt, TryFutureExt};
use image::ImageError;
use parking_lot::Mutex;
use std::sync::Arc;
@ -44,10 +41,10 @@ impl From<ImageError> for Error {
pub(crate) struct ImageCache {
client: Arc<dyn HttpClient>,
images: Arc<Mutex<HashMap<SharedUrl, FetchImageFuture>>>,
images: Arc<Mutex<HashMap<SharedUrl, FetchImageTask>>>,
}
type FetchImageFuture = Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>>;
type FetchImageTask = Shared<Task<Result<Arc<ImageData>, Error>>>;
impl ImageCache {
pub fn new(client: Arc<dyn HttpClient>) -> Self {
@ -57,10 +54,7 @@ impl ImageCache {
}
}
pub fn get(
&self,
uri: impl Into<SharedUrl>,
) -> Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>> {
pub fn get(&self, uri: impl Into<SharedUrl>, cx: &AppContext) -> FetchImageTask {
let uri = uri.into();
let mut images = self.images.lock();
@ -68,36 +62,39 @@ impl ImageCache {
Some(future) => future.clone(),
None => {
let client = self.client.clone();
let future = {
let uri = uri.clone();
async move {
let mut response = client.get(uri.as_ref(), ().into(), true).await?;
let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await?;
let future = cx
.background_executor()
.spawn(
{
let uri = uri.clone();
async move {
let mut response =
client.get(uri.as_ref(), ().into(), true).await?;
let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await?;
if !response.status().is_success() {
return Err(Error::BadStatus {
status: response.status(),
body: String::from_utf8_lossy(&body).into_owned(),
});
if !response.status().is_success() {
return Err(Error::BadStatus {
status: response.status(),
body: String::from_utf8_lossy(&body).into_owned(),
});
}
let format = image::guess_format(&body)?;
let image = image::load_from_memory_with_format(&body, format)?
.into_bgra8();
Ok(Arc::new(ImageData::new(image)))
}
}
let format = image::guess_format(&body)?;
let image =
image::load_from_memory_with_format(&body, format)?.into_bgra8();
Ok(Arc::new(ImageData::new(image)))
}
}
.map_err({
let uri = uri.clone();
move |error| {
log::log!(log::Level::Error, "{:?} {:?}", &uri, &error);
error
}
})
.boxed()
.shared();
.map_err({
let uri = uri.clone();
move |error| {
log::log!(log::Level::Error, "{:?} {:?}", &uri, &error);
error
}
}),
)
.shared();
images.insert(uri, future.clone());
future

View file

@ -9,7 +9,7 @@ use crate::{
Action, AnyWindowHandle, AsyncWindowContext, BackgroundExecutor, Bounds, DevicePixels, Font,
FontId, FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, Keymap, LineLayout,
Pixels, PlatformInput, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result,
Scene, SharedString, Size, TaskLabel, WindowContext,
Scene, SharedString, Size, Task, TaskLabel, WindowContext,
};
use anyhow::anyhow;
use async_task::Runnable;
@ -108,9 +108,9 @@ pub(crate) trait Platform: 'static {
fn write_to_clipboard(&self, item: ClipboardItem);
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()>;
fn read_credentials(&self, url: &str) -> Result<Option<(String, Vec<u8>)>>;
fn delete_credentials(&self, url: &str) -> Result<()>;
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
}
/// A handle to a platform's display, e.g. a monitor or laptop screen.

View file

@ -3,7 +3,7 @@ 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, WindowOptions,
PlatformTextSystem, PlatformWindow, Result, SemanticVersion, Task, WindowOptions,
};
use anyhow::anyhow;
use block::ConcreteBlock;
@ -856,104 +856,115 @@ impl Platform for MacPlatform {
}
}
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()> {
let url = CFString::from(url);
let username = CFString::from(username);
let password = CFData::from_buffer(password);
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
let url = url.to_string();
let username = username.to_string();
let password = password.to_vec();
self.background_executor().spawn(async move {
unsafe {
use security::*;
unsafe {
use security::*;
let url = CFString::from(url.as_str());
let username = CFString::from(username.as_str());
let password = CFData::from_buffer(&password);
// First, check if there are already credentials for the given server. If so, then
// update the username and password.
let mut verb = "updating";
let mut query_attrs = CFMutableDictionary::with_capacity(2);
query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
// First, check if there are already credentials for the given server. If so, then
// update the username and password.
let mut verb = "updating";
let mut query_attrs = CFMutableDictionary::with_capacity(2);
query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
let mut attrs = CFMutableDictionary::with_capacity(4);
attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
attrs.set(kSecAttrAccount as *const _, username.as_CFTypeRef());
attrs.set(kSecValueData as *const _, password.as_CFTypeRef());
let mut attrs = CFMutableDictionary::with_capacity(4);
attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
attrs.set(kSecAttrAccount as *const _, username.as_CFTypeRef());
attrs.set(kSecValueData as *const _, password.as_CFTypeRef());
let mut status = SecItemUpdate(
query_attrs.as_concrete_TypeRef(),
attrs.as_concrete_TypeRef(),
);
let mut status = SecItemUpdate(
query_attrs.as_concrete_TypeRef(),
attrs.as_concrete_TypeRef(),
);
// If there were no existing credentials for the given server, then create them.
if status == errSecItemNotFound {
verb = "creating";
status = SecItemAdd(attrs.as_concrete_TypeRef(), ptr::null_mut());
// If there were no existing credentials for the given server, then create them.
if status == errSecItemNotFound {
verb = "creating";
status = SecItemAdd(attrs.as_concrete_TypeRef(), ptr::null_mut());
}
if status != errSecSuccess {
return Err(anyhow!("{} password failed: {}", verb, status));
}
}
if status != errSecSuccess {
return Err(anyhow!("{} password failed: {}", verb, status));
}
}
Ok(())
Ok(())
})
}
fn read_credentials(&self, url: &str) -> Result<Option<(String, Vec<u8>)>> {
let url = CFString::from(url);
let cf_true = CFBoolean::true_value().as_CFTypeRef();
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
let url = url.to_string();
self.background_executor().spawn(async move {
let url = CFString::from(url.as_str());
let cf_true = CFBoolean::true_value().as_CFTypeRef();
unsafe {
use security::*;
unsafe {
use security::*;
// Find any credentials for the given server URL.
let mut attrs = CFMutableDictionary::with_capacity(5);
attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
attrs.set(kSecReturnAttributes as *const _, cf_true);
attrs.set(kSecReturnData as *const _, cf_true);
// Find any credentials for the given server URL.
let mut attrs = CFMutableDictionary::with_capacity(5);
attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
attrs.set(kSecReturnAttributes as *const _, cf_true);
attrs.set(kSecReturnData as *const _, cf_true);
let mut result = CFTypeRef::from(ptr::null());
let status = SecItemCopyMatching(attrs.as_concrete_TypeRef(), &mut result);
match status {
security::errSecSuccess => {}
security::errSecItemNotFound | security::errSecUserCanceled => return Ok(None),
_ => return Err(anyhow!("reading password failed: {}", status)),
let mut result = CFTypeRef::from(ptr::null());
let status = SecItemCopyMatching(attrs.as_concrete_TypeRef(), &mut result);
match status {
security::errSecSuccess => {}
security::errSecItemNotFound | security::errSecUserCanceled => return Ok(None),
_ => return Err(anyhow!("reading password failed: {}", status)),
}
let result = CFType::wrap_under_create_rule(result)
.downcast::<CFDictionary>()
.ok_or_else(|| anyhow!("keychain item was not a dictionary"))?;
let username = result
.find(kSecAttrAccount as *const _)
.ok_or_else(|| anyhow!("account was missing from keychain item"))?;
let username = CFType::wrap_under_get_rule(*username)
.downcast::<CFString>()
.ok_or_else(|| anyhow!("account was not a string"))?;
let password = result
.find(kSecValueData as *const _)
.ok_or_else(|| anyhow!("password was missing from keychain item"))?;
let password = CFType::wrap_under_get_rule(*password)
.downcast::<CFData>()
.ok_or_else(|| anyhow!("password was not a string"))?;
Ok(Some((username.to_string(), password.bytes().to_vec())))
}
let result = CFType::wrap_under_create_rule(result)
.downcast::<CFDictionary>()
.ok_or_else(|| anyhow!("keychain item was not a dictionary"))?;
let username = result
.find(kSecAttrAccount as *const _)
.ok_or_else(|| anyhow!("account was missing from keychain item"))?;
let username = CFType::wrap_under_get_rule(*username)
.downcast::<CFString>()
.ok_or_else(|| anyhow!("account was not a string"))?;
let password = result
.find(kSecValueData as *const _)
.ok_or_else(|| anyhow!("password was missing from keychain item"))?;
let password = CFType::wrap_under_get_rule(*password)
.downcast::<CFData>()
.ok_or_else(|| anyhow!("password was not a string"))?;
Ok(Some((username.to_string(), password.bytes().to_vec())))
}
})
}
fn delete_credentials(&self, url: &str) -> Result<()> {
let url = CFString::from(url);
fn delete_credentials(&self, url: &str) -> Task<Result<()>> {
let url = url.to_string();
unsafe {
use security::*;
self.background_executor().spawn(async move {
unsafe {
use security::*;
let mut query_attrs = CFMutableDictionary::with_capacity(2);
query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
let url = CFString::from(url.as_str());
let mut query_attrs = CFMutableDictionary::with_capacity(2);
query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
let status = SecItemDelete(query_attrs.as_concrete_TypeRef());
let status = SecItemDelete(query_attrs.as_concrete_TypeRef());
if status != errSecSuccess {
return Err(anyhow!("delete password failed: {}", status));
if status != errSecSuccess {
return Err(anyhow!("delete password failed: {}", status));
}
}
}
Ok(())
Ok(())
})
}
}

View file

@ -1,6 +1,7 @@
use crate::{
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor,
Keymap, Platform, PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions,
Keymap, Platform, PlatformDisplay, PlatformTextSystem, Task, TestDisplay, TestWindow,
WindowOptions,
};
use anyhow::{anyhow, Result};
use collections::VecDeque;
@ -280,16 +281,16 @@ impl Platform for TestPlatform {
self.current_clipboard_item.lock().clone()
}
fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Result<()> {
Ok(())
fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task<Result<()>> {
Task::ready(Ok(()))
}
fn read_credentials(&self, _url: &str) -> Result<Option<(String, Vec<u8>)>> {
Ok(None)
fn read_credentials(&self, _url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
Task::ready(Ok(None))
}
fn delete_credentials(&self, _url: &str) -> Result<()> {
Ok(())
fn delete_credentials(&self, _url: &str) -> Task<Result<()>> {
Task::ready(Ok(()))
}
fn double_click_interval(&self) -> std::time::Duration {

View file

@ -4,7 +4,7 @@ use std::{borrow::Borrow, sync::Arc};
use util::arc_cow::ArcCow;
/// A shared string is an immutable string that can be cheaply cloned in GPUI
/// tasks. Essentially an abstraction over an Arc<str> and &'static str,
/// tasks. Essentially an abstraction over an `Arc<str>` and `&'static str`,
#[derive(Deref, DerefMut, Eq, PartialEq, Hash, Clone)]
pub struct SharedString(ArcCow<'static, str>);

View file

@ -41,7 +41,7 @@ pub(crate) const SUBPIXEL_VARIANTS: u8 = 4;
pub struct TextSystem {
line_layout_cache: Arc<LineLayoutCache>,
platform_text_system: Arc<dyn PlatformTextSystem>,
font_ids_by_font: RwLock<FxHashMap<Font, FontId>>,
font_ids_by_font: RwLock<FxHashMap<Font, Result<FontId>>>,
font_metrics: RwLock<FxHashMap<FontId, FontMetrics>>,
raster_bounds: RwLock<FxHashMap<RenderGlyphParams, Bounds<DevicePixels>>>,
wrapper_pool: Mutex<FxHashMap<FontIdWithSize, Vec<LineWrapper>>>,
@ -91,13 +91,26 @@ impl TextSystem {
/// Get the FontId for the configure font family and style.
pub fn font_id(&self, font: &Font) -> Result<FontId> {
let font_id = self.font_ids_by_font.read().get(font).copied();
fn clone_font_id_result(font_id: &Result<FontId>) -> Result<FontId> {
match font_id {
Ok(font_id) => Ok(*font_id),
Err(err) => Err(anyhow!("{}", err)),
}
}
let font_id = self
.font_ids_by_font
.read()
.get(font)
.map(clone_font_id_result);
if let Some(font_id) = font_id {
Ok(font_id)
font_id
} else {
let font_id = self.platform_text_system.font_id(font)?;
self.font_ids_by_font.write().insert(font.clone(), font_id);
Ok(font_id)
let font_id = self.platform_text_system.font_id(font);
self.font_ids_by_font
.write()
.insert(font.clone(), clone_font_id_result(&font_id));
font_id
}
}

View file

@ -94,6 +94,7 @@ type AnyObserver = Box<dyn FnMut(&mut WindowContext) -> bool + 'static>;
type AnyWindowFocusListener = Box<dyn FnMut(&FocusEvent, &mut WindowContext) -> bool + 'static>;
#[derive(Debug)]
struct FocusEvent {
previous_focus_path: SmallVec<[FocusId; 8]>,
current_focus_path: SmallVec<[FocusId; 8]>,
@ -105,7 +106,7 @@ slotmap::new_key_type! {
}
thread_local! {
pub(crate) static ELEMENT_ARENA: RefCell<Arena> = RefCell::new(Arena::new(4 * 1024 * 1024));
pub(crate) static ELEMENT_ARENA: RefCell<Arena> = RefCell::new(Arena::new(8 * 1024 * 1024));
}
impl FocusId {
@ -1030,7 +1031,13 @@ impl<'a> WindowContext<'a> {
self.window
.next_frame
.finish(&mut self.window.rendered_frame);
ELEMENT_ARENA.with_borrow_mut(|element_arena| element_arena.clear());
ELEMENT_ARENA.with_borrow_mut(|element_arena| {
let percentage = (element_arena.len() as f32 / element_arena.capacity() as f32) * 100.;
if percentage >= 80. {
log::warn!("elevated element arena occupation: {}.", percentage);
}
element_arena.clear();
});
let previous_focus_path = self.window.rendered_frame.focus_path();
let previous_window_active = self.window.rendered_frame.window_active;
@ -2015,11 +2022,12 @@ impl<'a, V: 'static> ViewContext<'a, V> {
}
}
// Always emit a notify effect, so that handlers fire correctly
self.window_cx.app.push_effect(Effect::Notify {
emitter: self.view.model.entity_id,
});
if !self.window.drawing {
self.window_cx.window.dirty = true;
self.window_cx.app.push_effect(Effect::Notify {
emitter: self.view.model.entity_id,
});
}
}
@ -2669,7 +2677,7 @@ impl From<(&'static str, u64)> for ElementId {
}
/// A rectangle to be rendered in the window at the given position and size.
/// Passed as an argument [`WindowContext::paint_quad`].
/// Passed as an argument [`ElementContext::paint_quad`].
#[derive(Clone)]
pub struct PaintQuad {
bounds: Bounds<Pixels>,
@ -2751,3 +2759,59 @@ pub fn outline(bounds: impl Into<Bounds<Pixels>>, border_color: impl Into<Hsla>)
border_color: border_color.into(),
}
}
#[cfg(test)]
mod test {
use std::{cell::RefCell, rc::Rc};
use crate::{
self as gpui, div, FocusHandle, InteractiveElement, IntoElement, Render, TestAppContext,
ViewContext, VisualContext,
};
#[gpui::test]
fn test_notify_on_focus(cx: &mut TestAppContext) {
struct TestFocusView {
handle: FocusHandle,
}
impl Render for TestFocusView {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
div().id("test").track_focus(&self.handle)
}
}
let notify_counter = Rc::new(RefCell::new(0));
let (notify_producer, cx) = cx.add_window_view(|cx| {
cx.activate_window();
let handle = cx.focus_handle();
cx.on_focus(&handle, |_, cx| {
cx.notify();
})
.detach();
TestFocusView { handle }
});
let focus_handle = cx.update(|cx| notify_producer.read(cx).handle.clone());
let _notify_consumer = cx.new_view({
|cx| {
let notify_counter = notify_counter.clone();
cx.observe(&notify_producer, move |_, _, _| {
*notify_counter.borrow_mut() += 1;
})
.detach();
}
});
cx.update(|cx| {
cx.focus(&focus_handle);
});
assert_eq!(*notify_counter.borrow(), 1);
}
}

View file

@ -656,7 +656,7 @@ impl<'a> ElementContext<'a> {
/// Paint one or more quads into the scene for the next frame at the current stacking context.
/// Quads are colored rectangular regions with an optional background, border, and corner radius.
/// see [`fill`], [`outline`], and [`quad`] to construct this type.
/// see [`fill`](crate::fill), [`outline`](crate::outline), and [`quad`](crate::quad) to construct this type.
pub fn paint_quad(&mut self, quad: PaintQuad) {
let scale_factor = self.scale_factor();
let content_mask = self.content_mask();
@ -731,9 +731,11 @@ impl<'a> ElementContext<'a> {
);
}
/// Paint a monochrome (non-emoji) glyph into the scene for the next frame at the current z-index.
/// Paints a monochrome (non-emoji) glyph into the scene for the next frame at the current z-index.
///
/// The y component of the origin is the baseline of the glyph.
/// You should generally prefer to use the [`ShapedLine::paint`] or [`WrappedLine::paint`] methods in the [`text_system`].
/// You should generally prefer to use the [`ShapedLine::paint`](crate::ShapedLine::paint) or
/// [`WrappedLine::paint`](crate::WrappedLine::paint) methods in the [`TextSystem`](crate::TextSystem).
/// This method is only useful if you need to paint a single glyph that has already been shaped.
pub fn paint_glyph(
&mut self,
@ -790,9 +792,11 @@ impl<'a> ElementContext<'a> {
Ok(())
}
/// Paint an emoji glyph into the scene for the next frame at the current z-index.
/// Paints an emoji glyph into the scene for the next frame at the current z-index.
///
/// The y component of the origin is the baseline of the glyph.
/// You should generally prefer to use the [`ShapedLine::paint`] or [`WrappedLine::paint`] methods in the [`text_system`].
/// You should generally prefer to use the [`ShapedLine::paint`](crate::ShapedLine::paint) or
/// [`WrappedLine::paint`](crate::WrappedLine::paint) methods in the [`TextSystem`](crate::TextSystem).
/// This method is only useful if you need to paint a single emoji that has already been shaped.
pub fn paint_emoji(
&mut self,

View file

@ -90,13 +90,12 @@ pub fn init(
.detach();
cx.spawn(move |cx| async move {
let embedding_provider =
OpenAIEmbeddingProvider::new(http_client, cx.background_executor().clone()).await;
let semantic_index = SemanticIndex::new(
fs,
db_file_path,
Arc::new(OpenAIEmbeddingProvider::new(
http_client,
cx.background_executor().clone(),
)),
Arc::new(embedding_provider),
language_registry,
cx.clone(),
)
@ -279,14 +278,22 @@ impl SemanticIndex {
.map(|semantic_index| semantic_index.clone())
}
pub fn authenticate(&mut self, cx: &mut AppContext) -> bool {
pub fn authenticate(&mut self, cx: &mut AppContext) -> Task<bool> {
if !self.embedding_provider.has_credentials() {
self.embedding_provider.retrieve_credentials(cx);
} else {
return true;
}
let embedding_provider = self.embedding_provider.clone();
cx.spawn(|cx| async move {
if let Some(retrieve_credentials) = cx
.update(|cx| embedding_provider.retrieve_credentials(cx))
.log_err()
{
retrieve_credentials.await;
}
self.embedding_provider.has_credentials()
embedding_provider.has_credentials()
})
} else {
Task::ready(true)
}
}
pub fn is_authenticated(&self) -> bool {
@ -1006,12 +1013,26 @@ impl SemanticIndex {
project: Model<Project>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if !self.is_authenticated() {
if !self.authenticate(cx) {
return Task::ready(Err(anyhow!("user is not authenticated")));
}
if self.is_authenticated() {
self.index_project_internal(project, cx)
} else {
let authenticate = self.authenticate(cx);
cx.spawn(|this, mut cx| async move {
if authenticate.await {
this.update(&mut cx, |this, cx| this.index_project_internal(project, cx))?
.await
} else {
Err(anyhow!("user is not authenticated"))
}
})
}
}
fn index_project_internal(
&mut self,
project: Model<Project>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if !self.projects.contains_key(&project.downgrade()) {
let subscription = cx.subscribe(&project, |this, project, event, cx| match event {
project::Event::WorktreeAdded | project::Event::WorktreeRemoved(_) => {

View file

@ -1,4 +1,4 @@
use gpui::{px, rgb, Div, Hsla, IntoElement, Render, RenderOnce};
use gpui::{px, rgb, Div, IntoElement, Render, RenderOnce};
use story::Story;
use ui::prelude::*;
@ -51,22 +51,22 @@ trait Styles: Styled + Sized {
self.absolute()
.w(px(150.))
.h(px(50.))
.text_color(rgb::<Hsla>(0x000000))
.text_color(rgb(0x000000))
}
fn blue(self) -> Self {
self.bg(rgb::<Hsla>(0xe5e8fc))
self.bg(rgb(0xe5e8fc))
.border_5()
.border_color(rgb::<Hsla>(0x112382))
.border_color(rgb(0x112382))
.line_height(px(55.))
// HACK: Simulate `text-align: center`.
.pl(px(24.))
}
fn red(self) -> Self {
self.bg(rgb::<Hsla>(0xfce5e7))
self.bg(rgb(0xfce5e7))
.border_5()
.border_color(rgb::<Hsla>(0xe3a1a7))
.border_color(rgb(0xe3a1a7))
// HACK: Simulate `text-align: center`.
.pl(px(8.))
}
@ -92,10 +92,10 @@ impl RenderOnce for ZIndexExample {
.left(px(15.))
.w(px(180.))
.h(px(230.))
.bg(rgb::<Hsla>(0xfcfbe5))
.text_color(rgb::<Hsla>(0x000000))
.bg(rgb(0xfcfbe5))
.text_color(rgb(0x000000))
.border_5()
.border_color(rgb::<Hsla>(0xe3e0a1))
.border_color(rgb(0xe3e0a1))
.line_height(px(215.))
// HACK: Simulate `text-align: center`.
.pl(px(24.))

View file

@ -261,7 +261,7 @@ mod tests {
fn override_a_single_theme_color() {
let mut colors = ThemeColors::light();
let magenta: Hsla = gpui::rgb(0xff00ff);
let magenta: Hsla = gpui::rgb(0xff00ff).into();
assert_ne!(colors.text, magenta);
@ -279,8 +279,8 @@ mod tests {
fn override_multiple_theme_colors() {
let mut colors = ThemeColors::light();
let magenta: Hsla = gpui::rgb(0xff00ff);
let green: Hsla = gpui::rgb(0x00ff00);
let magenta: Hsla = gpui::rgb(0xff00ff).into();
let green: Hsla = gpui::rgb(0x00ff00).into();
assert_ne!(colors.text, magenta);
assert_ne!(colors.background, green);
@ -305,7 +305,7 @@ mod tests {
}))
.unwrap();
assert_eq!(colors.background, Some(gpui::rgb(0xff00ff)));
assert_eq!(colors.text, Some(gpui::rgb(0xff0000)));
assert_eq!(colors.background, Some(gpui::rgb(0xff00ff).into()));
assert_eq!(colors.text, Some(gpui::rgb(0xff0000).into()));
}
}

View file

@ -342,7 +342,16 @@ impl Pane {
}
pub fn has_focus(&self, cx: &WindowContext) -> bool {
// We not only check whether our focus handle contains focus, but also
// whether the active_item might have focus, because we might have just activated an item
// but that hasn't rendered yet.
// So before the next render, we might have transferred focus
// to the item and `focus_handle.contains_focus` returns false because the `active_item`
// is not hooked up to us in the dispatch tree.
self.focus_handle.contains_focused(cx)
|| self
.active_item()
.map_or(false, |item| item.focus_handle(cx).contains_focused(cx))
}
fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
@ -1470,7 +1479,7 @@ impl Pane {
),
)
})
.when(self.was_focused || self.has_focus(cx), |tab_bar| {
.when(self.has_focus(cx), |tab_bar| {
tab_bar.end_child({
let render_tab_buttons = self.render_tab_bar_buttons.clone();
render_tab_buttons(self, cx)

View file

@ -233,24 +233,28 @@ impl SerializedPane {
workspace: WeakView<Workspace>,
cx: &mut AsyncWindowContext,
) -> Result<Vec<Option<Box<dyn ItemHandle>>>> {
let mut items = Vec::new();
let mut item_tasks = Vec::new();
let mut active_item_index = None;
for (index, item) in self.children.iter().enumerate() {
let project = project.clone();
let item_handle = pane
.update(cx, |_, cx| {
if let Some(deserializer) = cx.global::<ItemDeserializers>().get(&item.kind) {
deserializer(project, workspace.clone(), workspace_id, item.item_id, cx)
} else {
Task::ready(Err(anyhow::anyhow!(
"Deserializer does not exist for item kind: {}",
item.kind
)))
}
})?
.await
.log_err();
item_tasks.push(pane.update(cx, |_, cx| {
if let Some(deserializer) = cx.global::<ItemDeserializers>().get(&item.kind) {
deserializer(project, workspace.clone(), workspace_id, item.item_id, cx)
} else {
Task::ready(Err(anyhow::anyhow!(
"Deserializer does not exist for item kind: {}",
item.kind
)))
}
})?);
if item.active {
active_item_index = Some(index);
}
}
let mut items = Vec::new();
for item_handle in futures::future::join_all(item_tasks).await {
let item_handle = item_handle.log_err();
items.push(item_handle.clone());
if let Some(item_handle) = item_handle {
@ -258,10 +262,6 @@ impl SerializedPane {
pane.add_item(item_handle.clone(), true, true, None, cx);
})?;
}
if item.active {
active_item_index = Some(index);
}
}
if let Some(active_item_index) = active_item_index {

View file

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

View file

@ -376,7 +376,7 @@ async fn authenticate(client: Arc<Client>, cx: &AsyncAppContext) -> Result<()> {
if client::IMPERSONATE_LOGIN.is_some() {
client.authenticate_and_connect(false, &cx).await?;
}
} else if client.has_keychain_credentials(&cx) {
} else if client.has_keychain_credentials(&cx).await {
client.authenticate_and_connect(true, &cx).await?;
}
Ok::<_, anyhow::Error>(())

View file

@ -2705,11 +2705,6 @@ mod tests {
.unwrap()
.to_vec()
.into(),
Assets
.load("fonts/plex/IBMPlexSans-Regular.ttf")
.unwrap()
.to_vec()
.into(),
])
.unwrap();
let themes = ThemeRegistry::default();

View file

@ -2,7 +2,7 @@
## macOS
Supported versions: Catalina (10.15) - Ventura (13.x).
Supported versions: Catalina (10.15) - Sonoma (14.x).
{% hint style="info" %}
The implementation of our screen sharing feature makes use of [LiveKit](https://livekit.io). The LiveKit SDK requires macOS Catalina (10.15); consequently, in v0.62.4, we dropped support for earlier macOS versions that we were initially supporting.