diff --git a/.github/actions/check_style/action.yml b/.github/actions/check_style/action.yml index 5dc7c42b02f387fe76295e9ae110f28972ef8450..25020e4e4c5399026c1ab32622903a3779ba86b2 100644 --- a/.github/actions/check_style/action.yml +++ b/.github/actions/check_style/action.yml @@ -2,16 +2,22 @@ name: "Check formatting" description: "Checks code formatting use cargo fmt" runs: - using: "composite" - steps: - - name: cargo fmt - shell: bash -euxo pipefail {0} - run: cargo fmt --all -- --check + using: "composite" + steps: + - name: cargo fmt + shell: bash -euxo pipefail {0} + run: cargo fmt --all -- --check - - name: cargo clippy - shell: bash -euxo pipefail {0} - # clippy.toml is not currently supporting specifying allowed lints - # so specify those here, and disable the rest until Zed's workspace - # will have more fixes & suppression for the standard lint set - run: | - cargo clippy --workspace --all-features --all-targets -- -A clippy::all -D clippy::dbg_macro -D clippy::todo + - name: cargo clippy + shell: bash -euxo pipefail {0} + # clippy.toml is not currently supporting specifying allowed lints + # so specify those here, and disable the rest until Zed's workspace + # will have more fixes & suppression for the standard lint set + run: | + cargo clippy --workspace --all-features --all-targets -- -A clippy::all -D clippy::dbg_macro -D clippy::todo + + - name: Find modified migrations + shell: bash -euxo pipefail {0} + run: | + export SQUAWK_GITHUB_TOKEN=${{ github.token }} + . ./script/squawk diff --git a/.zed/settings.json b/.zed/settings.json index d4b3375b0d21f95128c6bffb2c7a92f8bf97916c..205d610046fef48bbfa03911d2ababe86755dc71 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -1,5 +1,6 @@ { "JSON": { "tab_size": 4 - } + }, + "formatter": "auto" } diff --git a/Cargo.lock b/Cargo.lock index 51f02d2ad128b2dfc9f4a5f4ca087272de5b4891..9a7940d2405f38c1ba98b2e6ca7263ccb150bd40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1607,6 +1607,7 @@ name = "command_palette" version = "0.1.0" dependencies = [ "anyhow", + "client", "collections", "ctor", "editor", @@ -1949,6 +1950,16 @@ dependencies = [ "syn 2.0.37", ] +[[package]] +name = "ctrlc" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b467862cc8610ca6fc9a1532d7777cee0804e678ab45410897b9396495994a0b" +dependencies = [ + "nix 0.27.1", + "windows-sys 0.52.0", +] + [[package]] name = "curl" version = "0.4.44" @@ -2610,7 +2621,7 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "font-kit" version = "0.11.0" -source = "git+https://github.com/zed-industries/font-kit?rev=b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18#b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18" +source = "git+https://github.com/zed-industries/font-kit?rev=d97147f#d97147ff11a9024b9707d9c9c7e3a0bdaba048ac" dependencies = [ "bitflags 1.3.2", "byteorder", @@ -4489,6 +4500,17 @@ dependencies = [ "libc", ] +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.4.1", + "cfg-if 1.0.0", + "libc", +] + [[package]] name = "node_runtime" version = "0.1.0" @@ -7452,6 +7474,7 @@ dependencies = [ "chrono", "clap 4.4.4", "collab_ui", + "ctrlc", "dialoguer", "editor", "fuzzy", @@ -9288,6 +9311,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -9318,6 +9350,21 @@ dependencies = [ "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -9330,6 +9377,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -9342,6 +9395,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -9354,6 +9413,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -9366,6 +9431,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -9378,6 +9449,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -9390,6 +9467,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -9402,6 +9485,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "winnow" version = "0.5.15" diff --git a/assets/settings/default.json b/assets/settings/default.json index bd157c3e6137ccf5e990b94a3085896563931e60..87cf0517a2b6f1a32ca8b0d33dadda2d3418f3db 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -36,7 +36,7 @@ // }, "buffer_line_height": "comfortable", // The name of a font to use for rendering text in the UI - "ui_font_family": "Zed Mono", + "ui_font_family": "Zed Sans", // The OpenType features to enable for text in the UI "ui_font_features": { // Disable ligatures: diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index d04a5ab319f4c116a319a0a6f4d7b51a1de51184..ac63592d6bc21bc6ba46649eb1924d1cc5566d89 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -295,7 +295,7 @@ impl Render for ActivityIndicator { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let content = self.content_to_render(cx); - let mut result = h_stack() + let mut result = h_flex() .id("activity-indicator") .on_action(cx.listener(Self::show_error_message)) .on_action(cx.listener(Self::dismiss_error_message)); diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index d4743afb714ab47be3e38b196a45174fb33c2aa5..df3dc3754f66aff8d83a6fcd3b92edd38c7c4e45 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -40,7 +40,7 @@ use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset a use project::Project; use search::{buffer_search::DivRegistrar, BufferSearchBar}; use semantic_index::{SemanticIndex, SemanticIndexStatus}; -use settings::{Settings, SettingsStore}; +use settings::Settings; use std::{ cell::Cell, cmp, @@ -165,7 +165,7 @@ impl AssistantPanel { cx.on_focus_in(&focus_handle, Self::focus_in).detach(); cx.on_focus_out(&focus_handle, Self::focus_out).detach(); - let mut this = Self { + Self { workspace: workspace_handle, active_editor_index: Default::default(), prev_active_editor_index: Default::default(), @@ -190,20 +190,7 @@ impl AssistantPanel { _watch_saved_conversations, semantic_index, retrieve_context_in_next_inline_assist: false, - }; - - let mut old_dock_position = this.position(cx); - this.subscriptions = - vec![cx.observe_global::(move |this, cx| { - let new_dock_position = this.position(cx); - if new_dock_position != old_dock_position { - old_dock_position = new_dock_position; - cx.emit(PanelEvent::ChangePosition); - } - cx.notify(); - })]; - - this + } }) }) }) @@ -1103,7 +1090,7 @@ fn build_api_key_editor(cx: &mut ViewContext) -> View { impl Render for AssistantPanel { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { if let Some(api_key_editor) = self.api_key_editor.clone() { - v_stack() + v_flex() .on_action(cx.listener(AssistantPanel::save_credentials)) .track_focus(&self.focus_handle) .child(Label::new( @@ -1128,26 +1115,26 @@ impl Render for AssistantPanel { } else { let header = TabBar::new("assistant_header") .start_child( - h_stack().gap_1().child(Self::render_hamburger_button(cx)), // .children(title), + h_flex().gap_1().child(Self::render_hamburger_button(cx)), // .children(title), ) .children(self.active_editor().map(|editor| { - h_stack() - .h(rems(Tab::HEIGHT_IN_REMS)) + h_flex() + .h(rems(Tab::CONTAINER_HEIGHT_IN_REMS)) .flex_1() .px_2() .child(Label::new(editor.read(cx).title(cx)).into_element()) })) .end_child(if self.focus_handle.contains_focused(cx) { - h_stack() + h_flex() .gap_2() - .child(h_stack().gap_1().children(self.render_editor_tools(cx))) + .child(h_flex().gap_1().children(self.render_editor_tools(cx))) .child( ui::Divider::vertical() .inset() .color(ui::DividerColor::Border), ) .child( - h_stack() + h_flex() .gap_1() .child(Self::render_plus_button(cx)) .child(self.render_zoom_button(cx)), @@ -1166,7 +1153,7 @@ impl Render for AssistantPanel { } else { div() }; - v_stack() + v_flex() .key_context("AssistantPanel") .size_full() .on_action(cx.listener(|this, _: &workspace::NewFile, cx| { @@ -2543,7 +2530,7 @@ impl Render for ConversationEditor { .child(self.editor.clone()), ) .child( - h_stack() + h_flex() .absolute() .gap_1() .top_3() @@ -2629,7 +2616,7 @@ impl EventEmitter for InlineAssistant {} impl Render for InlineAssistant { fn render(&mut self, cx: &mut ViewContext) -> impl Element { let measurements = self.measurements.get(); - h_stack() + h_flex() .w_full() .py_2() .border_y_1() @@ -2641,7 +2628,7 @@ impl Render for InlineAssistant { .on_action(cx.listener(Self::move_up)) .on_action(cx.listener(Self::move_down)) .child( - h_stack() + h_flex() .justify_center() .w(measurements.gutter_width) .child( @@ -2689,7 +2676,7 @@ impl Render for InlineAssistant { }), ) .child( - h_stack() + h_flex() .w_full() .ml(measurements.anchor_x - measurements.gutter_width) .child(self.render_prompt_editor(cx)), @@ -3133,6 +3120,7 @@ mod tests { use crate::MessageId; use ai::test::FakeCompletionProvider; use gpui::AppContext; + use settings::SettingsStore; #[gpui::test] fn test_inserting_and_removing_messages(cx: &mut AppContext) { diff --git a/crates/auto_update/src/update_notification.rs b/crates/auto_update/src/update_notification.rs index 65f786bca4c4e1a729974459fbdf8e549a9893cc..985972da56364c646850fe7146f3323d06f56015 100644 --- a/crates/auto_update/src/update_notification.rs +++ b/crates/auto_update/src/update_notification.rs @@ -4,7 +4,7 @@ use gpui::{ }; use menu::Cancel; use util::channel::ReleaseChannel; -use workspace::ui::{h_stack, v_stack, Icon, IconName, Label, StyledExt}; +use workspace::ui::{h_flex, v_flex, Icon, IconName, Label, StyledExt}; pub struct UpdateNotification { version: SemanticVersion, @@ -16,12 +16,12 @@ impl Render for UpdateNotification { fn render(&mut self, cx: &mut gpui::ViewContext) -> impl IntoElement { let app_name = cx.global::().display_name(); - v_stack() + v_flex() .on_action(cx.listener(UpdateNotification::dismiss)) .elevation_3(cx) .p_4() .child( - h_stack() + h_flex() .justify_between() .child(Label::new(format!( "Updated to {app_name} {}", diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index e41c0c06b180cab749f7e34bb1199bd28709201b..ca07a205ac971ce5d7fd206704f7cce3b0ae7d70 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -31,7 +31,7 @@ impl EventEmitter for Breadcrumbs {} impl Render for Breadcrumbs { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let element = h_stack().text_ui(); + let element = h_flex().text_ui(); let Some(active_item) = self.active_item.as_ref() else { return element; }; @@ -51,7 +51,7 @@ impl Render for Breadcrumbs { Label::new("›").color(Color::Muted).into_any_element() }); - let breadcrumbs_stack = h_stack().gap_1().children(breadcrumbs); + let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs); match active_item .downcast::() .map(|editor| editor.downgrade()) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 87d2e9aa78df53676d003de0fd044cbab797bc5a..6d57a42ff7e689cb51140f89d0ef7f8863e394d2 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -442,6 +442,8 @@ impl ActiveCall { .location .as_ref() .and_then(|location| location.upgrade()); + let channel_id = room.read(cx).channel_id(); + cx.emit(Event::RoomJoined { channel_id }); room.update(cx, |room, cx| room.set_location(location.as_ref(), cx)) } } else { diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 45c6c15fb00a4cc42131d7b3acfa8524201044c1..97ba2fc97d92c1d245a772f35784bf32511bb2e4 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -26,6 +26,9 @@ pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); #[derive(Clone, Debug, PartialEq, Eq)] pub enum Event { + RoomJoined { + channel_id: Option, + }, ParticipantLocationChanged { participant_id: proto::PeerId, }, @@ -49,7 +52,9 @@ pub enum Event { RemoteProjectInvitationDiscarded { project_id: u64, }, - Left, + Left { + channel_id: Option, + }, } pub struct Room { @@ -150,17 +155,17 @@ impl Room { let connect = room.connect(&connection_info.server_url, &connection_info.token); cx.spawn(|this, mut cx| async move { connect.await?; - - let is_read_only = this - .update(&mut cx, |room, _| room.read_only()) - .unwrap_or(true); - - if !cx.update(|cx| Self::mute_on_join(cx))? && !is_read_only { - this.update(&mut cx, |this, cx| this.share_microphone(cx))? - .await?; - } - - anyhow::Ok(()) + this.update(&mut cx, |this, cx| { + if !this.read_only() { + if let Some(live_kit) = &this.live_kit { + if !live_kit.muted_by_user && !live_kit.deafened { + return this.share_microphone(cx); + } + } + } + Task::ready(Ok(())) + })? + .await }) .detach_and_log_err(cx); @@ -169,7 +174,7 @@ impl Room { screen_track: LocalTrack::None, microphone_track: LocalTrack::None, next_publish_id: 0, - muted_by_user: false, + muted_by_user: Self::mute_on_join(cx), deafened: false, speaking: false, _maintain_room, @@ -357,7 +362,9 @@ impl Room { pub(crate) fn leave(&mut self, cx: &mut ModelContext) -> Task> { cx.notify(); - cx.emit(Event::Left); + cx.emit(Event::Left { + channel_id: self.channel_id(), + }); self.leave_internal(cx) } @@ -598,6 +605,14 @@ impl Room { .map(|participant| participant.role) } + pub fn contains_guests(&self) -> bool { + self.local_participant.role == proto::ChannelRole::Guest + || self + .remote_participants + .values() + .any(|p| p.role == proto::ChannelRole::Guest) + } + pub fn local_participant_is_admin(&self) -> bool { self.local_participant.role == proto::ChannelRole::Admin } @@ -1032,6 +1047,15 @@ impl Room { } RoomUpdate::SubscribedToRemoteAudioTrack(track, publication) => { + if let Some(live_kit) = &self.live_kit { + if live_kit.deafened { + track.stop(); + cx.foreground_executor() + .spawn(publication.set_enabled(false)) + .detach(); + } + } + let user_id = track.publisher_id().parse()?; let track_id = track.sid().to_string(); let participant = self @@ -1286,15 +1310,12 @@ impl Room { }) } - pub fn is_muted(&self, cx: &AppContext) -> bool { - self.live_kit - .as_ref() - .and_then(|live_kit| match &live_kit.microphone_track { - LocalTrack::None => Some(Self::mute_on_join(cx)), - LocalTrack::Pending { muted, .. } => Some(*muted), - LocalTrack::Published { muted, .. } => Some(*muted), - }) - .unwrap_or(false) + pub fn is_muted(&self) -> bool { + self.live_kit.as_ref().map_or(false, |live_kit| { + matches!(live_kit.microphone_track, LocalTrack::None) + || live_kit.muted_by_user + || live_kit.deafened + }) } pub fn read_only(&self) -> bool { @@ -1316,16 +1337,11 @@ impl Room { pub fn share_microphone(&mut self, cx: &mut ModelContext) -> Task> { if self.status.is_offline() { return Task::ready(Err(anyhow!("room is offline"))); - } else if self.is_sharing_mic() { - return Task::ready(Err(anyhow!("microphone was already shared"))); } let publish_id = if let Some(live_kit) = self.live_kit.as_mut() { let publish_id = post_inc(&mut live_kit.next_publish_id); - live_kit.microphone_track = LocalTrack::Pending { - publish_id, - muted: false, - }; + live_kit.microphone_track = LocalTrack::Pending { publish_id }; cx.notify(); publish_id } else { @@ -1354,14 +1370,13 @@ impl Room { .as_mut() .ok_or_else(|| anyhow!("live-kit was not initialized"))?; - let (canceled, muted) = if let LocalTrack::Pending { + let canceled = if let LocalTrack::Pending { publish_id: cur_publish_id, - muted, } = &live_kit.microphone_track { - (*cur_publish_id != publish_id, *muted) + *cur_publish_id != publish_id } else { - (true, false) + true }; match publication { @@ -1369,14 +1384,13 @@ impl Room { if canceled { live_kit.room.unpublish_track(publication); } else { - if muted { + if live_kit.muted_by_user || live_kit.deafened { cx.background_executor() - .spawn(publication.set_mute(muted)) + .spawn(publication.set_mute(true)) .detach(); } live_kit.microphone_track = LocalTrack::Published { track_publication: publication, - muted, }; cx.notify(); } @@ -1405,10 +1419,7 @@ impl Room { let (displays, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() { let publish_id = post_inc(&mut live_kit.next_publish_id); - live_kit.screen_track = LocalTrack::Pending { - publish_id, - muted: false, - }; + live_kit.screen_track = LocalTrack::Pending { publish_id }; cx.notify(); (live_kit.room.display_sources(), publish_id) } else { @@ -1442,14 +1453,13 @@ impl Room { .as_mut() .ok_or_else(|| anyhow!("live-kit was not initialized"))?; - let (canceled, muted) = if let LocalTrack::Pending { + let canceled = if let LocalTrack::Pending { publish_id: cur_publish_id, - muted, } = &live_kit.screen_track { - (*cur_publish_id != publish_id, *muted) + *cur_publish_id != publish_id } else { - (true, false) + true }; match publication { @@ -1457,14 +1467,8 @@ impl Room { if canceled { live_kit.room.unpublish_track(publication); } else { - if muted { - cx.background_executor() - .spawn(publication.set_mute(muted)) - .detach(); - } live_kit.screen_track = LocalTrack::Published { track_publication: publication, - muted, }; cx.notify(); } @@ -1487,61 +1491,51 @@ impl Room { }) } - pub fn toggle_mute(&mut self, cx: &mut ModelContext) -> Result>> { - let should_mute = !self.is_muted(cx); + pub fn toggle_mute(&mut self, cx: &mut ModelContext) { if let Some(live_kit) = self.live_kit.as_mut() { - if matches!(live_kit.microphone_track, LocalTrack::None) { - return Ok(self.share_microphone(cx)); + // When unmuting, undeafen if the user was deafened before. + let was_deafened = live_kit.deafened; + if live_kit.muted_by_user + || live_kit.deafened + || matches!(live_kit.microphone_track, LocalTrack::None) + { + live_kit.muted_by_user = false; + live_kit.deafened = false; + } else { + live_kit.muted_by_user = true; } + let muted = live_kit.muted_by_user; + let should_undeafen = was_deafened && !live_kit.deafened; - let (ret_task, old_muted) = live_kit.set_mute(should_mute, cx)?; - live_kit.muted_by_user = should_mute; + if let Some(task) = self.set_mute(muted, cx) { + task.detach_and_log_err(cx); + } - if old_muted == true && live_kit.deafened == true { - if let Some(task) = self.toggle_deafen(cx).ok() { - task.detach(); + if should_undeafen { + if let Some(task) = self.set_deafened(false, cx) { + task.detach_and_log_err(cx); } } - - Ok(ret_task) - } else { - Err(anyhow!("LiveKit not started")) } } - pub fn toggle_deafen(&mut self, cx: &mut ModelContext) -> Result>> { + pub fn toggle_deafen(&mut self, cx: &mut ModelContext) { if let Some(live_kit) = self.live_kit.as_mut() { - (*live_kit).deafened = !live_kit.deafened; - - let mut tasks = Vec::with_capacity(self.remote_participants.len()); - // Context notification is sent within set_mute itself. - let mut mute_task = None; - // When deafening, mute user's mic as well. - // When undeafening, unmute user's mic unless it was manually muted prior to deafening. - if live_kit.deafened || !live_kit.muted_by_user { - mute_task = Some(live_kit.set_mute(live_kit.deafened, cx)?.0); - }; - for participant in self.remote_participants.values() { - for track in live_kit - .room - .remote_audio_track_publications(&participant.user.id.to_string()) - { - let deafened = live_kit.deafened; - tasks.push(cx.foreground_executor().spawn(track.set_enabled(!deafened))); - } + // When deafening, mute the microphone if it was not already muted. + // When un-deafening, unmute the microphone, unless it was explicitly muted. + let deafened = !live_kit.deafened; + live_kit.deafened = deafened; + let should_change_mute = !live_kit.muted_by_user; + + if let Some(task) = self.set_deafened(deafened, cx) { + task.detach_and_log_err(cx); } - Ok(cx.foreground_executor().spawn(async move { - if let Some(mute_task) = mute_task { - mute_task.await?; - } - for task in tasks { - task.await?; + if should_change_mute { + if let Some(task) = self.set_mute(deafened, cx) { + task.detach_and_log_err(cx); } - Ok(()) - })) - } else { - Err(anyhow!("LiveKit not started")) + } } } @@ -1572,6 +1566,70 @@ impl Room { } } + fn set_deafened( + &mut self, + deafened: bool, + cx: &mut ModelContext, + ) -> Option>> { + let live_kit = self.live_kit.as_mut()?; + cx.notify(); + + let mut track_updates = Vec::new(); + for participant in self.remote_participants.values() { + for publication in live_kit + .room + .remote_audio_track_publications(&participant.user.id.to_string()) + { + track_updates.push(publication.set_enabled(!deafened)); + } + + for track in participant.audio_tracks.values() { + if deafened { + track.stop(); + } else { + track.start(); + } + } + } + + Some(cx.foreground_executor().spawn(async move { + for result in futures::future::join_all(track_updates).await { + result?; + } + Ok(()) + })) + } + + fn set_mute( + &mut self, + should_mute: bool, + cx: &mut ModelContext, + ) -> Option>> { + let live_kit = self.live_kit.as_mut()?; + cx.notify(); + + if should_mute { + Audio::play_sound(Sound::Mute, cx); + } else { + Audio::play_sound(Sound::Unmute, cx); + } + + match &mut live_kit.microphone_track { + LocalTrack::None => { + if should_mute { + None + } else { + Some(self.share_microphone(cx)) + } + } + LocalTrack::Pending { .. } => None, + LocalTrack::Published { track_publication } => Some( + cx.foreground_executor() + .spawn(track_publication.set_mute(should_mute)), + ), + } + } + #[cfg(any(test, feature = "test-support"))] pub fn set_display_sources(&self, sources: Vec) { self.live_kit @@ -1596,50 +1654,6 @@ struct LiveKitRoom { } impl LiveKitRoom { - fn set_mute( - self: &mut LiveKitRoom, - should_mute: bool, - cx: &mut ModelContext, - ) -> Result<(Task>, bool)> { - if !should_mute { - // clear user muting state. - self.muted_by_user = false; - } - - let (result, old_muted) = match &mut self.microphone_track { - LocalTrack::None => Err(anyhow!("microphone was not shared")), - LocalTrack::Pending { muted, .. } => { - let old_muted = *muted; - *muted = should_mute; - cx.notify(); - Ok((Task::Ready(Some(Ok(()))), old_muted)) - } - LocalTrack::Published { - track_publication, - muted, - } => { - let old_muted = *muted; - *muted = should_mute; - cx.notify(); - Ok(( - cx.background_executor() - .spawn(track_publication.set_mute(*muted)), - old_muted, - )) - } - }?; - - if old_muted != should_mute { - if should_mute { - Audio::play_sound(Sound::Mute, cx); - } else { - Audio::play_sound(Sound::Unmute, cx); - } - } - - Ok((result, old_muted)) - } - fn stop_publishing(&mut self, cx: &mut ModelContext) { if let LocalTrack::Published { track_publication, .. @@ -1663,11 +1677,9 @@ enum LocalTrack { None, Pending { publish_id: usize, - muted: bool, }, Published { track_publication: LocalTrackPublication, - muted: bool, }, } diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs index d2250972f3095893458a873980d26ceb13240cf3..e9353a14419adf069a6319ed76997754008caa18 100644 --- a/crates/channel/src/channel_chat.rs +++ b/crates/channel/src/channel_chat.rs @@ -144,7 +144,7 @@ impl ChannelChat { message: MessageParams, cx: &mut ModelContext, ) -> Result>> { - if message.text.is_empty() { + if message.text.trim().is_empty() { Err(anyhow!("message body can't be empty"))?; } @@ -174,6 +174,8 @@ impl ChannelChat { let user_store = self.user_store.clone(); let rpc = self.rpc.clone(); let outgoing_messages_lock = self.outgoing_messages_lock.clone(); + + // todo - handle messages that fail to send (e.g. >1024 chars) Ok(cx.spawn(move |this, mut cx| async move { let outgoing_message_guard = outgoing_messages_lock.lock().await; let request = rpc.request(proto::SendChannelMessage { diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index aa0c7c4af58be6c181a04e2b73cff55230096bf9..32cf9efba230da83f68d568818d20a52bfc862dd 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -14,6 +14,8 @@ use sysinfo::{ }; use tempfile::NamedTempFile; use util::http::HttpClient; +#[cfg(not(debug_assertions))] +use util::ResultExt; use util::{channel::ReleaseChannel, TryFutureExt}; use self::event_coalescer::EventCoalescer; @@ -114,7 +116,7 @@ pub enum Event { milliseconds_since_first_event: i64, }, App { - operation: &'static str, + operation: String, milliseconds_since_first_event: i64, }, Setting { @@ -127,6 +129,11 @@ pub enum Event { environment: &'static str, milliseconds_since_first_event: i64, }, + Action { + source: &'static str, + action: String, + milliseconds_since_first_event: i64, + }, } #[cfg(debug_assertions)] @@ -167,6 +174,20 @@ impl Telemetry { event_coalescer: EventCoalescer::new(), })); + #[cfg(not(debug_assertions))] + cx.background_executor() + .spawn({ + let state = state.clone(); + async move { + if let Some(tempfile) = + NamedTempFile::new_in(util::paths::CONFIG_DIR.as_path()).log_err() + { + state.lock().log_file = Some(tempfile); + } + } + }) + .detach(); + cx.observe_global::({ let state = state.clone(); @@ -203,7 +224,7 @@ impl Telemetry { // TestAppContext ends up calling this function on shutdown and it panics when trying to find the TelemetrySettings #[cfg(not(any(test, feature = "test-support")))] fn shutdown_telemetry(self: &Arc) -> impl Future { - self.report_app_event("close"); + self.report_app_event("close".to_string()); // TODO: close final edit period and make sure it's sent Task::ready(()) } @@ -369,7 +390,7 @@ impl Telemetry { self.report_event(event) } - pub fn report_app_event(self: &Arc, operation: &'static str) { + pub fn report_app_event(self: &Arc, operation: String) { let event = Event::App { operation, milliseconds_since_first_event: self.milliseconds_since_first_event(), @@ -388,20 +409,6 @@ impl Telemetry { self.report_event(event) } - fn milliseconds_since_first_event(&self) -> i64 { - let mut state = self.state.lock(); - match state.first_event_datetime { - Some(first_event_datetime) => { - let now: DateTime = Utc::now(); - now.timestamp_millis() - first_event_datetime.timestamp_millis() - } - None => { - state.first_event_datetime = Some(Utc::now()); - 0 - } - } - } - pub fn log_edit_event(self: &Arc, environment: &'static str) { let mut state = self.state.lock(); let period_data = state.event_coalescer.log_event(environment); @@ -418,6 +425,31 @@ impl Telemetry { } } + pub fn report_action_event(self: &Arc, source: &'static str, action: String) { + let event = Event::Action { + source, + action, + milliseconds_since_first_event: self.milliseconds_since_first_event(), + }; + + self.report_event(event) + } + + fn milliseconds_since_first_event(&self) -> i64 { + let mut state = self.state.lock(); + + match state.first_event_datetime { + Some(first_event_datetime) => { + let now: DateTime = Utc::now(); + now.timestamp_millis() - first_event_datetime.timestamp_millis() + } + None => { + state.first_event_datetime = Some(Utc::now()); + 0 + } + } + } + fn report_event(self: &Arc, event: Event) { let mut state = self.state.lock(); diff --git a/crates/collab/src/db/queries/messages.rs b/crates/collab/src/db/queries/messages.rs index 47bb27df39060a7fff27bd50218e9b3626d13dd6..96942c052df75c9406272da3a563533342f64406 100644 --- a/crates/collab/src/db/queries/messages.rs +++ b/crates/collab/src/db/queries/messages.rs @@ -256,6 +256,7 @@ impl Database { message_id = result.last_insert_id; let mentioned_user_ids = mentions.iter().map(|m| m.user_id).collect::>(); + let mentions = mentions .iter() .filter_map(|mention| { diff --git a/crates/collab/src/tests/channel_guest_tests.rs b/crates/collab/src/tests/channel_guest_tests.rs index d5933235926fa1da8c1e8538dedd2a7506504cd8..f3326cd6922b7482f6c9d953a5a57a89676b717d 100644 --- a/crates/collab/src/tests/channel_guest_tests.rs +++ b/crates/collab/src/tests/channel_guest_tests.rs @@ -57,7 +57,7 @@ async fn test_channel_guests( }) .await .is_err()); - assert!(room_b.read_with(cx_b, |room, _| !room.is_sharing_mic())); + assert!(room_b.read_with(cx_b, |room, _| room.is_muted())); } #[gpui::test] @@ -104,6 +104,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test }); assert!(project_b.read_with(cx_b, |project, _| project.is_read_only())); assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx))); + assert!(room_b.read_with(cx_b, |room, _| room.read_only())); assert!(room_b .update(cx_b, |room, cx| room.share_microphone(cx)) .await @@ -127,10 +128,13 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test // project and buffers are now editable assert!(project_b.read_with(cx_b, |project, _| !project.is_read_only())); assert!(editor_b.update(cx_b, |editor, cx| !editor.read_only(cx))); - room_b - .update(cx_b, |room, cx| room.share_microphone(cx)) - .await - .unwrap(); + + // B sees themselves as muted, and can unmute. + assert!(room_b.read_with(cx_b, |room, _| !room.read_only())); + room_b.read_with(cx_b, |room, _| assert!(room.is_muted())); + room_b.update(cx_b, |room, cx| room.toggle_mute(cx)); + cx_a.run_until_parked(); + room_b.read_with(cx_b, |room, _| assert!(!room.is_muted())); // B is demoted active_call_a diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 2a88bc4c579db9855184e278d1fef37200d1f470..e80fe0fdca312bed54d98fce0a1ea69a8a4a6e86 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -1418,8 +1418,6 @@ async fn test_channel_moving( ) { let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; - // let client_b = server.create_client(cx_b, "user_b").await; - // let client_c = server.create_client(cx_c, "user_c").await; let channels = server .make_channel_tree( diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index cedc841527ff5c4c40d2630dc9c15166ccec46d8..e68fd10d8d16b29300c3fa658596468df06be880 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -1876,6 +1876,186 @@ fn active_call_events(cx: &mut TestAppContext) -> Rc>> events } +#[gpui::test] +async fn test_mute_deafen( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + + server + .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) + .await; + + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + let active_call_c = cx_c.read(ActiveCall::global); + + // User A calls user B, B answers. + active_call_a + .update(cx_a, |call, cx| { + call.invite(client_b.user_id().unwrap(), None, cx) + }) + .await + .unwrap(); + executor.run_until_parked(); + active_call_b + .update(cx_b, |call, cx| call.accept_incoming(cx)) + .await + .unwrap(); + executor.run_until_parked(); + + let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); + let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone()); + + room_a.read_with(cx_a, |room, _| assert!(!room.is_muted())); + room_b.read_with(cx_b, |room, _| assert!(!room.is_muted())); + + // Users A and B are both muted. + assert_eq!( + participant_audio_state(&room_a, cx_a), + &[ParticipantAudioState { + user_id: client_b.user_id().unwrap(), + is_muted: false, + audio_tracks_playing: vec![true], + }] + ); + assert_eq!( + participant_audio_state(&room_b, cx_b), + &[ParticipantAudioState { + user_id: client_a.user_id().unwrap(), + is_muted: false, + audio_tracks_playing: vec![true], + }] + ); + + // User A mutes + room_a.update(cx_a, |room, cx| room.toggle_mute(cx)); + executor.run_until_parked(); + + // User A hears user B, but B doesn't hear A. + room_a.read_with(cx_a, |room, _| assert!(room.is_muted())); + room_b.read_with(cx_b, |room, _| assert!(!room.is_muted())); + assert_eq!( + participant_audio_state(&room_a, cx_a), + &[ParticipantAudioState { + user_id: client_b.user_id().unwrap(), + is_muted: false, + audio_tracks_playing: vec![true], + }] + ); + assert_eq!( + participant_audio_state(&room_b, cx_b), + &[ParticipantAudioState { + user_id: client_a.user_id().unwrap(), + is_muted: true, + audio_tracks_playing: vec![true], + }] + ); + + // User A deafens + room_a.update(cx_a, |room, cx| room.toggle_deafen(cx)); + executor.run_until_parked(); + + // User A does not hear user B. + room_a.read_with(cx_a, |room, _| assert!(room.is_muted())); + room_b.read_with(cx_b, |room, _| assert!(!room.is_muted())); + assert_eq!( + participant_audio_state(&room_a, cx_a), + &[ParticipantAudioState { + user_id: client_b.user_id().unwrap(), + is_muted: false, + audio_tracks_playing: vec![false], + }] + ); + assert_eq!( + participant_audio_state(&room_b, cx_b), + &[ParticipantAudioState { + user_id: client_a.user_id().unwrap(), + is_muted: true, + audio_tracks_playing: vec![true], + }] + ); + + // User B calls user C, C joins. + active_call_b + .update(cx_b, |call, cx| { + call.invite(client_c.user_id().unwrap(), None, cx) + }) + .await + .unwrap(); + executor.run_until_parked(); + active_call_c + .update(cx_c, |call, cx| call.accept_incoming(cx)) + .await + .unwrap(); + executor.run_until_parked(); + + // User A does not hear users B or C. + assert_eq!( + participant_audio_state(&room_a, cx_a), + &[ + ParticipantAudioState { + user_id: client_b.user_id().unwrap(), + is_muted: false, + audio_tracks_playing: vec![false], + }, + ParticipantAudioState { + user_id: client_c.user_id().unwrap(), + is_muted: false, + audio_tracks_playing: vec![false], + } + ] + ); + assert_eq!( + participant_audio_state(&room_b, cx_b), + &[ + ParticipantAudioState { + user_id: client_a.user_id().unwrap(), + is_muted: true, + audio_tracks_playing: vec![true], + }, + ParticipantAudioState { + user_id: client_c.user_id().unwrap(), + is_muted: false, + audio_tracks_playing: vec![true], + } + ] + ); + + #[derive(PartialEq, Eq, Debug)] + struct ParticipantAudioState { + user_id: u64, + is_muted: bool, + audio_tracks_playing: Vec, + } + + fn participant_audio_state( + room: &Model, + cx: &TestAppContext, + ) -> Vec { + room.read_with(cx, |room, _| { + room.remote_participants() + .iter() + .map(|(user_id, participant)| ParticipantAudioState { + user_id: *user_id, + is_muted: participant.muted, + audio_tracks_playing: participant + .audio_tracks + .values() + .map(|track| track.is_playing()) + .collect(), + }) + .collect::>() + }) + } +} + #[gpui::test(iterations = 10)] async fn test_room_location( executor: BackgroundExecutor, diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 4fcf6aa67652fabe9187ba5f78b8899379dc471b..cda0621cb32385a399fdfdaef51821dd531281b2 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -248,7 +248,6 @@ impl TestServer { language::init(cx); editor::init(cx); workspace::init(app_state.clone(), cx); - audio::init((), cx); call::init(client.clone(), user_store.clone(), cx); channel::init(&client, user_store.clone(), cx); notifications::init(client.clone(), user_store, cx); diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index ce68acfbd83379e77c298152aa95b51280883e0c..033889f771d84bafc13dae5005957096e61cb3c6 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -266,6 +266,10 @@ impl Item for ChannelView { .into_any_element() } + fn telemetry_event_text(&self) -> Option<&'static str> { + None + } + fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext) -> Option> { Some(cx.new_view(|cx| { Self::new( diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 5786ab10d4ca59b998b1b16ea7bb3c53611b4399..921e3388a65637fad51f9a093f8538500965923a 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -1,15 +1,15 @@ -use crate::{channel_view::ChannelView, is_channels_feature_enabled, ChatPanelSettings}; +use crate::{collab_panel, is_channels_feature_enabled, ChatPanelSettings}; use anyhow::Result; -use call::ActiveCall; +use call::{room, ActiveCall}; use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore}; use client::Client; use collections::HashMap; use db::kvp::KEY_VALUE_STORE; use editor::Editor; use gpui::{ - actions, div, list, prelude::*, px, AnyElement, AppContext, AsyncWindowContext, ClickEvent, - ElementId, EventEmitter, FocusableView, ListOffset, ListScrollEvent, ListState, Model, Render, - Subscription, Task, View, ViewContext, VisualContext, WeakView, + actions, div, list, prelude::*, px, Action, AppContext, AsyncWindowContext, DismissEvent, + ElementId, EventEmitter, FocusHandle, FocusableView, FontWeight, ListOffset, ListScrollEvent, + ListState, Model, Render, Subscription, Task, View, ViewContext, VisualContext, WeakView, }; use language::LanguageRegistry; use menu::Confirm; @@ -17,10 +17,13 @@ use message_editor::MessageEditor; use project::Fs; use rich_text::RichText; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsStore}; +use settings::Settings; use std::sync::Arc; use time::{OffsetDateTime, UtcOffset}; -use ui::{prelude::*, Avatar, Button, IconButton, IconName, Label, TabBar, Tooltip}; +use ui::{ + popover_menu, prelude::*, Avatar, Button, ContextMenu, IconButton, IconName, KeyBinding, Label, + TabBar, +}; use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, @@ -54,9 +57,10 @@ pub struct ChatPanel { active: bool, pending_serialization: Task>, subscriptions: Vec, - workspace: WeakView, is_scrolled_to_bottom: bool, markdown_data: HashMap, + focus_handle: FocusHandle, + open_context_menu: Option<(u64, Subscription)>, } #[derive(Serialize, Deserialize)] @@ -64,13 +68,6 @@ struct SerializedChatPanel { width: Option, } -#[derive(Debug)] -pub enum Event { - DockPositionChanged, - Focus, - Dismissed, -} - actions!(chat_panel, [ToggleFocus]); impl ChatPanel { @@ -89,8 +86,6 @@ impl ChatPanel { ) }); - let workspace_handle = workspace.weak_handle(); - cx.new_view(|cx: &mut ViewContext| { let view = cx.view().downgrade(); let message_list = @@ -108,7 +103,7 @@ impl ChatPanel { if event.visible_range.start < MESSAGE_LOADING_THRESHOLD { this.load_more_messages(cx); } - this.is_scrolled_to_bottom = event.visible_range.end == event.count; + this.is_scrolled_to_bottom = !event.is_scrolled; })); let mut this = Self { @@ -122,22 +117,37 @@ impl ChatPanel { message_editor: input_editor, local_timezone: cx.local_timezone(), subscriptions: Vec::new(), - workspace: workspace_handle, is_scrolled_to_bottom: true, active: false, width: None, markdown_data: Default::default(), + focus_handle: cx.focus_handle(), + open_context_menu: None, }; - let mut old_dock_position = this.position(cx); - this.subscriptions.push(cx.observe_global::( - move |this: &mut Self, cx| { - let new_dock_position = this.position(cx); - if new_dock_position != old_dock_position { - old_dock_position = new_dock_position; - cx.emit(Event::DockPositionChanged); + this.subscriptions.push(cx.subscribe( + &ActiveCall::global(cx), + move |this: &mut Self, call, event: &room::Event, cx| match event { + room::Event::RoomJoined { channel_id } => { + if let Some(channel_id) = channel_id { + this.select_channel(*channel_id, None, cx) + .detach_and_log_err(cx); + + if call + .read(cx) + .room() + .is_some_and(|room| room.read(cx).contains_guests()) + { + cx.emit(PanelEvent::Activate) + } + } + } + room::Event::Left { channel_id } => { + if channel_id == &this.channel_id(cx) { + cx.emit(PanelEvent::Close) + } } - cx.notify(); + _ => {} }, )); @@ -145,6 +155,12 @@ impl ChatPanel { }) } + pub fn channel_id(&self, cx: &AppContext) -> Option { + self.active_chat + .as_ref() + .map(|(chat, _)| chat.read(cx).channel_id) + } + pub fn is_scrolled_to_bottom(&self) -> bool { self.is_scrolled_to_bottom } @@ -259,53 +275,9 @@ impl ChatPanel { } } - fn render_channel(&self, cx: &mut ViewContext) -> AnyElement { - v_stack() - .full() - .on_action(cx.listener(Self::send)) - .child( - h_stack().z_index(1).child( - TabBar::new("chat_header") - .child( - h_stack() - .w_full() - .h(rems(ui::Tab::HEIGHT_IN_REMS)) - .px_2() - .child(Label::new( - self.active_chat - .as_ref() - .and_then(|c| { - Some(format!("#{}", c.0.read(cx).channel(cx)?.name)) - }) - .unwrap_or_default(), - )), - ) - .end_child( - IconButton::new("notes", IconName::File) - .on_click(cx.listener(Self::open_notes)) - .tooltip(|cx| Tooltip::text("Open notes", cx)), - ) - .end_child( - IconButton::new("call", IconName::AudioOn) - .on_click(cx.listener(Self::join_call)) - .tooltip(|cx| Tooltip::text("Join call", cx)), - ), - ), - ) - .child(div().flex_grow().px_2().py_1().map(|this| { - if self.active_chat.is_some() { - this.child(list(self.message_list.clone()).full()) - } else { - this - } - })) - .child(h_stack().p_2().child(self.message_editor.clone())) - .into_any() - } - fn render_message(&mut self, ix: usize, cx: &mut ViewContext) -> impl IntoElement { let active_chat = &self.active_chat.as_ref().unwrap().0; - let (message, is_continuation_from_previous, is_continuation_to_next, is_admin) = + let (message, is_continuation_from_previous, is_admin) = active_chat.update(cx, |active_chat, cx| { let is_admin = self .channel_store @@ -314,13 +286,9 @@ impl ChatPanel { let last_message = active_chat.message(ix.saturating_sub(1)); let this_message = active_chat.message(ix).clone(); - let next_message = - active_chat.message(ix.saturating_add(1).min(active_chat.message_count() - 1)); let is_continuation_from_previous = last_message.id != this_message.id && last_message.sender.id == this_message.sender.id; - let is_continuation_to_next = this_message.id != next_message.id - && this_message.sender.id == next_message.sender.id; if let ChannelMessageId::Saved(id) = this_message.id { if this_message @@ -332,12 +300,7 @@ impl ChatPanel { } } - ( - this_message, - is_continuation_from_previous, - is_continuation_to_next, - is_admin, - ) + (this_message, is_continuation_from_previous, is_admin) }); let _is_pending = message.is_pending(); @@ -360,50 +323,100 @@ impl ChatPanel { ChannelMessageId::Saved(id) => ("saved-message", id).into(), ChannelMessageId::Pending(id) => ("pending-message", id).into(), }; + let this = cx.view().clone(); - v_stack() + v_flex() .w_full() - .id(element_id) .relative() .overflow_hidden() - .group("") .when(!is_continuation_from_previous, |this| { - this.child( - h_stack() - .gap_2() - .child(Avatar::new(message.sender.avatar_uri.clone())) - .child(Label::new(message.sender.github_login.clone())) + this.pt_3().child( + h_flex() + .child( + div().absolute().child( + Avatar::new(message.sender.avatar_uri.clone()) + .size(cx.rem_size() * 1.5), + ), + ) + .child( + div() + .pl(cx.rem_size() * 1.5 + px(6.0)) + .pr(px(8.0)) + .font_weight(FontWeight::BOLD) + .child(Label::new(message.sender.github_login.clone())), + ) .child( Label::new(format_timestamp( message.timestamp, now, self.local_timezone, )) + .size(LabelSize::Small) .color(Color::Muted), ), ) }) - .when(!is_continuation_to_next, |this| - // HACK: This should really be a margin, but margins seem to get collapsed. - this.pb_2()) - .child(text.element("body".into(), cx)) + .when(is_continuation_from_previous, |this| this.pt_1()) .child( - div() - .absolute() - .top_1() - .right_2() - .w_8() - .visible_on_hover("") - .children(message_id_to_remove.map(|message_id| { - IconButton::new(("remove", message_id), IconName::XCircle).on_click( - cx.listener(move |this, _, cx| { - this.remove_message(message_id, cx); - }), - ) - })), + v_flex() + .w_full() + .text_ui_sm() + .id(element_id) + .group("") + .child(text.element("body".into(), cx)) + .child( + div() + .absolute() + .z_index(1) + .right_0() + .w_6() + .bg(cx.theme().colors().panel_background) + .when(!self.has_open_menu(message_id_to_remove), |el| { + el.visible_on_hover("") + }) + .children(message_id_to_remove.map(|message_id| { + popover_menu(("menu", message_id)) + .trigger(IconButton::new( + ("trigger", message_id), + IconName::Ellipsis, + )) + .menu(move |cx| { + Some(Self::render_message_menu(&this, message_id, cx)) + }) + })), + ), ) } + fn has_open_menu(&self, message_id: Option) -> bool { + match self.open_context_menu.as_ref() { + Some((id, _)) => Some(*id) == message_id, + None => false, + } + } + + fn render_message_menu( + this: &View, + message_id: u64, + cx: &mut WindowContext, + ) -> View { + let menu = { + let this = this.clone(); + ContextMenu::build(cx, move |menu, _| { + menu.entry("Delete message", None, move |cx| { + this.update(cx, |this, cx| this.remove_message(message_id, cx)) + }) + }) + }; + this.update(cx, |this, cx| { + let subscription = cx.subscribe(&menu, |this: &mut Self, _, _: &DismissEvent, _| { + this.open_context_menu = None; + }); + this.open_context_menu = Some((message_id, subscription)); + }); + menu + } + fn render_markdown_with_mentions( language_registry: &Arc, current_user_id: u64, @@ -421,44 +434,6 @@ impl ChatPanel { rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None) } - fn render_sign_in_prompt(&self, cx: &mut ViewContext) -> impl IntoElement { - v_stack() - .gap_2() - .p_4() - .child( - Button::new("sign-in", "Sign in") - .style(ButtonStyle::Filled) - .icon_color(Color::Muted) - .icon(IconName::Github) - .icon_position(IconPosition::Start) - .full_width() - .on_click(cx.listener(move |this, _, cx| { - let client = this.client.clone(); - cx.spawn(|this, mut cx| async move { - if client - .authenticate_and_connect(true, &cx) - .log_err() - .await - .is_some() - { - this.update(&mut cx, |_, cx| { - cx.focus_self(); - }) - .ok(); - } - }) - .detach(); - })), - ) - .child( - div().flex().w_full().items_center().child( - Label::new("Sign in to chat.") - .color(Color::Muted) - .size(LabelSize::Small), - ), - ) - } - fn send(&mut self, _: &Confirm, cx: &mut ViewContext) { if let Some((chat, _)) = self.active_chat.as_ref() { let message = self @@ -535,50 +510,93 @@ impl ChatPanel { Ok(()) }) } - - fn open_notes(&mut self, _: &ClickEvent, cx: &mut ViewContext) { - if let Some((chat, _)) = &self.active_chat { - let channel_id = chat.read(cx).channel_id; - if let Some(workspace) = self.workspace.upgrade() { - ChannelView::open(channel_id, workspace, cx).detach(); - } - } - } - - fn join_call(&mut self, _: &ClickEvent, cx: &mut ViewContext) { - if let Some((chat, _)) = &self.active_chat { - let channel_id = chat.read(cx).channel_id; - ActiveCall::global(cx) - .update(cx, |call, cx| call.join_channel(channel_id, cx)) - .detach_and_log_err(cx); - } - } } -impl EventEmitter for ChatPanel {} - impl Render for ChatPanel { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - v_stack() - .size_full() - .map(|this| match (self.client.user_id(), self.active_chat()) { - (Some(_), Some(_)) => this.child(self.render_channel(cx)), - (Some(_), None) => this.child( - div().p_4().child( - Label::new("Select a channel to chat in.") - .size(LabelSize::Small) - .color(Color::Muted), + v_flex() + .track_focus(&self.focus_handle) + .full() + .on_action(cx.listener(Self::send)) + .child( + h_flex().z_index(1).child( + TabBar::new("chat_header").child( + h_flex() + .w_full() + .h(rems(ui::Tab::CONTAINER_HEIGHT_IN_REMS)) + .px_2() + .child(Label::new( + self.active_chat + .as_ref() + .and_then(|c| { + Some(format!("#{}", c.0.read(cx).channel(cx)?.name)) + }) + .unwrap_or("Chat".to_string()), + )), ), ), - (None, _) => this.child(self.render_sign_in_prompt(cx)), - }) - .min_w(px(150.)) + ) + .child(div().flex_grow().px_2().pt_1().map(|this| { + if self.active_chat.is_some() { + this.child(list(self.message_list.clone()).full()) + } else { + this.child( + div() + .p_4() + .child( + Label::new("Select a channel to chat in.") + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child( + div().pt_1().w_full().items_center().child( + Button::new("toggle-collab", "Open") + .full_width() + .key_binding(KeyBinding::for_action( + &collab_panel::ToggleFocus, + cx, + )) + .on_click(|_, cx| { + cx.dispatch_action( + collab_panel::ToggleFocus.boxed_clone(), + ) + }), + ), + ), + ) + } + })) + .child( + h_flex() + .when(!self.is_scrolled_to_bottom, |el| { + el.border_t_1().border_color(cx.theme().colors().border) + }) + .p_2() + .map(|el| { + if self.active_chat.is_some() { + el.child(self.message_editor.clone()) + } else { + el.child( + div() + .rounded_md() + .h_7() + .w_full() + .bg(cx.theme().colors().editor_background), + ) + } + }), + ) + .into_any() } } impl FocusableView for ChatPanel { fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { - self.message_editor.read(cx).focus_handle(cx) + if self.active_chat.is_some() { + self.message_editor.read(cx).focus_handle(cx) + } else { + self.focus_handle.clone() + } } } @@ -613,7 +631,7 @@ impl Panel for ChatPanel { if active { self.acknowledge_last_message(cx); if !is_channels_feature_enabled(cx) { - cx.emit(Event::Dismissed); + cx.emit(PanelEvent::Close); } } } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 5ad3d6cfa3213119551ed341be33816336f8ca5c..9cb447153c1bc39b9d291a2e17020cae788b43ca 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -26,7 +26,7 @@ use menu::{Cancel, Confirm, SelectNext, SelectPrev}; use project::{Fs, Project}; use rpc::proto::{self, PeerId}; use serde_derive::{Deserialize, Serialize}; -use settings::{Settings, SettingsStore}; +use settings::Settings; use smallvec::SmallVec; use std::{mem, sync::Arc}; use theme::{ActiveTheme, ThemeSettings}; @@ -254,19 +254,6 @@ impl CollabPanel { this.update_entries(false, cx); - // Update the dock position when the setting changes. - let mut old_dock_position = this.position(cx); - this.subscriptions.push(cx.observe_global::( - move |this: &mut Self, cx| { - let new_dock_position = this.position(cx); - if new_dock_position != old_dock_position { - old_dock_position = new_dock_position; - cx.emit(PanelEvent::ChangePosition); - } - cx.notify(); - }, - )); - let active_call = ActiveCall::global(cx); this.subscriptions .push(cx.observe(&this.user_store, |this, _, cx| { @@ -900,7 +887,7 @@ impl CollabPanel { .ok(); })) .start_slot( - h_stack() + h_flex() .gap_1() .child(render_tree_branch(is_last, false, cx)) .child(IconButton::new(0, IconName::Folder)), @@ -921,7 +908,7 @@ impl CollabPanel { ListItem::new(("screen", id)) .selected(is_selected) .start_slot( - h_stack() + h_flex() .gap_1() .child(render_tree_branch(is_last, false, cx)) .child(IconButton::new(0, IconName::Screen)), @@ -962,7 +949,7 @@ impl CollabPanel { this.open_channel_notes(channel_id, cx); })) .start_slot( - h_stack() + h_flex() .gap_1() .child(render_tree_branch(false, true, cx)) .child(IconButton::new(0, IconName::File)), @@ -983,7 +970,7 @@ impl CollabPanel { this.join_channel_chat(channel_id, cx); })) .start_slot( - h_stack() + h_flex() .gap_1() .child(render_tree_branch(false, false, cx)) .child(IconButton::new(0, IconName::MessageBubbles)), @@ -1426,14 +1413,6 @@ impl CollabPanel { self.toggle_channel_collapsed(id, cx) } - // fn toggle_channel_collapsed_action( - // &mut self, - // action: &ToggleCollapse, - // cx: &mut ViewContext, - // ) { - // self.toggle_channel_collapsed(action.location, cx); - // } - fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { match self.collapsed_channels.binary_search(&channel_id) { Ok(ix) => { @@ -1747,12 +1726,12 @@ impl CollabPanel { fn render_signed_out(&mut self, cx: &mut ViewContext) -> Div { let collab_blurb = "Work with your team in realtime with collaborative editing, voice, shared notes and more."; - v_stack() + v_flex() .gap_6() .p_4() .child(Label::new(collab_blurb)) .child( - v_stack() + v_flex() .gap_2() .child( Button::new("sign_in", "Sign in") @@ -1853,14 +1832,14 @@ impl CollabPanel { } fn render_signed_in(&mut self, cx: &mut ViewContext) -> Div { - v_stack() + v_flex() .size_full() .child(list(self.list_state.clone()).full()) .child( - v_stack() + v_flex() .child(div().mx_2().border_primary(cx).border_t()) .child( - v_stack() + v_flex() .p_2() .child(self.render_filter_input(&self.filter_editor, cx)), ), @@ -1910,7 +1889,6 @@ impl CollabPanel { let mut channel_link = None; let mut channel_tooltip_text = None; let mut channel_icon = None; - // let mut is_dragged_over = false; let text = match section { Section::ActiveCall => { @@ -1983,7 +1961,7 @@ impl CollabPanel { | Section::Offline => true, }; - h_stack() + h_flex() .w_full() .group("section-header") .child( @@ -2029,7 +2007,7 @@ impl CollabPanel { .selected(is_selected) .on_click(cx.listener(move |this, _, cx| this.call(user_id, cx))) .child( - h_stack() + h_flex() .w_full() .justify_between() .child(Label::new(github_login.clone())) @@ -2052,7 +2030,7 @@ impl CollabPanel { }), ) .start_slot( - // todo!() handle contacts with no avatar + // todo handle contacts with no avatar Avatar::new(contact.user.avatar_uri.clone()) .availability_indicator(if online { Some(!busy) } else { None }), ) @@ -2127,11 +2105,11 @@ impl CollabPanel { .indent_step_size(px(20.)) .selected(is_selected) .child( - h_stack() + h_flex() .w_full() .justify_between() .child(Label::new(github_login.clone())) - .child(h_stack().children(controls)), + .child(h_flex().children(controls)), ) .start_slot(Avatar::new(user.avatar_uri.clone())) } @@ -2171,11 +2149,11 @@ impl CollabPanel { ListItem::new(("channel-invite", channel.id as usize)) .selected(is_selected) .child( - h_stack() + h_flex() .w_full() .justify_between() .child(Label::new(channel.name.clone())) - .child(h_stack().children(controls)), + .child(h_flex().children(controls)), ) .start_slot( Icon::new(IconName::Hash) @@ -2311,21 +2289,21 @@ impl CollabPanel { .color(Color::Muted), ) .child( - h_stack() + h_flex() .id(channel_id as usize) .child(Label::new(channel.name.clone())) .children(face_pile.map(|face_pile| face_pile.render(cx))), ), ) .child( - h_stack() + h_flex() .absolute() .right(rems(0.)) .h_full() // HACK: Without this the channel name clips on top of the icons, but I'm not sure why. .z_index(10) .child( - h_stack() + h_flex() .h_full() .gap_1() .px_1() @@ -2432,7 +2410,7 @@ fn render_tree_branch(is_last: bool, overdraw: bool, cx: &mut WindowContext) -> impl Render for CollabPanel { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - v_stack() + v_flex() .key_context("CollabPanel") .on_action(cx.listener(CollabPanel::cancel)) .on_action(cx.listener(CollabPanel::select_next)) @@ -2625,7 +2603,7 @@ struct DraggedChannelView { impl Render for DraggedChannelView { fn render(&mut self, cx: &mut ViewContext) -> impl Element { let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); - h_stack() + h_flex() .font(ui_font) .bg(cx.theme().colors().background) .w(self.width) diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 8020613c1ae2a4cc2ac9e0ad61293541c451f0aa..c207e31bbeaa2b087578860c09ae176827074388 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -152,19 +152,19 @@ impl Render for ChannelModal { let visibility = channel.visibility; let mode = self.picker.read(cx).delegate.mode; - v_stack() + v_flex() .key_context("ChannelModal") .on_action(cx.listener(Self::toggle_mode)) .on_action(cx.listener(Self::dismiss)) .elevation_3(cx) .w(rems(34.)) .child( - v_stack() + v_flex() .px_2() .py_1() .gap_2() .child( - h_stack() + h_flex() .w_px() .flex_1() .gap_1() @@ -172,13 +172,13 @@ impl Render for ChannelModal { .child(Label::new(channel_name)), ) .child( - h_stack() + h_flex() .w_full() .h(rems(22. / 16.)) .justify_between() .line_height(rems(1.25)) .child( - h_stack() + h_flex() .gap_2() .child( Checkbox::new( @@ -212,7 +212,7 @@ impl Render for ChannelModal { ), ) .child( - h_stack() + h_flex() .child( div() .id("manage-members") @@ -391,7 +391,7 @@ impl PickerDelegate for ChannelModalDelegate { .selected(selected) .start_slot(Avatar::new(user.avatar_uri.clone())) .child(Label::new(user.github_login.clone())) - .end_slot(h_stack().gap_2().map(|slot| { + .end_slot(h_flex().gap_2().map(|slot| { match self.mode { Mode::ManageMembers => slot .children( diff --git a/crates/collab_ui/src/collab_panel/contact_finder.rs b/crates/collab_ui/src/collab_panel/contact_finder.rs index b769ec7e7f394fb7a94c38f6220b507d43e77ba1..2c59df2eb53f7fa209bca449daefc838f9662e9c 100644 --- a/crates/collab_ui/src/collab_panel/contact_finder.rs +++ b/crates/collab_ui/src/collab_panel/contact_finder.rs @@ -36,17 +36,17 @@ impl ContactFinder { impl Render for ContactFinder { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - v_stack() + v_flex() .elevation_3(cx) .child( - v_stack() + v_flex() .px_2() .py_1() .bg(cx.theme().colors().element_background) // HACK: Prevent the background color from overflowing the parent container. .rounded_t(px(8.)) .child(Label::new("Contacts")) - .child(h_stack().child(Label::new("Invite new contacts"))), + .child(h_flex().child(Label::new("Invite new contacts"))), ) .child(self.picker.clone()) .w(rems(34.)) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 03dfd450704153b31c52ec3d0baad0d215389190..432f8f6cd242b3b7bbeb53464de98980b4079e47 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -14,7 +14,7 @@ use rpc::proto; use std::sync::Arc; use theme::{ActiveTheme, PlayerColors}; use ui::{ - h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon, + h_flex, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon, IconButton, IconName, TintColor, Tooltip, }; use util::ResultExt; @@ -58,7 +58,7 @@ impl Render for CollabTitlebarItem { let client = self.client.clone(); let project_id = self.project.read(cx).remote_id(); - h_stack() + h_flex() .id("titlebar") .justify_between() .w_full() @@ -83,7 +83,7 @@ impl Render for CollabTitlebarItem { }) // left side .child( - h_stack() + h_flex() .gap_1() .children(self.render_project_host(cx)) .child(self.render_project_name(cx)) @@ -102,7 +102,7 @@ impl Render for CollabTitlebarItem { peer_id, true, room.is_speaking(), - room.is_muted(cx), + room.is_muted(), &room, project_id, ¤t_user, @@ -128,7 +128,7 @@ impl Render for CollabTitlebarItem { )?; Some( - v_stack() + v_flex() .id(("collaborator", collaborator.user.id)) .child(face_pile) .child(render_color_ribbon( @@ -160,7 +160,7 @@ impl Render for CollabTitlebarItem { ) // right side .child( - h_stack() + h_flex() .gap_1() .pr_1() .when_some(room, |this, room| { @@ -168,7 +168,7 @@ impl Render for CollabTitlebarItem { let project = self.project.read(cx); let is_local = project.is_local(); let is_shared = is_local && project.is_shared(); - let is_muted = room.is_muted(cx); + let is_muted = room.is_muted(); let is_deafened = room.is_deafened().unwrap_or(false); let is_screen_sharing = room.is_screen_sharing(); let read_only = room.read_only(); @@ -634,7 +634,7 @@ impl CollabTitlebarItem { .trigger( ButtonLike::new("user-menu") .child( - h_stack() + h_flex() .gap_0p5() .child(Avatar::new(user.avatar_uri.clone())) .child(Icon::new(IconName::ChevronDown).color(Color::Muted)), @@ -657,7 +657,7 @@ impl CollabTitlebarItem { .trigger( ButtonLike::new("user-menu") .child( - h_stack() + h_flex() .gap_0p5() .child(Icon::new(IconName::ChevronDown).color(Color::Muted)), ) diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index c8230620b4c7756dbfee2b26011c32634f3b005a..f4248506781a4dca1dbe23c59fd67182a59f392f 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -9,7 +9,7 @@ mod panel_settings; use std::{rc::Rc, sync::Arc}; -use call::{report_call_event_for_room, ActiveCall, Room}; +use call::{report_call_event_for_room, ActiveCall}; pub use collab_panel::CollabPanel; pub use collab_titlebar_item::CollabTitlebarItem; use feature_flags::{ChannelsAlpha, FeatureFlagAppExt}; @@ -21,7 +21,6 @@ pub use panel_settings::{ ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings, }; use settings::Settings; -use util::ResultExt; use workspace::AppState; actions!( @@ -41,10 +40,6 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { chat_panel::init(cx); notification_panel::init(cx); notifications::init(&app_state, cx); - - // cx.add_global_action(toggle_screen_sharing); - // cx.add_global_action(toggle_mute); - // cx.add_global_action(toggle_deafen); } pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) { @@ -79,7 +74,7 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) { if let Some(room) = call.room().cloned() { let client = call.client(); room.update(cx, |room, cx| { - let operation = if room.is_muted(cx) { + let operation = if room.is_muted() { "enable microphone" } else { "disable microphone" @@ -87,17 +82,13 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) { report_call_event_for_room(operation, room.id(), room.channel_id(), &client); room.toggle_mute(cx) - }) - .map(|task| task.detach_and_log_err(cx)) - .log_err(); + }); } } pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) { if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { - room.update(cx, Room::toggle_deafen) - .map(|task| task.detach_and_log_err(cx)) - .log_err(); + room.update(cx, |room, cx| room.toggle_deafen(cx)); } } @@ -131,34 +122,6 @@ fn notification_window_options( } } -// fn render_avatar( -// avatar: Option>, -// avatar_style: &AvatarStyle, -// container: ContainerStyle, -// ) -> AnyElement { -// avatar -// .map(|avatar| { -// Image::from_data(avatar) -// .with_style(avatar_style.image) -// .aligned() -// .contained() -// .with_corner_radius(avatar_style.outer_corner_radius) -// .constrained() -// .with_width(avatar_style.outer_width) -// .with_height(avatar_style.outer_width) -// .into_any() -// }) -// .unwrap_or_else(|| { -// Empty::new() -// .constrained() -// .with_width(avatar_style.outer_width) -// .into_any() -// }) -// .contained() -// .with_style(container) -// .into_any() -// } - fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool { cx.is_staff() || cx.has_flag::() } diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index 95473044a3f4242cd497ce0087fe1e47e8865d6f..b30f8d15f035b5bc49e08449d20efd21b0e5b8c9 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -19,7 +19,7 @@ use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; use std::{sync::Arc, time::Duration}; use time::{OffsetDateTime, UtcOffset}; -use ui::{h_stack, prelude::*, v_stack, Avatar, Button, Icon, IconButton, IconName, Label}; +use ui::{h_flex, prelude::*, v_flex, Avatar, Button, Icon, IconButton, IconName, Label}; use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, @@ -251,13 +251,13 @@ impl NotificationPanel { .rounded_full() })) .child( - v_stack() + v_flex() .gap_1() .size_full() .overflow_hidden() .child(Label::new(text.clone())) .child( - h_stack() + h_flex() .child( Label::new(format_timestamp( timestamp, @@ -276,7 +276,7 @@ impl NotificationPanel { ))) } else if needs_response { Some( - h_stack() + h_flex() .flex_grow() .justify_end() .child(Button::new("decline", "Decline").on_click({ @@ -541,15 +541,15 @@ impl NotificationPanel { impl Render for NotificationPanel { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - v_stack() + v_flex() .size_full() .child( - h_stack() + h_flex() .justify_between() .px_2() .py_1() // Match the height of the tab bar so they line up. - .h(rems(ui::Tab::HEIGHT_IN_REMS)) + .h(rems(ui::Tab::CONTAINER_HEIGHT_IN_REMS)) .border_b_1() .border_color(cx.theme().colors().border) .child(Label::new("Notifications")) @@ -558,7 +558,7 @@ impl Render for NotificationPanel { .map(|this| { if self.client.user_id().is_none() { this.child( - v_stack() + v_flex() .gap_2() .p_4() .child( @@ -592,7 +592,7 @@ impl Render for NotificationPanel { ) } else if self.notification_list.item_count() == 0 { this.child( - v_stack().p_4().child( + v_flex().p_4().child( div().flex().w_full().items_center().child( Label::new("You have no notifications.") .color(Color::Muted) @@ -711,7 +711,7 @@ impl Render for NotificationToast { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let user = self.actor.clone(); - h_stack() + h_flex() .id("notification_panel_toast") .children(user.map(|user| Avatar::new(user.avatar_uri.clone()))) .child(Label::new(self.text.clone())) diff --git a/crates/collab_ui/src/notifications/collab_notification.rs b/crates/collab_ui/src/notifications/collab_notification.rs index fa0b0a1b14782b8bbe586348487228d75df743f7..8157bc1318dac52710732880d565e4914d01d8de 100644 --- a/crates/collab_ui/src/notifications/collab_notification.rs +++ b/crates/collab_ui/src/notifications/collab_notification.rs @@ -33,7 +33,7 @@ impl ParentElement for CollabNotification { impl RenderOnce for CollabNotification { fn render(self, cx: &mut WindowContext) -> impl IntoElement { - h_stack() + h_flex() .text_ui() .justify_between() .size_full() @@ -42,9 +42,9 @@ impl RenderOnce for CollabNotification { .p_2() .gap_2() .child(img(self.avatar_uri).w_12().h_12().rounded_full()) - .child(v_stack().overflow_hidden().children(self.children)) + .child(v_flex().overflow_hidden().children(self.children)) .child( - v_stack() + v_flex() .child(self.accept_button) .child(self.dismiss_button), ) diff --git a/crates/collab_ui/src/notifications/incoming_call_notification.rs b/crates/collab_ui/src/notifications/incoming_call_notification.rs index 93df9a4be5445bd67072affaaf2093e7fd2a32a1..f66194c52a0006c46c37cba2514e505c4ee5ec9b 100644 --- a/crates/collab_ui/src/notifications/incoming_call_notification.rs +++ b/crates/collab_ui/src/notifications/incoming_call_notification.rs @@ -137,7 +137,7 @@ impl Render for IncomingCallNotification { move |_, cx| state.respond(false, cx) }), ) - .child(v_stack().overflow_hidden().child(Label::new(format!( + .child(v_flex().overflow_hidden().child(Label::new(format!( "{} is sharing a project in Zed", self.state.call.calling_user.github_login )))), diff --git a/crates/collab_ui/src/notifications/project_shared_notification.rs b/crates/collab_ui/src/notifications/project_shared_notification.rs index 88fe540c397b65c8ddc3ad0230c47210b8bc0e7e..b8ceefcd765f4e1797bcf1584ac93594a1fffdaa 100644 --- a/crates/collab_ui/src/notifications/project_shared_notification.rs +++ b/crates/collab_ui/src/notifications/project_shared_notification.rs @@ -58,7 +58,7 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { } } - room::Event::Left => { + room::Event::Left { .. } => { for (_, windows) in notification_windows.drain() { for window in windows { window diff --git a/crates/collab_ui/src/notifications/stories/collab_notification.rs b/crates/collab_ui/src/notifications/stories/collab_notification.rs index c43cac46d21352ac8375e33cf530891385c36da0..e67ce817b69db6c4c6e0c24c2093b8cbb708e153 100644 --- a/crates/collab_ui/src/notifications/stories/collab_notification.rs +++ b/crates/collab_ui/src/notifications/stories/collab_notification.rs @@ -24,7 +24,7 @@ impl Render for CollabNotificationStory { Button::new("decline", "Decline"), ) .child( - v_stack() + v_flex() .overflow_hidden() .child(Label::new("maxdeviant is sharing a project in Zed")), ), diff --git a/crates/command_palette/Cargo.toml b/crates/command_palette/Cargo.toml index 39ed4fd95e183db7306bbcbfca616af9bc7a6b96..c762af7c487e1603b4c3b38f7887f38a56e99276 100644 --- a/crates/command_palette/Cargo.toml +++ b/crates/command_palette/Cargo.toml @@ -9,6 +9,7 @@ path = "src/command_palette.rs" doctest = false [dependencies] +client = { path = "../client" } collections = { path = "../collections" } editor = { path = "../editor" } fuzzy = { path = "../fuzzy" } @@ -16,9 +17,9 @@ gpui = { path = "../gpui" } picker = { path = "../picker" } project = { path = "../project" } settings = { path = "../settings" } +theme = { path = "../theme" } ui = { path = "../ui" } util = { path = "../util" } -theme = { path = "../theme" } workspace = { path = "../workspace" } zed_actions = { path = "../zed_actions" } anyhow.workspace = true diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index bbc2cd412305aff870f712187c4b7e13d916303d..c90e44886568b6def4e7c66cced834553cf5bb96 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -3,6 +3,7 @@ use std::{ sync::Arc, }; +use client::telemetry::Telemetry; use collections::{CommandPaletteFilter, HashMap}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ @@ -11,7 +12,7 @@ use gpui::{ }; use picker::{Picker, PickerDelegate}; -use ui::{h_stack, prelude::*, v_stack, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing}; +use ui::{h_flex, prelude::*, v_flex, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing}; use util::{ channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, ResultExt, @@ -39,11 +40,18 @@ impl CommandPalette { let Some(previous_focus_handle) = cx.focused() else { return; }; - workspace.toggle_modal(cx, move |cx| CommandPalette::new(previous_focus_handle, cx)); + let telemetry = workspace.client().telemetry().clone(); + workspace.toggle_modal(cx, move |cx| { + CommandPalette::new(previous_focus_handle, telemetry, cx) + }); }); } - fn new(previous_focus_handle: FocusHandle, cx: &mut ViewContext) -> Self { + fn new( + previous_focus_handle: FocusHandle, + telemetry: Arc, + cx: &mut ViewContext, + ) -> Self { let filter = cx.try_global::(); let commands = cx @@ -66,8 +74,12 @@ impl CommandPalette { }) .collect(); - let delegate = - CommandPaletteDelegate::new(cx.view().downgrade(), commands, previous_focus_handle); + let delegate = CommandPaletteDelegate::new( + cx.view().downgrade(), + commands, + telemetry, + previous_focus_handle, + ); let picker = cx.new_view(|cx| Picker::new(delegate, cx)); Self { picker } @@ -84,7 +96,7 @@ impl FocusableView for CommandPalette { impl Render for CommandPalette { fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - v_stack().w(rems(34.)).child(self.picker.clone()) + v_flex().w(rems(34.)).child(self.picker.clone()) } } @@ -103,6 +115,7 @@ pub struct CommandPaletteDelegate { commands: Vec, matches: Vec, selected_ix: usize, + telemetry: Arc, previous_focus_handle: FocusHandle, } @@ -130,6 +143,7 @@ impl CommandPaletteDelegate { fn new( command_palette: WeakView, commands: Vec, + telemetry: Arc, previous_focus_handle: FocusHandle, ) -> Self { Self { @@ -138,6 +152,7 @@ impl CommandPaletteDelegate { matches: vec![], commands, selected_ix: 0, + telemetry, previous_focus_handle, } } @@ -284,6 +299,10 @@ impl PickerDelegate for CommandPaletteDelegate { } let action_ix = self.matches[self.selected_ix].candidate_id; let command = self.commands.swap_remove(action_ix); + + self.telemetry + .report_action_event("command palette", command.name.clone()); + self.matches.clear(); self.commands.clear(); cx.update_global(|hit_counts: &mut HitCounts, _| { @@ -311,7 +330,7 @@ impl PickerDelegate for CommandPaletteDelegate { .spacing(ListItemSpacing::Sparse) .selected(selected) .child( - h_stack() + h_flex() .w_full() .justify_between() .child(HighlightedLabel::new( diff --git a/crates/copilot_ui/src/sign_in.rs b/crates/copilot_ui/src/sign_in.rs index ba6f54b634a0e2f9f14ce296423fc905d40bf744..f78a82699dc3c70925accc3e70a3242e9aad5061 100644 --- a/crates/copilot_ui/src/sign_in.rs +++ b/crates/copilot_ui/src/sign_in.rs @@ -57,7 +57,7 @@ impl CopilotCodeVerification { .read_from_clipboard() .map(|item| item.text() == &data.user_code) .unwrap_or(false); - h_stack() + h_flex() .w_full() .p_1() .border() @@ -69,7 +69,7 @@ impl CopilotCodeVerification { let user_code = data.user_code.clone(); move |_, cx| { cx.write_to_clipboard(ClipboardItem::new(user_code.clone())); - cx.notify(); + cx.refresh(); } }) .child(div().flex_1().child(Label::new(data.user_code.clone()))) @@ -90,7 +90,7 @@ impl CopilotCodeVerification { } else { "Connect to Github" }; - v_stack() + v_flex() .flex_1() .gap_2() .items_center() @@ -118,7 +118,7 @@ impl CopilotCodeVerification { ) } fn render_enabled_modal(cx: &mut ViewContext) -> impl Element { - v_stack() + v_flex() .gap_2() .child(Headline::new("Copilot Enabled!").size(HeadlineSize::Large)) .child(Label::new( @@ -132,7 +132,7 @@ impl CopilotCodeVerification { } fn render_unauthorized_modal() -> impl Element { - v_stack() + v_flex() .child(Headline::new("You must have an active GitHub Copilot subscription.").size(HeadlineSize::Large)) .child(Label::new( @@ -163,7 +163,7 @@ impl Render for CopilotCodeVerification { _ => div().into_any_element(), }; - v_stack() + v_flex() .id("copilot code verification") .elevation_3(cx) .w_96() diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 844a44c54f8bcf6eeae17ab3e629b4dee6e4a04b..ca701e626e7f02284e22f553b507ca55a09524a5 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -36,7 +36,7 @@ use std::{ }; use theme::ActiveTheme; pub use toolbar_controls::ToolbarControls; -use ui::{h_stack, prelude::*, Icon, IconName, Label}; +use ui::{h_flex, prelude::*, Icon, IconName, Label}; use util::TryFutureExt; use workspace::{ item::{BreadcrumbText, Item, ItemEvent, ItemHandle}, @@ -654,11 +654,11 @@ impl Item for ProjectDiagnosticsEditor { }) .into_any_element() } else { - h_stack() + h_flex() .gap_1() .when(self.summary.error_count > 0, |then| { then.child( - h_stack() + h_flex() .gap_1() .child(Icon::new(IconName::XCircle).color(Color::Error)) .child(Label::new(self.summary.error_count.to_string()).color( @@ -672,7 +672,7 @@ impl Item for ProjectDiagnosticsEditor { }) .when(self.summary.warning_count > 0, |then| { then.child( - h_stack() + h_flex() .gap_1() .child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning)) .child(Label::new(self.summary.warning_count.to_string()).color( @@ -688,6 +688,10 @@ impl Item for ProjectDiagnosticsEditor { } } + fn telemetry_event_text(&self) -> Option<&'static str> { + Some("project diagnostics") + } + fn for_each_project_item( &self, cx: &AppContext, @@ -796,7 +800,7 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { let message: SharedString = message.into(); Arc::new(move |cx| { let highlight_style: HighlightStyle = cx.theme().colors().text_accent.into(); - h_stack() + h_flex() .id("diagnostic header") .py_2() .pl_10() @@ -805,7 +809,7 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { .justify_between() .gap_2() .child( - h_stack() + h_flex() .gap_3() .map(|stack| { stack.child( @@ -824,7 +828,7 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { ) }) .child( - h_stack() + h_flex() .gap_1() .child( StyledText::new(message.clone()).with_highlights( @@ -844,7 +848,7 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { ), ) .child( - h_stack() + h_flex() .gap_1() .when_some(diagnostic.source.as_ref(), |stack, source| { stack.child( diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 035b84e1020048cd7c6d2cd107577b7c79786169..462718c0f345268131cd04867d2bff9f48a451a0 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -6,7 +6,7 @@ use gpui::{ }; use language::Diagnostic; use lsp::LanguageServerId; -use ui::{h_stack, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip}; +use ui::{h_flex, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip}; use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace}; use crate::{Deploy, ProjectDiagnosticsEditor}; @@ -23,14 +23,14 @@ pub struct DiagnosticIndicator { impl Render for DiagnosticIndicator { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) { - (0, 0) => h_stack().map(|this| { + (0, 0) => h_flex().map(|this| { this.child( Icon::new(IconName::Check) .size(IconSize::Small) .color(Color::Default), ) }), - (0, warning_count) => h_stack() + (0, warning_count) => h_flex() .gap_1() .child( Icon::new(IconName::ExclamationTriangle) @@ -38,7 +38,7 @@ impl Render for DiagnosticIndicator { .color(Color::Warning), ) .child(Label::new(warning_count.to_string()).size(LabelSize::Small)), - (error_count, 0) => h_stack() + (error_count, 0) => h_flex() .gap_1() .child( Icon::new(IconName::XCircle) @@ -46,7 +46,7 @@ impl Render for DiagnosticIndicator { .color(Color::Error), ) .child(Label::new(error_count.to_string()).size(LabelSize::Small)), - (error_count, warning_count) => h_stack() + (error_count, warning_count) => h_flex() .gap_1() .child( Icon::new(IconName::XCircle) @@ -64,7 +64,7 @@ impl Render for DiagnosticIndicator { let status = if !self.in_progress_checks.is_empty() { Some( - h_stack() + h_flex() .gap_2() .child(Icon::new(IconName::ArrowCircle).size(IconSize::Small)) .child( @@ -91,7 +91,7 @@ impl Render for DiagnosticIndicator { None }; - h_stack() + h_flex() .h(rems(1.375)) .gap_2() .child( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 7fe942f14561c8781b17c9ae66ea356190425422..30b0a73d37e093bcb456d4fa6cf8e0c2ff98d5ff 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -97,10 +97,13 @@ use std::{ pub use sum_tree::Bias; use sum_tree::TreeMap; use text::{OffsetUtf16, Rope}; -use theme::{ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, ThemeColors, ThemeSettings}; +use theme::{ + observe_buffer_font_size_adjustment, ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, + ThemeColors, ThemeSettings, +}; use ui::{ - h_stack, prelude::*, ButtonSize, ButtonStyle, IconButton, IconName, IconSize, ListItem, - Popover, Tooltip, + h_flex, prelude::*, ButtonSize, ButtonStyle, IconButton, IconName, IconSize, ListItem, Popover, + Tooltip, }; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::{searchable::SearchEvent, ItemNavHistory, Pane, SplitDirection, ViewId, Workspace}; @@ -604,6 +607,7 @@ pub struct Editor { gutter_width: Pixels, style: Option, editor_actions: Vec)>>, + show_copilot_suggestions: bool, } pub struct EditorSnapshot { @@ -1263,7 +1267,7 @@ impl CompletionsMenu { None } else { Some( - h_stack().ml_4().child( + h_flex().ml_4().child( Label::new(text.clone()) .size(LabelSize::Small) .color(Color::Muted), @@ -1289,7 +1293,7 @@ impl CompletionsMenu { ) .map(|task| task.detach_and_log_err(cx)); })) - .child(h_stack().overflow_hidden().child(completion_label)) + .child(h_flex().overflow_hidden().child(completion_label)) .end_slot::
(documentation_label), ) }) @@ -1804,12 +1808,14 @@ impl Editor { gutter_width: Default::default(), style: None, editor_actions: Default::default(), + show_copilot_suggestions: mode == EditorMode::Full, _subscriptions: vec![ cx.observe(&buffer, Self::on_buffer_changed), cx.subscribe(&buffer, Self::on_buffer_event), cx.observe(&display_map, Self::on_display_map_changed), cx.observe(&blink_manager, |_, _, cx| cx.notify()), cx.observe_global::(Self::settings_changed), + observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()), cx.observe_window_activation(|editor, cx| { let active = cx.is_window_active(); editor.blink_manager.update(cx, |blink_manager, cx| { @@ -1955,17 +1961,21 @@ impl Editor { } } - // pub fn language_at<'a, T: ToOffset>( - // &self, - // point: T, - // cx: &'a AppContext, - // ) -> Option> { - // self.buffer.read(cx).language_at(point, cx) - // } + pub fn language_at<'a, T: ToOffset>( + &self, + point: T, + cx: &'a AppContext, + ) -> Option> { + self.buffer.read(cx).language_at(point, cx) + } - // pub fn file_at<'a, T: ToOffset>(&self, point: T, cx: &'a AppContext) -> Option> { - // self.buffer.read(cx).read(cx).file_at(point).cloned() - // } + pub fn file_at<'a, T: ToOffset>( + &self, + point: T, + cx: &'a AppContext, + ) -> Option> { + self.buffer.read(cx).read(cx).file_at(point).cloned() + } pub fn active_excerpt( &self, @@ -1976,15 +1986,6 @@ impl Editor { .excerpt_containing(self.selections.newest_anchor().head(), cx) } - // pub fn style(&self, cx: &AppContext) -> EditorStyle { - // build_style( - // settings::get::(cx), - // self.get_field_editor_theme.as_deref(), - // self.override_text_style.as_deref(), - // cx, - // ) - // } - pub fn mode(&self) -> EditorMode { self.mode } @@ -2071,6 +2072,10 @@ impl Editor { self.read_only = read_only; } + pub fn set_show_copilot_suggestions(&mut self, show_copilot_suggestions: bool) { + self.show_copilot_suggestions = show_copilot_suggestions; + } + fn selections_did_change( &mut self, local: bool, @@ -3981,7 +3986,7 @@ impl Editor { cx: &mut ViewContext, ) -> Option<()> { let copilot = Copilot::global(cx)?; - if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() { + if !self.show_copilot_suggestions || !copilot.read(cx).status().is_authorized() { self.clear_copilot_suggestions(cx); return None; } @@ -4041,7 +4046,7 @@ impl Editor { cx: &mut ViewContext, ) -> Option<()> { let copilot = Copilot::global(cx)?; - if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() { + if !self.show_copilot_suggestions || !copilot.read(cx).status().is_authorized() { return None; } @@ -4166,7 +4171,8 @@ impl Editor { let file = snapshot.file_at(location); let language = snapshot.language_at(location); let settings = all_language_settings(file, cx); - settings.copilot_enabled(language, file.map(|f| f.path().as_ref())) + self.show_copilot_suggestions + && settings.copilot_enabled(language, file.map(|f| f.path().as_ref())) } fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool { @@ -4511,7 +4517,7 @@ impl Editor { } pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext) { - if self.move_to_next_snippet_tabstop(cx) { + if self.move_to_next_snippet_tabstop(cx) || self.read_only(cx) { return; } @@ -5443,6 +5449,10 @@ impl Editor { } pub fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { + if self.read_only(cx) { + return; + } + self.transact(cx, |this, cx| { if let Some(item) = cx.read_from_clipboard() { let clipboard_text = Cow::Borrowed(item.text()); @@ -5515,6 +5525,10 @@ impl Editor { } pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext) { + if self.read_only(cx) { + return; + } + if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) { if let Some((selections, _)) = self.selection_history.transaction(tx_id).cloned() { self.change_selections(None, cx, |s| { @@ -5529,6 +5543,10 @@ impl Editor { } pub fn redo(&mut self, _: &Redo, cx: &mut ViewContext) { + if self.read_only(cx) { + return; + } + if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.redo(cx)) { if let Some((_, Some(selections))) = self.selection_history.transaction(tx_id).cloned() { @@ -6453,42 +6471,79 @@ impl Editor { } self.select_next_state = Some(select_next_state); - } else if selections.len() == 1 { - let selection = selections.last_mut().unwrap(); - if selection.start == selection.end { - let word_range = movement::surrounding_word( - &display_map, - selection.start.to_display_point(&display_map), - ); - selection.start = word_range.start.to_offset(&display_map, Bias::Left); - selection.end = word_range.end.to_offset(&display_map, Bias::Left); - selection.goal = SelectionGoal::None; - selection.reversed = false; - - let query = buffer - .text_for_range(selection.start..selection.end) - .collect::(); - - let is_empty = query.is_empty(); - let select_state = SelectNextState { - query: AhoCorasick::new(&[query])?, - wordwise: true, - done: is_empty, - }; - select_next_match_ranges( - self, - selection.start..selection.end, - replace_newest, - autoscroll, - cx, - ); - self.select_next_state = Some(select_state); - } else { - let query = buffer - .text_for_range(selection.start..selection.end) - .collect::(); + } else { + let mut only_carets = true; + let mut same_text_selected = true; + let mut selected_text = None; + + let mut selections_iter = selections.iter().peekable(); + while let Some(selection) = selections_iter.next() { + if selection.start != selection.end { + only_carets = false; + } + + if same_text_selected { + if selected_text.is_none() { + selected_text = + Some(buffer.text_for_range(selection.range()).collect::()); + } + + if let Some(next_selection) = selections_iter.peek() { + if next_selection.range().len() == selection.range().len() { + let next_selected_text = buffer + .text_for_range(next_selection.range()) + .collect::(); + if Some(next_selected_text) != selected_text { + same_text_selected = false; + selected_text = None; + } + } else { + same_text_selected = false; + selected_text = None; + } + } + } + } + + if only_carets { + for selection in &mut selections { + let word_range = movement::surrounding_word( + &display_map, + selection.start.to_display_point(&display_map), + ); + selection.start = word_range.start.to_offset(&display_map, Bias::Left); + selection.end = word_range.end.to_offset(&display_map, Bias::Left); + selection.goal = SelectionGoal::None; + selection.reversed = false; + select_next_match_ranges( + self, + selection.start..selection.end, + replace_newest, + autoscroll, + cx, + ); + } + + if selections.len() == 1 { + let selection = selections + .last() + .expect("ensured that there's only one selection"); + let query = buffer + .text_for_range(selection.start..selection.end) + .collect::(); + let is_empty = query.is_empty(); + let select_state = SelectNextState { + query: AhoCorasick::new(&[query])?, + wordwise: true, + done: is_empty, + }; + self.select_next_state = Some(select_state); + } else { + self.select_next_state = None; + } + } else if let Some(selected_text) = selected_text { self.select_next_state = Some(SelectNextState { - query: AhoCorasick::new(&[query])?, + query: AhoCorasick::new(&[selected_text])?, wordwise: false, done: false, }); @@ -6592,39 +6647,81 @@ impl Editor { } self.select_prev_state = Some(select_prev_state); - } else if selections.len() == 1 { - let selection = selections.last_mut().unwrap(); - if selection.start == selection.end { - let word_range = movement::surrounding_word( - &display_map, - selection.start.to_display_point(&display_map), + } else { + let mut only_carets = true; + let mut same_text_selected = true; + let mut selected_text = None; + + let mut selections_iter = selections.iter().peekable(); + while let Some(selection) = selections_iter.next() { + if selection.start != selection.end { + only_carets = false; + } + + if same_text_selected { + if selected_text.is_none() { + selected_text = + Some(buffer.text_for_range(selection.range()).collect::()); + } + + if let Some(next_selection) = selections_iter.peek() { + if next_selection.range().len() == selection.range().len() { + let next_selected_text = buffer + .text_for_range(next_selection.range()) + .collect::(); + if Some(next_selected_text) != selected_text { + same_text_selected = false; + selected_text = None; + } + } else { + same_text_selected = false; + selected_text = None; + } + } + } + } + + if only_carets { + for selection in &mut selections { + let word_range = movement::surrounding_word( + &display_map, + selection.start.to_display_point(&display_map), + ); + selection.start = word_range.start.to_offset(&display_map, Bias::Left); + selection.end = word_range.end.to_offset(&display_map, Bias::Left); + selection.goal = SelectionGoal::None; + selection.reversed = false; + } + if selections.len() == 1 { + let selection = selections + .last() + .expect("ensured that there's only one selection"); + let query = buffer + .text_for_range(selection.start..selection.end) + .collect::(); + let is_empty = query.is_empty(); + let select_state = SelectNextState { + query: AhoCorasick::new(&[query.chars().rev().collect::()])?, + wordwise: true, + done: is_empty, + }; + self.select_prev_state = Some(select_state); + } else { + self.select_prev_state = None; + } + + self.unfold_ranges( + selections.iter().map(|s| s.range()).collect::>(), + false, + true, + cx, ); - selection.start = word_range.start.to_offset(&display_map, Bias::Left); - selection.end = word_range.end.to_offset(&display_map, Bias::Left); - selection.goal = SelectionGoal::None; - selection.reversed = false; - - let query = buffer - .text_for_range(selection.start..selection.end) - .collect::(); - let query = query.chars().rev().collect::(); - let select_state = SelectNextState { - query: AhoCorasick::new(&[query])?, - wordwise: true, - done: false, - }; - self.unfold_ranges([selection.start..selection.end], false, true, cx); self.change_selections(Some(Autoscroll::newest()), cx, |s| { s.select(selections); }); - self.select_prev_state = Some(select_state); - } else { - let query = buffer - .text_for_range(selection.start..selection.end) - .collect::(); - let query = query.chars().rev().collect::(); + } else if let Some(selected_text) = selected_text { self.select_prev_state = Some(SelectNextState { - query: AhoCorasick::new(&[query])?, + query: AhoCorasick::new(&[selected_text.chars().rev().collect::()])?, wordwise: false, done: false, }); @@ -8727,6 +8824,7 @@ impl Editor { )), cx, ); + cx.notify(); } pub fn set_searchable(&mut self, searchable: bool) { @@ -9733,7 +9831,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren let group_id: SharedString = cx.block_id.to_string().into(); // TODO: Nate: We should tint the background of the block with the severity color // We need to extend the theme before we can do this - h_stack() + h_flex() .id(cx.block_id) .group(group_id.clone()) .relative() diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 520c3714d3d529dbcd2df4d4cc4d750db2a7a53c..a6e3d19995c2126e57b698178e8d0cac1bc264c5 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -3821,62 +3821,137 @@ async fn test_select_next(cx: &mut gpui::TestAppContext) { } #[gpui::test] -async fn test_select_previous(cx: &mut gpui::TestAppContext) { +async fn test_select_next_with_multiple_carets(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); - { - // `Select previous` without a selection (selects wordwise) - let mut cx = EditorTestContext::new(cx).await; - cx.set_state("abc\nˇabc abc\ndefabc\nabc"); - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); + let mut cx = EditorTestContext::new(cx).await; + cx.set_state( + r#"let foo = 2; +lˇet foo = 2; +let fooˇ = 2; +let foo = 2; +let foo = ˇ2;"#, + ); - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); + cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) + .unwrap(); + cx.assert_editor_state( + r#"let foo = 2; +«letˇ» foo = 2; +let «fooˇ» = 2; +let foo = 2; +let foo = «2ˇ»;"#, + ); - cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); - cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); + // noop for multiple selections with different contents + cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) + .unwrap(); + cx.assert_editor_state( + r#"let foo = 2; +«letˇ» foo = 2; +let «fooˇ» = 2; +let foo = 2; +let foo = «2ˇ»;"#, + ); +} - cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); - cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); +#[gpui::test] +async fn test_select_previous_with_single_caret(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»"); + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("abc\nˇabc abc\ndefabc\nabc"); - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); - } - { - // `Select previous` with a selection - let mut cx = EditorTestContext::new(cx).await; - cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc"); + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); + cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); + cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); - cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); - cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); + cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); + cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); - cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); - cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»"); - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»"); + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»"); - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»"); - } + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»"); +} + +#[gpui::test] +async fn test_select_previous_with_multiple_carets(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + cx.set_state( + r#"let foo = 2; +lˇet foo = 2; +let fooˇ = 2; +let foo = 2; +let foo = ˇ2;"#, + ); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state( + r#"let foo = 2; +«letˇ» foo = 2; +let «fooˇ» = 2; +let foo = 2; +let foo = «2ˇ»;"#, + ); + + // noop for multiple selections with different contents + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state( + r#"let foo = 2; +«letˇ» foo = 2; +let «fooˇ» = 2; +let foo = 2; +let foo = «2ˇ»;"#, + ); +} + +#[gpui::test] +async fn test_select_previous_with_single_selection(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); + + cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); + + cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»"); } #[gpui::test] diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 4a648b37709fe62c5e2deef0432ccdd9dbf1d0f4..895df153406934f9d38dca0d25994baf6f69adb6 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -26,11 +26,11 @@ use git::diff::DiffHunkStatus; use gpui::{ div, fill, outline, overlay, point, px, quad, relative, size, transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Corners, - CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Hsla, InteractiveBounds, - InteractiveElement, IntoElement, ModifiersChangedEvent, MouseButton, MouseDownEvent, - MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, - SharedString, Size, StackingOrder, StatefulInteractiveElement, Style, Styled, TextRun, - TextStyle, View, ViewContext, WindowContext, + CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity, Hsla, + InteractiveBounds, InteractiveElement, IntoElement, ModifiersChangedEvent, MouseButton, + MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, ScrollDelta, + ScrollWheelEvent, ShapedLine, SharedString, Size, StackingOrder, StatefulInteractiveElement, + Style, Styled, TextRun, TextStyle, View, ViewContext, WindowContext, }; use itertools::Itertools; use language::language_settings::ShowWhitespaceSetting; @@ -53,7 +53,7 @@ use std::{ use sum_tree::Bias; use theme::{ActiveTheme, PlayerColor}; use ui::prelude::*; -use ui::{h_stack, ButtonLike, ButtonStyle, IconButton, Tooltip}; +use ui::{h_flex, ButtonLike, ButtonStyle, IconButton, Tooltip}; use util::ResultExt; use workspace::item::Item; @@ -388,7 +388,9 @@ impl EditorElement { let mut click_count = event.click_count; let modifiers = event.modifiers; - if gutter_bounds.contains(&event.position) { + if cx.default_prevented() { + return; + } else if gutter_bounds.contains(&event.position) { click_count = 3; // Simulate triple-click when clicking the gutter to select lines } else if !text_bounds.contains(&event.position) { return; @@ -2269,11 +2271,9 @@ impl EditorElement { .map_or(range.context.start, |primary| primary.start); let jump_position = language::ToPoint::to_point(&jump_anchor, buffer); - let jump_handler = cx.listener_for(&self.editor, move |editor, _, cx| { + cx.listener_for(&self.editor, move |editor, _, cx| { editor.jump(jump_path.clone(), jump_position, jump_anchor, cx); - }); - - jump_handler + }) }); let element = if *starts_new_buffer { @@ -2293,7 +2293,7 @@ impl EditorElement { .size_full() .p_1p5() .child( - h_stack() + h_flex() .id("path header block") .py_1p5() .pl_3() @@ -2306,8 +2306,8 @@ impl EditorElement { .justify_between() .hover(|style| style.bg(cx.theme().colors().element_hover)) .child( - h_stack().gap_3().child( - h_stack() + h_flex().gap_3().child( + h_flex() .gap_2() .child( filename @@ -2339,12 +2339,12 @@ impl EditorElement { }), ) } else { - h_stack() + h_flex() .id(("collapsed context", block_id)) .size_full() .gap(gutter_padding) .child( - h_stack() + h_flex() .justify_end() .flex_none() .w(gutter_width - gutter_padding) @@ -2353,34 +2353,25 @@ impl EditorElement { .text_color(cx.theme().colors().editor_line_number) .child("..."), ) - .map(|this| { - if let Some(jump_handler) = jump_handler { - this.child( - ButtonLike::new("jump to collapsed context") - .style(ButtonStyle::Transparent) - .full_width() - .on_click(jump_handler) - .tooltip(|cx| { - Tooltip::for_action( - "Jump to Buffer", - &OpenExcerpts, - cx, - ) - }) - .child( - div() - .h_px() - .w_full() - .bg(cx.theme().colors().border_variant) - .group_hover("", |style| { - style.bg(cx.theme().colors().border) - }), - ), + .child( + ButtonLike::new("jump to collapsed context") + .style(ButtonStyle::Transparent) + .full_width() + .child( + div() + .h_px() + .w_full() + .bg(cx.theme().colors().border_variant) + .group_hover("", |style| { + style.bg(cx.theme().colors().border) + }), ) - } else { - this.child(div().size_full().bg(gpui::green())) - } - }) + .when_some(jump_handler, |this, jump_handler| { + this.on_click(jump_handler).tooltip(|cx| { + Tooltip::for_action("Jump to Buffer", &OpenExcerpts, cx) + }) + }), + ) }; element.into_any() } @@ -2812,44 +2803,49 @@ impl Element for EditorElement { _element_state: Option, cx: &mut gpui::WindowContext, ) -> (gpui::LayoutId, Self::State) { - self.editor.update(cx, |editor, cx| { - editor.set_style(self.style.clone(), cx); - - let layout_id = match editor.mode { - EditorMode::SingleLine => { - let rem_size = cx.rem_size(); - let mut style = Style::default(); - style.size.width = relative(1.).into(); - style.size.height = self.style.text.line_height_in_pixels(rem_size).into(); - cx.request_layout(&style, None) - } - EditorMode::AutoHeight { max_lines } => { - let editor_handle = cx.view().clone(); - let max_line_number_width = - self.max_line_number_width(&editor.snapshot(cx), cx); - cx.request_measured_layout(Style::default(), move |known_dimensions, _, cx| { - editor_handle - .update(cx, |editor, cx| { - compute_auto_height_layout( - editor, - max_lines, - max_line_number_width, - known_dimensions, - cx, - ) - }) - .unwrap_or_default() - }) - } - EditorMode::Full => { - let mut style = Style::default(); - style.size.width = relative(1.).into(); - style.size.height = relative(1.).into(); - cx.request_layout(&style, None) - } - }; + cx.with_view_id(self.editor.entity_id(), |cx| { + self.editor.update(cx, |editor, cx| { + editor.set_style(self.style.clone(), cx); + + let layout_id = match editor.mode { + EditorMode::SingleLine => { + let rem_size = cx.rem_size(); + let mut style = Style::default(); + style.size.width = relative(1.).into(); + style.size.height = self.style.text.line_height_in_pixels(rem_size).into(); + cx.request_layout(&style, None) + } + EditorMode::AutoHeight { max_lines } => { + let editor_handle = cx.view().clone(); + let max_line_number_width = + self.max_line_number_width(&editor.snapshot(cx), cx); + cx.request_measured_layout( + Style::default(), + move |known_dimensions, _, cx| { + editor_handle + .update(cx, |editor, cx| { + compute_auto_height_layout( + editor, + max_lines, + max_line_number_width, + known_dimensions, + cx, + ) + }) + .unwrap_or_default() + }, + ) + } + EditorMode::Full => { + let mut style = Style::default(); + style.size.width = relative(1.).into(); + style.size.height = relative(1.).into(); + cx.request_layout(&style, None) + } + }; - (layout_id, ()) + (layout_id, ()) + }) }) } @@ -2861,65 +2857,67 @@ impl Element for EditorElement { ) { let editor = self.editor.clone(); - cx.with_text_style( - Some(gpui::TextStyleRefinement { - font_size: Some(self.style.text.font_size), - ..Default::default() - }), - |cx| { - let mut layout = self.compute_layout(bounds, cx); - let gutter_bounds = Bounds { - origin: bounds.origin, - size: layout.gutter_size, - }; - let text_bounds = Bounds { - origin: gutter_bounds.upper_right(), - size: layout.text_size, - }; + cx.paint_view(self.editor.entity_id(), |cx| { + cx.with_text_style( + Some(gpui::TextStyleRefinement { + font_size: Some(self.style.text.font_size), + ..Default::default() + }), + |cx| { + let mut layout = self.compute_layout(bounds, cx); + let gutter_bounds = Bounds { + origin: bounds.origin, + size: layout.gutter_size, + }; + let text_bounds = Bounds { + origin: gutter_bounds.upper_right(), + size: layout.text_size, + }; - let focus_handle = editor.focus_handle(cx); - let key_context = self.editor.read(cx).key_context(cx); - cx.with_key_dispatch(Some(key_context), Some(focus_handle.clone()), |_, cx| { - self.register_actions(cx); - self.register_key_listeners(cx); + let focus_handle = editor.focus_handle(cx); + let key_context = self.editor.read(cx).key_context(cx); + cx.with_key_dispatch(Some(key_context), Some(focus_handle.clone()), |_, cx| { + self.register_actions(cx); + self.register_key_listeners(cx); - cx.with_content_mask(Some(ContentMask { bounds }), |cx| { - let input_handler = - ElementInputHandler::new(bounds, self.editor.clone(), cx); - cx.handle_input(&focus_handle, input_handler); + cx.with_content_mask(Some(ContentMask { bounds }), |cx| { + let input_handler = + ElementInputHandler::new(bounds, self.editor.clone(), cx); + cx.handle_input(&focus_handle, input_handler); - self.paint_background(gutter_bounds, text_bounds, &layout, cx); - if layout.gutter_size.width > Pixels::ZERO { - self.paint_gutter(gutter_bounds, &mut layout, cx); - } - self.paint_text(text_bounds, &mut layout, cx); + self.paint_background(gutter_bounds, text_bounds, &layout, cx); + if layout.gutter_size.width > Pixels::ZERO { + self.paint_gutter(gutter_bounds, &mut layout, cx); + } + self.paint_text(text_bounds, &mut layout, cx); - cx.with_z_index(0, |cx| { - self.paint_mouse_listeners( - bounds, - gutter_bounds, - text_bounds, - &layout, - cx, - ); - }); - if !layout.blocks.is_empty() { cx.with_z_index(0, |cx| { - cx.with_element_id(Some("editor_blocks"), |cx| { - self.paint_blocks(bounds, &mut layout, cx); - }); - }) - } + self.paint_mouse_listeners( + bounds, + gutter_bounds, + text_bounds, + &layout, + cx, + ); + }); + if !layout.blocks.is_empty() { + cx.with_z_index(0, |cx| { + cx.with_element_id(Some("editor_blocks"), |cx| { + self.paint_blocks(bounds, &mut layout, cx); + }); + }) + } - cx.with_z_index(1, |cx| { - self.paint_overlays(text_bounds, &mut layout, cx); - }); + cx.with_z_index(1, |cx| { + self.paint_overlays(text_bounds, &mut layout, cx); + }); - cx.with_z_index(2, |cx| self.paint_scrollbar(bounds, &mut layout, cx)); - }); - }) - }, - ); + cx.with_z_index(2, |cx| self.paint_scrollbar(bounds, &mut layout, cx)); + }); + }) + }, + ) + }) } } @@ -3415,14 +3413,16 @@ mod tests { }) .unwrap(); let state = cx - .update_window(window.into(), |_, cx| { - element.compute_layout( - Bounds { - origin: point(px(500.), px(500.)), - size: size(px(500.), px(500.)), - }, - cx, - ) + .update_window(window.into(), |view, cx| { + cx.with_view_id(view.entity_id(), |cx| { + element.compute_layout( + Bounds { + origin: point(px(500.), px(500.)), + size: size(px(500.), px(500.)), + }, + cx, + ) + }) }) .unwrap(); @@ -3507,14 +3507,16 @@ mod tests { }); let state = cx - .update_window(window.into(), |_, cx| { - element.compute_layout( - Bounds { - origin: point(px(500.), px(500.)), - size: size(px(500.), px(500.)), - }, - cx, - ) + .update_window(window.into(), |view, cx| { + cx.with_view_id(view.entity_id(), |cx| { + element.compute_layout( + Bounds { + origin: point(px(500.), px(500.)), + size: size(px(500.), px(500.)), + }, + cx, + ) + }) }) .unwrap(); assert_eq!(state.selections.len(), 1); @@ -3569,14 +3571,16 @@ mod tests { let mut element = EditorElement::new(&editor, style); let state = cx - .update_window(window.into(), |_, cx| { - element.compute_layout( - Bounds { - origin: point(px(500.), px(500.)), - size: size(px(500.), px(500.)), - }, - cx, - ) + .update_window(window.into(), |view, cx| { + cx.with_view_id(view.entity_id(), |cx| { + element.compute_layout( + Bounds { + origin: point(px(500.), px(500.)), + size: size(px(500.), px(500.)), + }, + cx, + ) + }) }) .unwrap(); let size = state.position_map.size; @@ -3593,10 +3597,8 @@ mod tests { // Don't panic. let bounds = Bounds::::new(Default::default(), size); - cx.update_window(window.into(), |_, cx| { - element.paint(bounds, &mut (), cx); - }) - .unwrap() + cx.update_window(window.into(), |_, cx| element.paint(bounds, &mut (), cx)) + .unwrap() } #[gpui::test] diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index a8583d48afae6abfee508026a65d109f78aafc95..36a48b293788ab22f509f941c7b2a66591614d9a 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -32,7 +32,7 @@ use std::{ }; use text::Selection; use theme::Theme; -use ui::{h_stack, prelude::*, Label}; +use ui::{h_flex, prelude::*, Label}; use util::{paths::PathExt, paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt}; use workspace::{ item::{BreadcrumbText, FollowEvent, FollowableItemHandle}, @@ -578,6 +578,10 @@ impl Item for Editor { Some(file_path.into()) } + fn telemetry_event_text(&self) -> Option<&'static str> { + None + } + fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option { let path = path_for_buffer(&self.buffer, detail, true, cx)?; Some(path.to_string_lossy().to_string().into()) @@ -619,7 +623,7 @@ impl Item for Editor { Some(util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN)) }); - h_stack() + h_flex() .gap_2() .child(Label::new(self.title(cx).to_string()).color(label_color)) .when_some(description, |this, description| { diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs index ba70739942c429e6b5eb11139a395b98db38475a..2a5ac568b79bb9df45489bf5e6b37f37ffcba25b 100644 --- a/crates/editor/src/scroll/autoscroll.rs +++ b/crates/editor/src/scroll/autoscroll.rs @@ -5,7 +5,7 @@ use language::Point; use crate::{display_map::ToDisplayPoint, Editor, EditorMode, LineWithInvisibles}; -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Clone, Copy)] pub enum Autoscroll { Next, Strategy(AutoscrollStrategy), @@ -25,7 +25,7 @@ impl Autoscroll { } } -#[derive(PartialEq, Eq, Default)] +#[derive(PartialEq, Eq, Default, Clone, Copy)] pub enum AutoscrollStrategy { Fit, Newest, diff --git a/crates/feedback/src/feedback_modal.rs b/crates/feedback/src/feedback_modal.rs index 2444a8e94850f8128aafb0948cb356eee81f1086..80722580b7afe9c37de4e1fb7edde97f3515db76 100644 --- a/crates/feedback/src/feedback_modal.rs +++ b/crates/feedback/src/feedback_modal.rs @@ -186,6 +186,7 @@ impl FeedbackModal { cx, ); editor.set_show_gutter(false, cx); + editor.set_show_copilot_suggestions(false); editor.set_vertical_scroll_margin(5, cx); editor }); @@ -421,7 +422,7 @@ impl Render for FeedbackModal { let open_community_repo = cx.listener(|_, _, cx| cx.dispatch_action(Box::new(OpenZedCommunityRepo))); - v_stack() + v_flex() .elevation_3(cx) .key_context("GiveFeedback") .on_action(cx.listener(Self::cancel)) @@ -460,10 +461,10 @@ impl Render for FeedbackModal { .child(self.feedback_editor.clone()), ) .child( - v_stack() + v_flex() .gap_1() .child( - h_stack() + h_flex() .bg(cx.theme().colors().editor_background) .p_2() .border() @@ -482,7 +483,7 @@ impl Render for FeedbackModal { ), ) .child( - h_stack() + h_flex() .justify_between() .gap_1() .child( @@ -494,7 +495,7 @@ impl Render for FeedbackModal { .on_click(open_community_repo), ) .child( - h_stack() + h_flex() .gap_1() .child( Button::new("cancel_feedback", "Cancel") diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index ed3e57ba28a0bfbebbcac1f8aff3482c953be276..022860ea4718dbf39717cc86c631721d5e86621b 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -119,7 +119,7 @@ impl FocusableView for FileFinder { } impl Render for FileFinder { fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - v_stack().w(rems(34.)).child(self.picker.clone()) + v_flex().w(rems(34.)).child(self.picker.clone()) } } @@ -786,7 +786,7 @@ impl PickerDelegate for FileFinderDelegate { .inset(true) .selected(selected) .child( - v_stack() + v_flex() .child(HighlightedLabel::new(file_name, file_name_positions)) .child(HighlightedLabel::new(full_path, full_path_positions)), ), diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index ec58cbdc60556742414cc3692ef79f0658fbc750..b7e3f27fac257bab08ac1e85401cf7cb5a383dfc 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -5,7 +5,7 @@ use gpui::{ }; use text::{Bias, Point}; use theme::ActiveTheme; -use ui::{h_stack, prelude::*, v_stack, Label}; +use ui::{h_flex, prelude::*, v_flex, Label}; use util::paths::FILE_ROW_COLUMN_DELIMITER; use workspace::ModalView; @@ -160,12 +160,12 @@ impl Render for GoToLine { .on_action(cx.listener(Self::confirm)) .w_96() .child( - v_stack() + v_flex() .px_1() .pt_0p5() .gap_px() .child( - v_stack() + v_flex() .py_0p5() .px_1() .child(div().px_1().py_0p5().child(self.line_editor.clone())), @@ -177,7 +177,7 @@ impl Render for GoToLine { .bg(cx.theme().colors().element_background), ) .child( - h_stack() + h_flex() .justify_between() .px_2() .py_1() diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 6ea3524fcc4eb2a60ae2411f7eed6a2ef05a17a8..ee7549287378f537de71b08abf59bc6af2ab4a98 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -78,7 +78,7 @@ cocoa = "0.24" core-foundation = { version = "0.9.3", features = ["with-uuid"] } core-graphics = "0.22.3" core-text = "19.2" -font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18" } +font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "d97147f" } foreign-types = "0.3" log.workspace = true metal = "0.21.0" diff --git a/crates/gpui/build.rs b/crates/gpui/build.rs index 24e493cb812d6e7478b266cc621eea7cbc77b051..44228b2e75229d36907eabcc82409e1f3eb79cb7 100644 --- a/crates/gpui/build.rs +++ b/crates/gpui/build.rs @@ -70,13 +70,23 @@ fn generate_shader_bindings() -> PathBuf { ]); config.no_includes = true; config.enumeration.prefix_with_name = true; - cbindgen::Builder::new() - .with_src(crate_dir.join("src/scene.rs")) - .with_src(crate_dir.join("src/geometry.rs")) - .with_src(crate_dir.join("src/color.rs")) - .with_src(crate_dir.join("src/window.rs")) - .with_src(crate_dir.join("src/platform.rs")) - .with_src(crate_dir.join("src/platform/mac/metal_renderer.rs")) + + let mut builder = cbindgen::Builder::new(); + + let src_paths = [ + crate_dir.join("src/scene.rs"), + crate_dir.join("src/geometry.rs"), + crate_dir.join("src/color.rs"), + crate_dir.join("src/window.rs"), + crate_dir.join("src/platform.rs"), + crate_dir.join("src/platform/mac/metal_renderer.rs"), + ]; + for src_path in src_paths { + println!("cargo:rerun-if-changed={}", src_path.display()); + builder = builder.with_src(src_path); + } + + builder .with_config(config) .generate() .expect("Unable to generate bindings") diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 108ad28d24a16191b6a419d09b9d56bf212c4050..41519f0ae4d3623a5b3e57c06a265c5f0a754b23 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -196,7 +196,6 @@ pub struct AppContext { pending_updates: usize, pub(crate) actions: Rc, pub(crate) active_drag: Option, - pub(crate) active_tooltip: Option, pub(crate) next_frame_callbacks: FxHashMap>, pub(crate) frame_consumers: FxHashMap>, pub(crate) background_executor: BackgroundExecutor, @@ -258,7 +257,6 @@ impl AppContext { flushing_effects: false, pending_updates: 0, active_drag: None, - active_tooltip: None, next_frame_callbacks: FxHashMap::default(), frame_consumers: FxHashMap::default(), background_executor: executor, @@ -845,6 +843,7 @@ impl AppContext { /// Remove the global of the given type from the app context. Does not notify global observers. pub fn remove_global(&mut self) -> G { let global_type = TypeId::of::(); + self.push_effect(Effect::NotifyGlobalObservers { global_type }); *self .globals_by_type .remove(&global_type) @@ -1268,8 +1267,10 @@ pub struct AnyDrag { pub cursor_offset: Point, } +/// Contains state associated with a tooltip. You'll only need this struct if you're implementing +/// tooltip behavior on a custom element. Otherwise, use [Div::tooltip]. #[derive(Clone)] -pub(crate) struct AnyTooltip { +pub struct AnyTooltip { pub view: AnyView, pub cursor_offset: Point, } diff --git a/crates/gpui/src/app/entity_map.rs b/crates/gpui/src/app/entity_map.rs index 0b213b20f769975e22761f8294251051e39e2753..1e593caf98a34f64939b6253fbb1eccd0244bfb3 100644 --- a/crates/gpui/src/app/entity_map.rs +++ b/crates/gpui/src/app/entity_map.rs @@ -2,7 +2,7 @@ use crate::{seal::Sealed, AppContext, Context, Entity, ModelContext}; use anyhow::{anyhow, Result}; use derive_more::{Deref, DerefMut}; use parking_lot::{RwLock, RwLockUpgradableReadGuard}; -use slotmap::{SecondaryMap, SlotMap}; +use slotmap::{KeyData, SecondaryMap, SlotMap}; use std::{ any::{type_name, Any, TypeId}, fmt::{self, Display}, @@ -24,6 +24,12 @@ slotmap::new_key_type! { pub struct EntityId; } +impl From for EntityId { + fn from(value: u64) -> Self { + Self(KeyData::from_ffi(value)) + } +} + impl EntityId { pub fn as_u64(self) -> u64 { self.0.as_ffi() diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index bc764e564c3340957f947ef669243be0a7d27e4d..23fcc25f6aeed436309a3670393d4cbca10536e6 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -355,16 +355,6 @@ impl Hsla { } } -// impl From for Rgba { -// fn from(value: Hsla) -> Self { -// let h = value.h; -// let s = value.s; -// let l = value.l; - -// let c = (1 - |2L - 1|) X s -// } -// } - impl From for Hsla { fn from(color: Rgba) -> Self { let r = color.r; diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 627a2ac339d631c666926d4fc8e1354396cb36f7..74000da0512ffac35e4cf87c122bdecf08d67aed 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -978,12 +978,31 @@ impl Interactivity { f: impl FnOnce(&Style, Point, &mut WindowContext), ) { let style = self.compute_style(Some(bounds), element_state, cx); + let z_index = style.z_index.unwrap_or(0); + + let paint_hover_group_handler = |cx: &mut WindowContext| { + let hover_group_bounds = self + .group_hover_style + .as_ref() + .and_then(|group_hover| GroupBounds::get(&group_hover.group, cx)); + + if let Some(group_bounds) = hover_group_bounds { + let hovered = group_bounds.contains(&cx.mouse_position()); + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { + if phase == DispatchPhase::Capture + && group_bounds.contains(&event.position) != hovered + { + cx.refresh(); + } + }); + } + }; if style.visibility == Visibility::Hidden { + cx.with_z_index(z_index, |cx| paint_hover_group_handler(cx)); return; } - let z_index = style.z_index.unwrap_or(0); cx.with_z_index(z_index, |cx| { style.paint(bounds, cx, |cx| { cx.with_text_style(style.text_style().cloned(), |cx| { @@ -1027,7 +1046,7 @@ impl Interactivity { if e.modifiers.command != command_held && text_bounds.contains(&cx.mouse_position()) { - cx.notify(); + cx.refresh(); } } }); @@ -1038,7 +1057,7 @@ impl Interactivity { if phase == DispatchPhase::Capture && bounds.contains(&event.position) != hovered { - cx.notify(); + cx.refresh(); } }, ); @@ -1166,21 +1185,7 @@ impl Interactivity { }) } - let hover_group_bounds = self - .group_hover_style - .as_ref() - .and_then(|group_hover| GroupBounds::get(&group_hover.group, cx)); - - if let Some(group_bounds) = hover_group_bounds { - let hovered = group_bounds.contains(&cx.mouse_position()); - cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { - if phase == DispatchPhase::Capture - && group_bounds.contains(&event.position) != hovered - { - cx.notify(); - } - }); - } + paint_hover_group_handler(cx); if self.hover_style.is_some() || self.base_style.mouse_cursor.is_some() @@ -1192,7 +1197,7 @@ impl Interactivity { if phase == DispatchPhase::Capture && bounds.contains(&event.position) != hovered { - cx.notify(); + cx.refresh(); } }); } @@ -1226,7 +1231,7 @@ impl Interactivity { if can_drop { listener(drag.value.as_ref(), cx); - cx.notify(); + cx.refresh(); cx.stop_propagation(); } } @@ -1257,7 +1262,7 @@ impl Interactivity { && interactive_bounds.visibly_contains(&event.position, cx) { *pending_mouse_down.borrow_mut() = Some(event.clone()); - cx.notify(); + cx.refresh(); } } }); @@ -1288,7 +1293,7 @@ impl Interactivity { cursor_offset, }); pending_mouse_down.take(); - cx.notify(); + cx.refresh(); cx.stop_propagation(); } } @@ -1308,7 +1313,7 @@ impl Interactivity { pending_mouse_down.borrow_mut(); if pending_mouse_down.is_some() { captured_mouse_down = pending_mouse_down.take(); - cx.notify(); + cx.refresh(); } } // Fire click handlers during the bubble phase. @@ -1402,7 +1407,7 @@ impl Interactivity { _task: None, }, ); - cx.notify(); + cx.refresh(); }) .ok(); } @@ -1428,8 +1433,8 @@ impl Interactivity { .borrow() .as_ref() { - if active_tooltip.tooltip.is_some() { - cx.active_tooltip = active_tooltip.tooltip.clone() + if let Some(tooltip) = active_tooltip.tooltip.clone() { + cx.set_tooltip(tooltip); } } } @@ -1442,7 +1447,7 @@ impl Interactivity { cx.on_mouse_event(move |_: &MouseUpEvent, phase, cx| { if phase == DispatchPhase::Capture { *active_state.borrow_mut() = ElementClickedState::default(); - cx.notify(); + cx.refresh(); } }); } else { @@ -1460,7 +1465,7 @@ impl Interactivity { if group || element { *active_state.borrow_mut() = ElementClickedState { group, element }; - cx.notify(); + cx.refresh(); } } }); @@ -1520,7 +1525,7 @@ impl Interactivity { } if *scroll_offset != old_scroll_offset { - cx.notify(); + cx.refresh(); cx.stop_propagation(); } } diff --git a/crates/gpui/src/elements/img.rs b/crates/gpui/src/elements/img.rs index 71a51351fdb24ccf4b275b42e2541a87569014dd..5a656db9fbe8a104ee8c92d46d152cd146dbadfe 100644 --- a/crates/gpui/src/elements/img.rs +++ b/crates/gpui/src/elements/img.rs @@ -109,7 +109,7 @@ impl Element for Img { } else { cx.spawn(|mut cx| async move { if image_future.await.ok().is_some() { - cx.on_next_frame(|cx| cx.notify()); + cx.on_next_frame(|cx| cx.refresh()); } }) .detach(); diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 2a47a16741cf67c0cefb8a094d2f9e506cacbdf4..2c076c8bdcdcb77fcc477f82dfba4f04a29bc2f2 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -43,6 +43,7 @@ pub enum ListAlignment { pub struct ListScrollEvent { pub visible_range: Range, pub count: usize, + pub is_scrolled: bool, } #[derive(Clone)] @@ -253,12 +254,13 @@ impl StateInner { &ListScrollEvent { visible_range, count: self.items.summary().count, + is_scrolled: self.logical_scroll_top.is_some(), }, cx, ); } - cx.notify(); + cx.refresh(); } fn logical_scroll_top(&self) -> ListOffset { diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 4e5c6721472398233a457573a56d27d43881c071..f72b7c6fa9111c4735fe95241ab9b63d25e39a37 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -392,7 +392,7 @@ impl Element for InteractiveText { } mouse_down.take(); - cx.notify(); + cx.refresh(); } }); } else { @@ -402,7 +402,7 @@ impl Element for InteractiveText { text_state.index_for_position(bounds, event.position) { mouse_down.set(Some(mouse_down_index)); - cx.notify(); + cx.refresh(); } } }); diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index a50de8c344247c705d24c8d9a0e94c7c799278bb..89e47994a34b76d4310e3d8afe0c15be411608be 100644 --- a/crates/gpui/src/geometry.rs +++ b/crates/gpui/src/geometry.rs @@ -2272,7 +2272,7 @@ impl From for GlobalPixels { /// For example, if the root element's font-size is `16px`, then `1rem` equals `16px`. A length of `2rems` would then be `32px`. /// /// [set_rem_size]: crate::WindowContext::set_rem_size -#[derive(Clone, Copy, Default, Add, Sub, Mul, Div, Neg)] +#[derive(Clone, Copy, Default, Add, Sub, Mul, Div, Neg, PartialEq)] pub struct Rems(pub f32); impl Mul for Rems { @@ -2295,7 +2295,7 @@ impl Debug for Rems { /// affected by the current font size, or a number of rems, which is relative to the font size of /// the root element. It is used for specifying dimensions that are either independent of or /// related to the typographic scale. -#[derive(Clone, Copy, Debug, Neg)] +#[derive(Clone, Copy, Debug, Neg, PartialEq)] pub enum AbsoluteLength { /// A length in pixels. Pixels(Pixels), @@ -2366,7 +2366,7 @@ impl Default for AbsoluteLength { /// This enum represents lengths that have a specific value, as opposed to lengths that are automatically /// determined by the context. It includes absolute lengths in pixels or rems, and relative lengths as a /// fraction of the parent's size. -#[derive(Clone, Copy, Neg)] +#[derive(Clone, Copy, Neg, PartialEq)] pub enum DefiniteLength { /// An absolute length specified in pixels or rems. Absolute(AbsoluteLength), diff --git a/crates/gpui/src/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index cc4af6d7e30d4bcc546ba264d1999cdbf5bec26c..85a67168e5e1d89588b6d58088a8f381a195e0eb 100644 --- a/crates/gpui/src/key_dispatch.rs +++ b/crates/gpui/src/key_dispatch.rs @@ -1,12 +1,13 @@ use crate::{ - arena::ArenaRef, Action, ActionRegistry, DispatchPhase, FocusId, KeyBinding, KeyContext, - KeyMatch, Keymap, Keystroke, KeystrokeMatcher, WindowContext, + Action, ActionRegistry, DispatchPhase, EntityId, FocusId, KeyBinding, KeyContext, KeyMatch, + Keymap, Keystroke, KeystrokeMatcher, WindowContext, }; -use collections::HashMap; +use collections::FxHashMap; use parking_lot::Mutex; -use smallvec::SmallVec; +use smallvec::{smallvec, SmallVec}; use std::{ any::{Any, TypeId}, + mem, rc::Rc, sync::Arc, }; @@ -18,8 +19,9 @@ pub(crate) struct DispatchTree { node_stack: Vec, pub(crate) context_stack: Vec, nodes: Vec, - focusable_node_ids: HashMap, - keystroke_matchers: HashMap, KeystrokeMatcher>, + focusable_node_ids: FxHashMap, + view_node_ids: FxHashMap, + keystroke_matchers: FxHashMap, KeystrokeMatcher>, keymap: Arc>, action_registry: Rc, } @@ -30,15 +32,16 @@ pub(crate) struct DispatchNode { pub action_listeners: Vec, pub context: Option, focus_id: Option, + view_id: Option, parent: Option, } -type KeyListener = ArenaRef; +type KeyListener = Rc; #[derive(Clone)] pub(crate) struct DispatchActionListener { pub(crate) action_type: TypeId, - pub(crate) listener: ArenaRef, + pub(crate) listener: Rc, } impl DispatchTree { @@ -47,8 +50,9 @@ impl DispatchTree { node_stack: Vec::new(), context_stack: Vec::new(), nodes: Vec::new(), - focusable_node_ids: HashMap::default(), - keystroke_matchers: HashMap::default(), + focusable_node_ids: FxHashMap::default(), + view_node_ids: FxHashMap::default(), + keystroke_matchers: FxHashMap::default(), keymap, action_registry, } @@ -56,31 +60,101 @@ impl DispatchTree { pub fn clear(&mut self) { self.node_stack.clear(); - self.nodes.clear(); self.context_stack.clear(); + self.nodes.clear(); self.focusable_node_ids.clear(); + self.view_node_ids.clear(); self.keystroke_matchers.clear(); } - pub fn push_node(&mut self, context: Option) { + pub fn push_node( + &mut self, + context: Option, + focus_id: Option, + view_id: Option, + ) { let parent = self.node_stack.last().copied(); let node_id = DispatchNodeId(self.nodes.len()); self.nodes.push(DispatchNode { parent, + focus_id, + view_id, ..Default::default() }); self.node_stack.push(node_id); + if let Some(context) = context { self.active_node().context = Some(context.clone()); self.context_stack.push(context); } + + if let Some(focus_id) = focus_id { + self.focusable_node_ids.insert(focus_id, node_id); + } + + if let Some(view_id) = view_id { + self.view_node_ids.insert(view_id, node_id); + } } pub fn pop_node(&mut self) { - let node_id = self.node_stack.pop().unwrap(); - if self.nodes[node_id.0].context.is_some() { + let node = &self.nodes[self.active_node_id().0]; + if node.context.is_some() { self.context_stack.pop(); } + self.node_stack.pop(); + } + + fn move_node(&mut self, source: &mut DispatchNode) { + self.push_node(source.context.take(), source.focus_id, source.view_id); + let target = self.active_node(); + target.key_listeners = mem::take(&mut source.key_listeners); + target.action_listeners = mem::take(&mut source.action_listeners); + } + + pub fn reuse_view(&mut self, view_id: EntityId, source: &mut Self) -> SmallVec<[EntityId; 8]> { + let view_source_node_id = source + .view_node_ids + .get(&view_id) + .expect("view should exist in previous dispatch tree"); + let view_source_node = &mut source.nodes[view_source_node_id.0]; + self.move_node(view_source_node); + + let mut grafted_view_ids = smallvec![view_id]; + let mut source_stack = vec![*view_source_node_id]; + for (source_node_id, source_node) in source + .nodes + .iter_mut() + .enumerate() + .skip(view_source_node_id.0 + 1) + { + let source_node_id = DispatchNodeId(source_node_id); + while let Some(source_ancestor) = source_stack.last() { + if source_node.parent != Some(*source_ancestor) { + source_stack.pop(); + self.pop_node(); + } else { + break; + } + } + + if source_stack.is_empty() { + break; + } else { + source_stack.push(source_node_id); + self.move_node(source_node); + if let Some(view_id) = source_node.view_id { + grafted_view_ids.push(view_id); + } + } + } + + while !source_stack.is_empty() { + source_stack.pop(); + self.pop_node(); + } + + grafted_view_ids } pub fn clear_pending_keystrokes(&mut self) { @@ -117,7 +191,7 @@ impl DispatchTree { pub fn on_action( &mut self, action_type: TypeId, - listener: ArenaRef, + listener: Rc, ) { self.active_node() .action_listeners @@ -127,12 +201,6 @@ impl DispatchTree { }); } - pub fn make_focusable(&mut self, focus_id: FocusId) { - let node_id = self.active_node_id(); - self.active_node().focus_id = Some(focus_id); - self.focusable_node_ids.insert(focus_id, node_id); - } - pub fn focus_contains(&self, parent: FocusId, child: FocusId) -> bool { if parent == child { return true; @@ -261,6 +329,20 @@ impl DispatchTree { focus_path } + pub fn view_path(&self, view_id: EntityId) -> SmallVec<[EntityId; 8]> { + let mut view_path: SmallVec<[EntityId; 8]> = SmallVec::new(); + let mut current_node_id = self.view_node_ids.get(&view_id).copied(); + while let Some(node_id) = current_node_id { + let node = self.node(node_id); + if let Some(view_id) = node.view_id { + view_path.push(view_id); + } + current_node_id = node.parent; + } + view_path.reverse(); // Reverse the path so it goes from the root to the view node. + view_path + } + pub fn node(&self, node_id: DispatchNodeId) -> &DispatchNode { &self.nodes[node_id.0] } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 0ef345d98d1de34bbb7c6b567b4716608e2e6dbf..5a2335919ebe6ff9b2277e02d4f6f55b4dc9a80c 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -44,8 +44,6 @@ pub(crate) fn current_platform() -> Rc { Rc::new(MacPlatform::new()) } -pub type DrawWindow = Box Result>; - pub(crate) trait Platform: 'static { fn background_executor(&self) -> BackgroundExecutor; fn foreground_executor(&self) -> ForegroundExecutor; @@ -66,7 +64,6 @@ pub(crate) trait Platform: 'static { &self, handle: AnyWindowHandle, options: WindowOptions, - draw: DrawWindow, ) -> Box; fn set_display_link_output_callback( @@ -148,7 +145,7 @@ pub trait PlatformWindow { fn modifiers(&self) -> Modifiers; fn as_any_mut(&mut self) -> &mut dyn Any; fn set_input_handler(&mut self, input_handler: Box); - fn clear_input_handler(&mut self); + fn take_input_handler(&mut self) -> Option>; fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver; fn activate(&self); fn set_title(&mut self, title: &str); @@ -157,6 +154,7 @@ pub trait PlatformWindow { fn minimize(&self); fn zoom(&self); fn toggle_full_screen(&self); + fn on_request_frame(&self, callback: Box); fn on_input(&self, callback: Box bool>); fn on_active_status_change(&self, callback: Box); fn on_resize(&self, callback: Box, f32)>); @@ -167,6 +165,7 @@ pub trait PlatformWindow { fn on_appearance_changed(&self, callback: Box); fn is_topmost_for_position(&self, position: Point) -> bool; fn invalidate(&self); + fn draw(&self, scene: &Scene); fn sprite_atlas(&self) -> Arc; diff --git a/crates/gpui/src/platform/mac/display.rs b/crates/gpui/src/platform/mac/display.rs index 25e0921fee28efd25c0c1ddf88d6df84318a19aa..2b72c335c80e80f700d6caa25fd64d5c8bf4f414 100644 --- a/crates/gpui/src/platform/mac/display.rs +++ b/crates/gpui/src/platform/mac/display.rs @@ -1,10 +1,16 @@ use crate::{point, size, Bounds, DisplayId, GlobalPixels, PlatformDisplay}; use anyhow::Result; +use cocoa::{ + appkit::NSScreen, + base::{id, nil}, + foundation::{NSDictionary, NSString}, +}; use core_foundation::uuid::{CFUUIDGetUUIDBytes, CFUUIDRef}; use core_graphics::{ display::{CGDirectDisplayID, CGDisplayBounds, CGGetActiveDisplayList}, geometry::{CGPoint, CGRect, CGSize}, }; +use objc::{msg_send, sel, sel_impl}; use std::any::Any; use uuid::Uuid; @@ -27,23 +33,41 @@ impl MacDisplay { /// Get the primary screen - the one with the menu bar, and whose bottom left /// corner is at the origin of the AppKit coordinate system. pub fn primary() -> Self { - Self::all().next().unwrap() + // Instead of iterating through all active systems displays via `all()` we use the first + // NSScreen and gets its CGDirectDisplayID, because we can't be sure that `CGGetActiveDisplayList` + // will always return a list of active displays (machine might be sleeping). + // + // The following is what Chromium does too: + // + // https://chromium.googlesource.com/chromium/src/+/66.0.3359.158/ui/display/mac/screen_mac.mm#56 + unsafe { + let screens = NSScreen::screens(nil); + let screen = cocoa::foundation::NSArray::objectAtIndex(screens, 0); + let device_description = NSScreen::deviceDescription(screen); + let screen_number_key: id = NSString::alloc(nil).init_str("NSScreenNumber"); + let screen_number = device_description.objectForKey_(screen_number_key); + let screen_number: CGDirectDisplayID = msg_send![screen_number, unsignedIntegerValue]; + Self(screen_number) + } } /// Obtains an iterator over all currently active system displays. pub fn all() -> impl Iterator { unsafe { - let mut display_count: u32 = 0; - let result = CGGetActiveDisplayList(0, std::ptr::null_mut(), &mut display_count); + // We're assuming there aren't more than 32 displays connected to the system. + let mut displays = Vec::with_capacity(32); + let mut display_count = 0; + let result = CGGetActiveDisplayList( + displays.capacity() as u32, + displays.as_mut_ptr(), + &mut display_count, + ); if result == 0 { - let mut displays = Vec::with_capacity(display_count as usize); - CGGetActiveDisplayList(display_count, displays.as_mut_ptr(), &mut display_count); displays.set_len(display_count as usize); - displays.into_iter().map(MacDisplay) } else { - panic!("Failed to get active display list"); + panic!("Failed to get active display list. Result: {result}"); } } } diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index a6cdd166d312d5eb97fc749a645a8cb09ea0dd8a..20e749a2f607fa96ec6fbdfc76574307648a621d 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -18,7 +18,7 @@ use smallvec::SmallVec; use std::{ffi::c_void, mem, ptr, sync::Arc}; const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib")); -const INSTANCE_BUFFER_SIZE: usize = 8192 * 1024; // This is an arbitrary decision. There's probably a more optimal value. +const INSTANCE_BUFFER_SIZE: usize = 32 * 1024 * 1024; // This is an arbitrary decision. There's probably a more optimal value (maybe even we could adjust dynamically...) pub(crate) struct MetalRenderer { layer: metal::MetalLayer, @@ -204,7 +204,11 @@ impl MetalRenderer { let command_buffer = command_queue.new_command_buffer(); let mut instance_offset = 0; - let path_tiles = self.rasterize_paths(scene.paths(), &mut instance_offset, command_buffer); + let Some(path_tiles) = + self.rasterize_paths(scene.paths(), &mut instance_offset, command_buffer) + else { + panic!("failed to rasterize {} paths", scene.paths().len()); + }; let render_pass_descriptor = metal::RenderPassDescriptor::new(); let color_attachment = render_pass_descriptor @@ -228,67 +232,67 @@ impl MetalRenderer { zfar: 1.0, }); for batch in scene.batches() { - match batch { - PrimitiveBatch::Shadows(shadows) => { - self.draw_shadows( - shadows, - &mut instance_offset, - viewport_size, - command_encoder, - ); - } + let ok = match batch { + PrimitiveBatch::Shadows(shadows) => self.draw_shadows( + shadows, + &mut instance_offset, + viewport_size, + command_encoder, + ), PrimitiveBatch::Quads(quads) => { - self.draw_quads(quads, &mut instance_offset, viewport_size, command_encoder); - } - PrimitiveBatch::Paths(paths) => { - self.draw_paths( - paths, - &path_tiles, - &mut instance_offset, - viewport_size, - command_encoder, - ); - } - PrimitiveBatch::Underlines(underlines) => { - self.draw_underlines( - underlines, - &mut instance_offset, - viewport_size, - command_encoder, - ); + self.draw_quads(quads, &mut instance_offset, viewport_size, command_encoder) } + PrimitiveBatch::Paths(paths) => self.draw_paths( + paths, + &path_tiles, + &mut instance_offset, + viewport_size, + command_encoder, + ), + PrimitiveBatch::Underlines(underlines) => self.draw_underlines( + underlines, + &mut instance_offset, + viewport_size, + command_encoder, + ), PrimitiveBatch::MonochromeSprites { texture_id, sprites, - } => { - self.draw_monochrome_sprites( - texture_id, - sprites, - &mut instance_offset, - viewport_size, - command_encoder, - ); - } + } => self.draw_monochrome_sprites( + texture_id, + sprites, + &mut instance_offset, + viewport_size, + command_encoder, + ), PrimitiveBatch::PolychromeSprites { texture_id, sprites, - } => { - self.draw_polychrome_sprites( - texture_id, - sprites, - &mut instance_offset, - viewport_size, - command_encoder, - ); - } - PrimitiveBatch::Surfaces(surfaces) => { - self.draw_surfaces( - surfaces, - &mut instance_offset, - viewport_size, - command_encoder, - ); - } + } => self.draw_polychrome_sprites( + texture_id, + sprites, + &mut instance_offset, + viewport_size, + command_encoder, + ), + PrimitiveBatch::Surfaces(surfaces) => self.draw_surfaces( + surfaces, + &mut instance_offset, + viewport_size, + command_encoder, + ), + }; + + if !ok { + panic!("scene too large: {} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces", + scene.paths.len(), + scene.shadows.len(), + scene.quads.len(), + scene.underlines.len(), + scene.monochrome_sprites.len(), + scene.polychrome_sprites.len(), + scene.surfaces.len(), + ) } } @@ -311,7 +315,7 @@ impl MetalRenderer { paths: &[Path], offset: &mut usize, command_buffer: &metal::CommandBufferRef, - ) -> HashMap { + ) -> Option> { let mut tiles = HashMap::default(); let mut vertices_by_texture_id = HashMap::default(); for path in paths { @@ -337,10 +341,9 @@ impl MetalRenderer { for (texture_id, vertices) in vertices_by_texture_id { align_offset(offset); let next_offset = *offset + vertices.len() * mem::size_of::>(); - assert!( - next_offset <= INSTANCE_BUFFER_SIZE, - "instance buffer exhausted" - ); + if next_offset > INSTANCE_BUFFER_SIZE { + return None; + } let render_pass_descriptor = metal::RenderPassDescriptor::new(); let color_attachment = render_pass_descriptor @@ -389,7 +392,7 @@ impl MetalRenderer { *offset = next_offset; } - tiles + Some(tiles) } fn draw_shadows( @@ -398,9 +401,9 @@ impl MetalRenderer { offset: &mut usize, viewport_size: Size, command_encoder: &metal::RenderCommandEncoderRef, - ) { + ) -> bool { if shadows.is_empty() { - return; + return true; } align_offset(offset); @@ -429,6 +432,12 @@ impl MetalRenderer { let shadow_bytes_len = std::mem::size_of_val(shadows); let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) }; + + let next_offset = *offset + shadow_bytes_len; + if next_offset > INSTANCE_BUFFER_SIZE { + return false; + } + unsafe { ptr::copy_nonoverlapping( shadows.as_ptr() as *const u8, @@ -437,12 +446,6 @@ impl MetalRenderer { ); } - let next_offset = *offset + shadow_bytes_len; - assert!( - next_offset <= INSTANCE_BUFFER_SIZE, - "instance buffer exhausted" - ); - command_encoder.draw_primitives_instanced( metal::MTLPrimitiveType::Triangle, 0, @@ -450,6 +453,7 @@ impl MetalRenderer { shadows.len() as u64, ); *offset = next_offset; + true } fn draw_quads( @@ -458,9 +462,9 @@ impl MetalRenderer { offset: &mut usize, viewport_size: Size, command_encoder: &metal::RenderCommandEncoderRef, - ) { + ) -> bool { if quads.is_empty() { - return; + return true; } align_offset(offset); @@ -489,16 +493,16 @@ impl MetalRenderer { let quad_bytes_len = std::mem::size_of_val(quads); let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) }; + + let next_offset = *offset + quad_bytes_len; + if next_offset > INSTANCE_BUFFER_SIZE { + return false; + } + unsafe { ptr::copy_nonoverlapping(quads.as_ptr() as *const u8, buffer_contents, quad_bytes_len); } - let next_offset = *offset + quad_bytes_len; - assert!( - next_offset <= INSTANCE_BUFFER_SIZE, - "instance buffer exhausted" - ); - command_encoder.draw_primitives_instanced( metal::MTLPrimitiveType::Triangle, 0, @@ -506,6 +510,7 @@ impl MetalRenderer { quads.len() as u64, ); *offset = next_offset; + true } fn draw_paths( @@ -515,9 +520,9 @@ impl MetalRenderer { offset: &mut usize, viewport_size: Size, command_encoder: &metal::RenderCommandEncoderRef, - ) { + ) -> bool { if paths.is_empty() { - return; + return true; } command_encoder.set_render_pipeline_state(&self.path_sprites_pipeline_state); @@ -587,8 +592,14 @@ impl MetalRenderer { .set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture)); let sprite_bytes_len = mem::size_of::() * sprites.len(); + let next_offset = *offset + sprite_bytes_len; + if next_offset > INSTANCE_BUFFER_SIZE { + return false; + } + let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) }; + unsafe { ptr::copy_nonoverlapping( sprites.as_ptr() as *const u8, @@ -597,12 +608,6 @@ impl MetalRenderer { ); } - let next_offset = *offset + sprite_bytes_len; - assert!( - next_offset <= INSTANCE_BUFFER_SIZE, - "instance buffer exhausted" - ); - command_encoder.draw_primitives_instanced( metal::MTLPrimitiveType::Triangle, 0, @@ -613,6 +618,7 @@ impl MetalRenderer { sprites.clear(); } } + true } fn draw_underlines( @@ -621,9 +627,9 @@ impl MetalRenderer { offset: &mut usize, viewport_size: Size, command_encoder: &metal::RenderCommandEncoderRef, - ) { + ) -> bool { if underlines.is_empty() { - return; + return true; } align_offset(offset); @@ -661,10 +667,9 @@ impl MetalRenderer { } let next_offset = *offset + quad_bytes_len; - assert!( - next_offset <= INSTANCE_BUFFER_SIZE, - "instance buffer exhausted" - ); + if next_offset > INSTANCE_BUFFER_SIZE { + return false; + } command_encoder.draw_primitives_instanced( metal::MTLPrimitiveType::Triangle, @@ -673,6 +678,7 @@ impl MetalRenderer { underlines.len() as u64, ); *offset = next_offset; + true } fn draw_monochrome_sprites( @@ -682,9 +688,9 @@ impl MetalRenderer { offset: &mut usize, viewport_size: Size, command_encoder: &metal::RenderCommandEncoderRef, - ) { + ) -> bool { if sprites.is_empty() { - return; + return true; } align_offset(offset); @@ -723,6 +729,12 @@ impl MetalRenderer { let sprite_bytes_len = std::mem::size_of_val(sprites); let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) }; + + let next_offset = *offset + sprite_bytes_len; + if next_offset > INSTANCE_BUFFER_SIZE { + return false; + } + unsafe { ptr::copy_nonoverlapping( sprites.as_ptr() as *const u8, @@ -731,12 +743,6 @@ impl MetalRenderer { ); } - let next_offset = *offset + sprite_bytes_len; - assert!( - next_offset <= INSTANCE_BUFFER_SIZE, - "instance buffer exhausted" - ); - command_encoder.draw_primitives_instanced( metal::MTLPrimitiveType::Triangle, 0, @@ -744,6 +750,7 @@ impl MetalRenderer { sprites.len() as u64, ); *offset = next_offset; + true } fn draw_polychrome_sprites( @@ -753,9 +760,9 @@ impl MetalRenderer { offset: &mut usize, viewport_size: Size, command_encoder: &metal::RenderCommandEncoderRef, - ) { + ) -> bool { if sprites.is_empty() { - return; + return true; } align_offset(offset); @@ -794,6 +801,12 @@ impl MetalRenderer { let sprite_bytes_len = std::mem::size_of_val(sprites); let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) }; + + let next_offset = *offset + sprite_bytes_len; + if next_offset > INSTANCE_BUFFER_SIZE { + return false; + } + unsafe { ptr::copy_nonoverlapping( sprites.as_ptr() as *const u8, @@ -802,12 +815,6 @@ impl MetalRenderer { ); } - let next_offset = *offset + sprite_bytes_len; - assert!( - next_offset <= INSTANCE_BUFFER_SIZE, - "instance buffer exhausted" - ); - command_encoder.draw_primitives_instanced( metal::MTLPrimitiveType::Triangle, 0, @@ -815,6 +822,7 @@ impl MetalRenderer { sprites.len() as u64, ); *offset = next_offset; + true } fn draw_surfaces( @@ -823,7 +831,7 @@ impl MetalRenderer { offset: &mut usize, viewport_size: Size, command_encoder: &metal::RenderCommandEncoderRef, - ) { + ) -> bool { command_encoder.set_render_pipeline_state(&self.surfaces_pipeline_state); command_encoder.set_vertex_buffer( SurfaceInputIndex::Vertices as u64, @@ -874,10 +882,9 @@ impl MetalRenderer { align_offset(offset); let next_offset = *offset + mem::size_of::(); - assert!( - next_offset <= INSTANCE_BUFFER_SIZE, - "instance buffer exhausted" - ); + if next_offset > INSTANCE_BUFFER_SIZE { + return false; + } command_encoder.set_vertex_buffer( SurfaceInputIndex::Surfaces as u64, @@ -913,6 +920,7 @@ impl MetalRenderer { command_encoder.draw_primitives(metal::MTLPrimitiveType::Triangle, 0, 6); *offset = next_offset; } + true } } diff --git a/crates/gpui/src/platform/mac/open_type.rs b/crates/gpui/src/platform/mac/open_type.rs index 50e93a866db6c569926b791e5c89d82be7f8199b..c9d7197c0d3aa87d33d2e10048b14aa6c77483ec 100644 --- a/crates/gpui/src/platform/mac/open_type.rs +++ b/crates/gpui/src/platform/mac/open_type.rs @@ -378,7 +378,7 @@ fn toggle_open_type_feature( new_descriptor.as_concrete_TypeRef(), ); let new_font = CTFont::wrap_under_create_rule(new_font); - *font = Font::from_native_font(new_font); + *font = Font::from_native_font(&new_font); } } } diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 8370e2a4953c1280a59d4a9cb74a93ae97214db2..8061cc136064c8624d4e8ad217d0a1703274001f 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -3,8 +3,7 @@ use crate::{ Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor, InputEvent, Keymap, MacDispatcher, MacDisplay, MacDisplayLinker, MacTextSystem, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, - PlatformTextSystem, PlatformWindow, Result, Scene, SemanticVersion, VideoTimestamp, - WindowOptions, + PlatformTextSystem, PlatformWindow, Result, SemanticVersion, VideoTimestamp, WindowOptions, }; use anyhow::anyhow; use block::ConcreteBlock; @@ -498,14 +497,8 @@ impl Platform for MacPlatform { &self, handle: AnyWindowHandle, options: WindowOptions, - draw: Box Result>, ) -> Box { - Box::new(MacWindow::open( - handle, - options, - draw, - self.foreground_executor(), - )) + Box::new(MacWindow::open(handle, options, self.foreground_executor())) } fn set_display_link_output_callback( @@ -985,8 +978,12 @@ extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) { unsafe { if let Some(event) = InputEvent::from_native(native_event, None) { let platform = get_mac_platform(this); - if let Some(callback) = platform.0.lock().event.as_mut() { - if !callback(event) { + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.event.take() { + drop(lock); + let result = callback(event); + platform.0.lock().event.get_or_insert(callback); + if !result { return; } } @@ -1011,30 +1008,42 @@ extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) { extern "C" fn should_handle_reopen(this: &mut Object, _: Sel, _: id, has_open_windows: bool) { if !has_open_windows { let platform = unsafe { get_mac_platform(this) }; - if let Some(callback) = platform.0.lock().reopen.as_mut() { + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.reopen.take() { + drop(lock); callback(); + platform.0.lock().reopen.get_or_insert(callback); } } } extern "C" fn did_become_active(this: &mut Object, _: Sel, _: id) { let platform = unsafe { get_mac_platform(this) }; - if let Some(callback) = platform.0.lock().become_active.as_mut() { + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.become_active.take() { + drop(lock); callback(); + platform.0.lock().become_active.get_or_insert(callback); } } extern "C" fn did_resign_active(this: &mut Object, _: Sel, _: id) { let platform = unsafe { get_mac_platform(this) }; - if let Some(callback) = platform.0.lock().resign_active.as_mut() { + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.resign_active.take() { + drop(lock); callback(); + platform.0.lock().resign_active.get_or_insert(callback); } } extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) { let platform = unsafe { get_mac_platform(this) }; - if let Some(callback) = platform.0.lock().quit.as_mut() { + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.quit.take() { + drop(lock); callback(); + platform.0.lock().quit.get_or_insert(callback); } } @@ -1054,22 +1063,27 @@ extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) { .collect::>() }; let platform = unsafe { get_mac_platform(this) }; - if let Some(callback) = platform.0.lock().open_urls.as_mut() { + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.open_urls.take() { + drop(lock); callback(urls); + platform.0.lock().open_urls.get_or_insert(callback); } } extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { unsafe { let platform = get_mac_platform(this); - let mut platform = platform.0.lock(); - if let Some(mut callback) = platform.menu_command.take() { + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.menu_command.take() { let tag: NSInteger = msg_send![item, tag]; let index = tag as usize; - if let Some(action) = platform.menu_actions.get(index) { - callback(action.as_ref()); + if let Some(action) = lock.menu_actions.get(index) { + let action = action.boxed_clone(); + drop(lock); + callback(&*action); } - platform.menu_command = Some(callback); + platform.0.lock().menu_command.get_or_insert(callback); } } } @@ -1078,14 +1092,20 @@ extern "C" fn validate_menu_item(this: &mut Object, _: Sel, item: id) -> bool { unsafe { let mut result = false; let platform = get_mac_platform(this); - let mut platform = platform.0.lock(); - if let Some(mut callback) = platform.validate_menu_command.take() { + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.validate_menu_command.take() { let tag: NSInteger = msg_send![item, tag]; let index = tag as usize; - if let Some(action) = platform.menu_actions.get(index) { + if let Some(action) = lock.menu_actions.get(index) { + let action = action.boxed_clone(); + drop(lock); result = callback(action.as_ref()); } - platform.validate_menu_command = Some(callback); + platform + .0 + .lock() + .validate_menu_command + .get_or_insert(callback); } result } @@ -1094,10 +1114,11 @@ extern "C" fn validate_menu_item(this: &mut Object, _: Sel, item: id) -> bool { extern "C" fn menu_will_open(this: &mut Object, _: Sel, _: id) { unsafe { let platform = get_mac_platform(this); - let mut platform = platform.0.lock(); - if let Some(mut callback) = platform.will_open_menu.take() { + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.will_open_menu.take() { + drop(lock); callback(); - platform.will_open_menu = Some(callback); + platform.0.lock().will_open_menu.get_or_insert(callback); } } } diff --git a/crates/gpui/src/platform/mac/text_system.rs b/crates/gpui/src/platform/mac/text_system.rs index 79ffb8dc8e4aa54b954494a8cae4f3ca6186b377..68f4a63326757f8a87217a133aef005d0c8e5c86 100644 --- a/crates/gpui/src/platform/mac/text_system.rs +++ b/crates/gpui/src/platform/mac/text_system.rs @@ -190,6 +190,9 @@ impl MacTextSystemState { for font in family.fonts() { let mut font = font.load()?; open_type::apply_features(&mut font, features); + let Some(_) = font.glyph_for_char('m') else { + continue; + }; let font_id = FontId(self.fonts.len()); font_ids.push(font_id); let postscript_name = font.postscript_name().unwrap(); diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 6d4fd9c4896060b30621804ce532e97e0d369138..c364021281a3690635713bd756c520b1d5f3558b 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1,6 +1,6 @@ use super::{display_bounds_from_native, ns_string, MacDisplay, MetalRenderer, NSRange}; use crate::{ - display_bounds_to_native, point, px, size, AnyWindowHandle, Bounds, DrawWindow, ExternalPaths, + display_bounds_to_native, point, px, size, AnyWindowHandle, Bounds, ExternalPaths, FileDropEvent, ForegroundExecutor, GlobalPixels, InputEvent, KeyDownEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, @@ -46,7 +46,6 @@ use std::{ sync::{Arc, Weak}, time::Duration, }; -use util::ResultExt; const WINDOW_STATE_IVAR: &str = "windowState"; @@ -318,8 +317,8 @@ struct MacWindowState { executor: ForegroundExecutor, native_window: id, renderer: MetalRenderer, - draw: Option, kind: WindowKind, + request_frame_callback: Option>, event_callback: Option bool>>, activate_callback: Option>, resize_callback: Option, f32)>>, @@ -455,7 +454,6 @@ impl MacWindow { pub fn open( handle: AnyWindowHandle, options: WindowOptions, - draw: DrawWindow, executor: ForegroundExecutor, ) -> Self { unsafe { @@ -486,7 +484,7 @@ impl MacWindow { let display = options .display_id - .and_then(|display_id| MacDisplay::all().find(|display| display.id() == display_id)) + .and_then(MacDisplay::find_by_id) .unwrap_or_else(MacDisplay::primary); let mut target_screen = nil; @@ -547,8 +545,8 @@ impl MacWindow { executor, native_window, renderer: MetalRenderer::new(true), - draw: Some(draw), kind: options.kind, + request_frame_callback: None, event_callback: None, activate_callback: None, resize_callback: None, @@ -770,8 +768,8 @@ impl PlatformWindow for MacWindow { self.0.as_ref().lock().input_handler = Some(input_handler); } - fn clear_input_handler(&mut self) { - self.0.as_ref().lock().input_handler = None; + fn take_input_handler(&mut self) -> Option> { + self.0.as_ref().lock().input_handler.take() } fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver { @@ -926,6 +924,10 @@ impl PlatformWindow for MacWindow { .detach(); } + fn on_request_frame(&self, callback: Box) { + self.0.as_ref().lock().request_frame_callback = Some(callback); + } + fn on_input(&self, callback: Box bool>) { self.0.as_ref().lock().event_callback = Some(callback); } @@ -990,6 +992,11 @@ impl PlatformWindow for MacWindow { } } + fn draw(&self, scene: &crate::Scene) { + let mut this = self.0.lock(); + this.renderer.draw(scene); + } + fn sprite_atlas(&self) -> Arc { self.0.lock().renderer.sprite_atlas().clone() } @@ -1437,15 +1444,12 @@ extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) { } extern "C" fn display_layer(this: &Object, _: Sel, _: id) { - unsafe { - let window_state = get_window_state(this); - let mut draw = window_state.lock().draw.take().unwrap(); - let scene = draw().log_err(); - let mut window_state = window_state.lock(); - window_state.draw = Some(draw); - if let Some(scene) = scene { - window_state.renderer.draw(&scene); - } + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.lock(); + if let Some(mut callback) = lock.request_frame_callback.take() { + drop(lock); + callback(); + window_state.lock().request_frame_callback = Some(callback); } } diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index a7dc6d48419cc2e5d54dc132617752ae030e561e..3a4f5bb36a1360d7d994e82e9347563f985caa79 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -1,7 +1,6 @@ use crate::{ AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor, - Keymap, Platform, PlatformDisplay, PlatformTextSystem, Scene, TestDisplay, TestWindow, - WindowOptions, + Keymap, Platform, PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions, }; use anyhow::{anyhow, Result}; use collections::VecDeque; @@ -166,7 +165,6 @@ impl Platform for TestPlatform { &self, handle: AnyWindowHandle, options: WindowOptions, - _draw: Box Result>, ) -> Box { let window = TestWindow::new( options, diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index 91f965c10ac2987f4f6c8c15cb40a7cd94171370..f05e13e3a027e2be9d4f17690fe7162a73396d03 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/crates/gpui/src/platform/test/window.rs @@ -167,8 +167,8 @@ impl PlatformWindow for TestWindow { self.0.lock().input_handler = Some(input_handler); } - fn clear_input_handler(&mut self) { - self.0.lock().input_handler = None; + fn take_input_handler(&mut self) -> Option> { + self.0.lock().input_handler.take() } fn prompt( @@ -218,6 +218,8 @@ impl PlatformWindow for TestWindow { unimplemented!() } + fn on_request_frame(&self, _callback: Box) {} + fn on_input(&self, callback: Box bool>) { self.0.lock().input_callback = Some(callback) } @@ -254,9 +256,9 @@ impl PlatformWindow for TestWindow { unimplemented!() } - fn invalidate(&self) { - // (self.draw.lock())().unwrap(); - } + fn invalidate(&self) {} + + fn draw(&self, _scene: &crate::Scene) {} fn sprite_atlas(&self) -> sync::Arc { self.0.lock().sprite_atlas.clone() diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index e922c11f533680e102e67bfa7bd5d51b741678a3..11341a2cbc524899a3455f490b20652e7f17fc4b 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -1,9 +1,9 @@ use crate::{ - point, AtlasTextureId, AtlasTile, Bounds, ContentMask, Corners, Edges, Hsla, Pixels, Point, - ScaledPixels, StackingOrder, + point, AtlasTextureId, AtlasTile, Bounds, ContentMask, Corners, Edges, EntityId, Hsla, Pixels, + Point, ScaledPixels, StackingOrder, }; -use collections::BTreeMap; -use std::{fmt::Debug, iter::Peekable, mem, slice}; +use collections::{BTreeMap, FxHashSet}; +use std::{fmt::Debug, iter::Peekable, slice}; // Exported to metal pub(crate) type PointF = Point; @@ -11,74 +11,85 @@ pub(crate) type PointF = Point; pub(crate) type PathVertex_ScaledPixels = PathVertex; pub type LayerId = u32; - pub type DrawOrder = u32; -#[derive(Default)] -pub(crate) struct SceneBuilder { - last_order: Option<(StackingOrder, LayerId)>, - layers_by_order: BTreeMap, - shadows: Vec, - quads: Vec, - paths: Vec>, - underlines: Vec, - monochrome_sprites: Vec, - polychrome_sprites: Vec, - surfaces: Vec, +#[derive(Default, Copy, Clone, Debug, Eq, PartialEq, Hash)] +#[repr(C)] +pub struct ViewId { + low_bits: u32, + high_bits: u32, } -impl SceneBuilder { - pub fn build(&mut self) -> Scene { - let mut orders = vec![0; self.layers_by_order.len()]; - for (ix, layer_id) in self.layers_by_order.values().enumerate() { - orders[*layer_id as usize] = ix as u32; - } - self.layers_by_order.clear(); - self.last_order = None; - - for shadow in &mut self.shadows { - shadow.order = orders[shadow.order as usize]; - } - self.shadows.sort_by_key(|shadow| shadow.order); - - for quad in &mut self.quads { - quad.order = orders[quad.order as usize]; - } - self.quads.sort_by_key(|quad| quad.order); - - for path in &mut self.paths { - path.order = orders[path.order as usize]; +impl From for ViewId { + fn from(value: EntityId) -> Self { + let value = value.as_u64(); + Self { + low_bits: value as u32, + high_bits: (value >> 32) as u32, } - self.paths.sort_by_key(|path| path.order); + } +} - for underline in &mut self.underlines { - underline.order = orders[underline.order as usize]; - } - self.underlines.sort_by_key(|underline| underline.order); +impl From for EntityId { + fn from(value: ViewId) -> Self { + let value = (value.low_bits as u64) | ((value.high_bits as u64) << 32); + value.into() + } +} - for monochrome_sprite in &mut self.monochrome_sprites { - monochrome_sprite.order = orders[monochrome_sprite.order as usize]; - } - self.monochrome_sprites.sort_by_key(|sprite| sprite.order); +#[derive(Default)] +pub struct Scene { + layers_by_order: BTreeMap, + orders_by_layer: BTreeMap, + pub(crate) shadows: Vec, + pub(crate) quads: Vec, + pub(crate) paths: Vec>, + pub(crate) underlines: Vec, + pub(crate) monochrome_sprites: Vec, + pub(crate) polychrome_sprites: Vec, + pub(crate) surfaces: Vec, +} - for polychrome_sprite in &mut self.polychrome_sprites { - polychrome_sprite.order = orders[polychrome_sprite.order as usize]; - } - self.polychrome_sprites.sort_by_key(|sprite| sprite.order); +impl Scene { + pub fn clear(&mut self) { + self.layers_by_order.clear(); + self.orders_by_layer.clear(); + self.shadows.clear(); + self.quads.clear(); + self.paths.clear(); + self.underlines.clear(); + self.monochrome_sprites.clear(); + self.polychrome_sprites.clear(); + self.surfaces.clear(); + } - for surface in &mut self.surfaces { - surface.order = orders[surface.order as usize]; - } - self.surfaces.sort_by_key(|surface| surface.order); + pub fn paths(&self) -> &[Path] { + &self.paths + } - Scene { - shadows: mem::take(&mut self.shadows), - quads: mem::take(&mut self.quads), - paths: mem::take(&mut self.paths), - underlines: mem::take(&mut self.underlines), - monochrome_sprites: mem::take(&mut self.monochrome_sprites), - polychrome_sprites: mem::take(&mut self.polychrome_sprites), - surfaces: mem::take(&mut self.surfaces), + pub(crate) fn batches(&self) -> impl Iterator { + BatchIterator { + shadows: &self.shadows, + shadows_start: 0, + shadows_iter: self.shadows.iter().peekable(), + quads: &self.quads, + quads_start: 0, + quads_iter: self.quads.iter().peekable(), + paths: &self.paths, + paths_start: 0, + paths_iter: self.paths.iter().peekable(), + underlines: &self.underlines, + underlines_start: 0, + underlines_iter: self.underlines.iter().peekable(), + monochrome_sprites: &self.monochrome_sprites, + monochrome_sprites_start: 0, + monochrome_sprites_iter: self.monochrome_sprites.iter().peekable(), + polychrome_sprites: &self.polychrome_sprites, + polychrome_sprites_start: 0, + polychrome_sprites_iter: self.polychrome_sprites.iter().peekable(), + surfaces: &self.surfaces, + surfaces_start: 0, + surfaces_iter: self.surfaces.iter().peekable(), } } @@ -96,95 +107,139 @@ impl SceneBuilder { let layer_id = self.layer_id_for_order(order); match primitive { Primitive::Shadow(mut shadow) => { - shadow.order = layer_id; + shadow.layer_id = layer_id; self.shadows.push(shadow); } Primitive::Quad(mut quad) => { - quad.order = layer_id; + quad.layer_id = layer_id; self.quads.push(quad); } Primitive::Path(mut path) => { - path.order = layer_id; + path.layer_id = layer_id; path.id = PathId(self.paths.len()); self.paths.push(path); } Primitive::Underline(mut underline) => { - underline.order = layer_id; + underline.layer_id = layer_id; self.underlines.push(underline); } Primitive::MonochromeSprite(mut sprite) => { - sprite.order = layer_id; + sprite.layer_id = layer_id; self.monochrome_sprites.push(sprite); } Primitive::PolychromeSprite(mut sprite) => { - sprite.order = layer_id; + sprite.layer_id = layer_id; self.polychrome_sprites.push(sprite); } Primitive::Surface(mut surface) => { - surface.order = layer_id; + surface.layer_id = layer_id; self.surfaces.push(surface); } } } - fn layer_id_for_order(&mut self, order: &StackingOrder) -> u32 { - if let Some((last_order, last_layer_id)) = self.last_order.as_ref() { - if last_order == order { - return *last_layer_id; - } - }; - - let layer_id = if let Some(layer_id) = self.layers_by_order.get(order) { + fn layer_id_for_order(&mut self, order: &StackingOrder) -> LayerId { + if let Some(layer_id) = self.layers_by_order.get(order) { *layer_id } else { let next_id = self.layers_by_order.len() as LayerId; self.layers_by_order.insert(order.clone(), next_id); + self.orders_by_layer.insert(next_id, order.clone()); next_id - }; - self.last_order = Some((order.clone(), layer_id)); - layer_id + } } -} -pub struct Scene { - pub shadows: Vec, - pub quads: Vec, - pub paths: Vec>, - pub underlines: Vec, - pub monochrome_sprites: Vec, - pub polychrome_sprites: Vec, - pub surfaces: Vec, -} + pub fn reuse_views(&mut self, views: &FxHashSet, prev_scene: &mut Self) { + for shadow in prev_scene.shadows.drain(..) { + if views.contains(&shadow.view_id.into()) { + let order = &prev_scene.orders_by_layer[&shadow.layer_id]; + self.insert(&order, shadow); + } + } -impl Scene { - pub fn paths(&self) -> &[Path] { - &self.paths + for quad in prev_scene.quads.drain(..) { + if views.contains(&quad.view_id.into()) { + let order = &prev_scene.orders_by_layer[&quad.layer_id]; + self.insert(&order, quad); + } + } + + for path in prev_scene.paths.drain(..) { + if views.contains(&path.view_id.into()) { + let order = &prev_scene.orders_by_layer[&path.layer_id]; + self.insert(&order, path); + } + } + + for underline in prev_scene.underlines.drain(..) { + if views.contains(&underline.view_id.into()) { + let order = &prev_scene.orders_by_layer[&underline.layer_id]; + self.insert(&order, underline); + } + } + + for sprite in prev_scene.monochrome_sprites.drain(..) { + if views.contains(&sprite.view_id.into()) { + let order = &prev_scene.orders_by_layer[&sprite.layer_id]; + self.insert(&order, sprite); + } + } + + for sprite in prev_scene.polychrome_sprites.drain(..) { + if views.contains(&sprite.view_id.into()) { + let order = &prev_scene.orders_by_layer[&sprite.layer_id]; + self.insert(&order, sprite); + } + } + + for surface in prev_scene.surfaces.drain(..) { + if views.contains(&surface.view_id.into()) { + let order = &prev_scene.orders_by_layer[&surface.layer_id]; + self.insert(&order, surface); + } + } } - pub(crate) fn batches(&self) -> impl Iterator { - BatchIterator { - shadows: &self.shadows, - shadows_start: 0, - shadows_iter: self.shadows.iter().peekable(), - quads: &self.quads, - quads_start: 0, - quads_iter: self.quads.iter().peekable(), - paths: &self.paths, - paths_start: 0, - paths_iter: self.paths.iter().peekable(), - underlines: &self.underlines, - underlines_start: 0, - underlines_iter: self.underlines.iter().peekable(), - monochrome_sprites: &self.monochrome_sprites, - monochrome_sprites_start: 0, - monochrome_sprites_iter: self.monochrome_sprites.iter().peekable(), - polychrome_sprites: &self.polychrome_sprites, - polychrome_sprites_start: 0, - polychrome_sprites_iter: self.polychrome_sprites.iter().peekable(), - surfaces: &self.surfaces, - surfaces_start: 0, - surfaces_iter: self.surfaces.iter().peekable(), + pub fn finish(&mut self) { + let mut orders = vec![0; self.layers_by_order.len()]; + for (ix, layer_id) in self.layers_by_order.values().enumerate() { + orders[*layer_id as usize] = ix as u32; + } + + for shadow in &mut self.shadows { + shadow.order = orders[shadow.layer_id as usize]; + } + self.shadows.sort_by_key(|shadow| shadow.order); + + for quad in &mut self.quads { + quad.order = orders[quad.layer_id as usize]; + } + self.quads.sort_by_key(|quad| quad.order); + + for path in &mut self.paths { + path.order = orders[path.layer_id as usize]; + } + self.paths.sort_by_key(|path| path.order); + + for underline in &mut self.underlines { + underline.order = orders[underline.layer_id as usize]; + } + self.underlines.sort_by_key(|underline| underline.order); + + for monochrome_sprite in &mut self.monochrome_sprites { + monochrome_sprite.order = orders[monochrome_sprite.layer_id as usize]; } + self.monochrome_sprites.sort_by_key(|sprite| sprite.order); + + for polychrome_sprite in &mut self.polychrome_sprites { + polychrome_sprite.order = orders[polychrome_sprite.layer_id as usize]; + } + self.polychrome_sprites.sort_by_key(|sprite| sprite.order); + + for surface in &mut self.surfaces { + surface.order = orders[surface.layer_id as usize]; + } + self.surfaces.sort_by_key(|surface| surface.order); } } @@ -439,7 +494,9 @@ pub(crate) enum PrimitiveBatch<'a> { #[derive(Default, Debug, Clone, Eq, PartialEq)] #[repr(C)] pub struct Quad { - pub order: u32, // Initially a LayerId, then a DrawOrder. + pub view_id: ViewId, + pub layer_id: LayerId, + pub order: DrawOrder, pub bounds: Bounds, pub content_mask: ContentMask, pub background: Hsla, @@ -469,7 +526,9 @@ impl From for Primitive { #[derive(Debug, Clone, Eq, PartialEq)] #[repr(C)] pub struct Underline { - pub order: u32, + pub view_id: ViewId, + pub layer_id: LayerId, + pub order: DrawOrder, pub bounds: Bounds, pub content_mask: ContentMask, pub thickness: ScaledPixels, @@ -498,7 +557,9 @@ impl From for Primitive { #[derive(Debug, Clone, Eq, PartialEq)] #[repr(C)] pub struct Shadow { - pub order: u32, + pub view_id: ViewId, + pub layer_id: LayerId, + pub order: DrawOrder, pub bounds: Bounds, pub corner_radii: Corners, pub content_mask: ContentMask, @@ -527,7 +588,9 @@ impl From for Primitive { #[derive(Clone, Debug, Eq, PartialEq)] #[repr(C)] pub struct MonochromeSprite { - pub order: u32, + pub view_id: ViewId, + pub layer_id: LayerId, + pub order: DrawOrder, pub bounds: Bounds, pub content_mask: ContentMask, pub color: Hsla, @@ -558,7 +621,9 @@ impl From for Primitive { #[derive(Clone, Debug, Eq, PartialEq)] #[repr(C)] pub struct PolychromeSprite { - pub order: u32, + pub view_id: ViewId, + pub layer_id: LayerId, + pub order: DrawOrder, pub bounds: Bounds, pub content_mask: ContentMask, pub corner_radii: Corners, @@ -589,7 +654,9 @@ impl From for Primitive { #[derive(Clone, Debug, Eq, PartialEq)] pub struct Surface { - pub order: u32, + pub view_id: ViewId, + pub layer_id: LayerId, + pub order: DrawOrder, pub bounds: Bounds, pub content_mask: ContentMask, pub image_buffer: media::core_video::CVImageBuffer, @@ -619,7 +686,9 @@ pub(crate) struct PathId(pub(crate) usize); #[derive(Debug)] pub struct Path { pub(crate) id: PathId, - order: u32, + pub(crate) view_id: ViewId, + layer_id: LayerId, + order: DrawOrder, pub(crate) bounds: Bounds

, pub(crate) content_mask: ContentMask

, pub(crate) vertices: Vec>, @@ -633,7 +702,9 @@ impl Path { pub fn new(start: Point) -> Self { Self { id: PathId(0), - order: 0, + view_id: ViewId::default(), + layer_id: LayerId::default(), + order: DrawOrder::default(), vertices: Vec::new(), start, current: start, @@ -650,6 +721,8 @@ impl Path { pub fn scale(&self, factor: f32) -> Path { Path { id: self.id, + view_id: self.view_id, + layer_id: self.layer_id, order: self.order, bounds: self.bounds.scale(factor), content_mask: self.content_mask.scale(factor), diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index a21957611d09feb25f59d4a842ab9b998e83b4d4..9f46d8dab6fd6214b0f22cf0b4e616300d595e8a 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -1,10 +1,10 @@ use std::{iter, mem, ops::Range}; use crate::{ - black, phi, point, quad, rems, AbsoluteLength, BorrowWindow, Bounds, ContentMask, Corners, - CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font, FontFeatures, - FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba, SharedString, Size, - SizeRefinement, Styled, TextRun, WindowContext, + black, phi, point, quad, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, + ContentMask, Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, + Font, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba, + SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext, }; use collections::HashSet; use refineable::{Cascade, Refineable}; @@ -146,7 +146,7 @@ pub enum WhiteSpace { Nowrap, } -#[derive(Refineable, Clone, Debug)] +#[derive(Refineable, Clone, Debug, PartialEq)] #[refineable(Debug)] pub struct TextStyle { pub color: Hsla, @@ -308,54 +308,54 @@ impl Style { } } - // pub fn apply_text_style(&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(&self, bounds: Bounds, 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) - // } + pub fn apply_text_style(&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(&self, bounds: Bounds, cx: &mut C, f: F) -> R + where + C: BorrowWindow, + F: FnOnce(&mut C) -> R, + { + let current_mask = cx.content_mask(); + + let min = current_mask.bounds.origin; + let max = current_mask.bounds.lower_right(); + + let mask_bounds = match ( + self.overflow.x == Overflow::Visible, + self.overflow.y == Overflow::Visible, + ) { + // x and y both visible + (true, true) => return f(cx), + // x visible, y hidden + (true, false) => Bounds::from_corners( + point(min.x, bounds.origin.y), + point(max.x, bounds.lower_right().y), + ), + // x hidden, y visible + (false, true) => Bounds::from_corners( + point(bounds.origin.x, min.y), + point(bounds.lower_right().x, max.y), + ), + // both hidden + (false, false) => bounds, + }; + let mask = ContentMask { + bounds: mask_bounds, + }; + + cx.with_content_mask(Some(mask), f) + } /// Paints the background of an element styled with this style. pub fn paint( diff --git a/crates/gpui/src/styled.rs b/crates/gpui/src/styled.rs index 2749c31a788f8d1fd83fe1353aaae15179b859cb..0eba1771f52d47bde32f465a887e52547f3a89b2 100644 --- a/crates/gpui/src/styled.rs +++ b/crates/gpui/src/styled.rs @@ -1,7 +1,7 @@ use crate::{ self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle, - DefiniteLength, Display, Fill, FlexDirection, Hsla, JustifyContent, Length, Position, - SharedString, StyleRefinement, Visibility, WhiteSpace, + DefiniteLength, Display, Fill, FlexDirection, FontWeight, Hsla, JustifyContent, Length, + Position, SharedString, StyleRefinement, Visibility, WhiteSpace, }; use crate::{BoxShadow, TextStyleRefinement}; use smallvec::{smallvec, SmallVec}; @@ -494,6 +494,13 @@ pub trait Styled: Sized { self } + fn font_weight(mut self, weight: FontWeight) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .font_weight = Some(weight); + self + } + fn text_bg(mut self, bg: impl Into) -> Self { self.text_style() .get_or_insert_with(Default::default) diff --git a/crates/gpui/src/taffy.rs b/crates/gpui/src/taffy.rs index 0ebd394217ae06c6a2256281f389ab506bfca934..26d5a2e69ea2dcc8e1664b4fa8ef885f84e8feeb 100644 --- a/crates/gpui/src/taffy.rs +++ b/crates/gpui/src/taffy.rs @@ -14,6 +14,7 @@ use taffy::{ pub struct TaffyLayoutEngine { taffy: Taffy, + styles: FxHashMap, children_to_parents: FxHashMap, absolute_layout_bounds: FxHashMap>, computed_layouts: FxHashSet, @@ -35,6 +36,7 @@ impl TaffyLayoutEngine { pub fn new() -> Self { TaffyLayoutEngine { taffy: Taffy::new(), + styles: FxHashMap::default(), children_to_parents: FxHashMap::default(), absolute_layout_bounds: FxHashMap::default(), computed_layouts: FxHashSet::default(), @@ -48,6 +50,11 @@ impl TaffyLayoutEngine { self.absolute_layout_bounds.clear(); self.computed_layouts.clear(); self.nodes_to_measure.clear(); + self.styles.clear(); + } + + pub fn requested_style(&self, layout_id: LayoutId) -> Option<&Style> { + self.styles.get(&layout_id) } pub fn request_layout( @@ -56,21 +63,26 @@ impl TaffyLayoutEngine { rem_size: Pixels, children: &[LayoutId], ) -> LayoutId { - let style = style.to_taffy(rem_size); - if children.is_empty() { - self.taffy.new_leaf(style).expect(EXPECT_MESSAGE).into() + let taffy_style = style.to_taffy(rem_size); + let layout_id = if children.is_empty() { + self.taffy + .new_leaf(taffy_style) + .expect(EXPECT_MESSAGE) + .into() } else { let parent_id = self .taffy // This is safe because LayoutId is repr(transparent) to taffy::tree::NodeId. - .new_with_children(style, unsafe { std::mem::transmute(children) }) + .new_with_children(taffy_style, unsafe { std::mem::transmute(children) }) .expect(EXPECT_MESSAGE) .into(); for child_id in children { self.children_to_parents.insert(*child_id, parent_id); } parent_id - } + }; + self.styles.insert(layout_id, style.clone()); + layout_id } pub fn request_measured_layout( @@ -80,14 +92,16 @@ impl TaffyLayoutEngine { measure: impl FnMut(Size>, Size, &mut WindowContext) -> Size + 'static, ) -> LayoutId { - let style = style.to_taffy(rem_size); + let style = style.clone(); + let taffy_style = style.to_taffy(rem_size); let layout_id = self .taffy - .new_leaf_with_context(style, ()) + .new_leaf_with_context(taffy_style, ()) .expect(EXPECT_MESSAGE) .into(); self.nodes_to_measure.insert(layout_id, Box::new(measure)); + self.styles.insert(layout_id, style.clone()); layout_id } @@ -271,20 +285,6 @@ impl ToTaffy for Style { } } -// impl ToTaffy for Bounds { -// type Output = taffy::prelude::Bounds; - -// fn to_taffy( -// &self, -// rem_size: Pixels, -// ) -> taffy::prelude::Bounds { -// taffy::prelude::Bounds { -// origin: self.origin.to_taffy(rem_size), -// size: self.size.to_taffy(rem_size), -// } -// } -// } - impl ToTaffy for Length { fn to_taffy(&self, rem_size: Pixels) -> taffy::prelude::LengthPercentageAuto { match self { diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index 2d3cc34f3fc09e6402ece055f9aa6ba45fe5434e..24438d8c819527a3e8dd869f9e1dac179d9dd618 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -9,11 +9,11 @@ pub use line_layout::*; pub use line_wrapper::*; use crate::{ - px, Bounds, DevicePixels, Hsla, Pixels, PlatformTextSystem, Point, Result, SharedString, Size, - UnderlineStyle, + px, Bounds, DevicePixels, EntityId, Hsla, Pixels, PlatformTextSystem, Point, Result, + SharedString, Size, UnderlineStyle, }; use anyhow::anyhow; -use collections::FxHashMap; +use collections::{FxHashMap, FxHashSet}; use core::fmt; use itertools::Itertools; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; @@ -65,6 +65,17 @@ impl TextSystem { } } + pub fn all_font_families(&self) -> Vec { + let mut families = self.platform_text_system.all_font_families(); + families.append( + &mut self + .fallback_font_stack + .iter() + .map(|font| font.family.to_string()) + .collect(), + ); + families + } pub fn add_fonts(&self, fonts: &[Arc>]) -> Result<()> { self.platform_text_system.add_fonts(fonts) } @@ -186,6 +197,10 @@ impl TextSystem { } } + pub fn with_view(&self, view_id: EntityId, f: impl FnOnce() -> R) -> R { + self.line_layout_cache.with_view(view_id, f) + } + pub fn layout_line( &self, text: &str, @@ -360,8 +375,8 @@ impl TextSystem { Ok(lines) } - pub fn start_frame(&self) { - self.line_layout_cache.start_frame() + pub fn finish_frame(&self, reused_views: &FxHashSet) { + self.line_layout_cache.finish_frame(reused_views) } pub fn line_wrapper(self: &Arc, font: Font, font_size: Pixels) -> LineWrapperHandle { diff --git a/crates/gpui/src/text_system/line_layout.rs b/crates/gpui/src/text_system/line_layout.rs index 6506d7794c7b4693e61af6d278c06a191528f448..6c466f9680e8ec8ff75812bcdf15a172c9ff6258 100644 --- a/crates/gpui/src/text_system/line_layout.rs +++ b/crates/gpui/src/text_system/line_layout.rs @@ -1,5 +1,5 @@ -use crate::{px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, Size}; -use collections::FxHashMap; +use crate::{px, EntityId, FontId, GlyphId, Pixels, PlatformTextSystem, Point, Size}; +use collections::{FxHashMap, FxHashSet}; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; use smallvec::SmallVec; use std::{ @@ -236,6 +236,7 @@ impl WrappedLineLayout { } pub(crate) struct LineLayoutCache { + view_stack: Mutex>, previous_frame: Mutex>>, current_frame: RwLock>>, previous_frame_wrapped: Mutex>>, @@ -246,6 +247,7 @@ pub(crate) struct LineLayoutCache { impl LineLayoutCache { pub fn new(platform_text_system: Arc) -> Self { Self { + view_stack: Mutex::default(), previous_frame: Mutex::default(), current_frame: RwLock::default(), previous_frame_wrapped: Mutex::default(), @@ -254,11 +256,43 @@ impl LineLayoutCache { } } - pub fn start_frame(&self) { + pub fn finish_frame(&self, reused_views: &FxHashSet) { + debug_assert_eq!(self.view_stack.lock().len(), 0); + let mut prev_frame = self.previous_frame.lock(); let mut curr_frame = self.current_frame.write(); + for (key, layout) in prev_frame.drain() { + if key + .parent_view_id + .map_or(false, |view_id| reused_views.contains(&view_id)) + { + curr_frame.insert(key, layout); + } + } std::mem::swap(&mut *prev_frame, &mut *curr_frame); - curr_frame.clear(); + + let mut prev_frame_wrapped = self.previous_frame_wrapped.lock(); + let mut curr_frame_wrapped = self.current_frame_wrapped.write(); + for (key, layout) in prev_frame_wrapped.drain() { + if key + .parent_view_id + .map_or(false, |view_id| reused_views.contains(&view_id)) + { + curr_frame_wrapped.insert(key, layout); + } + } + std::mem::swap(&mut *prev_frame_wrapped, &mut *curr_frame_wrapped); + } + + pub fn with_view(&self, view_id: EntityId, f: impl FnOnce() -> R) -> R { + self.view_stack.lock().push(view_id); + let result = f(); + self.view_stack.lock().pop(); + result + } + + fn parent_view_id(&self) -> Option { + self.view_stack.lock().last().copied() } pub fn layout_wrapped_line( @@ -273,6 +307,7 @@ impl LineLayoutCache { font_size, runs, wrap_width, + parent_view_id: self.parent_view_id(), } as &dyn AsCacheKeyRef; let current_frame = self.current_frame_wrapped.upgradable_read(); @@ -301,6 +336,7 @@ impl LineLayoutCache { font_size, runs: SmallVec::from(runs), wrap_width, + parent_view_id: self.parent_view_id(), }; current_frame.insert(key, layout.clone()); layout @@ -313,6 +349,7 @@ impl LineLayoutCache { font_size, runs, wrap_width: None, + parent_view_id: self.parent_view_id(), } as &dyn AsCacheKeyRef; let current_frame = self.current_frame.upgradable_read(); @@ -331,6 +368,7 @@ impl LineLayoutCache { font_size, runs: SmallVec::from(runs), wrap_width: None, + parent_view_id: self.parent_view_id(), }; current_frame.insert(key, layout.clone()); layout @@ -348,12 +386,13 @@ trait AsCacheKeyRef { fn as_cache_key_ref(&self) -> CacheKeyRef; } -#[derive(Eq)] +#[derive(Debug, Eq)] struct CacheKey { text: String, font_size: Pixels, runs: SmallVec<[FontRun; 1]>, wrap_width: Option, + parent_view_id: Option, } #[derive(Copy, Clone, PartialEq, Eq, Hash)] @@ -362,6 +401,7 @@ struct CacheKeyRef<'a> { font_size: Pixels, runs: &'a [FontRun], wrap_width: Option, + parent_view_id: Option, } impl<'a> PartialEq for (dyn AsCacheKeyRef + 'a) { @@ -385,6 +425,7 @@ impl AsCacheKeyRef for CacheKey { font_size: self.font_size, runs: self.runs.as_slice(), wrap_width: self.wrap_width, + parent_view_id: self.parent_view_id, } } } diff --git a/crates/gpui/src/view.rs b/crates/gpui/src/view.rs index 4472da02e71fda1bb17d4353056b67ad58639813..968fbbd94cd142bdfc6629539da997652f71d91c 100644 --- a/crates/gpui/src/view.rs +++ b/crates/gpui/src/view.rs @@ -1,8 +1,8 @@ use crate::{ seal::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, BorrowWindow, - Bounds, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, IntoElement, - LayoutId, Model, Pixels, Point, Render, Size, ViewContext, VisualContext, WeakModel, - WindowContext, + Bounds, ContentMask, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, + IntoElement, LayoutId, Model, Pixels, Point, Render, Size, StackingOrder, Style, TextStyle, + ViewContext, VisualContext, WeakModel, WindowContext, }; use anyhow::{Context, Result}; use std::{ @@ -17,6 +17,19 @@ pub struct View { impl Sealed for View {} +pub struct AnyViewState { + root_style: Style, + cache_key: Option, + element: Option, +} + +struct ViewCacheKey { + bounds: Bounds, + stacking_order: StackingOrder, + content_mask: ContentMask, + text_style: TextStyle, +} + impl Entity for View { type Weak = WeakView; @@ -60,16 +73,6 @@ impl View { self.model.read(cx) } - // pub fn render_with(&self, component: E) -> RenderViewWith - // where - // E: 'static + Element, - // { - // RenderViewWith { - // view: self.clone(), - // element: Some(component), - // } - // } - pub fn focus_handle(&self, cx: &AppContext) -> FocusHandle where V: FocusableView, @@ -86,13 +89,15 @@ impl Element for View { _state: Option, cx: &mut WindowContext, ) -> (LayoutId, Self::State) { - let mut element = self.update(cx, |view, cx| view.render(cx).into_any_element()); - let layout_id = element.request_layout(cx); - (layout_id, Some(element)) + cx.with_view_id(self.entity_id(), |cx| { + let mut element = self.update(cx, |view, cx| view.render(cx).into_any_element()); + let layout_id = element.request_layout(cx); + (layout_id, Some(element)) + }) } fn paint(&mut self, _: Bounds, element: &mut Self::State, cx: &mut WindowContext) { - element.take().unwrap().paint(cx); + cx.paint_view(self.entity_id(), |cx| element.take().unwrap().paint(cx)); } } @@ -183,16 +188,20 @@ impl Eq for WeakView {} #[derive(Clone, Debug)] pub struct AnyView { model: AnyModel, - layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement), - paint: fn(&AnyView, &mut AnyElement, &mut WindowContext), + request_layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement), + cache: bool, } impl AnyView { + pub fn cached(mut self) -> Self { + self.cache = true; + self + } + pub fn downgrade(&self) -> AnyWeakView { AnyWeakView { model: self.model.downgrade(), - layout: self.layout, - paint: self.paint, + layout: self.request_layout, } } @@ -201,8 +210,8 @@ impl AnyView { Ok(model) => Ok(View { model }), Err(model) => Err(Self { model, - layout: self.layout, - paint: self.paint, + request_layout: self.request_layout, + cache: self.cache, }), } } @@ -221,10 +230,12 @@ impl AnyView { available_space: Size, cx: &mut WindowContext, ) { - cx.with_absolute_element_offset(origin, |cx| { - let (layout_id, mut rendered_element) = (self.layout)(self, cx); - cx.compute_layout(layout_id, available_space); - (self.paint)(self, &mut rendered_element, cx); + cx.paint_view(self.entity_id(), |cx| { + cx.with_absolute_element_offset(origin, |cx| { + let (layout_id, mut rendered_element) = (self.request_layout)(self, cx); + cx.compute_layout(layout_id, available_space); + rendered_element.paint(cx) + }); }) } } @@ -233,30 +244,72 @@ impl From> for AnyView { fn from(value: View) -> Self { AnyView { model: value.model.into_any(), - layout: any_view::layout::, - paint: any_view::paint, + request_layout: any_view::request_layout::, + cache: false, } } } impl Element for AnyView { - type State = Option; + type State = AnyViewState; fn request_layout( &mut self, - _state: Option, + state: Option, cx: &mut WindowContext, ) -> (LayoutId, Self::State) { - let (layout_id, state) = (self.layout)(self, cx); - (layout_id, Some(state)) + cx.with_view_id(self.entity_id(), |cx| { + if self.cache { + if let Some(state) = state { + let layout_id = cx.request_layout(&state.root_style, None); + return (layout_id, state); + } + } + + let (layout_id, element) = (self.request_layout)(self, cx); + let root_style = cx.layout_style(layout_id).unwrap().clone(); + let state = AnyViewState { + root_style, + cache_key: None, + element: Some(element), + }; + (layout_id, state) + }) } - fn paint(&mut self, _: Bounds, state: &mut Self::State, cx: &mut WindowContext) { - debug_assert!( - state.is_some(), - "state is None. Did you include an AnyView twice in the tree?" - ); - (self.paint)(self, state.as_mut().unwrap(), cx) + fn paint(&mut self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext) { + cx.paint_view(self.entity_id(), |cx| { + if !self.cache { + state.element.take().unwrap().paint(cx); + return; + } + + if let Some(cache_key) = state.cache_key.as_mut() { + if cache_key.bounds == bounds + && cache_key.content_mask == cx.content_mask() + && cache_key.stacking_order == *cx.stacking_order() + && cache_key.text_style == cx.text_style() + && !cx.window.dirty_views.contains(&self.entity_id()) + && !cx.window.refreshing + { + cx.reuse_view(); + return; + } + } + + let mut element = state + .element + .take() + .unwrap_or_else(|| (self.request_layout)(self, cx).1); + element.draw(bounds.origin, bounds.size.into(), cx); + + state.cache_key = Some(ViewCacheKey { + bounds, + stacking_order: cx.stacking_order().clone(), + content_mask: cx.content_mask(), + text_style: cx.text_style(), + }); + }) } } @@ -287,7 +340,6 @@ impl IntoElement for AnyView { pub struct AnyWeakView { model: AnyWeakModel, layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement), - paint: fn(&AnyView, &mut AnyElement, &mut WindowContext), } impl AnyWeakView { @@ -295,8 +347,8 @@ impl AnyWeakView { let model = self.model.upgrade()?; Some(AnyView { model, - layout: self.layout, - paint: self.paint, + request_layout: self.layout, + cache: false, }) } } @@ -305,8 +357,7 @@ impl From> for AnyWeakView { fn from(view: WeakView) -> Self { Self { model: view.model.into(), - layout: any_view::layout::, - paint: any_view::paint, + layout: any_view::request_layout::, } } } @@ -328,7 +379,7 @@ impl std::fmt::Debug for AnyWeakView { mod any_view { use crate::{AnyElement, AnyView, IntoElement, LayoutId, Render, WindowContext}; - pub(crate) fn layout( + pub(crate) fn request_layout( view: &AnyView, cx: &mut WindowContext, ) -> (LayoutId, AnyElement) { @@ -337,8 +388,4 @@ mod any_view { let layout_id = element.request_layout(cx); (layout_id, element) } - - pub(crate) fn paint(_view: &AnyView, element: &mut AnyElement, cx: &mut WindowContext) { - element.paint(cx); - } } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 509a6d8466609b5041f4f958ca1676107b639a14..869d6b18268cc64bc18f9187dd34e8032237d28b 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1,7 +1,7 @@ #![deny(missing_docs)] use crate::{ - px, size, transparent_black, Action, AnyDrag, AnyView, AppContext, Arena, ArenaBox, ArenaRef, + px, size, transparent_black, Action, AnyDrag, AnyTooltip, AnyView, AppContext, Arena, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle, DevicePixels, DispatchActionListener, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten, FontId, GlobalElementId, GlyphId, Hsla, @@ -9,12 +9,12 @@ use crate::{ Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, - RenderSvgParams, ScaledPixels, Scene, SceneBuilder, Shadow, SharedString, Size, Style, - SubscriberSet, Subscription, Surface, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, - VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, + RenderSvgParams, ScaledPixels, Scene, Shadow, SharedString, Size, Style, SubscriberSet, + Subscription, Surface, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, + WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, }; use anyhow::{anyhow, Context as _, Result}; -use collections::FxHashMap; +use collections::{FxHashMap, FxHashSet}; use derive_more::{Deref, DerefMut}; use futures::{ channel::{mpsc, oneshot}, @@ -99,7 +99,7 @@ impl DispatchPhase { } type AnyObserver = Box bool + 'static>; -type AnyMouseListener = ArenaBox; +type AnyMouseListener = Box; type AnyWindowFocusListener = Box bool + 'static>; struct FocusEvent { @@ -266,19 +266,19 @@ pub struct Window { pub(crate) element_id_stack: GlobalElementId, pub(crate) rendered_frame: Frame, pub(crate) next_frame: Frame, - frame_arena: Arena, + pub(crate) dirty_views: FxHashSet, pub(crate) focus_handles: Arc>>, focus_listeners: SubscriberSet<(), AnyWindowFocusListener>, focus_lost_listeners: SubscriberSet<(), AnyObserver>, default_prevented: bool, mouse_position: Point, modifiers: Modifiers, - requested_cursor_style: Option, scale_factor: f32, bounds: WindowBounds, bounds_observers: SubscriberSet<(), AnyObserver>, active: bool, pub(crate) dirty: bool, + pub(crate) refreshing: bool, pub(crate) drawing: bool, activation_observers: SubscriberSet<(), AnyObserver>, pub(crate) focus: Option, @@ -290,22 +290,39 @@ pub struct Window { pub(crate) struct ElementStateBox { inner: Box, + parent_view_id: EntityId, #[cfg(debug_assertions)] type_name: &'static str, } +struct RequestedInputHandler { + view_id: EntityId, + handler: Option>, +} + +struct TooltipRequest { + view_id: EntityId, + tooltip: AnyTooltip, +} + pub(crate) struct Frame { focus: Option, window_active: bool, pub(crate) element_states: FxHashMap, - mouse_listeners: FxHashMap>, + mouse_listeners: FxHashMap>, pub(crate) dispatch_tree: DispatchTree, - pub(crate) scene_builder: SceneBuilder, - pub(crate) depth_map: Vec<(StackingOrder, Bounds)>, + pub(crate) scene: Scene, + pub(crate) depth_map: Vec<(StackingOrder, EntityId, Bounds)>, pub(crate) z_index_stack: StackingOrder, pub(crate) next_stacking_order_id: u32, content_mask_stack: Vec>, element_offset_stack: Vec>, + requested_input_handler: Option, + tooltip_request: Option, + cursor_styles: FxHashMap, + requested_cursor_style: Option, + pub(crate) view_stack: Vec, + pub(crate) reused_views: FxHashSet, } impl Frame { @@ -316,12 +333,18 @@ impl Frame { element_states: FxHashMap::default(), mouse_listeners: FxHashMap::default(), dispatch_tree, - scene_builder: SceneBuilder::default(), + scene: Scene::default(), + depth_map: Vec::new(), z_index_stack: StackingOrder::default(), next_stacking_order_id: 0, - depth_map: Default::default(), content_mask_stack: Vec::new(), element_offset_stack: Vec::new(), + requested_input_handler: None, + tooltip_request: None, + cursor_styles: FxHashMap::default(), + requested_cursor_style: None, + view_stack: Vec::new(), + reused_views: FxHashSet::default(), } } @@ -331,6 +354,13 @@ impl Frame { self.dispatch_tree.clear(); self.depth_map.clear(); self.next_stacking_order_id = 0; + self.reused_views.clear(); + self.scene.clear(); + self.requested_input_handler.take(); + self.tooltip_request.take(); + self.cursor_styles.clear(); + self.requested_cursor_style.take(); + debug_assert_eq!(self.view_stack.len(), 0); } fn focus_path(&self) -> SmallVec<[FocusId; 8]> { @@ -338,6 +368,42 @@ impl Frame { .map(|focus_id| self.dispatch_tree.focus_path(focus_id)) .unwrap_or_default() } + + fn finish(&mut self, prev_frame: &mut Self) { + // Reuse mouse listeners that didn't change since the last frame. + for (type_id, listeners) in &mut prev_frame.mouse_listeners { + let next_listeners = self.mouse_listeners.entry(*type_id).or_default(); + for (order, view_id, listener) in listeners.drain(..) { + if self.reused_views.contains(&view_id) { + next_listeners.push((order, view_id, listener)); + } + } + } + + // Reuse entries in the depth map that didn't change since the last frame. + for (order, view_id, bounds) in prev_frame.depth_map.drain(..) { + if self.reused_views.contains(&view_id) { + match self + .depth_map + .binary_search_by(|(level, _, _)| order.cmp(level)) + { + Ok(i) | Err(i) => self.depth_map.insert(i, (order, view_id, bounds)), + } + } + } + + // Retain element states for views that didn't change since the last frame. + for (element_id, state) in prev_frame.element_states.drain() { + if self.reused_views.contains(&state.parent_view_id) { + self.element_states.entry(element_id).or_insert(state); + } + } + + // Reuse geometry that didn't change since the last frame. + self.scene + .reuse_views(&self.reused_views, &mut prev_frame.scene); + self.scene.finish(); + } } impl Window { @@ -346,14 +412,7 @@ impl Window { options: WindowOptions, cx: &mut AppContext, ) -> Self { - let platform_window = cx.platform.open_window( - handle, - options, - Box::new({ - let mut cx = cx.to_async(); - move || handle.update(&mut cx, |_, cx| cx.draw()) - }), - ); + let platform_window = cx.platform.open_window(handle, options); let display_id = platform_window.display().id(); let sprite_atlas = platform_window.sprite_atlas(); let mouse_position = platform_window.mouse_position(); @@ -362,6 +421,12 @@ impl Window { let scale_factor = platform_window.scale_factor(); let bounds = platform_window.bounds(); + platform_window.on_request_frame(Box::new({ + let mut cx = cx.to_async(); + move || { + handle.update(&mut cx, |_, cx| cx.draw()).log_err(); + } + })); platform_window.on_resize(Box::new({ let mut cx = cx.to_async(); move |_, _| { @@ -416,19 +481,19 @@ impl Window { element_id_stack: GlobalElementId::default(), rendered_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())), next_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())), - frame_arena: Arena::new(1024 * 1024), + dirty_views: FxHashSet::default(), focus_handles: Arc::new(RwLock::new(SlotMap::with_key())), focus_listeners: SubscriberSet::new(), focus_lost_listeners: SubscriberSet::new(), default_prevented: true, mouse_position, modifiers, - requested_cursor_style: None, scale_factor, bounds, bounds_observers: SubscriberSet::new(), active: false, dirty: false, + refreshing: false, drawing: false, activation_observers: SubscriberSet::new(), focus: None, @@ -484,8 +549,9 @@ impl<'a> WindowContext<'a> { } /// Mark the window as dirty, scheduling it to be redrawn on the next frame. - pub fn notify(&mut self) { + pub fn refresh(&mut self) { if !self.window.drawing { + self.window.refreshing = true; self.window.dirty = true; } } @@ -525,7 +591,7 @@ impl<'a> WindowContext<'a> { self.window.focus_invalidated = true; } - self.notify(); + self.refresh(); } /// Remove focus from all elements within this context's window. @@ -535,7 +601,7 @@ impl<'a> WindowContext<'a> { } self.window.focus = None; - self.notify(); + self.refresh(); } /// Blur the window and don't allow anything in it to be focused again. @@ -772,6 +838,14 @@ impl<'a> WindowContext<'a> { .request_measured_layout(style, rem_size, measure) } + pub(crate) fn layout_style(&self, layout_id: LayoutId) -> Option<&Style> { + self.window + .layout_engine + .as_ref() + .unwrap() + .requested_style(layout_id) + } + /// Compute the layout for the given id within the given available space. /// This method is called for its side effect, typically by the framework prior to painting. /// After calling it, you can request the bounds of the given layout node id or any descendant. @@ -801,7 +875,7 @@ impl<'a> WindowContext<'a> { self.window.viewport_size = self.window.platform_window.content_size(); self.window.bounds = self.window.platform_window.bounds(); self.window.display_id = self.window.platform_window.display().id(); - self.notify(); + self.refresh(); self.window .bounds_observers @@ -898,22 +972,22 @@ impl<'a> WindowContext<'a> { &mut self, mut handler: impl FnMut(&Event, DispatchPhase, &mut WindowContext) + 'static, ) { + let view_id = self.parent_view_id(); let order = self.window.next_frame.z_index_stack.clone(); - let handler = self - .window - .frame_arena - .alloc(|| { - move |event: &dyn Any, phase: DispatchPhase, cx: &mut WindowContext<'_>| { - handler(event.downcast_ref().unwrap(), phase, cx) - } - }) - .map(|handler| handler as _); self.window .next_frame .mouse_listeners .entry(TypeId::of::()) .or_default() - .push((order, handler)) + .push(( + order, + view_id, + Box::new( + move |event: &dyn Any, phase: DispatchPhase, cx: &mut WindowContext<'_>| { + handler(event.downcast_ref().unwrap(), phase, cx) + }, + ), + )) } /// Register a key event listener on the window for the next frame. The type of event @@ -926,21 +1000,13 @@ impl<'a> WindowContext<'a> { &mut self, listener: impl Fn(&Event, DispatchPhase, &mut WindowContext) + 'static, ) { - let listener = self - .window - .frame_arena - .alloc(|| { - move |event: &dyn Any, phase, cx: &mut WindowContext<'_>| { - if let Some(event) = event.downcast_ref::() { - listener(event, phase, cx) - } + self.window.next_frame.dispatch_tree.on_key_event(Rc::new( + move |event: &dyn Any, phase, cx: &mut WindowContext<'_>| { + if let Some(event) = event.downcast_ref::() { + listener(event, phase, cx) } - }) - .map(|handler| handler as _); - self.window - .next_frame - .dispatch_tree - .on_key_event(ArenaRef::from(listener)); + }, + )); } /// Register an action listener on the window for the next frame. The type of action @@ -954,15 +1020,10 @@ impl<'a> WindowContext<'a> { action_type: TypeId, listener: impl Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static, ) { - let listener = self - .window - .frame_arena - .alloc(|| listener) - .map(|handler| handler as _); self.window .next_frame .dispatch_tree - .on_action(action_type, ArenaRef::from(listener)); + .on_action(action_type, Rc::new(listener)); } /// Determine whether the given action is available along the dispatch path to the currently focused element. @@ -994,15 +1055,24 @@ impl<'a> WindowContext<'a> { /// Update the cursor style at the platform level. pub fn set_cursor_style(&mut self, style: CursorStyle) { - self.window.requested_cursor_style = Some(style) + let view_id = self.parent_view_id(); + self.window.next_frame.cursor_styles.insert(view_id, style); + self.window.next_frame.requested_cursor_style = Some(style); + } + + /// Set a tooltip to be rendered for the upcoming frame + pub fn set_tooltip(&mut self, tooltip: AnyTooltip) { + let view_id = self.parent_view_id(); + self.window.next_frame.tooltip_request = Some(TooltipRequest { view_id, tooltip }); } /// Called during painting to track which z-index is on top at each pixel position pub fn add_opaque_layer(&mut self, bounds: Bounds) { let stacking_order = self.window.next_frame.z_index_stack.clone(); + let view_id = self.parent_view_id(); let depth_map = &mut self.window.next_frame.depth_map; - match depth_map.binary_search_by(|(level, _)| stacking_order.cmp(level)) { - Ok(i) | Err(i) => depth_map.insert(i, (stacking_order, bounds)), + match depth_map.binary_search_by(|(level, _, _)| stacking_order.cmp(level)) { + Ok(i) | Err(i) => depth_map.insert(i, (stacking_order, view_id, bounds)), } } @@ -1010,7 +1080,7 @@ impl<'a> WindowContext<'a> { /// on top of the given level. Layers whose level is an extension of the /// level are not considered to be on top of the level. pub fn was_top_layer(&self, point: &Point, level: &StackingOrder) -> bool { - for (opaque_level, bounds) in self.window.rendered_frame.depth_map.iter() { + for (opaque_level, _, bounds) in self.window.rendered_frame.depth_map.iter() { if level >= opaque_level { break; } @@ -1027,7 +1097,7 @@ impl<'a> WindowContext<'a> { point: &Point, level: &StackingOrder, ) -> bool { - for (opaque_level, bounds) in self.window.rendered_frame.depth_map.iter() { + for (opaque_level, _, bounds) in self.window.rendered_frame.depth_map.iter() { if level >= opaque_level { break; } @@ -1056,14 +1126,17 @@ impl<'a> WindowContext<'a> { ) { let scale_factor = self.scale_factor(); let content_mask = self.content_mask(); + let view_id = self.parent_view_id(); let window = &mut *self.window; for shadow in shadows { let mut shadow_bounds = bounds; shadow_bounds.origin += shadow.offset; shadow_bounds.dilate(shadow.spread_radius); - window.next_frame.scene_builder.insert( + window.next_frame.scene.insert( &window.next_frame.z_index_stack, Shadow { + view_id: view_id.into(), + layer_id: 0, order: 0, bounds: shadow_bounds.scale(scale_factor), content_mask: content_mask.scale(scale_factor), @@ -1081,11 +1154,14 @@ impl<'a> WindowContext<'a> { pub fn paint_quad(&mut self, quad: PaintQuad) { let scale_factor = self.scale_factor(); let content_mask = self.content_mask(); + let view_id = self.parent_view_id(); let window = &mut *self.window; - window.next_frame.scene_builder.insert( + window.next_frame.scene.insert( &window.next_frame.z_index_stack, Quad { + view_id: view_id.into(), + layer_id: 0, order: 0, bounds: quad.bounds.scale(scale_factor), content_mask: content_mask.scale(scale_factor), @@ -1101,12 +1177,15 @@ impl<'a> WindowContext<'a> { pub fn paint_path(&mut self, mut path: Path, color: impl Into) { let scale_factor = self.scale_factor(); let content_mask = self.content_mask(); + let view_id = self.parent_view_id(); + path.content_mask = content_mask; path.color = color.into(); + path.view_id = view_id.into(); let window = &mut *self.window; window .next_frame - .scene_builder + .scene .insert(&window.next_frame.z_index_stack, path.scale(scale_factor)); } @@ -1128,10 +1207,14 @@ impl<'a> WindowContext<'a> { size: size(width, height), }; let content_mask = self.content_mask(); + let view_id = self.parent_view_id(); + let window = &mut *self.window; - window.next_frame.scene_builder.insert( + window.next_frame.scene.insert( &window.next_frame.z_index_stack, Underline { + view_id: view_id.into(), + layer_id: 0, order: 0, bounds: bounds.scale(scale_factor), content_mask: content_mask.scale(scale_factor), @@ -1181,10 +1264,13 @@ impl<'a> WindowContext<'a> { size: tile.bounds.size.map(Into::into), }; let content_mask = self.content_mask().scale(scale_factor); + let view_id = self.parent_view_id(); let window = &mut *self.window; - window.next_frame.scene_builder.insert( + window.next_frame.scene.insert( &window.next_frame.z_index_stack, MonochromeSprite { + view_id: view_id.into(), + layer_id: 0, order: 0, bounds, content_mask, @@ -1231,11 +1317,14 @@ impl<'a> WindowContext<'a> { size: tile.bounds.size.map(Into::into), }; let content_mask = self.content_mask().scale(scale_factor); + let view_id = self.parent_view_id(); let window = &mut *self.window; - window.next_frame.scene_builder.insert( + window.next_frame.scene.insert( &window.next_frame.z_index_stack, PolychromeSprite { + view_id: view_id.into(), + layer_id: 0, order: 0, bounds, corner_radii: Default::default(), @@ -1273,11 +1362,14 @@ impl<'a> WindowContext<'a> { Ok((params.size, Cow::Owned(bytes))) })?; let content_mask = self.content_mask().scale(scale_factor); + let view_id = self.parent_view_id(); let window = &mut *self.window; - window.next_frame.scene_builder.insert( + window.next_frame.scene.insert( &window.next_frame.z_index_stack, MonochromeSprite { + view_id: view_id.into(), + layer_id: 0, order: 0, bounds, content_mask, @@ -1309,11 +1401,14 @@ impl<'a> WindowContext<'a> { })?; let content_mask = self.content_mask().scale(scale_factor); let corner_radii = corner_radii.scale(scale_factor); + let view_id = self.parent_view_id(); let window = &mut *self.window; - window.next_frame.scene_builder.insert( + window.next_frame.scene.insert( &window.next_frame.z_index_stack, PolychromeSprite { + view_id: view_id.into(), + layer_id: 0, order: 0, bounds, content_mask, @@ -1330,10 +1425,13 @@ impl<'a> WindowContext<'a> { let scale_factor = self.scale_factor(); let bounds = bounds.scale(scale_factor); let content_mask = self.content_mask().scale(scale_factor); + let view_id = self.parent_view_id(); let window = &mut *self.window; - window.next_frame.scene_builder.insert( + window.next_frame.scene.insert( &window.next_frame.z_index_stack, Surface { + view_id: view_id.into(), + layer_id: 0, order: 0, bounds, content_mask, @@ -1342,8 +1440,50 @@ impl<'a> WindowContext<'a> { ); } + pub(crate) fn reuse_view(&mut self) { + let view_id = self.parent_view_id(); + let grafted_view_ids = self + .window + .next_frame + .dispatch_tree + .reuse_view(view_id, &mut self.window.rendered_frame.dispatch_tree); + for view_id in grafted_view_ids { + assert!(self.window.next_frame.reused_views.insert(view_id)); + + // Reuse the previous input handler requested during painting of the reused view. + if self + .window + .rendered_frame + .requested_input_handler + .as_ref() + .map_or(false, |requested| requested.view_id == view_id) + { + self.window.next_frame.requested_input_handler = + self.window.rendered_frame.requested_input_handler.take(); + } + + // Reuse the tooltip previously requested during painting of the reused view. + if self + .window + .rendered_frame + .tooltip_request + .as_ref() + .map_or(false, |requested| requested.view_id == view_id) + { + self.window.next_frame.tooltip_request = + self.window.rendered_frame.tooltip_request.take(); + } + + // Reuse the cursor styles previously requested during painting of the reused view. + if let Some(style) = self.window.rendered_frame.cursor_styles.remove(&view_id) { + self.window.next_frame.cursor_styles.insert(view_id, style); + self.window.next_frame.requested_cursor_style = Some(style); + } + } + } + /// Draw pixels to the display for this window based on the contents of its scene. - pub(crate) fn draw(&mut self) -> Scene { + pub(crate) fn draw(&mut self) { self.window.dirty = false; self.window.drawing = true; @@ -1352,30 +1492,23 @@ impl<'a> WindowContext<'a> { self.window.focus_invalidated = false; } - self.text_system().start_frame(); - self.window.platform_window.clear_input_handler(); - self.window.layout_engine.as_mut().unwrap().clear(); - self.window.next_frame.clear(); - self.window.frame_arena.clear(); + if let Some(requested_handler) = self.window.rendered_frame.requested_input_handler.as_mut() + { + requested_handler.handler = self.window.platform_window.take_input_handler(); + } + let root_view = self.window.root_view.take().unwrap(); self.with_z_index(0, |cx| { cx.with_key_dispatch(Some(KeyContext::default()), None, |_, cx| { for (action_type, action_listeners) in &cx.app.global_action_listeners { for action_listener in action_listeners.iter().cloned() { - let listener = cx - .window - .frame_arena - .alloc(|| { - move |action: &dyn Any, phase, cx: &mut WindowContext<'_>| { - action_listener(action, phase, cx) - } - }) - .map(|listener| listener as _); - cx.window - .next_frame - .dispatch_tree - .on_action(*action_type, ArenaRef::from(listener)) + cx.window.next_frame.dispatch_tree.on_action( + *action_type, + Rc::new(move |action: &dyn Any, phase, cx: &mut WindowContext<'_>| { + action_listener(action, phase, cx) + }), + ) } } @@ -1391,14 +1524,18 @@ impl<'a> WindowContext<'a> { active_drag.view.draw(offset, available_space, cx); }); self.active_drag = Some(active_drag); - } else if let Some(active_tooltip) = self.app.active_tooltip.take() { + } else if let Some(tooltip_request) = self.window.next_frame.tooltip_request.take() { self.with_z_index(1, |cx| { let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); - active_tooltip - .view - .draw(active_tooltip.cursor_offset, available_space, cx); + tooltip_request.tooltip.view.draw( + tooltip_request.tooltip.cursor_offset, + available_space, + cx, + ); }); + self.window.next_frame.tooltip_request = Some(tooltip_request); } + self.window.dirty_views.clear(); self.window .next_frame @@ -1411,17 +1548,10 @@ impl<'a> WindowContext<'a> { self.window.next_frame.window_active = self.window.active; self.window.root_view = Some(root_view); - let previous_focus_path = self.window.rendered_frame.focus_path(); - let previous_window_active = self.window.rendered_frame.window_active; - mem::swap(&mut self.window.rendered_frame, &mut self.window.next_frame); - let current_focus_path = self.window.rendered_frame.focus_path(); - let current_window_active = self.window.rendered_frame.window_active; - - let scene = self.window.rendered_frame.scene_builder.build(); - // Set the cursor only if we're the active window. let cursor_style = self .window + .next_frame .requested_cursor_style .take() .unwrap_or(CursorStyle::Arrow); @@ -1429,6 +1559,28 @@ impl<'a> WindowContext<'a> { self.platform.set_cursor_style(cursor_style); } + // Register requested input handler with the platform window. + if let Some(requested_input) = self.window.next_frame.requested_input_handler.as_mut() { + if let Some(handler) = requested_input.handler.take() { + self.window.platform_window.set_input_handler(handler); + } + } + + self.window.layout_engine.as_mut().unwrap().clear(); + self.text_system() + .finish_frame(&self.window.next_frame.reused_views); + self.window + .next_frame + .finish(&mut self.window.rendered_frame); + ELEMENT_ARENA.with_borrow_mut(|element_arena| element_arena.clear()); + + let previous_focus_path = self.window.rendered_frame.focus_path(); + let previous_window_active = self.window.rendered_frame.window_active; + mem::swap(&mut self.window.rendered_frame, &mut self.window.next_frame); + self.window.next_frame.clear(); + let current_focus_path = self.window.rendered_frame.focus_path(); + let current_window_active = self.window.rendered_frame.window_active; + if previous_focus_path != current_focus_path || previous_window_active != current_window_active { @@ -1457,10 +1609,11 @@ impl<'a> WindowContext<'a> { .retain(&(), |listener| listener(&event, self)); } + self.window + .platform_window + .draw(&self.window.rendered_frame.scene); + self.window.refreshing = false; self.window.drawing = false; - ELEMENT_ARENA.with_borrow_mut(|element_arena| element_arena.clear()); - - scene } /// Dispatch a mouse or keyboard event on the window. @@ -1564,11 +1717,11 @@ impl<'a> WindowContext<'a> { .remove(&event.type_id()) { // Because handlers may add other handlers, we sort every time. - handlers.sort_by(|(a, _), (b, _)| a.cmp(b)); + handlers.sort_by(|(a, _, _), (b, _, _)| a.cmp(b)); // Capture phase, events bubble from back to front. Handlers for this phase are used for // special purposes, such as detecting events outside of a given Bounds. - for (_, handler) in &mut handlers { + for (_, _, handler) in &mut handlers { handler(event, DispatchPhase::Capture, self); if !self.app.propagate_event { break; @@ -1577,7 +1730,7 @@ impl<'a> WindowContext<'a> { // Bubble phase, where most normal handlers do their work. if self.app.propagate_event { - for (_, handler) in handlers.iter_mut().rev() { + for (_, _, handler) in handlers.iter_mut().rev() { handler(event, DispatchPhase::Bubble, self); if !self.app.propagate_event { break; @@ -1595,12 +1748,12 @@ impl<'a> WindowContext<'a> { if event.is::() { // If this was a mouse move event, redraw the window so that the // active drag can follow the mouse cursor. - self.notify(); + self.refresh(); } else if event.is::() { // If this was a mouse up event, cancel the active drag and redraw // the window. self.active_drag = None; - self.notify(); + self.refresh(); } } } @@ -1867,13 +2020,12 @@ impl<'a> WindowContext<'a> { f: impl FnOnce(Option, &mut Self) -> R, ) -> R { let window = &mut self.window; - window.next_frame.dispatch_tree.push_node(context.clone()); - if let Some(focus_handle) = focus_handle.as_ref() { - window - .next_frame - .dispatch_tree - .make_focusable(focus_handle.id); - } + let focus_id = focus_handle.as_ref().map(|handle| handle.id); + window + .next_frame + .dispatch_tree + .push_node(context.clone(), focus_id, None); + let result = f(focus_handle, self); self.window.next_frame.dispatch_tree.pop_node(); @@ -1881,9 +2033,149 @@ impl<'a> WindowContext<'a> { result } + /// Invoke the given function with the given view id present on the view stack. + /// This is a fairly low-level method used to layout views. + pub fn with_view_id(&mut self, view_id: EntityId, f: impl FnOnce(&mut Self) -> R) -> R { + let text_system = self.text_system().clone(); + text_system.with_view(view_id, || { + if self.window.next_frame.view_stack.last() == Some(&view_id) { + return f(self); + } else { + self.window.next_frame.view_stack.push(view_id); + let result = f(self); + self.window.next_frame.view_stack.pop(); + result + } + }) + } + + /// Invoke the given function with the given view id present on the view stack. + /// This is a fairly low-level method used to paint views. + pub fn paint_view(&mut self, view_id: EntityId, f: impl FnOnce(&mut Self) -> R) -> R { + let text_system = self.text_system().clone(); + text_system.with_view(view_id, || { + if self.window.next_frame.view_stack.last() == Some(&view_id) { + return f(self); + } else { + self.window.next_frame.view_stack.push(view_id); + self.window + .next_frame + .dispatch_tree + .push_node(None, None, Some(view_id)); + let result = f(self); + self.window.next_frame.dispatch_tree.pop_node(); + self.window.next_frame.view_stack.pop(); + result + } + }) + } + + /// Update or initialize state for an element with the given id that lives across multiple + /// frames. If an element with this id existed in the rendered frame, its state will be passed + /// to the given closure. The state returned by the closure will be stored so it can be referenced + /// when drawing the next frame. + pub(crate) fn with_element_state( + &mut self, + id: ElementId, + f: impl FnOnce(Option, &mut Self) -> (R, S), + ) -> R + where + S: 'static, + { + self.with_element_id(Some(id), |cx| { + let global_id = cx.window().element_id_stack.clone(); + + if let Some(any) = cx + .window_mut() + .next_frame + .element_states + .remove(&global_id) + .or_else(|| { + cx.window_mut() + .rendered_frame + .element_states + .remove(&global_id) + }) + { + let ElementStateBox { + inner, + parent_view_id, + #[cfg(debug_assertions)] + type_name + } = any; + // Using the extra inner option to avoid needing to reallocate a new box. + let mut state_box = inner + .downcast::>() + .map_err(|_| { + #[cfg(debug_assertions)] + { + anyhow!( + "invalid element state type for id, requested_type {:?}, actual type: {:?}", + std::any::type_name::(), + type_name + ) + } + + #[cfg(not(debug_assertions))] + { + anyhow!( + "invalid element state type for id, requested_type {:?}", + std::any::type_name::(), + ) + } + }) + .unwrap(); + + // Actual: Option <- View + // Requested: () <- AnyElemet + let state = state_box + .take() + .expect("element state is already on the stack"); + let (result, state) = f(Some(state), cx); + state_box.replace(state); + cx.window_mut() + .next_frame + .element_states + .insert(global_id, ElementStateBox { + inner: state_box, + parent_view_id, + #[cfg(debug_assertions)] + type_name + }); + result + } else { + let (result, state) = f(None, cx); + let parent_view_id = cx.parent_view_id(); + cx.window_mut() + .next_frame + .element_states + .insert(global_id, + ElementStateBox { + inner: Box::new(Some(state)), + parent_view_id, + #[cfg(debug_assertions)] + type_name: std::any::type_name::() + } + + ); + result + } + }) + } + + fn parent_view_id(&self) -> EntityId { + *self + .window + .next_frame + .view_stack + .last() + .expect("a view should always be on the stack while drawing") + } + /// Set an input handler, such as [`ElementInputHandler`][element_input_handler], which interfaces with the /// platform to receive textual input with proper integration with concerns such - /// as IME interactions. + /// as IME interactions. This handler will be active for the upcoming frame until the following frame is + /// rendered. /// /// [element_input_handler]: crate::ElementInputHandler pub fn handle_input( @@ -1892,9 +2184,11 @@ impl<'a> WindowContext<'a> { input_handler: impl PlatformInputHandler, ) { if focus_handle.is_focused(self) { - self.window - .platform_window - .set_input_handler(Box::new(input_handler)); + let view_id = self.parent_view_id(); + self.window.next_frame.requested_input_handler = Some(RequestedInputHandler { + view_id, + handler: Some(Box::new(input_handler)), + }) } } @@ -2040,7 +2334,7 @@ impl VisualContext for WindowContext<'_> { { let view = self.new_view(build_view); self.window.root_view = Some(view.clone().into()); - self.notify(); + self.refresh(); view } @@ -2223,98 +2517,6 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { .unwrap_or_default() } - /// Update or initialize state for an element with the given id that lives across multiple - /// frames. If an element with this id existed in the rendered frame, its state will be passed - /// to the given closure. The state returned by the closure will be stored so it can be referenced - /// when drawing the next frame. - fn with_element_state( - &mut self, - id: ElementId, - f: impl FnOnce(Option, &mut Self) -> (R, S), - ) -> R - where - S: 'static, - { - self.with_element_id(Some(id), |cx| { - let global_id = cx.window().element_id_stack.clone(); - - if let Some(any) = cx - .window_mut() - .next_frame - .element_states - .remove(&global_id) - .or_else(|| { - cx.window_mut() - .rendered_frame - .element_states - .remove(&global_id) - }) - { - let ElementStateBox { - inner, - - #[cfg(debug_assertions)] - type_name - } = any; - // Using the extra inner option to avoid needing to reallocate a new box. - let mut state_box = inner - .downcast::>() - .map_err(|_| { - #[cfg(debug_assertions)] - { - anyhow!( - "invalid element state type for id, requested_type {:?}, actual type: {:?}", - std::any::type_name::(), - type_name - ) - } - - #[cfg(not(debug_assertions))] - { - anyhow!( - "invalid element state type for id, requested_type {:?}", - std::any::type_name::(), - ) - } - }) - .unwrap(); - - // Actual: Option <- View - // Requested: () <- AnyElemet - let state = state_box - .take() - .expect("element state is already on the stack"); - let (result, state) = f(Some(state), cx); - state_box.replace(state); - cx.window_mut() - .next_frame - .element_states - .insert(global_id, ElementStateBox { - inner: state_box, - - #[cfg(debug_assertions)] - type_name - }); - result - } else { - let (result, state) = f(None, cx); - cx.window_mut() - .next_frame - .element_states - .insert(global_id, - ElementStateBox { - inner: Box::new(Some(state)), - - #[cfg(debug_assertions)] - type_name: std::any::type_name::() - } - - ); - result - } - }) - } - /// Obtain the current content mask. fn content_mask(&self) -> ContentMask { self.window() @@ -2554,8 +2756,21 @@ impl<'a, V: 'static> ViewContext<'a, V> { /// Indicate that this view has changed, which will invoke any observers and also mark the window as dirty. /// If this view or any of its ancestors are *cached*, notifying it will cause it or its ancestors to be redrawn. pub fn notify(&mut self) { + for view_id in self + .window + .rendered_frame + .dispatch_tree + .view_path(self.view.entity_id()) + .into_iter() + .rev() + { + if !self.window.dirty_views.insert(view_id) { + break; + } + } + if !self.window.drawing { - self.window_cx.notify(); + self.window_cx.window.dirty = true; self.window_cx.app.push_effect(Effect::Notify { emitter: self.view.model.entity_id, }); @@ -3137,13 +3352,6 @@ impl AnyWindowHandle { } } -// #[cfg(any(test, feature = "test-support"))] -// impl From> for StackingOrder { -// fn from(small_vec: SmallVec<[u32; 16]>) -> Self { -// StackingOrder(small_vec) -// } -// } - /// An identifier for an [`Element`](crate::Element). /// /// Can be constructed with a string, a number, or both, as well diff --git a/crates/gpui_macros/src/register_action.rs b/crates/gpui_macros/src/register_action.rs index c18e4f4b89a68859b1c413357649cf6ad025e8d5..2772ec963485852e570ee27a8e7e3381dcd465d2 100644 --- a/crates/gpui_macros/src/register_action.rs +++ b/crates/gpui_macros/src/register_action.rs @@ -1,16 +1,3 @@ -// Input: -// -// struct FooBar {} - -// Output: -// -// struct FooBar {} -// -// #[allow(non_snake_case)] -// #[gpui2::ctor] -// fn register_foobar_builder() { -// gpui2::register_action_builder::() -// } use proc_macro::TokenStream; use proc_macro2::Ident; use quote::{format_ident, quote}; diff --git a/crates/language_selector/src/language_selector.rs b/crates/language_selector/src/language_selector.rs index 33b2e481263e9fea84e77993668de16f5735726f..00ff809fc4dcd499c6d11ce27726827213f64a67 100644 --- a/crates/language_selector/src/language_selector.rs +++ b/crates/language_selector/src/language_selector.rs @@ -68,7 +68,7 @@ impl LanguageSelector { impl Render for LanguageSelector { fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - v_stack().w(rems(34.)).child(self.picker.clone()) + v_flex().w(rems(34.)).child(self.picker.clone()) } } diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index e3718267582b5331faa0dc71be3338a93022730f..75b4305b58329a48cd19563fc659be6a27f09af0 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -405,8 +405,14 @@ impl LspLogView { { log_view.editor.update(cx, |editor, cx| { editor.set_read_only(false); - editor.handle_input(entry.trim(), cx); - editor.handle_input("\n", cx); + let last_point = editor.buffer().read(cx).len(cx); + editor.edit( + vec![ + (last_point..last_point, entry.trim()), + (last_point..last_point, "\n"), + ], + cx, + ); editor.set_read_only(true); }); } @@ -449,6 +455,7 @@ impl LspLogView { editor.set_text(log_contents, cx); editor.move_to_end(&MoveToEnd, cx); editor.set_read_only(true); + editor.set_show_copilot_suggestions(false); editor }); let editor_subscription = cx.subscribe( @@ -624,6 +631,10 @@ impl Item for LspLogView { .into_any_element() } + fn telemetry_event_text(&self) -> Option<&'static str> { + None + } + fn as_searchable(&self, handle: &View) -> Option> { Some(Box::new(handle.clone())) } @@ -784,7 +795,7 @@ impl Render for LspLogToolbarItemView { { let log_toolbar_view = log_toolbar_view.clone(); move |cx| { - h_stack() + h_flex() .w_full() .justify_between() .child(Label::new(RPC_MESSAGES)) @@ -836,7 +847,7 @@ impl Render for LspLogToolbarItemView { .into() }); - h_stack().size_full().child(lsp_menu).child( + h_flex().size_full().child(lsp_menu).child( div() .child( Button::new("clear_log_button", "Clear").on_click(cx.listener( diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index b49e4eb649ae28fea72ad2d83402653961caf0b8..5acc6bff7fb1472697bd95c363c2914470457ea3 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -9,7 +9,7 @@ use language::{Buffer, OwnedSyntaxLayerInfo}; use std::{mem, ops::Range}; use theme::ActiveTheme; use tree_sitter::{Node, TreeCursor}; -use ui::{h_stack, popover_menu, ButtonLike, Color, ContextMenu, Label, LabelCommon, PopoverMenu}; +use ui::{h_flex, popover_menu, ButtonLike, Color, ContextMenu, Label, LabelCommon, PopoverMenu}; use workspace::{ item::{Item, ItemHandle}, SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, @@ -239,7 +239,7 @@ impl SyntaxTreeView { fn render_node(cursor: &TreeCursor, depth: u32, selected: bool, cx: &AppContext) -> Div { let colors = cx.theme().colors(); - let mut row = h_stack(); + let mut row = h_flex(); if let Some(field_name) = cursor.field_name() { row = row.children([Label::new(field_name).color(Color::Info), Label::new(": ")]); } @@ -397,6 +397,10 @@ impl Item for SyntaxTreeView { .into_any_element() } + fn telemetry_event_text(&self) -> Option<&'static str> { + None + } + fn clone_on_split( &self, _: workspace::WorkspaceId, diff --git a/crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift b/crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift index db5da8e0e9ec5608e81fe34f3aa901d2e819f21d..7468c08791f8b0782cf711cb9b546bf2940ae6fe 100644 --- a/crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift +++ b/crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift @@ -286,6 +286,18 @@ public func LKRemoteAudioTrackGetSid(track: UnsafeRawPointer) -> CFString { return track.sid! as CFString } +@_cdecl("LKRemoteAudioTrackStart") +public func LKRemoteAudioTrackStart(track: UnsafeRawPointer) { + let track = Unmanaged.fromOpaque(track).takeUnretainedValue() + track.start() +} + +@_cdecl("LKRemoteAudioTrackStop") +public func LKRemoteAudioTrackStop(track: UnsafeRawPointer) { + let track = Unmanaged.fromOpaque(track).takeUnretainedValue() + track.stop() +} + @_cdecl("LKDisplaySources") public func LKDisplaySources(data: UnsafeRawPointer, callback: @escaping @convention(c) (UnsafeRawPointer, CFArray?, CFString?) -> Void) { MacOSScreenCapturer.sources(for: .display, includeCurrentApplication: false, preferredMethod: .legacy).then { displaySources in diff --git a/crates/live_kit_client/src/prod.rs b/crates/live_kit_client/src/prod.rs index f1660cc3d1a7ce9f7951cfd5f1a354158a41d334..a4bd9d4f07b6c88f3b7fe7dca343a972659d975f 100644 --- a/crates/live_kit_client/src/prod.rs +++ b/crates/live_kit_client/src/prod.rs @@ -18,8 +18,6 @@ use std::{ sync::{Arc, Weak}, }; -// SAFETY: Most live kit types are threadsafe: -// https://github.com/livekit/client-sdk-swift#thread-safety macro_rules! pointer_type { ($pointer_name:ident) => { #[repr(transparent)] @@ -134,8 +132,10 @@ extern "C" { ) -> *const c_void; fn LKRemoteAudioTrackGetSid(track: swift::RemoteAudioTrack) -> CFStringRef; - fn LKVideoTrackAddRenderer(track: swift::RemoteVideoTrack, renderer: *const c_void); fn LKRemoteVideoTrackGetSid(track: swift::RemoteVideoTrack) -> CFStringRef; + fn LKRemoteAudioTrackStart(track: swift::RemoteAudioTrack); + fn LKRemoteAudioTrackStop(track: swift::RemoteAudioTrack); + fn LKVideoTrackAddRenderer(track: swift::RemoteVideoTrack, renderer: *const c_void); fn LKDisplaySources( callback_data: *mut c_void, @@ -853,12 +853,12 @@ impl RemoteAudioTrack { &self.publisher_id } - pub fn enable(&self) -> impl Future> { - async { Ok(()) } + pub fn start(&self) { + unsafe { LKRemoteAudioTrackStart(self.native_track) } } - pub fn disable(&self) -> impl Future> { - async { Ok(()) } + pub fn stop(&self) { + unsafe { LKRemoteAudioTrackStop(self.native_track) } } } diff --git a/crates/live_kit_client/src/test.rs b/crates/live_kit_client/src/test.rs index 0716042ff196e1e0ea8f3543f03b645648ed473c..96ca2b90dcd5de645b927358e2ff2e2779592003 100644 --- a/crates/live_kit_client/src/test.rs +++ b/crates/live_kit_client/src/test.rs @@ -1,7 +1,7 @@ use crate::{ConnectionState, RoomUpdate, Sid}; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; -use collections::{BTreeMap, HashMap}; +use collections::{BTreeMap, HashMap, HashSet}; use futures::Stream; use gpui::BackgroundExecutor; use live_kit_server::{proto, token}; @@ -13,7 +13,7 @@ use std::{ mem, sync::{ atomic::{AtomicBool, Ordering::SeqCst}, - Arc, + Arc, Weak, }, }; @@ -113,7 +113,25 @@ impl TestServer { .0 .lock() .updates_tx - .try_broadcast(RoomUpdate::SubscribedToRemoteVideoTrack(track.clone())) + .try_broadcast(RoomUpdate::SubscribedToRemoteVideoTrack(Arc::new( + RemoteVideoTrack { + server_track: track.clone(), + }, + ))) + .unwrap(); + } + for track in &room.audio_tracks { + client_room + .0 + .lock() + .updates_tx + .try_broadcast(RoomUpdate::SubscribedToRemoteAudioTrack( + Arc::new(RemoteAudioTrack { + server_track: track.clone(), + room: Arc::downgrade(&client_room), + }), + Arc::new(RemoteTrackPublication), + )) .unwrap(); } room.client_rooms.insert(identity, client_room); @@ -210,7 +228,7 @@ impl TestServer { } let sid = nanoid::nanoid!(17); - let track = Arc::new(RemoteVideoTrack { + let track = Arc::new(TestServerVideoTrack { sid: sid.clone(), publisher_id: identity.clone(), frames_rx: local_track.frames_rx.clone(), @@ -224,7 +242,11 @@ impl TestServer { .0 .lock() .updates_tx - .try_broadcast(RoomUpdate::SubscribedToRemoteVideoTrack(track.clone())) + .try_broadcast(RoomUpdate::SubscribedToRemoteVideoTrack(Arc::new( + RemoteVideoTrack { + server_track: track.clone(), + }, + ))) .unwrap(); } } @@ -259,9 +281,10 @@ impl TestServer { } let sid = nanoid::nanoid!(17); - let track = Arc::new(RemoteAudioTrack { + let track = Arc::new(TestServerAudioTrack { sid: sid.clone(), publisher_id: identity.clone(), + muted: AtomicBool::new(false), }); let publication = Arc::new(RemoteTrackPublication); @@ -275,7 +298,10 @@ impl TestServer { .lock() .updates_tx .try_broadcast(RoomUpdate::SubscribedToRemoteAudioTrack( - track.clone(), + Arc::new(RemoteAudioTrack { + server_track: track.clone(), + room: Arc::downgrade(&client_room), + }), publication.clone(), )) .unwrap(); @@ -285,37 +311,123 @@ impl TestServer { Ok(sid) } + fn set_track_muted(&self, token: &str, track_sid: &str, muted: bool) -> Result<()> { + let claims = live_kit_server::token::validate(&token, &self.secret_key)?; + let room_name = claims.video.room.unwrap(); + let identity = claims.sub.unwrap(); + let mut server_rooms = self.rooms.lock(); + let room = server_rooms + .get_mut(&*room_name) + .ok_or_else(|| anyhow!("room {} does not exist", room_name))?; + if let Some(track) = room + .audio_tracks + .iter_mut() + .find(|track| track.sid == track_sid) + { + track.muted.store(muted, SeqCst); + for (id, client_room) in room.client_rooms.iter() { + if *id != identity { + client_room + .0 + .lock() + .updates_tx + .try_broadcast(RoomUpdate::RemoteAudioTrackMuteChanged { + track_id: track_sid.to_string(), + muted, + }) + .unwrap(); + } + } + } + Ok(()) + } + + fn is_track_muted(&self, token: &str, track_sid: &str) -> Option { + let claims = live_kit_server::token::validate(&token, &self.secret_key).ok()?; + let room_name = claims.video.room.unwrap(); + + let mut server_rooms = self.rooms.lock(); + let room = server_rooms.get_mut(&*room_name)?; + room.audio_tracks.iter().find_map(|track| { + if track.sid == track_sid { + Some(track.muted.load(SeqCst)) + } else { + None + } + }) + } + fn video_tracks(&self, token: String) -> Result>> { let claims = live_kit_server::token::validate(&token, &self.secret_key)?; let room_name = claims.video.room.unwrap(); + let identity = claims.sub.unwrap(); let mut server_rooms = self.rooms.lock(); let room = server_rooms .get_mut(&*room_name) .ok_or_else(|| anyhow!("room {} does not exist", room_name))?; - Ok(room.video_tracks.clone()) + room.client_rooms + .get(identity.as_ref()) + .ok_or_else(|| anyhow!("not a participant in room"))?; + Ok(room + .video_tracks + .iter() + .map(|track| { + Arc::new(RemoteVideoTrack { + server_track: track.clone(), + }) + }) + .collect()) } fn audio_tracks(&self, token: String) -> Result>> { let claims = live_kit_server::token::validate(&token, &self.secret_key)?; let room_name = claims.video.room.unwrap(); + let identity = claims.sub.unwrap(); let mut server_rooms = self.rooms.lock(); let room = server_rooms .get_mut(&*room_name) .ok_or_else(|| anyhow!("room {} does not exist", room_name))?; - Ok(room.audio_tracks.clone()) + let client_room = room + .client_rooms + .get(identity.as_ref()) + .ok_or_else(|| anyhow!("not a participant in room"))?; + Ok(room + .audio_tracks + .iter() + .map(|track| { + Arc::new(RemoteAudioTrack { + server_track: track.clone(), + room: Arc::downgrade(&client_room), + }) + }) + .collect()) } } #[derive(Default)] struct TestServerRoom { client_rooms: HashMap>, - video_tracks: Vec>, - audio_tracks: Vec>, + video_tracks: Vec>, + audio_tracks: Vec>, participant_permissions: HashMap, } +#[derive(Debug)] +struct TestServerVideoTrack { + sid: Sid, + publisher_id: Sid, + frames_rx: async_broadcast::Receiver, +} + +#[derive(Debug)] +struct TestServerAudioTrack { + sid: Sid, + publisher_id: Sid, + muted: AtomicBool, +} + impl TestServerRoom {} pub struct TestApiClient { @@ -386,6 +498,7 @@ struct RoomState { watch::Receiver, ), display_sources: Vec, + paused_audio_tracks: HashSet, updates_tx: async_broadcast::Sender, updates_rx: async_broadcast::Receiver, } @@ -398,6 +511,7 @@ impl Room { Arc::new(Self(Mutex::new(RoomState { connection: watch::channel_with(ConnectionState::Disconnected), display_sources: Default::default(), + paused_audio_tracks: Default::default(), updates_tx, updates_rx, }))) @@ -443,11 +557,12 @@ impl Room { .publish_video_track(this.token(), track) .await?; Ok(LocalTrackPublication { - muted: Default::default(), + room: Arc::downgrade(&this), sid, }) } } + pub fn publish_audio_track( self: &Arc, track: LocalAudioTrack, @@ -460,7 +575,7 @@ impl Room { .publish_audio_track(this.token(), &track) .await?; Ok(LocalTrackPublication { - muted: Default::default(), + room: Arc::downgrade(&this), sid, }) } @@ -560,20 +675,31 @@ impl Drop for Room { #[derive(Clone)] pub struct LocalTrackPublication { sid: String, - muted: Arc, + room: Weak, } impl LocalTrackPublication { pub fn set_mute(&self, mute: bool) -> impl Future> { - let muted = self.muted.clone(); + let sid = self.sid.clone(); + let room = self.room.clone(); async move { - muted.store(mute, SeqCst); - Ok(()) + if let Some(room) = room.upgrade() { + room.test_server() + .set_track_muted(&room.token(), &sid, mute) + } else { + Err(anyhow!("no such room")) + } } } pub fn is_muted(&self) -> bool { - self.muted.load(SeqCst) + if let Some(room) = self.room.upgrade() { + room.test_server() + .is_track_muted(&room.token(), &self.sid) + .unwrap_or(false) + } else { + false + } } pub fn sid(&self) -> String { @@ -621,46 +747,65 @@ impl LocalAudioTrack { #[derive(Debug)] pub struct RemoteVideoTrack { - sid: Sid, - publisher_id: Sid, - frames_rx: async_broadcast::Receiver, + server_track: Arc, } impl RemoteVideoTrack { pub fn sid(&self) -> &str { - &self.sid + &self.server_track.sid } pub fn publisher_id(&self) -> &str { - &self.publisher_id + &self.server_track.publisher_id } pub fn frames(&self) -> async_broadcast::Receiver { - self.frames_rx.clone() + self.server_track.frames_rx.clone() } } #[derive(Debug)] pub struct RemoteAudioTrack { - sid: Sid, - publisher_id: Sid, + server_track: Arc, + room: Weak, } impl RemoteAudioTrack { pub fn sid(&self) -> &str { - &self.sid + &self.server_track.sid } pub fn publisher_id(&self) -> &str { - &self.publisher_id + &self.server_track.publisher_id } - pub fn enable(&self) -> impl Future> { - async { Ok(()) } + pub fn start(&self) { + if let Some(room) = self.room.upgrade() { + room.0 + .lock() + .paused_audio_tracks + .remove(&self.server_track.sid); + } } - pub fn disable(&self) -> impl Future> { - async { Ok(()) } + pub fn stop(&self) { + if let Some(room) = self.room.upgrade() { + room.0 + .lock() + .paused_audio_tracks + .insert(self.server_track.sid.clone()); + } + } + + pub fn is_playing(&self) -> bool { + !self + .room + .upgrade() + .unwrap() + .0 + .lock() + .paused_audio_tracks + .contains(&self.server_track.sid) } } diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index a661a693b1f5733dd27218f27ad502ea0d763c78..1f2112003974aeb95ba55202af87ec20e7e04331 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -64,7 +64,7 @@ impl ModalView for OutlineView { impl Render for OutlineView { fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - v_stack().w(rems(34.)).child(self.picker.clone()) + v_flex().w(rems(34.)).child(self.picker.clone()) } } diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index ad7552052055be100bcd3d1cad9e398416387aa6..0af4675c9803505a4848049233fb02b60b308632 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -5,7 +5,7 @@ use gpui::{ View, ViewContext, WindowContext, }; use std::{cmp, sync::Arc}; -use ui::{prelude::*, v_stack, Color, Divider, Label, ListItem, ListItemSpacing, ListSeparator}; +use ui::{prelude::*, v_flex, Color, Divider, Label, ListItem, ListItemSpacing, ListSeparator}; use workspace::ModalView; pub struct Picker { @@ -236,7 +236,7 @@ impl ModalView for Picker {} impl Render for Picker { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let picker_editor = h_stack() + let picker_editor = h_flex() .overflow_hidden() .flex_none() .h_9() @@ -264,7 +264,7 @@ impl Render for Picker { .child(Divider::horizontal()) .when(self.delegate.match_count() > 0, |el| { el.child( - v_stack() + v_flex() .flex_grow() .py_2() .max_h(self.max_height.unwrap_or(rems(18.).into())) @@ -309,7 +309,7 @@ impl Render for Picker { }) .when(self.delegate.match_count() == 0, |el| { el.child( - v_stack().flex_grow().py_2().child( + v_flex().flex_grow().py_2().child( ListItem::new("empty_state") .inset(true) .spacing(ListItemSpacing::Sparse) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 06b6da75b3ed382d03becbb5a418ddf28cbc05c0..5f37bbfce6483e359866a0dadb1d63b2e32b4651 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -99,20 +99,6 @@ pub trait Item { fn project_path(&self, cx: &AppContext) -> Option; } -// Language server state is stored across 3 collections: -// language_servers => -// a mapping from unique server id to LanguageServerState which can either be a task for a -// server in the process of starting, or a running server with adapter and language server arcs -// language_server_ids => a mapping from worktreeId and server name to the unique server id -// language_server_statuses => a mapping from unique server id to the current server status -// -// Multiple worktrees can map to the same language server for example when you jump to the definition -// of a file in the standard library. So language_server_ids is used to look up which server is active -// for a given worktree and language server name -// -// When starting a language server, first the id map is checked to make sure a server isn't already available -// for that worktree. If there is one, it finishes early. Otherwise, a new id is allocated and and -// the Starting variant of LanguageServerState is stored in the language_servers map. pub struct Project { worktrees: Vec, active_entry: Option, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 251e26ebfba004b81a49c1ce28956e01f42bbce5..4301b6e392df1af4d2f411fc6e51b50564ffb16f 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1,6 +1,6 @@ pub mod file_associations; mod project_panel_settings; -use settings::{Settings, SettingsStore}; +use settings::Settings; use db::kvp::KEY_VALUE_STORE; use editor::{scroll::autoscroll::Autoscroll, Cancel, Editor}; @@ -30,7 +30,7 @@ use std::{ sync::Arc, }; use theme::ThemeSettings; -use ui::{prelude::*, v_stack, ContextMenu, Icon, KeyBinding, Label, ListItem}; +use ui::{prelude::*, v_flex, ContextMenu, Icon, KeyBinding, Label, ListItem}; use unicase::UniCase; use util::{maybe, ResultExt, TryFutureExt}; use workspace::{ @@ -58,6 +58,7 @@ pub struct ProjectPanel { workspace: WeakView, width: Option, pending_serialization: Task>, + was_deserialized: bool, } #[derive(Copy, Clone, Debug)] @@ -221,10 +222,10 @@ impl ProjectPanel { }) .detach(); - // cx.observe_global::(|_, cx| { - // cx.notify(); - // }) - // .detach(); + cx.observe_global::(|_, cx| { + cx.notify(); + }) + .detach(); let mut this = Self { project: project.clone(), @@ -243,21 +244,10 @@ impl ProjectPanel { workspace: workspace.weak_handle(), width: None, pending_serialization: Task::ready(None), + was_deserialized: false, }; this.update_visible_entries(None, cx); - // Update the dock position when the setting changes. - let mut old_dock_position = this.position(cx); - ProjectPanelSettings::register(cx); - cx.observe_global::(move |this, cx| { - let new_dock_position = this.position(cx); - if new_dock_position != old_dock_position { - old_dock_position = new_dock_position; - cx.emit(PanelEvent::ChangePosition); - } - }) - .detach(); - this }); @@ -292,16 +282,16 @@ impl ProjectPanel { } &Event::SplitEntry { entry_id } => { if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) { - if let Some(_entry) = worktree.read(cx).entry_for_id(entry_id) { - // workspace - // .split_path( - // ProjectPath { - // worktree_id: worktree.read(cx).id(), - // path: entry.path.clone(), - // }, - // cx, - // ) - // .detach_and_log_err(cx); + if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { + workspace + .split_path( + ProjectPath { + worktree_id: worktree.read(cx).id(), + path: entry.path.clone(), + }, + cx, + ) + .detach_and_log_err(cx); } } } @@ -334,6 +324,7 @@ impl ProjectPanel { if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width; + panel.was_deserialized = true; cx.notify(); }); } @@ -788,10 +779,6 @@ impl ProjectPanel { cx.notify(); } } - - // cx.update_global(|drag_and_drop: &mut DragAndDrop, cx| { - // drag_and_drop.cancel_dragging::(cx); - // }) } } @@ -1481,6 +1468,9 @@ impl ProjectPanel { cx.notify(); } } + pub fn was_deserialized(&self) -> bool { + self.was_deserialized + } } impl Render for ProjectPanel { @@ -1557,7 +1547,7 @@ impl Render for ProjectPanel { .child(menu.clone()) })) } else { - v_stack() + v_flex() .id("empty-project_panel") .size_full() .p_4() @@ -1581,7 +1571,7 @@ impl Render for DraggedProjectEntryView { fn render(&mut self, cx: &mut ViewContext) -> impl Element { let settings = ProjectPanelSettings::get_global(cx); let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); - h_stack() + h_flex() .font(ui_font) .bg(cx.theme().colors().background) .w(self.width) diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 23dfd21b8256574bf122215cd6d34e36d8059f1f..68a0721b4c54386f31176aef44fb6a0733b02524 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -11,7 +11,7 @@ use std::{borrow::Cow, cmp::Reverse, sync::Arc}; use theme::ActiveTheme; use util::ResultExt; use workspace::{ - ui::{v_stack, Color, Label, LabelCommon, LabelLike, ListItem, ListItemSpacing, Selectable}, + ui::{v_flex, Color, Label, LabelCommon, LabelLike, ListItem, ListItemSpacing, Selectable}, Workspace, }; @@ -242,7 +242,7 @@ impl PickerDelegate for ProjectSymbolsDelegate { .spacing(ListItemSpacing::Sparse) .selected(selected) .child( - v_stack() + v_flex() .child( LabelLike::new().child( StyledText::new(label) diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index cf4941bcec66cdef77f3f4453a6b3eb25d8b1321..865632142a48978f33a446a49fe65c20183cf832 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -93,7 +93,7 @@ impl Render for QuickActionBar { }, ); - h_stack() + h_flex() .id("quick action bar") .gap_2() .children(inlay_hints_button) diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index c3b2c21d522ed3efdbf208433e6070b2b49dcf44..6208635e22d969bfa9219d7eb4d1466a35a0999d 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -104,7 +104,7 @@ impl FocusableView for RecentProjects { impl Render for RecentProjects { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - v_stack() + v_flex() .w(rems(self.rem_width)) .child(self.picker.clone()) .on_mouse_down_out(cx.listener(|this, _, cx| { @@ -236,7 +236,7 @@ impl PickerDelegate for RecentProjectsDelegate { .spacing(ListItemSpacing::Sparse) .selected(selected) .child( - v_stack() + v_flex() .child(highlighted_location.names) .when(self.render_paths, |this| { this.children(highlighted_location.paths) diff --git a/crates/refineable/derive_refineable/src/derive_refineable.rs b/crates/refineable/derive_refineable/src/derive_refineable.rs index ad7678b58fe696f61c14776c316bb9d159044b2f..99418206462a0dc7bc3babd2f9bda534a69a0f39 100644 --- a/crates/refineable/derive_refineable/src/derive_refineable.rs +++ b/crates/refineable/derive_refineable/src/derive_refineable.rs @@ -69,13 +69,6 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream { path: parse_quote!(Clone), })); - // punctuated.push_punct(syn::token::Add::default()); - // punctuated.push_value(TypeParamBound::Trait(TraitBound { - // paren_token: None, - // modifier: syn::TraitBoundModifier::None, - // lifetimes: None, - // path: parse_quote!(Default), - // })); punctuated }, }) @@ -94,10 +87,6 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream { }, }; - // refinable_refine_assignments - // refinable_refined_assignments - // refinement_refine_assignments - let refineable_refine_assignments: Vec = fields .iter() .map(|field| { diff --git a/crates/rich_text/src/rich_text.rs b/crates/rich_text/src/rich_text.rs index b4a87b1e5de3ed3c71af8c1dcaf9a3ab7a2e4e96..8bc0d73fd4dc80c583ac532896004f97c1365e94 100644 --- a/crates/rich_text/src/rich_text.rs +++ b/crates/rich_text/src/rich_text.rs @@ -39,6 +39,7 @@ pub struct RichText { /// Allows one to specify extra links to the rendered markdown, which can be used /// for e.g. mentions. +#[derive(Debug)] pub struct Mention { pub range: Range, pub is_self_mention: bool, @@ -85,31 +86,6 @@ impl RichText { }) .into_any_element() } - - // pub fn add_mention( - // &mut self, - // range: Range, - // is_current_user: bool, - // mention_style: HighlightStyle, - // ) -> anyhow::Result<()> { - // if range.end > self.text.len() { - // bail!( - // "Mention in range {range:?} is outside of bounds for a message of length {}", - // self.text.len() - // ); - // } - - // if is_current_user { - // self.region_ranges.push(range.clone()); - // self.regions.push(RenderedRegion { - // background_kind: Some(BackgroundKind::Mention), - // link_url: None, - // }); - // } - // self.highlights - // .push((range, Highlight::Highlight(mention_style))); - // Ok(()) - // } } pub fn render_markdown_mut( @@ -138,20 +114,21 @@ pub fn render_markdown_mut( if let Some(language) = ¤t_language { render_code(text, highlights, t.as_ref(), language); } else { - if let Some(mention) = mentions.first() { - if source_range.contains_inclusive(&mention.range) { - mentions = &mentions[1..]; - let range = (prev_len + mention.range.start - source_range.start) - ..(prev_len + mention.range.end - source_range.start); - highlights.push(( - range.clone(), - if mention.is_self_mention { - Highlight::SelfMention - } else { - Highlight::Mention - }, - )); + while let Some(mention) = mentions.first() { + if !source_range.contains_inclusive(&mention.range) { + break; } + mentions = &mentions[1..]; + let range = (prev_len + mention.range.start - source_range.start) + ..(prev_len + mention.range.end - source_range.start); + highlights.push(( + range.clone(), + if mention.is_self_mention { + Highlight::SelfMention + } else { + Highlight::Mention + }, + )); } text.push_str(t.as_ref()); @@ -272,13 +249,6 @@ pub fn render_markdown( language_registry: &Arc, language: Option<&Arc>, ) -> RichText { - // let mut data = RichText { - // text: Default::default(), - // highlights: Default::default(), - // region_ranges: Default::default(), - // regions: Default::default(), - // }; - let mut text = String::new(); let mut highlights = Vec::new(); let mut link_ranges = Vec::new(); diff --git a/crates/rpc/build.rs b/crates/rpc/build.rs index 66b289f1db83ab47d9ffaeaff8ec172838b4921f..25dff9b007c148c25aab0f4bd87e07bcb543d08a 100644 --- a/crates/rpc/build.rs +++ b/crates/rpc/build.rs @@ -1,6 +1,5 @@ fn main() { let mut build = prost_build::Config::new(); - // build.protoc_arg("--experimental_allow_proto3_optional"); build .type_attribute(".", "#[derive(serde::Serialize)]") .compile_protos(&["proto/zed.proto"], &["proto"]) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index f7e36fe696258fa1a361bc6bd212f1d888efc2f8..e217a7ab73cd2fd1aaa540f1b56cf13b7ec1c84b 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -21,7 +21,7 @@ use settings::Settings; use std::{any::Any, sync::Arc}; use theme::ThemeSettings; -use ui::{h_stack, prelude::*, Icon, IconButton, IconName, ToggleButton, Tooltip}; +use ui::{h_flex, prelude::*, Icon, IconButton, IconName, ToggleButton, Tooltip}; use util::ResultExt; use workspace::{ item::ItemHandle, @@ -186,7 +186,7 @@ impl Render for BufferSearchBar { } else { cx.theme().colors().border }; - h_stack() + h_flex() .w_full() .gap_2() .key_context(key_context) @@ -216,7 +216,7 @@ impl Render for BufferSearchBar { this.on_action(cx.listener(Self::toggle_whole_word)) }) .child( - h_stack() + h_flex() .flex_1() .px_2() .py_1() @@ -243,11 +243,11 @@ impl Render for BufferSearchBar { })), ) .child( - h_stack() + h_flex() .gap_2() .flex_none() .child( - h_stack() + h_flex() .child( ToggleButton::new("search-mode-text", SearchMode::Text.label()) .style(ButtonStyle::Filled) @@ -303,12 +303,12 @@ impl Render for BufferSearchBar { }), ) .child( - h_stack() + h_flex() .gap_0p5() .flex_1() .when(self.replace_enabled, |this| { this.child( - h_stack() + h_flex() .flex_1() // We're giving this a fixed height to match the height of the search input, // which has an icon inside that is increasing its height. @@ -346,7 +346,7 @@ impl Render for BufferSearchBar { }), ) .child( - h_stack() + h_flex() .gap_0p5() .flex_none() .child( @@ -1648,7 +1648,6 @@ mod tests { #[gpui::test] async fn test_search_query_history(cx: &mut TestAppContext) { - //crate::project_search::tests::init_test(cx); init_globals(cx); let buffer_text = r#" A regular expression (shortened as regex or regexp;[1] also referred to as diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 8897ae4bcfcd2ec2f14266dac81d97e8cf161985..3827b46c67fc29c96c5b361440fb66143c96db1a 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -38,7 +38,7 @@ use std::{ use theme::ThemeSettings; use ui::{ - h_stack, prelude::*, v_stack, Icon, IconButton, IconName, Label, LabelCommon, LabelSize, + h_flex, prelude::*, v_flex, Icon, IconButton, IconName, Label, LabelCommon, LabelSize, Selectable, ToggleButton, Tooltip, }; use util::{paths::PathMatcher, ResultExt as _}; @@ -360,19 +360,19 @@ impl Render for ProjectSearchView { .max_w_96() .child(Label::new(text).size(LabelSize::Small)) }); - v_stack() + v_flex() .flex_1() .size_full() .justify_center() .bg(cx.theme().colors().editor_background) .track_focus(&self.focus_handle) .child( - h_stack() + h_flex() .size_full() .justify_center() - .child(h_stack().flex_1()) - .child(v_stack().child(major_text).children(minor_text)) - .child(h_stack().flex_1()), + .child(h_flex().flex_1()) + .child(v_flex().child(major_text).children(minor_text)) + .child(h_flex().flex_1()), ) } } @@ -431,7 +431,7 @@ impl Item for ProjectSearchView { let tab_name = last_query .filter(|query| !query.is_empty()) .unwrap_or_else(|| "Project search".into()); - h_stack() + h_flex() .gap_2() .child(Icon::new(IconName::MagnifyingGlass).color(if selected { Color::Default @@ -446,6 +446,10 @@ impl Item for ProjectSearchView { .into_any() } + fn telemetry_event_text(&self) -> Option<&'static str> { + Some("project search") + } + fn for_each_project_item( &self, cx: &AppContext, @@ -1601,8 +1605,8 @@ impl Render for ProjectSearchBar { let search = search.read(cx); let semantic_is_available = SemanticIndex::enabled(cx); - let query_column = v_stack().child( - h_stack() + let query_column = v_flex().child( + h_flex() .min_w(rems(512. / 16.)) .px_2() .py_1() @@ -1617,7 +1621,7 @@ impl Render for ProjectSearchBar { .child(Icon::new(IconName::MagnifyingGlass)) .child(self.render_text_input(&search.query_editor, cx)) .child( - h_stack() + h_flex() .child( IconButton::new("project-search-filter-button", IconName::Filter) .tooltip(|cx| { @@ -1674,11 +1678,11 @@ impl Render for ProjectSearchBar { ), ); - let mode_column = v_stack().items_start().justify_start().child( - h_stack() + let mode_column = v_flex().items_start().justify_start().child( + h_flex() .gap_2() .child( - h_stack() + h_flex() .child( ToggleButton::new("project-search-text-button", "Text") .style(ButtonStyle::Filled) @@ -1744,7 +1748,7 @@ impl Render for ProjectSearchBar { ), ); let replace_column = if search.replace_enabled { - h_stack() + h_flex() .flex_1() .h_full() .gap_2() @@ -1757,9 +1761,9 @@ impl Render for ProjectSearchBar { .child(self.render_text_input(&search.replacement_editor, cx)) } else { // Fill out the space if we don't have a replacement editor. - h_stack().flex_1() + h_flex().flex_1() }; - let actions_column = h_stack() + let actions_column = h_flex() .when(search.replace_enabled, |this| { this.child( IconButton::new("project-search-replace-next", IconName::ReplaceNext) @@ -1820,7 +1824,7 @@ impl Render for ProjectSearchBar { .tooltip(|cx| Tooltip::for_action("Go to next match", &SelectNextMatch, cx)), ); - v_stack() + v_flex() .key_context(key_context) .flex_grow() .gap_2() @@ -1880,7 +1884,7 @@ impl Render for ProjectSearchBar { }) }) .child( - h_stack() + h_flex() .justify_between() .gap_2() .child(query_column) @@ -1890,12 +1894,12 @@ impl Render for ProjectSearchBar { ) .when(search.filters_enabled, |this| { this.child( - h_stack() + h_flex() .flex_1() .gap_2() .justify_between() .child( - h_stack() + h_flex() .flex_1() .h_full() .px_2() @@ -1921,7 +1925,7 @@ impl Render for ProjectSearchBar { }), ) .child( - h_stack() + h_flex() .flex_1() .h_full() .px_2() diff --git a/crates/semantic_index/src/semantic_index_tests.rs b/crates/semantic_index/src/semantic_index_tests.rs index ced08f4cbc30a991bfad0577af24f96c8ff81d8b..e340b44a58377b8a9bda52786dea660637ce54c1 100644 --- a/crates/semantic_index/src/semantic_index_tests.rs +++ b/crates/semantic_index/src/semantic_index_tests.rs @@ -1677,8 +1677,6 @@ fn elixir_lang() -> Arc { #[gpui::test] fn test_subtract_ranges() { - // collapsed_ranges: Vec>, keep_ranges: Vec> - assert_eq!( subtract_ranges(&[0..5, 10..21], &[0..1, 4..5]), vec![1..4, 10..21] diff --git a/crates/story/src/story.rs b/crates/story/src/story.rs index 65bebfc423f3812f28a31c6c42d9d6307189fcf2..f5448831cb168c240d4e8d8c038cd3f02a0bd2dd 100644 --- a/crates/story/src/story.rs +++ b/crates/story/src/story.rs @@ -255,8 +255,8 @@ impl Story { .child(label.into()) } - /// Note: Not ui::v_stack() as the story crate doesn't depend on the ui crate. - pub fn v_stack() -> Div { + /// Note: Not `ui::v_flex` as the `story` crate doesn't depend on the `ui` crate. + pub fn v_flex() -> Div { div().flex().flex_col().gap_1() } } @@ -298,7 +298,7 @@ impl RenderOnce for StoryItem { .gap_4() .w_full() .child( - Story::v_stack() + Story::v_flex() .px_2() .w_1_2() .min_h_px() @@ -319,7 +319,7 @@ impl RenderOnce for StoryItem { }), ) .child( - Story::v_stack() + Story::v_flex() .px_2() .flex_none() .w_1_2() diff --git a/crates/storybook/Cargo.toml b/crates/storybook/Cargo.toml index 9f08556757b6b59d1aed6fa7361029a36664a40a..fec2d42e5980b9e8b2ebb93f865dd74070aa3bc1 100644 --- a/crates/storybook/Cargo.toml +++ b/crates/storybook/Cargo.toml @@ -35,6 +35,7 @@ menu = { path = "../menu" } ui = { path = "../ui", features = ["stories"] } util = { path = "../util" } picker = { path = "../picker" } +ctrlc = "3.4" [dev-dependencies] gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/storybook/src/stories/overflow_scroll.rs b/crates/storybook/src/stories/overflow_scroll.rs index 32ab940be5d68fdf31681c5ae787e6a27a466a7f..ee2ea12c00e0dc9c2af38eeaa9592a378102e5d9 100644 --- a/crates/storybook/src/stories/overflow_scroll.rs +++ b/crates/storybook/src/stories/overflow_scroll.rs @@ -11,7 +11,7 @@ impl Render for OverflowScrollStory { .child(Story::title("Overflow Scroll")) .child(Story::label("`overflow_x_scroll`")) .child( - h_stack() + h_flex() .id("overflow_x_scroll") .gap_2() .overflow_x_scroll() @@ -24,7 +24,7 @@ impl Render for OverflowScrollStory { ) .child(Story::label("`overflow_y_scroll`")) .child( - v_stack() + v_flex() .id("overflow_y_scroll") .gap_2() .overflow_y_scroll() diff --git a/crates/storybook/src/stories/text.rs b/crates/storybook/src/stories/text.rs index 1c302cb48fae2d284c9f1667bde221c0ecfd3693..065b5bf795ba89fa1747c40fc1387ff00d4cb9c0 100644 --- a/crates/storybook/src/stories/text.rs +++ b/crates/storybook/src/stories/text.rs @@ -117,7 +117,7 @@ impl Render for TextStory { // type Element = Div; // fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { -// v_stack() +// v_flex() // .bg(blue()) // .child( // div() diff --git a/crates/storybook/src/storybook.rs b/crates/storybook/src/storybook.rs index 8d60e29a136013a8e38e33f8ebb5ce8044d4b2d6..7da1d67b307e660963c34297fc1fcfcca0c4222b 100644 --- a/crates/storybook/src/storybook.rs +++ b/crates/storybook/src/storybook.rs @@ -21,11 +21,6 @@ use crate::assets::Assets; use crate::story_selector::{ComponentStory, StorySelector}; pub use indoc::indoc; -// gpui::actions! { -// storybook, -// [ToggleInspector] -// } - #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Args { @@ -49,11 +44,17 @@ fn main() { let story_selector = args.story.clone().unwrap_or_else(|| { let stories = ComponentStory::iter().collect::>(); - let selection = FuzzySelect::new() + ctrlc::set_handler(move || {}).unwrap(); + + let result = FuzzySelect::new() .with_prompt("Choose a story to run:") .items(&stories) - .interact() - .unwrap(); + .interact(); + + let Ok(selection) = result else { + dialoguer::console::Term::stderr().show_cursor().unwrap(); + std::process::exit(0); + }; StorySelector::Component(stories[selection]) }); diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 3e72acc51bd4b442514e61c18d4755fa868f8fe6..1fec041de9c633493993696cdc6fdda142355c77 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -598,7 +598,7 @@ impl TerminalElement { this.update(cx, |term, _| term.try_modifiers_change(&event.modifiers)); if handled { - cx.notify(); + cx.refresh(); } } }); diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index d0b52f5eb217ed60cc9b62e20657e5033506f133..8954e70e8fc18d3d78775ba4eb56b11ec251c0de 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -11,9 +11,9 @@ use itertools::Itertools; use project::{Fs, ProjectEntryId}; use search::{buffer_search::DivRegistrar, BufferSearchBar}; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsStore}; +use settings::Settings; use terminal::terminal_settings::{TerminalDockPosition, TerminalSettings}; -use ui::{h_stack, ButtonCommon, Clickable, IconButton, IconSize, Selectable, Tooltip}; +use ui::{h_flex, ButtonCommon, Clickable, IconButton, IconSize, Selectable, Tooltip}; use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, @@ -68,7 +68,7 @@ impl TerminalPanel { pane.display_nav_history_buttons(false); pane.set_render_tab_bar_buttons(cx, move |pane, cx| { let terminal_panel = terminal_panel.clone(); - h_stack() + h_flex() .gap_2() .child( IconButton::new("plus", IconName::Plus) @@ -159,15 +159,6 @@ impl TerminalPanel { height: None, _subscriptions: subscriptions, }; - let mut old_dock_position = this.position(cx); - cx.observe_global::(move |this, cx| { - let new_dock_position = this.position(cx); - if new_dock_position != old_dock_position { - old_dock_position = new_dock_position; - cx.emit(PanelEvent::ChangePosition); - } - }) - .detach(); this } diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index ced122402f138e5f5b964792d7a1561260b063b6..db4b21627f13a3e050888ca1cf18a06488484b1a 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -20,7 +20,7 @@ use terminal::{ Clear, Copy, Event, MaybeNavigationTarget, Paste, ShowCharacterPalette, Terminal, }; use terminal_element::TerminalElement; -use ui::{h_stack, prelude::*, ContextMenu, Icon, IconName, Label}; +use ui::{h_flex, prelude::*, ContextMenu, Icon, IconName, Label}; use util::{paths::PathLikeWithPosition, ResultExt}; use workspace::{ item::{BreadcrumbText, Item, ItemEvent}, @@ -600,6 +600,9 @@ fn possible_open_targets( pub fn regex_search_for_query(query: &project::search::SearchQuery) -> Option { let query = query.as_str(); + if query == "." { + return None; + } let searcher = RegexSearch::new(&query); searcher.ok() } @@ -694,7 +697,7 @@ impl Item for TerminalView { cx: &WindowContext, ) -> AnyElement { let title = self.terminal().read(cx).title(true); - h_stack() + h_flex() .gap_2() .child(Icon::new(IconName::Terminal)) .child(Label::new(title).color(if selected { @@ -705,6 +708,10 @@ impl Item for TerminalView { .into_any() } + fn telemetry_event_text(&self) -> Option<&'static str> { + None + } + fn clone_on_split( &self, _workspace_id: WorkspaceId, diff --git a/crates/theme/src/default_colors.rs b/crates/theme/src/default_colors.rs index 8cbfc23fa34655283ddaa5418e15922929c1b5bc..fb88afb7dacf79d947a170e26136b293d093542b 100644 --- a/crates/theme/src/default_colors.rs +++ b/crates/theme/src/default_colors.rs @@ -8,6 +8,11 @@ pub(crate) fn neutral() -> ColorScaleSet { sand() } +// Note: We aren't currently making use of the default colors, as all of the +// themes have a value set for each color. +// +// We'll need to revisit these once we're ready to launch user themes, which may +// not specify a value for each color (and thus should fall back to the defaults). impl ThemeColors { pub fn light() -> Self { let system = SystemColors::default(); @@ -23,12 +28,12 @@ impl ThemeColors { surface_background: neutral().light().step_2(), background: neutral().light().step_1(), element_background: neutral().light().step_3(), - element_hover: neutral().light_alpha().step_4(), // todo!("pick the right colors") + element_hover: neutral().light_alpha().step_4(), element_active: neutral().light_alpha().step_5(), element_selected: neutral().light_alpha().step_5(), - element_disabled: neutral().light_alpha().step_3(), // todo!("pick the right colors") - drop_target_background: blue().light_alpha().step_2(), // todo!("pick the right colors") - ghost_element_background: system.transparent, // todo!("pick the right colors") + element_disabled: neutral().light_alpha().step_3(), + drop_target_background: blue().light_alpha().step_2(), + ghost_element_background: system.transparent, ghost_element_hover: neutral().light_alpha().step_3(), ghost_element_active: neutral().light_alpha().step_4(), ghost_element_selected: neutral().light_alpha().step_5(), @@ -59,7 +64,7 @@ impl ThemeColors { scrollbar_track_background: gpui::transparent_black(), scrollbar_track_border: neutral().light().step_5(), editor_foreground: neutral().light().step_12(), - editor_background: neutral().light().step_1(), // todo!(this was inserted by Mikayla) + editor_background: neutral().light().step_1(), editor_gutter_background: neutral().light().step_1(), editor_subheader_background: neutral().light().step_2(), editor_active_line_background: neutral().light_alpha().step_3(), @@ -106,17 +111,17 @@ impl ThemeColors { surface_background: neutral().dark().step_2(), background: neutral().dark().step_1(), element_background: neutral().dark().step_3(), - element_hover: neutral().dark_alpha().step_4(), // todo!("pick the right colors") + element_hover: neutral().dark_alpha().step_4(), element_active: neutral().dark_alpha().step_5(), - element_selected: neutral().dark_alpha().step_5(), // todo!("pick the right colors") - element_disabled: neutral().dark_alpha().step_3(), // todo!("pick the right colors") + element_selected: neutral().dark_alpha().step_5(), + element_disabled: neutral().dark_alpha().step_3(), drop_target_background: blue().dark_alpha().step_2(), ghost_element_background: system.transparent, - ghost_element_hover: neutral().dark_alpha().step_4(), // todo!("pick the right colors") - ghost_element_active: neutral().dark_alpha().step_5(), // todo!("pick the right colors") + ghost_element_hover: neutral().dark_alpha().step_4(), + ghost_element_active: neutral().dark_alpha().step_5(), ghost_element_selected: neutral().dark_alpha().step_5(), ghost_element_disabled: neutral().dark_alpha().step_3(), - text: neutral().dark().step_12(), // todo!("pick the right colors") + text: neutral().dark().step_12(), text_muted: neutral().dark().step_11(), text_placeholder: neutral().dark().step_10(), text_disabled: neutral().dark().step_9(), @@ -140,7 +145,7 @@ impl ThemeColors { scrollbar_thumb_hover_background: neutral().dark_alpha().step_4(), scrollbar_thumb_border: gpui::transparent_black(), scrollbar_track_background: gpui::transparent_black(), - scrollbar_track_border: neutral().dark().step_5(), // todo!(this was inserted by Mikayla) + scrollbar_track_border: neutral().dark().step_5(), editor_foreground: neutral().dark().step_12(), editor_background: neutral().dark().step_1(), editor_gutter_background: neutral().dark().step_1(), diff --git a/crates/theme/src/one_themes.rs b/crates/theme/src/one_themes.rs index d0bae590f6d32797ec65b34eb39de13d4befe177..fab3631d13ca890ca3ccb9fa2e06605534602abd 100644 --- a/crates/theme/src/one_themes.rs +++ b/crates/theme/src/one_themes.rs @@ -7,6 +7,10 @@ use crate::{ ThemeColors, ThemeFamily, ThemeStyles, }; +// Note: This theme family is not the one you see in Zed at the moment. +// This is a from-scratch rebuild that Nate started work on. We currently +// only use this in the tests, and the One family from the `themes/` directory +// is what gets loaded into Zed when running it. pub fn one_family() -> ThemeFamily { ThemeFamily { id: "one".to_string(), @@ -75,7 +79,7 @@ pub(crate) fn one_dark() -> Theme { tab_bar_background: bg, tab_inactive_background: bg, tab_active_background: editor, - search_match_background: bg, // todo!(this was inserted by Mikayla) + search_match_background: bg, editor_background: editor, editor_gutter_background: editor, diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index 3ecf1935a47eeb7fb4c8e4edbce9d99f33a2b3f3..efc62ed59c429b97420fc276d1f2e2eca6c841cc 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -1,7 +1,9 @@ use crate::one_themes::one_dark; use crate::{Theme, ThemeRegistry}; use anyhow::Result; -use gpui::{px, AppContext, Font, FontFeatures, FontStyle, FontWeight, Pixels}; +use gpui::{ + px, AppContext, Font, FontFeatures, FontStyle, FontWeight, Pixels, Subscription, ViewContext, +}; use schemars::{ gen::SchemaGenerator, schema::{InstanceType, Schema, SchemaObject}, @@ -80,6 +82,13 @@ impl ThemeSettings { } } +pub fn observe_buffer_font_size_adjustment( + cx: &mut ViewContext, + f: impl 'static + Fn(&mut V, &mut ViewContext), +) -> Subscription { + cx.observe_global::(f) +} + pub fn adjusted_font_size(size: Pixels, cx: &mut AppContext) -> Pixels { if let Some(AdjustedBufferFontSize(adjusted_size)) = cx.try_global::() { let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size; @@ -194,9 +203,21 @@ impl settings::Settings for ThemeSettings { ..Default::default() }; - root_schema - .definitions - .extend([("ThemeName".into(), theme_name_schema.into())]); + let available_fonts = cx + .text_system() + .all_font_families() + .into_iter() + .map(Value::String) + .collect(); + let fonts_schema = SchemaObject { + instance_type: Some(InstanceType::String.into()), + enum_values: Some(available_fonts), + ..Default::default() + }; + root_schema.definitions.extend([ + ("ThemeName".into(), theme_name_schema.into()), + ("FontFamilies".into(), fonts_schema.into()), + ]); root_schema .schema @@ -204,10 +225,16 @@ impl settings::Settings for ThemeSettings { .as_mut() .unwrap() .properties - .extend([( - "theme".to_owned(), - Schema::new_ref("#/definitions/ThemeName".into()), - )]); + .extend([ + ( + "theme".to_owned(), + Schema::new_ref("#/definitions/ThemeName".into()), + ), + ( + "buffer_font_family".to_owned(), + Schema::new_ref("#/definitions/FontFamilies".into()), + ), + ]); root_schema } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index f8d90b7bdc823b0b52348fe94908454002616347..92a5877f514ead1143b89b4d6d217accc153d767 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -20,7 +20,7 @@ mod user_theme; use std::sync::Arc; -use ::settings::Settings; +use ::settings::{Settings, SettingsStore}; pub use default_colors::*; pub use default_theme::*; pub use registry::*; @@ -62,13 +62,21 @@ pub enum LoadThemes { pub fn init(themes_to_load: LoadThemes, cx: &mut AppContext) { cx.set_global(ThemeRegistry::default()); - match themes_to_load { LoadThemes::JustBase => (), LoadThemes::All => cx.global_mut::().load_user_themes(), } - ThemeSettings::register(cx); + + let mut prev_buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size; + cx.observe_global::(move |cx| { + let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size; + if buffer_font_size != prev_buffer_font_size { + prev_buffer_font_size = buffer_font_size; + reset_font_size(cx); + } + }) + .detach(); } pub trait ActiveTheme { diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 2bb8c6648cab0ff1ef79f7f79cf5ee85da8af07c..df66c746de6a16fa51dec22086d765e6d2bfa7ff 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -10,7 +10,7 @@ use picker::{Picker, PickerDelegate}; use settings::{update_settings_file, SettingsStore}; use std::sync::Arc; use theme::{Theme, ThemeMeta, ThemeRegistry, ThemeSettings}; -use ui::{prelude::*, v_stack, ListItem, ListItemSpacing}; +use ui::{prelude::*, v_flex, ListItem, ListItemSpacing}; use util::ResultExt; use workspace::{ui::HighlightedLabel, ModalView, Workspace}; @@ -70,7 +70,7 @@ impl FocusableView for ThemeSelector { impl Render for ThemeSelector { fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - v_stack().w(rems(34.)).child(self.picker.clone()) + v_flex().w(rems(34.)).child(self.picker.clone()) } } diff --git a/crates/ui/src/components/avatar.rs b/crates/ui/src/components/avatar.rs index 9e64e1223c346f4d153fb7d2955b606ff53736ae..a97adb73b7d88ae0dfb1c60c25eff3942c5ad52d 100644 --- a/crates/ui/src/components/avatar.rs +++ b/crates/ui/src/components/avatar.rs @@ -26,6 +26,7 @@ pub enum AvatarShape { #[derive(IntoElement)] pub struct Avatar { image: Img, + size: Option, border_color: Option, is_available: Option, } @@ -36,7 +37,7 @@ impl RenderOnce for Avatar { self = self.shape(AvatarShape::Circle); } - let size = cx.rem_size(); + let size = self.size.unwrap_or_else(|| cx.rem_size()); div() .size(size + px(2.)) @@ -78,6 +79,7 @@ impl Avatar { image: img(src), is_available: None, border_color: None, + size: None, } } @@ -124,4 +126,10 @@ impl Avatar { self.is_available = is_available.into(); self } + + /// Size overrides the avatar size. By default they are 1rem. + pub fn size(mut self, size: impl Into>) -> Self { + self.size = size.into(); + self + } } diff --git a/crates/ui/src/components/button/button.rs b/crates/ui/src/components/button/button.rs index fcc30e633815fd764909c32bb7bbc34b8c5e0624..3ca6d2867222200e44916c3b4adf86e8ecbb9022 100644 --- a/crates/ui/src/components/button/button.rs +++ b/crates/ui/src/components/button/button.rs @@ -362,7 +362,7 @@ impl RenderOnce for Button { }; self.base.child( - h_stack() + h_flex() .gap_1() .when(self.icon_position == Some(IconPosition::Start), |this| { this.children(self.icon.map(|icon| { @@ -375,7 +375,7 @@ impl RenderOnce for Button { })) }) .child( - h_stack() + h_flex() .gap_2() .justify_between() .child( diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs index 3e4b478a9a237c44b95e5de09158e35e40e55449..018d31dafdb4491bc88733117c8e42f66e789aa3 100644 --- a/crates/ui/src/components/button/button_like.rs +++ b/crates/ui/src/components/button/button_like.rs @@ -300,6 +300,7 @@ pub struct ButtonLike { pub(super) selected: bool, pub(super) selected_style: Option, pub(super) width: Option, + pub(super) height: Option, size: ButtonSize, rounding: Option, tooltip: Option AnyView>>, @@ -317,6 +318,7 @@ impl ButtonLike { selected: false, selected_style: None, width: None, + height: None, size: ButtonSize::Default, rounding: Some(ButtonLikeRounding::All), tooltip: None, @@ -325,6 +327,11 @@ impl ButtonLike { } } + pub(crate) fn height(mut self, height: DefiniteLength) -> Self { + self.height = Some(height); + self + } + pub(crate) fn rounding(mut self, rounding: impl Into>) -> Self { self.rounding = rounding.into(); self @@ -417,7 +424,7 @@ impl RenderOnce for ButtonLike { .id(self.id.clone()) .group("") .flex_none() - .h(self.size.height()) + .h(self.height.unwrap_or(self.size.height().into())) .when_some(self.width, |this, width| this.w(width).justify_center()) .when_some(self.rounding, |this, rounding| match rounding { ButtonLikeRounding::All => this.rounded_md(), diff --git a/crates/ui/src/components/button/icon_button.rs b/crates/ui/src/components/button/icon_button.rs index 7c5313497c77ff6532838966001d2cbd4a3688e0..1e37a872922b4473616b551352f8100bbd9b1327 100644 --- a/crates/ui/src/components/button/icon_button.rs +++ b/crates/ui/src/components/button/icon_button.rs @@ -5,9 +5,17 @@ use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, IconName, IconSiz use super::button_icon::ButtonIcon; +/// The shape of an [`IconButton`]. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +pub enum IconButtonShape { + Square, + Wide, +} + #[derive(IntoElement)] pub struct IconButton { base: ButtonLike, + shape: IconButtonShape, icon: IconName, icon_size: IconSize, icon_color: Color, @@ -18,6 +26,7 @@ impl IconButton { pub fn new(id: impl Into, icon: IconName) -> Self { Self { base: ButtonLike::new(id), + shape: IconButtonShape::Wide, icon, icon_size: IconSize::default(), icon_color: Color::Default, @@ -25,6 +34,11 @@ impl IconButton { } } + pub fn shape(mut self, shape: IconButtonShape) -> Self { + self.shape = shape; + self + } + pub fn icon_size(mut self, icon_size: IconSize) -> Self { self.icon_size = icon_size; self @@ -118,14 +132,21 @@ impl RenderOnce for IconButton { let is_selected = self.base.selected; let selected_style = self.base.selected_style; - self.base.child( - ButtonIcon::new(self.icon) - .disabled(is_disabled) - .selected(is_selected) - .selected_icon(self.selected_icon) - .when_some(selected_style, |this, style| this.selected_style(style)) - .size(self.icon_size) - .color(self.icon_color), - ) + self.base + .map(|this| match self.shape { + IconButtonShape::Square => this + .width(self.icon_size.rems().into()) + .height(self.icon_size.rems().into()), + IconButtonShape::Wide => this, + }) + .child( + ButtonIcon::new(self.icon) + .disabled(is_disabled) + .selected(is_selected) + .selected_icon(self.selected_icon) + .when_some(selected_style, |this, style| this.selected_style(style)) + .size(self.icon_size) + .color(self.icon_color), + ) } } diff --git a/crates/ui/src/components/checkbox.rs b/crates/ui/src/components/checkbox.rs index 2180e0061773f8626292224e8fb3a2b97ab4cc65..4b66c7bbeec3c898cb84f968241e92c3b0467ea7 100644 --- a/crates/ui/src/components/checkbox.rs +++ b/crates/ui/src/components/checkbox.rs @@ -103,7 +103,7 @@ impl RenderOnce for Checkbox { ), }; - h_stack() + h_flex() .id(self.id) // Rather than adding `px_1()` to add some space around the checkbox, // we use a larger parent element to create a slightly larger diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 098c54f33cb98d831c0806a2b4ec9f5b0a18ad07..5c4f110a415b2e1e8ef0ac2d36b79d7d8bc2b4be 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -1,5 +1,5 @@ use crate::{ - h_stack, prelude::*, v_stack, Icon, IconName, KeyBinding, Label, List, ListItem, ListSeparator, + h_flex, prelude::*, v_flex, Icon, IconName, KeyBinding, Label, List, ListItem, ListSeparator, ListSubHeader, }; use gpui::{ @@ -234,7 +234,7 @@ impl ContextMenuItem { impl Render for ContextMenu { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { div().elevation_2(cx).flex().flex_row().child( - v_stack() + v_flex() .min_w(px(200.)) .track_focus(&self.focus_handle) .on_mouse_down_out(cx.listener(|this, _, cx| this.cancel(&menu::Cancel, cx))) @@ -277,7 +277,7 @@ impl Render for ContextMenu { let menu = cx.view().downgrade(); let label_element = if let Some(icon) = icon { - h_stack() + h_flex() .gap_1() .child(Label::new(label.clone())) .child(Icon::new(*icon)) @@ -298,7 +298,7 @@ impl Render for ContextMenu { .ok(); }) .child( - h_stack() + h_flex() .w_full() .justify_between() .child(label_element) diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index e0e0583b7cb25e4966c183ae54d9f4742c66935d..d8077f0ffca2d27cf9b8b9660fcef8bfda62173d 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -1,4 +1,4 @@ -use crate::{h_stack, prelude::*, Icon, IconName, IconSize}; +use crate::{h_flex, prelude::*, Icon, IconName, IconSize}; use gpui::{relative, rems, Action, FocusHandle, IntoElement, Keystroke}; #[derive(IntoElement, Clone)] @@ -12,13 +12,13 @@ pub struct KeyBinding { impl RenderOnce for KeyBinding { fn render(self, cx: &mut WindowContext) -> impl IntoElement { - h_stack() + h_flex() .flex_none() .gap_2() .children(self.key_binding.keystrokes().iter().map(|keystroke| { let key_icon = Self::icon_for_key(&keystroke); - h_stack() + h_flex() .flex_none() .gap_0p5() .p_0p5() diff --git a/crates/ui/src/components/list/list.rs b/crates/ui/src/components/list/list.rs index 8c657fdd92a4d35891e2cdae30193ac147741907..436f3e034d17a757e9598fff0584d7239d6f4d65 100644 --- a/crates/ui/src/components/list/list.rs +++ b/crates/ui/src/components/list/list.rs @@ -1,7 +1,7 @@ use gpui::AnyElement; use smallvec::SmallVec; -use crate::{prelude::*, v_stack, Label, ListHeader}; +use crate::{prelude::*, v_flex, Label, ListHeader}; #[derive(IntoElement)] pub struct List { @@ -47,7 +47,7 @@ impl ParentElement for List { impl RenderOnce for List { fn render(self, _cx: &mut WindowContext) -> impl IntoElement { - v_stack().w_full().py_1().children(self.header).map(|this| { + v_flex().w_full().py_1().children(self.header).map(|this| { match (self.children.is_empty(), self.toggle) { (false, _) => this.children(self.children), (true, Some(false)) => this, diff --git a/crates/ui/src/components/list/list_header.rs b/crates/ui/src/components/list/list_header.rs index 1bed11601512348c7227b7e048468946145ffe41..7d47f4d3934fd7c7b68ae3728b3b76c6abeb8971 100644 --- a/crates/ui/src/components/list/list_header.rs +++ b/crates/ui/src/components/list/list_header.rs @@ -1,4 +1,4 @@ -use crate::{h_stack, prelude::*, Disclosure, Label}; +use crate::{h_flex, prelude::*, Disclosure, Label}; use gpui::{AnyElement, ClickEvent}; #[derive(IntoElement)] @@ -76,7 +76,7 @@ impl Selectable for ListHeader { impl RenderOnce for ListHeader { fn render(self, cx: &mut WindowContext) -> impl IntoElement { - h_stack() + h_flex() .id(self.label.clone()) .w_full() .relative() @@ -95,7 +95,7 @@ impl RenderOnce for ListHeader { .w_full() .gap_1() .child( - h_stack() + h_flex() .gap_1() .children(self.toggle.map(|is_open| { Disclosure::new("toggle", is_open).on_toggle(self.on_toggle) @@ -109,7 +109,7 @@ impl RenderOnce for ListHeader { .child(Label::new(self.label.clone()).color(Color::Muted)), ), ) - .child(h_stack().children(self.end_slot)) + .child(h_flex().children(self.end_slot)) .when_some(self.end_hover_slot, |this, end_hover_slot| { this.child( div() diff --git a/crates/ui/src/components/list/list_item.rs b/crates/ui/src/components/list/list_item.rs index d43de18f9317e9f17d303294f1285d2d1c954b42..804e5191abedb4f2b4f6fa79a739023554817f12 100644 --- a/crates/ui/src/components/list/list_item.rs +++ b/crates/ui/src/components/list/list_item.rs @@ -146,7 +146,7 @@ impl ParentElement for ListItem { impl RenderOnce for ListItem { fn render(self, cx: &mut WindowContext) -> impl IntoElement { - h_stack() + h_flex() .id(self.id) .w_full() .relative() @@ -169,7 +169,7 @@ impl RenderOnce for ListItem { }) }) .child( - h_stack() + h_flex() .id("inner_list_item") .w_full() .relative() @@ -219,9 +219,9 @@ impl RenderOnce for ListItem { .child(Disclosure::new("toggle", is_open).on_toggle(self.on_toggle)) })) .child( - h_stack() + h_flex() // HACK: We need to set *any* width value here in order for this container to size correctly. - // Without this the `h_stack` will overflow the parent `inner_list_item`. + // Without this the `h_flex` will overflow the parent `inner_list_item`. .w_px() .flex_1() .gap_1() @@ -230,7 +230,7 @@ impl RenderOnce for ListItem { ) .when_some(self.end_slot, |this, end_slot| { this.justify_between().child( - h_stack() + h_flex() .when(self.end_hover_slot.is_some(), |this| { this.visible() .group_hover("list_item", |this| this.invisible()) @@ -240,7 +240,7 @@ impl RenderOnce for ListItem { }) .when_some(self.end_hover_slot, |this, end_hover_slot| { this.child( - h_stack() + h_flex() .h_full() .absolute() .right_2() diff --git a/crates/ui/src/components/list/list_sub_header.rs b/crates/ui/src/components/list/list_sub_header.rs index fc9f35e175c0d42a1517fc1227a4befc7dfdb2da..e607dcaaa896d87e36594178166922a4d92c26d5 100644 --- a/crates/ui/src/components/list/list_sub_header.rs +++ b/crates/ui/src/components/list/list_sub_header.rs @@ -1,5 +1,5 @@ use crate::prelude::*; -use crate::{h_stack, Icon, IconName, IconSize, Label}; +use crate::{h_flex, Icon, IconName, IconSize, Label}; #[derive(IntoElement)] pub struct ListSubHeader { @@ -25,7 +25,7 @@ impl ListSubHeader { impl RenderOnce for ListSubHeader { fn render(self, _cx: &mut WindowContext) -> impl IntoElement { - h_stack().flex_1().w_full().relative().py_1().child( + h_flex().flex_1().w_full().relative().py_1().child( div() .h_6() .when(self.inset, |this| this.px_2()) diff --git a/crates/ui/src/components/popover.rs b/crates/ui/src/components/popover.rs index acab1e2087667092f20035740d868604528a3935..2e0c5bfec87b84f820bf3bdb512053460e90360f 100644 --- a/crates/ui/src/components/popover.rs +++ b/crates/ui/src/components/popover.rs @@ -1,5 +1,5 @@ use crate::prelude::*; -use crate::v_stack; +use crate::v_flex; use gpui::{ div, AnyElement, Element, IntoElement, ParentElement, RenderOnce, Styled, WindowContext, }; @@ -43,10 +43,10 @@ impl RenderOnce for Popover { div() .flex() .gap_1() - .child(v_stack().elevation_2(cx).px_1().children(self.children)) + .child(v_flex().elevation_2(cx).px_1().children(self.children)) .when_some(self.aside, |this, aside| { this.child( - v_stack() + v_flex() .elevation_2(cx) .bg(cx.theme().colors().surface_background) .px_1() diff --git a/crates/ui/src/components/popover_menu.rs b/crates/ui/src/components/popover_menu.rs index 52c907fab51f5cf07e92e42aaedfe055de995247..39202bf7ef5c68e5722c5b6d55a6cab3676c8ef7 100644 --- a/crates/ui/src/components/popover_menu.rs +++ b/crates/ui/src/components/popover_menu.rs @@ -55,7 +55,7 @@ impl PopoverMenu { } } *menu2.borrow_mut() = None; - cx.notify(); + cx.refresh(); }) .detach(); cx.focus_view(&new_menu); diff --git a/crates/ui/src/components/right_click_menu.rs b/crates/ui/src/components/right_click_menu.rs index cbc924ff59936d4904695cda12d253a28610af36..55cdd93a5bee9d4be4c3f293a262b868b6b7825a 100644 --- a/crates/ui/src/components/right_click_menu.rs +++ b/crates/ui/src/components/right_click_menu.rs @@ -134,6 +134,7 @@ impl Element for RightClickMenu { let position = element_state.position.clone(); let attach = self.attach.clone(); let child_layout_id = element_state.child_layout_id.clone(); + let child_bounds = cx.layout_bounds(child_layout_id.unwrap()); cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { if phase == DispatchPhase::Bubble @@ -154,20 +155,18 @@ impl Element for RightClickMenu { } } *menu2.borrow_mut() = None; - cx.notify(); + cx.refresh(); }) .detach(); cx.focus_view(&new_menu); *menu.borrow_mut() = Some(new_menu); *position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() { - attach - .unwrap() - .corner(cx.layout_bounds(child_layout_id.unwrap())) + attach.unwrap().corner(child_bounds) } else { cx.mouse_position() }; - cx.notify(); + cx.refresh(); } }); } diff --git a/crates/ui/src/components/stack.rs b/crates/ui/src/components/stack.rs index 76f08de911e8e1e60a67c1d9311355f62d55914b..74a5e80575bfe0ebe28334f4533f12b91b1fcfb2 100644 --- a/crates/ui/src/components/stack.rs +++ b/crates/ui/src/components/stack.rs @@ -4,12 +4,12 @@ use crate::StyledExt; /// Horizontally stacks elements. Sets `flex()`, `flex_row()`, `items_center()` #[track_caller] -pub fn h_stack() -> Div { +pub fn h_flex() -> Div { div().h_flex() } /// Vertically stacks elements. Sets `flex()`, `flex_col()` #[track_caller] -pub fn v_stack() -> Div { +pub fn v_flex() -> Div { div().v_flex() } diff --git a/crates/ui/src/components/stories/checkbox.rs b/crates/ui/src/components/stories/checkbox.rs index a064c673935a31bc74878833aca22eaff611dfde..b4a966b67e3ae287c604068ec3854c5cf8373a49 100644 --- a/crates/ui/src/components/stories/checkbox.rs +++ b/crates/ui/src/components/stories/checkbox.rs @@ -2,7 +2,7 @@ use gpui::{Render, ViewContext}; use story::Story; use crate::prelude::*; -use crate::{h_stack, Checkbox}; +use crate::{h_flex, Checkbox}; pub struct CheckboxStory; @@ -12,7 +12,7 @@ impl Render for CheckboxStory { .child(Story::title_for::()) .child(Story::label("Default")) .child( - h_stack() + h_flex() .p_2() .gap_2() .rounded_md() @@ -27,7 +27,7 @@ impl Render for CheckboxStory { ) .child(Story::label("Disabled")) .child( - h_stack() + h_flex() .p_2() .gap_2() .rounded_md() diff --git a/crates/ui/src/components/stories/icon_button.rs b/crates/ui/src/components/stories/icon_button.rs index 6a67183e97c73b3795fc14a71c11a16b6012f549..df9f37b164782f35c9a2ca1cfba6aa96d8783d60 100644 --- a/crates/ui/src/components/stories/icon_button.rs +++ b/crates/ui/src/components/stories/icon_button.rs @@ -117,55 +117,5 @@ impl Render for IconButtonStory { ) .children(vec![StorySection::new().children(buttons)]) .into_element() - - // Story::container() - // .child(Story::title_for::()) - // .child(Story::label("Default")) - // .child(div().w_8().child(IconButton::new("icon_a", Icon::Hash))) - // .child(Story::label("Selected")) - // .child( - // div() - // .w_8() - // .child(IconButton::new("icon_a", Icon::Hash).selected(true)), - // ) - // .child(Story::label("Selected with `selected_icon`")) - // .child( - // div().w_8().child( - // IconButton::new("icon_a", Icon::AudioOn) - // .selected(true) - // .selected_icon(Icon::AudioOff), - // ), - // ) - // .child(Story::label("Disabled")) - // .child( - // div() - // .w_8() - // .child(IconButton::new("icon_a", Icon::Hash).disabled(true)), - // ) - // .child(Story::label("With `on_click`")) - // .child( - // div() - // .w_8() - // .child( - // IconButton::new("with_on_click", Icon::Ai).on_click(|_event, _cx| { - // println!("Clicked!"); - // }), - // ), - // ) - // .child(Story::label("With `tooltip`")) - // .child( - // div().w_8().child( - // IconButton::new("with_tooltip", Icon::MessageBubbles) - // .tooltip(|cx| Tooltip::text("Open messages", cx)), - // ), - // ) - // .child(Story::label("Selected with `tooltip`")) - // .child( - // div().w_8().child( - // IconButton::new("selected_with_tooltip", Icon::InlayHint) - // .selected(true) - // .tooltip(|cx| Tooltip::text("Toggle inlay hints", cx)), - // ), - // ) } } diff --git a/crates/ui/src/components/stories/list_item.rs b/crates/ui/src/components/stories/list_item.rs index a25b07df849aeb67a185e3984ab11ec341b07045..f2af011db8e8554b07434e699b61c8492040ede8 100644 --- a/crates/ui/src/components/stories/list_item.rs +++ b/crates/ui/src/components/stories/list_item.rs @@ -60,7 +60,7 @@ impl Render for ListItemStory { ListItem::new("with_end_hover_slot") .child("Hello, world!") .end_slot( - h_stack() + h_flex() .gap_2() .child(Avatar::new(SharedUrl::from( "https://avatars.githubusercontent.com/u/1789?v=4", diff --git a/crates/ui/src/components/stories/tab.rs b/crates/ui/src/components/stories/tab.rs index bd7b602620938775b32be3d46a41b519f2639f63..541af75ba4762ef60890d1f21e4ca101680b59a1 100644 --- a/crates/ui/src/components/stories/tab.rs +++ b/crates/ui/src/components/stories/tab.rs @@ -3,7 +3,7 @@ use std::cmp::Ordering; use gpui::Render; use story::Story; -use crate::{prelude::*, TabPosition}; +use crate::{prelude::*, IconButtonShape, TabPosition}; use crate::{Indicator, Tab}; pub struct TabStory; @@ -13,10 +13,10 @@ impl Render for TabStory { Story::container() .child(Story::title_for::()) .child(Story::label("Default")) - .child(h_stack().child(Tab::new("tab_1").child("Tab 1"))) + .child(h_flex().child(Tab::new("tab_1").child("Tab 1"))) .child(Story::label("With indicator")) .child( - h_stack().child( + h_flex().child( Tab::new("tab_1") .start_slot(Indicator::dot().color(Color::Warning)) .child("Tab 1"), @@ -24,10 +24,11 @@ impl Render for TabStory { ) .child(Story::label("With close button")) .child( - h_stack().child( + h_flex().child( Tab::new("tab_1") .end_slot( IconButton::new("close_button", IconName::Close) + .shape(IconButtonShape::Square) .icon_color(Color::Muted) .size(ButtonSize::None) .icon_size(IconSize::XSmall), @@ -37,13 +38,13 @@ impl Render for TabStory { ) .child(Story::label("List of tabs")) .child( - h_stack() + h_flex() .child(Tab::new("tab_1").child("Tab 1")) .child(Tab::new("tab_2").child("Tab 2")), ) .child(Story::label("List of tabs with first tab selected")) .child( - h_stack() + h_flex() .child( Tab::new("tab_1") .selected(true) @@ -64,7 +65,7 @@ impl Render for TabStory { ) .child(Story::label("List of tabs with last tab selected")) .child( - h_stack() + h_flex() .child( Tab::new("tab_1") .position(TabPosition::First) @@ -89,7 +90,7 @@ impl Render for TabStory { ) .child(Story::label("List of tabs with second tab selected")) .child( - h_stack() + h_flex() .child( Tab::new("tab_1") .position(TabPosition::First) diff --git a/crates/ui/src/components/stories/tab_bar.rs b/crates/ui/src/components/stories/tab_bar.rs index 289ceff9a6f576739daacd02b57e260b295a7ae8..d6d42fa5e0f3c3ee5540bbd6cdbf4c21ec7edda4 100644 --- a/crates/ui/src/components/stories/tab_bar.rs +++ b/crates/ui/src/components/stories/tab_bar.rs @@ -35,7 +35,7 @@ impl Render for TabBarStory { .child(Story::title_for::()) .child(Story::label("Default")) .child( - h_stack().child( + h_flex().child( TabBar::new("tab_bar_1") .start_child( IconButton::new("navigate_backward", IconName::ArrowLeft) diff --git a/crates/ui/src/components/stories/toggle_button.rs b/crates/ui/src/components/stories/toggle_button.rs index 518165345ca8b483f7c299a22aae4cc91566b839..da2a2512c44a7705d817a12a25b6355f7f792c04 100644 --- a/crates/ui/src/components/stories/toggle_button.rs +++ b/crates/ui/src/components/stories/toggle_button.rs @@ -25,7 +25,7 @@ impl Render for ToggleButtonStory { StorySection::new().child( StoryItem::new( "Toggle button group", - h_stack() + h_flex() .child( ToggleButton::new(1, "Apple") .style(ButtonStyle::Filled) @@ -59,7 +59,7 @@ impl Render for ToggleButtonStory { StorySection::new().child( StoryItem::new( "Toggle button group with selection", - h_stack() + h_flex() .child( ToggleButton::new(1, "Apple") .style(ButtonStyle::Filled) diff --git a/crates/ui/src/components/tab.rs b/crates/ui/src/components/tab.rs index 351c851bb9088db8907e7defff71fe21d79ffe75..ade939fdaabcddfcdc6ac0a22b0222ff4f2b4170 100644 --- a/crates/ui/src/components/tab.rs +++ b/crates/ui/src/components/tab.rs @@ -48,7 +48,9 @@ impl Tab { } } - pub const HEIGHT_IN_REMS: f32 = 30. / 16.; + pub const CONTAINER_HEIGHT_IN_REMS: f32 = 29. / 16.; + + const CONTENT_HEIGHT_IN_REMS: f32 = 28. / 16.; pub fn position(mut self, position: TabPosition) -> Self { self.position = position; @@ -111,7 +113,7 @@ impl RenderOnce for Tab { }; self.div - .h(rems(Self::HEIGHT_IN_REMS)) + .h(rems(Self::CONTAINER_HEIGHT_IN_REMS)) .bg(tab_bg) .border_color(cx.theme().colors().border) .map(|this| match self.position { @@ -135,17 +137,17 @@ impl RenderOnce for Tab { }) .cursor_pointer() .child( - h_stack() + h_flex() .group("") .relative() - .h_full() + .h(rems(Self::CONTENT_HEIGHT_IN_REMS)) .px_5() .gap_1() .text_color(text_color) // .hover(|style| style.bg(tab_hover_bg)) // .active(|style| style.bg(tab_active_bg)) .child( - h_stack() + h_flex() .w_3() .h_3() .justify_center() @@ -157,7 +159,7 @@ impl RenderOnce for Tab { .children(self.start_slot), ) .child( - h_stack() + h_flex() .w_3() .h_3() .justify_center() diff --git a/crates/ui/src/components/tab_bar.rs b/crates/ui/src/components/tab_bar.rs index 0a86f1ae0ce52713bde0761340a86c6d8676a0ec..adee8389e47a8db3f343e01cc9d81a9c1d08ead4 100644 --- a/crates/ui/src/components/tab_bar.rs +++ b/crates/ui/src/components/tab_bar.rs @@ -90,7 +90,7 @@ impl ParentElement for TabBar { impl RenderOnce for TabBar { fn render(self, cx: &mut WindowContext) -> impl IntoElement { - const HEIGHT_IN_REMS: f32 = 30. / 16.; + const HEIGHT_IN_REMS: f32 = 29. / 16.; div() .id(self.id) @@ -102,7 +102,7 @@ impl RenderOnce for TabBar { .bg(cx.theme().colors().tab_bar_background) .when(!self.start_children.is_empty(), |this| { this.child( - h_stack() + h_flex() .flex_none() .gap_1() .px_1() @@ -129,7 +129,7 @@ impl RenderOnce for TabBar { .border_color(cx.theme().colors().border), ) .child( - h_stack() + h_flex() .id("tabs") .z_index(2) .flex_grow() @@ -142,7 +142,7 @@ impl RenderOnce for TabBar { ) .when(!self.end_children.is_empty(), |this| { this.child( - h_stack() + h_flex() .flex_none() .gap_1() .px_1() diff --git a/crates/ui/src/components/tooltip.rs b/crates/ui/src/components/tooltip.rs index 77fd8d6c0b109cac05bd559fce71529436b7a825..f76085daa35ccfa37131b44bca2d83b9532cffb5 100644 --- a/crates/ui/src/components/tooltip.rs +++ b/crates/ui/src/components/tooltip.rs @@ -3,7 +3,7 @@ use settings::Settings; use theme::ThemeSettings; use crate::prelude::*; -use crate::{h_stack, v_stack, Color, KeyBinding, Label, LabelSize, StyledExt}; +use crate::{h_flex, v_flex, Color, KeyBinding, Label, LabelSize, StyledExt}; pub struct Tooltip { title: SharedString, @@ -73,7 +73,7 @@ impl Render for Tooltip { overlay().child( // padding to avoid mouse cursor div().pl_2().pt_2p5().child( - v_stack() + v_flex() .elevation_2(cx) .font(ui_font) .text_ui() @@ -81,7 +81,7 @@ impl Render for Tooltip { .py_1() .px_2() .child( - h_stack() + h_flex() .gap_4() .child(self.title.clone()) .when_some(self.key_binding.clone(), |this, key_binding| { diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs index 69d1d0583d70e8176f577ce7d7fa651845df82f6..837d93db2d20017742a618202cd41eb89075ce6d 100644 --- a/crates/ui/src/prelude.rs +++ b/crates/ui/src/prelude.rs @@ -13,7 +13,7 @@ pub use crate::fixed::*; pub use crate::selectable::*; pub use crate::styles::{vh, vw}; pub use crate::visible_on_hover::*; -pub use crate::{h_stack, v_stack}; +pub use crate::{h_flex, v_flex}; pub use crate::{Button, ButtonSize, ButtonStyle, IconButton, SelectableButton}; pub use crate::{ButtonCommon, Color, StyledExt}; pub use crate::{Headline, HeadlineSize}; diff --git a/crates/vcs_menu/src/lib.rs b/crates/vcs_menu/src/lib.rs index 0774c6f5755324de77b16f691ee0dde11ed38f16..67a12a852bdaeeae08f5fbcdc41ab11a91572605 100644 --- a/crates/vcs_menu/src/lib.rs +++ b/crates/vcs_menu/src/lib.rs @@ -9,7 +9,7 @@ use gpui::{ use picker::{Picker, PickerDelegate}; use std::{ops::Not, sync::Arc}; use ui::{ - h_stack, v_stack, Button, ButtonCommon, Clickable, HighlightedLabel, Label, LabelCommon, + h_flex, v_flex, Button, ButtonCommon, Clickable, HighlightedLabel, Label, LabelCommon, LabelSize, ListItem, ListItemSpacing, Selectable, }; use util::ResultExt; @@ -65,7 +65,7 @@ impl FocusableView for BranchList { impl Render for BranchList { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - v_stack() + v_flex() .w(rems(self.rem_width)) .child(self.picker.clone()) .on_mouse_down_out(cx.listener(|this, _, cx| { @@ -290,7 +290,7 @@ impl PickerDelegate for BranchListDelegate { } fn render_header(&self, _: &mut ViewContext>) -> Option { let label = if self.last_query.is_empty() { - h_stack() + h_flex() .ml_3() .child(Label::new("Recent branches").size(LabelSize::Small)) } else { @@ -298,7 +298,7 @@ impl PickerDelegate for BranchListDelegate { let suffix = if self.matches.len() == 1 { "" } else { "es" }; Label::new(format!("{} match{}", self.matches.len(), suffix)).size(LabelSize::Small) }); - h_stack() + h_flex() .px_3() .h_full() .justify_between() @@ -313,7 +313,7 @@ impl PickerDelegate for BranchListDelegate { } Some( - h_stack().mr_3().pb_2().child(h_stack().w_full()).child( + h_flex().mr_3().pb_2().child(h_flex().w_full()).child( Button::new("branch-picker-create-branch-button", "Create branch").on_click( cx.listener(|_, _, cx| { cx.spawn(|picker, mut cx| async move { diff --git a/crates/welcome/src/base_keymap_picker.rs b/crates/welcome/src/base_keymap_picker.rs index e22c89cef882d6e1977e2e349f49f5f4d98c14a1..7913e4df37a39140764722c5f9554a247aef55ec 100644 --- a/crates/welcome/src/base_keymap_picker.rs +++ b/crates/welcome/src/base_keymap_picker.rs @@ -62,7 +62,7 @@ impl BaseKeymapSelector { impl Render for BaseKeymapSelector { fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - v_stack().w(rems(34.)).child(self.picker.clone()) + v_flex().w(rems(34.)).child(self.picker.clone()) } } diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 76988fadb06b9124f1b197178cb0c89106670f7a..677b57a0225059430b141ca23573d42433f52b77 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -60,8 +60,8 @@ pub struct WelcomePage { impl Render for WelcomePage { fn render(&mut self, cx: &mut gpui::ViewContext) -> impl IntoElement { - h_stack().full().track_focus(&self.focus_handle).child( - v_stack() + h_flex().full().track_focus(&self.focus_handle).child( + v_flex() .w_96() .gap_4() .mx_auto() @@ -74,19 +74,19 @@ impl Render for WelcomePage { .mx_auto(), ) .child( - h_stack() + h_flex() .justify_center() .child(Label::new("Code at the speed of thought")), ) .child( - v_stack() + v_flex() .gap_2() .child( Button::new("choose-theme", "Choose a theme") .full_width() .on_click(cx.listener(|this, _, cx| { this.telemetry - .report_app_event("welcome page: change theme"); + .report_app_event("welcome page: change theme".to_string()); this.workspace .update(cx, |workspace, cx| { theme_selector::toggle( @@ -102,8 +102,9 @@ impl Render for WelcomePage { Button::new("choose-keymap", "Choose a keymap") .full_width() .on_click(cx.listener(|this, _, cx| { - this.telemetry - .report_app_event("welcome page: change keymap"); + this.telemetry.report_app_event( + "welcome page: change keymap".to_string(), + ); this.workspace .update(cx, |workspace, cx| { base_keymap_picker::toggle( @@ -119,7 +120,8 @@ impl Render for WelcomePage { Button::new("install-cli", "Install the CLI") .full_width() .on_click(cx.listener(|this, _, cx| { - this.telemetry.report_app_event("welcome page: install cli"); + this.telemetry + .report_app_event("welcome page: install cli".to_string()); cx.app_mut() .spawn( |cx| async move { install_cli::install_cli(&cx).await }, @@ -129,7 +131,7 @@ impl Render for WelcomePage { ), ) .child( - v_stack() + v_flex() .p_3() .gap_2() .bg(cx.theme().colors().elevated_surface_background) @@ -137,7 +139,7 @@ impl Render for WelcomePage { .border_color(cx.theme().colors().border) .rounded_md() .child( - h_stack() + h_flex() .gap_2() .child( Checkbox::new( @@ -150,8 +152,9 @@ impl Render for WelcomePage { ) .on_click(cx.listener( move |this, selection, cx| { - this.telemetry - .report_app_event("welcome page: toggle vim"); + this.telemetry.report_app_event( + "welcome page: toggle vim".to_string(), + ); this.update_settings::( selection, cx, @@ -163,7 +166,7 @@ impl Render for WelcomePage { .child(Label::new("Enable vim mode")), ) .child( - h_stack() + h_flex() .gap_2() .child( Checkbox::new( @@ -177,7 +180,7 @@ impl Render for WelcomePage { .on_click(cx.listener( move |this, selection, cx| { this.telemetry.report_app_event( - "welcome page: toggle metric telemetry", + "welcome page: toggle metric telemetry".to_string(), ); this.update_settings::( selection, @@ -201,7 +204,7 @@ impl Render for WelcomePage { .child(Label::new("Send anonymous usage data")), ) .child( - h_stack() + h_flex() .gap_2() .child( Checkbox::new( @@ -215,7 +218,8 @@ impl Render for WelcomePage { .on_click(cx.listener( move |this, selection, cx| { this.telemetry.report_app_event( - "welcome page: toggle diagnostic telemetry", + "welcome page: toggle diagnostic telemetry" + .to_string(), ); this.update_settings::( selection, @@ -247,7 +251,8 @@ impl WelcomePage { pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> View { let this = cx.new_view(|cx| { cx.on_release(|this: &mut Self, _, _| { - this.telemetry.report_app_event("welcome page: close"); + this.telemetry + .report_app_event("welcome page: close".to_string()); }) .detach(); @@ -306,6 +311,10 @@ impl Item for WelcomePage { .into_any_element() } + fn telemetry_event_text(&self) -> Option<&'static str> { + Some("welcome page") + } + fn show_toolbar(&self) -> bool { false } diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 0c752597262ca1f64a996d09795db289941a30b2..4ae408993572e06e97635f8fb579fbee1395238b 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -7,14 +7,14 @@ use gpui::{ }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use settings::SettingsStore; use std::sync::Arc; -use ui::{h_stack, ContextMenu, IconButton, Tooltip}; +use ui::{h_flex, ContextMenu, IconButton, Tooltip}; use ui::{prelude::*, right_click_menu}; const RESIZE_HANDLE_SIZE: Pixels = Pixels(6.); pub enum PanelEvent { - ChangePosition, ZoomIn, ZoomOut, Activate, @@ -177,7 +177,7 @@ impl DockPosition { struct PanelEntry { panel: Arc, - _subscriptions: [Subscription; 2], + _subscriptions: [Subscription; 3], } pub struct PanelButtons { @@ -321,9 +321,15 @@ impl Dock { ) { let subscriptions = [ cx.observe(&panel, |_, _, cx| cx.notify()), - cx.subscribe(&panel, move |this, panel, event, cx| match event { - PanelEvent::ChangePosition => { + cx.observe_global::({ + let workspace = workspace.clone(); + let panel = panel.clone(); + + move |this, cx| { let new_position = panel.read(cx).position(cx); + if new_position == this.position { + return; + } let Ok(new_dock) = workspace.update(cx, |workspace, cx| { if panel.is_zoomed(cx) { @@ -354,6 +360,8 @@ impl Dock { } }); } + }), + cx.subscribe(&panel, move |this, panel, event, cx| match event { PanelEvent::ZoomIn => { this.set_panel_zoomed(&panel.to_any(), true, cx); if !panel.focus_handle(cx).contains_focused(cx) { @@ -575,7 +583,7 @@ impl Render for Dock { Axis::Horizontal => this.min_w(size).h_full(), Axis::Vertical => this.min_h(size).w_full(), }) - .child(entry.panel.to_any()), + .child(entry.panel.to_any().cached()), ) .child(handle) } else { @@ -674,7 +682,7 @@ impl Render for PanelButtons { ) }); - h_stack().gap_0p5().children(buttons) + h_flex().gap_0p5().children(buttons) } } @@ -737,7 +745,7 @@ pub mod test { fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { self.position = position; - cx.emit(PanelEvent::ChangePosition); + cx.update_global::(|_, _| {}); } fn size(&self, _: &WindowContext) -> Pixels { diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index c629edc696b87e0552ca05956e2a1c5cf3b5e6b0..4f696e4a335040eebc10396262382f60c8f2821f 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -114,6 +114,8 @@ pub trait Item: FocusableView + EventEmitter { } fn tab_content(&self, detail: Option, selected: bool, cx: &WindowContext) -> AnyElement; + fn telemetry_event_text(&self) -> Option<&'static str>; + /// (model id, Item) fn for_each_project_item( &self, @@ -225,6 +227,7 @@ pub trait ItemHandle: 'static + Send { fn tab_tooltip_text(&self, cx: &AppContext) -> Option; fn tab_description(&self, detail: usize, cx: &AppContext) -> Option; fn tab_content(&self, detail: Option, selected: bool, cx: &WindowContext) -> AnyElement; + fn telemetry_event_text(&self, cx: &WindowContext) -> Option<&'static str>; fn dragged_tab_content(&self, detail: Option, cx: &WindowContext) -> AnyElement; fn project_path(&self, cx: &AppContext) -> Option; fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>; @@ -313,6 +316,10 @@ impl ItemHandle for View { self.read(cx).tab_tooltip_text(cx) } + fn telemetry_event_text(&self, cx: &WindowContext) -> Option<&'static str> { + self.read(cx).telemetry_event_text() + } + fn tab_description(&self, detail: usize, cx: &AppContext) -> Option { self.read(cx).tab_description(detail, cx) } @@ -809,27 +816,6 @@ pub mod test { Edit, } - // impl Clone for TestItem { - // fn clone(&self) -> Self { - // Self { - // state: self.state.clone(), - // label: self.label.clone(), - // save_count: self.save_count, - // save_as_count: self.save_as_count, - // reload_count: self.reload_count, - // is_dirty: self.is_dirty, - // is_singleton: self.is_singleton, - // has_conflict: self.has_conflict, - // project_items: self.project_items.clone(), - // nav_history: None, - // tab_descriptions: None, - // tab_detail: Default::default(), - // workspace_id: self.workspace_id, - // focus_handle: self.focus_handle.clone(), - // } - // } - // } - impl TestProjectItem { pub fn new(id: u64, path: &str, cx: &mut AppContext) -> Model { let entry_id = Some(ProjectEntryId::from_proto(id)); @@ -943,6 +929,10 @@ pub mod test { }) } + fn telemetry_event_text(&self) -> Option<&'static str> { + None + } + fn tab_content( &self, detail: Option, diff --git a/crates/workspace/src/modal_layer.rs b/crates/workspace/src/modal_layer.rs index 627581c4760c0209de3379d5a2cbf0ead7cbbc62..d940f1d16842a712bb8aaef23281c5e8ab06217f 100644 --- a/crates/workspace/src/modal_layer.rs +++ b/crates/workspace/src/modal_layer.rs @@ -2,7 +2,7 @@ use gpui::{ div, prelude::*, px, AnyView, DismissEvent, FocusHandle, ManagedView, Render, Subscription, View, ViewContext, WindowContext, }; -use ui::{h_stack, v_stack}; +use ui::{h_flex, v_flex}; pub trait ModalView: ManagedView { fn on_before_dismiss(&mut self, _: &mut ViewContext) -> bool { @@ -120,7 +120,7 @@ impl Render for ModalLayer { .left_0() .z_index(169) .child( - v_stack() + v_flex() .h(px(0.0)) .top_20() .flex() @@ -128,7 +128,7 @@ impl Render for ModalLayer { .items_center() .track_focus(&active_modal.focus_handle) .child( - h_stack() + h_flex() .on_mouse_down_out(cx.listener(|this, _, cx| { this.hide_modal(cx); })) diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index cc2450587d7304a8491aa59137cac9ae758da5ee..6e7590c7d3886493f00d2b43728326549e5665f8 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -173,7 +173,7 @@ pub mod simple_message_notification { }; use std::sync::Arc; use ui::prelude::*; - use ui::{h_stack, v_stack, Button, Icon, IconName, Label, StyledExt}; + use ui::{h_flex, v_flex, Button, Icon, IconName, Label, StyledExt}; pub struct MessageNotification { message: SharedString, @@ -218,11 +218,11 @@ pub mod simple_message_notification { impl Render for MessageNotification { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - v_stack() + v_flex() .elevation_3(cx) .p_4() .child( - h_stack() + h_flex() .justify_between() .child(div().max_w_80().child(Label::new(self.message.clone()))) .child( diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index aec33c2dd32ee9bda77e7fe7927e1346a86178df..655acc29c004e5750a7d949ab1f01088ee04ad58 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -32,10 +32,10 @@ use std::{ use theme::ThemeSettings; use ui::{ - prelude::*, right_click_menu, ButtonSize, Color, IconButton, IconName, IconSize, Indicator, - Label, Tab, TabBar, TabPosition, Tooltip, + prelude::*, right_click_menu, ButtonSize, Color, IconButton, IconButtonShape, IconName, + IconSize, Indicator, Label, Tab, TabBar, TabPosition, Tooltip, }; -use ui::{v_stack, ContextMenu}; +use ui::{v_flex, ContextMenu}; use util::{maybe, truncate_and_remove_front, ResultExt}; #[derive(PartialEq, Clone, Copy, Deserialize, Debug)] @@ -60,24 +60,6 @@ pub enum SaveIntent { #[derive(Clone, Deserialize, PartialEq, Debug)] pub struct ActivateItem(pub usize); -// #[derive(Clone, PartialEq)] -// pub struct CloseItemById { -// pub item_id: usize, -// pub pane: WeakView, -// } - -// #[derive(Clone, PartialEq)] -// pub struct CloseItemsToTheLeftById { -// pub item_id: usize, -// pub pane: WeakView, -// } - -// #[derive(Clone, PartialEq)] -// pub struct CloseItemsToTheRightById { -// pub item_id: usize, -// pub pane: WeakView, -// } - #[derive(Clone, PartialEq, Debug, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct CloseActiveItem { @@ -237,8 +219,8 @@ pub struct NavigationEntry { #[derive(Clone)] pub struct DraggedTab { pub pane: View, + pub item: Box, pub ix: usize, - pub item_id: EntityId, pub detail: usize, pub is_active: bool, } @@ -289,7 +271,7 @@ impl Pane { custom_drop_handle: None, can_split: true, render_tab_bar_buttons: Rc::new(move |pane, cx| { - h_stack() + h_flex() .gap_2() .child( IconButton::new("plus", IconName::Plus) @@ -1226,125 +1208,6 @@ impl Pane { cx.emit(Event::Split(direction)); } - // fn deploy_split_menu(&mut self, cx: &mut ViewContext) { - // self.tab_bar_context_menu.handle.update(cx, |menu, cx| { - // menu.toggle( - // Default::default(), - // AnchorCorner::TopRight, - // vec![ - // ContextMenuItem::action("Split Right", SplitRight), - // ContextMenuItem::action("Split Left", SplitLeft), - // ContextMenuItem::action("Split Up", SplitUp), - // ContextMenuItem::action("Split Down", SplitDown), - // ], - // cx, - // ); - // }); - - // self.tab_bar_context_menu.kind = TabBarContextMenuKind::Split; - // } - - // fn deploy_new_menu(&mut self, cx: &mut ViewContext) { - // self.tab_bar_context_menu.handle.update(cx, |menu, cx| { - // menu.toggle( - // Default::default(), - // AnchorCorner::TopRight, - // vec![ - // ContextMenuItem::action("New File", NewFile), - // ContextMenuItem::action("New Terminal", NewCenterTerminal), - // ContextMenuItem::action("New Search", NewSearch), - // ], - // cx, - // ); - // }); - - // self.tab_bar_context_menu.kind = TabBarContextMenuKind::New; - // } - - // fn deploy_tab_context_menu( - // &mut self, - // position: Vector2F, - // target_item_id: usize, - // cx: &mut ViewContext, - // ) { - // let active_item_id = self.items[self.active_item_index].id(); - // let is_active_item = target_item_id == active_item_id; - // let target_pane = cx.weak_handle(); - - // // The `CloseInactiveItems` action should really be called "CloseOthers" and the behaviour should be dynamically based on the tab the action is ran on. Currently, this is a weird action because you can run it on a non-active tab and it will close everything by the actual active tab - - // self.tab_context_menu.update(cx, |menu, cx| { - // menu.show( - // position, - // AnchorCorner::TopLeft, - // if is_active_item { - // vec![ - // ContextMenuItem::action( - // "Close Active Item", - // CloseActiveItem { save_intent: None }, - // ), - // ContextMenuItem::action("Close Inactive Items", CloseInactiveItems), - // ContextMenuItem::action("Close Clean Items", CloseCleanItems), - // ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft), - // ContextMenuItem::action("Close Items To The Right", CloseItemsToTheRight), - // ContextMenuItem::action( - // "Close All Items", - // CloseAllItems { save_intent: None }, - // ), - // ] - // } else { - // // In the case of the user right clicking on a non-active tab, for some item-closing commands, we need to provide the id of the tab, for the others, we can reuse the existing command. - // vec![ - // ContextMenuItem::handler("Close Inactive Item", { - // let pane = target_pane.clone(); - // move |cx| { - // if let Some(pane) = pane.upgrade(cx) { - // pane.update(cx, |pane, cx| { - // pane.close_item_by_id( - // target_item_id, - // SaveIntent::Close, - // cx, - // ) - // .detach_and_log_err(cx); - // }) - // } - // } - // }), - // ContextMenuItem::action("Close Inactive Items", CloseInactiveItems), - // ContextMenuItem::action("Close Clean Items", CloseCleanItems), - // ContextMenuItem::handler("Close Items To The Left", { - // let pane = target_pane.clone(); - // move |cx| { - // if let Some(pane) = pane.upgrade(cx) { - // pane.update(cx, |pane, cx| { - // pane.close_items_to_the_left_by_id(target_item_id, cx) - // .detach_and_log_err(cx); - // }) - // } - // } - // }), - // ContextMenuItem::handler("Close Items To The Right", { - // let pane = target_pane.clone(); - // move |cx| { - // if let Some(pane) = pane.upgrade(cx) { - // pane.update(cx, |pane, cx| { - // pane.close_items_to_the_right_by_id(target_item_id, cx) - // .detach_and_log_err(cx); - // }) - // } - // } - // }), - // ContextMenuItem::action( - // "Close All Items", - // CloseAllItems { save_intent: None }, - // ), - // ] - // }, - // cx, - // ); - // }); - // } - pub fn toolbar(&self) -> &View { &self.toolbar } @@ -1447,9 +1310,9 @@ impl Pane { ) .on_drag( DraggedTab { + item: item.boxed_clone(), pane: cx.view().clone(), detail, - item_id, is_active, ix, }, @@ -1478,6 +1341,7 @@ impl Pane { .start_slot::(indicator) .end_slot( IconButton::new("close tab", IconName::Close) + .shape(IconButtonShape::Square) .icon_color(Color::Muted) .size(ButtonSize::None) .icon_size(IconSize::XSmall) @@ -1580,7 +1444,7 @@ impl Pane { .track_scroll(self.tab_bar_scroll_handle.clone()) .when(self.display_nav_history_buttons, |tab_bar| { tab_bar.start_child( - h_stack() + h_flex() .gap_2() .child( IconButton::new("navigate_backward", IconName::ArrowLeft) @@ -1739,7 +1603,7 @@ impl Pane { } let mut to_pane = cx.view().clone(); let split_direction = self.drag_split_direction; - let item_id = dragged_tab.item_id; + let item_id = dragged_tab.item.item_id(); let from_pane = dragged_tab.pane.clone(); self.workspace .update(cx, |_, cx| { @@ -1854,7 +1718,7 @@ impl FocusableView for Pane { impl Render for Pane { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - v_stack() + v_flex() .key_context("Pane") .track_focus(&self.focus_handle) .size_full() @@ -2739,8 +2603,7 @@ mod tests { impl Render for DraggedTab { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); - let item = &self.pane.read(cx).items[self.ix]; - let label = item.tab_content(Some(self.detail), false, cx); + let label = self.item.tab_content(Some(self.detail), false, cx); Tab::new("") .selected(self.is_active) .child(label) diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 3dcdeec37f4e2cceb9e2159d6577b303f5355979..ce58e51678c589c39e97666d71ccc097bc0a5324 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -3,8 +3,8 @@ use anyhow::{anyhow, Result}; use call::{ActiveCall, ParticipantLocation}; use collections::HashMap; use gpui::{ - point, size, AnyWeakView, Axis, Bounds, Entity as _, IntoElement, Model, Pixels, Point, View, - ViewContext, + point, size, AnyView, AnyWeakView, Axis, Bounds, Entity as _, IntoElement, Model, Pixels, + Point, View, ViewContext, }; use parking_lot::Mutex; use project::Project; @@ -244,7 +244,7 @@ impl Member { .relative() .flex_1() .size_full() - .child(pane.clone()) + .child(AnyView::from(pane.clone()).cached()) .when_some(leader_border, |this, color| { this.child( div() @@ -701,7 +701,7 @@ mod element { workspace .update(cx, |this, cx| this.schedule_serialize(cx)) .log_err(); - cx.notify(); + cx.refresh(); } fn push_handle( @@ -757,7 +757,7 @@ mod element { workspace .update(cx, |this, cx| this.schedule_serialize(cx)) .log_err(); - cx.notify(); + cx.refresh(); } } } diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index d03c7b3d0f73c21bde176d416060ccc200aaa62e..56aa6e4322bce652aacda0ddb1f0c77cef61b7ee 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -1,5 +1,3 @@ -//#![allow(dead_code)] - pub mod model; use std::path::Path; diff --git a/crates/workspace/src/shared_screen.rs b/crates/workspace/src/shared_screen.rs index 5b1ca6477ee99b47ad7abc08e64e0421444c5dfd..bfc16022749c10e5862933dd82d777e3adcbd5f2 100644 --- a/crates/workspace/src/shared_screen.rs +++ b/crates/workspace/src/shared_screen.rs @@ -12,7 +12,7 @@ use gpui::{ WindowContext, }; use std::sync::{Arc, Weak}; -use ui::{h_stack, prelude::*, Icon, IconName, Label}; +use ui::{h_flex, prelude::*, Icon, IconName, Label}; pub enum Event { Close, @@ -98,7 +98,7 @@ impl Item for SharedScreen { selected: bool, _: &WindowContext<'_>, ) -> gpui::AnyElement { - h_stack() + h_flex() .gap_1() .child(Icon::new(IconName::Screen)) .child( @@ -111,6 +111,10 @@ impl Item for SharedScreen { .into_any() } + fn telemetry_event_text(&self) -> Option<&'static str> { + None + } + fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { self.nav_history = Some(history); } diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index bfa1a8f8ba0f39e382b02aa9735720719fdcfbfb..34a1412533bf14829e193df680085bc50c1166c2 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -4,7 +4,7 @@ use gpui::{ WindowContext, }; use std::any::TypeId; -use ui::{h_stack, prelude::*}; +use ui::{h_flex, prelude::*}; use util::ResultExt; pub trait StatusItemView: Render { @@ -50,14 +50,14 @@ impl Render for StatusBar { impl StatusBar { fn render_left_tools(&self, _: &mut ViewContext) -> impl IntoElement { - h_stack() + h_flex() .items_center() .gap_2() .children(self.left_items.iter().map(|item| item.to_any())) } fn render_right_tools(&self, _: &mut ViewContext) -> impl IntoElement { - h_stack() + h_flex() .items_center() .gap_2() .children(self.right_items.iter().rev().map(|item| item.to_any())) diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index cc072b08b9eac0cf13039dd45216483018d5701a..3d5df3294e1fc84faab10c69c46c7ce424682aad 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -4,7 +4,7 @@ use gpui::{ WindowContext, }; use ui::prelude::*; -use ui::{h_stack, v_stack}; +use ui::{h_flex, v_flex}; pub enum ToolbarItemEvent { ChangeLocation(ToolbarItemLocation), @@ -103,18 +103,18 @@ impl Render for Toolbar { let has_left_items = self.left_items().count() > 0; let has_right_items = self.right_items().count() > 0; - v_stack() + v_flex() .p_2() .when(has_left_items || has_right_items, |this| this.gap_2()) .border_b() .border_color(cx.theme().colors().border_variant) .bg(cx.theme().colors().toolbar_background) .child( - h_stack() + h_flex() .justify_between() .when(has_left_items, |this| { this.child( - h_stack() + h_flex() .flex_1() .justify_start() .children(self.left_items().map(|item| item.to_any())), @@ -122,7 +122,7 @@ impl Render for Toolbar { }) .when(has_right_items, |this| { this.child( - h_stack() + h_flex() .flex_1() .justify_end() .children(self.right_items().map(|item| item.to_any())), diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ca463e76e058c645839f853eadd8e0877ecca6da..a1fb954d470ca9fd5a788f2c272274844dfd7ed4 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1271,7 +1271,9 @@ impl Workspace { } pub fn open(&mut self, _: &Open, cx: &mut ViewContext) { - self.client().telemetry().report_app_event("open project"); + self.client() + .telemetry() + .report_app_event("open project".to_string()); let paths = cx.prompt_for_paths(PathPromptOptions { files: true, directories: true, @@ -1776,6 +1778,12 @@ impl Workspace { } pub fn add_item(&mut self, item: Box, cx: &mut ViewContext) { + if let Some(text) = item.telemetry_event_text(cx) { + self.client() + .telemetry() + .report_app_event(format!("{}: open", text)); + } + self.active_pane .update(cx, |pane, cx| pane.add_item(item, true, true, None, cx)); } @@ -2250,17 +2258,16 @@ impl Workspace { destination_index: usize, cx: &mut ViewContext, ) { - let item_to_move = source + let Some((item_ix, item_handle)) = source .read(cx) .items() .enumerate() - .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move); - - if item_to_move.is_none() { - log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop"); + .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move) + else { + // Tab was closed during drag return; - } - let (item_ix, item_handle) = item_to_move.unwrap(); + }; + let item_handle = item_handle.clone(); if source != destination { @@ -3324,36 +3331,6 @@ impl Workspace { workspace } - // fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option> { - // let dock = match position { - // DockPosition::Left => &self.left_dock, - // DockPosition::Right => &self.right_dock, - // DockPosition::Bottom => &self.bottom_dock, - // }; - // let active_panel = dock.read(cx).visible_panel()?; - // let element = if Some(active_panel.id()) == self.zoomed.as_ref().map(|zoomed| zoomed.id()) { - // dock.read(cx).render_placeholder(cx) - // } else { - // ChildView::new(dock, cx).into_any() - // }; - - // Some( - // element - // .constrained() - // .dynamically(move |constraint, _, cx| match position { - // DockPosition::Left | DockPosition::Right => SizeConstraint::new( - // Vector2F::new(20., constraint.min.y()), - // Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()), - // ), - // DockPosition::Bottom => SizeConstraint::new( - // Vector2F::new(constraint.min.x(), 20.), - // Vector2F::new(constraint.max.x(), cx.window_size().y() * 0.8), - // ), - // }) - // .into_any(), - // ) - // } - // } pub fn register_action( &mut self, callback: impl Fn(&mut Self, &A, &mut ViewContext) + 'static, diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index d28cd9f6e410cec04b3a3081e65166fea80ec159..8a30121d49c2ba350a42e5352065e1b7f886f0c7 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -184,7 +184,7 @@ mod tests { #[gpui::test] async fn test_python_autoindent(cx: &mut TestAppContext) { - // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX); + cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX); let language = crate::languages::language("python", tree_sitter_python::language(), None).await; cx.update(|cx| { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index e10c52a175c8633fa3b3bebdb09223f3505587ad..821668001c4fa42f757eea55b5e80361e0456e6d 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -176,10 +176,13 @@ fn main() { telemetry.start(installation_id, session_id, cx); telemetry.report_setting_event("theme", cx.theme().name.to_string()); telemetry.report_setting_event("keymap", BaseKeymap::get_global(cx).to_string()); - telemetry.report_app_event(match existing_installation_id_found { - Some(false) => "first open", - _ => "open", - }); + telemetry.report_app_event( + match existing_installation_id_found { + Some(false) => "first open", + _ => "open", + } + .to_string(), + ); telemetry.flush_events(); let app_state = Arc::new(AppState { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index c2725eef64029a11cbf449769ee5f025ae7b0535..bbe5e781094e33e8a53845d699e50ecd21552871 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -113,12 +113,6 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { }) .detach(); - // cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone())); - - // let collab_titlebar_item = - // cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx)); - // workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx); - let copilot = cx.new_view(|cx| copilot_ui::CopilotButton::new(app_state.fs.clone(), cx)); let diagnostic_summary = cx.new_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx)); @@ -184,7 +178,10 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { )?; workspace_handle.update(&mut cx, |workspace, cx| { - let position = project_panel.read(cx).position(cx); + let (position, was_deserialized) = { + let project_panel = project_panel.read(cx); + (project_panel.position(cx), project_panel.was_deserialized()) + }; workspace.add_panel(project_panel, cx); workspace.add_panel(terminal_panel, cx); workspace.add_panel(assistant_panel, cx); @@ -192,15 +189,16 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { workspace.add_panel(chat_panel, cx); workspace.add_panel(notification_panel, cx); - if workspace - .project() - .read(cx) - .visible_worktrees(cx) - .any(|tree| { - tree.read(cx) - .root_entry() - .map_or(false, |entry| entry.is_dir()) - }) + if !was_deserialized + && workspace + .project() + .read(cx) + .visible_worktrees(cx) + .any(|tree| { + tree.read(cx) + .root_entry() + .map_or(false, |entry| entry.is_dir()) + }) { workspace.toggle_dock(position, cx); } diff --git a/script/squawk b/script/squawk new file mode 100755 index 0000000000000000000000000000000000000000..0fb3e5a3325e8005fe4f0667debc7626cab5c9bd --- /dev/null +++ b/script/squawk @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +# Squawk is a linter for database migrations. It helps identify dangerous patterns, and suggests alternatives. +# Squawk flagging an error does not mean that you need to take a different approach, but it does indicate you need to think about what you're doing. +# See also: https://squawkhq.com + +set -e + +if [ -z "$GITHUB_BASE_REF" ]; then + echo 'Not a pull request, skipping squawk modified migrations linting' + return 0 +fi + +SQUAWK_VERSION=0.26.0 +SQUAWK_BIN="./target/squawk-$SQUAWK_VERSION" +SQUAWK_ARGS="--assume-in-transaction" + + +if [ ! -f "$SQUAWK_BIN" ]; then + curl -L -o "$SQUAWK_BIN" "https://github.com/sbdchd/squawk/releases/download/v$SQUAWK_VERSION/squawk-darwin-x86_64" + chmod +x "$SQUAWK_BIN" +fi + +if [ -n "$SQUAWK_GITHUB_TOKEN" ]; then + export SQUAWK_GITHUB_REPO_OWNER=$(echo $GITHUB_REPOSITORY | awk -F/ '{print $1}') + export SQUAWK_GITHUB_REPO_NAME=$(echo $GITHUB_REPOSITORY | awk -F/ '{print $2}') + export SQUAWK_GITHUB_PR_NUMBER=$(echo $GITHUB_REF | awk 'BEGIN { FS = "/" } ; { print $3 }') + + $SQUAWK_BIN $SQUAWK_ARGS upload-to-github $(git diff --name-only origin/$GITHUB_BASE_REF...origin/$GITHUB_HEAD_REF 'crates/collab/migrations/*.sql') +else + $SQUAWK_BIN $SQUAWK_ARGS $(git ls-files --others crates/collab/migrations/*.sql) $(git diff --name-only main crates/collab/migrations/*.sql) +fi diff --git a/script/zed-local b/script/zed-local index 4519ede38cf06bab355d0e65c530b76426e9108c..090fbd58760cd04779340be19d06a199d2a0a01d 100755 --- a/script/zed-local +++ b/script/zed-local @@ -1,31 +1,44 @@ #!/usr/bin/env node +const HELP = ` +USAGE + zed-local [options] [zed args] + +OPTIONS + --help Print this help message + --release Build Zed in release mode + -2, -3, -4 Spawn 2, 3, or 4 Zed instances, with their windows tiled. + --top Arrange the Zed windows so they take up the top half of the screen. +`.trim(); + const { spawn, execFileSync } = require("child_process"); const RESOLUTION_REGEX = /(\d+) x (\d+)/; const DIGIT_FLAG_REGEX = /^--?(\d+)$/; -// Parse the number of Zed instances to spawn. let instanceCount = 1; let isReleaseMode = false; let isTop = false; const args = process.argv.slice(2); -for (const arg of args) { +while (args.length > 0) { + const arg = args[0]; + const digitMatch = arg.match(DIGIT_FLAG_REGEX); if (digitMatch) { instanceCount = parseInt(digitMatch[1]); - continue; - } - - if (arg == "--release") { + } else if (arg === "--release") { isReleaseMode = true; - continue; - } - - if (arg == "--top") { + } else if (arg === "--top") { isTop = true; + } else if (arg === "--help") { + console.log(HELP); + process.exit(0); + } else { + break; } + + args.shift(); } // Parse the resolution of the main screen