Merge branch 'main' into z-index-with-flicker-fix

This commit is contained in:
Mikayla 2024-01-21 21:14:22 -08:00
commit 86b363d7f2
No known key found for this signature in database
129 changed files with 4283 additions and 2313 deletions

3
Cargo.lock generated
View file

@ -1548,6 +1548,7 @@ dependencies = [
"log",
"menu",
"notifications",
"parking_lot 0.11.2",
"picker",
"postage",
"pretty_assertions",
@ -6144,6 +6145,7 @@ dependencies = [
"smol",
"sum_tree",
"theme",
"ui",
"util",
]
@ -9079,6 +9081,7 @@ dependencies = [
"nvim-rs",
"parking_lot 0.11.2",
"project",
"regex",
"search",
"serde",
"serde_derive",

View file

@ -183,6 +183,7 @@
"context": "Editor && mode == auto_height",
"bindings": {
"ctrl-enter": "editor::Newline",
"shift-enter": "editor::Newline",
"ctrl-shift-enter": "editor::NewlineBelow"
}
},

View file

@ -104,8 +104,6 @@
"shift-v": "vim::ToggleVisualLine",
"ctrl-v": "vim::ToggleVisualBlock",
"ctrl-q": "vim::ToggleVisualBlock",
"*": "vim::MoveToNext",
"#": "vim::MoveToPrev",
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
"ctrl-f": "vim::PageDown",
"pagedown": "vim::PageDown",
@ -329,6 +327,8 @@
"backwards": true
}
],
"*": "vim::MoveToNext",
"#": "vim::MoveToPrev",
";": "vim::RepeatFind",
",": [
"vim::RepeatFind",
@ -421,6 +421,18 @@
"shift-r": "vim::SubstituteLine",
"c": "vim::Substitute",
"~": "vim::ChangeCase",
"*": [
"vim::MoveToNext",
{
"partialWord": true
}
],
"#": [
"vim::MoveToPrev",
{
"partialWord": true
}
],
"ctrl-a": "vim::Increment",
"ctrl-x": "vim::Decrement",
"g ctrl-a": [

View file

@ -72,6 +72,9 @@
// Whether to use additional LSP queries to format (and amend) the code after
// every "trigger" symbol input, defined by LSP server capabilities.
"use_on_type_format": true,
// Whether to automatically type closing characters for you. For example,
// when you type (, Zed will automatically add a closing ) at the correct position.
"use_autoclose": true,
// Controls whether copilot provides suggestion immediately
// or waits for a `copilot::Toggle`
"show_copilot_suggestions": true,

View file

@ -134,7 +134,7 @@ pub async fn stream_completion(
line: Result<String, io::Error>,
) -> Result<Option<OpenAIResponseStreamEvent>> {
if let Some(data) = line?.strip_prefix("data: ") {
let event = serde_json::from_str(&data)?;
let event = serde_json::from_str(data)?;
Ok(Some(event))
} else {
Ok(None)

View file

@ -1432,7 +1432,7 @@ impl Room {
let display = displays
.first()
.ok_or_else(|| anyhow!("no display found"))?;
let track = LocalVideoTrack::screen_share_for_display(&display);
let track = LocalVideoTrack::screen_share_for_display(display);
this.upgrade()
.ok_or_else(|| anyhow!("room was dropped"))?
.update(&mut cx, |this, _| {

View file

@ -86,11 +86,11 @@ pub struct ChannelPathsInsertGuard<'a> {
impl<'a> ChannelPathsInsertGuard<'a> {
pub fn note_changed(&mut self, channel_id: ChannelId, epoch: u64, version: &clock::Global) {
insert_note_changed(&mut self.channels_by_id, channel_id, epoch, &version);
insert_note_changed(self.channels_by_id, channel_id, epoch, version);
}
pub fn new_messages(&mut self, channel_id: ChannelId, message_id: u64) {
insert_new_message(&mut self.channels_by_id, channel_id, message_id)
insert_new_message(self.channels_by_id, channel_id, message_id)
}
pub fn insert(&mut self, channel_proto: proto::Channel) -> bool {
@ -131,8 +131,8 @@ impl<'a> ChannelPathsInsertGuard<'a> {
impl<'a> Drop for ChannelPathsInsertGuard<'a> {
fn drop(&mut self) {
self.channels_ordered.sort_by(|a, b| {
let a = channel_path_sorting_key(*a, &self.channels_by_id);
let b = channel_path_sorting_key(*b, &self.channels_by_id);
let a = channel_path_sorting_key(*a, self.channels_by_id);
let b = channel_path_sorting_key(*b, self.channels_by_id);
a.cmp(b)
});
self.channels_ordered.dedup();
@ -167,7 +167,7 @@ fn insert_note_changed(
if epoch > unseen_version.0 {
*unseen_version = (epoch, version.clone());
} else {
unseen_version.1.join(&version);
unseen_version.1.join(version);
}
}
}

View file

@ -1310,7 +1310,7 @@ impl Client {
drop(state);
if let Some(handler) = handler {
let future = handler(subscriber, message, &self, cx.clone());
let future = handler(subscriber, message, self, cx.clone());
let client_id = self.id();
log::debug!(
"rpc message received. client_id:{}, sender_id:{:?}, type:{}",

View file

@ -38,8 +38,9 @@ struct TelemetryState {
flush_events_task: Option<Task<()>>,
log_file: Option<NamedTempFile>,
is_staff: Option<bool>,
first_event_datetime: Option<DateTime<Utc>>,
first_event_date_time: Option<DateTime<Utc>>,
event_coalescer: EventCoalescer,
max_queue_size: usize,
}
const EVENTS_URL_PATH: &'static str = "/api/events";
@ -69,14 +70,14 @@ struct EventWrapper {
event: Event,
}
#[derive(Serialize, Debug)]
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AssistantKind {
Panel,
Inline,
}
#[derive(Serialize, Debug)]
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(tag = "type")]
pub enum Event {
Editor {
@ -168,8 +169,9 @@ impl Telemetry {
flush_events_task: None,
log_file: None,
is_staff: None,
first_event_datetime: None,
first_event_date_time: None,
event_coalescer: EventCoalescer::new(),
max_queue_size: MAX_QUEUE_LEN,
}));
#[cfg(not(debug_assertions))]
@ -310,7 +312,7 @@ impl Telemetry {
operation,
copilot_enabled,
copilot_enabled_for_language,
milliseconds_since_first_event: self.milliseconds_since_first_event(),
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
};
self.report_event(event)
@ -326,7 +328,7 @@ impl Telemetry {
suggestion_id,
suggestion_accepted,
file_extension,
milliseconds_since_first_event: self.milliseconds_since_first_event(),
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
};
self.report_event(event)
@ -342,7 +344,7 @@ impl Telemetry {
conversation_id,
kind,
model,
milliseconds_since_first_event: self.milliseconds_since_first_event(),
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
};
self.report_event(event)
@ -358,7 +360,7 @@ impl Telemetry {
operation,
room_id,
channel_id,
milliseconds_since_first_event: self.milliseconds_since_first_event(),
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
};
self.report_event(event)
@ -368,7 +370,7 @@ impl Telemetry {
let event = Event::Cpu {
usage_as_percentage,
core_count,
milliseconds_since_first_event: self.milliseconds_since_first_event(),
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
};
self.report_event(event)
@ -382,26 +384,36 @@ impl Telemetry {
let event = Event::Memory {
memory_in_bytes,
virtual_memory_in_bytes,
milliseconds_since_first_event: self.milliseconds_since_first_event(),
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
};
self.report_event(event)
}
pub fn report_app_event(self: &Arc<Self>, operation: String) {
self.report_app_event_with_date_time(operation, Utc::now());
}
fn report_app_event_with_date_time(
self: &Arc<Self>,
operation: String,
date_time: DateTime<Utc>,
) -> Event {
let event = Event::App {
operation,
milliseconds_since_first_event: self.milliseconds_since_first_event(),
milliseconds_since_first_event: self.milliseconds_since_first_event(date_time),
};
self.report_event(event)
self.report_event(event.clone());
event
}
pub fn report_setting_event(self: &Arc<Self>, setting: &'static str, value: String) {
let event = Event::Setting {
setting,
value,
milliseconds_since_first_event: self.milliseconds_since_first_event(),
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
};
self.report_event(event)
@ -416,7 +428,7 @@ impl Telemetry {
let event = Event::Edit {
duration: end.timestamp_millis() - start.timestamp_millis(),
environment,
milliseconds_since_first_event: self.milliseconds_since_first_event(),
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
};
self.report_event(event);
@ -427,22 +439,21 @@ impl Telemetry {
let event = Event::Action {
source,
action,
milliseconds_since_first_event: self.milliseconds_since_first_event(),
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
};
self.report_event(event)
}
fn milliseconds_since_first_event(&self) -> i64 {
fn milliseconds_since_first_event(self: &Arc<Self>, date_time: DateTime<Utc>) -> i64 {
let mut state = self.state.lock();
match state.first_event_datetime {
Some(first_event_datetime) => {
let now: DateTime<Utc> = Utc::now();
now.timestamp_millis() - first_event_datetime.timestamp_millis()
match state.first_event_date_time {
Some(first_event_date_time) => {
date_time.timestamp_millis() - first_event_date_time.timestamp_millis()
}
None => {
state.first_event_datetime = Some(Utc::now());
state.first_event_date_time = Some(date_time);
0
}
}
@ -468,7 +479,7 @@ impl Telemetry {
state.events_queue.push(EventWrapper { signed_in, event });
if state.installation_id.is_some() {
if state.events_queue.len() >= MAX_QUEUE_LEN {
if state.events_queue.len() >= state.max_queue_size {
drop(state);
self.flush_events();
}
@ -489,7 +500,7 @@ impl Telemetry {
pub fn flush_events(self: &Arc<Self>) {
let mut state = self.state.lock();
state.first_event_datetime = None;
state.first_event_date_time = None;
let mut events = mem::take(&mut state.events_queue);
state.flush_events_task.take();
drop(state);
@ -548,3 +559,159 @@ impl Telemetry {
.detach();
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
use gpui::TestAppContext;
use util::http::FakeHttpClient;
#[gpui::test]
fn test_telemetry_flush_on_max_queue_size(cx: &mut TestAppContext) {
init_test(cx);
let http = FakeHttpClient::with_200_response();
let installation_id = Some("installation_id".to_string());
let session_id = "session_id".to_string();
cx.update(|cx| {
let telemetry = Telemetry::new(http, cx);
telemetry.state.lock().max_queue_size = 4;
telemetry.start(installation_id, session_id, cx);
assert!(is_empty_state(&telemetry));
let first_date_time = Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap();
let operation = "test".to_string();
let event =
telemetry.report_app_event_with_date_time(operation.clone(), first_date_time);
assert_eq!(
event,
Event::App {
operation: operation.clone(),
milliseconds_since_first_event: 0
}
);
assert_eq!(telemetry.state.lock().events_queue.len(), 1);
assert!(telemetry.state.lock().flush_events_task.is_some());
assert_eq!(
telemetry.state.lock().first_event_date_time,
Some(first_date_time)
);
let mut date_time = first_date_time + chrono::Duration::milliseconds(100);
let event = telemetry.report_app_event_with_date_time(operation.clone(), date_time);
assert_eq!(
event,
Event::App {
operation: operation.clone(),
milliseconds_since_first_event: 100
}
);
assert_eq!(telemetry.state.lock().events_queue.len(), 2);
assert!(telemetry.state.lock().flush_events_task.is_some());
assert_eq!(
telemetry.state.lock().first_event_date_time,
Some(first_date_time)
);
date_time += chrono::Duration::milliseconds(100);
let event = telemetry.report_app_event_with_date_time(operation.clone(), date_time);
assert_eq!(
event,
Event::App {
operation: operation.clone(),
milliseconds_since_first_event: 200
}
);
assert_eq!(telemetry.state.lock().events_queue.len(), 3);
assert!(telemetry.state.lock().flush_events_task.is_some());
assert_eq!(
telemetry.state.lock().first_event_date_time,
Some(first_date_time)
);
date_time += chrono::Duration::milliseconds(100);
// Adding a 4th event should cause a flush
let event = telemetry.report_app_event_with_date_time(operation.clone(), date_time);
assert_eq!(
event,
Event::App {
operation: operation.clone(),
milliseconds_since_first_event: 300
}
);
assert!(is_empty_state(&telemetry));
});
}
#[gpui::test]
async fn test_connection_timeout(executor: BackgroundExecutor, cx: &mut TestAppContext) {
init_test(cx);
let http = FakeHttpClient::with_200_response();
let installation_id = Some("installation_id".to_string());
let session_id = "session_id".to_string();
cx.update(|cx| {
let telemetry = Telemetry::new(http, cx);
telemetry.state.lock().max_queue_size = 4;
telemetry.start(installation_id, session_id, cx);
assert!(is_empty_state(&telemetry));
let first_date_time = Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap();
let operation = "test".to_string();
let event =
telemetry.report_app_event_with_date_time(operation.clone(), first_date_time);
assert_eq!(
event,
Event::App {
operation: operation.clone(),
milliseconds_since_first_event: 0
}
);
assert_eq!(telemetry.state.lock().events_queue.len(), 1);
assert!(telemetry.state.lock().flush_events_task.is_some());
assert_eq!(
telemetry.state.lock().first_event_date_time,
Some(first_date_time)
);
let duration = Duration::from_millis(1);
// Test 1 millisecond before the flush interval limit is met
executor.advance_clock(FLUSH_INTERVAL - duration);
assert!(!is_empty_state(&telemetry));
// Test the exact moment the flush interval limit is met
executor.advance_clock(duration);
assert!(is_empty_state(&telemetry));
});
}
// TODO:
// Test settings
// Update FakeHTTPClient to keep track of the number of requests and assert on it
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
});
}
fn is_empty_state(telemetry: &Telemetry) -> bool {
telemetry.state.lock().events_queue.is_empty()
&& telemetry.state.lock().flush_events_task.is_none()
&& telemetry.state.lock().first_event_date_time.is_none()
}
}

View file

@ -60,6 +60,7 @@ anyhow.workspace = true
futures.workspace = true
lazy_static.workspace = true
log.workspace = true
parking_lot.workspace = true
schemars.workspace = true
postage.workspace = true
serde.workspace = true

View file

@ -18,7 +18,7 @@ use project::Fs;
use rich_text::RichText;
use serde::{Deserialize, Serialize};
use settings::Settings;
use std::sync::Arc;
use std::{sync::Arc, time::Duration};
use time::{OffsetDateTime, UtcOffset};
use ui::{
popover_menu, prelude::*, Avatar, Button, ContextMenu, IconButton, IconName, KeyBinding, Label,
@ -304,8 +304,11 @@ impl ChatPanel {
let last_message = active_chat.message(ix.saturating_sub(1));
let this_message = active_chat.message(ix).clone();
let is_continuation_from_previous = last_message.id != this_message.id
&& last_message.sender.id == this_message.sender.id;
let duration_since_last_message = this_message.timestamp - last_message.timestamp;
let is_continuation_from_previous = last_message.sender.id
== this_message.sender.id
&& last_message.id != this_message.id
&& duration_since_last_message < Duration::from_secs(5 * 60);
if let ChannelMessageId::Saved(id) = this_message.id {
if this_message
@ -325,8 +328,6 @@ impl ChatPanel {
Self::render_markdown_with_mentions(&self.languages, self.client.id(), &message)
});
let now = OffsetDateTime::now_utc();
let belongs_to_user = Some(message.sender.id) == self.client.user_id();
let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
(message.id, belongs_to_user || is_admin)
@ -349,23 +350,21 @@ impl ChatPanel {
.when(!is_continuation_from_previous, |this| {
this.pt_3().child(
h_flex()
.child(
div().absolute().child(
Avatar::new(message.sender.avatar_uri.clone())
.size(cx.rem_size() * 1.5),
),
)
.text_ui_sm()
.child(div().absolute().child(
Avatar::new(message.sender.avatar_uri.clone()).size(cx.rem_size()),
))
.child(
div()
.pl(cx.rem_size() * 1.5 + px(6.0))
.pl(cx.rem_size() + px(6.0))
.pr(px(8.0))
.font_weight(FontWeight::BOLD)
.child(Label::new(message.sender.github_login.clone())),
)
.child(
Label::new(format_timestamp(
OffsetDateTime::now_utc(),
message.timestamp,
now,
self.local_timezone,
))
.size(LabelSize::Small)
@ -597,7 +596,7 @@ impl Render for ChatPanel {
el.child(
div()
.rounded_md()
.h_7()
.h_6()
.w_full()
.bg(cx.theme().colors().editor_background),
)
@ -671,28 +670,44 @@ impl Panel for ChatPanel {
impl EventEmitter<PanelEvent> for ChatPanel {}
fn format_timestamp(
mut timestamp: OffsetDateTime,
mut now: OffsetDateTime,
local_timezone: UtcOffset,
reference: OffsetDateTime,
timestamp: OffsetDateTime,
timezone: UtcOffset,
) -> String {
timestamp = timestamp.to_offset(local_timezone);
now = now.to_offset(local_timezone);
let timestamp_local = timestamp.to_offset(timezone);
let timestamp_local_hour = timestamp_local.hour();
let today = now.date();
let date = timestamp.date();
let mut hour = timestamp.hour();
let mut part = "am";
if hour > 12 {
hour -= 12;
part = "pm";
}
if date == today {
format!("{:02}:{:02}{}", hour, timestamp.minute(), part)
} else if date.next_day() == Some(today) {
format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part)
let hour_12 = match timestamp_local_hour {
0 => 12, // Midnight
13..=23 => timestamp_local_hour - 12, // PM hours
_ => timestamp_local_hour, // AM hours
};
let meridiem = if timestamp_local_hour >= 12 {
"pm"
} else {
format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
"am"
};
let timestamp_local_minute = timestamp_local.minute();
let formatted_time = format!("{:02}:{:02} {}", hour_12, timestamp_local_minute, meridiem);
let reference_local = reference.to_offset(timezone);
let reference_local_date = reference_local.date();
let timestamp_local_date = timestamp_local.date();
if timestamp_local_date == reference_local_date {
return formatted_time;
}
if reference_local_date.previous_day() == Some(timestamp_local_date) {
return format!("yesterday at {}", formatted_time);
}
format!(
"{:02}/{:02}/{}",
timestamp_local_date.month() as u32,
timestamp_local_date.day(),
timestamp_local_date.year()
)
}
#[cfg(test)]
@ -701,6 +716,7 @@ mod tests {
use gpui::HighlightStyle;
use pretty_assertions::assert_eq;
use rich_text::Highlight;
use time::{Date, OffsetDateTime, Time, UtcOffset};
use util::test::marked_text_ranges;
#[gpui::test]
@ -749,4 +765,99 @@ mod tests {
]
);
}
#[test]
fn test_format_today() {
let reference = create_offset_datetime(1990, 4, 12, 16, 45, 0);
let timestamp = create_offset_datetime(1990, 4, 12, 15, 30, 0);
assert_eq!(
format_timestamp(reference, timestamp, test_timezone()),
"03:30 pm"
);
}
#[test]
fn test_format_yesterday() {
let reference = create_offset_datetime(1990, 4, 12, 10, 30, 0);
let timestamp = create_offset_datetime(1990, 4, 11, 9, 0, 0);
assert_eq!(
format_timestamp(reference, timestamp, test_timezone()),
"yesterday at 09:00 am"
);
}
#[test]
fn test_format_yesterday_less_than_24_hours_ago() {
let reference = create_offset_datetime(1990, 4, 12, 19, 59, 0);
let timestamp = create_offset_datetime(1990, 4, 11, 20, 0, 0);
assert_eq!(
format_timestamp(reference, timestamp, test_timezone()),
"yesterday at 08:00 pm"
);
}
#[test]
fn test_format_yesterday_more_than_24_hours_ago() {
let reference = create_offset_datetime(1990, 4, 12, 19, 59, 0);
let timestamp = create_offset_datetime(1990, 4, 11, 18, 0, 0);
assert_eq!(
format_timestamp(reference, timestamp, test_timezone()),
"yesterday at 06:00 pm"
);
}
#[test]
fn test_format_yesterday_over_midnight() {
let reference = create_offset_datetime(1990, 4, 12, 0, 5, 0);
let timestamp = create_offset_datetime(1990, 4, 11, 23, 55, 0);
assert_eq!(
format_timestamp(reference, timestamp, test_timezone()),
"yesterday at 11:55 pm"
);
}
#[test]
fn test_format_yesterday_over_month() {
let reference = create_offset_datetime(1990, 4, 2, 9, 0, 0);
let timestamp = create_offset_datetime(1990, 4, 1, 20, 0, 0);
assert_eq!(
format_timestamp(reference, timestamp, test_timezone()),
"yesterday at 08:00 pm"
);
}
#[test]
fn test_format_before_yesterday() {
let reference = create_offset_datetime(1990, 4, 12, 10, 30, 0);
let timestamp = create_offset_datetime(1990, 4, 10, 20, 20, 0);
assert_eq!(
format_timestamp(reference, timestamp, test_timezone()),
"04/10/1990"
);
}
fn test_timezone() -> UtcOffset {
UtcOffset::from_hms(0, 0, 0).expect("Valid timezone offset")
}
fn create_offset_datetime(
year: i32,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
) -> OffsetDateTime {
let date =
Date::from_calendar_date(year, time::Month::try_from(month).unwrap(), day).unwrap();
let time = Time::from_hms(hour, minute, second).unwrap();
date.with_time(time).assume_utc() // Assume UTC for simplicity
}
}

View file

@ -1,19 +1,24 @@
use std::{sync::Arc, time::Duration};
use anyhow::Result;
use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
use client::UserId;
use collections::HashMap;
use editor::{AnchorRangeExt, Editor, EditorElement, EditorStyle};
use editor::{AnchorRangeExt, CompletionProvider, Editor, EditorElement, EditorStyle};
use fuzzy::StringMatchCandidate;
use gpui::{
AsyncWindowContext, FocusableView, FontStyle, FontWeight, HighlightStyle, IntoElement, Model,
Render, SharedString, Task, TextStyle, View, ViewContext, WeakView, WhiteSpace,
};
use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
use language::{
language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, Completion,
LanguageRegistry, LanguageServerId, ToOffset,
};
use lazy_static::lazy_static;
use parking_lot::RwLock;
use project::search::SearchQuery;
use settings::Settings;
use std::{sync::Arc, time::Duration};
use theme::ThemeSettings;
use ui::prelude::*;
use ui::{prelude::*, UiTextSize};
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
@ -31,6 +36,43 @@ pub struct MessageEditor {
channel_id: Option<ChannelId>,
}
struct MessageEditorCompletionProvider(WeakView<MessageEditor>);
impl CompletionProvider for MessageEditorCompletionProvider {
fn completions(
&self,
buffer: &Model<Buffer>,
buffer_position: language::Anchor,
cx: &mut ViewContext<Editor>,
) -> Task<anyhow::Result<Vec<language::Completion>>> {
let Some(handle) = self.0.upgrade() else {
return Task::ready(Ok(Vec::new()));
};
handle.update(cx, |message_editor, cx| {
message_editor.completions(buffer, buffer_position, cx)
})
}
fn resolve_completions(
&self,
_completion_indices: Vec<usize>,
_completions: Arc<RwLock<Box<[language::Completion]>>>,
_cx: &mut ViewContext<Editor>,
) -> Task<anyhow::Result<bool>> {
Task::ready(Ok(false))
}
fn apply_additional_edits_for_completion(
&self,
_buffer: Model<Buffer>,
_completion: Completion,
_push_to_history: bool,
_cx: &mut ViewContext<Editor>,
) -> Task<Result<Option<language::Transaction>>> {
Task::ready(Ok(None))
}
}
impl MessageEditor {
pub fn new(
language_registry: Arc<LanguageRegistry>,
@ -38,8 +80,11 @@ impl MessageEditor {
editor: View<Editor>,
cx: &mut ViewContext<Self>,
) -> Self {
let this = cx.view().downgrade();
editor.update(cx, |editor, cx| {
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_use_autoclose(false);
editor.set_completion_provider(Box::new(MessageEditorCompletionProvider(this)));
});
let buffer = editor
@ -149,6 +194,71 @@ impl MessageEditor {
}
}
fn completions(
&mut self,
buffer: &Model<Buffer>,
end_anchor: Anchor,
cx: &mut ViewContext<Self>,
) -> Task<Result<Vec<Completion>>> {
let end_offset = end_anchor.to_offset(buffer.read(cx));
let Some(query) = buffer.update(cx, |buffer, _| {
let mut query = String::new();
for ch in buffer.reversed_chars_at(end_offset).take(100) {
if ch == '@' {
return Some(query.chars().rev().collect::<String>());
}
if ch.is_whitespace() || !ch.is_ascii() {
break;
}
query.push(ch);
}
return None;
}) else {
return Task::ready(Ok(vec![]));
};
let start_offset = end_offset - query.len();
let start_anchor = buffer.read(cx).anchor_before(start_offset);
let candidates = self
.users
.keys()
.map(|user| StringMatchCandidate {
id: 0,
string: user.clone(),
char_bag: user.chars().collect(),
})
.collect::<Vec<_>>();
cx.spawn(|_, cx| async move {
let matches = fuzzy::match_strings(
&candidates,
&query,
true,
10,
&Default::default(),
cx.background_executor().clone(),
)
.await;
Ok(matches
.into_iter()
.map(|mat| Completion {
old_range: start_anchor..end_anchor,
new_text: mat.string.clone(),
label: CodeLabel {
filter_range: 1..mat.string.len() + 1,
text: format!("@{}", mat.string),
runs: Vec::new(),
},
documentation: None,
server_id: LanguageServerId(0), // TODO: Make this optional or something?
lsp_completion: Default::default(), // TODO: Make this optional or something?
})
.collect())
})
}
async fn find_mentions(
this: WeakView<MessageEditor>,
buffer: BufferSnapshot,
@ -216,7 +326,7 @@ impl Render for MessageEditor {
},
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features,
font_size: rems(0.875).into(),
font_size: UiTextSize::Small.rems().into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
line_height: relative(1.3).into(),

View file

@ -85,7 +85,14 @@ impl Render for CollabTitlebarItem {
.gap_1()
.children(self.render_project_host(cx))
.child(self.render_project_name(cx))
.child(div().pr_1().children(self.render_project_branch(cx)))
.children(self.render_project_branch(cx)),
)
.child(
h_flex()
.id("collaborator-list")
.w_full()
.gap_1()
.overflow_x_scroll()
.when_some(
current_user.clone().zip(client.peer_id()).zip(room.clone()),
|this, ((current_user, peer_id), room)| {

View file

@ -37,8 +37,8 @@ impl RenderOnce for FacePile {
}
impl ParentElement for FacePile {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
&mut self.faces
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.faces.extend(elements);
}
}

View file

@ -26,8 +26,8 @@ impl CollabNotification {
}
impl ParentElement for CollabNotification {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
&mut self.children
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
self.children.extend(elements)
}
}

View file

@ -311,7 +311,7 @@ impl PickerDelegate for CommandPaletteDelegate {
let action = command.action;
cx.focus(&self.previous_focus_handle);
cx.window_context()
.spawn(move |mut cx| async move { cx.update(|_, cx| cx.dispatch_action(action)) })
.spawn(move |mut cx| async move { cx.update(|cx| cx.dispatch_action(action)) })
.detach_and_log_err(cx);
self.dismissed(cx);
}

View file

@ -974,7 +974,7 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
.browser_download_url;
let mut response = http
.get(&url, Default::default(), true)
.get(url, Default::default(), true)
.await
.map_err(|err| anyhow!("error downloading copilot release: {}", err))?;
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));

View file

@ -355,7 +355,7 @@ fn initiate_sign_in(cx: &mut WindowContext) {
cx.spawn(|mut cx| async move {
task.await;
if let Some(copilot) = cx.update(|_, cx| Copilot::global(cx)).ok().flatten() {
if let Some(copilot) = cx.update(|cx| Copilot::global(cx)).ok().flatten() {
workspace
.update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
Status::Authorized => workspace.show_toast(

View file

@ -1584,27 +1584,34 @@ mod tests {
}
fn editor_blocks(editor: &View<Editor>, cx: &mut WindowContext) -> Vec<(u32, SharedString)> {
let editor_view = editor.clone();
editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
snapshot
.blocks_in_range(0..snapshot.max_point().row())
.enumerate()
.filter_map(|(ix, (row, block))| {
let name = match block {
TransformBlock::Custom(block) => block
.render(&mut BlockContext {
view_context: cx,
anchor_x: px(0.),
gutter_padding: px(0.),
gutter_width: px(0.),
line_height: px(0.),
em_width: px(0.),
block_id: ix,
editor_style: &editor::EditorStyle::default(),
})
.inner_id()?
.try_into()
.ok()?,
let name: SharedString = match block {
TransformBlock::Custom(block) => cx.with_element_context({
let editor_view = editor_view.clone();
|cx| -> Option<SharedString> {
block
.render(&mut BlockContext {
context: cx,
anchor_x: px(0.),
gutter_padding: px(0.),
gutter_width: px(0.),
line_height: px(0.),
em_width: px(0.),
block_id: ix,
view: editor_view,
editor_style: &editor::EditorStyle::default(),
})
.inner_id()?
.try_into()
.ok()
}
})?,
TransformBlock::ExcerptHeader {
starts_new_buffer, ..

View file

@ -4,7 +4,7 @@ use super::{
};
use crate::{Anchor, Editor, EditorStyle, ExcerptId, ExcerptRange, ToPoint as _};
use collections::{Bound, HashMap, HashSet};
use gpui::{AnyElement, Pixels, ViewContext};
use gpui::{AnyElement, ElementContext, Pixels, View};
use language::{BufferSnapshot, Chunk, Patch, Point};
use parking_lot::Mutex;
use std::{
@ -81,7 +81,8 @@ pub enum BlockStyle {
}
pub struct BlockContext<'a, 'b> {
pub view_context: &'b mut ViewContext<'a, Editor>,
pub context: &'b mut ElementContext<'a>,
pub view: View<Editor>,
pub anchor_x: Pixels,
pub gutter_width: Pixels,
pub gutter_padding: Pixels,
@ -933,16 +934,16 @@ impl BlockDisposition {
}
impl<'a> Deref for BlockContext<'a, '_> {
type Target = ViewContext<'a, Editor>;
type Target = ElementContext<'a>;
fn deref(&self) -> &Self::Target {
self.view_context
self.context
}
}
impl DerefMut for BlockContext<'_, '_> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.view_context
self.context
}
}

View file

@ -40,7 +40,7 @@ pub(crate) use actions::*;
use aho_corasick::AhoCorasick;
use anyhow::{anyhow, Context as _, Result};
use blink_manager::BlinkManager;
use client::{Client, Collaborator, ParticipantIndex};
use client::{Collaborator, ParticipantIndex};
use clock::ReplicaId;
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
use convert_case::{Case, Casing};
@ -57,9 +57,10 @@ use gpui::{
div, impl_actions, point, prelude::*, px, relative, rems, size, uniform_list, Action,
AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context,
DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight,
HighlightStyle, Hsla, InputHandler, InteractiveText, KeyContext, Model, MouseButton,
ParentElement, Pixels, Render, SharedString, Styled, StyledText, Subscription, Task, TextStyle,
UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext,
HighlightStyle, Hsla, InteractiveText, KeyContext, Model, MouseButton, ParentElement, Pixels,
Render, SharedString, Styled, StyledText, Subscription, Task, TextStyle,
UniformListScrollHandle, View, ViewContext, ViewInputHandler, VisualContext, WeakView,
WhiteSpace, WindowContext,
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState};
@ -71,8 +72,7 @@ use language::{
language_settings::{self, all_language_settings, InlayHintSettings},
markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CodeAction,
CodeLabel, Completion, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize,
Language, LanguageRegistry, LanguageServerName, OffsetRangeExt, Point, Selection,
SelectionGoal, TransactionId,
Language, LanguageServerName, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId,
};
use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState};
@ -88,7 +88,7 @@ use ordered_float::OrderedFloat;
use parking_lot::RwLock;
use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction};
use rand::prelude::*;
use rpc::proto::{self, *};
use rpc::proto::*;
use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide};
use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
use serde::{Deserialize, Serialize};
@ -365,6 +365,7 @@ pub struct Editor {
active_diagnostics: Option<ActiveDiagnosticGroup>,
soft_wrap_mode_override: Option<language_settings::SoftWrap>,
project: Option<Model<Project>>,
completion_provider: Option<Box<dyn CompletionProvider>>,
collaboration_hub: Option<Box<dyn CollaborationHub>>,
blink_manager: Model<BlinkManager>,
show_cursor_names: bool,
@ -408,6 +409,7 @@ pub struct Editor {
style: Option<EditorStyle>,
editor_actions: Vec<Box<dyn Fn(&mut ViewContext<Self>)>>,
show_copilot_suggestions: bool,
use_autoclose: bool,
}
pub struct EditorSnapshot {
@ -731,85 +733,21 @@ impl CompletionsMenu {
return None;
}
let Some(project) = editor.project.clone() else {
let Some(provider) = editor.completion_provider.as_ref() else {
return None;
};
let client = project.read(cx).client();
let language_registry = project.read(cx).languages().clone();
let resolve_task = provider.resolve_completions(
self.matches.iter().map(|m| m.candidate_id).collect(),
self.completions.clone(),
cx,
);
let is_remote = project.read(cx).is_remote();
let project_id = project.read(cx).remote_id();
let completions = self.completions.clone();
let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect();
Some(cx.spawn(move |this, mut cx| async move {
if is_remote {
let Some(project_id) = project_id else {
log::error!("Remote project without remote_id");
return;
};
for completion_index in completion_indices {
let completions_guard = completions.read();
let completion = &completions_guard[completion_index];
if completion.documentation.is_some() {
continue;
}
let server_id = completion.server_id;
let completion = completion.lsp_completion.clone();
drop(completions_guard);
Self::resolve_completion_documentation_remote(
project_id,
server_id,
completions.clone(),
completion_index,
completion,
client.clone(),
language_registry.clone(),
)
.await;
_ = this.update(&mut cx, |_, cx| cx.notify());
}
} else {
for completion_index in completion_indices {
let completions_guard = completions.read();
let completion = &completions_guard[completion_index];
if completion.documentation.is_some() {
continue;
}
let server_id = completion.server_id;
let completion = completion.lsp_completion.clone();
drop(completions_guard);
let server = project
.read_with(&mut cx, |project, _| {
project.language_server_for_id(server_id)
})
.ok()
.flatten();
let Some(server) = server else {
return;
};
Self::resolve_completion_documentation_local(
server,
completions.clone(),
completion_index,
completion,
language_registry.clone(),
)
.await;
_ = this.update(&mut cx, |_, cx| cx.notify());
}
return Some(cx.spawn(move |this, mut cx| async move {
if let Some(true) = resolve_task.await.log_err() {
this.update(&mut cx, |_, cx| cx.notify()).ok();
}
}))
}));
}
fn attempt_resolve_selected_completion_documentation(
@ -826,146 +764,16 @@ impl CompletionsMenu {
let Some(project) = project else {
return;
};
let language_registry = project.read(cx).languages().clone();
let completions = self.completions.clone();
let completions_guard = completions.read();
let completion = &completions_guard[completion_index];
if completion.documentation.is_some() {
return;
}
let server_id = completion.server_id;
let completion = completion.lsp_completion.clone();
drop(completions_guard);
if project.read(cx).is_remote() {
let Some(project_id) = project.read(cx).remote_id() else {
log::error!("Remote project without remote_id");
return;
};
let client = project.read(cx).client();
cx.spawn(move |this, mut cx| async move {
Self::resolve_completion_documentation_remote(
project_id,
server_id,
completions.clone(),
completion_index,
completion,
client,
language_registry.clone(),
)
.await;
_ = this.update(&mut cx, |_, cx| cx.notify());
})
.detach();
} else {
let Some(server) = project.read(cx).language_server_for_id(server_id) else {
return;
};
cx.spawn(move |this, mut cx| async move {
Self::resolve_completion_documentation_local(
server,
completions,
completion_index,
completion,
language_registry,
)
.await;
_ = this.update(&mut cx, |_, cx| cx.notify());
})
.detach();
}
}
async fn resolve_completion_documentation_remote(
project_id: u64,
server_id: LanguageServerId,
completions: Arc<RwLock<Box<[Completion]>>>,
completion_index: usize,
completion: lsp::CompletionItem,
client: Arc<Client>,
language_registry: Arc<LanguageRegistry>,
) {
let request = proto::ResolveCompletionDocumentation {
project_id,
language_server_id: server_id.0 as u64,
lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(),
};
let Some(response) = client
.request(request)
.await
.context("completion documentation resolve proto request")
.log_err()
else {
return;
};
if response.text.is_empty() {
let mut completions = completions.write();
let completion = &mut completions[completion_index];
completion.documentation = Some(Documentation::Undocumented);
}
let documentation = if response.is_markdown {
Documentation::MultiLineMarkdown(
markdown::parse_markdown(&response.text, &language_registry, None).await,
)
} else if response.text.lines().count() <= 1 {
Documentation::SingleLine(response.text)
} else {
Documentation::MultiLinePlainText(response.text)
};
let mut completions = completions.write();
let completion = &mut completions[completion_index];
completion.documentation = Some(documentation);
}
async fn resolve_completion_documentation_local(
server: Arc<lsp::LanguageServer>,
completions: Arc<RwLock<Box<[Completion]>>>,
completion_index: usize,
completion: lsp::CompletionItem,
language_registry: Arc<LanguageRegistry>,
) {
let can_resolve = server
.capabilities()
.completion_provider
.as_ref()
.and_then(|options| options.resolve_provider)
.unwrap_or(false);
if !can_resolve {
return;
}
let request = server.request::<lsp::request::ResolveCompletionItem>(completion);
let Some(completion_item) = request.await.log_err() else {
return;
};
if let Some(lsp_documentation) = completion_item.documentation {
let documentation = language::prepare_completion_documentation(
&lsp_documentation,
&language_registry,
None, // TODO: Try to reasonably work out which language the completion is for
)
.await;
let mut completions = completions.write();
let completion = &mut completions[completion_index];
completion.documentation = Some(documentation);
} else {
let mut completions = completions.write();
let completion = &mut completions[completion_index];
completion.documentation = Some(Documentation::Undocumented);
}
let resolve_task = project.update(cx, |project, cx| {
project.resolve_completions(vec![completion_index], self.completions.clone(), cx)
});
cx.spawn(move |this, mut cx| async move {
if let Some(true) = resolve_task.await.log_err() {
this.update(&mut cx, |_, cx| cx.notify()).ok();
}
})
.detach();
}
fn visible(&self) -> bool {
@ -1574,6 +1382,7 @@ impl Editor {
ime_transaction: Default::default(),
active_diagnostics: None,
soft_wrap_mode_override,
completion_provider: project.clone().map(|project| Box::new(project) as _),
collaboration_hub: project.clone().map(|project| Box::new(project) as _),
project,
blink_manager: blink_manager.clone(),
@ -1603,6 +1412,7 @@ impl Editor {
keymap_context_layers: Default::default(),
input_enabled: true,
read_only: false,
use_autoclose: true,
leader_peer_id: None,
remote_id: None,
hover_state: Default::default(),
@ -1806,6 +1616,10 @@ impl Editor {
self.collaboration_hub = Some(hub);
}
pub fn set_completion_provider(&mut self, hub: Box<dyn CompletionProvider>) {
self.completion_provider = Some(hub);
}
pub fn placeholder_text(&self) -> Option<&str> {
self.placeholder_text.as_deref()
}
@ -1880,6 +1694,10 @@ impl Editor {
self.read_only = read_only;
}
pub fn set_use_autoclose(&mut self, autoclose: bool) {
self.use_autoclose = autoclose;
}
pub fn set_show_copilot_suggestions(&mut self, show_copilot_suggestions: bool) {
self.show_copilot_suggestions = show_copilot_suggestions;
}
@ -2478,7 +2296,12 @@ impl Editor {
),
&bracket_pair.start[..prefix_len],
));
if following_text_allows_autoclose && preceding_text_matches_prefix {
let autoclose = self.use_autoclose
&& snapshot.settings_at(selection.start, cx).use_autoclose;
if autoclose
&& following_text_allows_autoclose
&& preceding_text_matches_prefix
{
let anchor = snapshot.anchor_before(selection.end);
new_selections.push((selection.map(|_| anchor), text.len()));
new_autoclose_regions.push((
@ -3252,9 +3075,7 @@ impl Editor {
return;
}
let project = if let Some(project) = self.project.clone() {
project
} else {
let Some(provider) = self.completion_provider.as_ref() else {
return;
};
@ -3270,9 +3091,7 @@ impl Editor {
};
let query = Self::completion_query(&self.buffer.read(cx).read(cx), position.clone());
let completions = project.update(cx, |project, cx| {
project.completions(&buffer, buffer_position, cx)
});
let completions = provider.completions(&buffer, buffer_position, cx);
let id = post_inc(&mut self.next_completion_id);
let task = cx.spawn(|this, mut cx| {
@ -3381,6 +3200,7 @@ impl Editor {
let buffer_handle = completions_menu.buffer;
let completions = completions_menu.completions.read();
let completion = completions.get(mat.candidate_id)?;
cx.stop_propagation();
let snippet;
let text;
@ -3477,15 +3297,13 @@ impl Editor {
this.refresh_copilot_suggestions(true, cx);
});
let project = self.project.clone()?;
let apply_edits = project.update(cx, |project, cx| {
project.apply_additional_edits_for_completion(
buffer_handle,
completion.clone(),
true,
cx,
)
});
let provider = self.completion_provider.as_ref()?;
let apply_edits = provider.apply_additional_edits_for_completion(
buffer_handle,
completion.clone(),
true,
cx,
);
Some(cx.foreground_executor().spawn(async move {
apply_edits.await?;
Ok(())
@ -3572,7 +3390,7 @@ impl Editor {
let replica_id = this.update(&mut cx, |this, cx| this.replica_id(cx))?;
let mut entries = transaction.0.into_iter().collect::<Vec<_>>();
cx.update(|_, cx| {
cx.update(|cx| {
entries.sort_unstable_by_key(|(buffer, _)| {
buffer.read(cx).file().map(|f| f.path().clone())
});
@ -3907,7 +3725,7 @@ impl Editor {
self.show_cursor_names = true;
cx.notify();
cx.spawn(|this, mut cx| async move {
cx.background_executor().timer(Duration::from_secs(2)).await;
cx.background_executor().timer(Duration::from_secs(3)).await;
this.update(&mut cx, |this, cx| {
this.show_cursor_names = false;
cx.notify()
@ -4094,7 +3912,7 @@ impl Editor {
gutter_hovered: bool,
_line_height: Pixels,
_gutter_margin: Pixels,
cx: &mut ViewContext<Self>,
editor_view: View<Editor>,
) -> Vec<Option<IconButton>> {
fold_data
.iter()
@ -4104,14 +3922,19 @@ impl Editor {
.map(|(fold_status, buffer_row, active)| {
(active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| {
IconButton::new(ix as usize, ui::IconName::ChevronDown)
.on_click(cx.listener(move |editor, _e, cx| match fold_status {
FoldStatus::Folded => {
editor.unfold_at(&UnfoldAt { buffer_row }, cx);
.on_click({
let view = editor_view.clone();
move |_e, cx| {
view.update(cx, |editor, cx| match fold_status {
FoldStatus::Folded => {
editor.unfold_at(&UnfoldAt { buffer_row }, cx);
}
FoldStatus::Foldable => {
editor.fold_at(&FoldAt { buffer_row }, cx);
}
})
}
FoldStatus::Foldable => {
editor.fold_at(&FoldAt { buffer_row }, cx);
}
}))
})
.icon_color(ui::Color::Muted)
.icon_size(ui::IconSize::Small)
.selected(fold_status == FoldStatus::Folded)
@ -9097,6 +8920,66 @@ impl CollaborationHub for Model<Project> {
}
}
pub trait CompletionProvider {
fn completions(
&self,
buffer: &Model<Buffer>,
buffer_position: text::Anchor,
cx: &mut ViewContext<Editor>,
) -> Task<Result<Vec<Completion>>>;
fn resolve_completions(
&self,
completion_indices: Vec<usize>,
completions: Arc<RwLock<Box<[Completion]>>>,
cx: &mut ViewContext<Editor>,
) -> Task<Result<bool>>;
fn apply_additional_edits_for_completion(
&self,
buffer: Model<Buffer>,
completion: Completion,
push_to_history: bool,
cx: &mut ViewContext<Editor>,
) -> Task<Result<Option<language::Transaction>>>;
}
impl CompletionProvider for Model<Project> {
fn completions(
&self,
buffer: &Model<Buffer>,
buffer_position: text::Anchor,
cx: &mut ViewContext<Editor>,
) -> Task<Result<Vec<Completion>>> {
self.update(cx, |project, cx| {
project.completions(&buffer, buffer_position, cx)
})
}
fn resolve_completions(
&self,
completion_indices: Vec<usize>,
completions: Arc<RwLock<Box<[Completion]>>>,
cx: &mut ViewContext<Editor>,
) -> Task<Result<bool>> {
self.update(cx, |project, cx| {
project.resolve_completions(completion_indices, completions, cx)
})
}
fn apply_additional_edits_for_completion(
&self,
buffer: Model<Buffer>,
completion: Completion,
push_to_history: bool,
cx: &mut ViewContext<Editor>,
) -> Task<Result<Option<language::Transaction>>> {
self.update(cx, |project, cx| {
project.apply_additional_edits_for_completion(buffer, completion, push_to_history, cx)
})
}
}
fn inlay_hint_settings(
location: Anchor,
snapshot: &MultiBufferSnapshot,
@ -9300,7 +9183,7 @@ impl Render for Editor {
}
}
impl InputHandler for Editor {
impl ViewInputHandler for Editor {
fn text_for_range(
&mut self,
range_utf16: Range<usize>,
@ -9697,10 +9580,10 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
.size(ButtonSize::Compact)
.style(ButtonStyle::Transparent)
.visible_on_hover(group_id)
.on_click(cx.listener({
.on_click({
let message = diagnostic.message.clone();
move |_, _, cx| cx.write_to_clipboard(ClipboardItem::new(message.clone()))
}))
move |_click, cx| cx.write_to_clipboard(ClipboardItem::new(message.clone()))
})
.tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)),
)
.into_any_element()

View file

@ -25,12 +25,12 @@ use collections::{BTreeMap, HashMap};
use git::diff::DiffHunkStatus;
use gpui::{
div, fill, outline, overlay, point, px, quad, relative, size, transparent_black, Action,
AnchorCorner, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Corners,
CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity, Hsla,
InteractiveBounds, InteractiveElement, IntoElement, ModifiersChangedEvent, MouseButton,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, ScrollDelta,
ScrollWheelEvent, ShapedLine, SharedString, Size, StackingOrder, StatefulInteractiveElement,
Style, Styled, TextRun, TextStyle, View, ViewContext, WindowContext,
AnchorCorner, AnyElement, AvailableSpace, Bounds, ContentMask, Corners, CursorStyle,
DispatchPhase, Edges, Element, ElementInputHandler, Entity, Hsla, InteractiveBounds,
InteractiveElement, IntoElement, ModifiersChangedEvent, MouseButton, MouseDownEvent,
MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine,
SharedString, Size, StackingOrder, StatefulInteractiveElement, Style, Styled, TextRun,
TextStyle, View, ViewContext, WindowContext,
};
use itertools::Itertools;
use language::language_settings::ShowWhitespaceSetting;
@ -330,7 +330,7 @@ impl EditorElement {
register_action(view, cx, Editor::display_cursor_names);
}
fn register_key_listeners(&self, cx: &mut WindowContext) {
fn register_key_listeners(&self, cx: &mut ElementContext) {
cx.on_key_event({
let editor = self.editor.clone();
move |event: &ModifiersChangedEvent, phase, cx| {
@ -628,7 +628,7 @@ impl EditorElement {
gutter_bounds: Bounds<Pixels>,
text_bounds: Bounds<Pixels>,
layout: &LayoutState,
cx: &mut WindowContext,
cx: &mut ElementContext,
) {
let bounds = gutter_bounds.union(&text_bounds);
let scroll_top =
@ -711,7 +711,7 @@ impl EditorElement {
&mut self,
bounds: Bounds<Pixels>,
layout: &mut LayoutState,
cx: &mut WindowContext,
cx: &mut ElementContext,
) {
let line_height = layout.position_map.line_height;
@ -782,7 +782,7 @@ impl EditorElement {
});
}
fn paint_diff_hunks(bounds: Bounds<Pixels>, layout: &LayoutState, cx: &mut WindowContext) {
fn paint_diff_hunks(bounds: Bounds<Pixels>, layout: &LayoutState, cx: &mut ElementContext) {
let line_height = layout.position_map.line_height;
let scroll_position = layout.position_map.snapshot.scroll_position();
@ -886,7 +886,7 @@ impl EditorElement {
&mut self,
text_bounds: Bounds<Pixels>,
layout: &mut LayoutState,
cx: &mut WindowContext,
cx: &mut ElementContext,
) {
let start_row = layout.visible_display_row_range.start;
let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO);
@ -1153,7 +1153,7 @@ impl EditorElement {
&mut self,
text_bounds: Bounds<Pixels>,
layout: &mut LayoutState,
cx: &mut WindowContext,
cx: &mut ElementContext,
) {
let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO);
let start_row = layout.visible_display_row_range.start;
@ -1218,9 +1218,11 @@ impl EditorElement {
popover_origin.x = popover_origin.x + x_out_of_bounds;
}
cx.break_content_mask(|cx| {
hover_popover.draw(popover_origin, available_space, cx)
});
if cx.was_top_layer(&popover_origin, cx.stacking_order()) {
cx.break_content_mask(|cx| {
hover_popover.draw(popover_origin, available_space, cx)
});
}
current_y = popover_origin.y - HOVER_POPOVER_GAP;
}
@ -1266,7 +1268,7 @@ impl EditorElement {
&mut self,
bounds: Bounds<Pixels>,
layout: &mut LayoutState,
cx: &mut WindowContext,
cx: &mut ElementContext,
) {
if layout.mode != EditorMode::Full {
return;
@ -1510,7 +1512,7 @@ impl EditorElement {
layout: &LayoutState,
content_origin: gpui::Point<Pixels>,
bounds: Bounds<Pixels>,
cx: &mut WindowContext,
cx: &mut ElementContext,
) {
let start_row = layout.visible_display_row_range.start;
let end_row = layout.visible_display_row_range.end;
@ -1562,7 +1564,7 @@ impl EditorElement {
&mut self,
bounds: Bounds<Pixels>,
layout: &mut LayoutState,
cx: &mut WindowContext,
cx: &mut ElementContext,
) {
let scroll_position = layout.position_map.snapshot.scroll_position();
let scroll_left = scroll_position.x * layout.position_map.em_width;
@ -1812,7 +1814,7 @@ impl EditorElement {
}
}
fn compute_layout(&mut self, bounds: Bounds<Pixels>, cx: &mut WindowContext) -> LayoutState {
fn compute_layout(&mut self, bounds: Bounds<Pixels>, cx: &mut ElementContext) -> LayoutState {
self.editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let style = self.style.clone();
@ -2081,7 +2083,9 @@ impl EditorElement {
.width;
let scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.width;
let (scroll_width, blocks) = cx.with_element_id(Some("editor_blocks"), |cx| {
let editor_view = cx.view().clone();
let (scroll_width, blocks) = cx.with_element_context(|cx| {
cx.with_element_id(Some("editor_blocks"), |cx| {
self.layout_blocks(
start_row..end_row,
&snapshot,
@ -2095,8 +2099,10 @@ impl EditorElement {
&style,
&line_layouts,
editor,
editor_view,
cx,
)
})
});
let scroll_max = point(
@ -2128,7 +2134,13 @@ impl EditorElement {
if let Some(newest_selection_head) = newest_selection_head {
if (start_row..end_row).contains(&newest_selection_head.row()) {
if editor.context_menu_visible() {
let max_height = (12. * line_height).min((bounds.size.height - line_height) / 2.);
let max_height = cmp::min(
12. * line_height,
cmp::max(
3. * line_height,
(bounds.size.height - line_height) / 2.,
)
);
context_menu =
editor.render_context_menu(newest_selection_head, &self.style, max_height, cx);
}
@ -2166,15 +2178,19 @@ impl EditorElement {
cx,
);
let fold_indicators = cx.with_element_id(Some("gutter_fold_indicators"), |cx| {
let editor_view = cx.view().clone();
let fold_indicators = cx.with_element_context(|cx| {
cx.with_element_id(Some("gutter_fold_indicators"), |_cx| {
editor.render_fold_indicators(
fold_statuses,
&style,
editor.gutter_hovered,
line_height,
gutter_margin,
cx,
editor_view,
)
})
});
let invisible_symbol_font_size = font_size / 2.;
@ -2265,7 +2281,8 @@ impl EditorElement {
style: &EditorStyle,
line_layouts: &[LineWithInvisibles],
editor: &mut Editor,
cx: &mut ViewContext<Editor>,
editor_view: View<Editor>,
cx: &mut ElementContext,
) -> (Pixels, Vec<BlockLayout>) {
let mut block_id = 0;
let (fixed_blocks, non_fixed_blocks) = snapshot
@ -2279,7 +2296,7 @@ impl EditorElement {
available_space: Size<AvailableSpace>,
block_id: usize,
editor: &mut Editor,
cx: &mut ViewContext<Editor>| {
cx: &mut ElementContext| {
let mut element = match block {
TransformBlock::Custom(block) => {
let align_to = block
@ -2298,13 +2315,14 @@ impl EditorElement {
};
block.render(&mut BlockContext {
view_context: cx,
context: cx,
anchor_x,
gutter_padding,
line_height,
gutter_width,
em_width,
block_id,
view: editor_view.clone(),
editor_style: &self.style,
})
}
@ -2496,7 +2514,7 @@ impl EditorElement {
&mut self,
interactive_bounds: &InteractiveBounds,
layout: &LayoutState,
cx: &mut WindowContext,
cx: &mut ElementContext,
) {
cx.on_mouse_event({
let position_map = layout.position_map.clone();
@ -2556,7 +2574,7 @@ impl EditorElement {
gutter_bounds: Bounds<Pixels>,
text_bounds: Bounds<Pixels>,
layout: &LayoutState,
cx: &mut WindowContext,
cx: &mut ElementContext,
) {
let interactive_bounds = InteractiveBounds {
bounds: bounds.intersect(&cx.content_mask().bounds),
@ -2779,7 +2797,7 @@ impl LineWithInvisibles {
content_origin: gpui::Point<Pixels>,
whitespace_setting: ShowWhitespaceSetting,
selection_ranges: &[Range<DisplayPoint>],
cx: &mut WindowContext,
cx: &mut ElementContext,
) {
let line_height = layout.position_map.line_height;
let line_y = line_height * row as f32 - layout.position_map.scroll_position.y;
@ -2813,7 +2831,7 @@ impl LineWithInvisibles {
row: u32,
line_height: Pixels,
whitespace_setting: ShowWhitespaceSetting,
cx: &mut WindowContext,
cx: &mut ElementContext,
) {
let allowed_invisibles_regions = match whitespace_setting {
ShowWhitespaceSetting::None => return,
@ -2862,7 +2880,7 @@ impl Element for EditorElement {
fn request_layout(
&mut self,
_element_state: Option<Self::State>,
cx: &mut gpui::WindowContext,
cx: &mut gpui::ElementContext,
) -> (gpui::LayoutId, Self::State) {
cx.with_view_id(self.editor.entity_id(), |cx| {
self.editor.update(cx, |editor, cx| {
@ -2874,34 +2892,36 @@ impl Element for EditorElement {
let mut style = Style::default();
style.size.width = relative(1.).into();
style.size.height = self.style.text.line_height_in_pixels(rem_size).into();
cx.request_layout(&style, None)
cx.with_element_context(|cx| cx.request_layout(&style, None))
}
EditorMode::AutoHeight { max_lines } => {
let editor_handle = cx.view().clone();
let max_line_number_width =
self.max_line_number_width(&editor.snapshot(cx), cx);
cx.request_measured_layout(
Style::default(),
move |known_dimensions, _, cx| {
editor_handle
.update(cx, |editor, cx| {
compute_auto_height_layout(
editor,
max_lines,
max_line_number_width,
known_dimensions,
cx,
)
})
.unwrap_or_default()
},
)
cx.with_element_context(|cx| {
cx.request_measured_layout(
Style::default(),
move |known_dimensions, _, cx| {
editor_handle
.update(cx, |editor, cx| {
compute_auto_height_layout(
editor,
max_lines,
max_line_number_width,
known_dimensions,
cx,
)
})
.unwrap_or_default()
},
)
})
}
EditorMode::Full => {
let mut style = Style::default();
style.size.width = relative(1.).into();
style.size.height = relative(1.).into();
cx.request_layout(&style, None)
cx.with_element_context(|cx| cx.request_layout(&style, None))
}
};
@ -2914,7 +2934,7 @@ impl Element for EditorElement {
&mut self,
bounds: Bounds<gpui::Pixels>,
_element_state: &mut Self::State,
cx: &mut gpui::WindowContext,
cx: &mut gpui::ElementContext,
) {
let editor = self.editor.clone();
@ -2943,9 +2963,10 @@ impl Element for EditorElement {
self.register_key_listeners(cx);
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
let input_handler =
ElementInputHandler::new(bounds, self.editor.clone(), cx);
cx.handle_input(&focus_handle, input_handler);
cx.handle_input(
&focus_handle,
ElementInputHandler::new(bounds, self.editor.clone()),
);
self.paint_background(gutter_bounds, text_bounds, &layout, cx);
if layout.gutter_size.width > Pixels::ZERO {
@ -3195,7 +3216,7 @@ impl Cursor {
}
}
pub fn paint(&self, origin: gpui::Point<Pixels>, cx: &mut WindowContext) {
pub fn paint(&self, origin: gpui::Point<Pixels>, cx: &mut ElementContext) {
let bounds = match self.shape {
CursorShape::Bar => Bounds {
origin: self.origin + origin,
@ -3275,7 +3296,7 @@ pub struct HighlightedRangeLine {
}
impl HighlightedRange {
pub fn paint(&self, bounds: Bounds<Pixels>, cx: &mut WindowContext) {
pub fn paint(&self, bounds: Bounds<Pixels>, cx: &mut ElementContext) {
if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x {
self.paint_lines(self.start_y, &self.lines[0..1], bounds, cx);
self.paint_lines(
@ -3294,7 +3315,7 @@ impl HighlightedRange {
start_y: Pixels,
lines: &[HighlightedRangeLine],
_bounds: Bounds<Pixels>,
cx: &mut WindowContext,
cx: &mut ElementContext,
) {
if lines.is_empty() {
return;
@ -3512,14 +3533,16 @@ mod tests {
.unwrap();
let state = cx
.update_window(window.into(), |view, cx| {
cx.with_view_id(view.entity_id(), |cx| {
element.compute_layout(
Bounds {
origin: point(px(500.), px(500.)),
size: size(px(500.), px(500.)),
},
cx,
)
cx.with_element_context(|cx| {
cx.with_view_id(view.entity_id(), |cx| {
element.compute_layout(
Bounds {
origin: point(px(500.), px(500.)),
size: size(px(500.), px(500.)),
},
cx,
)
})
})
})
.unwrap();
@ -3606,14 +3629,16 @@ mod tests {
let state = cx
.update_window(window.into(), |view, cx| {
cx.with_view_id(view.entity_id(), |cx| {
element.compute_layout(
Bounds {
origin: point(px(500.), px(500.)),
size: size(px(500.), px(500.)),
},
cx,
)
cx.with_element_context(|cx| {
cx.with_view_id(view.entity_id(), |cx| {
element.compute_layout(
Bounds {
origin: point(px(500.), px(500.)),
size: size(px(500.), px(500.)),
},
cx,
)
})
})
})
.unwrap();
@ -3670,14 +3695,16 @@ mod tests {
let mut element = EditorElement::new(&editor, style);
let state = cx
.update_window(window.into(), |view, cx| {
cx.with_view_id(view.entity_id(), |cx| {
element.compute_layout(
Bounds {
origin: point(px(500.), px(500.)),
size: size(px(500.), px(500.)),
},
cx,
)
cx.with_element_context(|cx| {
cx.with_view_id(view.entity_id(), |cx| {
element.compute_layout(
Bounds {
origin: point(px(500.), px(500.)),
size: size(px(500.), px(500.)),
},
cx,
)
})
})
})
.unwrap();
@ -3695,8 +3722,10 @@ mod tests {
// Don't panic.
let bounds = Bounds::<Pixels>::new(Default::default(), size);
cx.update_window(window.into(), |_, cx| element.paint(bounds, &mut (), cx))
.unwrap()
cx.update_window(window.into(), |_, cx| {
cx.with_element_context(|cx| element.paint(bounds, &mut (), cx))
})
.unwrap()
}
#[gpui::test]
@ -3871,13 +3900,15 @@ mod tests {
.unwrap();
let layout_state = cx
.update_window(window.into(), |_, cx| {
element.compute_layout(
Bounds {
origin: point(px(500.), px(500.)),
size: size(px(500.), px(500.)),
},
cx,
)
cx.with_element_context(|cx| {
element.compute_layout(
Bounds {
origin: point(px(500.), px(500.)),
size: size(px(500.), px(500.)),
},
cx,
)
})
})
.unwrap();

View file

@ -247,7 +247,7 @@ fn show_hover(
};
// query the LSP for hover info
let hover_request = cx.update(|_, cx| {
let hover_request = cx.update(|cx| {
project.update(cx, |project, cx| {
project.hover(&buffer, buffer_position, cx)
})

View file

@ -72,7 +72,7 @@ impl GitRepository for LibGitRepository {
// This check is required because index.get_path() unwraps internally :(
check_path_to_repo_path_errors(relative_file_path)?;
let oid = match index.get_path(&relative_file_path, STAGE_NORMAL) {
let oid = match index.get_path(relative_file_path, STAGE_NORMAL) {
Some(entry) => entry.id,
None => return Ok(None),
};
@ -81,7 +81,7 @@ impl GitRepository for LibGitRepository {
Ok(Some(String::from_utf8(content)?))
}
match logic(&self, relative_file_path) {
match logic(self, relative_file_path) {
Ok(value) => return value,
Err(err) => log::error!("Error loading head text: {:?}", err),
}
@ -199,7 +199,7 @@ impl GitRepository for LibGitRepository {
fn matches_index(repo: &LibGitRepository, path: &RepoPath, mtime: SystemTime) -> bool {
if let Some(index) = repo.index().log_err() {
if let Some(entry) = index.get_path(&path, 0) {
if let Some(entry) = index.get_path(path, 0) {
if let Some(mtime) = mtime.duration_since(SystemTime::UNIX_EPOCH).log_err() {
if entry.mtime.seconds() == mtime.as_secs() as i32
&& entry.mtime.nanoseconds() == mtime.subsec_nanos()

View file

@ -165,7 +165,7 @@ impl BufferDiff {
let mut tree = SumTree::new();
let buffer_text = buffer.as_rope().to_string();
let patch = Self::diff(&diff_base, &buffer_text);
let patch = Self::diff(diff_base, &buffer_text);
if let Some(patch) = patch {
let mut divergence = 0;

View file

@ -40,14 +40,25 @@ use std::any::{Any, TypeId};
/// register_action!(Paste);
/// ```
pub trait Action: 'static {
/// Clone the action into a new box
fn boxed_clone(&self) -> Box<dyn Action>;
/// Cast the action to the any type
fn as_any(&self) -> &dyn Any;
/// Do a partial equality check on this action and the other
fn partial_eq(&self, action: &dyn Action) -> bool;
/// Get the name of this action, for displaying in UI
fn name(&self) -> &str;
/// Get the name of this action for debugging
fn debug_name() -> &'static str
where
Self: Sized;
/// Build this action from a JSON value. This is used to construct actions from the keymap.
/// A value of `{}` will be passed for actions that don't have any parameters.
fn build(value: serde_json::Value) -> Result<Box<dyn Action>>
where
Self: Sized;
@ -62,6 +73,7 @@ impl std::fmt::Debug for dyn Action {
}
impl dyn Action {
/// Get the type id of this action
pub fn type_id(&self) -> TypeId {
self.as_any().type_id()
}
@ -170,6 +182,7 @@ impl ActionRegistry {
macro_rules! actions {
($namespace:path, [ $($name:ident),* $(,)? ]) => {
$(
/// The `$name` action see [`gpui::actions!`]
#[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, gpui::private::serde_derive::Deserialize)]
#[serde(crate = "gpui::private::serde")]
pub struct $name;

View file

@ -1,5 +1,3 @@
#![deny(missing_docs)]
mod async_context;
mod entity_map;
mod model_context;

View file

@ -213,7 +213,12 @@ impl AsyncWindowContext {
}
/// A convenience method for [WindowContext::update()]
pub fn update<R>(
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()]
pub fn update_root<R>(
&mut self,
update: impl FnOnce(AnyView, &mut WindowContext) -> R,
) -> Result<R> {

View file

@ -1,5 +1,3 @@
#![deny(missing_docs)]
use crate::{
Action, AnyElement, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem, Context, Entity, EventEmitter,
@ -352,7 +350,7 @@ impl TestAppContext {
}
/// Returns the `TestWindow` backing the given handle.
pub fn test_window(&self, window: AnyWindowHandle) -> TestWindow {
pub(crate) fn test_window(&self, window: AnyWindowHandle) -> TestWindow {
self.app
.borrow_mut()
.windows
@ -642,8 +640,11 @@ impl<'a> VisualTestContext {
.as_ref()
.expect("Can't draw to this window without a root view")
.entity_id();
cx.with_view_id(entity_id, |cx| {
f(cx).draw(origin, space, cx);
cx.with_element_context(|cx| {
cx.with_view_id(entity_id, |cx| {
f(cx).draw(origin, space, cx);
})
});
cx.refresh();

View file

@ -8,8 +8,12 @@ use std::{
sync::atomic::{AtomicUsize, Ordering::SeqCst},
};
/// A source of assets for this app to use.
pub trait AssetSource: 'static + Send + Sync {
/// Load the given asset from the source path.
fn load(&self, path: &str) -> Result<Cow<[u8]>>;
/// List the assets at the given path.
fn list(&self, path: &str) -> Result<Vec<SharedString>>;
}
@ -26,15 +30,19 @@ impl AssetSource for () {
}
}
/// A unique identifier for the image cache
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct ImageId(usize);
/// A cached and processed image.
pub struct ImageData {
/// The ID associated with this image
pub id: ImageId,
data: ImageBuffer<Bgra<u8>, Vec<u8>>,
}
impl ImageData {
/// Create a new image from the given data.
pub fn new(data: ImageBuffer<Bgra<u8>, Vec<u8>>) -> Self {
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
@ -44,10 +52,12 @@ impl ImageData {
}
}
/// Convert this image into a byte slice.
pub fn as_bytes(&self) -> &[u8] {
&self.data
}
/// Get the size of this image, in pixels
pub fn size(&self) -> Size<DevicePixels> {
let (width, height) = self.data.dimensions();
size(width.into(), height.into())

View file

@ -2,6 +2,7 @@ use anyhow::bail;
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 {
let r = ((hex >> 16) & 0xFF) as f32 / 255.0;
let g = ((hex >> 8) & 0xFF) as f32 / 255.0;
@ -9,6 +10,7 @@ pub fn rgb<C: From<Rgba>>(hex: u32) -> C {
Rgba { r, g, b, a: 1.0 }.into()
}
/// Convert an RGBA hex color code number to [`Rgba`]
pub fn rgba(hex: u32) -> Rgba {
let r = ((hex >> 24) & 0xFF) as f32 / 255.0;
let g = ((hex >> 16) & 0xFF) as f32 / 255.0;
@ -17,11 +19,16 @@ pub fn rgba(hex: u32) -> Rgba {
Rgba { r, g, b, a }
}
/// An RGBA color
#[derive(PartialEq, Clone, Copy, Default)]
pub struct Rgba {
/// The red component of the color, in the range 0.0 to 1.0
pub r: f32,
/// The green component of the color, in the range 0.0 to 1.0
pub g: f32,
/// The blue component of the color, in the range 0.0 to 1.0
pub b: f32,
/// The alpha component of the color, in the range 0.0 to 1.0
pub a: f32,
}
@ -32,6 +39,8 @@ 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
@ -165,12 +174,20 @@ impl TryFrom<&'_ str> for Rgba {
}
}
/// An HSLA color
#[derive(Default, Copy, Clone, Debug)]
#[repr(C)]
pub struct Hsla {
/// Hue, in a range from 0 to 1
pub h: f32,
/// Saturation, in a range from 0 to 1
pub s: f32,
/// Lightness, in a range from 0 to 1
pub l: f32,
/// Alpha, in a range from 0 to 1
pub a: f32,
}
@ -203,38 +220,9 @@ impl Ord for Hsla {
}
}
impl Hsla {
pub fn to_rgb(self) -> Rgba {
self.into()
}
pub fn red() -> Self {
red()
}
pub fn green() -> Self {
green()
}
pub fn blue() -> Self {
blue()
}
pub fn black() -> Self {
black()
}
pub fn white() -> Self {
white()
}
pub fn transparent_black() -> Self {
transparent_black()
}
}
impl Eq for Hsla {}
/// Construct an [`Hsla`] object from plain values
pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
Hsla {
h: h.clamp(0., 1.),
@ -244,6 +232,7 @@ pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
}
}
/// Pure black in [`Hsla`]
pub fn black() -> Hsla {
Hsla {
h: 0.,
@ -253,6 +242,7 @@ pub fn black() -> Hsla {
}
}
/// Transparent black in [`Hsla`]
pub fn transparent_black() -> Hsla {
Hsla {
h: 0.,
@ -262,6 +252,7 @@ pub fn transparent_black() -> Hsla {
}
}
/// Pure white in [`Hsla`]
pub fn white() -> Hsla {
Hsla {
h: 0.,
@ -271,6 +262,7 @@ pub fn white() -> Hsla {
}
}
/// The color red in [`Hsla`]
pub fn red() -> Hsla {
Hsla {
h: 0.,
@ -280,6 +272,7 @@ pub fn red() -> Hsla {
}
}
/// The color blue in [`Hsla`]
pub fn blue() -> Hsla {
Hsla {
h: 0.6,
@ -289,6 +282,7 @@ pub fn blue() -> Hsla {
}
}
/// The color green in [`Hsla`]
pub fn green() -> Hsla {
Hsla {
h: 0.33,
@ -298,6 +292,7 @@ pub fn green() -> Hsla {
}
}
/// The color yellow in [`Hsla`]
pub fn yellow() -> Hsla {
Hsla {
h: 0.16,
@ -308,6 +303,41 @@ pub fn yellow() -> Hsla {
}
impl Hsla {
/// Converts this HSLA color to an RGBA color.
pub fn to_rgb(self) -> Rgba {
self.into()
}
/// The color red
pub fn red() -> Self {
red()
}
/// The color green
pub fn green() -> Self {
green()
}
/// The color blue
pub fn blue() -> Self {
blue()
}
/// The color black
pub fn black() -> Self {
black()
}
/// The color white
pub fn white() -> Self {
white()
}
/// The color transparent black
pub fn transparent_black() -> Self {
transparent_black()
}
/// Returns true if the HSLA color is fully transparent, false otherwise.
pub fn is_transparent(&self) -> bool {
self.a == 0.0
@ -339,6 +369,7 @@ impl Hsla {
}
}
/// Returns a new HSLA color with the same hue, and lightness, but with no saturation.
pub fn grayscale(&self) -> Self {
Hsla {
h: self.h,

View file

@ -1,26 +1,69 @@
//! Elements are the workhorses of GPUI. They are responsible for laying out and painting all of
//! the contents of a window. Elements form a tree and are laid out according to the web layout
//! standards as implemented by [taffy](https://github.com/DioxusLabs/taffy). Most of the time,
//! you won't need to interact with this module or these APIs directly. Elements provide their
//! own APIs and GPUI, or other element implementation, uses the APIs in this module to convert
//! that element tree into the pixels you see on the screen.
//!
//! # Element Basics
//!
//! Elements are constructed by calling [`Render::render()`] on the root view of the window, which
//! which recursively constructs the element tree from the current state of the application,.
//! These elements are then laid out by Taffy, and painted to the screen according to their own
//! implementation of [`Element::paint()`]. Before the start of the next frame, the entire element
//! tree and any callbacks they have registered with GPUI are dropped and the process repeats.
//!
//! But some state is too simple and voluminous to store in every view that needs it, e.g.
//! whether a hover has been started or not. For this, GPUI provides the [`Element::State`], associated type.
//! If an element returns an [`ElementId`] from [`IntoElement::element_id()`], and that element id
//! appears in the same place relative to other views and ElementIds in the frame, then the previous
//! frame's state will be passed to the element's layout and paint methods.
//!
//! # Implementing your own elements
//!
//! Elements are intended to be the low level, imperative API to GPUI. They are responsible for upholding,
//! or breaking, GPUI's features as they deem necessary. As an example, most GPUI elements are expected
//! to stay in the bounds that their parent element gives them. But with [`WindowContext::break_content_mask`],
//! you can ignore this restriction and paint anywhere inside of the window's bounds. This is useful for overlays
//! and popups and anything else that shows up 'on top' of other elements.
//! With great power, comes great responsibility.
//!
//! However, most of the time, you won't need to implement your own elements. GPUI provides a number of
//! elements that should cover most common use cases out of the box and it's recommended that you use those
//! to construct `components`, using the [`RenderOnce`] trait and the `#[derive(IntoElement)]` macro. Only implement
//! elements when you need to take manual control of the layout and painting process, such as when using
//! your own custom layout algorithm or rendering a code editor.
use crate::{
util::FluentBuilder, ArenaBox, AvailableSpace, BorrowWindow, Bounds, ElementId, LayoutId,
util::FluentBuilder, ArenaBox, AvailableSpace, Bounds, ElementContext, ElementId, LayoutId,
Pixels, Point, Size, ViewContext, WindowContext, ELEMENT_ARENA,
};
use derive_more::{Deref, DerefMut};
pub(crate) use smallvec::SmallVec;
use std::{any::Any, fmt::Debug};
use std::{any::Any, fmt::Debug, ops::DerefMut};
/// Implemented by types that participate in laying out and painting the contents of a window.
/// Elements form a tree and are laid out according to web-based layout rules.
/// Rather than calling methods on implementers of this trait directly, you'll usually call `into_any` to convert them into an AnyElement, which manages state internally.
/// You can create custom elements by implementing this trait.
/// Elements form a tree and are laid out according to web-based layout rules, as implemented by Taffy.
/// You can create custom elements by implementing this trait, see the module-level documentation
/// for more details.
pub trait Element: 'static + IntoElement {
/// The type of state to store for this element between frames. See the module-level documentation
/// for details.
type State: 'static;
/// Before an element can be painted, we need to know where it's going to be and how big it is.
/// Use this method to request a layout from Taffy and initialize the element's state.
fn request_layout(
&mut self,
state: Option<Self::State>,
cx: &mut WindowContext,
cx: &mut ElementContext,
) -> (LayoutId, Self::State);
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext);
/// Once layout has been completed, this method will be called to paint the element to the screen.
/// The state argument is the same state that was returned from [`Element::request_layout()`].
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut ElementContext);
/// Convert this element into a dynamically-typed [`AnyElement`].
fn into_any(self) -> AnyElement {
AnyElement::new(self)
}
@ -29,6 +72,7 @@ pub trait Element: 'static + IntoElement {
/// Implemented by any type that can be converted into an element.
pub trait IntoElement: Sized {
/// The specific type of element into which the implementing type is converted.
/// Useful for converting other types into elements automatically, like Strings
type Element: Element;
/// The [`ElementId`] of self once converted into an [`Element`].
@ -51,8 +95,8 @@ pub trait IntoElement: Sized {
self,
origin: Point<Pixels>,
available_space: Size<T>,
cx: &mut WindowContext,
f: impl FnOnce(&mut <Self::Element as Element>::State, &mut WindowContext) -> R,
cx: &mut ElementContext,
f: impl FnOnce(&mut <Self::Element as Element>::State, &mut ElementContext) -> R,
) -> R
where
T: Clone + Default + Debug + Into<AvailableSpace>,
@ -81,7 +125,10 @@ pub trait IntoElement: Sized {
impl<T: IntoElement> FluentBuilder for T {}
/// An object that can be drawn to the screen. This is the trait that distinguishes `Views` from
/// models. Views are drawn to the screen and care about the current window's state, models are not and do not.
pub trait Render: 'static + Sized {
/// Render this view into an element tree.
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement;
}
@ -92,35 +139,49 @@ impl Render for () {
}
/// You can derive [`IntoElement`] on any type that implements this trait.
/// It is used to allow views to be expressed in terms of abstract data.
/// 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
/// you to invoke this pattern, without breaking the fluent builder pattern of
/// the element APIs.
pub trait RenderOnce: 'static {
/// Render this component into an element tree. Note that this method
/// takes ownership of self, as compared to [`Render::render()`] method
/// which takes a mutable reference.
fn render(self, cx: &mut WindowContext) -> impl IntoElement;
}
/// This is a helper trait to provide a uniform interface for constructing elements that
/// can accept any number of any kind of child elements
pub trait ParentElement {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]>;
/// Extend this element's children with the given child elements.
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>);
/// Add a single child element to this element.
fn child(mut self, child: impl IntoElement) -> Self
where
Self: Sized,
{
self.children_mut().push(child.into_element().into_any());
self.extend(std::iter::once(child.into_element().into_any()));
self
}
/// Add multiple child elements to this element.
fn children(mut self, children: impl IntoIterator<Item = impl IntoElement>) -> Self
where
Self: Sized,
{
self.children_mut()
.extend(children.into_iter().map(|child| child.into_any_element()));
self.extend(children.into_iter().map(|child| child.into_any_element()));
self
}
}
/// An element for rendering components. An implementation detail of the [`IntoElement`] derive macro
/// for [`RenderOnce`]
#[doc(hidden)]
pub struct Component<C: RenderOnce>(Option<C>);
impl<C: RenderOnce> Component<C> {
/// Create a new component from the given RenderOnce type.
pub fn new(component: C) -> Self {
Component(Some(component))
}
@ -132,14 +193,19 @@ impl<C: RenderOnce> Element for Component<C> {
fn request_layout(
&mut self,
_: Option<Self::State>,
cx: &mut WindowContext,
cx: &mut ElementContext,
) -> (LayoutId, Self::State) {
let mut element = self.0.take().unwrap().render(cx).into_any_element();
let mut element = self
.0
.take()
.unwrap()
.render(cx.deref_mut())
.into_any_element();
let layout_id = element.request_layout(cx);
(layout_id, element)
}
fn paint(&mut self, _: Bounds<Pixels>, element: &mut Self::State, cx: &mut WindowContext) {
fn paint(&mut self, _: Bounds<Pixels>, element: &mut Self::State, cx: &mut ElementContext) {
element.paint(cx)
}
}
@ -156,31 +222,33 @@ impl<C: RenderOnce> IntoElement for Component<C> {
}
}
/// A globally unique identifier for an element, used to track state across frames.
#[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)]
pub struct GlobalElementId(SmallVec<[ElementId; 32]>);
pub(crate) struct GlobalElementId(SmallVec<[ElementId; 32]>);
trait ElementObject {
fn element_id(&self) -> Option<ElementId>;
fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId;
fn request_layout(&mut self, cx: &mut ElementContext) -> LayoutId;
fn paint(&mut self, cx: &mut WindowContext);
fn paint(&mut self, cx: &mut ElementContext);
fn measure(
&mut self,
available_space: Size<AvailableSpace>,
cx: &mut WindowContext,
cx: &mut ElementContext,
) -> Size<Pixels>;
fn draw(
&mut self,
origin: Point<Pixels>,
available_space: Size<AvailableSpace>,
cx: &mut WindowContext,
cx: &mut ElementContext,
);
}
pub struct DrawableElement<E: Element> {
/// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window.
pub(crate) struct DrawableElement<E: Element> {
element: Option<E>,
phase: ElementDrawPhase<E::State>,
}
@ -213,7 +281,7 @@ impl<E: Element> DrawableElement<E> {
self.element.as_ref()?.element_id()
}
fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId {
fn request_layout(&mut self, cx: &mut ElementContext) -> LayoutId {
let (layout_id, frame_state) = if let Some(id) = self.element.as_ref().unwrap().element_id()
{
let layout_id = cx.with_element_state(id, |element_state, cx| {
@ -235,7 +303,7 @@ impl<E: Element> DrawableElement<E> {
layout_id
}
fn paint(mut self, cx: &mut WindowContext) -> Option<E::State> {
fn paint(mut self, cx: &mut ElementContext) -> Option<E::State> {
match self.phase {
ElementDrawPhase::LayoutRequested {
layout_id,
@ -280,7 +348,7 @@ impl<E: Element> DrawableElement<E> {
fn measure(
&mut self,
available_space: Size<AvailableSpace>,
cx: &mut WindowContext,
cx: &mut ElementContext,
) -> Size<Pixels> {
if matches!(&self.phase, ElementDrawPhase::Start) {
self.request_layout(cx);
@ -321,7 +389,7 @@ impl<E: Element> DrawableElement<E> {
mut self,
origin: Point<Pixels>,
available_space: Size<AvailableSpace>,
cx: &mut WindowContext,
cx: &mut ElementContext,
) -> Option<E::State> {
self.measure(available_space, cx);
cx.with_absolute_element_offset(origin, |cx| self.paint(cx))
@ -337,18 +405,18 @@ where
self.as_ref().unwrap().element_id()
}
fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId {
fn request_layout(&mut self, cx: &mut ElementContext) -> LayoutId {
DrawableElement::request_layout(self.as_mut().unwrap(), cx)
}
fn paint(&mut self, cx: &mut WindowContext) {
fn paint(&mut self, cx: &mut ElementContext) {
DrawableElement::paint(self.take().unwrap(), cx);
}
fn measure(
&mut self,
available_space: Size<AvailableSpace>,
cx: &mut WindowContext,
cx: &mut ElementContext,
) -> Size<Pixels> {
DrawableElement::measure(self.as_mut().unwrap(), available_space, cx)
}
@ -357,16 +425,17 @@ where
&mut self,
origin: Point<Pixels>,
available_space: Size<AvailableSpace>,
cx: &mut WindowContext,
cx: &mut ElementContext,
) {
DrawableElement::draw(self.take().unwrap(), origin, available_space, cx);
}
}
/// A dynamically typed element that can be used to store any element type.
pub struct AnyElement(ArenaBox<dyn ElementObject>);
impl AnyElement {
pub fn new<E>(element: E) -> Self
pub(crate) fn new<E>(element: E) -> Self
where
E: 'static + Element,
E::State: Any,
@ -377,11 +446,14 @@ impl AnyElement {
AnyElement(element)
}
pub fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId {
/// Request the layout ID of the element stored in this `AnyElement`.
/// Used for laying out child elements in a parent element.
pub fn request_layout(&mut self, cx: &mut ElementContext) -> LayoutId {
self.0.request_layout(cx)
}
pub fn paint(&mut self, cx: &mut WindowContext) {
/// Paints the element stored in this `AnyElement`.
pub fn paint(&mut self, cx: &mut ElementContext) {
self.0.paint(cx)
}
@ -389,7 +461,7 @@ impl AnyElement {
pub fn measure(
&mut self,
available_space: Size<AvailableSpace>,
cx: &mut WindowContext,
cx: &mut ElementContext,
) -> Size<Pixels> {
self.0.measure(available_space, cx)
}
@ -399,11 +471,12 @@ impl AnyElement {
&mut self,
origin: Point<Pixels>,
available_space: Size<AvailableSpace>,
cx: &mut WindowContext,
cx: &mut ElementContext,
) {
self.0.draw(origin, available_space, cx)
}
/// Returns the element ID of the element stored in this `AnyElement`, if any.
pub fn inner_id(&self) -> Option<ElementId> {
self.0.element_id()
}
@ -415,13 +488,13 @@ impl Element for AnyElement {
fn request_layout(
&mut self,
_: Option<Self::State>,
cx: &mut WindowContext,
cx: &mut ElementContext,
) -> (LayoutId, Self::State) {
let layout_id = self.request_layout(cx);
(layout_id, ())
}
fn paint(&mut self, _: Bounds<Pixels>, _: &mut Self::State, cx: &mut WindowContext) {
fn paint(&mut self, _: Bounds<Pixels>, _: &mut Self::State, cx: &mut ElementContext) {
self.paint(cx)
}
}
@ -463,7 +536,7 @@ impl Element for () {
fn request_layout(
&mut self,
_state: Option<Self::State>,
cx: &mut WindowContext,
cx: &mut ElementContext,
) -> (LayoutId, Self::State) {
(cx.request_layout(&crate::Style::default(), None), ())
}
@ -472,7 +545,7 @@ impl Element for () {
&mut self,
_bounds: Bounds<Pixels>,
_state: &mut Self::State,
_cx: &mut WindowContext,
_cx: &mut ElementContext,
) {
}
}

View file

@ -1,16 +1,20 @@
use refineable::Refineable as _;
use crate::{Bounds, Element, IntoElement, Pixels, Style, StyleRefinement, Styled, WindowContext};
use crate::{Bounds, Element, ElementContext, IntoElement, Pixels, Style, StyleRefinement, Styled};
pub fn canvas(callback: impl 'static + FnOnce(&Bounds<Pixels>, &mut WindowContext)) -> Canvas {
/// Construct a canvas element with the given paint callback.
/// Useful for adding short term custom drawing to a view.
pub fn canvas(callback: impl 'static + FnOnce(&Bounds<Pixels>, &mut ElementContext)) -> Canvas {
Canvas {
paint_callback: Some(Box::new(callback)),
style: StyleRefinement::default(),
}
}
/// A canvas element, meant for accessing the low level paint API without defining a whole
/// custom element
pub struct Canvas {
paint_callback: Option<Box<dyn FnOnce(&Bounds<Pixels>, &mut WindowContext)>>,
paint_callback: Option<Box<dyn FnOnce(&Bounds<Pixels>, &mut ElementContext)>>,
style: StyleRefinement,
}
@ -32,7 +36,7 @@ impl Element for Canvas {
fn request_layout(
&mut self,
_: Option<Self::State>,
cx: &mut WindowContext,
cx: &mut ElementContext,
) -> (crate::LayoutId, Self::State) {
let mut style = Style::default();
style.refine(&self.style);
@ -40,7 +44,7 @@ impl Element for Canvas {
(layout_id, style)
}
fn paint(&mut self, bounds: Bounds<Pixels>, style: &mut Style, cx: &mut WindowContext) {
fn paint(&mut self, bounds: Bounds<Pixels>, style: &mut Style, cx: &mut ElementContext) {
style.paint(bounds, cx, |cx| {
(self.paint_callback.take().unwrap())(&bounds, cx)
});

File diff suppressed because it is too large Load diff

View file

@ -1,19 +1,23 @@
use std::sync::Arc;
use crate::{
point, size, BorrowWindow, Bounds, DevicePixels, Element, ImageData, InteractiveElement,
point, size, Bounds, DevicePixels, Element, ElementContext, ImageData, InteractiveElement,
InteractiveElementState, Interactivity, IntoElement, LayoutId, Pixels, SharedUrl, Size,
StyleRefinement, Styled, WindowContext,
StyleRefinement, Styled,
};
use futures::FutureExt;
use media::core_video::CVImageBuffer;
use util::ResultExt;
/// A source of image content.
#[derive(Clone, Debug)]
pub enum ImageSource {
/// Image content will be loaded from provided URI at render time.
Uri(SharedUrl),
/// Cached image data
Data(Arc<ImageData>),
// TODO: move surface definitions into mac platform module
/// A CoreVideo image buffer
Surface(CVImageBuffer),
}
@ -47,12 +51,14 @@ impl From<CVImageBuffer> for ImageSource {
}
}
/// An image element.
pub struct Img {
interactivity: Interactivity,
source: ImageSource,
grayscale: bool,
}
/// Create a new image element.
pub fn img(source: impl Into<ImageSource>) -> Img {
Img {
interactivity: Interactivity::default(),
@ -62,6 +68,7 @@ pub fn img(source: impl Into<ImageSource>) -> Img {
}
impl Img {
/// Set the image to be displayed in grayscale.
pub fn grayscale(mut self, grayscale: bool) -> Self {
self.grayscale = grayscale;
self
@ -74,7 +81,7 @@ impl Element for Img {
fn request_layout(
&mut self,
element_state: Option<Self::State>,
cx: &mut WindowContext,
cx: &mut ElementContext,
) -> (LayoutId, Self::State) {
self.interactivity
.layout(element_state, cx, |style, cx| cx.request_layout(&style, []))
@ -84,7 +91,7 @@ impl Element for Img {
&mut self,
bounds: Bounds<Pixels>,
element_state: &mut Self::State,
cx: &mut WindowContext,
cx: &mut ElementContext,
) {
let source = self.source.clone();
self.interactivity.paint(

View file

@ -1,13 +1,22 @@
//! A list element that can be used to render a large number of differently sized elements
//! efficiently. Clients of this API need to ensure that elements outside of the scrolled
//! area do not change their height for this element to function correctly. In order to minimize
//! re-renders, this element's state is stored intrusively on your own views, so that your code
//! can coordinate directly with the list element's cached state.
//!
//! If all of your elements are the same height, see [`UniformList`] for a simpler API
use crate::{
point, px, AnyElement, AvailableSpace, BorrowAppContext, BorrowWindow, Bounds, ContentMask,
DispatchPhase, Element, IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style,
StyleRefinement, Styled, WindowContext,
point, px, AnyElement, AvailableSpace, Bounds, ContentMask, DispatchPhase, Element,
IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style, StyleRefinement, Styled,
WindowContext,
};
use collections::VecDeque;
use refineable::Refineable as _;
use std::{cell::RefCell, ops::Range, rc::Rc};
use sum_tree::{Bias, SumTree};
/// Construct a new list element
pub fn list(state: ListState) -> List {
List {
state,
@ -15,11 +24,13 @@ pub fn list(state: ListState) -> List {
}
}
/// A list element
pub struct List {
state: ListState,
style: StyleRefinement,
}
/// The list state that views must hold on behalf of the list element.
#[derive(Clone)]
pub struct ListState(Rc<RefCell<StateInner>>);
@ -35,15 +46,24 @@ struct StateInner {
scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut WindowContext)>>,
}
/// Whether the list is scrolling from top to bottom or bottom to top.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ListAlignment {
/// The list is scrolling from top to bottom, like most lists.
Top,
/// The list is scrolling from bottom to top, like a chat log.
Bottom,
}
/// A scroll event that has been converted to be in terms of the list's items.
pub struct ListScrollEvent {
/// The range of items currently visible in the list, after applying the scroll event.
pub visible_range: Range<usize>,
/// The number of items that are currently visible in the list, after applying the scroll event.
pub count: usize,
/// Whether the list has been scrolled.
pub is_scrolled: bool,
}
@ -74,6 +94,11 @@ struct UnrenderedCount(usize);
struct Height(Pixels);
impl ListState {
/// Construct a new list state, for storage on a view.
///
/// the overdraw parameter controls how much extra space is rendered
/// above and below the visible area. This can help ensure that the list
/// doesn't flicker or pop in when scrolling.
pub fn new<F>(
element_count: usize,
orientation: ListAlignment,
@ -111,10 +136,13 @@ impl ListState {
.extend((0..element_count).map(|_| ListItem::Unrendered), &());
}
/// The number of items in this list.
pub fn item_count(&self) -> usize {
self.0.borrow().items.summary().count
}
/// Register with the list state that the items in `old_range` have been replaced
/// by `count` new items that must be recalculated.
pub fn splice(&self, old_range: Range<usize>, count: usize) {
let state = &mut *self.0.borrow_mut();
@ -141,6 +169,7 @@ impl ListState {
state.items = new_heights;
}
/// Set a handler that will be called when the list is scrolled.
pub fn set_scroll_handler(
&self,
handler: impl FnMut(&ListScrollEvent, &mut WindowContext) + 'static,
@ -148,10 +177,12 @@ impl ListState {
self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
}
/// Get the current scroll offset, in terms of the list's items.
pub fn logical_scroll_top(&self) -> ListOffset {
self.0.borrow().logical_scroll_top()
}
/// Scroll the list to the given offset
pub fn scroll_to(&self, mut scroll_top: ListOffset) {
let state = &mut *self.0.borrow_mut();
let item_count = state.items.summary().count;
@ -163,6 +194,7 @@ impl ListState {
state.logical_scroll_top = Some(scroll_top);
}
/// Scroll the list to the given item, such that the item is fully visible.
pub fn scroll_to_reveal_item(&self, ix: usize) {
let state = &mut *self.0.borrow_mut();
@ -193,7 +225,8 @@ impl ListState {
state.logical_scroll_top = Some(scroll_top);
}
/// Get the bounds for the given item in window coordinates.
/// Get the bounds for the given item in window coordinates, if it's
/// been rendered.
pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
let state = &*self.0.borrow();
@ -310,9 +343,13 @@ impl std::fmt::Debug for ListItem {
}
}
/// An offset into the list's items, in terms of the item index and the number
/// of pixels off the top left of the item.
#[derive(Debug, Clone, Copy, Default)]
pub struct ListOffset {
/// The index of an item in the list
pub item_ix: usize,
/// The number of pixels to offset from the item index.
pub offset_in_item: Pixels,
}
@ -322,7 +359,7 @@ impl Element for List {
fn request_layout(
&mut self,
_state: Option<Self::State>,
cx: &mut crate::WindowContext,
cx: &mut crate::ElementContext,
) -> (crate::LayoutId, Self::State) {
let mut style = Style::default();
style.refine(&self.style);
@ -336,7 +373,7 @@ impl Element for List {
&mut self,
bounds: Bounds<crate::Pixels>,
_state: &mut Self::State,
cx: &mut crate::WindowContext,
cx: &mut crate::ElementContext,
) {
let state = &mut *self.state.0.borrow_mut();

View file

@ -2,14 +2,17 @@ use smallvec::SmallVec;
use taffy::style::{Display, Position};
use crate::{
point, AnyElement, BorrowWindow, Bounds, Element, IntoElement, LayoutId, ParentElement, Pixels,
Point, Size, Style, WindowContext,
point, AnyElement, Bounds, Element, ElementContext, IntoElement, LayoutId, ParentElement,
Pixels, Point, Size, Style,
};
/// The state that the overlay element uses to track its children.
pub struct OverlayState {
child_layout_ids: SmallVec<[LayoutId; 4]>,
}
/// An overlay element that can be used to display UI that
/// floats on top of other UI elements.
pub struct Overlay {
children: SmallVec<[AnyElement; 2]>,
anchor_corner: AnchorCorner,
@ -60,8 +63,8 @@ impl Overlay {
}
impl ParentElement for Overlay {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
&mut self.children
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
self.children.extend(elements)
}
}
@ -71,7 +74,7 @@ impl Element for Overlay {
fn request_layout(
&mut self,
_: Option<Self::State>,
cx: &mut WindowContext,
cx: &mut ElementContext,
) -> (crate::LayoutId, Self::State) {
let child_layout_ids = self
.children
@ -94,7 +97,7 @@ impl Element for Overlay {
&mut self,
bounds: crate::Bounds<crate::Pixels>,
element_state: &mut Self::State,
cx: &mut WindowContext,
cx: &mut ElementContext,
) {
if element_state.child_layout_ids.is_empty() {
return;
@ -191,15 +194,21 @@ enum Axis {
Vertical,
}
/// Which algorithm to use when fitting the overlay to be inside the window.
#[derive(Copy, Clone, PartialEq)]
pub enum OverlayFitMode {
/// Snap the overlay to the window edge
SnapToWindow,
/// Switch which corner anchor this overlay is attached to
SwitchAnchor,
}
/// Which algorithm to use when positioning the overlay.
#[derive(Copy, Clone, PartialEq)]
pub enum OverlayPositionMode {
/// Position the overlay relative to the window
Window,
/// Position the overlay relative to its parent
Local,
}
@ -226,11 +235,16 @@ impl OverlayPositionMode {
}
}
/// Which corner of the overlay should be considered the anchor.
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum AnchorCorner {
/// The top left corner
TopLeft,
/// The top right corner
TopRight,
/// The bottom left corner
BottomLeft,
/// The bottom right corner
BottomRight,
}
@ -255,6 +269,7 @@ impl AnchorCorner {
Bounds { origin, size }
}
/// Get the point corresponding to this anchor corner in `bounds`.
pub fn corner(&self, bounds: Bounds<Pixels>) -> Point<Pixels> {
match self {
Self::TopLeft => bounds.origin,

View file

@ -1,14 +1,16 @@
use crate::{
Bounds, Element, ElementId, InteractiveElement, InteractiveElementState, Interactivity,
IntoElement, LayoutId, Pixels, SharedString, StyleRefinement, Styled, WindowContext,
Bounds, Element, ElementContext, ElementId, InteractiveElement, InteractiveElementState,
Interactivity, IntoElement, LayoutId, Pixels, SharedString, StyleRefinement, Styled,
};
use util::ResultExt;
/// An SVG element.
pub struct Svg {
interactivity: Interactivity,
path: Option<SharedString>,
}
/// Create a new SVG element.
pub fn svg() -> Svg {
Svg {
interactivity: Interactivity::default(),
@ -17,6 +19,7 @@ pub fn svg() -> Svg {
}
impl Svg {
/// Set the path to the SVG file for this element.
pub fn path(mut self, path: impl Into<SharedString>) -> Self {
self.path = Some(path.into());
self
@ -29,7 +32,7 @@ impl Element for Svg {
fn request_layout(
&mut self,
element_state: Option<Self::State>,
cx: &mut WindowContext,
cx: &mut ElementContext,
) -> (LayoutId, Self::State) {
self.interactivity.layout(element_state, cx, |style, cx| {
cx.request_layout(&style, None)
@ -40,7 +43,7 @@ impl Element for Svg {
&mut self,
bounds: Bounds<Pixels>,
element_state: &mut Self::State,
cx: &mut WindowContext,
cx: &mut ElementContext,
) where
Self: Sized,
{

View file

@ -1,12 +1,19 @@
use crate::{
Bounds, DispatchPhase, Element, ElementId, HighlightStyle, IntoElement, LayoutId,
MouseDownEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextRun, TextStyle,
WhiteSpace, WindowContext, WrappedLine,
ActiveTooltip, AnyTooltip, AnyView, Bounds, DispatchPhase, Element, ElementContext, ElementId,
HighlightStyle, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
Point, SharedString, Size, TextRun, TextStyle, WhiteSpace, WindowContext, WrappedLine,
TOOLTIP_DELAY,
};
use anyhow::anyhow;
use parking_lot::{Mutex, MutexGuard};
use smallvec::SmallVec;
use std::{cell::Cell, mem, ops::Range, rc::Rc, sync::Arc};
use std::{
cell::{Cell, RefCell},
mem,
ops::Range,
rc::Rc,
sync::Arc,
};
use util::ResultExt;
impl Element for &'static str {
@ -15,14 +22,14 @@ impl Element for &'static str {
fn request_layout(
&mut self,
_: Option<Self::State>,
cx: &mut WindowContext,
cx: &mut ElementContext,
) -> (LayoutId, Self::State) {
let mut state = TextState::default();
let layout_id = state.layout(SharedString::from(*self), None, cx);
(layout_id, state)
}
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut ElementContext) {
state.paint(bounds, self, cx)
}
}
@ -45,14 +52,14 @@ impl Element for SharedString {
fn request_layout(
&mut self,
_: Option<Self::State>,
cx: &mut WindowContext,
cx: &mut ElementContext,
) -> (LayoutId, Self::State) {
let mut state = TextState::default();
let layout_id = state.layout(self.clone(), None, cx);
(layout_id, state)
}
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut ElementContext) {
let text_str: &str = self.as_ref();
state.paint(bounds, text_str, cx)
}
@ -81,6 +88,7 @@ pub struct StyledText {
}
impl StyledText {
/// Construct a new styled text element from the given string.
pub fn new(text: impl Into<SharedString>) -> Self {
StyledText {
text: text.into(),
@ -88,6 +96,8 @@ impl StyledText {
}
}
/// Set the styling attributes for the given text, as well as
/// as any ranges of text that have had their style customized.
pub fn with_highlights(
mut self,
default_style: &TextStyle,
@ -121,14 +131,14 @@ impl Element for StyledText {
fn request_layout(
&mut self,
_: Option<Self::State>,
cx: &mut WindowContext,
cx: &mut ElementContext,
) -> (LayoutId, Self::State) {
let mut state = TextState::default();
let layout_id = state.layout(self.text.clone(), self.runs.take(), cx);
(layout_id, state)
}
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut ElementContext) {
state.paint(bounds, &self.text, cx)
}
}
@ -145,6 +155,7 @@ impl IntoElement for StyledText {
}
}
#[doc(hidden)]
#[derive(Default, Clone)]
pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
@ -164,7 +175,7 @@ impl TextState {
&mut self,
text: SharedString,
runs: Option<Vec<TextRun>>,
cx: &mut WindowContext,
cx: &mut ElementContext,
) -> LayoutId {
let text_style = cx.text_style();
let font_size = text_style.font_size.to_pixels(cx.rem_size());
@ -239,7 +250,7 @@ impl TextState {
layout_id
}
fn paint(&mut self, bounds: Bounds<Pixels>, text: &str, cx: &mut WindowContext) {
fn paint(&mut self, bounds: Bounds<Pixels>, text: &str, cx: &mut ElementContext) {
let element_state = self.lock();
let element_state = element_state
.as_ref()
@ -284,11 +295,14 @@ impl TextState {
}
}
/// A text element that can be interacted with.
pub struct InteractiveText {
element_id: ElementId,
text: StyledText,
click_listener:
Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut WindowContext<'_>)>>,
hover_listener: Option<Box<dyn Fn(Option<usize>, MouseMoveEvent, &mut WindowContext<'_>)>>,
tooltip_builder: Option<Rc<dyn Fn(usize, &mut WindowContext<'_>) -> Option<AnyView>>>,
clickable_ranges: Vec<Range<usize>>,
}
@ -297,21 +311,30 @@ struct InteractiveTextClickEvent {
mouse_up_index: usize,
}
#[doc(hidden)]
pub struct InteractiveTextState {
text_state: TextState,
mouse_down_index: Rc<Cell<Option<usize>>>,
hovered_index: Rc<Cell<Option<usize>>>,
active_tooltip: Rc<RefCell<Option<ActiveTooltip>>>,
}
/// InteractiveTest is a wrapper around StyledText that adds mouse interactions.
impl InteractiveText {
/// Creates a new InteractiveText from the given text.
pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
Self {
element_id: id.into(),
text,
click_listener: None,
hover_listener: None,
tooltip_builder: None,
clickable_ranges: Vec::new(),
}
}
/// on_click is called when the user clicks on one of the given ranges, passing the index of
/// the clicked range.
pub fn on_click(
mut self,
ranges: Vec<Range<usize>>,
@ -328,6 +351,25 @@ impl InteractiveText {
self.clickable_ranges = ranges;
self
}
/// on_hover is called when the mouse moves over a character within the text, passing the
/// index of the hovered character, or None if the mouse leaves the text.
pub fn on_hover(
mut self,
listener: impl Fn(Option<usize>, MouseMoveEvent, &mut WindowContext<'_>) + 'static,
) -> Self {
self.hover_listener = Some(Box::new(listener));
self
}
/// tooltip lets you specify a tooltip for a given character index in the string.
pub fn tooltip(
mut self,
builder: impl Fn(usize, &mut WindowContext<'_>) -> Option<AnyView> + 'static,
) -> Self {
self.tooltip_builder = Some(Rc::new(builder));
self
}
}
impl Element for InteractiveText {
@ -336,16 +378,21 @@ impl Element for InteractiveText {
fn request_layout(
&mut self,
state: Option<Self::State>,
cx: &mut WindowContext,
cx: &mut ElementContext,
) -> (LayoutId, Self::State) {
if let Some(InteractiveTextState {
mouse_down_index, ..
mouse_down_index,
hovered_index,
active_tooltip,
..
}) = state
{
let (layout_id, text_state) = self.text.request_layout(None, cx);
let element_state = InteractiveTextState {
text_state,
mouse_down_index,
hovered_index,
active_tooltip,
};
(layout_id, element_state)
} else {
@ -353,12 +400,14 @@ impl Element for InteractiveText {
let element_state = InteractiveTextState {
text_state,
mouse_down_index: Rc::default(),
hovered_index: Rc::default(),
active_tooltip: Rc::default(),
};
(layout_id, element_state)
}
}
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut ElementContext) {
if let Some(click_listener) = self.click_listener.take() {
let mouse_position = cx.mouse_position();
if let Some(ix) = state.text_state.index_for_position(bounds, mouse_position) {
@ -408,6 +457,83 @@ impl Element for InteractiveText {
});
}
}
if let Some(hover_listener) = self.hover_listener.take() {
let text_state = state.text_state.clone();
let hovered_index = state.hovered_index.clone();
cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
if phase == DispatchPhase::Bubble {
let current = hovered_index.get();
let updated = text_state.index_for_position(bounds, event.position);
if current != updated {
hovered_index.set(updated);
hover_listener(updated, event.clone(), cx);
cx.refresh();
}
}
});
}
if let Some(tooltip_builder) = self.tooltip_builder.clone() {
let active_tooltip = state.active_tooltip.clone();
let pending_mouse_down = state.mouse_down_index.clone();
let text_state = state.text_state.clone();
cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
let position = text_state.index_for_position(bounds, event.position);
let is_hovered = position.is_some() && pending_mouse_down.get().is_none();
if !is_hovered {
active_tooltip.take();
return;
}
let position = position.unwrap();
if phase != DispatchPhase::Bubble {
return;
}
if active_tooltip.borrow().is_none() {
let task = cx.spawn({
let active_tooltip = active_tooltip.clone();
let tooltip_builder = tooltip_builder.clone();
move |mut cx| async move {
cx.background_executor().timer(TOOLTIP_DELAY).await;
cx.update(|cx| {
let new_tooltip =
tooltip_builder(position, cx).map(|tooltip| ActiveTooltip {
tooltip: Some(AnyTooltip {
view: tooltip,
cursor_offset: cx.mouse_position(),
}),
_task: None,
});
*active_tooltip.borrow_mut() = new_tooltip;
cx.refresh();
})
.ok();
}
});
*active_tooltip.borrow_mut() = Some(ActiveTooltip {
tooltip: None,
_task: Some(task),
});
}
});
let active_tooltip = state.active_tooltip.clone();
cx.on_mouse_event(move |_: &MouseDownEvent, _, _| {
active_tooltip.take();
});
if let Some(tooltip) = state
.active_tooltip
.clone()
.borrow()
.as_ref()
.and_then(|at| at.tooltip.clone())
{
cx.set_tooltip(tooltip);
}
}
self.text.paint(bounds, &mut state.text_state, cx)
}

View file

@ -1,5 +1,11 @@
//! A scrollable list of elements with uniform height, optimized for large lists.
//! Rather than use the full taffy layout system, uniform_list simply measures
//! the first element and then lays out all remaining elements in a line based on that
//! measurement. This is much faster than the full layout system, but only works for
//! elements with uniform height.
use crate::{
point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Element,
point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, Element, ElementContext,
ElementId, InteractiveElement, InteractiveElementState, Interactivity, IntoElement, LayoutId,
Pixels, Render, Size, StyleRefinement, Styled, View, ViewContext, WindowContext,
};
@ -53,6 +59,7 @@ where
}
}
/// A list element for efficiently laying out and displaying a list of uniform-height elements.
pub struct UniformList {
id: ElementId,
item_count: usize,
@ -63,18 +70,22 @@ pub struct UniformList {
scroll_handle: Option<UniformListScrollHandle>,
}
/// A handle for controlling the scroll position of a uniform list.
/// This should be stored in your view and passed to the uniform_list on each frame.
#[derive(Clone, Default)]
pub struct UniformListScrollHandle {
deferred_scroll_to_item: Rc<RefCell<Option<usize>>>,
}
impl UniformListScrollHandle {
/// Create a new scroll handle to bind to a uniform list.
pub fn new() -> Self {
Self {
deferred_scroll_to_item: Rc::new(RefCell::new(None)),
}
}
/// Scroll the list to the given item index.
pub fn scroll_to_item(&mut self, ix: usize) {
self.deferred_scroll_to_item.replace(Some(ix));
}
@ -86,6 +97,7 @@ impl Styled for UniformList {
}
}
#[doc(hidden)]
#[derive(Default)]
pub struct UniformListState {
interactive: InteractiveElementState,
@ -98,7 +110,7 @@ impl Element for UniformList {
fn request_layout(
&mut self,
state: Option<Self::State>,
cx: &mut WindowContext,
cx: &mut ElementContext,
) -> (LayoutId, Self::State) {
let max_items = self.item_count;
let item_size = state
@ -146,7 +158,7 @@ impl Element for UniformList {
&mut self,
bounds: Bounds<crate::Pixels>,
element_state: &mut Self::State,
cx: &mut WindowContext,
cx: &mut ElementContext,
) {
let style =
self.interactivity
@ -262,12 +274,13 @@ impl IntoElement for UniformList {
}
impl UniformList {
/// Selects a specific list item for measurement.
pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
self.item_to_measure_index = item_index.unwrap_or(0);
self
}
fn measure_item(&self, list_width: Option<Pixels>, cx: &mut WindowContext) -> Size<Pixels> {
fn measure_item(&self, list_width: Option<Pixels>, cx: &mut ElementContext) -> Size<Pixels> {
if self.item_count == 0 {
return Size::default();
}
@ -284,6 +297,7 @@ impl UniformList {
item_to_measure.measure(available_space, cx)
}
/// Track and render scroll state of this list with reference to the given scroll handle.
pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self {
self.scroll_handle = Some(handle);
self

View file

@ -21,11 +21,15 @@ use waker_fn::waker_fn;
#[cfg(any(test, feature = "test-support"))]
use rand::rngs::StdRng;
/// A pointer to the executor that is currently running,
/// for spawning background tasks.
#[derive(Clone)]
pub struct BackgroundExecutor {
dispatcher: Arc<dyn PlatformDispatcher>,
}
/// A pointer to the executor that is currently running,
/// for spawning tasks on the main thread.
#[derive(Clone)]
pub struct ForegroundExecutor {
dispatcher: Arc<dyn PlatformDispatcher>,
@ -37,11 +41,14 @@ pub struct ForegroundExecutor {
/// It implements [`Future`] so you can `.await` on it.
///
/// If you drop a task it will be cancelled immediately. Calling [`Task::detach`] allows
/// the task to continue running in the background, but with no way to return a value.
/// the task to continue running, but with no way to return a value.
#[must_use]
#[derive(Debug)]
pub enum Task<T> {
/// A task that is ready to return a value
Ready(Option<T>),
/// A task that is currently running.
Spawned(async_task::Task<T>),
}
@ -87,6 +94,8 @@ impl<T> Future for Task<T> {
}
}
/// A task label is an opaque identifier that you can use to
/// refer to a task in tests.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct TaskLabel(NonZeroUsize);
@ -97,6 +106,7 @@ impl Default for TaskLabel {
}
impl TaskLabel {
/// Construct a new task label.
pub fn new() -> Self {
static NEXT_TASK_LABEL: AtomicUsize = AtomicUsize::new(1);
Self(NEXT_TASK_LABEL.fetch_add(1, SeqCst).try_into().unwrap())
@ -363,6 +373,7 @@ impl BackgroundExecutor {
/// ForegroundExecutor runs things on the main thread.
impl ForegroundExecutor {
/// Creates a new ForegroundExecutor from the given PlatformDispatcher.
pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
Self {
dispatcher,
@ -411,13 +422,14 @@ impl<'a> Scope<'a> {
}
}
/// Spawn a future into this scope.
pub fn spawn<F>(&mut self, f: F)
where
F: Future<Output = ()> + Send + 'a,
{
let tx = self.tx.clone().unwrap();
// Safety: The 'a lifetime is guaranteed to outlive any of these futures because
// SAFETY: The 'a lifetime is guaranteed to outlive any of these futures because
// dropping this `Scope` blocks until all of the futures have resolved.
let f = unsafe {
mem::transmute::<

View file

@ -1,3 +1,31 @@
//! # 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.
//!
//! # Getting started with GPUI
//!
//! TODO!(docs): Write a code sample showing how to create a window and render a simple
//! div
//!
//! # Drawing interesting things
//!
//! 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.
//!
//! # Interacting with your application state
//!
//! TODO!(docs): Expand demo to show GPUI entity interactions, like subscriptions and entities
//! maybe make a network request to show async stuff?
//!
//! # Conclusion
//!
//! TODO!(docs): Wrap up with a conclusion and links to other places? Zed / GPUI website?
//! Discord for chatting about it? Other tutorials or references?
#[macro_use]
mod action;
mod app;
@ -58,10 +86,10 @@ pub use elements::*;
pub use executor::*;
pub use geometry::*;
pub use gpui_macros::{register_action, test, IntoElement, Render};
pub use image_cache::*;
use image_cache::*;
pub use input::*;
pub use interactive::*;
pub use key_dispatch::*;
use key_dispatch::*;
pub use keymap::*;
pub use platform::*;
pub use refineable::*;
@ -73,7 +101,7 @@ pub use smol::Timer;
pub use style::*;
pub use styled::*;
pub use subscription::*;
pub use svg_renderer::*;
use svg_renderer::*;
pub use taffy::{AvailableSpace, LayoutId};
#[cfg(any(test, feature = "test-support"))]
pub use test::*;
@ -82,20 +110,23 @@ pub use util::arc_cow::ArcCow;
pub use view::*;
pub use window::*;
use std::{
any::{Any, TypeId},
borrow::BorrowMut,
};
use std::{any::Any, borrow::BorrowMut};
use taffy::TaffyLayoutEngine;
/// The context trait, allows the different contexts in GPUI to be used
/// interchangeably for certain operations.
pub trait Context {
/// The result type for this context, used for async contexts that
/// can't hold a direct reference to the application context.
type Result<T>;
/// Create a new model in the app context.
fn new_model<T: 'static>(
&mut self,
build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T,
) -> Self::Result<Model<T>>;
/// Update a model in the app context.
fn update_model<T, R>(
&mut self,
handle: &Model<T>,
@ -104,6 +135,7 @@ pub trait Context {
where
T: 'static;
/// Read a model from the app context.
fn read_model<T, R>(
&self,
handle: &Model<T>,
@ -112,10 +144,12 @@ pub trait Context {
where
T: 'static;
/// Update a window for the given handle.
fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
where
F: FnOnce(AnyView, &mut WindowContext<'_>) -> T;
/// Read a window off of the application context.
fn read_window<T, R>(
&self,
window: &WindowHandle<T>,
@ -125,7 +159,10 @@ pub trait Context {
T: 'static;
}
/// This trait is used for the different visual contexts in GPUI that
/// require a window to be present.
pub trait VisualContext: Context {
/// Construct a new view in the window referenced by this context.
fn new_view<V>(
&mut self,
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
@ -133,12 +170,14 @@ pub trait VisualContext: Context {
where
V: 'static + Render;
/// Update a view with the given callback
fn update_view<V: 'static, R>(
&mut self,
view: &View<V>,
update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R,
) -> Self::Result<R>;
/// Replace the root view of a window with a new view.
fn replace_root_view<V>(
&mut self,
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
@ -146,38 +185,42 @@ pub trait VisualContext: Context {
where
V: 'static + Render;
/// Focus a view in the window, if it implements the [`FocusableView`] trait.
fn focus_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
where
V: FocusableView;
/// Dismiss a view in the window, if it implements the [`ManagedView`] trait.
fn dismiss_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
where
V: ManagedView;
}
/// A trait that allows models and views to be interchangeable in certain operations
pub trait Entity<T>: Sealed {
/// The weak reference type for this entity.
type Weak: 'static;
/// The ID for this entity
fn entity_id(&self) -> EntityId;
/// Downgrade this entity to a weak reference.
fn downgrade(&self) -> Self::Weak;
/// Upgrade this entity from a weak reference.
fn upgrade_from(weak: &Self::Weak) -> Option<Self>
where
Self: Sized;
}
/// A trait for tying together the types of a GPUI entity and the events it can
/// emit.
pub trait EventEmitter<E: Any>: 'static {}
pub enum GlobalKey {
Numeric(usize),
View(EntityId),
Type(TypeId),
}
/// A helper trait for auto-implementing certain methods on contexts that
/// can be used interchangeably.
pub trait BorrowAppContext {
fn with_text_style<F, R>(&mut self, style: Option<TextStyleRefinement>, f: F) -> R
where
F: FnOnce(&mut Self) -> R;
/// Set a global value on the context.
fn set_global<T: 'static>(&mut self, global: T);
}
@ -185,26 +228,14 @@ impl<C> BorrowAppContext for C
where
C: BorrowMut<AppContext>,
{
fn with_text_style<F, R>(&mut self, style: Option<TextStyleRefinement>, f: F) -> R
where
F: FnOnce(&mut Self) -> R,
{
if let Some(style) = style {
self.borrow_mut().push_text_style(style);
let result = f(self);
self.borrow_mut().pop_text_style();
result
} else {
f(self)
}
}
fn set_global<G: 'static>(&mut self, global: G) {
self.borrow_mut().set_global(global)
}
}
/// A flatten equivalent for anyhow `Result`s.
pub trait Flatten<T> {
/// Convert this type into a simple `Result<T>`.
fn flatten(self) -> Result<T>;
}

View file

@ -11,12 +11,12 @@ use thiserror::Error;
use util::http::{self, HttpClient};
#[derive(PartialEq, Eq, Hash, Clone)]
pub struct RenderImageParams {
pub(crate) struct RenderImageParams {
pub(crate) image_id: ImageId,
}
#[derive(Debug, Error, Clone)]
pub enum Error {
pub(crate) enum Error {
#[error("http error: {0}")]
Client(#[from] http::Error),
#[error("IO error: {0}")]
@ -42,7 +42,7 @@ impl From<ImageError> for Error {
}
}
pub struct ImageCache {
pub(crate) struct ImageCache {
client: Arc<dyn HttpClient>,
images: Arc<Mutex<HashMap<SharedUrl, FetchImageFuture>>>,
}

View file

@ -1,24 +1,35 @@
use crate::{
AsyncWindowContext, Bounds, Pixels, PlatformInputHandler, View, ViewContext, WindowContext,
};
use crate::{Bounds, InputHandler, Pixels, View, ViewContext, WindowContext};
use std::ops::Range;
/// Implement this trait to allow views to handle textual input when implementing an editor, field, etc.
///
/// Once your view `V` implements this trait, you can use it to construct an [`ElementInputHandler<V>`].
/// Once your view implements this trait, you can use it to construct an [`ElementInputHandler<V>`].
/// This input handler can then be assigned during paint by calling [`WindowContext::handle_input`].
pub trait InputHandler: 'static + Sized {
///
/// See [`InputHandler`] for details on how to implement each method.
pub trait ViewInputHandler: 'static + Sized {
/// See [`InputHandler::text_for_range`] for details
fn text_for_range(&mut self, range: Range<usize>, cx: &mut ViewContext<Self>)
-> Option<String>;
/// See [`InputHandler::selected_text_range`] for details
fn selected_text_range(&mut self, cx: &mut ViewContext<Self>) -> Option<Range<usize>>;
/// See [`InputHandler::marked_text_range`] for details
fn marked_text_range(&self, cx: &mut ViewContext<Self>) -> Option<Range<usize>>;
/// See [`InputHandler::unmark_text`] for details
fn unmark_text(&mut self, cx: &mut ViewContext<Self>);
/// See [`InputHandler::replace_text_in_range`] for details
fn replace_text_in_range(
&mut self,
range: Option<Range<usize>>,
text: &str,
cx: &mut ViewContext<Self>,
);
/// See [`InputHandler::replace_and_mark_text_in_range`] for details
fn replace_and_mark_text_in_range(
&mut self,
range: Option<Range<usize>>,
@ -26,6 +37,8 @@ pub trait InputHandler: 'static + Sized {
new_selected_range: Option<Range<usize>>,
cx: &mut ViewContext<Self>,
);
/// See [`InputHandler::bounds_for_range`] for details
fn bounds_for_range(
&mut self,
range_utf16: Range<usize>,
@ -39,7 +52,6 @@ pub trait InputHandler: 'static + Sized {
pub struct ElementInputHandler<V> {
view: View<V>,
element_bounds: Bounds<Pixels>,
cx: AsyncWindowContext,
}
impl<V: 'static> ElementInputHandler<V> {
@ -47,45 +59,42 @@ impl<V: 'static> ElementInputHandler<V> {
/// containing view.
///
/// [element_paint]: crate::Element::paint
pub fn new(element_bounds: Bounds<Pixels>, view: View<V>, cx: &mut WindowContext) -> Self {
pub fn new(element_bounds: Bounds<Pixels>, view: View<V>) -> Self {
ElementInputHandler {
view,
element_bounds,
cx: cx.to_async(),
}
}
}
impl<V: InputHandler> PlatformInputHandler for ElementInputHandler<V> {
fn selected_text_range(&mut self) -> Option<Range<usize>> {
impl<V: ViewInputHandler> InputHandler for ElementInputHandler<V> {
fn selected_text_range(&mut self, cx: &mut WindowContext) -> Option<Range<usize>> {
self.view
.update(&mut self.cx, |view, cx| view.selected_text_range(cx))
.ok()
.flatten()
.update(cx, |view, cx| view.selected_text_range(cx))
}
fn marked_text_range(&mut self) -> Option<Range<usize>> {
self.view
.update(&mut self.cx, |view, cx| view.marked_text_range(cx))
.ok()
.flatten()
fn marked_text_range(&mut self, cx: &mut WindowContext) -> Option<Range<usize>> {
self.view.update(cx, |view, cx| view.marked_text_range(cx))
}
fn text_for_range(&mut self, range_utf16: Range<usize>) -> Option<String> {
fn text_for_range(
&mut self,
range_utf16: Range<usize>,
cx: &mut WindowContext,
) -> Option<String> {
self.view
.update(&mut self.cx, |view, cx| {
view.text_for_range(range_utf16, cx)
})
.ok()
.flatten()
.update(cx, |view, cx| view.text_for_range(range_utf16, cx))
}
fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
self.view
.update(&mut self.cx, |view, cx| {
view.replace_text_in_range(replacement_range, text, cx)
})
.ok();
fn replace_text_in_range(
&mut self,
replacement_range: Option<Range<usize>>,
text: &str,
cx: &mut WindowContext,
) {
self.view.update(cx, |view, cx| {
view.replace_text_in_range(replacement_range, text, cx)
});
}
fn replace_and_mark_text_in_range(
@ -93,26 +102,24 @@ impl<V: InputHandler> PlatformInputHandler for ElementInputHandler<V> {
range_utf16: Option<Range<usize>>,
new_text: &str,
new_selected_range: Option<Range<usize>>,
cx: &mut WindowContext,
) {
self.view
.update(&mut self.cx, |view, cx| {
view.replace_and_mark_text_in_range(range_utf16, new_text, new_selected_range, cx)
})
.ok();
self.view.update(cx, |view, cx| {
view.replace_and_mark_text_in_range(range_utf16, new_text, new_selected_range, cx)
});
}
fn unmark_text(&mut self) {
self.view
.update(&mut self.cx, |view, cx| view.unmark_text(cx))
.ok();
fn unmark_text(&mut self, cx: &mut WindowContext) {
self.view.update(cx, |view, cx| view.unmark_text(cx));
}
fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
self.view
.update(&mut self.cx, |view, cx| {
view.bounds_for_range(range_utf16, self.element_bounds, cx)
})
.ok()
.flatten()
fn bounds_for_range(
&mut self,
range_utf16: Range<usize>,
cx: &mut WindowContext,
) -> Option<Bounds<Pixels>> {
self.view.update(cx, |view, cx| {
view.bounds_for_range(range_utf16, self.element_bounds, cx)
})
}
}

View file

@ -4,15 +4,25 @@ use crate::{
use smallvec::SmallVec;
use std::{any::Any, fmt::Debug, ops::Deref, path::PathBuf};
/// An event from a platform input source.
pub trait InputEvent: Sealed + 'static {
/// Convert this event into the platform input enum.
fn to_platform_input(self) -> PlatformInput;
}
/// A key event from the platform.
pub trait KeyEvent: InputEvent {}
/// A mouse event from the platform.
pub trait MouseEvent: InputEvent {}
/// The key down event equivalent for the platform.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct KeyDownEvent {
/// The keystroke that was generated.
pub keystroke: Keystroke,
/// Whether the key is currently held down.
pub is_held: bool,
}
@ -24,8 +34,10 @@ impl InputEvent for KeyDownEvent {
}
impl KeyEvent for KeyDownEvent {}
/// The key up event equivalent for the platform.
#[derive(Clone, Debug)]
pub struct KeyUpEvent {
/// The keystroke that was released.
pub keystroke: Keystroke,
}
@ -37,8 +49,10 @@ impl InputEvent for KeyUpEvent {
}
impl KeyEvent for KeyUpEvent {}
/// The modifiers changed event equivalent for the platform.
#[derive(Clone, Debug, Default)]
pub struct ModifiersChangedEvent {
/// The new state of the modifier keys
pub modifiers: Modifiers,
}
@ -62,17 +76,28 @@ impl Deref for ModifiersChangedEvent {
/// Based on the winit enum of the same name.
#[derive(Clone, Copy, Debug, Default)]
pub enum TouchPhase {
/// The touch started.
Started,
/// The touch event is moving.
#[default]
Moved,
/// The touch phase has ended
Ended,
}
/// A mouse down event from the platform
#[derive(Clone, Debug, Default)]
pub struct MouseDownEvent {
/// Which mouse button was pressed.
pub button: MouseButton,
/// The position of the mouse on the window.
pub position: Point<Pixels>,
/// The modifiers that were held down when the mouse was pressed.
pub modifiers: Modifiers,
/// The number of times the button has been clicked.
pub click_count: usize,
}
@ -84,11 +109,19 @@ impl InputEvent for MouseDownEvent {
}
impl MouseEvent for MouseDownEvent {}
/// A mouse up event from the platform
#[derive(Clone, Debug, Default)]
pub struct MouseUpEvent {
/// Which mouse button was released.
pub button: MouseButton,
/// The position of the mouse on the window.
pub position: Point<Pixels>,
/// The modifiers that were held down when the mouse was released.
pub modifiers: Modifiers,
/// The number of times the button has been clicked.
pub click_count: usize,
}
@ -100,21 +133,34 @@ impl InputEvent for MouseUpEvent {
}
impl MouseEvent for MouseUpEvent {}
/// A click event, generated when a mouse button is pressed and released.
#[derive(Clone, Debug, Default)]
pub struct ClickEvent {
/// The mouse event when the button was pressed.
pub down: MouseDownEvent,
/// The mouse event when the button was released.
pub up: MouseUpEvent,
}
/// An enum representing the mouse button that was pressed.
#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
pub enum MouseButton {
/// The left mouse button.
Left,
/// The right mouse button.
Right,
/// The middle mouse button.
Middle,
/// A navigation button, such as back or forward.
Navigate(NavigationDirection),
}
impl MouseButton {
/// Get all the mouse buttons in a list.
pub fn all() -> Vec<Self> {
vec![
MouseButton::Left,
@ -132,9 +178,13 @@ impl Default for MouseButton {
}
}
/// A navigation direction, such as back or forward.
#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
pub enum NavigationDirection {
/// The back button.
Back,
/// The forward button.
Forward,
}
@ -144,10 +194,16 @@ impl Default for NavigationDirection {
}
}
/// A mouse move event from the platform
#[derive(Clone, Debug, Default)]
pub struct MouseMoveEvent {
/// The position of the mouse on the window.
pub position: Point<Pixels>,
/// The mouse button that was pressed, if any.
pub pressed_button: Option<MouseButton>,
/// The modifiers that were held down when the mouse was moved.
pub modifiers: Modifiers,
}
@ -160,16 +216,25 @@ impl InputEvent for MouseMoveEvent {
impl MouseEvent for MouseMoveEvent {}
impl MouseMoveEvent {
/// Returns true if the left mouse button is currently held down.
pub fn dragging(&self) -> bool {
self.pressed_button == Some(MouseButton::Left)
}
}
/// A mouse wheel event from the platform
#[derive(Clone, Debug, Default)]
pub struct ScrollWheelEvent {
/// The position of the mouse on the window.
pub position: Point<Pixels>,
/// The change in scroll wheel position for this event.
pub delta: ScrollDelta,
/// The modifiers that were held down when the mouse was moved.
pub modifiers: Modifiers,
/// The phase of the touch event.
pub touch_phase: TouchPhase,
}
@ -189,9 +254,12 @@ impl Deref for ScrollWheelEvent {
}
}
/// The scroll delta for a scroll wheel event.
#[derive(Clone, Copy, Debug)]
pub enum ScrollDelta {
/// An exact scroll delta in pixels.
Pixels(Point<Pixels>),
/// An inexact scroll delta in lines.
Lines(Point<f32>),
}
@ -202,6 +270,7 @@ impl Default for ScrollDelta {
}
impl ScrollDelta {
/// Returns true if this is a precise scroll delta in pixels.
pub fn precise(&self) -> bool {
match self {
ScrollDelta::Pixels(_) => true,
@ -209,6 +278,7 @@ impl ScrollDelta {
}
}
/// Converts this scroll event into exact pixels.
pub fn pixel_delta(&self, line_height: Pixels) -> Point<Pixels> {
match self {
ScrollDelta::Pixels(delta) => *delta,
@ -216,6 +286,7 @@ impl ScrollDelta {
}
}
/// Combines two scroll deltas into one.
pub fn coalesce(self, other: ScrollDelta) -> ScrollDelta {
match (self, other) {
(ScrollDelta::Pixels(px_a), ScrollDelta::Pixels(px_b)) => {
@ -231,10 +302,15 @@ impl ScrollDelta {
}
}
/// A mouse exit event from the platform, generated when the mouse leaves the window.
/// The position generated should be just outside of the window's bounds.
#[derive(Clone, Debug, Default)]
pub struct MouseExitEvent {
/// The position of the mouse relative to the window.
pub position: Point<Pixels>,
/// The mouse button that was pressed, if any.
pub pressed_button: Option<MouseButton>,
/// The modifiers that were held down when the mouse was moved.
pub modifiers: Modifiers,
}
@ -254,10 +330,12 @@ impl Deref for MouseExitEvent {
}
}
/// A collection of paths from the platform, such as from a file drop.
#[derive(Debug, Clone, Default)]
pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>);
impl ExternalPaths {
/// Convert this collection of paths into a slice.
pub fn paths(&self) -> &[PathBuf] {
&self.0
}
@ -269,18 +347,27 @@ impl Render for ExternalPaths {
}
}
/// A file drop event from the platform, generated when files are dragged and dropped onto the window.
#[derive(Debug, Clone)]
pub enum FileDropEvent {
/// The files have entered the window.
Entered {
/// The position of the mouse relative to the window.
position: Point<Pixels>,
/// The paths of the files that are being dragged.
paths: ExternalPaths,
},
/// The files are being dragged over the window
Pending {
/// The position of the mouse relative to the window.
position: Point<Pixels>,
},
/// The files have been dropped onto the window.
Submit {
/// The position of the mouse relative to the window.
position: Point<Pixels>,
},
/// The user has stopped dragging the files over the window.
Exited,
}
@ -292,40 +379,31 @@ impl InputEvent for FileDropEvent {
}
impl MouseEvent for FileDropEvent {}
/// An enum corresponding to all kinds of platform input events.
#[derive(Clone, Debug)]
pub enum PlatformInput {
/// A key was pressed.
KeyDown(KeyDownEvent),
/// A key was released.
KeyUp(KeyUpEvent),
/// The keyboard modifiers were changed.
ModifiersChanged(ModifiersChangedEvent),
/// The mouse was pressed.
MouseDown(MouseDownEvent),
/// The mouse was released.
MouseUp(MouseUpEvent),
/// The mouse was moved.
MouseMove(MouseMoveEvent),
/// The mouse exited the window.
MouseExited(MouseExitEvent),
/// The scroll wheel was used.
ScrollWheel(ScrollWheelEvent),
/// Files were dragged and dropped onto the window.
FileDrop(FileDropEvent),
}
impl PlatformInput {
pub fn position(&self) -> Option<Point<Pixels>> {
match self {
PlatformInput::KeyDown { .. } => None,
PlatformInput::KeyUp { .. } => None,
PlatformInput::ModifiersChanged { .. } => None,
PlatformInput::MouseDown(event) => Some(event.position),
PlatformInput::MouseUp(event) => Some(event.position),
PlatformInput::MouseMove(event) => Some(event.position),
PlatformInput::MouseExited(event) => Some(event.position),
PlatformInput::ScrollWheel(event) => Some(event.position),
PlatformInput::FileDrop(FileDropEvent::Exited) => None,
PlatformInput::FileDrop(
FileDropEvent::Entered { position, .. }
| FileDropEvent::Pending { position, .. }
| FileDropEvent::Submit { position, .. },
) => Some(*position),
}
}
pub fn mouse_event(&self) -> Option<&dyn Any> {
pub(crate) fn mouse_event(&self) -> Option<&dyn Any> {
match self {
PlatformInput::KeyDown { .. } => None,
PlatformInput::KeyUp { .. } => None,
@ -339,7 +417,7 @@ impl PlatformInput {
}
}
pub fn keyboard_event(&self) -> Option<&dyn Any> {
pub(crate) fn keyboard_event(&self) -> Option<&dyn Any> {
match self {
PlatformInput::KeyDown(event) => Some(event),
PlatformInput::KeyUp(event) => Some(event),

View file

@ -1,6 +1,6 @@
use crate::{
Action, ActionRegistry, DispatchPhase, EntityId, FocusId, KeyBinding, KeyContext, KeyMatch,
Keymap, Keystroke, KeystrokeMatcher, WindowContext,
Action, ActionRegistry, DispatchPhase, ElementContext, EntityId, FocusId, KeyBinding,
KeyContext, KeyMatch, Keymap, Keystroke, KeystrokeMatcher, WindowContext,
};
use collections::FxHashMap;
use parking_lot::Mutex;
@ -13,7 +13,7 @@ use std::{
};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct DispatchNodeId(usize);
pub(crate) struct DispatchNodeId(usize);
pub(crate) struct DispatchTree {
node_stack: Vec<DispatchNodeId>,
@ -36,7 +36,7 @@ pub(crate) struct DispatchNode {
parent: Option<DispatchNodeId>,
}
type KeyListener = Rc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>;
type KeyListener = Rc<dyn Fn(&dyn Any, DispatchPhase, &mut ElementContext)>;
#[derive(Clone)]
pub(crate) struct DispatchActionListener {

View file

@ -2,6 +2,7 @@ use crate::{Action, KeyBindingContextPredicate, KeyMatch, Keystroke};
use anyhow::Result;
use smallvec::SmallVec;
/// A keybinding and it's associated metadata, from the keymap.
pub struct KeyBinding {
pub(crate) action: Box<dyn Action>,
pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
@ -19,10 +20,12 @@ impl Clone for KeyBinding {
}
impl KeyBinding {
/// Construct a new keybinding from the given data.
pub fn new<A: Action>(keystrokes: &str, action: A, context_predicate: Option<&str>) -> Self {
Self::load(keystrokes, Box::new(action), context_predicate).unwrap()
}
/// Load a keybinding from the given raw data.
pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> {
let context = if let Some(context) = context {
Some(KeyBindingContextPredicate::parse(context)?)
@ -42,6 +45,7 @@ impl KeyBinding {
})
}
/// Check if the given keystrokes match this binding.
pub fn match_keystrokes(&self, pending_keystrokes: &[Keystroke]) -> KeyMatch {
if self.keystrokes.as_ref().starts_with(pending_keystrokes) {
// If the binding is completed, push it onto the matches list
@ -55,10 +59,12 @@ impl KeyBinding {
}
}
/// Get the keystrokes associated with this binding
pub fn keystrokes(&self) -> &[Keystroke] {
self.keystrokes.as_slice()
}
/// Get the action associated with this binding
pub fn action(&self) -> &dyn Action {
self.action.as_ref()
}

View file

@ -3,6 +3,10 @@ use anyhow::{anyhow, Result};
use smallvec::SmallVec;
use std::fmt;
/// A datastructure for resolving whether an action should be dispatched
/// at this point in the element tree. Contains a set of identifiers
/// and/or key value pairs representing the current context for the
/// keymap.
#[derive(Clone, Default, Eq, PartialEq, Hash)]
pub struct KeyContext(SmallVec<[ContextEntry; 1]>);
@ -21,6 +25,11 @@ impl<'a> TryFrom<&'a str> for KeyContext {
}
impl KeyContext {
/// Parse a key context from a string.
/// The key context format is very simple:
/// - either a single identifier, such as `StatusBar`
/// - or a key value pair, such as `mode = visible`
/// - separated by whitespace, such as `StatusBar mode = visible`
pub fn parse(source: &str) -> Result<Self> {
let mut context = Self::default();
let source = skip_whitespace(source);
@ -53,14 +62,17 @@ impl KeyContext {
Self::parse_expr(source, context)
}
/// Check if this context is empty.
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
/// Clear this context.
pub fn clear(&mut self) {
self.0.clear();
}
/// Extend this context with another context.
pub fn extend(&mut self, other: &Self) {
for entry in &other.0 {
if !self.contains(&entry.key) {
@ -69,6 +81,7 @@ impl KeyContext {
}
}
/// Add an identifier to this context, if it's not already in this context.
pub fn add<I: Into<SharedString>>(&mut self, identifier: I) {
let key = identifier.into();
@ -77,6 +90,7 @@ impl KeyContext {
}
}
/// Set a key value pair in this context, if it's not already set.
pub fn set<S1: Into<SharedString>, S2: Into<SharedString>>(&mut self, key: S1, value: S2) {
let key = key.into();
if !self.contains(&key) {
@ -87,10 +101,12 @@ impl KeyContext {
}
}
/// Check if this context contains a given identifier or key.
pub fn contains(&self, key: &str) -> bool {
self.0.iter().any(|entry| entry.key.as_ref() == key)
}
/// Get the associated value for a given identifier or key.
pub fn get(&self, key: &str) -> Option<&SharedString> {
self.0
.iter()
@ -117,20 +133,31 @@ impl fmt::Debug for KeyContext {
}
}
/// A datastructure for resolving whether an action should be dispatched
/// Representing a small language for describing which contexts correspond
/// to which actions.
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum KeyBindingContextPredicate {
/// A predicate that will match a given identifier.
Identifier(SharedString),
/// A predicate that will match a given key-value pair.
Equal(SharedString, SharedString),
/// A predicate that will match a given key-value pair not being present.
NotEqual(SharedString, SharedString),
/// A predicate that will match a given predicate appearing below another predicate.
/// in the element tree
Child(
Box<KeyBindingContextPredicate>,
Box<KeyBindingContextPredicate>,
),
/// Predicate that will invert another predicate.
Not(Box<KeyBindingContextPredicate>),
/// A predicate that will match if both of its children match.
And(
Box<KeyBindingContextPredicate>,
Box<KeyBindingContextPredicate>,
),
/// A predicate that will match if either of its children match.
Or(
Box<KeyBindingContextPredicate>,
Box<KeyBindingContextPredicate>,
@ -138,6 +165,34 @@ pub enum KeyBindingContextPredicate {
}
impl KeyBindingContextPredicate {
/// Parse a string in the same format as the keymap's context field.
///
/// A basic equivalence check against a set of identifiers can performed by
/// simply writing a string:
///
/// `StatusBar` -> A predicate that will match a context with the identifier `StatusBar`
///
/// You can also specify a key-value pair:
///
/// `mode == visible` -> A predicate that will match a context with the key `mode`
/// with the value `visible`
///
/// And a logical operations combining these two checks:
///
/// `StatusBar && mode == visible` -> A predicate that will match a context with the
/// identifier `StatusBar` and the key `mode`
/// with the value `visible`
///
///
/// There is also a special child `>` operator that will match a predicate that is
/// below another predicate:
///
/// `StatusBar > mode == visible` -> A predicate that will match a context identifier `StatusBar`
/// and a child context that has the key `mode` with the
/// value `visible`
///
/// This syntax supports `!=`, `||` and `&&` as logical operators.
/// You can also preface an operation or check with a `!` to negate it.
pub fn parse(source: &str) -> Result<Self> {
let source = skip_whitespace(source);
let (predicate, rest) = Self::parse_expr(source, 0)?;
@ -148,6 +203,7 @@ impl KeyBindingContextPredicate {
}
}
/// Eval a predicate against a set of contexts, arranged from lowest to highest.
pub fn eval(&self, contexts: &[KeyContext]) -> bool {
let Some(context) = contexts.last() else {
return false;

View file

@ -6,9 +6,12 @@ use std::{
collections::HashMap,
};
/// An opaque identifier of which version of the keymap is currently active.
/// The keymap's version is changed whenever bindings are added or removed.
#[derive(Copy, Clone, Eq, PartialEq, Default)]
pub struct KeymapVersion(usize);
/// A collection of key bindings for the user's application.
#[derive(Default)]
pub struct Keymap {
bindings: Vec<KeyBinding>,
@ -19,16 +22,19 @@ pub struct Keymap {
}
impl Keymap {
/// Create a new keymap with the given bindings.
pub fn new(bindings: Vec<KeyBinding>) -> Self {
let mut this = Self::default();
this.add_bindings(bindings);
this
}
/// Get the current version of the keymap.
pub fn version(&self) -> KeymapVersion {
self.version
}
/// Add more bindings to the keymap.
pub fn add_bindings<T: IntoIterator<Item = KeyBinding>>(&mut self, bindings: T) {
let no_action_id = (NoAction {}).type_id();
@ -51,6 +57,7 @@ impl Keymap {
self.version.0 += 1;
}
/// Reset this keymap to its initial state.
pub fn clear(&mut self) {
self.bindings.clear();
self.binding_indices_by_action_id.clear();
@ -77,6 +84,7 @@ impl Keymap {
.filter(move |binding| binding.action().partial_eq(action))
}
/// Check if the given binding is enabled, given a certain key context.
pub fn binding_enabled(&self, binding: &KeyBinding, context: &[KeyContext]) -> bool {
// If binding has a context predicate, it must match the current context,
if let Some(predicate) = &binding.context_predicate {

View file

@ -2,7 +2,7 @@ use crate::{Action, KeyContext, Keymap, KeymapVersion, Keystroke};
use parking_lot::Mutex;
use std::sync::Arc;
pub struct KeystrokeMatcher {
pub(crate) struct KeystrokeMatcher {
pending_keystrokes: Vec<Keystroke>,
keymap: Arc<Mutex<Keymap>>,
keymap_version: KeymapVersion,
@ -35,7 +35,7 @@ impl KeystrokeMatcher {
/// - KeyMatch::Complete(matches) =>
/// One or more bindings have received the necessary key presses.
/// Bindings added later will take precedence over earlier bindings.
pub fn match_keystroke(
pub(crate) fn match_keystroke(
&mut self,
keystroke: &Keystroke,
context_stack: &[KeyContext],
@ -73,9 +73,7 @@ impl KeystrokeMatcher {
if !found_actions.is_empty() {
self.pending_keystrokes.clear();
return KeyMatch::Some(found_actions);
}
if let Some(pending_key) = pending_key {
} else if let Some(pending_key) = pending_key {
self.pending_keystrokes.push(pending_key);
KeyMatch::Pending
} else {
@ -85,6 +83,10 @@ impl KeystrokeMatcher {
}
}
/// The result of matching a keystroke against a given keybinding.
/// - KeyMatch::None => No match is valid for this key given any pending keystrokes.
/// - KeyMatch::Pending => There exist bindings that is still waiting for more keys.
/// - KeyMatch::Some(matches) => One or more bindings have received the necessary key presses.
#[derive(Debug)]
pub enum KeyMatch {
None,
@ -93,10 +95,12 @@ pub enum KeyMatch {
}
impl KeyMatch {
/// Returns true if the match is complete.
pub fn is_some(&self) -> bool {
matches!(self, KeyMatch::Some(_))
}
/// Get the matches if the match is complete.
pub fn matches(self) -> Option<Vec<Box<dyn Action>>> {
match self {
KeyMatch::Some(matches) => Some(matches),

View file

@ -6,4 +6,4 @@ mod matcher;
pub use binding::*;
pub use context::*;
pub use keymap::*;
pub use matcher::*;
pub(crate) use matcher::*;

View file

@ -6,10 +6,10 @@ mod mac;
mod test;
use crate::{
Action, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId, FontMetrics,
FontRun, ForegroundExecutor, GlobalPixels, GlyphId, Keymap, LineLayout, Pixels, PlatformInput,
Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, Scene, SharedString,
Size, TaskLabel,
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,
};
use anyhow::anyhow;
use async_task::Runnable;
@ -34,9 +34,9 @@ use uuid::Uuid;
pub use app_menu::*;
pub use keystroke::*;
#[cfg(target_os = "macos")]
pub use mac::*;
pub(crate) use mac::*;
#[cfg(any(test, feature = "test-support"))]
pub use test::*;
pub(crate) use test::*;
use time::UtcOffset;
#[cfg(target_os = "macos")]
@ -69,11 +69,10 @@ pub(crate) trait Platform: 'static {
fn set_display_link_output_callback(
&self,
display_id: DisplayId,
callback: Box<dyn FnMut(&VideoTimestamp, &VideoTimestamp) + Send>,
callback: Box<dyn FnMut() + Send>,
);
fn start_display_link(&self, display_id: DisplayId);
fn stop_display_link(&self, display_id: DisplayId);
// fn add_status_item(&self, _handle: AnyWindowHandle) -> Box<dyn PlatformWindow>;
fn open_url(&self, url: &str);
fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
@ -149,8 +148,8 @@ pub(crate) trait PlatformWindow {
fn mouse_position(&self) -> Point<Pixels>;
fn modifiers(&self) -> Modifiers;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn set_input_handler(&mut self, input_handler: Box<dyn PlatformInputHandler>);
fn take_input_handler(&mut self) -> Option<Box<dyn PlatformInputHandler>>;
fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize>;
fn activate(&self);
fn set_title(&mut self, title: &str);
@ -325,30 +324,168 @@ impl From<TileId> for etagere::AllocId {
}
}
pub trait PlatformInputHandler: 'static {
fn selected_text_range(&mut self) -> Option<Range<usize>>;
fn marked_text_range(&mut self) -> Option<Range<usize>>;
fn text_for_range(&mut self, range_utf16: Range<usize>) -> Option<String>;
fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str);
pub(crate) struct PlatformInputHandler {
cx: AsyncWindowContext,
handler: Box<dyn InputHandler>,
}
impl PlatformInputHandler {
pub fn new(cx: AsyncWindowContext, handler: Box<dyn InputHandler>) -> Self {
Self { cx, handler }
}
fn selected_text_range(&mut self) -> Option<Range<usize>> {
self.cx
.update(|cx| self.handler.selected_text_range(cx))
.ok()
.flatten()
}
fn marked_text_range(&mut self) -> Option<Range<usize>> {
self.cx
.update(|cx| self.handler.marked_text_range(cx))
.ok()
.flatten()
}
fn text_for_range(&mut self, range_utf16: Range<usize>) -> Option<String> {
self.cx
.update(|cx| self.handler.text_for_range(range_utf16, cx))
.ok()
.flatten()
}
fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
self.cx
.update(|cx| {
self.handler
.replace_text_in_range(replacement_range, text, cx)
})
.ok();
}
fn replace_and_mark_text_in_range(
&mut self,
range_utf16: Option<Range<usize>>,
new_text: &str,
new_selected_range: Option<Range<usize>>,
);
fn unmark_text(&mut self);
fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>>;
) {
self.cx
.update(|cx| {
self.handler.replace_and_mark_text_in_range(
range_utf16,
new_text,
new_selected_range,
cx,
)
})
.ok();
}
fn unmark_text(&mut self) {
self.cx.update(|cx| self.handler.unmark_text(cx)).ok();
}
fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
self.cx
.update(|cx| self.handler.bounds_for_range(range_utf16, cx))
.ok()
.flatten()
}
}
/// Zed's interface for handling text input from the platform's IME system
/// This is currently a 1:1 exposure of the NSTextInputClient API:
///
/// <https://developer.apple.com/documentation/appkit/nstextinputclient>
pub trait InputHandler: 'static {
/// Get the range of the user's currently selected text, if any
/// Corresponds to [selectedRange()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438242-selectedrange)
///
/// Return value is in terms of UTF-16 characters, from 0 to the length of the document
fn selected_text_range(&mut self, cx: &mut WindowContext) -> Option<Range<usize>>;
/// Get the range of the currently marked text, if any
/// Corresponds to [markedRange()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438250-markedrange)
///
/// Return value is in terms of UTF-16 characters, from 0 to the length of the document
fn marked_text_range(&mut self, cx: &mut WindowContext) -> Option<Range<usize>>;
/// Get the text for the given document range in UTF-16 characters
/// Corresponds to [attributedSubstring(forProposedRange: actualRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438238-attributedsubstring)
///
/// range_utf16 is in terms of UTF-16 characters
fn text_for_range(
&mut self,
range_utf16: Range<usize>,
cx: &mut WindowContext,
) -> Option<String>;
/// Replace the text in the given document range with the given text
/// Corresponds to [insertText(_:replacementRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438258-inserttext)
///
/// replacement_range is in terms of UTF-16 characters
fn replace_text_in_range(
&mut self,
replacement_range: Option<Range<usize>>,
text: &str,
cx: &mut WindowContext,
);
/// Replace the text in the given document range with the given text,
/// and mark the given text as part of of an IME 'composing' state
/// Corresponds to [setMarkedText(_:selectedRange:replacementRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438246-setmarkedtext)
///
/// range_utf16 is in terms of UTF-16 characters
/// new_selected_range is in terms of UTF-16 characters
fn replace_and_mark_text_in_range(
&mut self,
range_utf16: Option<Range<usize>>,
new_text: &str,
new_selected_range: Option<Range<usize>>,
cx: &mut WindowContext,
);
/// Remove the IME 'composing' state from the document
/// Corresponds to [unmarkText()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438239-unmarktext)
fn unmark_text(&mut self, cx: &mut WindowContext);
/// Get the bounds of the given document range in screen coordinates
/// Corresponds to [firstRect(forCharacterRange:actualRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438240-firstrect)
///
/// This is used for positioning the IME candidate window
fn bounds_for_range(
&mut self,
range_utf16: Range<usize>,
cx: &mut WindowContext,
) -> Option<Bounds<Pixels>>;
}
/// The variables that can be configured when creating a new window
#[derive(Debug)]
pub struct WindowOptions {
/// The initial bounds of the window
pub bounds: WindowBounds,
/// The titlebar configuration of the window
pub titlebar: Option<TitlebarOptions>,
/// Whether the window should be centered on the screen
pub center: bool,
/// Whether the window should be focused when created
pub focus: bool,
/// Whether the window should be shown when created
pub show: bool,
/// The kind of window to create
pub kind: WindowKind,
/// Whether the window should be movable by the user
pub is_movable: bool,
/// The display to create the window on
pub display_id: Option<DisplayId>,
}
@ -371,46 +508,67 @@ impl Default for WindowOptions {
}
}
/// The options that can be configured for a window's titlebar
#[derive(Debug, Default)]
pub struct TitlebarOptions {
/// The initial title of the window
pub title: Option<SharedString>,
/// Whether the titlebar should appear transparent
pub appears_transparent: bool,
/// The position of the macOS traffic light buttons
pub traffic_light_position: Option<Point<Pixels>>,
}
#[derive(Copy, Clone, Debug)]
pub enum Appearance {
Light,
VibrantLight,
Dark,
VibrantDark,
}
impl Default for Appearance {
fn default() -> Self {
Self::Light
}
}
/// The kind of window to create
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum WindowKind {
/// A normal application window
Normal,
/// A window that appears above all other windows, usually used for alerts or popups
/// use sparingly!
PopUp,
}
/// Which bounds algorithm to use for the initial size a window
#[derive(Copy, Clone, Debug, PartialEq, Default)]
pub enum WindowBounds {
/// The window should be full screen, on macOS this corresponds to the full screen feature
Fullscreen,
/// Make the window as large as the current display's size.
#[default]
Maximized,
/// Set the window to the given size in pixels
Fixed(Bounds<GlobalPixels>),
}
/// The appearance of the window, as defined by the operating system
/// On macOS, this corresponds to named [NSAppearance](https://developer.apple.com/documentation/appkit/nsappearance)
/// values
#[derive(Copy, Clone, Debug)]
pub enum WindowAppearance {
/// A light appearance
///
/// on macOS, this corresponds to the `aqua` appearance
Light,
/// A light appearance with vibrant colors
///
/// on macOS, this corresponds to the `NSAppearanceNameVibrantLight` appearance
VibrantLight,
/// A dark appearance
///
/// on macOS, this corresponds to the `darkAqua` appearance
Dark,
/// A dark appearance with vibrant colors
///
/// on macOS, this corresponds to the `NSAppearanceNameVibrantDark` appearance
VibrantDark,
}
@ -420,40 +578,102 @@ impl Default for WindowAppearance {
}
}
/// The options that can be configured for a file dialog prompt
#[derive(Copy, Clone, Debug)]
pub struct PathPromptOptions {
/// Should the prompt allow files to be selected?
pub files: bool,
/// Should the prompt allow directories to be selected?
pub directories: bool,
/// Should the prompt allow multiple files to be selected?
pub multiple: bool,
}
/// What kind of prompt styling to show
#[derive(Copy, Clone, Debug)]
pub enum PromptLevel {
/// A prompt that is shown when the user should be notified of something
Info,
/// A prompt that is shown when the user needs to be warned of a potential problem
Warning,
/// A prompt that is shown when a critical problem has occurred
Critical,
}
/// The style of the cursor (pointer)
#[derive(Copy, Clone, Debug)]
pub enum CursorStyle {
/// The default cursor
Arrow,
/// A text input cursor
/// corresponds to the CSS cursor value `text`
IBeam,
/// A crosshair cursor
/// corresponds to the CSS cursor value `crosshair`
Crosshair,
/// A closed hand cursor
/// corresponds to the CSS cursor value `grabbing`
ClosedHand,
/// An open hand cursor
/// corresponds to the CSS cursor value `grab`
OpenHand,
/// A pointing hand cursor
/// corresponds to the CSS cursor value `pointer`
PointingHand,
/// A resize left cursor
/// corresponds to the CSS cursor value `w-resize`
ResizeLeft,
/// A resize right cursor
/// corresponds to the CSS cursor value `e-resize`
ResizeRight,
/// A resize cursor to the left and right
/// corresponds to the CSS cursor value `col-resize`
ResizeLeftRight,
/// A resize up cursor
/// corresponds to the CSS cursor value `n-resize`
ResizeUp,
/// A resize down cursor
/// corresponds to the CSS cursor value `s-resize`
ResizeDown,
/// A resize cursor directing up and down
/// corresponds to the CSS cursor value `row-resize`
ResizeUpDown,
/// A cursor indicating that something will disappear if moved here
/// Does not correspond to a CSS cursor value
DisappearingItem,
/// A text input cursor for vertical layout
/// corresponds to the CSS cursor value `vertical-text`
IBeamCursorForVerticalLayout,
/// A cursor indicating that the operation is not allowed
/// corresponds to the CSS cursor value `not-allowed`
OperationNotAllowed,
/// A cursor indicating that the operation will result in a link
/// corresponds to the CSS cursor value `alias`
DragLink,
/// A cursor indicating that the operation will result in a copy
/// corresponds to the CSS cursor value `copy`
DragCopy,
/// A cursor indicating that the operation will result in a context menu
/// corresponds to the CSS cursor value `context-menu`
ContextualMenu,
}
@ -463,6 +683,7 @@ impl Default for CursorStyle {
}
}
/// A datastructure representing a semantic version number
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct SemanticVersion {
major: usize,
@ -501,6 +722,7 @@ impl Display for SemanticVersion {
}
}
/// A clipboard item that should be copied to the clipboard
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ClipboardItem {
pub(crate) text: String,
@ -508,6 +730,7 @@ pub struct ClipboardItem {
}
impl ClipboardItem {
/// Create a new clipboard item with the given text
pub fn new(text: String) -> Self {
Self {
text,
@ -515,15 +738,18 @@ impl ClipboardItem {
}
}
/// Create a new clipboard item with the given text and metadata
pub fn with_metadata<T: Serialize>(mut self, metadata: T) -> Self {
self.metadata = Some(serde_json::to_string(&metadata).unwrap());
self
}
/// Get the text of the clipboard item
pub fn text(&self) -> &String {
&self.text
}
/// Get the metadata of the clipboard item
pub fn metadata<T>(&self) -> Option<T>
where
T: for<'a> Deserialize<'a>,

View file

@ -1,30 +1,49 @@
use crate::{Action, AppContext, Platform};
use util::ResultExt;
/// A menu of the application, either a main menu or a submenu
pub struct Menu<'a> {
/// The name of the menu
pub name: &'a str,
/// The items in the menu
pub items: Vec<MenuItem<'a>>,
}
/// The different kinds of items that can be in a menu
pub enum MenuItem<'a> {
/// A separator between items
Separator,
/// A submenu
Submenu(Menu<'a>),
/// An action that can be performed
Action {
/// The name of this menu item
name: &'a str,
/// the action to perform when this menu item is selected
action: Box<dyn Action>,
/// The OS Action that corresponds to this action, if any
/// See [`OsAction`] for more information
os_action: Option<OsAction>,
},
}
impl<'a> MenuItem<'a> {
/// Creates a new menu item that is a separator
pub fn separator() -> Self {
Self::Separator
}
/// Creates a new menu item that is a submenu
pub fn submenu(menu: Menu<'a>) -> Self {
Self::Submenu(menu)
}
/// Creates a new menu item that invokes an action
pub fn action(name: &'a str, action: impl Action) -> Self {
Self::Action {
name,
@ -33,6 +52,7 @@ impl<'a> MenuItem<'a> {
}
}
/// Creates a new menu item that invokes an action and has an OS action
pub fn os_action(name: &'a str, action: impl Action, os_action: OsAction) -> Self {
Self::Action {
name,
@ -42,13 +62,31 @@ impl<'a> MenuItem<'a> {
}
}
// TODO: As part of the global selections refactor, these should
// be moved to GPUI-provided actions that make this association
// without leaking the platform details to GPUI users
/// OS actions are actions that are recognized by the operating system
/// This allows the operating system to provide specialized behavior for
/// these actions
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum OsAction {
/// The 'cut' action
Cut,
/// The 'copy' action
Copy,
/// The 'paste' action
Paste,
/// The 'select all' action
SelectAll,
/// The 'undo' action
Undo,
/// The 'redo' action
Redo,
}

View file

@ -3,24 +3,31 @@ use serde::Deserialize;
use smallvec::SmallVec;
use std::fmt::Write;
/// A keystroke and associated metadata generated by the platform
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
pub struct Keystroke {
/// the state of the modifier keys at the time the keystroke was generated
pub modifiers: Modifiers,
/// key is the character printed on the key that was pressed
/// e.g. for option-s, key is "s"
pub key: String,
/// ime_key is the character inserted by the IME engine when that key was pressed.
/// e.g. for option-s, ime_key is "ß"
pub ime_key: Option<String>,
}
impl Keystroke {
// When matching a key we cannot know whether the user intended to type
// the ime_key or the key. On some non-US keyboards keys we use in our
// bindings are behind option (for example `$` is typed `alt-ç` on a Czech keyboard),
// and on some keyboards the IME handler converts a sequence of keys into a
// specific character (for example `"` is typed as `" space` on a brazilian keyboard).
pub fn match_candidates(&self) -> SmallVec<[Keystroke; 2]> {
/// When matching a key we cannot know whether the user intended to type
/// the ime_key or the key itself. On some non-US keyboards keys we use in our
/// bindings are behind option (for example `$` is typed `alt-ç` on a Czech keyboard),
/// and on some keyboards the IME handler converts a sequence of keys into a
/// specific character (for example `"` is typed as `" space` on a brazilian keyboard).
///
/// This method generates a list of potential keystroke candidates that could be matched
/// against when resolving a keybinding.
pub(crate) fn match_candidates(&self) -> SmallVec<[Keystroke; 2]> {
let mut possibilities = SmallVec::new();
match self.ime_key.as_ref() {
None => possibilities.push(self.clone()),
@ -47,7 +54,7 @@ impl Keystroke {
/// key syntax is:
/// [ctrl-][alt-][shift-][cmd-][fn-]key[->ime_key]
/// ime_key is only used for generating test events,
/// ime_key syntax is only used for generating test events,
/// when matching a key with an ime_key set will be matched without it.
pub fn parse(source: &str) -> anyhow::Result<Self> {
let mut control = false;
@ -135,16 +142,29 @@ impl std::fmt::Display for Keystroke {
}
}
/// The state of the modifier keys at some point in time
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
pub struct Modifiers {
/// The control key
pub control: bool,
/// The alt key
/// Sometimes also known as the 'meta' key
pub alt: bool,
/// The shift key
pub shift: bool,
/// The command key, on macos
/// the windows key, on windows
pub command: bool,
/// The function key
pub function: bool,
}
impl Modifiers {
/// Returns true if any modifier key is pressed
pub fn modified(&self) -> bool {
self.control || self.alt || self.shift || self.command || self.function
}

View file

@ -21,13 +21,13 @@ use metal_renderer::*;
use objc::runtime::{BOOL, NO, YES};
use std::ops::Range;
pub use dispatcher::*;
pub use display::*;
pub use display_linker::*;
pub use metal_atlas::*;
pub use platform::*;
pub use text_system::*;
pub use window::*;
pub(crate) use dispatcher::*;
pub(crate) use display::*;
pub(crate) use display_linker::*;
pub(crate) use metal_atlas::*;
pub(crate) use platform::*;
pub(crate) use text_system::*;
pub(crate) use window::*;
trait BoolExt {
fn to_objc(self) -> BOOL;

View file

@ -24,7 +24,7 @@ pub(crate) fn dispatch_get_main_queue() -> dispatch_queue_t {
unsafe { &_dispatch_main_q as *const _ as dispatch_queue_t }
}
pub struct MacDispatcher {
pub(crate) struct MacDispatcher {
parker: Arc<Mutex<Parker>>,
}

View file

@ -3,18 +3,15 @@ use anyhow::Result;
use cocoa::{
appkit::NSScreen,
base::{id, nil},
foundation::{NSDictionary, NSString},
foundation::{NSDictionary, NSPoint, NSRect, NSSize, NSString},
};
use core_foundation::uuid::{CFUUIDGetUUIDBytes, CFUUIDRef};
use core_graphics::{
display::{CGDirectDisplayID, CGDisplayBounds, CGGetActiveDisplayList},
geometry::{CGPoint, CGRect, CGSize},
};
use core_graphics::display::{CGDirectDisplayID, CGDisplayBounds, CGGetActiveDisplayList};
use objc::{msg_send, sel, sel_impl};
use uuid::Uuid;
#[derive(Debug)]
pub struct MacDisplay(pub(crate) CGDirectDisplayID);
pub(crate) struct MacDisplay(pub(crate) CGDirectDisplayID);
unsafe impl Send for MacDisplay {}
@ -24,11 +21,6 @@ impl MacDisplay {
Self::all().find(|screen| screen.id() == id)
}
/// Get the screen with the given persistent [`Uuid`].
pub fn find_by_uuid(uuid: Uuid) -> Option<Self> {
Self::all().find(|screen| screen.uuid().ok() == Some(uuid))
}
/// Get the primary screen - the one with the menu bar, and whose bottom left
/// corner is at the origin of the AppKit coordinate system.
pub fn primary() -> Self {
@ -77,14 +69,14 @@ extern "C" {
fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef;
}
/// Convert the given rectangle from CoreGraphics' native coordinate space to GPUI's coordinate space.
/// Convert the given rectangle from Cocoa's coordinate space to GPUI's coordinate space.
///
/// CoreGraphics' coordinate space has its origin at the bottom left of the primary screen,
/// Cocoa's coordinate space has its origin at the bottom left of the primary screen,
/// with the Y axis pointing upwards.
///
/// Conversely, in GPUI's coordinate system, the origin is placed at the top left of the primary
/// screen, with the Y axis pointing downwards.
pub(crate) fn display_bounds_from_native(rect: CGRect) -> Bounds<GlobalPixels> {
/// screen, with the Y axis pointing downwards (matching CoreGraphics)
pub(crate) fn global_bounds_from_ns_rect(rect: NSRect) -> Bounds<GlobalPixels> {
let primary_screen_size = unsafe { CGDisplayBounds(MacDisplay::primary().id().0) }.size;
Bounds {
@ -101,22 +93,22 @@ pub(crate) fn display_bounds_from_native(rect: CGRect) -> Bounds<GlobalPixels> {
}
}
/// Convert the given rectangle from GPUI's coordinate system to CoreGraphics' native coordinate space.
/// Convert the given rectangle from GPUI's coordinate system to Cocoa's native coordinate space.
///
/// CoreGraphics' coordinate space has its origin at the bottom left of the primary screen,
/// Cocoa's coordinate space has its origin at the bottom left of the primary screen,
/// with the Y axis pointing upwards.
///
/// Conversely, in GPUI's coordinate system, the origin is placed at the top left of the primary
/// screen, with the Y axis pointing downwards.
pub(crate) fn display_bounds_to_native(bounds: Bounds<GlobalPixels>) -> CGRect {
/// screen, with the Y axis pointing downwards (matching CoreGraphics)
pub(crate) fn global_bounds_to_ns_rect(bounds: Bounds<GlobalPixels>) -> NSRect {
let primary_screen_height = MacDisplay::primary().bounds().size.height;
CGRect::new(
&CGPoint::new(
NSRect::new(
NSPoint::new(
bounds.origin.x.into(),
(primary_screen_height - bounds.origin.y - bounds.size.height).into(),
),
&CGSize::new(bounds.size.width.into(), bounds.size.height.into()),
NSSize::new(bounds.size.width.into(), bounds.size.height.into()),
)
}
@ -155,8 +147,20 @@ impl PlatformDisplay for MacDisplay {
fn bounds(&self) -> Bounds<GlobalPixels> {
unsafe {
let native_bounds = CGDisplayBounds(self.0);
display_bounds_from_native(native_bounds)
// CGDisplayBounds is in "global display" coordinates, where 0 is
// the top left of the primary display.
let bounds = CGDisplayBounds(self.0);
Bounds {
origin: point(
GlobalPixels(bounds.origin.x as f32),
GlobalPixels(bounds.origin.y as f32),
),
size: size(
GlobalPixels(bounds.size.width as f32),
GlobalPixels(bounds.size.height as f32),
),
}
}
}
}

View file

@ -7,8 +7,6 @@ use std::{
use crate::DisplayId;
use collections::HashMap;
use parking_lot::Mutex;
pub use sys::CVSMPTETime as SmtpeTime;
pub use sys::CVTimeStamp as VideoTimestamp;
pub(crate) struct MacDisplayLinker {
links: HashMap<DisplayId, MacDisplayLink>,
@ -27,13 +25,13 @@ impl MacDisplayLinker {
}
}
type OutputCallback = Mutex<Box<dyn FnMut(&VideoTimestamp, &VideoTimestamp) + Send>>;
type OutputCallback = Mutex<Box<dyn FnMut() + Send>>;
impl MacDisplayLinker {
pub fn set_output_callback(
&mut self,
display_id: DisplayId,
output_callback: Box<dyn FnMut(&VideoTimestamp, &VideoTimestamp) + Send>,
output_callback: Box<dyn FnMut() + Send>,
) {
if let Some(mut system_link) = unsafe { sys::DisplayLink::on_display(display_id.0) } {
let callback = Arc::new(Mutex::new(output_callback));
@ -81,11 +79,11 @@ unsafe extern "C" fn trampoline(
_flags_out: *mut i64,
user_data: *mut c_void,
) -> i32 {
if let Some((current_time, output_time)) = current_time.as_ref().zip(output_time.as_ref()) {
if let Some((_current_time, _output_time)) = current_time.as_ref().zip(output_time.as_ref()) {
let output_callback: Weak<OutputCallback> =
Weak::from_raw(user_data as *mut OutputCallback);
if let Some(output_callback) = output_callback.upgrade() {
(output_callback.lock())(current_time, output_time)
(output_callback.lock())()
}
mem::forget(output_callback);
}
@ -126,7 +124,7 @@ mod sys {
#[repr(C)]
#[derive(Clone, Copy)]
pub struct CVTimeStamp {
pub(crate) struct CVTimeStamp {
pub version: u32,
pub video_time_scale: i32,
pub video_time: i64,
@ -154,7 +152,7 @@ mod sys {
#[repr(C)]
#[derive(Clone, Copy, Default)]
pub struct CVSMPTETime {
pub(crate) struct CVSMPTETime {
pub subframes: i16,
pub subframe_divisor: i16,
pub counter: u32,

View file

@ -83,7 +83,10 @@ unsafe fn read_modifiers(native_event: id) -> Modifiers {
}
impl PlatformInput {
pub unsafe fn from_native(native_event: id, window_height: Option<Pixels>) -> Option<Self> {
pub(crate) unsafe fn from_native(
native_event: id,
window_height: Option<Pixels>,
) -> Option<Self> {
let event_type = native_event.eventType();
// Filter out event types that aren't in the NSEventType enum.

View file

@ -10,10 +10,10 @@ use metal::Device;
use parking_lot::Mutex;
use std::borrow::Cow;
pub struct MetalAtlas(Mutex<MetalAtlasState>);
pub(crate) struct MetalAtlas(Mutex<MetalAtlasState>);
impl MetalAtlas {
pub fn new(device: Device) -> Self {
pub(crate) fn new(device: Device) -> Self {
MetalAtlas(Mutex::new(MetalAtlasState {
device: AssertSend(device),
monochrome_textures: Default::default(),

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, VideoTimestamp, WindowOptions,
PlatformTextSystem, PlatformWindow, Result, SemanticVersion, WindowOptions,
};
use anyhow::anyhow;
use block::ConcreteBlock;
@ -139,9 +139,9 @@ unsafe fn build_classes() {
}
}
pub struct MacPlatform(Mutex<MacPlatformState>);
pub(crate) struct MacPlatform(Mutex<MacPlatformState>);
pub struct MacPlatformState {
pub(crate) struct MacPlatformState {
background_executor: BackgroundExecutor,
foreground_executor: ForegroundExecutor,
text_system: Arc<MacTextSystem>,
@ -169,7 +169,7 @@ impl Default for MacPlatform {
}
impl MacPlatform {
pub fn new() -> Self {
pub(crate) fn new() -> Self {
let dispatcher = Arc::new(MacDispatcher::new());
Self(Mutex::new(MacPlatformState {
background_executor: BackgroundExecutor::new(dispatcher.clone()),
@ -475,10 +475,6 @@ impl Platform for MacPlatform {
}
}
// fn add_status_item(&self, _handle: AnyWindowHandle) -> Box<dyn platform::Window> {
// Box::new(StatusItem::add(self.fonts()))
// }
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
MacDisplay::all()
.map(|screen| Rc::new(screen) as Rc<_>)
@ -504,7 +500,7 @@ impl Platform for MacPlatform {
fn set_display_link_output_callback(
&self,
display_id: DisplayId,
callback: Box<dyn FnMut(&VideoTimestamp, &VideoTimestamp) + Send>,
callback: Box<dyn FnMut() + Send>,
) {
self.0
.lock()

View file

@ -41,7 +41,7 @@ use super::open_type;
#[allow(non_upper_case_globals)]
const kCGImageAlphaOnly: u32 = 7;
pub struct MacTextSystem(RwLock<MacTextSystemState>);
pub(crate) struct MacTextSystem(RwLock<MacTextSystemState>);
struct MacTextSystemState {
memory_source: MemSource,
@ -54,7 +54,7 @@ struct MacTextSystemState {
}
impl MacTextSystem {
pub fn new() -> Self {
pub(crate) fn new() -> Self {
Self(RwLock::new(MacTextSystemState {
memory_source: MemSource::empty(),
system_source: SystemSource::new(),

View file

@ -1,9 +1,9 @@
use super::{display_bounds_from_native, ns_string, MacDisplay, MetalRenderer, NSRange};
use super::{global_bounds_from_ns_rect, ns_string, MacDisplay, MetalRenderer, NSRange};
use crate::{
display_bounds_to_native, point, px, size, AnyWindowHandle, Bounds, ExternalPaths,
FileDropEvent, ForegroundExecutor, GlobalPixels, KeyDownEvent, Keystroke, Modifiers,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
global_bounds_to_ns_rect, platform::PlatformInputHandler, point, px, size, AnyWindowHandle,
Bounds, ExternalPaths, FileDropEvent, ForegroundExecutor, GlobalPixels, KeyDownEvent,
Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent,
MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point,
PromptLevel, Size, Timer, WindowAppearance, WindowBounds, WindowKind, WindowOptions,
};
use block::ConcreteBlock;
@ -220,7 +220,7 @@ unsafe fn build_classes() {
};
}
pub fn convert_mouse_position(position: NSPoint, window_height: Pixels) -> Point<Pixels> {
pub(crate) fn convert_mouse_position(position: NSPoint, window_height: Pixels) -> Point<Pixels> {
point(
px(position.x as f32),
// MacOS screen coordinates are relative to bottom left
@ -327,7 +327,7 @@ struct MacWindowState {
should_close_callback: Option<Box<dyn FnMut() -> bool>>,
close_callback: Option<Box<dyn FnOnce()>>,
appearance_changed_callback: Option<Box<dyn FnMut()>>,
input_handler: Option<Box<dyn PlatformInputHandler>>,
input_handler: Option<PlatformInputHandler>,
pending_key_down: Option<(KeyDownEvent, Option<InsertText>)>,
last_key_equivalent: Option<KeyDownEvent>,
synthetic_drag_counter: usize,
@ -411,10 +411,8 @@ impl MacWindowState {
}
fn frame(&self) -> Bounds<GlobalPixels> {
unsafe {
let frame = NSWindow::frame(self.native_window);
display_bounds_from_native(mem::transmute::<NSRect, CGRect>(frame))
}
let frame = unsafe { NSWindow::frame(self.native_window) };
global_bounds_from_ns_rect(frame)
}
fn content_size(&self) -> Size<Pixels> {
@ -448,7 +446,7 @@ impl MacWindowState {
unsafe impl Send for MacWindowState {}
pub struct MacWindow(Arc<Mutex<MacWindowState>>);
pub(crate) struct MacWindow(Arc<Mutex<MacWindowState>>);
impl MacWindow {
pub fn open(
@ -650,11 +648,11 @@ impl MacWindow {
WindowBounds::Fixed(bounds) => {
let display_bounds = display.bounds();
let frame = if bounds.intersects(&display_bounds) {
display_bounds_to_native(bounds)
global_bounds_to_ns_rect(bounds)
} else {
display_bounds_to_native(display_bounds)
global_bounds_to_ns_rect(display_bounds)
};
native_window.setFrame_display_(mem::transmute::<CGRect, NSRect>(frame), YES);
native_window.setFrame_display_(frame, YES);
}
}
@ -766,11 +764,11 @@ impl PlatformWindow for MacWindow {
self
}
fn set_input_handler(&mut self, input_handler: Box<dyn PlatformInputHandler>) {
fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
self.0.as_ref().lock().input_handler = Some(input_handler);
}
fn take_input_handler(&mut self) -> Option<Box<dyn PlatformInputHandler>> {
fn take_input_handler(&mut self) -> Option<PlatformInputHandler> {
self.0.as_ref().lock().input_handler.take()
}
@ -1763,13 +1761,13 @@ fn drag_event_position(window_state: &Mutex<MacWindowState>, dragging_info: id)
fn with_input_handler<F, R>(window: &Object, f: F) -> Option<R>
where
F: FnOnce(&mut dyn PlatformInputHandler) -> R,
F: FnOnce(&mut PlatformInputHandler) -> R,
{
let window_state = unsafe { get_window_state(window) };
let mut lock = window_state.as_ref().lock();
if let Some(mut input_handler) = lock.input_handler.take() {
drop(lock);
let result = f(input_handler.as_mut());
let result = f(&mut input_handler);
window_state.lock().input_handler = Some(input_handler);
Some(result)
} else {

View file

@ -8,7 +8,7 @@ use objc::{msg_send, sel, sel_impl};
use std::ffi::CStr;
impl WindowAppearance {
pub unsafe fn from_native(appearance: id) -> Self {
pub(crate) unsafe fn from_native(appearance: id) -> Self {
let name: id = msg_send![appearance, name];
if name == NSAppearanceNameVibrantLight {
Self::VibrantLight

View file

@ -3,7 +3,7 @@ mod display;
mod platform;
mod window;
pub use dispatcher::*;
pub use display::*;
pub use platform::*;
pub use window::*;
pub(crate) use dispatcher::*;
pub(crate) use display::*;
pub(crate) use platform::*;
pub(crate) use window::*;

View file

@ -18,6 +18,7 @@ use util::post_inc;
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
struct TestDispatcherId(usize);
#[doc(hidden)]
pub struct TestDispatcher {
id: TestDispatcherId,
state: Arc<Mutex<TestDispatcherState>>,

View file

@ -3,7 +3,7 @@ use anyhow::{Ok, Result};
use crate::{Bounds, DisplayId, GlobalPixels, PlatformDisplay, Point};
#[derive(Debug)]
pub struct TestDisplay {
pub(crate) struct TestDisplay {
id: DisplayId,
uuid: uuid::Uuid,
bounds: Bounds<GlobalPixels>,

View file

@ -15,7 +15,7 @@ use std::{
};
/// TestPlatform implements the Platform trait for use in tests.
pub struct TestPlatform {
pub(crate) struct TestPlatform {
background_executor: BackgroundExecutor,
foreground_executor: ForegroundExecutor,
@ -178,20 +178,9 @@ impl Platform for TestPlatform {
fn set_display_link_output_callback(
&self,
_display_id: DisplayId,
mut callback: Box<dyn FnMut(&crate::VideoTimestamp, &crate::VideoTimestamp) + Send>,
mut callback: Box<dyn FnMut() + Send>,
) {
let timestamp = crate::VideoTimestamp {
version: 0,
video_time_scale: 0,
video_time: 0,
host_time: 0,
rate_scalar: 0.0,
video_refresh_period: 0,
smpte_time: crate::SmtpeTime::default(),
flags: 0,
reserved: 0,
};
callback(&timestamp, &timestamp)
callback()
}
fn start_display_link(&self, _display_id: DisplayId) {}

View file

@ -10,7 +10,7 @@ use std::{
sync::{self, Arc},
};
pub struct TestWindowState {
pub(crate) struct TestWindowState {
pub(crate) bounds: WindowBounds,
pub(crate) handle: AnyWindowHandle,
display: Rc<dyn PlatformDisplay>,
@ -23,11 +23,11 @@ pub struct TestWindowState {
active_status_change_callback: Option<Box<dyn FnMut(bool)>>,
resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
moved_callback: Option<Box<dyn FnMut()>>,
input_handler: Option<Box<dyn PlatformInputHandler>>,
input_handler: Option<PlatformInputHandler>,
}
#[derive(Clone)]
pub struct TestWindow(pub(crate) Arc<Mutex<TestWindowState>>);
pub(crate) struct TestWindow(pub(crate) Arc<Mutex<TestWindowState>>);
impl TestWindow {
pub fn new(
@ -117,9 +117,6 @@ impl TestWindow {
self.0.lock().input_handler = Some(input_handler);
}
pub fn edited(&self) -> bool {
self.0.lock().edited
}
}
impl PlatformWindow for TestWindow {
@ -163,11 +160,11 @@ impl PlatformWindow for TestWindow {
self
}
fn set_input_handler(&mut self, input_handler: Box<dyn crate::PlatformInputHandler>) {
fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
self.0.lock().input_handler = Some(input_handler);
}
fn take_input_handler(&mut self) -> Option<Box<dyn PlatformInputHandler>> {
fn take_input_handler(&mut self) -> Option<PlatformInputHandler> {
self.0.lock().input_handler.take()
}
@ -269,12 +266,12 @@ impl PlatformWindow for TestWindow {
}
}
pub struct TestAtlasState {
pub(crate) struct TestAtlasState {
next_id: u32,
tiles: HashMap<AtlasKey, AtlasTile>,
}
pub struct TestAtlas(Mutex<TestAtlasState>);
pub(crate) struct TestAtlas(Mutex<TestAtlasState>);
impl TestAtlas {
pub fn new() -> Self {

View file

@ -1,3 +1,7 @@
//! The GPUI prelude is a collection of traits and types that are widely used
//! throughout the library. It is recommended to import this prelude into your
//! application to avoid having to import each trait individually.
pub use crate::{
util::FluentBuilder, BorrowAppContext, BorrowWindow, Context, Element, FocusableElement,
InteractiveElement, IntoElement, ParentElement, Refineable, Render, RenderOnce,

View file

@ -10,12 +10,12 @@ pub(crate) type PointF = Point<f32>;
#[allow(non_camel_case_types, unused)]
pub(crate) type PathVertex_ScaledPixels = PathVertex<ScaledPixels>;
pub type LayerId = u32;
pub type DrawOrder = u32;
pub(crate) type LayerId = u32;
pub(crate) type DrawOrder = u32;
#[derive(Default, Copy, Clone, Debug, Eq, PartialEq, Hash)]
#[repr(C)]
pub struct ViewId {
pub(crate) struct ViewId {
low_bits: u32,
high_bits: u32,
}
@ -38,7 +38,7 @@ impl From<ViewId> for EntityId {
}
#[derive(Default)]
pub struct Scene {
pub(crate) struct Scene {
layers_by_order: BTreeMap<StackingOrder, LayerId>,
orders_by_layer: BTreeMap<LayerId, StackingOrder>,
pub(crate) shadows: Vec<Shadow>,
@ -153,49 +153,49 @@ impl Scene {
for shadow in prev_scene.shadows.drain(..) {
if views.contains(&shadow.view_id.into()) {
let order = &prev_scene.orders_by_layer[&shadow.layer_id];
self.insert(&order, shadow);
self.insert(order, shadow);
}
}
for quad in prev_scene.quads.drain(..) {
if views.contains(&quad.view_id.into()) {
let order = &prev_scene.orders_by_layer[&quad.layer_id];
self.insert(&order, quad);
self.insert(order, quad);
}
}
for path in prev_scene.paths.drain(..) {
if views.contains(&path.view_id.into()) {
let order = &prev_scene.orders_by_layer[&path.layer_id];
self.insert(&order, path);
self.insert(order, path);
}
}
for underline in prev_scene.underlines.drain(..) {
if views.contains(&underline.view_id.into()) {
let order = &prev_scene.orders_by_layer[&underline.layer_id];
self.insert(&order, underline);
self.insert(order, underline);
}
}
for sprite in prev_scene.monochrome_sprites.drain(..) {
if views.contains(&sprite.view_id.into()) {
let order = &prev_scene.orders_by_layer[&sprite.layer_id];
self.insert(&order, sprite);
self.insert(order, sprite);
}
}
for sprite in prev_scene.polychrome_sprites.drain(..) {
if views.contains(&sprite.view_id.into()) {
let order = &prev_scene.orders_by_layer[&sprite.layer_id];
self.insert(&order, sprite);
self.insert(order, sprite);
}
}
for surface in prev_scene.surfaces.drain(..) {
if views.contains(&surface.view_id.into()) {
let order = &prev_scene.orders_by_layer[&surface.layer_id];
self.insert(&order, surface);
self.insert(order, surface);
}
}
}
@ -429,7 +429,7 @@ impl<'a> Iterator for BatchIterator<'a> {
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Default)]
pub enum PrimitiveKind {
pub(crate) enum PrimitiveKind {
Shadow,
#[default]
Quad,
@ -495,7 +495,7 @@ pub(crate) enum PrimitiveBatch<'a> {
#[derive(Default, Debug, Clone, Eq, PartialEq)]
#[repr(C)]
pub struct Quad {
pub(crate) struct Quad {
pub view_id: ViewId,
pub layer_id: LayerId,
pub order: DrawOrder,
@ -527,7 +527,7 @@ impl From<Quad> for Primitive {
#[derive(Debug, Clone, Eq, PartialEq)]
#[repr(C)]
pub struct Underline {
pub(crate) struct Underline {
pub view_id: ViewId,
pub layer_id: LayerId,
pub order: DrawOrder,
@ -558,7 +558,7 @@ impl From<Underline> for Primitive {
#[derive(Debug, Clone, Eq, PartialEq)]
#[repr(C)]
pub struct Shadow {
pub(crate) struct Shadow {
pub view_id: ViewId,
pub layer_id: LayerId,
pub order: DrawOrder,
@ -655,7 +655,7 @@ impl From<PolychromeSprite> for Primitive {
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Surface {
pub(crate) struct Surface {
pub view_id: ViewId,
pub layer_id: LayerId,
pub order: DrawOrder,
@ -685,6 +685,7 @@ impl From<Surface> for Primitive {
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) struct PathId(pub(crate) usize);
/// A line made up of a series of vertices and control points.
#[derive(Debug)]
pub struct Path<P: Clone + Default + Debug> {
pub(crate) id: PathId,
@ -701,6 +702,7 @@ pub struct Path<P: Clone + Default + Debug> {
}
impl Path<Pixels> {
/// Create a new path with the given starting point.
pub fn new(start: Point<Pixels>) -> Self {
Self {
id: PathId(0),
@ -720,6 +722,7 @@ impl Path<Pixels> {
}
}
/// Scale this path by the given factor.
pub fn scale(&self, factor: f32) -> Path<ScaledPixels> {
Path {
id: self.id,
@ -740,6 +743,7 @@ impl Path<Pixels> {
}
}
/// Draw a straight line from the current point to the given point.
pub fn line_to(&mut self, to: Point<Pixels>) {
self.contour_count += 1;
if self.contour_count > 1 {
@ -751,6 +755,7 @@ impl Path<Pixels> {
self.current = to;
}
/// Draw a curve from the current point to the given point, using the given control point.
pub fn curve_to(&mut self, to: Point<Pixels>, ctrl: Point<Pixels>) {
self.contour_count += 1;
if self.contour_count > 1 {
@ -833,7 +838,7 @@ impl From<Path<ScaledPixels>> for Primitive {
#[derive(Clone, Debug)]
#[repr(C)]
pub struct PathVertex<P: Clone + Default + Debug> {
pub(crate) struct PathVertex<P: Clone + Default + Debug> {
pub(crate) xy_position: Point<P>,
pub(crate) st_position: Point<f32>,
pub(crate) content_mask: ContentMask<P>,
@ -850,4 +855,4 @@ impl PathVertex<Pixels> {
}
#[derive(Copy, Clone, Debug)]
pub struct AtlasId(pub(crate) usize);
pub(crate) struct AtlasId(pub(crate) usize);

View file

@ -3,6 +3,8 @@ use serde::{Deserialize, Serialize};
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,
#[derive(Deref, DerefMut, Eq, PartialEq, Hash, Clone)]
pub struct SharedString(ArcCow<'static, str>);

View file

@ -1,10 +1,10 @@
use std::{iter, mem, ops::Range};
use crate::{
black, phi, point, quad, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds,
ContentMask, Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement,
Font, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext,
black, phi, point, quad, rems, AbsoluteLength, BorrowAppContext, Bounds, ContentMask, Corners,
CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, ElementContext, Font,
FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
SharedString, Size, SizeRefinement, Styled, TextRun,
};
use collections::HashSet;
use refineable::{Cascade, Refineable};
@ -308,61 +308,12 @@ impl Style {
}
}
pub fn apply_text_style<C, F, R>(&self, cx: &mut C, f: F) -> R
where
C: BorrowAppContext,
F: FnOnce(&mut C) -> R,
{
if self.text.is_some() {
cx.with_text_style(Some(self.text.clone()), f)
} else {
f(cx)
}
}
/// Apply overflow to content mask
pub fn apply_overflow<C, F, R>(&self, bounds: Bounds<Pixels>, cx: &mut C, f: F) -> R
where
C: BorrowWindow,
F: FnOnce(&mut C) -> R,
{
let current_mask = cx.content_mask();
let min = current_mask.bounds.origin;
let max = current_mask.bounds.lower_right();
let mask_bounds = match (
self.overflow.x == Overflow::Visible,
self.overflow.y == Overflow::Visible,
) {
// x and y both visible
(true, true) => return f(cx),
// x visible, y hidden
(true, false) => Bounds::from_corners(
point(min.x, bounds.origin.y),
point(max.x, bounds.lower_right().y),
),
// x hidden, y visible
(false, true) => Bounds::from_corners(
point(bounds.origin.x, min.y),
point(bounds.lower_right().x, max.y),
),
// both hidden
(false, false) => bounds,
};
let mask = ContentMask {
bounds: mask_bounds,
};
cx.with_content_mask(Some(mask), f)
}
/// Paints the background of an element styled with this style.
pub fn paint(
&self,
bounds: Bounds<Pixels>,
cx: &mut WindowContext,
continuation: impl FnOnce(&mut WindowContext),
cx: &mut ElementContext,
continuation: impl FnOnce(&mut ElementContext),
) {
#[cfg(debug_assertions)]
if self.debug_below {

View file

@ -1,11 +1,11 @@
use crate::{
self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle,
DefiniteLength, Display, Fill, FlexDirection, FontWeight, Hsla, JustifyContent, Length,
Position, SharedString, StyleRefinement, Visibility, WhiteSpace,
DefiniteLength, Fill, FlexDirection, FontWeight, Hsla, JustifyContent, Length, Position,
SharedString, StyleRefinement, Visibility, WhiteSpace,
};
use crate::{BoxShadow, TextStyleRefinement};
use smallvec::{smallvec, SmallVec};
use taffy::style::Overflow;
use taffy::style::{Display, Overflow};
pub trait Styled: Sized {
fn style(&mut self) -> &mut StyleRefinement;

View file

@ -147,12 +147,17 @@ where
}
}
/// A handle to a subscription created by GPUI. When dropped, the subscription
/// is cancelled and the callback will no longer be invoked.
#[must_use]
pub struct Subscription {
unsubscribe: Option<Box<dyn FnOnce() + 'static>>,
}
impl Subscription {
/// Detaches the subscription from this handle. The callback will
/// continue to be invoked until the views or models it has been
/// subscribed to are dropped
pub fn detach(mut self) {
self.unsubscribe.take();
}

View file

@ -3,12 +3,12 @@ use anyhow::anyhow;
use std::{hash::Hash, sync::Arc};
#[derive(Clone, PartialEq, Hash, Eq)]
pub struct RenderSvgParams {
pub(crate) struct RenderSvgParams {
pub(crate) path: SharedString,
pub(crate) size: Size<DevicePixels>,
}
pub struct SvgRenderer {
pub(crate) struct SvgRenderer {
asset_source: Arc<dyn AssetSource>,
}

View file

@ -229,6 +229,7 @@ impl TaffyLayoutEngine {
}
}
/// A unique identifier for a layout node, generated when requesting a layout from Taffy
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
#[repr(transparent)]
pub struct LayoutId(NodeId);
@ -440,6 +441,7 @@ where
}
}
/// The space available for an element to be laid out in
#[derive(Copy, Clone, Default, Debug, Eq, PartialEq)]
pub enum AvailableSpace {
/// The amount of space available is the specified number of pixels

View file

@ -34,6 +34,9 @@ use std::{
panic::{self, RefUnwindSafe},
};
/// Run the given test function with the configured parameters.
/// This is intended for use with the `gpui::test` macro
/// and generally should not be used directly.
pub fn run_test(
mut num_iterations: u64,
max_retries: usize,
@ -78,6 +81,7 @@ pub fn run_test(
}
}
/// A test struct for converting an observation callback into a stream.
pub struct Observation<T> {
rx: channel::Receiver<T>,
_subscription: Subscription,

View file

@ -377,7 +377,7 @@ impl TextSystem {
Ok(lines)
}
pub fn finish_frame(&self, reused_views: &FxHashSet<EntityId>) {
pub(crate) fn finish_frame(&self, reused_views: &FxHashSet<EntityId>) {
self.line_layout_cache.finish_frame(reused_views)
}

View file

@ -1,6 +1,6 @@
use crate::{
black, fill, point, px, size, BorrowWindow, Bounds, Hsla, LineLayout, Pixels, Point, Result,
SharedString, UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout,
black, fill, point, px, size, Bounds, ElementContext, Hsla, LineLayout, Pixels, Point, Result,
SharedString, UnderlineStyle, WrapBoundary, WrappedLineLayout,
};
use derive_more::{Deref, DerefMut};
use smallvec::SmallVec;
@ -24,6 +24,7 @@ pub struct ShapedLine {
}
impl ShapedLine {
/// The length of the line in utf-8 bytes.
pub fn len(&self) -> usize {
self.layout.len
}
@ -32,7 +33,7 @@ impl ShapedLine {
&self,
origin: Point<Pixels>,
line_height: Pixels,
cx: &mut WindowContext,
cx: &mut ElementContext,
) -> Result<()> {
paint_line(
origin,
@ -65,7 +66,7 @@ impl WrappedLine {
&self,
origin: Point<Pixels>,
line_height: Pixels,
cx: &mut WindowContext,
cx: &mut ElementContext,
) -> Result<()> {
paint_line(
origin,
@ -86,7 +87,7 @@ fn paint_line(
line_height: Pixels,
decoration_runs: &[DecorationRun],
wrap_boundaries: &[WrapBoundary],
cx: &mut WindowContext<'_>,
cx: &mut ElementContext<'_>,
) -> Result<()> {
let padding_top = (line_height - layout.ascent - layout.descent) / 2.;
let baseline_offset = point(px(0.), padding_top + layout.ascent);

View file

@ -1,10 +1,8 @@
#![deny(missing_docs)]
use crate::{
seal::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, BorrowWindow,
Bounds, ContentMask, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView,
IntoElement, LayoutId, Model, Pixels, Point, Render, Size, StackingOrder, Style, TextStyle,
ViewContext, VisualContext, WeakModel, WindowContext,
seal::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, Bounds,
ContentMask, Element, ElementContext, ElementId, Entity, EntityId, Flatten, FocusHandle,
FocusableView, IntoElement, LayoutId, Model, Pixels, Point, Render, Size, StackingOrder, Style,
TextStyle, ViewContext, VisualContext, WeakModel,
};
use anyhow::{Context, Result};
use std::{
@ -96,7 +94,7 @@ impl<V: Render> Element for View<V> {
fn request_layout(
&mut self,
_state: Option<Self::State>,
cx: &mut WindowContext,
cx: &mut ElementContext,
) -> (LayoutId, Self::State) {
cx.with_view_id(self.entity_id(), |cx| {
let mut element = self.update(cx, |view, cx| view.render(cx).into_any_element());
@ -105,7 +103,7 @@ impl<V: Render> Element for View<V> {
})
}
fn paint(&mut self, _: Bounds<Pixels>, element: &mut Self::State, cx: &mut WindowContext) {
fn paint(&mut self, _: Bounds<Pixels>, element: &mut Self::State, cx: &mut ElementContext) {
cx.paint_view(self.entity_id(), |cx| element.take().unwrap().paint(cx));
}
}
@ -204,7 +202,7 @@ impl<V> Eq for WeakView<V> {}
#[derive(Clone, Debug)]
pub struct AnyView {
model: AnyModel,
request_layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement),
request_layout: fn(&AnyView, &mut ElementContext) -> (LayoutId, AnyElement),
cache: bool,
}
@ -252,7 +250,7 @@ impl AnyView {
&self,
origin: Point<Pixels>,
available_space: Size<AvailableSpace>,
cx: &mut WindowContext,
cx: &mut ElementContext,
) {
cx.paint_view(self.entity_id(), |cx| {
cx.with_absolute_element_offset(origin, |cx| {
@ -280,7 +278,7 @@ impl Element for AnyView {
fn request_layout(
&mut self,
state: Option<Self::State>,
cx: &mut WindowContext,
cx: &mut ElementContext,
) -> (LayoutId, Self::State) {
cx.with_view_id(self.entity_id(), |cx| {
if self.cache {
@ -301,7 +299,7 @@ impl Element for AnyView {
})
}
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut ElementContext) {
cx.paint_view(self.entity_id(), |cx| {
if !self.cache {
state.element.take().unwrap().paint(cx);
@ -365,7 +363,7 @@ impl IntoElement for AnyView {
/// A weak, dynamically-typed view handle that does not prevent the view from being released.
pub struct AnyWeakView {
model: AnyWeakModel,
layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement),
layout: fn(&AnyView, &mut ElementContext) -> (LayoutId, AnyElement),
}
impl AnyWeakView {
@ -404,11 +402,11 @@ impl std::fmt::Debug for AnyWeakView {
}
mod any_view {
use crate::{AnyElement, AnyView, IntoElement, LayoutId, Render, WindowContext};
use crate::{AnyElement, AnyView, ElementContext, IntoElement, LayoutId, Render};
pub(crate) fn request_layout<V: 'static + Render>(
view: &AnyView,
cx: &mut WindowContext,
cx: &mut ElementContext,
) -> (LayoutId, AnyElement) {
let view = view.clone().downcast::<V>().unwrap();
let mut element = view.update(cx, |view, cx| view.render(cx).into_any_element());

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -102,7 +102,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut WindowContext) {
cx.spawn(|mut cx| async move {
let (journal_dir, entry_path) = create_entry.await?;
let (workspace, _) = cx
.update(|_, cx| workspace::open_paths(&[journal_dir], &app_state, None, cx))?
.update(|cx| workspace::open_paths(&[journal_dir], &app_state, None, cx))?
.await?;
let opened = workspace

View file

@ -3007,7 +3007,7 @@ impl BufferSnapshot {
groups.sort_by(|(id_a, group_a), (id_b, group_b)| {
let a_start = &group_a.entries[group_a.primary_ix].range.start;
let b_start = &group_b.entries[group_b.primary_ix].range.start;
a_start.cmp(b_start, self).then_with(|| id_a.cmp(&id_b))
a_start.cmp(b_start, self).then_with(|| id_a.cmp(id_b))
});
groups

View file

@ -194,7 +194,7 @@ impl DiagnosticSet {
.range
.start
.cmp(&group_b.entries[group_b.primary_ix].range.start, buffer)
.then_with(|| id_a.cmp(&id_b))
.then_with(|| id_a.cmp(id_b))
});
}

View file

@ -379,8 +379,11 @@ pub trait LspAdapter: 'static + Send + Sync {
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CodeLabel {
/// The text to display.
pub text: String,
/// Syntax highlighting runs.
pub runs: Vec<(Range<usize>, HighlightId)>,
/// The portion of the text that should be used in fuzzy filtering.
pub filter_range: Range<usize>,
}
@ -849,7 +852,7 @@ impl LanguageRegistry {
let mut state = self.state.write();
state.theme = Some(theme.clone());
for language in &state.languages {
language.set_theme(&theme.syntax());
language.set_theme(theme.syntax());
}
}
@ -1163,7 +1166,7 @@ impl LanguageRegistryState {
fn add(&mut self, language: Arc<Language>) {
if let Some(theme) = self.theme.as_ref() {
language.set_theme(&theme.syntax());
language.set_theme(theme.syntax());
}
self.languages.push(language);
self.version += 1;

View file

@ -91,6 +91,8 @@ pub struct LanguageSettings {
pub extend_comment_on_newline: bool,
/// Inlay hint related settings.
pub inlay_hints: InlayHintSettings,
/// Whether to automatically close brackets.
pub use_autoclose: bool,
}
/// The settings for [GitHub Copilot](https://github.com/features/copilot).
@ -208,6 +210,11 @@ pub struct LanguageSettingsContent {
/// Inlay hint related settings.
#[serde(default)]
pub inlay_hints: Option<InlayHintSettings>,
/// Whether to automatically type closing characters for you. For example,
/// when you type (, Zed will automatically add a closing ) at the correct position.
///
/// Default: true
pub use_autoclose: Option<bool>,
}
/// The contents of the GitHub Copilot settings.
@ -421,7 +428,7 @@ impl settings::Settings for AllLanguageSettings {
let mut languages = HashMap::default();
for (language_name, settings) in &default_value.languages {
let mut language_settings = defaults.clone();
merge_settings(&mut language_settings, &settings);
merge_settings(&mut language_settings, settings);
languages.insert(language_name.clone(), language_settings);
}
@ -461,7 +468,7 @@ impl settings::Settings for AllLanguageSettings {
languages
.entry(language_name.clone())
.or_insert_with(|| defaults.clone()),
&user_language_settings,
user_language_settings,
);
}
}
@ -540,6 +547,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
merge(&mut settings.tab_size, src.tab_size);
merge(&mut settings.hard_tabs, src.hard_tabs);
merge(&mut settings.soft_wrap, src.soft_wrap);
merge(&mut settings.use_autoclose, src.use_autoclose);
merge(&mut settings.show_wrap_guides, src.show_wrap_guides);
merge(&mut settings.wrap_guides, src.wrap_guides.clone());

View file

@ -155,7 +155,7 @@ pub async fn parse_markdown_block(
let mut current_language = None;
let mut list_stack = Vec::new();
for event in Parser::new_ext(&markdown, Options::all()) {
for event in Parser::new_ext(markdown, Options::all()) {
let prev_len = text.len();
match event {
Event::Text(t) => {

View file

@ -283,7 +283,7 @@ impl SyntaxSnapshot {
depth,
position: edit_range.start,
};
if target.cmp(&cursor.start(), text).is_gt() {
if target.cmp(cursor.start(), text).is_gt() {
let slice = cursor.slice(&target, Bias::Left, text);
layers.append(slice, text);
}
@ -368,7 +368,7 @@ impl SyntaxSnapshot {
cursor.next(text);
}
layers.append(cursor.suffix(&text), &text);
layers.append(cursor.suffix(text), text);
drop(cursor);
self.layers = layers;
}
@ -433,7 +433,7 @@ impl SyntaxSnapshot {
let max_depth = self.layers.summary().max_depth;
let mut cursor = self.layers.cursor::<SyntaxLayerSummary>();
cursor.next(&text);
cursor.next(text);
let mut layers = SumTree::new();
let mut changed_regions = ChangeRegionSet::default();
@ -471,17 +471,17 @@ impl SyntaxSnapshot {
};
let mut done = cursor.item().is_none();
while !done && position.cmp(&cursor.end(text), &text).is_gt() {
while !done && position.cmp(&cursor.end(text), text).is_gt() {
done = true;
let bounded_position = SyntaxLayerPositionBeforeChange {
position: position.clone(),
change: changed_regions.start_position(),
};
if bounded_position.cmp(&cursor.start(), &text).is_gt() {
if bounded_position.cmp(cursor.start(), text).is_gt() {
let slice = cursor.slice(&bounded_position, Bias::Left, text);
if !slice.is_empty() {
layers.append(slice, &text);
layers.append(slice, text);
if changed_regions.prune(cursor.end(text), text) {
done = false;
}
@ -491,7 +491,7 @@ impl SyntaxSnapshot {
while position.cmp(&cursor.end(text), text).is_gt() {
let Some(layer) = cursor.item() else { break };
if changed_regions.intersects(&layer, text) {
if changed_regions.intersects(layer, text) {
if let SyntaxLayerContent::Parsed { language, .. } = &layer.content {
log::trace!(
"discard layer. language:{}, range:{:?}. changed_regions:{:?}",
@ -529,7 +529,7 @@ impl SyntaxSnapshot {
if layer.range.to_offset(text) == (step_start_byte..step_end_byte)
&& layer.content.language_id() == step.language.id()
{
cursor.next(&text);
cursor.next(text);
} else {
old_layer = None;
}
@ -561,7 +561,7 @@ impl SyntaxSnapshot {
log::trace!(
"existing layer. language:{}, start:{:?}, ranges:{:?}",
language.name(),
LogPoint(layer_start.to_point(&text)),
LogPoint(layer_start.to_point(text)),
LogIncludedRanges(&old_tree.included_ranges())
);
@ -584,7 +584,7 @@ impl SyntaxSnapshot {
insert_newlines_between_ranges(
changed_indices,
&mut included_ranges,
&text,
text,
step_start_byte,
step_start_point,
);
@ -701,7 +701,7 @@ impl SyntaxSnapshot {
range: step.range,
content,
},
&text,
text,
);
}
@ -860,7 +860,7 @@ impl<'a> SyntaxMapCaptures<'a> {
Some(grammar) => grammar,
None => continue,
};
let query = match query(&grammar) {
let query = match query(grammar) {
Some(query) => query,
None => continue,
};
@ -978,7 +978,7 @@ impl<'a> SyntaxMapMatches<'a> {
Some(grammar) => grammar,
None => continue,
};
let query = match query(&grammar) {
let query = match query(grammar) {
Some(query) => query,
None => continue,
};
@ -1087,7 +1087,7 @@ impl<'a> SyntaxMapMatchesLayer<'a> {
fn advance(&mut self) {
if let Some(mat) = self.matches.next() {
self.next_captures.clear();
self.next_captures.extend_from_slice(&mat.captures);
self.next_captures.extend_from_slice(mat.captures);
self.next_pattern_index = mat.pattern_index;
self.has_next = true;
} else {
@ -1517,7 +1517,7 @@ impl Eq for ParseStep {}
impl PartialOrd for ParseStep {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(&other))
Some(self.cmp(other))
}
}

View file

@ -345,7 +345,7 @@ impl LanguageServer {
if let Some(handler) = notification_handlers.lock().get_mut(msg.method) {
handler(
msg.id,
&msg.params.map(|params| params.get()).unwrap_or("null"),
msg.params.map(|params| params.get()).unwrap_or("null"),
cx.clone(),
);
} else {

View file

@ -2316,7 +2316,7 @@ impl MultiBufferSnapshot {
&self,
point: T,
) -> Option<(&BufferSnapshot, usize)> {
let offset = point.to_offset(&self);
let offset = point.to_offset(self);
let mut cursor = self.excerpts.cursor::<usize>();
cursor.seek(&offset, Bias::Right, &());
if cursor.item().is_none() {
@ -3694,7 +3694,7 @@ impl ExcerptId {
pub fn cmp(&self, other: &Self, snapshot: &MultiBufferSnapshot) -> cmp::Ordering {
let a = snapshot.excerpt_locator_for_id(*self);
let b = snapshot.excerpt_locator_for_id(*other);
a.cmp(&b).then_with(|| self.0.cmp(&other.0))
a.cmp(b).then_with(|| self.0.cmp(&other.0))
}
}

View file

@ -1532,7 +1532,7 @@ impl LspCommand for GetCompletions {
.iter()
.map(language::proto::serialize_completion)
.collect(),
version: serialize_version(&buffer_version),
version: serialize_version(buffer_version),
}
}
@ -1672,7 +1672,7 @@ impl LspCommand for GetCodeActions {
.iter()
.map(language::proto::serialize_code_action)
.collect(),
version: serialize_version(&buffer_version),
version: serialize_version(buffer_version),
}
}

View file

@ -34,16 +34,16 @@ use gpui::{
use itertools::Itertools;
use language::{
language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
point_to_lsp,
markdown, point_to_lsp,
proto::{
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
serialize_anchor, serialize_version, split_operations,
},
range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, Capability,
CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff,
Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile,
LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16,
TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
Documentation, Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName,
LocalFile, LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer,
PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
};
use log::error;
use lsp::{
@ -52,7 +52,7 @@ use lsp::{
};
use lsp_command::*;
use node_runtime::NodeRuntime;
use parking_lot::Mutex;
use parking_lot::{Mutex, RwLock};
use postage::watch;
use prettier_support::{DefaultPrettier, PrettierInstance};
use project_settings::{LspSettings, ProjectSettings};
@ -2947,7 +2947,7 @@ impl Project {
};
task.await;
this.update(&mut cx, |this, mut cx| {
this.update(&mut cx, |this, cx| {
let worktrees = this.worktrees.clone();
for worktree in worktrees {
let worktree = match worktree.upgrade() {
@ -2962,7 +2962,7 @@ impl Project {
root_path,
adapter.clone(),
language.clone(),
&mut cx,
cx,
);
}
})
@ -3544,8 +3544,8 @@ impl Project {
if errored {
log::warn!("test binary check failed");
let task = this
.update(&mut cx, move |this, mut cx| {
this.reinstall_language_server(language, adapter, server_id, &mut cx)
.update(&mut cx, move |this, cx| {
this.reinstall_language_server(language, adapter, server_id, cx)
})
.ok()
.flatten();
@ -3768,8 +3768,7 @@ impl Project {
}
};
if let Some(relative_glob_pattern) = relative_glob_pattern {
let literal_prefix =
glob_literal_prefix(&relative_glob_pattern);
let literal_prefix = glob_literal_prefix(relative_glob_pattern);
tree.as_local_mut()
.unwrap()
.add_path_prefix_to_scan(Path::new(literal_prefix).into());
@ -4231,9 +4230,9 @@ impl Project {
format_operation = Some(FormatOperation::Lsp(
Self::format_via_lsp(
&project,
&buffer,
buffer,
buffer_abs_path,
&language_server,
language_server,
tab_size,
&mut cx,
)
@ -4252,8 +4251,8 @@ impl Project {
format_operation = Self::format_via_external_command(
buffer,
buffer_abs_path,
&command,
&arguments,
command,
arguments,
&mut cx,
)
.await
@ -4276,9 +4275,9 @@ impl Project {
format_operation = Some(FormatOperation::Lsp(
Self::format_via_lsp(
&project,
&buffer,
buffer,
buffer_abs_path,
&language_server,
language_server,
tab_size,
&mut cx,
)
@ -4828,6 +4827,170 @@ impl Project {
}
}
pub fn resolve_completions(
&self,
completion_indices: Vec<usize>,
completions: Arc<RwLock<Box<[Completion]>>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<bool>> {
let client = self.client();
let language_registry = self.languages().clone();
let is_remote = self.is_remote();
let project_id = self.remote_id();
cx.spawn(move |this, mut cx| async move {
let mut did_resolve = false;
if is_remote {
let project_id =
project_id.ok_or_else(|| anyhow!("Remote project without remote_id"))?;
for completion_index in completion_indices {
let completions_guard = completions.read();
let completion = &completions_guard[completion_index];
if completion.documentation.is_some() {
continue;
}
did_resolve = true;
let server_id = completion.server_id;
let completion = completion.lsp_completion.clone();
drop(completions_guard);
Self::resolve_completion_documentation_remote(
project_id,
server_id,
completions.clone(),
completion_index,
completion,
client.clone(),
language_registry.clone(),
)
.await;
}
} else {
for completion_index in completion_indices {
let completions_guard = completions.read();
let completion = &completions_guard[completion_index];
if completion.documentation.is_some() {
continue;
}
let server_id = completion.server_id;
let completion = completion.lsp_completion.clone();
drop(completions_guard);
let server = this
.read_with(&mut cx, |project, _| {
project.language_server_for_id(server_id)
})
.ok()
.flatten();
let Some(server) = server else {
continue;
};
did_resolve = true;
Self::resolve_completion_documentation_local(
server,
completions.clone(),
completion_index,
completion,
language_registry.clone(),
)
.await;
}
}
Ok(did_resolve)
})
}
async fn resolve_completion_documentation_local(
server: Arc<lsp::LanguageServer>,
completions: Arc<RwLock<Box<[Completion]>>>,
completion_index: usize,
completion: lsp::CompletionItem,
language_registry: Arc<LanguageRegistry>,
) {
let can_resolve = server
.capabilities()
.completion_provider
.as_ref()
.and_then(|options| options.resolve_provider)
.unwrap_or(false);
if !can_resolve {
return;
}
let request = server.request::<lsp::request::ResolveCompletionItem>(completion);
let Some(completion_item) = request.await.log_err() else {
return;
};
if let Some(lsp_documentation) = completion_item.documentation {
let documentation = language::prepare_completion_documentation(
&lsp_documentation,
&language_registry,
None, // TODO: Try to reasonably work out which language the completion is for
)
.await;
let mut completions = completions.write();
let completion = &mut completions[completion_index];
completion.documentation = Some(documentation);
} else {
let mut completions = completions.write();
let completion = &mut completions[completion_index];
completion.documentation = Some(Documentation::Undocumented);
}
}
async fn resolve_completion_documentation_remote(
project_id: u64,
server_id: LanguageServerId,
completions: Arc<RwLock<Box<[Completion]>>>,
completion_index: usize,
completion: lsp::CompletionItem,
client: Arc<Client>,
language_registry: Arc<LanguageRegistry>,
) {
let request = proto::ResolveCompletionDocumentation {
project_id,
language_server_id: server_id.0 as u64,
lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(),
};
let Some(response) = client
.request(request)
.await
.context("completion documentation resolve proto request")
.log_err()
else {
return;
};
if response.text.is_empty() {
let mut completions = completions.write();
let completion = &mut completions[completion_index];
completion.documentation = Some(Documentation::Undocumented);
}
let documentation = if response.is_markdown {
Documentation::MultiLineMarkdown(
markdown::parse_markdown(&response.text, &language_registry, None).await,
)
} else if response.text.lines().count() <= 1 {
Documentation::SingleLine(response.text)
} else {
Documentation::MultiLinePlainText(response.text)
};
let mut completions = completions.write();
let completion = &mut completions[completion_index];
completion.documentation = Some(documentation);
}
pub fn apply_additional_edits_for_completion(
&self,
buffer_handle: Model<Buffer>,
@ -5683,7 +5846,7 @@ impl Project {
snapshot.file().map(|file| file.path().as_ref()),
) {
query
.search(&snapshot, None)
.search(snapshot, None)
.await
.iter()
.map(|range| {
@ -6526,7 +6689,7 @@ impl Project {
snapshot.repository_and_work_directory_for_path(&path)?;
let repo = snapshot.get_local_repo(&repo)?;
let relative_path = path.strip_prefix(&work_directory).ok()?;
let base_text = repo.repo_ptr.lock().load_index_text(&relative_path);
let base_text = repo.repo_ptr.lock().load_index_text(relative_path);
Some((buffer, base_text))
})
.collect::<Vec<_>>()
@ -6659,7 +6822,7 @@ impl Project {
for (_, _, path_summary) in
self.diagnostic_summaries(include_ignored, cx)
.filter(|(path, _, _)| {
let worktree = self.entry_for_path(&path, cx).map(|entry| entry.is_ignored);
let worktree = self.entry_for_path(path, cx).map(|entry| entry.is_ignored);
include_ignored || worktree == Some(false)
})
{
@ -6685,7 +6848,7 @@ impl Project {
})
})
.filter(move |(path, _, _)| {
let worktree = self.entry_for_path(&path, cx).map(|entry| entry.is_ignored);
let worktree = self.entry_for_path(path, cx).map(|entry| entry.is_ignored);
include_ignored || worktree == Some(false)
})
}

View file

@ -61,7 +61,7 @@ impl Project {
if let Some(python_settings) = &python_settings.as_option() {
let activate_script_path =
self.find_activate_script_path(&python_settings, working_directory);
self.find_activate_script_path(python_settings, working_directory);
self.activate_python_virtual_environment(
activate_script_path,
&terminal_handle,

View file

@ -1437,7 +1437,7 @@ impl LocalWorktree {
if let Err(e) = self.client.send(proto::UpdateDiagnosticSummary {
project_id,
worktree_id: cx.entity_id().as_u64(),
summary: Some(summary.to_proto(server_id, &path)),
summary: Some(summary.to_proto(server_id, path)),
}) {
return Task::ready(Err(e));
}
@ -2309,7 +2309,7 @@ impl LocalSnapshot {
impl BackgroundScannerState {
fn should_scan_directory(&self, entry: &Entry) -> bool {
(!entry.is_external && !entry.is_ignored)
|| entry.path.file_name() == Some(&*DOT_GIT)
|| entry.path.file_name() == Some(*DOT_GIT)
|| self.scanned_dirs.contains(&entry.id) // If we've ever scanned it, keep scanning
|| self
.paths_to_scan
@ -3374,7 +3374,7 @@ impl BackgroundScanner {
let mut is_git_related = false;
if let Some(dot_git_dir) = abs_path
.ancestors()
.find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT))
.find(|ancestor| ancestor.file_name() == Some(*DOT_GIT))
{
let dot_git_path = dot_git_dir
.strip_prefix(&root_canonical_path)
@ -3772,7 +3772,7 @@ impl BackgroundScanner {
for entry in &mut new_entries {
state.reuse_entry_id(entry);
if entry.is_dir() {
if state.should_scan_directory(&entry) {
if state.should_scan_directory(entry) {
job_ix += 1;
} else {
log::debug!("defer scanning directory {:?}", entry.path);
@ -3814,9 +3814,9 @@ impl BackgroundScanner {
abs_paths
.iter()
.map(|abs_path| async move {
let metadata = self.fs.metadata(&abs_path).await?;
let metadata = self.fs.metadata(abs_path).await?;
if let Some(metadata) = metadata {
let canonical_path = self.fs.canonicalize(&abs_path).await?;
let canonical_path = self.fs.canonicalize(abs_path).await?;
anyhow::Ok(Some((metadata, canonical_path)))
} else {
Ok(None)
@ -3864,7 +3864,7 @@ impl BackgroundScanner {
fs_entry.is_external = !canonical_path.starts_with(&root_canonical_path);
if !is_dir && !fs_entry.is_ignored {
if let Some((work_dir, repo)) = state.snapshot.local_repo_for_path(&path) {
if let Some((work_dir, repo)) = state.snapshot.local_repo_for_path(path) {
if let Ok(repo_path) = path.strip_prefix(work_dir.0) {
let repo_path = RepoPath(repo_path.into());
let repo = repo.repo_ptr.lock();
@ -3884,7 +3884,7 @@ impl BackgroundScanner {
state.insert_entry(fs_entry, self.fs.as_ref());
}
Ok(None) => {
self.remove_repo_path(&path, &mut state.snapshot);
self.remove_repo_path(path, &mut state.snapshot);
}
Err(err) => {
// TODO - create a special 'error' entry in the entries tree to mark this

View file

@ -21,6 +21,7 @@ sum_tree = { path = "../sum_tree" }
theme = { path = "../theme" }
language = { path = "../language" }
util = { path = "../util" }
ui = { path = "../ui" }
anyhow.workspace = true
futures.workspace = true
lazy_static.workspace = true

View file

@ -6,6 +6,7 @@ use gpui::{
use language::{HighlightId, Language, LanguageRegistry};
use std::{ops::Range, sync::Arc};
use theme::ActiveTheme;
use ui::LinkPreview;
use util::RangeExt;
#[derive(Debug, Clone, PartialEq, Eq)]
@ -64,7 +65,7 @@ impl RichText {
},
Highlight::Id(id) => HighlightStyle {
background_color: Some(code_background),
..id.style(&theme.syntax()).unwrap_or_default()
..id.style(theme.syntax()).unwrap_or_default()
},
Highlight::Highlight(highlight) => *highlight,
Highlight::Mention => HighlightStyle {
@ -84,6 +85,18 @@ impl RichText {
let link_urls = self.link_urls.clone();
move |ix, cx| cx.open_url(&link_urls[ix])
})
.tooltip({
let link_ranges = self.link_ranges.clone();
let link_urls = self.link_urls.clone();
move |idx, cx| {
for (ix, range) in link_ranges.iter().enumerate() {
if range.contains(&idx) {
return Some(LinkPreview::new(&link_urls[ix], cx));
}
}
None
}
})
.into_any_element()
}
}
@ -107,7 +120,7 @@ pub fn render_markdown_mut(
let mut list_stack = Vec::new();
let options = Options::all();
for (event, source_range) in Parser::new_ext(&block, options).into_offset_iter() {
for (event, source_range) in Parser::new_ext(block, options).into_offset_iter() {
let prev_len = text.len();
match event {
Event::Text(t) => {
@ -237,7 +250,7 @@ pub fn render_markdown_mut(
_ => {}
},
Event::HardBreak => text.push('\n'),
Event::SoftBreak => text.push(' '),
Event::SoftBreak => text.push('\n'),
_ => {}
}
}

View file

@ -533,7 +533,7 @@ impl SettingsStore {
}
if let Some(local_settings) =
setting_value.deserialize_setting(&local_settings).log_err()
setting_value.deserialize_setting(local_settings).log_err()
{
paths_stack.push(Some((*root_id, path.as_ref())));
user_settings_stack.push(local_settings);
@ -697,8 +697,7 @@ fn update_value_in_json_text<'a>(
if let Some(new_object) = new_value.as_object_mut() {
new_object.retain(|_, v| !v.is_null());
}
let (range, replacement) =
replace_value_in_json_text(text, &key_path, tab_size, &new_value);
let (range, replacement) = replace_value_in_json_text(text, key_path, tab_size, &new_value);
text.replace_range(range.clone(), &replacement);
edits.push((range, replacement));
}

View file

@ -67,8 +67,8 @@ impl StoryContainer {
}
impl ParentElement for StoryContainer {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
&mut self.children
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
self.children.extend(elements)
}
}
@ -372,7 +372,7 @@ impl RenderOnce for StorySection {
}
impl ParentElement for StorySection {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
&mut self.children
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
self.children.extend(elements)
}
}

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