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

View file

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

View file

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

View file

@ -72,6 +72,9 @@
// Whether to use additional LSP queries to format (and amend) the code after // Whether to use additional LSP queries to format (and amend) the code after
// every "trigger" symbol input, defined by LSP server capabilities. // every "trigger" symbol input, defined by LSP server capabilities.
"use_on_type_format": true, "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 // Controls whether copilot provides suggestion immediately
// or waits for a `copilot::Toggle` // or waits for a `copilot::Toggle`
"show_copilot_suggestions": true, "show_copilot_suggestions": true,

View file

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

View file

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

View file

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

View file

@ -1310,7 +1310,7 @@ impl Client {
drop(state); drop(state);
if let Some(handler) = handler { 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(); let client_id = self.id();
log::debug!( log::debug!(
"rpc message received. client_id:{}, sender_id:{:?}, type:{}", "rpc message received. client_id:{}, sender_id:{:?}, type:{}",

View file

@ -38,8 +38,9 @@ struct TelemetryState {
flush_events_task: Option<Task<()>>, flush_events_task: Option<Task<()>>,
log_file: Option<NamedTempFile>, log_file: Option<NamedTempFile>,
is_staff: Option<bool>, is_staff: Option<bool>,
first_event_datetime: Option<DateTime<Utc>>, first_event_date_time: Option<DateTime<Utc>>,
event_coalescer: EventCoalescer, event_coalescer: EventCoalescer,
max_queue_size: usize,
} }
const EVENTS_URL_PATH: &'static str = "/api/events"; const EVENTS_URL_PATH: &'static str = "/api/events";
@ -69,14 +70,14 @@ struct EventWrapper {
event: Event, event: Event,
} }
#[derive(Serialize, Debug)] #[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum AssistantKind { pub enum AssistantKind {
Panel, Panel,
Inline, Inline,
} }
#[derive(Serialize, Debug)] #[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum Event { pub enum Event {
Editor { Editor {
@ -168,8 +169,9 @@ impl Telemetry {
flush_events_task: None, flush_events_task: None,
log_file: None, log_file: None,
is_staff: None, is_staff: None,
first_event_datetime: None, first_event_date_time: None,
event_coalescer: EventCoalescer::new(), event_coalescer: EventCoalescer::new(),
max_queue_size: MAX_QUEUE_LEN,
})); }));
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
@ -310,7 +312,7 @@ impl Telemetry {
operation, operation,
copilot_enabled, copilot_enabled,
copilot_enabled_for_language, 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) self.report_event(event)
@ -326,7 +328,7 @@ impl Telemetry {
suggestion_id, suggestion_id,
suggestion_accepted, suggestion_accepted,
file_extension, 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) self.report_event(event)
@ -342,7 +344,7 @@ impl Telemetry {
conversation_id, conversation_id,
kind, kind,
model, 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) self.report_event(event)
@ -358,7 +360,7 @@ impl Telemetry {
operation, operation,
room_id, room_id,
channel_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) self.report_event(event)
@ -368,7 +370,7 @@ impl Telemetry {
let event = Event::Cpu { let event = Event::Cpu {
usage_as_percentage, usage_as_percentage,
core_count, 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) self.report_event(event)
@ -382,26 +384,36 @@ impl Telemetry {
let event = Event::Memory { let event = Event::Memory {
memory_in_bytes, memory_in_bytes,
virtual_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) self.report_event(event)
} }
pub fn report_app_event(self: &Arc<Self>, operation: String) { 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 { let event = Event::App {
operation, 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) { pub fn report_setting_event(self: &Arc<Self>, setting: &'static str, value: String) {
let event = Event::Setting { let event = Event::Setting {
setting, setting,
value, 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) self.report_event(event)
@ -416,7 +428,7 @@ impl Telemetry {
let event = Event::Edit { let event = Event::Edit {
duration: end.timestamp_millis() - start.timestamp_millis(), duration: end.timestamp_millis() - start.timestamp_millis(),
environment, 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); self.report_event(event);
@ -427,22 +439,21 @@ impl Telemetry {
let event = Event::Action { let event = Event::Action {
source, source,
action, 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) 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(); let mut state = self.state.lock();
match state.first_event_datetime { match state.first_event_date_time {
Some(first_event_datetime) => { Some(first_event_date_time) => {
let now: DateTime<Utc> = Utc::now(); date_time.timestamp_millis() - first_event_date_time.timestamp_millis()
now.timestamp_millis() - first_event_datetime.timestamp_millis()
} }
None => { None => {
state.first_event_datetime = Some(Utc::now()); state.first_event_date_time = Some(date_time);
0 0
} }
} }
@ -468,7 +479,7 @@ impl Telemetry {
state.events_queue.push(EventWrapper { signed_in, event }); state.events_queue.push(EventWrapper { signed_in, event });
if state.installation_id.is_some() { 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); drop(state);
self.flush_events(); self.flush_events();
} }
@ -489,7 +500,7 @@ impl Telemetry {
pub fn flush_events(self: &Arc<Self>) { pub fn flush_events(self: &Arc<Self>) {
let mut state = self.state.lock(); 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); let mut events = mem::take(&mut state.events_queue);
state.flush_events_task.take(); state.flush_events_task.take();
drop(state); drop(state);
@ -548,3 +559,159 @@ impl Telemetry {
.detach(); .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 futures.workspace = true
lazy_static.workspace = true lazy_static.workspace = true
log.workspace = true log.workspace = true
parking_lot.workspace = true
schemars.workspace = true schemars.workspace = true
postage.workspace = true postage.workspace = true
serde.workspace = true serde.workspace = true

View file

@ -18,7 +18,7 @@ use project::Fs;
use rich_text::RichText; use rich_text::RichText;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::Settings; use settings::Settings;
use std::sync::Arc; use std::{sync::Arc, time::Duration};
use time::{OffsetDateTime, UtcOffset}; use time::{OffsetDateTime, UtcOffset};
use ui::{ use ui::{
popover_menu, prelude::*, Avatar, Button, ContextMenu, IconButton, IconName, KeyBinding, Label, 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 last_message = active_chat.message(ix.saturating_sub(1));
let this_message = active_chat.message(ix).clone(); let this_message = active_chat.message(ix).clone();
let is_continuation_from_previous = last_message.id != this_message.id let duration_since_last_message = this_message.timestamp - last_message.timestamp;
&& last_message.sender.id == this_message.sender.id; 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 let ChannelMessageId::Saved(id) = this_message.id {
if this_message if this_message
@ -325,8 +328,6 @@ impl ChatPanel {
Self::render_markdown_with_mentions(&self.languages, self.client.id(), &message) 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 belongs_to_user = Some(message.sender.id) == self.client.user_id();
let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) = let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
(message.id, belongs_to_user || is_admin) (message.id, belongs_to_user || is_admin)
@ -349,23 +350,21 @@ impl ChatPanel {
.when(!is_continuation_from_previous, |this| { .when(!is_continuation_from_previous, |this| {
this.pt_3().child( this.pt_3().child(
h_flex() h_flex()
.child( .text_ui_sm()
div().absolute().child( .child(div().absolute().child(
Avatar::new(message.sender.avatar_uri.clone()) Avatar::new(message.sender.avatar_uri.clone()).size(cx.rem_size()),
.size(cx.rem_size() * 1.5), ))
),
)
.child( .child(
div() div()
.pl(cx.rem_size() * 1.5 + px(6.0)) .pl(cx.rem_size() + px(6.0))
.pr(px(8.0)) .pr(px(8.0))
.font_weight(FontWeight::BOLD) .font_weight(FontWeight::BOLD)
.child(Label::new(message.sender.github_login.clone())), .child(Label::new(message.sender.github_login.clone())),
) )
.child( .child(
Label::new(format_timestamp( Label::new(format_timestamp(
OffsetDateTime::now_utc(),
message.timestamp, message.timestamp,
now,
self.local_timezone, self.local_timezone,
)) ))
.size(LabelSize::Small) .size(LabelSize::Small)
@ -597,7 +596,7 @@ impl Render for ChatPanel {
el.child( el.child(
div() div()
.rounded_md() .rounded_md()
.h_7() .h_6()
.w_full() .w_full()
.bg(cx.theme().colors().editor_background), .bg(cx.theme().colors().editor_background),
) )
@ -671,28 +670,44 @@ impl Panel for ChatPanel {
impl EventEmitter<PanelEvent> for ChatPanel {} impl EventEmitter<PanelEvent> for ChatPanel {}
fn format_timestamp( fn format_timestamp(
mut timestamp: OffsetDateTime, reference: OffsetDateTime,
mut now: OffsetDateTime, timestamp: OffsetDateTime,
local_timezone: UtcOffset, timezone: UtcOffset,
) -> String { ) -> String {
timestamp = timestamp.to_offset(local_timezone); let timestamp_local = timestamp.to_offset(timezone);
now = now.to_offset(local_timezone); let timestamp_local_hour = timestamp_local.hour();
let today = now.date(); let hour_12 = match timestamp_local_hour {
let date = timestamp.date(); 0 => 12, // Midnight
let mut hour = timestamp.hour(); 13..=23 => timestamp_local_hour - 12, // PM hours
let mut part = "am"; _ => timestamp_local_hour, // AM hours
if hour > 12 { };
hour -= 12; let meridiem = if timestamp_local_hour >= 12 {
part = "pm"; "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)
} else { } 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)] #[cfg(test)]
@ -701,6 +716,7 @@ mod tests {
use gpui::HighlightStyle; use gpui::HighlightStyle;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use rich_text::Highlight; use rich_text::Highlight;
use time::{Date, OffsetDateTime, Time, UtcOffset};
use util::test::marked_text_ranges; use util::test::marked_text_ranges;
#[gpui::test] #[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 channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
use client::UserId; use client::UserId;
use collections::HashMap; use collections::HashMap;
use editor::{AnchorRangeExt, Editor, EditorElement, EditorStyle}; use editor::{AnchorRangeExt, CompletionProvider, Editor, EditorElement, EditorStyle};
use fuzzy::StringMatchCandidate;
use gpui::{ use gpui::{
AsyncWindowContext, FocusableView, FontStyle, FontWeight, HighlightStyle, IntoElement, Model, AsyncWindowContext, FocusableView, FontStyle, FontWeight, HighlightStyle, IntoElement, Model,
Render, SharedString, Task, TextStyle, View, ViewContext, WeakView, WhiteSpace, 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 lazy_static::lazy_static;
use parking_lot::RwLock;
use project::search::SearchQuery; use project::search::SearchQuery;
use settings::Settings; use settings::Settings;
use std::{sync::Arc, time::Duration};
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::prelude::*; use ui::{prelude::*, UiTextSize};
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50); const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
@ -31,6 +36,43 @@ pub struct MessageEditor {
channel_id: Option<ChannelId>, 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 { impl MessageEditor {
pub fn new( pub fn new(
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
@ -38,8 +80,11 @@ impl MessageEditor {
editor: View<Editor>, editor: View<Editor>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Self { ) -> Self {
let this = cx.view().downgrade();
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, 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 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( async fn find_mentions(
this: WeakView<MessageEditor>, this: WeakView<MessageEditor>,
buffer: BufferSnapshot, buffer: BufferSnapshot,
@ -216,7 +326,7 @@ impl Render for MessageEditor {
}, },
font_family: settings.ui_font.family.clone(), font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features, font_features: settings.ui_font.features,
font_size: rems(0.875).into(), font_size: UiTextSize::Small.rems().into(),
font_weight: FontWeight::NORMAL, font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal, font_style: FontStyle::Normal,
line_height: relative(1.3).into(), line_height: relative(1.3).into(),

View file

@ -85,7 +85,14 @@ impl Render for CollabTitlebarItem {
.gap_1() .gap_1()
.children(self.render_project_host(cx)) .children(self.render_project_host(cx))
.child(self.render_project_name(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( .when_some(
current_user.clone().zip(client.peer_id()).zip(room.clone()), current_user.clone().zip(client.peer_id()).zip(room.clone()),
|this, ((current_user, peer_id), room)| { |this, ((current_user, peer_id), room)| {

View file

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

View file

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

View file

@ -311,7 +311,7 @@ impl PickerDelegate for CommandPaletteDelegate {
let action = command.action; let action = command.action;
cx.focus(&self.previous_focus_handle); cx.focus(&self.previous_focus_handle);
cx.window_context() 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); .detach_and_log_err(cx);
self.dismissed(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; .browser_download_url;
let mut response = http let mut response = http
.get(&url, Default::default(), true) .get(url, Default::default(), true)
.await .await
.map_err(|err| anyhow!("error downloading copilot release: {}", err))?; .map_err(|err| anyhow!("error downloading copilot release: {}", err))?;
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); 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 { cx.spawn(|mut cx| async move {
task.await; 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 workspace
.update(&mut cx, |workspace, cx| match copilot.read(cx).status() { .update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
Status::Authorized => workspace.show_toast( 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)> { fn editor_blocks(editor: &View<Editor>, cx: &mut WindowContext) -> Vec<(u32, SharedString)> {
let editor_view = editor.clone();
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx); let snapshot = editor.snapshot(cx);
snapshot snapshot
.blocks_in_range(0..snapshot.max_point().row()) .blocks_in_range(0..snapshot.max_point().row())
.enumerate() .enumerate()
.filter_map(|(ix, (row, block))| { .filter_map(|(ix, (row, block))| {
let name = match block { let name: SharedString = match block {
TransformBlock::Custom(block) => block TransformBlock::Custom(block) => cx.with_element_context({
.render(&mut BlockContext { let editor_view = editor_view.clone();
view_context: cx, |cx| -> Option<SharedString> {
anchor_x: px(0.), block
gutter_padding: px(0.), .render(&mut BlockContext {
gutter_width: px(0.), context: cx,
line_height: px(0.), anchor_x: px(0.),
em_width: px(0.), gutter_padding: px(0.),
block_id: ix, gutter_width: px(0.),
editor_style: &editor::EditorStyle::default(), line_height: px(0.),
}) em_width: px(0.),
.inner_id()? block_id: ix,
.try_into() view: editor_view,
.ok()?, editor_style: &editor::EditorStyle::default(),
})
.inner_id()?
.try_into()
.ok()
}
})?,
TransformBlock::ExcerptHeader { TransformBlock::ExcerptHeader {
starts_new_buffer, .. starts_new_buffer, ..

View file

@ -4,7 +4,7 @@ use super::{
}; };
use crate::{Anchor, Editor, EditorStyle, ExcerptId, ExcerptRange, ToPoint as _}; use crate::{Anchor, Editor, EditorStyle, ExcerptId, ExcerptRange, ToPoint as _};
use collections::{Bound, HashMap, HashSet}; use collections::{Bound, HashMap, HashSet};
use gpui::{AnyElement, Pixels, ViewContext}; use gpui::{AnyElement, ElementContext, Pixels, View};
use language::{BufferSnapshot, Chunk, Patch, Point}; use language::{BufferSnapshot, Chunk, Patch, Point};
use parking_lot::Mutex; use parking_lot::Mutex;
use std::{ use std::{
@ -81,7 +81,8 @@ pub enum BlockStyle {
} }
pub struct BlockContext<'a, 'b> { 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 anchor_x: Pixels,
pub gutter_width: Pixels, pub gutter_width: Pixels,
pub gutter_padding: Pixels, pub gutter_padding: Pixels,
@ -933,16 +934,16 @@ impl BlockDisposition {
} }
impl<'a> Deref for BlockContext<'a, '_> { impl<'a> Deref for BlockContext<'a, '_> {
type Target = ViewContext<'a, Editor>; type Target = ElementContext<'a>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
self.view_context self.context
} }
} }
impl DerefMut for BlockContext<'_, '_> { impl DerefMut for BlockContext<'_, '_> {
fn deref_mut(&mut self) -> &mut Self::Target { 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 aho_corasick::AhoCorasick;
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
use blink_manager::BlinkManager; use blink_manager::BlinkManager;
use client::{Client, Collaborator, ParticipantIndex}; use client::{Collaborator, ParticipantIndex};
use clock::ReplicaId; use clock::ReplicaId;
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
use convert_case::{Case, Casing}; use convert_case::{Case, Casing};
@ -57,9 +57,10 @@ use gpui::{
div, impl_actions, point, prelude::*, px, relative, rems, size, uniform_list, Action, div, impl_actions, point, prelude::*, px, relative, rems, size, uniform_list, Action,
AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context, AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context,
DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight,
HighlightStyle, Hsla, InputHandler, InteractiveText, KeyContext, Model, MouseButton, HighlightStyle, Hsla, InteractiveText, KeyContext, Model, MouseButton, ParentElement, Pixels,
ParentElement, Pixels, Render, SharedString, Styled, StyledText, Subscription, Task, TextStyle, Render, SharedString, Styled, StyledText, Subscription, Task, TextStyle,
UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext, UniformListScrollHandle, View, ViewContext, ViewInputHandler, VisualContext, WeakView,
WhiteSpace, WindowContext,
}; };
use highlight_matching_bracket::refresh_matching_bracket_highlights; use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState}; use hover_popover::{hide_hover, HoverState};
@ -71,8 +72,7 @@ use language::{
language_settings::{self, all_language_settings, InlayHintSettings}, language_settings::{self, all_language_settings, InlayHintSettings},
markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CodeAction, markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CodeAction,
CodeLabel, Completion, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, CodeLabel, Completion, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize,
Language, LanguageRegistry, LanguageServerName, OffsetRangeExt, Point, Selection, Language, LanguageServerName, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId,
SelectionGoal, TransactionId,
}; };
use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState}; use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState};
@ -88,7 +88,7 @@ use ordered_float::OrderedFloat;
use parking_lot::RwLock; use parking_lot::RwLock;
use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction}; use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction};
use rand::prelude::*; use rand::prelude::*;
use rpc::proto::{self, *}; use rpc::proto::*;
use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide}; use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide};
use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection}; use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -365,6 +365,7 @@ pub struct Editor {
active_diagnostics: Option<ActiveDiagnosticGroup>, active_diagnostics: Option<ActiveDiagnosticGroup>,
soft_wrap_mode_override: Option<language_settings::SoftWrap>, soft_wrap_mode_override: Option<language_settings::SoftWrap>,
project: Option<Model<Project>>, project: Option<Model<Project>>,
completion_provider: Option<Box<dyn CompletionProvider>>,
collaboration_hub: Option<Box<dyn CollaborationHub>>, collaboration_hub: Option<Box<dyn CollaborationHub>>,
blink_manager: Model<BlinkManager>, blink_manager: Model<BlinkManager>,
show_cursor_names: bool, show_cursor_names: bool,
@ -408,6 +409,7 @@ pub struct Editor {
style: Option<EditorStyle>, style: Option<EditorStyle>,
editor_actions: Vec<Box<dyn Fn(&mut ViewContext<Self>)>>, editor_actions: Vec<Box<dyn Fn(&mut ViewContext<Self>)>>,
show_copilot_suggestions: bool, show_copilot_suggestions: bool,
use_autoclose: bool,
} }
pub struct EditorSnapshot { pub struct EditorSnapshot {
@ -731,85 +733,21 @@ impl CompletionsMenu {
return None; return None;
} }
let Some(project) = editor.project.clone() else { let Some(provider) = editor.completion_provider.as_ref() else {
return None; return None;
}; };
let client = project.read(cx).client(); let resolve_task = provider.resolve_completions(
let language_registry = project.read(cx).languages().clone(); self.matches.iter().map(|m| m.candidate_id).collect(),
self.completions.clone(),
cx,
);
let is_remote = project.read(cx).is_remote(); return Some(cx.spawn(move |this, mut cx| async move {
let project_id = project.read(cx).remote_id(); if let Some(true) = resolve_task.await.log_err() {
this.update(&mut cx, |_, cx| cx.notify()).ok();
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());
}
} }
})) }));
} }
fn attempt_resolve_selected_completion_documentation( fn attempt_resolve_selected_completion_documentation(
@ -826,146 +764,16 @@ impl CompletionsMenu {
let Some(project) = project else { let Some(project) = project else {
return; return;
}; };
let language_registry = project.read(cx).languages().clone();
let completions = self.completions.clone(); let resolve_task = project.update(cx, |project, cx| {
let completions_guard = completions.read(); project.resolve_completions(vec![completion_index], self.completions.clone(), cx)
let completion = &completions_guard[completion_index]; });
if completion.documentation.is_some() { cx.spawn(move |this, mut cx| async move {
return; if let Some(true) = resolve_task.await.log_err() {
} this.update(&mut cx, |_, cx| cx.notify()).ok();
}
let server_id = completion.server_id; })
let completion = completion.lsp_completion.clone(); .detach();
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);
}
} }
fn visible(&self) -> bool { fn visible(&self) -> bool {
@ -1574,6 +1382,7 @@ impl Editor {
ime_transaction: Default::default(), ime_transaction: Default::default(),
active_diagnostics: None, active_diagnostics: None,
soft_wrap_mode_override, soft_wrap_mode_override,
completion_provider: project.clone().map(|project| Box::new(project) as _),
collaboration_hub: project.clone().map(|project| Box::new(project) as _), collaboration_hub: project.clone().map(|project| Box::new(project) as _),
project, project,
blink_manager: blink_manager.clone(), blink_manager: blink_manager.clone(),
@ -1603,6 +1412,7 @@ impl Editor {
keymap_context_layers: Default::default(), keymap_context_layers: Default::default(),
input_enabled: true, input_enabled: true,
read_only: false, read_only: false,
use_autoclose: true,
leader_peer_id: None, leader_peer_id: None,
remote_id: None, remote_id: None,
hover_state: Default::default(), hover_state: Default::default(),
@ -1806,6 +1616,10 @@ impl Editor {
self.collaboration_hub = Some(hub); 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> { pub fn placeholder_text(&self) -> Option<&str> {
self.placeholder_text.as_deref() self.placeholder_text.as_deref()
} }
@ -1880,6 +1694,10 @@ impl Editor {
self.read_only = read_only; 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) { pub fn set_show_copilot_suggestions(&mut self, show_copilot_suggestions: bool) {
self.show_copilot_suggestions = show_copilot_suggestions; self.show_copilot_suggestions = show_copilot_suggestions;
} }
@ -2478,7 +2296,12 @@ impl Editor {
), ),
&bracket_pair.start[..prefix_len], &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); let anchor = snapshot.anchor_before(selection.end);
new_selections.push((selection.map(|_| anchor), text.len())); new_selections.push((selection.map(|_| anchor), text.len()));
new_autoclose_regions.push(( new_autoclose_regions.push((
@ -3252,9 +3075,7 @@ impl Editor {
return; return;
} }
let project = if let Some(project) = self.project.clone() { let Some(provider) = self.completion_provider.as_ref() else {
project
} else {
return; return;
}; };
@ -3270,9 +3091,7 @@ impl Editor {
}; };
let query = Self::completion_query(&self.buffer.read(cx).read(cx), position.clone()); let query = Self::completion_query(&self.buffer.read(cx).read(cx), position.clone());
let completions = project.update(cx, |project, cx| { let completions = provider.completions(&buffer, buffer_position, cx);
project.completions(&buffer, buffer_position, cx)
});
let id = post_inc(&mut self.next_completion_id); let id = post_inc(&mut self.next_completion_id);
let task = cx.spawn(|this, mut cx| { let task = cx.spawn(|this, mut cx| {
@ -3381,6 +3200,7 @@ impl Editor {
let buffer_handle = completions_menu.buffer; let buffer_handle = completions_menu.buffer;
let completions = completions_menu.completions.read(); let completions = completions_menu.completions.read();
let completion = completions.get(mat.candidate_id)?; let completion = completions.get(mat.candidate_id)?;
cx.stop_propagation();
let snippet; let snippet;
let text; let text;
@ -3477,15 +3297,13 @@ impl Editor {
this.refresh_copilot_suggestions(true, cx); this.refresh_copilot_suggestions(true, cx);
}); });
let project = self.project.clone()?; let provider = self.completion_provider.as_ref()?;
let apply_edits = project.update(cx, |project, cx| { let apply_edits = provider.apply_additional_edits_for_completion(
project.apply_additional_edits_for_completion( buffer_handle,
buffer_handle, completion.clone(),
completion.clone(), true,
true, cx,
cx, );
)
});
Some(cx.foreground_executor().spawn(async move { Some(cx.foreground_executor().spawn(async move {
apply_edits.await?; apply_edits.await?;
Ok(()) Ok(())
@ -3572,7 +3390,7 @@ impl Editor {
let replica_id = this.update(&mut cx, |this, cx| this.replica_id(cx))?; let replica_id = this.update(&mut cx, |this, cx| this.replica_id(cx))?;
let mut entries = transaction.0.into_iter().collect::<Vec<_>>(); let mut entries = transaction.0.into_iter().collect::<Vec<_>>();
cx.update(|_, cx| { cx.update(|cx| {
entries.sort_unstable_by_key(|(buffer, _)| { entries.sort_unstable_by_key(|(buffer, _)| {
buffer.read(cx).file().map(|f| f.path().clone()) buffer.read(cx).file().map(|f| f.path().clone())
}); });
@ -3907,7 +3725,7 @@ impl Editor {
self.show_cursor_names = true; self.show_cursor_names = true;
cx.notify(); cx.notify();
cx.spawn(|this, mut cx| async move { 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.update(&mut cx, |this, cx| {
this.show_cursor_names = false; this.show_cursor_names = false;
cx.notify() cx.notify()
@ -4094,7 +3912,7 @@ impl Editor {
gutter_hovered: bool, gutter_hovered: bool,
_line_height: Pixels, _line_height: Pixels,
_gutter_margin: Pixels, _gutter_margin: Pixels,
cx: &mut ViewContext<Self>, editor_view: View<Editor>,
) -> Vec<Option<IconButton>> { ) -> Vec<Option<IconButton>> {
fold_data fold_data
.iter() .iter()
@ -4104,14 +3922,19 @@ impl Editor {
.map(|(fold_status, buffer_row, active)| { .map(|(fold_status, buffer_row, active)| {
(active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| { (active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| {
IconButton::new(ix as usize, ui::IconName::ChevronDown) IconButton::new(ix as usize, ui::IconName::ChevronDown)
.on_click(cx.listener(move |editor, _e, cx| match fold_status { .on_click({
FoldStatus::Folded => { let view = editor_view.clone();
editor.unfold_at(&UnfoldAt { buffer_row }, cx); 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_color(ui::Color::Muted)
.icon_size(ui::IconSize::Small) .icon_size(ui::IconSize::Small)
.selected(fold_status == FoldStatus::Folded) .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( fn inlay_hint_settings(
location: Anchor, location: Anchor,
snapshot: &MultiBufferSnapshot, snapshot: &MultiBufferSnapshot,
@ -9300,7 +9183,7 @@ impl Render for Editor {
} }
} }
impl InputHandler for Editor { impl ViewInputHandler for Editor {
fn text_for_range( fn text_for_range(
&mut self, &mut self,
range_utf16: Range<usize>, range_utf16: Range<usize>,
@ -9697,10 +9580,10 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
.size(ButtonSize::Compact) .size(ButtonSize::Compact)
.style(ButtonStyle::Transparent) .style(ButtonStyle::Transparent)
.visible_on_hover(group_id) .visible_on_hover(group_id)
.on_click(cx.listener({ .on_click({
let message = diagnostic.message.clone(); 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)), .tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)),
) )
.into_any_element() .into_any_element()

View file

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

View file

@ -247,7 +247,7 @@ fn show_hover(
}; };
// query the LSP for hover info // query the LSP for hover info
let hover_request = cx.update(|_, cx| { let hover_request = cx.update(|cx| {
project.update(cx, |project, cx| { project.update(cx, |project, cx| {
project.hover(&buffer, buffer_position, 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 :( // This check is required because index.get_path() unwraps internally :(
check_path_to_repo_path_errors(relative_file_path)?; 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, Some(entry) => entry.id,
None => return Ok(None), None => return Ok(None),
}; };
@ -81,7 +81,7 @@ impl GitRepository for LibGitRepository {
Ok(Some(String::from_utf8(content)?)) Ok(Some(String::from_utf8(content)?))
} }
match logic(&self, relative_file_path) { match logic(self, relative_file_path) {
Ok(value) => return value, Ok(value) => return value,
Err(err) => log::error!("Error loading head text: {:?}", err), 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 { fn matches_index(repo: &LibGitRepository, path: &RepoPath, mtime: SystemTime) -> bool {
if let Some(index) = repo.index().log_err() { 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 let Some(mtime) = mtime.duration_since(SystemTime::UNIX_EPOCH).log_err() {
if entry.mtime.seconds() == mtime.as_secs() as i32 if entry.mtime.seconds() == mtime.as_secs() as i32
&& entry.mtime.nanoseconds() == mtime.subsec_nanos() && entry.mtime.nanoseconds() == mtime.subsec_nanos()

View file

@ -165,7 +165,7 @@ impl BufferDiff {
let mut tree = SumTree::new(); let mut tree = SumTree::new();
let buffer_text = buffer.as_rope().to_string(); 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 { if let Some(patch) = patch {
let mut divergence = 0; let mut divergence = 0;

View file

@ -40,14 +40,25 @@ use std::any::{Any, TypeId};
/// register_action!(Paste); /// register_action!(Paste);
/// ``` /// ```
pub trait Action: 'static { pub trait Action: 'static {
/// Clone the action into a new box
fn boxed_clone(&self) -> Box<dyn Action>; fn boxed_clone(&self) -> Box<dyn Action>;
/// Cast the action to the any type
fn as_any(&self) -> &dyn Any; 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; fn partial_eq(&self, action: &dyn Action) -> bool;
/// Get the name of this action, for displaying in UI
fn name(&self) -> &str; fn name(&self) -> &str;
/// Get the name of this action for debugging
fn debug_name() -> &'static str fn debug_name() -> &'static str
where where
Self: Sized; 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>> fn build(value: serde_json::Value) -> Result<Box<dyn Action>>
where where
Self: Sized; Self: Sized;
@ -62,6 +73,7 @@ impl std::fmt::Debug for dyn Action {
} }
impl dyn Action { impl dyn Action {
/// Get the type id of this action
pub fn type_id(&self) -> TypeId { pub fn type_id(&self) -> TypeId {
self.as_any().type_id() self.as_any().type_id()
} }
@ -170,6 +182,7 @@ impl ActionRegistry {
macro_rules! actions { macro_rules! actions {
($namespace:path, [ $($name:ident),* $(,)? ]) => { ($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)] #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, gpui::private::serde_derive::Deserialize)]
#[serde(crate = "gpui::private::serde")] #[serde(crate = "gpui::private::serde")]
pub struct $name; pub struct $name;

View file

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

View file

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

View file

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

View file

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

View file

@ -2,6 +2,7 @@ use anyhow::bail;
use serde::de::{self, Deserialize, Deserializer, Visitor}; use serde::de::{self, Deserialize, Deserializer, Visitor};
use std::fmt; use std::fmt;
/// Convert an RGB hex color code number to a color type
pub fn rgb<C: From<Rgba>>(hex: u32) -> C { pub fn rgb<C: From<Rgba>>(hex: u32) -> C {
let r = ((hex >> 16) & 0xFF) as f32 / 255.0; let r = ((hex >> 16) & 0xFF) as f32 / 255.0;
let g = ((hex >> 8) & 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() Rgba { r, g, b, a: 1.0 }.into()
} }
/// Convert an RGBA hex color code number to [`Rgba`]
pub fn rgba(hex: u32) -> Rgba { pub fn rgba(hex: u32) -> Rgba {
let r = ((hex >> 24) & 0xFF) as f32 / 255.0; let r = ((hex >> 24) & 0xFF) as f32 / 255.0;
let g = ((hex >> 16) & 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 } Rgba { r, g, b, a }
} }
/// An RGBA color
#[derive(PartialEq, Clone, Copy, Default)] #[derive(PartialEq, Clone, Copy, Default)]
pub struct Rgba { pub struct Rgba {
/// The red component of the color, in the range 0.0 to 1.0
pub r: f32, pub r: f32,
/// The green component of the color, in the range 0.0 to 1.0
pub g: f32, pub g: f32,
/// The blue component of the color, in the range 0.0 to 1.0
pub b: f32, pub b: f32,
/// The alpha component of the color, in the range 0.0 to 1.0
pub a: f32, pub a: f32,
} }
@ -32,6 +39,8 @@ impl fmt::Debug for Rgba {
} }
impl 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 { pub fn blend(&self, other: Rgba) -> Self {
if other.a >= 1.0 { if other.a >= 1.0 {
other other
@ -165,12 +174,20 @@ impl TryFrom<&'_ str> for Rgba {
} }
} }
/// An HSLA color
#[derive(Default, Copy, Clone, Debug)] #[derive(Default, Copy, Clone, Debug)]
#[repr(C)] #[repr(C)]
pub struct Hsla { pub struct Hsla {
/// Hue, in a range from 0 to 1
pub h: f32, pub h: f32,
/// Saturation, in a range from 0 to 1
pub s: f32, pub s: f32,
/// Lightness, in a range from 0 to 1
pub l: f32, pub l: f32,
/// Alpha, in a range from 0 to 1
pub a: f32, 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 {} impl Eq for Hsla {}
/// Construct an [`Hsla`] object from plain values
pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla { pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
Hsla { Hsla {
h: h.clamp(0., 1.), 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 { pub fn black() -> Hsla {
Hsla { Hsla {
h: 0., h: 0.,
@ -253,6 +242,7 @@ pub fn black() -> Hsla {
} }
} }
/// Transparent black in [`Hsla`]
pub fn transparent_black() -> Hsla { pub fn transparent_black() -> Hsla {
Hsla { Hsla {
h: 0., h: 0.,
@ -262,6 +252,7 @@ pub fn transparent_black() -> Hsla {
} }
} }
/// Pure white in [`Hsla`]
pub fn white() -> Hsla { pub fn white() -> Hsla {
Hsla { Hsla {
h: 0., h: 0.,
@ -271,6 +262,7 @@ pub fn white() -> Hsla {
} }
} }
/// The color red in [`Hsla`]
pub fn red() -> Hsla { pub fn red() -> Hsla {
Hsla { Hsla {
h: 0., h: 0.,
@ -280,6 +272,7 @@ pub fn red() -> Hsla {
} }
} }
/// The color blue in [`Hsla`]
pub fn blue() -> Hsla { pub fn blue() -> Hsla {
Hsla { Hsla {
h: 0.6, h: 0.6,
@ -289,6 +282,7 @@ pub fn blue() -> Hsla {
} }
} }
/// The color green in [`Hsla`]
pub fn green() -> Hsla { pub fn green() -> Hsla {
Hsla { Hsla {
h: 0.33, h: 0.33,
@ -298,6 +292,7 @@ pub fn green() -> Hsla {
} }
} }
/// The color yellow in [`Hsla`]
pub fn yellow() -> Hsla { pub fn yellow() -> Hsla {
Hsla { Hsla {
h: 0.16, h: 0.16,
@ -308,6 +303,41 @@ pub fn yellow() -> Hsla {
} }
impl 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. /// Returns true if the HSLA color is fully transparent, false otherwise.
pub fn is_transparent(&self) -> bool { pub fn is_transparent(&self) -> bool {
self.a == 0.0 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 { pub fn grayscale(&self) -> Self {
Hsla { Hsla {
h: self.h, 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::{ 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, Pixels, Point, Size, ViewContext, WindowContext, ELEMENT_ARENA,
}; };
use derive_more::{Deref, DerefMut}; use derive_more::{Deref, DerefMut};
pub(crate) use smallvec::SmallVec; 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. /// 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. /// Elements form a tree and are laid out according to web-based layout rules, as implemented by Taffy.
/// 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, see the module-level documentation
/// You can create custom elements by implementing this trait. /// for more details.
pub trait Element: 'static + IntoElement { 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; 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( fn request_layout(
&mut self, &mut self,
state: Option<Self::State>, state: Option<Self::State>,
cx: &mut WindowContext, cx: &mut ElementContext,
) -> (LayoutId, Self::State); ) -> (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 { fn into_any(self) -> AnyElement {
AnyElement::new(self) AnyElement::new(self)
} }
@ -29,6 +72,7 @@ pub trait Element: 'static + IntoElement {
/// Implemented by any type that can be converted into an element. /// Implemented by any type that can be converted into an element.
pub trait IntoElement: Sized { pub trait IntoElement: Sized {
/// The specific type of element into which the implementing type is converted. /// 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; type Element: Element;
/// The [`ElementId`] of self once converted into an [`Element`]. /// The [`ElementId`] of self once converted into an [`Element`].
@ -51,8 +95,8 @@ pub trait IntoElement: Sized {
self, self,
origin: Point<Pixels>, origin: Point<Pixels>,
available_space: Size<T>, available_space: Size<T>,
cx: &mut WindowContext, cx: &mut ElementContext,
f: impl FnOnce(&mut <Self::Element as Element>::State, &mut WindowContext) -> R, f: impl FnOnce(&mut <Self::Element as Element>::State, &mut ElementContext) -> R,
) -> R ) -> R
where where
T: Clone + Default + Debug + Into<AvailableSpace>, T: Clone + Default + Debug + Into<AvailableSpace>,
@ -81,7 +125,10 @@ pub trait IntoElement: Sized {
impl<T: IntoElement> FluentBuilder for T {} 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 { pub trait Render: 'static + Sized {
/// Render this view into an element tree.
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement; 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. /// 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 { 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; 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 { 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 fn child(mut self, child: impl IntoElement) -> Self
where where
Self: Sized, Self: Sized,
{ {
self.children_mut().push(child.into_element().into_any()); self.extend(std::iter::once(child.into_element().into_any()));
self self
} }
/// Add multiple child elements to this element.
fn children(mut self, children: impl IntoIterator<Item = impl IntoElement>) -> Self fn children(mut self, children: impl IntoIterator<Item = impl IntoElement>) -> Self
where where
Self: Sized, Self: Sized,
{ {
self.children_mut() self.extend(children.into_iter().map(|child| child.into_any_element()));
.extend(children.into_iter().map(|child| child.into_any_element()));
self 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>); pub struct Component<C: RenderOnce>(Option<C>);
impl<C: RenderOnce> Component<C> { impl<C: RenderOnce> Component<C> {
/// Create a new component from the given RenderOnce type.
pub fn new(component: C) -> Self { pub fn new(component: C) -> Self {
Component(Some(component)) Component(Some(component))
} }
@ -132,14 +193,19 @@ impl<C: RenderOnce> Element for Component<C> {
fn request_layout( fn request_layout(
&mut self, &mut self,
_: Option<Self::State>, _: Option<Self::State>,
cx: &mut WindowContext, cx: &mut ElementContext,
) -> (LayoutId, Self::State) { ) -> (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); let layout_id = element.request_layout(cx);
(layout_id, element) (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) 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)] #[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)]
pub struct GlobalElementId(SmallVec<[ElementId; 32]>); pub(crate) struct GlobalElementId(SmallVec<[ElementId; 32]>);
trait ElementObject { trait ElementObject {
fn element_id(&self) -> Option<ElementId>; 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( fn measure(
&mut self, &mut self,
available_space: Size<AvailableSpace>, available_space: Size<AvailableSpace>,
cx: &mut WindowContext, cx: &mut ElementContext,
) -> Size<Pixels>; ) -> Size<Pixels>;
fn draw( fn draw(
&mut self, &mut self,
origin: Point<Pixels>, origin: Point<Pixels>,
available_space: Size<AvailableSpace>, 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>, element: Option<E>,
phase: ElementDrawPhase<E::State>, phase: ElementDrawPhase<E::State>,
} }
@ -213,7 +281,7 @@ impl<E: Element> DrawableElement<E> {
self.element.as_ref()?.element_id() 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, frame_state) = if let Some(id) = self.element.as_ref().unwrap().element_id()
{ {
let layout_id = cx.with_element_state(id, |element_state, cx| { let layout_id = cx.with_element_state(id, |element_state, cx| {
@ -235,7 +303,7 @@ impl<E: Element> DrawableElement<E> {
layout_id 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 { match self.phase {
ElementDrawPhase::LayoutRequested { ElementDrawPhase::LayoutRequested {
layout_id, layout_id,
@ -280,7 +348,7 @@ impl<E: Element> DrawableElement<E> {
fn measure( fn measure(
&mut self, &mut self,
available_space: Size<AvailableSpace>, available_space: Size<AvailableSpace>,
cx: &mut WindowContext, cx: &mut ElementContext,
) -> Size<Pixels> { ) -> Size<Pixels> {
if matches!(&self.phase, ElementDrawPhase::Start) { if matches!(&self.phase, ElementDrawPhase::Start) {
self.request_layout(cx); self.request_layout(cx);
@ -321,7 +389,7 @@ impl<E: Element> DrawableElement<E> {
mut self, mut self,
origin: Point<Pixels>, origin: Point<Pixels>,
available_space: Size<AvailableSpace>, available_space: Size<AvailableSpace>,
cx: &mut WindowContext, cx: &mut ElementContext,
) -> Option<E::State> { ) -> Option<E::State> {
self.measure(available_space, cx); self.measure(available_space, cx);
cx.with_absolute_element_offset(origin, |cx| self.paint(cx)) cx.with_absolute_element_offset(origin, |cx| self.paint(cx))
@ -337,18 +405,18 @@ where
self.as_ref().unwrap().element_id() 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) 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); DrawableElement::paint(self.take().unwrap(), cx);
} }
fn measure( fn measure(
&mut self, &mut self,
available_space: Size<AvailableSpace>, available_space: Size<AvailableSpace>,
cx: &mut WindowContext, cx: &mut ElementContext,
) -> Size<Pixels> { ) -> Size<Pixels> {
DrawableElement::measure(self.as_mut().unwrap(), available_space, cx) DrawableElement::measure(self.as_mut().unwrap(), available_space, cx)
} }
@ -357,16 +425,17 @@ where
&mut self, &mut self,
origin: Point<Pixels>, origin: Point<Pixels>,
available_space: Size<AvailableSpace>, available_space: Size<AvailableSpace>,
cx: &mut WindowContext, cx: &mut ElementContext,
) { ) {
DrawableElement::draw(self.take().unwrap(), origin, available_space, cx); 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>); pub struct AnyElement(ArenaBox<dyn ElementObject>);
impl AnyElement { impl AnyElement {
pub fn new<E>(element: E) -> Self pub(crate) fn new<E>(element: E) -> Self
where where
E: 'static + Element, E: 'static + Element,
E::State: Any, E::State: Any,
@ -377,11 +446,14 @@ impl AnyElement {
AnyElement(element) 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) 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) self.0.paint(cx)
} }
@ -389,7 +461,7 @@ impl AnyElement {
pub fn measure( pub fn measure(
&mut self, &mut self,
available_space: Size<AvailableSpace>, available_space: Size<AvailableSpace>,
cx: &mut WindowContext, cx: &mut ElementContext,
) -> Size<Pixels> { ) -> Size<Pixels> {
self.0.measure(available_space, cx) self.0.measure(available_space, cx)
} }
@ -399,11 +471,12 @@ impl AnyElement {
&mut self, &mut self,
origin: Point<Pixels>, origin: Point<Pixels>,
available_space: Size<AvailableSpace>, available_space: Size<AvailableSpace>,
cx: &mut WindowContext, cx: &mut ElementContext,
) { ) {
self.0.draw(origin, available_space, cx) 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> { pub fn inner_id(&self) -> Option<ElementId> {
self.0.element_id() self.0.element_id()
} }
@ -415,13 +488,13 @@ impl Element for AnyElement {
fn request_layout( fn request_layout(
&mut self, &mut self,
_: Option<Self::State>, _: Option<Self::State>,
cx: &mut WindowContext, cx: &mut ElementContext,
) -> (LayoutId, Self::State) { ) -> (LayoutId, Self::State) {
let layout_id = self.request_layout(cx); let layout_id = self.request_layout(cx);
(layout_id, ()) (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) self.paint(cx)
} }
} }
@ -463,7 +536,7 @@ impl Element for () {
fn request_layout( fn request_layout(
&mut self, &mut self,
_state: Option<Self::State>, _state: Option<Self::State>,
cx: &mut WindowContext, cx: &mut ElementContext,
) -> (LayoutId, Self::State) { ) -> (LayoutId, Self::State) {
(cx.request_layout(&crate::Style::default(), None), ()) (cx.request_layout(&crate::Style::default(), None), ())
} }
@ -472,7 +545,7 @@ impl Element for () {
&mut self, &mut self,
_bounds: Bounds<Pixels>, _bounds: Bounds<Pixels>,
_state: &mut Self::State, _state: &mut Self::State,
_cx: &mut WindowContext, _cx: &mut ElementContext,
) { ) {
} }
} }

View file

@ -1,16 +1,20 @@
use refineable::Refineable as _; 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 { Canvas {
paint_callback: Some(Box::new(callback)), paint_callback: Some(Box::new(callback)),
style: StyleRefinement::default(), style: StyleRefinement::default(),
} }
} }
/// A canvas element, meant for accessing the low level paint API without defining a whole
/// custom element
pub struct Canvas { 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, style: StyleRefinement,
} }
@ -32,7 +36,7 @@ impl Element for Canvas {
fn request_layout( fn request_layout(
&mut self, &mut self,
_: Option<Self::State>, _: Option<Self::State>,
cx: &mut WindowContext, cx: &mut ElementContext,
) -> (crate::LayoutId, Self::State) { ) -> (crate::LayoutId, Self::State) {
let mut style = Style::default(); let mut style = Style::default();
style.refine(&self.style); style.refine(&self.style);
@ -40,7 +44,7 @@ impl Element for Canvas {
(layout_id, style) (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| { style.paint(bounds, cx, |cx| {
(self.paint_callback.take().unwrap())(&bounds, 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 std::sync::Arc;
use crate::{ 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, InteractiveElementState, Interactivity, IntoElement, LayoutId, Pixels, SharedUrl, Size,
StyleRefinement, Styled, WindowContext, StyleRefinement, Styled,
}; };
use futures::FutureExt; use futures::FutureExt;
use media::core_video::CVImageBuffer; use media::core_video::CVImageBuffer;
use util::ResultExt; use util::ResultExt;
/// A source of image content.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum ImageSource { pub enum ImageSource {
/// Image content will be loaded from provided URI at render time. /// Image content will be loaded from provided URI at render time.
Uri(SharedUrl), Uri(SharedUrl),
/// Cached image data
Data(Arc<ImageData>), Data(Arc<ImageData>),
// TODO: move surface definitions into mac platform module
/// A CoreVideo image buffer
Surface(CVImageBuffer), Surface(CVImageBuffer),
} }
@ -47,12 +51,14 @@ impl From<CVImageBuffer> for ImageSource {
} }
} }
/// An image element.
pub struct Img { pub struct Img {
interactivity: Interactivity, interactivity: Interactivity,
source: ImageSource, source: ImageSource,
grayscale: bool, grayscale: bool,
} }
/// Create a new image element.
pub fn img(source: impl Into<ImageSource>) -> Img { pub fn img(source: impl Into<ImageSource>) -> Img {
Img { Img {
interactivity: Interactivity::default(), interactivity: Interactivity::default(),
@ -62,6 +68,7 @@ pub fn img(source: impl Into<ImageSource>) -> Img {
} }
impl Img { impl Img {
/// Set the image to be displayed in grayscale.
pub fn grayscale(mut self, grayscale: bool) -> Self { pub fn grayscale(mut self, grayscale: bool) -> Self {
self.grayscale = grayscale; self.grayscale = grayscale;
self self
@ -74,7 +81,7 @@ impl Element for Img {
fn request_layout( fn request_layout(
&mut self, &mut self,
element_state: Option<Self::State>, element_state: Option<Self::State>,
cx: &mut WindowContext, cx: &mut ElementContext,
) -> (LayoutId, Self::State) { ) -> (LayoutId, Self::State) {
self.interactivity self.interactivity
.layout(element_state, cx, |style, cx| cx.request_layout(&style, [])) .layout(element_state, cx, |style, cx| cx.request_layout(&style, []))
@ -84,7 +91,7 @@ impl Element for Img {
&mut self, &mut self,
bounds: Bounds<Pixels>, bounds: Bounds<Pixels>,
element_state: &mut Self::State, element_state: &mut Self::State,
cx: &mut WindowContext, cx: &mut ElementContext,
) { ) {
let source = self.source.clone(); let source = self.source.clone();
self.interactivity.paint( 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::{ use crate::{
point, px, AnyElement, AvailableSpace, BorrowAppContext, BorrowWindow, Bounds, ContentMask, point, px, AnyElement, AvailableSpace, Bounds, ContentMask, DispatchPhase, Element,
DispatchPhase, Element, IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style, IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style, StyleRefinement, Styled,
StyleRefinement, Styled, WindowContext, WindowContext,
}; };
use collections::VecDeque; use collections::VecDeque;
use refineable::Refineable as _; use refineable::Refineable as _;
use std::{cell::RefCell, ops::Range, rc::Rc}; use std::{cell::RefCell, ops::Range, rc::Rc};
use sum_tree::{Bias, SumTree}; use sum_tree::{Bias, SumTree};
/// Construct a new list element
pub fn list(state: ListState) -> List { pub fn list(state: ListState) -> List {
List { List {
state, state,
@ -15,11 +24,13 @@ pub fn list(state: ListState) -> List {
} }
} }
/// A list element
pub struct List { pub struct List {
state: ListState, state: ListState,
style: StyleRefinement, style: StyleRefinement,
} }
/// The list state that views must hold on behalf of the list element.
#[derive(Clone)] #[derive(Clone)]
pub struct ListState(Rc<RefCell<StateInner>>); pub struct ListState(Rc<RefCell<StateInner>>);
@ -35,15 +46,24 @@ struct StateInner {
scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut WindowContext)>>, 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)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ListAlignment { pub enum ListAlignment {
/// The list is scrolling from top to bottom, like most lists.
Top, Top,
/// The list is scrolling from bottom to top, like a chat log.
Bottom, Bottom,
} }
/// A scroll event that has been converted to be in terms of the list's items.
pub struct ListScrollEvent { pub struct ListScrollEvent {
/// The range of items currently visible in the list, after applying the scroll event.
pub visible_range: Range<usize>, pub visible_range: Range<usize>,
/// The number of items that are currently visible in the list, after applying the scroll event.
pub count: usize, pub count: usize,
/// Whether the list has been scrolled.
pub is_scrolled: bool, pub is_scrolled: bool,
} }
@ -74,6 +94,11 @@ struct UnrenderedCount(usize);
struct Height(Pixels); struct Height(Pixels);
impl ListState { 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>( pub fn new<F>(
element_count: usize, element_count: usize,
orientation: ListAlignment, orientation: ListAlignment,
@ -111,10 +136,13 @@ impl ListState {
.extend((0..element_count).map(|_| ListItem::Unrendered), &()); .extend((0..element_count).map(|_| ListItem::Unrendered), &());
} }
/// The number of items in this list.
pub fn item_count(&self) -> usize { pub fn item_count(&self) -> usize {
self.0.borrow().items.summary().count 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) { pub fn splice(&self, old_range: Range<usize>, count: usize) {
let state = &mut *self.0.borrow_mut(); let state = &mut *self.0.borrow_mut();
@ -141,6 +169,7 @@ impl ListState {
state.items = new_heights; state.items = new_heights;
} }
/// Set a handler that will be called when the list is scrolled.
pub fn set_scroll_handler( pub fn set_scroll_handler(
&self, &self,
handler: impl FnMut(&ListScrollEvent, &mut WindowContext) + 'static, handler: impl FnMut(&ListScrollEvent, &mut WindowContext) + 'static,
@ -148,10 +177,12 @@ impl ListState {
self.0.borrow_mut().scroll_handler = Some(Box::new(handler)) 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 { pub fn logical_scroll_top(&self) -> ListOffset {
self.0.borrow().logical_scroll_top() self.0.borrow().logical_scroll_top()
} }
/// Scroll the list to the given offset
pub fn scroll_to(&self, mut scroll_top: ListOffset) { pub fn scroll_to(&self, mut scroll_top: ListOffset) {
let state = &mut *self.0.borrow_mut(); let state = &mut *self.0.borrow_mut();
let item_count = state.items.summary().count; let item_count = state.items.summary().count;
@ -163,6 +194,7 @@ impl ListState {
state.logical_scroll_top = Some(scroll_top); 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) { pub fn scroll_to_reveal_item(&self, ix: usize) {
let state = &mut *self.0.borrow_mut(); let state = &mut *self.0.borrow_mut();
@ -193,7 +225,8 @@ impl ListState {
state.logical_scroll_top = Some(scroll_top); 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>> { pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
let state = &*self.0.borrow(); 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)] #[derive(Debug, Clone, Copy, Default)]
pub struct ListOffset { pub struct ListOffset {
/// The index of an item in the list
pub item_ix: usize, pub item_ix: usize,
/// The number of pixels to offset from the item index.
pub offset_in_item: Pixels, pub offset_in_item: Pixels,
} }
@ -322,7 +359,7 @@ impl Element for List {
fn request_layout( fn request_layout(
&mut self, &mut self,
_state: Option<Self::State>, _state: Option<Self::State>,
cx: &mut crate::WindowContext, cx: &mut crate::ElementContext,
) -> (crate::LayoutId, Self::State) { ) -> (crate::LayoutId, Self::State) {
let mut style = Style::default(); let mut style = Style::default();
style.refine(&self.style); style.refine(&self.style);
@ -336,7 +373,7 @@ impl Element for List {
&mut self, &mut self,
bounds: Bounds<crate::Pixels>, bounds: Bounds<crate::Pixels>,
_state: &mut Self::State, _state: &mut Self::State,
cx: &mut crate::WindowContext, cx: &mut crate::ElementContext,
) { ) {
let state = &mut *self.state.0.borrow_mut(); let state = &mut *self.state.0.borrow_mut();

View file

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

View file

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

View file

@ -1,12 +1,19 @@
use crate::{ use crate::{
Bounds, DispatchPhase, Element, ElementId, HighlightStyle, IntoElement, LayoutId, ActiveTooltip, AnyTooltip, AnyView, Bounds, DispatchPhase, Element, ElementContext, ElementId,
MouseDownEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextRun, TextStyle, HighlightStyle, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
WhiteSpace, WindowContext, WrappedLine, Point, SharedString, Size, TextRun, TextStyle, WhiteSpace, WindowContext, WrappedLine,
TOOLTIP_DELAY,
}; };
use anyhow::anyhow; use anyhow::anyhow;
use parking_lot::{Mutex, MutexGuard}; use parking_lot::{Mutex, MutexGuard};
use smallvec::SmallVec; 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; use util::ResultExt;
impl Element for &'static str { impl Element for &'static str {
@ -15,14 +22,14 @@ impl Element for &'static str {
fn request_layout( fn request_layout(
&mut self, &mut self,
_: Option<Self::State>, _: Option<Self::State>,
cx: &mut WindowContext, cx: &mut ElementContext,
) -> (LayoutId, Self::State) { ) -> (LayoutId, Self::State) {
let mut state = TextState::default(); let mut state = TextState::default();
let layout_id = state.layout(SharedString::from(*self), None, cx); let layout_id = state.layout(SharedString::from(*self), None, cx);
(layout_id, state) (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) state.paint(bounds, self, cx)
} }
} }
@ -45,14 +52,14 @@ impl Element for SharedString {
fn request_layout( fn request_layout(
&mut self, &mut self,
_: Option<Self::State>, _: Option<Self::State>,
cx: &mut WindowContext, cx: &mut ElementContext,
) -> (LayoutId, Self::State) { ) -> (LayoutId, Self::State) {
let mut state = TextState::default(); let mut state = TextState::default();
let layout_id = state.layout(self.clone(), None, cx); let layout_id = state.layout(self.clone(), None, cx);
(layout_id, state) (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(); let text_str: &str = self.as_ref();
state.paint(bounds, text_str, cx) state.paint(bounds, text_str, cx)
} }
@ -81,6 +88,7 @@ pub struct StyledText {
} }
impl StyledText { impl StyledText {
/// Construct a new styled text element from the given string.
pub fn new(text: impl Into<SharedString>) -> Self { pub fn new(text: impl Into<SharedString>) -> Self {
StyledText { StyledText {
text: text.into(), 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( pub fn with_highlights(
mut self, mut self,
default_style: &TextStyle, default_style: &TextStyle,
@ -121,14 +131,14 @@ impl Element for StyledText {
fn request_layout( fn request_layout(
&mut self, &mut self,
_: Option<Self::State>, _: Option<Self::State>,
cx: &mut WindowContext, cx: &mut ElementContext,
) -> (LayoutId, Self::State) { ) -> (LayoutId, Self::State) {
let mut state = TextState::default(); let mut state = TextState::default();
let layout_id = state.layout(self.text.clone(), self.runs.take(), cx); let layout_id = state.layout(self.text.clone(), self.runs.take(), cx);
(layout_id, state) (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) state.paint(bounds, &self.text, cx)
} }
} }
@ -145,6 +155,7 @@ impl IntoElement for StyledText {
} }
} }
#[doc(hidden)]
#[derive(Default, Clone)] #[derive(Default, Clone)]
pub struct TextState(Arc<Mutex<Option<TextStateInner>>>); pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
@ -164,7 +175,7 @@ impl TextState {
&mut self, &mut self,
text: SharedString, text: SharedString,
runs: Option<Vec<TextRun>>, runs: Option<Vec<TextRun>>,
cx: &mut WindowContext, cx: &mut ElementContext,
) -> LayoutId { ) -> LayoutId {
let text_style = cx.text_style(); let text_style = cx.text_style();
let font_size = text_style.font_size.to_pixels(cx.rem_size()); let font_size = text_style.font_size.to_pixels(cx.rem_size());
@ -239,7 +250,7 @@ impl TextState {
layout_id 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 = self.lock();
let element_state = element_state let element_state = element_state
.as_ref() .as_ref()
@ -284,11 +295,14 @@ impl TextState {
} }
} }
/// A text element that can be interacted with.
pub struct InteractiveText { pub struct InteractiveText {
element_id: ElementId, element_id: ElementId,
text: StyledText, text: StyledText,
click_listener: click_listener:
Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut WindowContext<'_>)>>, 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>>, clickable_ranges: Vec<Range<usize>>,
} }
@ -297,21 +311,30 @@ struct InteractiveTextClickEvent {
mouse_up_index: usize, mouse_up_index: usize,
} }
#[doc(hidden)]
pub struct InteractiveTextState { pub struct InteractiveTextState {
text_state: TextState, text_state: TextState,
mouse_down_index: Rc<Cell<Option<usize>>>, 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 { impl InteractiveText {
/// Creates a new InteractiveText from the given text.
pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self { pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
Self { Self {
element_id: id.into(), element_id: id.into(),
text, text,
click_listener: None, click_listener: None,
hover_listener: None,
tooltip_builder: None,
clickable_ranges: Vec::new(), 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( pub fn on_click(
mut self, mut self,
ranges: Vec<Range<usize>>, ranges: Vec<Range<usize>>,
@ -328,6 +351,25 @@ impl InteractiveText {
self.clickable_ranges = ranges; self.clickable_ranges = ranges;
self 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 { impl Element for InteractiveText {
@ -336,16 +378,21 @@ impl Element for InteractiveText {
fn request_layout( fn request_layout(
&mut self, &mut self,
state: Option<Self::State>, state: Option<Self::State>,
cx: &mut WindowContext, cx: &mut ElementContext,
) -> (LayoutId, Self::State) { ) -> (LayoutId, Self::State) {
if let Some(InteractiveTextState { if let Some(InteractiveTextState {
mouse_down_index, .. mouse_down_index,
hovered_index,
active_tooltip,
..
}) = state }) = state
{ {
let (layout_id, text_state) = self.text.request_layout(None, cx); let (layout_id, text_state) = self.text.request_layout(None, cx);
let element_state = InteractiveTextState { let element_state = InteractiveTextState {
text_state, text_state,
mouse_down_index, mouse_down_index,
hovered_index,
active_tooltip,
}; };
(layout_id, element_state) (layout_id, element_state)
} else { } else {
@ -353,12 +400,14 @@ impl Element for InteractiveText {
let element_state = InteractiveTextState { let element_state = InteractiveTextState {
text_state, text_state,
mouse_down_index: Rc::default(), mouse_down_index: Rc::default(),
hovered_index: Rc::default(),
active_tooltip: Rc::default(),
}; };
(layout_id, element_state) (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() { if let Some(click_listener) = self.click_listener.take() {
let mouse_position = cx.mouse_position(); let mouse_position = cx.mouse_position();
if let Some(ix) = state.text_state.index_for_position(bounds, 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) 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::{ 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, ElementId, InteractiveElement, InteractiveElementState, Interactivity, IntoElement, LayoutId,
Pixels, Render, Size, StyleRefinement, Styled, View, ViewContext, WindowContext, 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 { pub struct UniformList {
id: ElementId, id: ElementId,
item_count: usize, item_count: usize,
@ -63,18 +70,22 @@ pub struct UniformList {
scroll_handle: Option<UniformListScrollHandle>, 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)] #[derive(Clone, Default)]
pub struct UniformListScrollHandle { pub struct UniformListScrollHandle {
deferred_scroll_to_item: Rc<RefCell<Option<usize>>>, deferred_scroll_to_item: Rc<RefCell<Option<usize>>>,
} }
impl UniformListScrollHandle { impl UniformListScrollHandle {
/// Create a new scroll handle to bind to a uniform list.
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
deferred_scroll_to_item: Rc::new(RefCell::new(None)), 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) { pub fn scroll_to_item(&mut self, ix: usize) {
self.deferred_scroll_to_item.replace(Some(ix)); self.deferred_scroll_to_item.replace(Some(ix));
} }
@ -86,6 +97,7 @@ impl Styled for UniformList {
} }
} }
#[doc(hidden)]
#[derive(Default)] #[derive(Default)]
pub struct UniformListState { pub struct UniformListState {
interactive: InteractiveElementState, interactive: InteractiveElementState,
@ -98,7 +110,7 @@ impl Element for UniformList {
fn request_layout( fn request_layout(
&mut self, &mut self,
state: Option<Self::State>, state: Option<Self::State>,
cx: &mut WindowContext, cx: &mut ElementContext,
) -> (LayoutId, Self::State) { ) -> (LayoutId, Self::State) {
let max_items = self.item_count; let max_items = self.item_count;
let item_size = state let item_size = state
@ -146,7 +158,7 @@ impl Element for UniformList {
&mut self, &mut self,
bounds: Bounds<crate::Pixels>, bounds: Bounds<crate::Pixels>,
element_state: &mut Self::State, element_state: &mut Self::State,
cx: &mut WindowContext, cx: &mut ElementContext,
) { ) {
let style = let style =
self.interactivity self.interactivity
@ -262,12 +274,13 @@ impl IntoElement for UniformList {
} }
impl UniformList { impl UniformList {
/// Selects a specific list item for measurement.
pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self { pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
self.item_to_measure_index = item_index.unwrap_or(0); self.item_to_measure_index = item_index.unwrap_or(0);
self 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 { if self.item_count == 0 {
return Size::default(); return Size::default();
} }
@ -284,6 +297,7 @@ impl UniformList {
item_to_measure.measure(available_space, cx) 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 { pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self {
self.scroll_handle = Some(handle); self.scroll_handle = Some(handle);
self self

View file

@ -21,11 +21,15 @@ use waker_fn::waker_fn;
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
use rand::rngs::StdRng; use rand::rngs::StdRng;
/// A pointer to the executor that is currently running,
/// for spawning background tasks.
#[derive(Clone)] #[derive(Clone)]
pub struct BackgroundExecutor { pub struct BackgroundExecutor {
dispatcher: Arc<dyn PlatformDispatcher>, dispatcher: Arc<dyn PlatformDispatcher>,
} }
/// A pointer to the executor that is currently running,
/// for spawning tasks on the main thread.
#[derive(Clone)] #[derive(Clone)]
pub struct ForegroundExecutor { pub struct ForegroundExecutor {
dispatcher: Arc<dyn PlatformDispatcher>, dispatcher: Arc<dyn PlatformDispatcher>,
@ -37,11 +41,14 @@ pub struct ForegroundExecutor {
/// It implements [`Future`] so you can `.await` on it. /// It implements [`Future`] so you can `.await` on it.
/// ///
/// If you drop a task it will be cancelled immediately. Calling [`Task::detach`] allows /// 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] #[must_use]
#[derive(Debug)] #[derive(Debug)]
pub enum Task<T> { pub enum Task<T> {
/// A task that is ready to return a value
Ready(Option<T>), Ready(Option<T>),
/// A task that is currently running.
Spawned(async_task::Task<T>), 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)] #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct TaskLabel(NonZeroUsize); pub struct TaskLabel(NonZeroUsize);
@ -97,6 +106,7 @@ impl Default for TaskLabel {
} }
impl TaskLabel { impl TaskLabel {
/// Construct a new task label.
pub fn new() -> Self { pub fn new() -> Self {
static NEXT_TASK_LABEL: AtomicUsize = AtomicUsize::new(1); static NEXT_TASK_LABEL: AtomicUsize = AtomicUsize::new(1);
Self(NEXT_TASK_LABEL.fetch_add(1, SeqCst).try_into().unwrap()) Self(NEXT_TASK_LABEL.fetch_add(1, SeqCst).try_into().unwrap())
@ -363,6 +373,7 @@ impl BackgroundExecutor {
/// ForegroundExecutor runs things on the main thread. /// ForegroundExecutor runs things on the main thread.
impl ForegroundExecutor { impl ForegroundExecutor {
/// Creates a new ForegroundExecutor from the given PlatformDispatcher.
pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self { pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
Self { Self {
dispatcher, dispatcher,
@ -411,13 +422,14 @@ impl<'a> Scope<'a> {
} }
} }
/// Spawn a future into this scope.
pub fn spawn<F>(&mut self, f: F) pub fn spawn<F>(&mut self, f: F)
where where
F: Future<Output = ()> + Send + 'a, F: Future<Output = ()> + Send + 'a,
{ {
let tx = self.tx.clone().unwrap(); 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. // dropping this `Scope` blocks until all of the futures have resolved.
let f = unsafe { let f = unsafe {
mem::transmute::< 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] #[macro_use]
mod action; mod action;
mod app; mod app;
@ -58,10 +86,10 @@ pub use elements::*;
pub use executor::*; pub use executor::*;
pub use geometry::*; pub use geometry::*;
pub use gpui_macros::{register_action, test, IntoElement, Render}; pub use gpui_macros::{register_action, test, IntoElement, Render};
pub use image_cache::*; use image_cache::*;
pub use input::*; pub use input::*;
pub use interactive::*; pub use interactive::*;
pub use key_dispatch::*; use key_dispatch::*;
pub use keymap::*; pub use keymap::*;
pub use platform::*; pub use platform::*;
pub use refineable::*; pub use refineable::*;
@ -73,7 +101,7 @@ pub use smol::Timer;
pub use style::*; pub use style::*;
pub use styled::*; pub use styled::*;
pub use subscription::*; pub use subscription::*;
pub use svg_renderer::*; use svg_renderer::*;
pub use taffy::{AvailableSpace, LayoutId}; pub use taffy::{AvailableSpace, LayoutId};
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub use test::*; pub use test::*;
@ -82,20 +110,23 @@ pub use util::arc_cow::ArcCow;
pub use view::*; pub use view::*;
pub use window::*; pub use window::*;
use std::{ use std::{any::Any, borrow::BorrowMut};
any::{Any, TypeId},
borrow::BorrowMut,
};
use taffy::TaffyLayoutEngine; use taffy::TaffyLayoutEngine;
/// The context trait, allows the different contexts in GPUI to be used
/// interchangeably for certain operations.
pub trait Context { 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>; type Result<T>;
/// Create a new model in the app context.
fn new_model<T: 'static>( fn new_model<T: 'static>(
&mut self, &mut self,
build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T,
) -> Self::Result<Model<T>>; ) -> Self::Result<Model<T>>;
/// Update a model in the app context.
fn update_model<T, R>( fn update_model<T, R>(
&mut self, &mut self,
handle: &Model<T>, handle: &Model<T>,
@ -104,6 +135,7 @@ pub trait Context {
where where
T: 'static; T: 'static;
/// Read a model from the app context.
fn read_model<T, R>( fn read_model<T, R>(
&self, &self,
handle: &Model<T>, handle: &Model<T>,
@ -112,10 +144,12 @@ pub trait Context {
where where
T: 'static; T: 'static;
/// Update a window for the given handle.
fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T> fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
where where
F: FnOnce(AnyView, &mut WindowContext<'_>) -> T; F: FnOnce(AnyView, &mut WindowContext<'_>) -> T;
/// Read a window off of the application context.
fn read_window<T, R>( fn read_window<T, R>(
&self, &self,
window: &WindowHandle<T>, window: &WindowHandle<T>,
@ -125,7 +159,10 @@ pub trait Context {
T: 'static; T: 'static;
} }
/// This trait is used for the different visual contexts in GPUI that
/// require a window to be present.
pub trait VisualContext: Context { pub trait VisualContext: Context {
/// Construct a new view in the window referenced by this context.
fn new_view<V>( fn new_view<V>(
&mut self, &mut self,
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
@ -133,12 +170,14 @@ pub trait VisualContext: Context {
where where
V: 'static + Render; V: 'static + Render;
/// Update a view with the given callback
fn update_view<V: 'static, R>( fn update_view<V: 'static, R>(
&mut self, &mut self,
view: &View<V>, view: &View<V>,
update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R, update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R,
) -> Self::Result<R>; ) -> Self::Result<R>;
/// Replace the root view of a window with a new view.
fn replace_root_view<V>( fn replace_root_view<V>(
&mut self, &mut self,
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
@ -146,38 +185,42 @@ pub trait VisualContext: Context {
where where
V: 'static + Render; 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<()> fn focus_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
where where
V: FocusableView; 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<()> fn dismiss_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
where where
V: ManagedView; V: ManagedView;
} }
/// A trait that allows models and views to be interchangeable in certain operations
pub trait Entity<T>: Sealed { pub trait Entity<T>: Sealed {
/// The weak reference type for this entity.
type Weak: 'static; type Weak: 'static;
/// The ID for this entity
fn entity_id(&self) -> EntityId; fn entity_id(&self) -> EntityId;
/// Downgrade this entity to a weak reference.
fn downgrade(&self) -> Self::Weak; fn downgrade(&self) -> Self::Weak;
/// Upgrade this entity from a weak reference.
fn upgrade_from(weak: &Self::Weak) -> Option<Self> fn upgrade_from(weak: &Self::Weak) -> Option<Self>
where where
Self: Sized; 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 trait EventEmitter<E: Any>: 'static {}
pub enum GlobalKey { /// A helper trait for auto-implementing certain methods on contexts that
Numeric(usize), /// can be used interchangeably.
View(EntityId),
Type(TypeId),
}
pub trait BorrowAppContext { pub trait BorrowAppContext {
fn with_text_style<F, R>(&mut self, style: Option<TextStyleRefinement>, f: F) -> R /// Set a global value on the context.
where
F: FnOnce(&mut Self) -> R;
fn set_global<T: 'static>(&mut self, global: T); fn set_global<T: 'static>(&mut self, global: T);
} }
@ -185,26 +228,14 @@ impl<C> BorrowAppContext for C
where where
C: BorrowMut<AppContext>, 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) { fn set_global<G: 'static>(&mut self, global: G) {
self.borrow_mut().set_global(global) self.borrow_mut().set_global(global)
} }
} }
/// A flatten equivalent for anyhow `Result`s.
pub trait Flatten<T> { pub trait Flatten<T> {
/// Convert this type into a simple `Result<T>`.
fn flatten(self) -> Result<T>; fn flatten(self) -> Result<T>;
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,6 +3,10 @@ use anyhow::{anyhow, Result};
use smallvec::SmallVec; use smallvec::SmallVec;
use std::fmt; 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)] #[derive(Clone, Default, Eq, PartialEq, Hash)]
pub struct KeyContext(SmallVec<[ContextEntry; 1]>); pub struct KeyContext(SmallVec<[ContextEntry; 1]>);
@ -21,6 +25,11 @@ impl<'a> TryFrom<&'a str> for KeyContext {
} }
impl 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> { pub fn parse(source: &str) -> Result<Self> {
let mut context = Self::default(); let mut context = Self::default();
let source = skip_whitespace(source); let source = skip_whitespace(source);
@ -53,14 +62,17 @@ impl KeyContext {
Self::parse_expr(source, context) Self::parse_expr(source, context)
} }
/// Check if this context is empty.
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.0.is_empty() self.0.is_empty()
} }
/// Clear this context.
pub fn clear(&mut self) { pub fn clear(&mut self) {
self.0.clear(); self.0.clear();
} }
/// Extend this context with another context.
pub fn extend(&mut self, other: &Self) { pub fn extend(&mut self, other: &Self) {
for entry in &other.0 { for entry in &other.0 {
if !self.contains(&entry.key) { 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) { pub fn add<I: Into<SharedString>>(&mut self, identifier: I) {
let key = identifier.into(); 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) { pub fn set<S1: Into<SharedString>, S2: Into<SharedString>>(&mut self, key: S1, value: S2) {
let key = key.into(); let key = key.into();
if !self.contains(&key) { 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 { pub fn contains(&self, key: &str) -> bool {
self.0.iter().any(|entry| entry.key.as_ref() == key) 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> { pub fn get(&self, key: &str) -> Option<&SharedString> {
self.0 self.0
.iter() .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)] #[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum KeyBindingContextPredicate { pub enum KeyBindingContextPredicate {
/// A predicate that will match a given identifier.
Identifier(SharedString), Identifier(SharedString),
/// A predicate that will match a given key-value pair.
Equal(SharedString, SharedString), Equal(SharedString, SharedString),
/// A predicate that will match a given key-value pair not being present.
NotEqual(SharedString, SharedString), NotEqual(SharedString, SharedString),
/// A predicate that will match a given predicate appearing below another predicate.
/// in the element tree
Child( Child(
Box<KeyBindingContextPredicate>, Box<KeyBindingContextPredicate>,
Box<KeyBindingContextPredicate>, Box<KeyBindingContextPredicate>,
), ),
/// Predicate that will invert another predicate.
Not(Box<KeyBindingContextPredicate>), Not(Box<KeyBindingContextPredicate>),
/// A predicate that will match if both of its children match.
And( And(
Box<KeyBindingContextPredicate>, Box<KeyBindingContextPredicate>,
Box<KeyBindingContextPredicate>, Box<KeyBindingContextPredicate>,
), ),
/// A predicate that will match if either of its children match.
Or( Or(
Box<KeyBindingContextPredicate>, Box<KeyBindingContextPredicate>,
Box<KeyBindingContextPredicate>, Box<KeyBindingContextPredicate>,
@ -138,6 +165,34 @@ pub enum KeyBindingContextPredicate {
} }
impl 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> { pub fn parse(source: &str) -> Result<Self> {
let source = skip_whitespace(source); let source = skip_whitespace(source);
let (predicate, rest) = Self::parse_expr(source, 0)?; 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 { pub fn eval(&self, contexts: &[KeyContext]) -> bool {
let Some(context) = contexts.last() else { let Some(context) = contexts.last() else {
return false; return false;

View file

@ -6,9 +6,12 @@ use std::{
collections::HashMap, 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)] #[derive(Copy, Clone, Eq, PartialEq, Default)]
pub struct KeymapVersion(usize); pub struct KeymapVersion(usize);
/// A collection of key bindings for the user's application.
#[derive(Default)] #[derive(Default)]
pub struct Keymap { pub struct Keymap {
bindings: Vec<KeyBinding>, bindings: Vec<KeyBinding>,
@ -19,16 +22,19 @@ pub struct Keymap {
} }
impl Keymap { impl Keymap {
/// Create a new keymap with the given bindings.
pub fn new(bindings: Vec<KeyBinding>) -> Self { pub fn new(bindings: Vec<KeyBinding>) -> Self {
let mut this = Self::default(); let mut this = Self::default();
this.add_bindings(bindings); this.add_bindings(bindings);
this this
} }
/// Get the current version of the keymap.
pub fn version(&self) -> KeymapVersion { pub fn version(&self) -> KeymapVersion {
self.version self.version
} }
/// Add more bindings to the keymap.
pub fn add_bindings<T: IntoIterator<Item = KeyBinding>>(&mut self, bindings: T) { pub fn add_bindings<T: IntoIterator<Item = KeyBinding>>(&mut self, bindings: T) {
let no_action_id = (NoAction {}).type_id(); let no_action_id = (NoAction {}).type_id();
@ -51,6 +57,7 @@ impl Keymap {
self.version.0 += 1; self.version.0 += 1;
} }
/// Reset this keymap to its initial state.
pub fn clear(&mut self) { pub fn clear(&mut self) {
self.bindings.clear(); self.bindings.clear();
self.binding_indices_by_action_id.clear(); self.binding_indices_by_action_id.clear();
@ -77,6 +84,7 @@ impl Keymap {
.filter(move |binding| binding.action().partial_eq(action)) .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 { pub fn binding_enabled(&self, binding: &KeyBinding, context: &[KeyContext]) -> bool {
// If binding has a context predicate, it must match the current context, // If binding has a context predicate, it must match the current context,
if let Some(predicate) = &binding.context_predicate { 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 parking_lot::Mutex;
use std::sync::Arc; use std::sync::Arc;
pub struct KeystrokeMatcher { pub(crate) struct KeystrokeMatcher {
pending_keystrokes: Vec<Keystroke>, pending_keystrokes: Vec<Keystroke>,
keymap: Arc<Mutex<Keymap>>, keymap: Arc<Mutex<Keymap>>,
keymap_version: KeymapVersion, keymap_version: KeymapVersion,
@ -35,7 +35,7 @@ impl KeystrokeMatcher {
/// - KeyMatch::Complete(matches) => /// - KeyMatch::Complete(matches) =>
/// One or more bindings have received the necessary key presses. /// One or more bindings have received the necessary key presses.
/// Bindings added later will take precedence over earlier bindings. /// Bindings added later will take precedence over earlier bindings.
pub fn match_keystroke( pub(crate) fn match_keystroke(
&mut self, &mut self,
keystroke: &Keystroke, keystroke: &Keystroke,
context_stack: &[KeyContext], context_stack: &[KeyContext],
@ -73,9 +73,7 @@ impl KeystrokeMatcher {
if !found_actions.is_empty() { if !found_actions.is_empty() {
self.pending_keystrokes.clear(); self.pending_keystrokes.clear();
return KeyMatch::Some(found_actions); return KeyMatch::Some(found_actions);
} } else if let Some(pending_key) = pending_key {
if let Some(pending_key) = pending_key {
self.pending_keystrokes.push(pending_key); self.pending_keystrokes.push(pending_key);
KeyMatch::Pending KeyMatch::Pending
} else { } 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)] #[derive(Debug)]
pub enum KeyMatch { pub enum KeyMatch {
None, None,
@ -93,10 +95,12 @@ pub enum KeyMatch {
} }
impl KeyMatch { impl KeyMatch {
/// Returns true if the match is complete.
pub fn is_some(&self) -> bool { pub fn is_some(&self) -> bool {
matches!(self, KeyMatch::Some(_)) matches!(self, KeyMatch::Some(_))
} }
/// Get the matches if the match is complete.
pub fn matches(self) -> Option<Vec<Box<dyn Action>>> { pub fn matches(self) -> Option<Vec<Box<dyn Action>>> {
match self { match self {
KeyMatch::Some(matches) => Some(matches), KeyMatch::Some(matches) => Some(matches),

View file

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

View file

@ -6,10 +6,10 @@ mod mac;
mod test; mod test;
use crate::{ use crate::{
Action, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId, FontMetrics, Action, AnyWindowHandle, AsyncWindowContext, BackgroundExecutor, Bounds, DevicePixels, Font,
FontRun, ForegroundExecutor, GlobalPixels, GlyphId, Keymap, LineLayout, Pixels, PlatformInput, FontId, FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, Keymap, LineLayout,
Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, Scene, SharedString, Pixels, PlatformInput, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result,
Size, TaskLabel, Scene, SharedString, Size, TaskLabel, WindowContext,
}; };
use anyhow::anyhow; use anyhow::anyhow;
use async_task::Runnable; use async_task::Runnable;
@ -34,9 +34,9 @@ use uuid::Uuid;
pub use app_menu::*; pub use app_menu::*;
pub use keystroke::*; pub use keystroke::*;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub use mac::*; pub(crate) use mac::*;
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub use test::*; pub(crate) use test::*;
use time::UtcOffset; use time::UtcOffset;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@ -69,11 +69,10 @@ pub(crate) trait Platform: 'static {
fn set_display_link_output_callback( fn set_display_link_output_callback(
&self, &self,
display_id: DisplayId, display_id: DisplayId,
callback: Box<dyn FnMut(&VideoTimestamp, &VideoTimestamp) + Send>, callback: Box<dyn FnMut() + Send>,
); );
fn start_display_link(&self, display_id: DisplayId); fn start_display_link(&self, display_id: DisplayId);
fn stop_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 open_url(&self, url: &str);
fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>); 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 mouse_position(&self) -> Point<Pixels>;
fn modifiers(&self) -> Modifiers; fn modifiers(&self) -> Modifiers;
fn as_any_mut(&mut self) -> &mut dyn Any; fn as_any_mut(&mut self) -> &mut dyn Any;
fn set_input_handler(&mut self, input_handler: Box<dyn PlatformInputHandler>); fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
fn take_input_handler(&mut self) -> Option<Box<dyn PlatformInputHandler>>; fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize>; fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize>;
fn activate(&self); fn activate(&self);
fn set_title(&mut self, title: &str); fn set_title(&mut self, title: &str);
@ -325,30 +324,168 @@ impl From<TileId> for etagere::AllocId {
} }
} }
pub trait PlatformInputHandler: 'static { pub(crate) struct PlatformInputHandler {
fn selected_text_range(&mut self) -> Option<Range<usize>>; cx: AsyncWindowContext,
fn marked_text_range(&mut self) -> Option<Range<usize>>; handler: Box<dyn InputHandler>,
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);
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( fn replace_and_mark_text_in_range(
&mut self, &mut self,
range_utf16: Option<Range<usize>>, range_utf16: Option<Range<usize>>,
new_text: &str, new_text: &str,
new_selected_range: Option<Range<usize>>, new_selected_range: Option<Range<usize>>,
); ) {
fn unmark_text(&mut self); self.cx
fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>>; .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)] #[derive(Debug)]
pub struct WindowOptions { pub struct WindowOptions {
/// The initial bounds of the window
pub bounds: WindowBounds, pub bounds: WindowBounds,
/// The titlebar configuration of the window
pub titlebar: Option<TitlebarOptions>, pub titlebar: Option<TitlebarOptions>,
/// Whether the window should be centered on the screen
pub center: bool, pub center: bool,
/// Whether the window should be focused when created
pub focus: bool, pub focus: bool,
/// Whether the window should be shown when created
pub show: bool, pub show: bool,
/// The kind of window to create
pub kind: WindowKind, pub kind: WindowKind,
/// Whether the window should be movable by the user
pub is_movable: bool, pub is_movable: bool,
/// The display to create the window on
pub display_id: Option<DisplayId>, 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)] #[derive(Debug, Default)]
pub struct TitlebarOptions { pub struct TitlebarOptions {
/// The initial title of the window
pub title: Option<SharedString>, pub title: Option<SharedString>,
/// Whether the titlebar should appear transparent
pub appears_transparent: bool, pub appears_transparent: bool,
/// The position of the macOS traffic light buttons
pub traffic_light_position: Option<Point<Pixels>>, pub traffic_light_position: Option<Point<Pixels>>,
} }
#[derive(Copy, Clone, Debug)] /// The kind of window to create
pub enum Appearance {
Light,
VibrantLight,
Dark,
VibrantDark,
}
impl Default for Appearance {
fn default() -> Self {
Self::Light
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum WindowKind { pub enum WindowKind {
/// A normal application window
Normal, Normal,
/// A window that appears above all other windows, usually used for alerts or popups
/// use sparingly!
PopUp, PopUp,
} }
/// Which bounds algorithm to use for the initial size a window
#[derive(Copy, Clone, Debug, PartialEq, Default)] #[derive(Copy, Clone, Debug, PartialEq, Default)]
pub enum WindowBounds { pub enum WindowBounds {
/// The window should be full screen, on macOS this corresponds to the full screen feature
Fullscreen, Fullscreen,
/// Make the window as large as the current display's size.
#[default] #[default]
Maximized, Maximized,
/// Set the window to the given size in pixels
Fixed(Bounds<GlobalPixels>), 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)] #[derive(Copy, Clone, Debug)]
pub enum WindowAppearance { pub enum WindowAppearance {
/// A light appearance
///
/// on macOS, this corresponds to the `aqua` appearance
Light, Light,
/// A light appearance with vibrant colors
///
/// on macOS, this corresponds to the `NSAppearanceNameVibrantLight` appearance
VibrantLight, VibrantLight,
/// A dark appearance
///
/// on macOS, this corresponds to the `darkAqua` appearance
Dark, Dark,
/// A dark appearance with vibrant colors
///
/// on macOS, this corresponds to the `NSAppearanceNameVibrantDark` appearance
VibrantDark, VibrantDark,
} }
@ -420,40 +578,102 @@ impl Default for WindowAppearance {
} }
} }
/// The options that can be configured for a file dialog prompt
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub struct PathPromptOptions { pub struct PathPromptOptions {
/// Should the prompt allow files to be selected?
pub files: bool, pub files: bool,
/// Should the prompt allow directories to be selected?
pub directories: bool, pub directories: bool,
/// Should the prompt allow multiple files to be selected?
pub multiple: bool, pub multiple: bool,
} }
/// What kind of prompt styling to show
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub enum PromptLevel { pub enum PromptLevel {
/// A prompt that is shown when the user should be notified of something
Info, Info,
/// A prompt that is shown when the user needs to be warned of a potential problem
Warning, Warning,
/// A prompt that is shown when a critical problem has occurred
Critical, Critical,
} }
/// The style of the cursor (pointer) /// The style of the cursor (pointer)
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub enum CursorStyle { pub enum CursorStyle {
/// The default cursor
Arrow, Arrow,
/// A text input cursor
/// corresponds to the CSS cursor value `text`
IBeam, IBeam,
/// A crosshair cursor
/// corresponds to the CSS cursor value `crosshair`
Crosshair, Crosshair,
/// A closed hand cursor
/// corresponds to the CSS cursor value `grabbing`
ClosedHand, ClosedHand,
/// An open hand cursor
/// corresponds to the CSS cursor value `grab`
OpenHand, OpenHand,
/// A pointing hand cursor
/// corresponds to the CSS cursor value `pointer`
PointingHand, PointingHand,
/// A resize left cursor
/// corresponds to the CSS cursor value `w-resize`
ResizeLeft, ResizeLeft,
/// A resize right cursor
/// corresponds to the CSS cursor value `e-resize`
ResizeRight, ResizeRight,
/// A resize cursor to the left and right
/// corresponds to the CSS cursor value `col-resize`
ResizeLeftRight, ResizeLeftRight,
/// A resize up cursor
/// corresponds to the CSS cursor value `n-resize`
ResizeUp, ResizeUp,
/// A resize down cursor
/// corresponds to the CSS cursor value `s-resize`
ResizeDown, ResizeDown,
/// A resize cursor directing up and down
/// corresponds to the CSS cursor value `row-resize`
ResizeUpDown, ResizeUpDown,
/// A cursor indicating that something will disappear if moved here
/// Does not correspond to a CSS cursor value
DisappearingItem, DisappearingItem,
/// A text input cursor for vertical layout
/// corresponds to the CSS cursor value `vertical-text`
IBeamCursorForVerticalLayout, IBeamCursorForVerticalLayout,
/// A cursor indicating that the operation is not allowed
/// corresponds to the CSS cursor value `not-allowed`
OperationNotAllowed, OperationNotAllowed,
/// A cursor indicating that the operation will result in a link
/// corresponds to the CSS cursor value `alias`
DragLink, DragLink,
/// A cursor indicating that the operation will result in a copy
/// corresponds to the CSS cursor value `copy`
DragCopy, DragCopy,
/// A cursor indicating that the operation will result in a context menu
/// corresponds to the CSS cursor value `context-menu`
ContextualMenu, 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)] #[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct SemanticVersion { pub struct SemanticVersion {
major: usize, 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)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct ClipboardItem { pub struct ClipboardItem {
pub(crate) text: String, pub(crate) text: String,
@ -508,6 +730,7 @@ pub struct ClipboardItem {
} }
impl ClipboardItem { impl ClipboardItem {
/// Create a new clipboard item with the given text
pub fn new(text: String) -> Self { pub fn new(text: String) -> Self {
Self { Self {
text, 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 { pub fn with_metadata<T: Serialize>(mut self, metadata: T) -> Self {
self.metadata = Some(serde_json::to_string(&metadata).unwrap()); self.metadata = Some(serde_json::to_string(&metadata).unwrap());
self self
} }
/// Get the text of the clipboard item
pub fn text(&self) -> &String { pub fn text(&self) -> &String {
&self.text &self.text
} }
/// Get the metadata of the clipboard item
pub fn metadata<T>(&self) -> Option<T> pub fn metadata<T>(&self) -> Option<T>
where where
T: for<'a> Deserialize<'a>, T: for<'a> Deserialize<'a>,

View file

@ -1,30 +1,49 @@
use crate::{Action, AppContext, Platform}; use crate::{Action, AppContext, Platform};
use util::ResultExt; use util::ResultExt;
/// A menu of the application, either a main menu or a submenu
pub struct Menu<'a> { pub struct Menu<'a> {
/// The name of the menu
pub name: &'a str, pub name: &'a str,
/// The items in the menu
pub items: Vec<MenuItem<'a>>, pub items: Vec<MenuItem<'a>>,
} }
/// The different kinds of items that can be in a menu
pub enum MenuItem<'a> { pub enum MenuItem<'a> {
/// A separator between items
Separator, Separator,
/// A submenu
Submenu(Menu<'a>), Submenu(Menu<'a>),
/// An action that can be performed
Action { Action {
/// The name of this menu item
name: &'a str, name: &'a str,
/// the action to perform when this menu item is selected
action: Box<dyn Action>, action: Box<dyn Action>,
/// The OS Action that corresponds to this action, if any
/// See [`OsAction`] for more information
os_action: Option<OsAction>, os_action: Option<OsAction>,
}, },
} }
impl<'a> MenuItem<'a> { impl<'a> MenuItem<'a> {
/// Creates a new menu item that is a separator
pub fn separator() -> Self { pub fn separator() -> Self {
Self::Separator Self::Separator
} }
/// Creates a new menu item that is a submenu
pub fn submenu(menu: Menu<'a>) -> Self { pub fn submenu(menu: Menu<'a>) -> Self {
Self::Submenu(menu) Self::Submenu(menu)
} }
/// Creates a new menu item that invokes an action
pub fn action(name: &'a str, action: impl Action) -> Self { pub fn action(name: &'a str, action: impl Action) -> Self {
Self::Action { Self::Action {
name, 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 { pub fn os_action(name: &'a str, action: impl Action, os_action: OsAction) -> Self {
Self::Action { Self::Action {
name, 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)] #[derive(Copy, Clone, Eq, PartialEq)]
pub enum OsAction { pub enum OsAction {
/// The 'cut' action
Cut, Cut,
/// The 'copy' action
Copy, Copy,
/// The 'paste' action
Paste, Paste,
/// The 'select all' action
SelectAll, SelectAll,
/// The 'undo' action
Undo, Undo,
/// The 'redo' action
Redo, Redo,
} }

View file

@ -3,24 +3,31 @@ use serde::Deserialize;
use smallvec::SmallVec; use smallvec::SmallVec;
use std::fmt::Write; use std::fmt::Write;
/// A keystroke and associated metadata generated by the platform
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)] #[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
pub struct Keystroke { pub struct Keystroke {
/// the state of the modifier keys at the time the keystroke was generated
pub modifiers: Modifiers, pub modifiers: Modifiers,
/// key is the character printed on the key that was pressed /// key is the character printed on the key that was pressed
/// e.g. for option-s, key is "s" /// e.g. for option-s, key is "s"
pub key: String, pub key: String,
/// ime_key is the character inserted by the IME engine when that key was pressed. /// ime_key is the character inserted by the IME engine when that key was pressed.
/// e.g. for option-s, ime_key is "ß" /// e.g. for option-s, ime_key is "ß"
pub ime_key: Option<String>, pub ime_key: Option<String>,
} }
impl Keystroke { impl Keystroke {
// When matching a key we cannot know whether the user intended to type /// 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 /// 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), /// 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 /// 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). /// specific character (for example `"` is typed as `" space` on a brazilian keyboard).
pub fn match_candidates(&self) -> SmallVec<[Keystroke; 2]> { ///
/// 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(); let mut possibilities = SmallVec::new();
match self.ime_key.as_ref() { match self.ime_key.as_ref() {
None => possibilities.push(self.clone()), None => possibilities.push(self.clone()),
@ -47,7 +54,7 @@ impl Keystroke {
/// key syntax is: /// key syntax is:
/// [ctrl-][alt-][shift-][cmd-][fn-]key[->ime_key] /// [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. /// when matching a key with an ime_key set will be matched without it.
pub fn parse(source: &str) -> anyhow::Result<Self> { pub fn parse(source: &str) -> anyhow::Result<Self> {
let mut control = false; 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)] #[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
pub struct Modifiers { pub struct Modifiers {
/// The control key
pub control: bool, pub control: bool,
/// The alt key
/// Sometimes also known as the 'meta' key
pub alt: bool, pub alt: bool,
/// The shift key
pub shift: bool, pub shift: bool,
/// The command key, on macos
/// the windows key, on windows
pub command: bool, pub command: bool,
/// The function key
pub function: bool, pub function: bool,
} }
impl Modifiers { impl Modifiers {
/// Returns true if any modifier key is pressed
pub fn modified(&self) -> bool { pub fn modified(&self) -> bool {
self.control || self.alt || self.shift || self.command || self.function 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 objc::runtime::{BOOL, NO, YES};
use std::ops::Range; use std::ops::Range;
pub use dispatcher::*; pub(crate) use dispatcher::*;
pub use display::*; pub(crate) use display::*;
pub use display_linker::*; pub(crate) use display_linker::*;
pub use metal_atlas::*; pub(crate) use metal_atlas::*;
pub use platform::*; pub(crate) use platform::*;
pub use text_system::*; pub(crate) use text_system::*;
pub use window::*; pub(crate) use window::*;
trait BoolExt { trait BoolExt {
fn to_objc(self) -> BOOL; 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 } unsafe { &_dispatch_main_q as *const _ as dispatch_queue_t }
} }
pub struct MacDispatcher { pub(crate) struct MacDispatcher {
parker: Arc<Mutex<Parker>>, parker: Arc<Mutex<Parker>>,
} }

View file

@ -3,18 +3,15 @@ use anyhow::Result;
use cocoa::{ use cocoa::{
appkit::NSScreen, appkit::NSScreen,
base::{id, nil}, base::{id, nil},
foundation::{NSDictionary, NSString}, foundation::{NSDictionary, NSPoint, NSRect, NSSize, NSString},
}; };
use core_foundation::uuid::{CFUUIDGetUUIDBytes, CFUUIDRef}; use core_foundation::uuid::{CFUUIDGetUUIDBytes, CFUUIDRef};
use core_graphics::{ use core_graphics::display::{CGDirectDisplayID, CGDisplayBounds, CGGetActiveDisplayList};
display::{CGDirectDisplayID, CGDisplayBounds, CGGetActiveDisplayList},
geometry::{CGPoint, CGRect, CGSize},
};
use objc::{msg_send, sel, sel_impl}; use objc::{msg_send, sel, sel_impl};
use uuid::Uuid; use uuid::Uuid;
#[derive(Debug)] #[derive(Debug)]
pub struct MacDisplay(pub(crate) CGDirectDisplayID); pub(crate) struct MacDisplay(pub(crate) CGDirectDisplayID);
unsafe impl Send for MacDisplay {} unsafe impl Send for MacDisplay {}
@ -24,11 +21,6 @@ impl MacDisplay {
Self::all().find(|screen| screen.id() == id) 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 /// Get the primary screen - the one with the menu bar, and whose bottom left
/// corner is at the origin of the AppKit coordinate system. /// corner is at the origin of the AppKit coordinate system.
pub fn primary() -> Self { pub fn primary() -> Self {
@ -77,14 +69,14 @@ extern "C" {
fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef; 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. /// with the Y axis pointing upwards.
/// ///
/// Conversely, in GPUI's coordinate system, the origin is placed at the top left of the primary /// Conversely, in GPUI's coordinate system, the origin is placed at the top left of the primary
/// screen, with the Y axis pointing downwards. /// screen, with the Y axis pointing downwards (matching CoreGraphics)
pub(crate) fn display_bounds_from_native(rect: CGRect) -> Bounds<GlobalPixels> { pub(crate) fn global_bounds_from_ns_rect(rect: NSRect) -> Bounds<GlobalPixels> {
let primary_screen_size = unsafe { CGDisplayBounds(MacDisplay::primary().id().0) }.size; let primary_screen_size = unsafe { CGDisplayBounds(MacDisplay::primary().id().0) }.size;
Bounds { 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. /// with the Y axis pointing upwards.
/// ///
/// Conversely, in GPUI's coordinate system, the origin is placed at the top left of the primary /// Conversely, in GPUI's coordinate system, the origin is placed at the top left of the primary
/// screen, with the Y axis pointing downwards. /// screen, with the Y axis pointing downwards (matching CoreGraphics)
pub(crate) fn display_bounds_to_native(bounds: Bounds<GlobalPixels>) -> CGRect { pub(crate) fn global_bounds_to_ns_rect(bounds: Bounds<GlobalPixels>) -> NSRect {
let primary_screen_height = MacDisplay::primary().bounds().size.height; let primary_screen_height = MacDisplay::primary().bounds().size.height;
CGRect::new( NSRect::new(
&CGPoint::new( NSPoint::new(
bounds.origin.x.into(), bounds.origin.x.into(),
(primary_screen_height - bounds.origin.y - bounds.size.height).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> { fn bounds(&self) -> Bounds<GlobalPixels> {
unsafe { unsafe {
let native_bounds = CGDisplayBounds(self.0); // CGDisplayBounds is in "global display" coordinates, where 0 is
display_bounds_from_native(native_bounds) // 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 crate::DisplayId;
use collections::HashMap; use collections::HashMap;
use parking_lot::Mutex; use parking_lot::Mutex;
pub use sys::CVSMPTETime as SmtpeTime;
pub use sys::CVTimeStamp as VideoTimestamp;
pub(crate) struct MacDisplayLinker { pub(crate) struct MacDisplayLinker {
links: HashMap<DisplayId, MacDisplayLink>, 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 { impl MacDisplayLinker {
pub fn set_output_callback( pub fn set_output_callback(
&mut self, &mut self,
display_id: DisplayId, 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) } { if let Some(mut system_link) = unsafe { sys::DisplayLink::on_display(display_id.0) } {
let callback = Arc::new(Mutex::new(output_callback)); let callback = Arc::new(Mutex::new(output_callback));
@ -81,11 +79,11 @@ unsafe extern "C" fn trampoline(
_flags_out: *mut i64, _flags_out: *mut i64,
user_data: *mut c_void, user_data: *mut c_void,
) -> i32 { ) -> 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> = let output_callback: Weak<OutputCallback> =
Weak::from_raw(user_data as *mut OutputCallback); Weak::from_raw(user_data as *mut OutputCallback);
if let Some(output_callback) = output_callback.upgrade() { if let Some(output_callback) = output_callback.upgrade() {
(output_callback.lock())(current_time, output_time) (output_callback.lock())()
} }
mem::forget(output_callback); mem::forget(output_callback);
} }
@ -126,7 +124,7 @@ mod sys {
#[repr(C)] #[repr(C)]
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct CVTimeStamp { pub(crate) struct CVTimeStamp {
pub version: u32, pub version: u32,
pub video_time_scale: i32, pub video_time_scale: i32,
pub video_time: i64, pub video_time: i64,
@ -154,7 +152,7 @@ mod sys {
#[repr(C)] #[repr(C)]
#[derive(Clone, Copy, Default)] #[derive(Clone, Copy, Default)]
pub struct CVSMPTETime { pub(crate) struct CVSMPTETime {
pub subframes: i16, pub subframes: i16,
pub subframe_divisor: i16, pub subframe_divisor: i16,
pub counter: u32, pub counter: u32,

View file

@ -83,7 +83,10 @@ unsafe fn read_modifiers(native_event: id) -> Modifiers {
} }
impl PlatformInput { 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(); let event_type = native_event.eventType();
// Filter out event types that aren't in the NSEventType enum. // 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 parking_lot::Mutex;
use std::borrow::Cow; use std::borrow::Cow;
pub struct MetalAtlas(Mutex<MetalAtlasState>); pub(crate) struct MetalAtlas(Mutex<MetalAtlasState>);
impl MetalAtlas { impl MetalAtlas {
pub fn new(device: Device) -> Self { pub(crate) fn new(device: Device) -> Self {
MetalAtlas(Mutex::new(MetalAtlasState { MetalAtlas(Mutex::new(MetalAtlasState {
device: AssertSend(device), device: AssertSend(device),
monochrome_textures: Default::default(), monochrome_textures: Default::default(),

View file

@ -3,7 +3,7 @@ use crate::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
ForegroundExecutor, Keymap, MacDispatcher, MacDisplay, MacDisplayLinker, MacTextSystem, ForegroundExecutor, Keymap, MacDispatcher, MacDisplay, MacDisplayLinker, MacTextSystem,
MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, PlatformInput, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, PlatformInput,
PlatformTextSystem, PlatformWindow, Result, SemanticVersion, VideoTimestamp, WindowOptions, PlatformTextSystem, PlatformWindow, Result, SemanticVersion, WindowOptions,
}; };
use anyhow::anyhow; use anyhow::anyhow;
use block::ConcreteBlock; 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, background_executor: BackgroundExecutor,
foreground_executor: ForegroundExecutor, foreground_executor: ForegroundExecutor,
text_system: Arc<MacTextSystem>, text_system: Arc<MacTextSystem>,
@ -169,7 +169,7 @@ impl Default for MacPlatform {
} }
impl MacPlatform { impl MacPlatform {
pub fn new() -> Self { pub(crate) fn new() -> Self {
let dispatcher = Arc::new(MacDispatcher::new()); let dispatcher = Arc::new(MacDispatcher::new());
Self(Mutex::new(MacPlatformState { Self(Mutex::new(MacPlatformState {
background_executor: BackgroundExecutor::new(dispatcher.clone()), 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>> { fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
MacDisplay::all() MacDisplay::all()
.map(|screen| Rc::new(screen) as Rc<_>) .map(|screen| Rc::new(screen) as Rc<_>)
@ -504,7 +500,7 @@ impl Platform for MacPlatform {
fn set_display_link_output_callback( fn set_display_link_output_callback(
&self, &self,
display_id: DisplayId, display_id: DisplayId,
callback: Box<dyn FnMut(&VideoTimestamp, &VideoTimestamp) + Send>, callback: Box<dyn FnMut() + Send>,
) { ) {
self.0 self.0
.lock() .lock()

View file

@ -41,7 +41,7 @@ use super::open_type;
#[allow(non_upper_case_globals)] #[allow(non_upper_case_globals)]
const kCGImageAlphaOnly: u32 = 7; const kCGImageAlphaOnly: u32 = 7;
pub struct MacTextSystem(RwLock<MacTextSystemState>); pub(crate) struct MacTextSystem(RwLock<MacTextSystemState>);
struct MacTextSystemState { struct MacTextSystemState {
memory_source: MemSource, memory_source: MemSource,
@ -54,7 +54,7 @@ struct MacTextSystemState {
} }
impl MacTextSystem { impl MacTextSystem {
pub fn new() -> Self { pub(crate) fn new() -> Self {
Self(RwLock::new(MacTextSystemState { Self(RwLock::new(MacTextSystemState {
memory_source: MemSource::empty(), memory_source: MemSource::empty(),
system_source: SystemSource::new(), 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::{ use crate::{
display_bounds_to_native, point, px, size, AnyWindowHandle, Bounds, ExternalPaths, global_bounds_to_ns_rect, platform::PlatformInputHandler, point, px, size, AnyWindowHandle,
FileDropEvent, ForegroundExecutor, GlobalPixels, KeyDownEvent, Keystroke, Modifiers, Bounds, ExternalPaths, FileDropEvent, ForegroundExecutor, GlobalPixels, KeyDownEvent,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent,
PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point,
PromptLevel, Size, Timer, WindowAppearance, WindowBounds, WindowKind, WindowOptions, PromptLevel, Size, Timer, WindowAppearance, WindowBounds, WindowKind, WindowOptions,
}; };
use block::ConcreteBlock; 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( point(
px(position.x as f32), px(position.x as f32),
// MacOS screen coordinates are relative to bottom left // MacOS screen coordinates are relative to bottom left
@ -327,7 +327,7 @@ struct MacWindowState {
should_close_callback: Option<Box<dyn FnMut() -> bool>>, should_close_callback: Option<Box<dyn FnMut() -> bool>>,
close_callback: Option<Box<dyn FnOnce()>>, close_callback: Option<Box<dyn FnOnce()>>,
appearance_changed_callback: Option<Box<dyn FnMut()>>, appearance_changed_callback: Option<Box<dyn FnMut()>>,
input_handler: Option<Box<dyn PlatformInputHandler>>, input_handler: Option<PlatformInputHandler>,
pending_key_down: Option<(KeyDownEvent, Option<InsertText>)>, pending_key_down: Option<(KeyDownEvent, Option<InsertText>)>,
last_key_equivalent: Option<KeyDownEvent>, last_key_equivalent: Option<KeyDownEvent>,
synthetic_drag_counter: usize, synthetic_drag_counter: usize,
@ -411,10 +411,8 @@ impl MacWindowState {
} }
fn frame(&self) -> Bounds<GlobalPixels> { fn frame(&self) -> Bounds<GlobalPixels> {
unsafe { let frame = unsafe { NSWindow::frame(self.native_window) };
let frame = NSWindow::frame(self.native_window); global_bounds_from_ns_rect(frame)
display_bounds_from_native(mem::transmute::<NSRect, CGRect>(frame))
}
} }
fn content_size(&self) -> Size<Pixels> { fn content_size(&self) -> Size<Pixels> {
@ -448,7 +446,7 @@ impl MacWindowState {
unsafe impl Send for MacWindowState {} unsafe impl Send for MacWindowState {}
pub struct MacWindow(Arc<Mutex<MacWindowState>>); pub(crate) struct MacWindow(Arc<Mutex<MacWindowState>>);
impl MacWindow { impl MacWindow {
pub fn open( pub fn open(
@ -650,11 +648,11 @@ impl MacWindow {
WindowBounds::Fixed(bounds) => { WindowBounds::Fixed(bounds) => {
let display_bounds = display.bounds(); let display_bounds = display.bounds();
let frame = if bounds.intersects(&display_bounds) { let frame = if bounds.intersects(&display_bounds) {
display_bounds_to_native(bounds) global_bounds_to_ns_rect(bounds)
} else { } 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 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); 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() 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> fn with_input_handler<F, R>(window: &Object, f: F) -> Option<R>
where where
F: FnOnce(&mut dyn PlatformInputHandler) -> R, F: FnOnce(&mut PlatformInputHandler) -> R,
{ {
let window_state = unsafe { get_window_state(window) }; let window_state = unsafe { get_window_state(window) };
let mut lock = window_state.as_ref().lock(); let mut lock = window_state.as_ref().lock();
if let Some(mut input_handler) = lock.input_handler.take() { if let Some(mut input_handler) = lock.input_handler.take() {
drop(lock); drop(lock);
let result = f(input_handler.as_mut()); let result = f(&mut input_handler);
window_state.lock().input_handler = Some(input_handler); window_state.lock().input_handler = Some(input_handler);
Some(result) Some(result)
} else { } else {

View file

@ -8,7 +8,7 @@ use objc::{msg_send, sel, sel_impl};
use std::ffi::CStr; use std::ffi::CStr;
impl WindowAppearance { 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]; let name: id = msg_send![appearance, name];
if name == NSAppearanceNameVibrantLight { if name == NSAppearanceNameVibrantLight {
Self::VibrantLight Self::VibrantLight

View file

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

View file

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

View file

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

View file

@ -15,7 +15,7 @@ use std::{
}; };
/// TestPlatform implements the Platform trait for use in tests. /// TestPlatform implements the Platform trait for use in tests.
pub struct TestPlatform { pub(crate) struct TestPlatform {
background_executor: BackgroundExecutor, background_executor: BackgroundExecutor,
foreground_executor: ForegroundExecutor, foreground_executor: ForegroundExecutor,
@ -178,20 +178,9 @@ impl Platform for TestPlatform {
fn set_display_link_output_callback( fn set_display_link_output_callback(
&self, &self,
_display_id: DisplayId, _display_id: DisplayId,
mut callback: Box<dyn FnMut(&crate::VideoTimestamp, &crate::VideoTimestamp) + Send>, mut callback: Box<dyn FnMut() + Send>,
) { ) {
let timestamp = crate::VideoTimestamp { callback()
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)
} }
fn start_display_link(&self, _display_id: DisplayId) {} fn start_display_link(&self, _display_id: DisplayId) {}

View file

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

View file

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

View file

@ -1,10 +1,10 @@
use std::{iter, mem, ops::Range}; use std::{iter, mem, ops::Range};
use crate::{ use crate::{
black, phi, point, quad, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, black, phi, point, quad, rems, AbsoluteLength, BorrowAppContext, Bounds, ContentMask, Corners,
ContentMask, Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, ElementContext, Font,
Font, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext, SharedString, Size, SizeRefinement, Styled, TextRun,
}; };
use collections::HashSet; use collections::HashSet;
use refineable::{Cascade, Refineable}; 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. /// Paints the background of an element styled with this style.
pub fn paint( pub fn paint(
&self, &self,
bounds: Bounds<Pixels>, bounds: Bounds<Pixels>,
cx: &mut WindowContext, cx: &mut ElementContext,
continuation: impl FnOnce(&mut WindowContext), continuation: impl FnOnce(&mut ElementContext),
) { ) {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
if self.debug_below { if self.debug_below {

View file

@ -1,11 +1,11 @@
use crate::{ use crate::{
self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle, self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle,
DefiniteLength, Display, Fill, FlexDirection, FontWeight, Hsla, JustifyContent, Length, DefiniteLength, Fill, FlexDirection, FontWeight, Hsla, JustifyContent, Length, Position,
Position, SharedString, StyleRefinement, Visibility, WhiteSpace, SharedString, StyleRefinement, Visibility, WhiteSpace,
}; };
use crate::{BoxShadow, TextStyleRefinement}; use crate::{BoxShadow, TextStyleRefinement};
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use taffy::style::Overflow; use taffy::style::{Display, Overflow};
pub trait Styled: Sized { pub trait Styled: Sized {
fn style(&mut self) -> &mut StyleRefinement; 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] #[must_use]
pub struct Subscription { pub struct Subscription {
unsubscribe: Option<Box<dyn FnOnce() + 'static>>, unsubscribe: Option<Box<dyn FnOnce() + 'static>>,
} }
impl Subscription { 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) { pub fn detach(mut self) {
self.unsubscribe.take(); self.unsubscribe.take();
} }

View file

@ -3,12 +3,12 @@ use anyhow::anyhow;
use std::{hash::Hash, sync::Arc}; use std::{hash::Hash, sync::Arc};
#[derive(Clone, PartialEq, Hash, Eq)] #[derive(Clone, PartialEq, Hash, Eq)]
pub struct RenderSvgParams { pub(crate) struct RenderSvgParams {
pub(crate) path: SharedString, pub(crate) path: SharedString,
pub(crate) size: Size<DevicePixels>, pub(crate) size: Size<DevicePixels>,
} }
pub struct SvgRenderer { pub(crate) struct SvgRenderer {
asset_source: Arc<dyn AssetSource>, 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)] #[derive(Copy, Clone, Eq, PartialEq, Debug)]
#[repr(transparent)] #[repr(transparent)]
pub struct LayoutId(NodeId); 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)] #[derive(Copy, Clone, Default, Debug, Eq, PartialEq)]
pub enum AvailableSpace { pub enum AvailableSpace {
/// The amount of space available is the specified number of pixels /// The amount of space available is the specified number of pixels

View file

@ -34,6 +34,9 @@ use std::{
panic::{self, RefUnwindSafe}, 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( pub fn run_test(
mut num_iterations: u64, mut num_iterations: u64,
max_retries: usize, 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> { pub struct Observation<T> {
rx: channel::Receiver<T>, rx: channel::Receiver<T>,
_subscription: Subscription, _subscription: Subscription,

View file

@ -377,7 +377,7 @@ impl TextSystem {
Ok(lines) 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) self.line_layout_cache.finish_frame(reused_views)
} }

View file

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

View file

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

View file

@ -3007,7 +3007,7 @@ impl BufferSnapshot {
groups.sort_by(|(id_a, group_a), (id_b, group_b)| { groups.sort_by(|(id_a, group_a), (id_b, group_b)| {
let a_start = &group_a.entries[group_a.primary_ix].range.start; let a_start = &group_a.entries[group_a.primary_ix].range.start;
let b_start = &group_b.entries[group_b.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 groups

View file

@ -194,7 +194,7 @@ impl DiagnosticSet {
.range .range
.start .start
.cmp(&group_b.entries[group_b.primary_ix].range.start, buffer) .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)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct CodeLabel { pub struct CodeLabel {
/// The text to display.
pub text: String, pub text: String,
/// Syntax highlighting runs.
pub runs: Vec<(Range<usize>, HighlightId)>, pub runs: Vec<(Range<usize>, HighlightId)>,
/// The portion of the text that should be used in fuzzy filtering.
pub filter_range: Range<usize>, pub filter_range: Range<usize>,
} }
@ -849,7 +852,7 @@ impl LanguageRegistry {
let mut state = self.state.write(); let mut state = self.state.write();
state.theme = Some(theme.clone()); state.theme = Some(theme.clone());
for language in &state.languages { 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>) { fn add(&mut self, language: Arc<Language>) {
if let Some(theme) = self.theme.as_ref() { if let Some(theme) = self.theme.as_ref() {
language.set_theme(&theme.syntax()); language.set_theme(theme.syntax());
} }
self.languages.push(language); self.languages.push(language);
self.version += 1; self.version += 1;

View file

@ -91,6 +91,8 @@ pub struct LanguageSettings {
pub extend_comment_on_newline: bool, pub extend_comment_on_newline: bool,
/// Inlay hint related settings. /// Inlay hint related settings.
pub inlay_hints: InlayHintSettings, pub inlay_hints: InlayHintSettings,
/// Whether to automatically close brackets.
pub use_autoclose: bool,
} }
/// The settings for [GitHub Copilot](https://github.com/features/copilot). /// The settings for [GitHub Copilot](https://github.com/features/copilot).
@ -208,6 +210,11 @@ pub struct LanguageSettingsContent {
/// Inlay hint related settings. /// Inlay hint related settings.
#[serde(default)] #[serde(default)]
pub inlay_hints: Option<InlayHintSettings>, 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. /// The contents of the GitHub Copilot settings.
@ -421,7 +428,7 @@ impl settings::Settings for AllLanguageSettings {
let mut languages = HashMap::default(); let mut languages = HashMap::default();
for (language_name, settings) in &default_value.languages { for (language_name, settings) in &default_value.languages {
let mut language_settings = defaults.clone(); 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); languages.insert(language_name.clone(), language_settings);
} }
@ -461,7 +468,7 @@ impl settings::Settings for AllLanguageSettings {
languages languages
.entry(language_name.clone()) .entry(language_name.clone())
.or_insert_with(|| defaults.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.tab_size, src.tab_size);
merge(&mut settings.hard_tabs, src.hard_tabs); merge(&mut settings.hard_tabs, src.hard_tabs);
merge(&mut settings.soft_wrap, src.soft_wrap); 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.show_wrap_guides, src.show_wrap_guides);
merge(&mut settings.wrap_guides, src.wrap_guides.clone()); 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 current_language = None;
let mut list_stack = Vec::new(); 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(); let prev_len = text.len();
match event { match event {
Event::Text(t) => { Event::Text(t) => {

View file

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

View file

@ -2316,7 +2316,7 @@ impl MultiBufferSnapshot {
&self, &self,
point: T, point: T,
) -> Option<(&BufferSnapshot, usize)> { ) -> Option<(&BufferSnapshot, usize)> {
let offset = point.to_offset(&self); let offset = point.to_offset(self);
let mut cursor = self.excerpts.cursor::<usize>(); let mut cursor = self.excerpts.cursor::<usize>();
cursor.seek(&offset, Bias::Right, &()); cursor.seek(&offset, Bias::Right, &());
if cursor.item().is_none() { if cursor.item().is_none() {
@ -3694,7 +3694,7 @@ impl ExcerptId {
pub fn cmp(&self, other: &Self, snapshot: &MultiBufferSnapshot) -> cmp::Ordering { pub fn cmp(&self, other: &Self, snapshot: &MultiBufferSnapshot) -> cmp::Ordering {
let a = snapshot.excerpt_locator_for_id(*self); let a = snapshot.excerpt_locator_for_id(*self);
let b = snapshot.excerpt_locator_for_id(*other); 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() .iter()
.map(language::proto::serialize_completion) .map(language::proto::serialize_completion)
.collect(), .collect(),
version: serialize_version(&buffer_version), version: serialize_version(buffer_version),
} }
} }
@ -1672,7 +1672,7 @@ impl LspCommand for GetCodeActions {
.iter() .iter()
.map(language::proto::serialize_code_action) .map(language::proto::serialize_code_action)
.collect(), .collect(),
version: serialize_version(&buffer_version), version: serialize_version(buffer_version),
} }
} }

View file

@ -34,16 +34,16 @@ use gpui::{
use itertools::Itertools; use itertools::Itertools;
use language::{ use language::{
language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind}, language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
point_to_lsp, markdown, point_to_lsp,
proto::{ proto::{
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version, deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
serialize_anchor, serialize_version, split_operations, serialize_anchor, serialize_version, split_operations,
}, },
range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, Capability, range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, Capability,
CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff,
Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, Documentation, Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName,
LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16, LocalFile, LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer,
TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
}; };
use log::error; use log::error;
use lsp::{ use lsp::{
@ -52,7 +52,7 @@ use lsp::{
}; };
use lsp_command::*; use lsp_command::*;
use node_runtime::NodeRuntime; use node_runtime::NodeRuntime;
use parking_lot::Mutex; use parking_lot::{Mutex, RwLock};
use postage::watch; use postage::watch;
use prettier_support::{DefaultPrettier, PrettierInstance}; use prettier_support::{DefaultPrettier, PrettierInstance};
use project_settings::{LspSettings, ProjectSettings}; use project_settings::{LspSettings, ProjectSettings};
@ -2947,7 +2947,7 @@ impl Project {
}; };
task.await; task.await;
this.update(&mut cx, |this, mut cx| { this.update(&mut cx, |this, cx| {
let worktrees = this.worktrees.clone(); let worktrees = this.worktrees.clone();
for worktree in worktrees { for worktree in worktrees {
let worktree = match worktree.upgrade() { let worktree = match worktree.upgrade() {
@ -2962,7 +2962,7 @@ impl Project {
root_path, root_path,
adapter.clone(), adapter.clone(),
language.clone(), language.clone(),
&mut cx, cx,
); );
} }
}) })
@ -3544,8 +3544,8 @@ impl Project {
if errored { if errored {
log::warn!("test binary check failed"); log::warn!("test binary check failed");
let task = this let task = this
.update(&mut cx, move |this, mut cx| { .update(&mut cx, move |this, cx| {
this.reinstall_language_server(language, adapter, server_id, &mut cx) this.reinstall_language_server(language, adapter, server_id, cx)
}) })
.ok() .ok()
.flatten(); .flatten();
@ -3768,8 +3768,7 @@ impl Project {
} }
}; };
if let Some(relative_glob_pattern) = relative_glob_pattern { if let Some(relative_glob_pattern) = relative_glob_pattern {
let literal_prefix = let literal_prefix = glob_literal_prefix(relative_glob_pattern);
glob_literal_prefix(&relative_glob_pattern);
tree.as_local_mut() tree.as_local_mut()
.unwrap() .unwrap()
.add_path_prefix_to_scan(Path::new(literal_prefix).into()); .add_path_prefix_to_scan(Path::new(literal_prefix).into());
@ -4231,9 +4230,9 @@ impl Project {
format_operation = Some(FormatOperation::Lsp( format_operation = Some(FormatOperation::Lsp(
Self::format_via_lsp( Self::format_via_lsp(
&project, &project,
&buffer, buffer,
buffer_abs_path, buffer_abs_path,
&language_server, language_server,
tab_size, tab_size,
&mut cx, &mut cx,
) )
@ -4252,8 +4251,8 @@ impl Project {
format_operation = Self::format_via_external_command( format_operation = Self::format_via_external_command(
buffer, buffer,
buffer_abs_path, buffer_abs_path,
&command, command,
&arguments, arguments,
&mut cx, &mut cx,
) )
.await .await
@ -4276,9 +4275,9 @@ impl Project {
format_operation = Some(FormatOperation::Lsp( format_operation = Some(FormatOperation::Lsp(
Self::format_via_lsp( Self::format_via_lsp(
&project, &project,
&buffer, buffer,
buffer_abs_path, buffer_abs_path,
&language_server, language_server,
tab_size, tab_size,
&mut cx, &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( pub fn apply_additional_edits_for_completion(
&self, &self,
buffer_handle: Model<Buffer>, buffer_handle: Model<Buffer>,
@ -5683,7 +5846,7 @@ impl Project {
snapshot.file().map(|file| file.path().as_ref()), snapshot.file().map(|file| file.path().as_ref()),
) { ) {
query query
.search(&snapshot, None) .search(snapshot, None)
.await .await
.iter() .iter()
.map(|range| { .map(|range| {
@ -6526,7 +6689,7 @@ impl Project {
snapshot.repository_and_work_directory_for_path(&path)?; snapshot.repository_and_work_directory_for_path(&path)?;
let repo = snapshot.get_local_repo(&repo)?; let repo = snapshot.get_local_repo(&repo)?;
let relative_path = path.strip_prefix(&work_directory).ok()?; 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)) Some((buffer, base_text))
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
@ -6659,7 +6822,7 @@ impl Project {
for (_, _, path_summary) in for (_, _, path_summary) in
self.diagnostic_summaries(include_ignored, cx) self.diagnostic_summaries(include_ignored, cx)
.filter(|(path, _, _)| { .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) include_ignored || worktree == Some(false)
}) })
{ {
@ -6685,7 +6848,7 @@ impl Project {
}) })
}) })
.filter(move |(path, _, _)| { .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) include_ignored || worktree == Some(false)
}) })
} }

View file

@ -61,7 +61,7 @@ impl Project {
if let Some(python_settings) = &python_settings.as_option() { if let Some(python_settings) = &python_settings.as_option() {
let activate_script_path = 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( self.activate_python_virtual_environment(
activate_script_path, activate_script_path,
&terminal_handle, &terminal_handle,

View file

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

View file

@ -6,6 +6,7 @@ use gpui::{
use language::{HighlightId, Language, LanguageRegistry}; use language::{HighlightId, Language, LanguageRegistry};
use std::{ops::Range, sync::Arc}; use std::{ops::Range, sync::Arc};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::LinkPreview;
use util::RangeExt; use util::RangeExt;
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@ -64,7 +65,7 @@ impl RichText {
}, },
Highlight::Id(id) => HighlightStyle { Highlight::Id(id) => HighlightStyle {
background_color: Some(code_background), background_color: Some(code_background),
..id.style(&theme.syntax()).unwrap_or_default() ..id.style(theme.syntax()).unwrap_or_default()
}, },
Highlight::Highlight(highlight) => *highlight, Highlight::Highlight(highlight) => *highlight,
Highlight::Mention => HighlightStyle { Highlight::Mention => HighlightStyle {
@ -84,6 +85,18 @@ impl RichText {
let link_urls = self.link_urls.clone(); let link_urls = self.link_urls.clone();
move |ix, cx| cx.open_url(&link_urls[ix]) 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() .into_any_element()
} }
} }
@ -107,7 +120,7 @@ pub fn render_markdown_mut(
let mut list_stack = Vec::new(); let mut list_stack = Vec::new();
let options = Options::all(); 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(); let prev_len = text.len();
match event { match event {
Event::Text(t) => { Event::Text(t) => {
@ -237,7 +250,7 @@ pub fn render_markdown_mut(
_ => {} _ => {}
}, },
Event::HardBreak => text.push('\n'), 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) = 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()))); paths_stack.push(Some((*root_id, path.as_ref())));
user_settings_stack.push(local_settings); 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() { if let Some(new_object) = new_value.as_object_mut() {
new_object.retain(|_, v| !v.is_null()); new_object.retain(|_, v| !v.is_null());
} }
let (range, replacement) = let (range, replacement) = replace_value_in_json_text(text, key_path, tab_size, &new_value);
replace_value_in_json_text(text, &key_path, tab_size, &new_value);
text.replace_range(range.clone(), &replacement); text.replace_range(range.clone(), &replacement);
edits.push((range, replacement)); edits.push((range, replacement));
} }

View file

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

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