Detailed changes
@@ -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
@@ -1,5 +1,6 @@
{
"JSON": {
"tab_size": 4
- }
+ },
+ "formatter": "auto"
}
@@ -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"
@@ -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:
@@ -295,7 +295,7 @@ impl Render for ActivityIndicator {
fn render(&mut self, cx: &mut ViewContext<Self>) -> 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));
@@ -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::<SettingsStore>(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<AssistantPanel>) -> View<Editor> {
impl Render for AssistantPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
if let Some(api_key_editor) = self.api_key_editor.clone() {
- 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<InlineAssistantEvent> for InlineAssistant {}
impl Render for InlineAssistant {
fn render(&mut self, cx: &mut ViewContext<Self>) -> 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) {
@@ -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<Self>) -> impl IntoElement {
let app_name = cx.global::<ReleaseChannel>().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} {}",
@@ -31,7 +31,7 @@ impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
impl Render for Breadcrumbs {
fn render(&mut self, cx: &mut ViewContext<Self>) -> 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::<Editor>()
.map(|editor| editor.downgrade())
@@ -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 {
@@ -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<u64>,
+ },
ParticipantLocationChanged {
participant_id: proto::PeerId,
},
@@ -49,7 +52,9 @@ pub enum Event {
RemoteProjectInvitationDiscarded {
project_id: u64,
},
- Left,
+ Left {
+ channel_id: Option<u64>,
+ },
}
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<Self>) -> Task<Result<()>> {
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<Self>) -> Task<Result<()>> {
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<Self>) -> Result<Task<Result<()>>> {
- let should_mute = !self.is_muted(cx);
+ pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) {
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<Self>) -> Result<Task<Result<()>>> {
+ pub fn toggle_deafen(&mut self, cx: &mut ModelContext<Self>) {
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<Self>,
+ ) -> Option<Task<Result<()>>> {
+ 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<Room>,
+ ) -> Option<Task<Result<()>>> {
+ 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<live_kit_client::MacOSDisplay>) {
self.live_kit
@@ -1596,50 +1654,6 @@ struct LiveKitRoom {
}
impl LiveKitRoom {
- fn set_mute(
- self: &mut LiveKitRoom,
- should_mute: bool,
- cx: &mut ModelContext<Room>,
- ) -> Result<(Task<Result<()>>, 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<Room>) {
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,
},
}
@@ -144,7 +144,7 @@ impl ChannelChat {
message: MessageParams,
cx: &mut ModelContext<Self>,
) -> Result<Task<Result<u64>>> {
- 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 {
@@ -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::<SettingsStore>({
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<Self>) -> impl Future<Output = ()> {
- 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<Self>, operation: &'static str) {
+ pub fn report_app_event(self: &Arc<Self>, 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> = 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<Self>, 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<Self>, 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> = Utc::now();
+ now.timestamp_millis() - first_event_datetime.timestamp_millis()
+ }
+ None => {
+ state.first_event_datetime = Some(Utc::now());
+ 0
+ }
+ }
+ }
+
fn report_event(self: &Arc<Self>, event: Event) {
let mut state = self.state.lock();
@@ -256,6 +256,7 @@ impl Database {
message_id = result.last_insert_id;
let mentioned_user_ids =
mentions.iter().map(|m| m.user_id).collect::<HashSet<_>>();
+
let mentions = mentions
.iter()
.filter_map(|mention| {
@@ -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
@@ -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(
@@ -1876,6 +1876,186 @@ fn active_call_events(cx: &mut TestAppContext) -> Rc<RefCell<Vec<room::Event>>>
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<bool>,
+ }
+
+ fn participant_audio_state(
+ room: &Model<Room>,
+ cx: &TestAppContext,
+ ) -> Vec<ParticipantAudioState> {
+ 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::<Vec<_>>()
+ })
+ }
+}
+
#[gpui::test(iterations = 10)]
async fn test_room_location(
executor: BackgroundExecutor,
@@ -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);
@@ -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<Self>) -> Option<View<Self>> {
Some(cx.new_view(|cx| {
Self::new(
@@ -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<Option<()>>,
subscriptions: Vec<gpui::Subscription>,
- workspace: WeakView<Workspace>,
is_scrolled_to_bottom: bool,
markdown_data: HashMap<ChannelMessageId, RichText>,
+ focus_handle: FocusHandle,
+ open_context_menu: Option<(u64, Subscription)>,
}
#[derive(Serialize, Deserialize)]
@@ -64,13 +68,6 @@ struct SerializedChatPanel {
width: Option<Pixels>,
}
-#[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<Self>| {
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::<SettingsStore>(
- 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<u64> {
+ 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<Self>) -> 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<Self>) -> 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<u64>) -> bool {
+ match self.open_context_menu.as_ref() {
+ Some((id, _)) => Some(*id) == message_id,
+ None => false,
+ }
+ }
+
+ fn render_message_menu(
+ this: &View<Self>,
+ message_id: u64,
+ cx: &mut WindowContext,
+ ) -> View<ContextMenu> {
+ 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<LanguageRegistry>,
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<Self>) -> 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<Self>) {
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<Self>) {
- 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<Self>) {
- 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<Event> for ChatPanel {}
-
impl Render for ChatPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> 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);
}
}
}
@@ -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::<SettingsStore>(
- 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>,
- // ) {
- // self.toggle_channel_collapsed(action.location, cx);
- // }
-
fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
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<Self>) -> 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<Self>) -> 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<Self>) -> 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<Self>) -> 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)
@@ -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(
@@ -36,17 +36,17 @@ impl ContactFinder {
impl Render for ContactFinder {
fn render(&mut self, cx: &mut ViewContext<Self>) -> 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.))
@@ -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)),
)
@@ -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<AppState>, 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<T: 'static>(
-// avatar: Option<Arc<ImageData>>,
-// avatar_style: &AvatarStyle,
-// container: ContainerStyle,
-// ) -> AnyElement<T> {
-// 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::<ChannelsAlpha>()
}
@@ -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<Self>) -> 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<Self>) -> 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()))
@@ -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),
)
@@ -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
)))),
@@ -58,7 +58,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
}
}
- room::Event::Left => {
+ room::Event::Left { .. } => {
for (_, windows) in notification_windows.drain() {
for window in windows {
window
@@ -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")),
),
@@ -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
@@ -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>) -> Self {
+ fn new(
+ previous_focus_handle: FocusHandle,
+ telemetry: Arc<Telemetry>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
let filter = cx.try_global::<CommandPaletteFilter>();
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<Self>) -> 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<Command>,
matches: Vec<StringMatch>,
selected_ix: usize,
+ telemetry: Arc<Telemetry>,
previous_focus_handle: FocusHandle,
}
@@ -130,6 +143,7 @@ impl CommandPaletteDelegate {
fn new(
command_palette: WeakView<CommandPalette>,
commands: Vec<Command>,
+ telemetry: Arc<Telemetry>,
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(
@@ -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<Self>) -> 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()
@@ -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(
@@ -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<Self>) -> 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(
@@ -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<EditorStyle>,
editor_actions: Vec<Box<dyn Fn(&mut ViewContext<Self>)>>,
+ 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::<Div>(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::<SettingsStore>(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<Arc<Language>> {
- // self.buffer.read(cx).language_at(point, cx)
- // }
+ pub fn language_at<'a, T: ToOffset>(
+ &self,
+ point: T,
+ cx: &'a AppContext,
+ ) -> Option<Arc<Language>> {
+ self.buffer.read(cx).language_at(point, cx)
+ }
- // pub fn file_at<'a, T: ToOffset>(&self, point: T, cx: &'a AppContext) -> Option<Arc<dyn File>> {
- // self.buffer.read(cx).read(cx).file_at(point).cloned()
- // }
+ pub fn file_at<'a, T: ToOffset>(
+ &self,
+ point: T,
+ cx: &'a AppContext,
+ ) -> Option<Arc<dyn language::File>> {
+ 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::<ThemeSettings>(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<Self>,
) -> 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<Self>,
) -> 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<Self>) {
- 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<Self>) {
+ 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<Self>) {
+ 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<Self>) {
+ 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::<String>();
-
- 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::<String>();
+ } 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::<String>());
+ }
+
+ 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::<String>();
+ 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::<String>();
+ 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::<String>());
+ }
+
+ 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::<String>();
+ 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::<String>();
+ let is_empty = query.is_empty();
+ let select_state = SelectNextState {
+ query: AhoCorasick::new(&[query.chars().rev().collect::<String>()])?,
+ 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::<Vec<_>>(),
+ 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::<String>();
- let query = query.chars().rev().collect::<String>();
- 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::<String>();
- let query = query.chars().rev().collect::<String>();
+ } 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::<String>()])?,
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()
@@ -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]
@@ -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<Self::State>,
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::<Pixels>::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]
@@ -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<SharedString> {
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| {
@@ -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,
@@ -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")
@@ -119,7 +119,7 @@ impl FocusableView for FileFinder {
}
impl Render for FileFinder {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> 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)),
),
@@ -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()
@@ -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"
@@ -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")
@@ -196,7 +196,6 @@ pub struct AppContext {
pending_updates: usize,
pub(crate) actions: Rc<ActionRegistry>,
pub(crate) active_drag: Option<AnyDrag>,
- pub(crate) active_tooltip: Option<AnyTooltip>,
pub(crate) next_frame_callbacks: FxHashMap<DisplayId, Vec<FrameCallback>>,
pub(crate) frame_consumers: FxHashMap<DisplayId, Task<()>>,
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<G: Any>(&mut self) -> G {
let global_type = TypeId::of::<G>();
+ 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<Pixels>,
}
+/// 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<Pixels>,
}
@@ -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<u64> for EntityId {
+ fn from(value: u64) -> Self {
+ Self(KeyData::from_ffi(value))
+ }
+}
+
impl EntityId {
pub fn as_u64(self) -> u64 {
self.0.as_ffi()
@@ -355,16 +355,6 @@ impl Hsla {
}
}
-// impl From<Hsla> 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<Rgba> for Hsla {
fn from(color: Rgba) -> Self {
let r = color.r;
@@ -978,12 +978,31 @@ impl Interactivity {
f: impl FnOnce(&Style, Point<Pixels>, &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();
}
}
@@ -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();
@@ -43,6 +43,7 @@ pub enum ListAlignment {
pub struct ListScrollEvent {
pub visible_range: Range<usize>,
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 {
@@ -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();
}
}
});
@@ -2272,7 +2272,7 @@ impl From<f64> 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<Pixels> 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),
@@ -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<DispatchNodeId>,
pub(crate) context_stack: Vec<KeyContext>,
nodes: Vec<DispatchNode>,
- focusable_node_ids: HashMap<FocusId, DispatchNodeId>,
- keystroke_matchers: HashMap<SmallVec<[KeyContext; 4]>, KeystrokeMatcher>,
+ focusable_node_ids: FxHashMap<FocusId, DispatchNodeId>,
+ view_node_ids: FxHashMap<EntityId, DispatchNodeId>,
+ keystroke_matchers: FxHashMap<SmallVec<[KeyContext; 4]>, KeystrokeMatcher>,
keymap: Arc<Mutex<Keymap>>,
action_registry: Rc<ActionRegistry>,
}
@@ -30,15 +32,16 @@ pub(crate) struct DispatchNode {
pub action_listeners: Vec<DispatchActionListener>,
pub context: Option<KeyContext>,
focus_id: Option<FocusId>,
+ view_id: Option<EntityId>,
parent: Option<DispatchNodeId>,
}
-type KeyListener = ArenaRef<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>;
+type KeyListener = Rc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>;
#[derive(Clone)]
pub(crate) struct DispatchActionListener {
pub(crate) action_type: TypeId,
- pub(crate) listener: ArenaRef<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>,
+ pub(crate) listener: Rc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>,
}
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<KeyContext>) {
+ pub fn push_node(
+ &mut self,
+ context: Option<KeyContext>,
+ focus_id: Option<FocusId>,
+ view_id: Option<EntityId>,
+ ) {
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<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>,
+ listener: Rc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>,
) {
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]
}
@@ -44,8 +44,6 @@ pub(crate) fn current_platform() -> Rc<dyn Platform> {
Rc::new(MacPlatform::new())
}
-pub type DrawWindow = Box<dyn FnMut() -> Result<Scene>>;
-
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<dyn PlatformWindow>;
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<dyn PlatformInputHandler>);
- fn clear_input_handler(&mut self);
+ fn take_input_handler(&mut self) -> Option<Box<dyn PlatformInputHandler>>;
fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize>;
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<dyn FnMut()>);
fn on_input(&self, callback: Box<dyn FnMut(InputEvent) -> bool>);
fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
@@ -167,6 +165,7 @@ pub trait PlatformWindow {
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
fn is_topmost_for_position(&self, position: Point<Pixels>) -> bool;
fn invalidate(&self);
+ fn draw(&self, scene: &Scene);
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
@@ -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<Item = Self> {
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}");
}
}
}
@@ -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<ScaledPixels>],
offset: &mut usize,
command_buffer: &metal::CommandBufferRef,
- ) -> HashMap<PathId, AtlasTile> {
+ ) -> Option<HashMap<PathId, AtlasTile>> {
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::<PathVertex<ScaledPixels>>();
- 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<DevicePixels>,
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<DevicePixels>,
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<DevicePixels>,
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::<MonochromeSprite>() * 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<DevicePixels>,
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<DevicePixels>,
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<DevicePixels>,
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<DevicePixels>,
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::<Surface>();
- 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
}
}
@@ -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);
}
}
}
@@ -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<dyn FnMut() -> Result<Scene>>,
) -> Box<dyn PlatformWindow> {
- 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::<Vec<_>>()
};
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);
}
}
}
@@ -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();
@@ -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<DrawWindow>,
kind: WindowKind,
+ request_frame_callback: Option<Box<dyn FnMut()>>,
event_callback: Option<Box<dyn FnMut(InputEvent) -> bool>>,
activate_callback: Option<Box<dyn FnMut(bool)>>,
resize_callback: Option<Box<dyn FnMut(Size<Pixels>, 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<Box<dyn PlatformInputHandler>> {
+ self.0.as_ref().lock().input_handler.take()
}
fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize> {
@@ -926,6 +924,10 @@ impl PlatformWindow for MacWindow {
.detach();
}
+ fn on_request_frame(&self, callback: Box<dyn FnMut()>) {
+ self.0.as_ref().lock().request_frame_callback = Some(callback);
+ }
+
fn on_input(&self, callback: Box<dyn FnMut(InputEvent) -> 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<dyn PlatformAtlas> {
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);
}
}
@@ -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<dyn FnMut() -> Result<Scene>>,
) -> Box<dyn crate::PlatformWindow> {
let window = TestWindow::new(
options,
@@ -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<Box<dyn PlatformInputHandler>> {
+ self.0.lock().input_handler.take()
}
fn prompt(
@@ -218,6 +218,8 @@ impl PlatformWindow for TestWindow {
unimplemented!()
}
+ fn on_request_frame(&self, _callback: Box<dyn FnMut()>) {}
+
fn on_input(&self, callback: Box<dyn FnMut(crate::InputEvent) -> 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<dyn crate::PlatformAtlas> {
self.0.lock().sprite_atlas.clone()
@@ -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<f32>;
@@ -11,74 +11,85 @@ pub(crate) type PointF = Point<f32>;
pub(crate) type PathVertex_ScaledPixels = PathVertex<ScaledPixels>;
pub type LayerId = u32;
-
pub type DrawOrder = u32;
-#[derive(Default)]
-pub(crate) struct SceneBuilder {
- last_order: Option<(StackingOrder, LayerId)>,
- layers_by_order: BTreeMap<StackingOrder, LayerId>,
- shadows: Vec<Shadow>,
- quads: Vec<Quad>,
- paths: Vec<Path<ScaledPixels>>,
- underlines: Vec<Underline>,
- monochrome_sprites: Vec<MonochromeSprite>,
- polychrome_sprites: Vec<PolychromeSprite>,
- surfaces: Vec<Surface>,
+#[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<EntityId> 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<ViewId> 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<StackingOrder, LayerId>,
+ orders_by_layer: BTreeMap<LayerId, StackingOrder>,
+ pub(crate) shadows: Vec<Shadow>,
+ pub(crate) quads: Vec<Quad>,
+ pub(crate) paths: Vec<Path<ScaledPixels>>,
+ pub(crate) underlines: Vec<Underline>,
+ pub(crate) monochrome_sprites: Vec<MonochromeSprite>,
+ pub(crate) polychrome_sprites: Vec<PolychromeSprite>,
+ pub(crate) surfaces: Vec<Surface>,
+}
- 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<ScaledPixels>] {
+ &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<Item = PrimitiveBatch> {
+ 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<Shadow>,
- pub quads: Vec<Quad>,
- pub paths: Vec<Path<ScaledPixels>>,
- pub underlines: Vec<Underline>,
- pub monochrome_sprites: Vec<MonochromeSprite>,
- pub polychrome_sprites: Vec<PolychromeSprite>,
- pub surfaces: Vec<Surface>,
-}
+ pub fn reuse_views(&mut self, views: &FxHashSet<EntityId>, 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<ScaledPixels>] {
- &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<Item = PrimitiveBatch> {
- 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<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
pub background: Hsla,
@@ -469,7 +526,9 @@ impl From<Quad> 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<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
pub thickness: ScaledPixels,
@@ -498,7 +557,9 @@ impl From<Underline> 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<ScaledPixels>,
pub corner_radii: Corners<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
@@ -527,7 +588,9 @@ impl From<Shadow> 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<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
pub color: Hsla,
@@ -558,7 +621,9 @@ impl From<MonochromeSprite> 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<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
pub corner_radii: Corners<ScaledPixels>,
@@ -589,7 +654,9 @@ impl From<PolychromeSprite> 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<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
pub image_buffer: media::core_video::CVImageBuffer,
@@ -619,7 +686,9 @@ pub(crate) struct PathId(pub(crate) usize);
#[derive(Debug)]
pub struct Path<P: Clone + Default + Debug> {
pub(crate) id: PathId,
- order: u32,
+ pub(crate) view_id: ViewId,
+ layer_id: LayerId,
+ order: DrawOrder,
pub(crate) bounds: Bounds<P>,
pub(crate) content_mask: ContentMask<P>,
pub(crate) vertices: Vec<PathVertex<P>>,
@@ -633,7 +702,9 @@ impl Path<Pixels> {
pub fn new(start: Point<Pixels>) -> 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<Pixels> {
pub fn scale(&self, factor: f32) -> Path<ScaledPixels> {
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),
@@ -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<C, F, R>(&self, cx: &mut C, f: F) -> R
- // where
- // C: BorrowAppContext,
- // F: FnOnce(&mut C) -> R,
- // {
- // if self.text.is_some() {
- // cx.with_text_style(Some(self.text.clone()), f)
- // } else {
- // f(cx)
- // }
- // }
-
- // /// Apply overflow to content mask
- // pub fn apply_overflow<C, F, R>(&self, bounds: Bounds<Pixels>, cx: &mut C, f: F) -> R
- // where
- // C: BorrowWindow,
- // F: FnOnce(&mut C) -> R,
- // {
- // let current_mask = cx.content_mask();
-
- // let min = current_mask.bounds.origin;
- // let max = current_mask.bounds.lower_right();
-
- // let mask_bounds = match (
- // self.overflow.x == Overflow::Visible,
- // self.overflow.y == Overflow::Visible,
- // ) {
- // // x and y both visible
- // (true, true) => return f(cx),
- // // x visible, y hidden
- // (true, false) => Bounds::from_corners(
- // point(min.x, bounds.origin.y),
- // point(max.x, bounds.lower_right().y),
- // ),
- // // x hidden, y visible
- // (false, true) => Bounds::from_corners(
- // point(bounds.origin.x, min.y),
- // point(bounds.lower_right().x, max.y),
- // ),
- // // both hidden
- // (false, false) => bounds,
- // };
- // let mask = ContentMask {
- // bounds: mask_bounds,
- // };
-
- // cx.with_content_mask(Some(mask), f)
- // }
+ pub fn apply_text_style<C, F, R>(&self, cx: &mut C, f: F) -> R
+ where
+ C: BorrowAppContext,
+ F: FnOnce(&mut C) -> R,
+ {
+ if self.text.is_some() {
+ cx.with_text_style(Some(self.text.clone()), f)
+ } else {
+ f(cx)
+ }
+ }
+
+ /// Apply overflow to content mask
+ pub fn apply_overflow<C, F, R>(&self, bounds: Bounds<Pixels>, cx: &mut C, f: F) -> R
+ where
+ C: BorrowWindow,
+ F: FnOnce(&mut C) -> R,
+ {
+ let current_mask = cx.content_mask();
+
+ let min = current_mask.bounds.origin;
+ let max = current_mask.bounds.lower_right();
+
+ let mask_bounds = match (
+ self.overflow.x == Overflow::Visible,
+ self.overflow.y == Overflow::Visible,
+ ) {
+ // x and y both visible
+ (true, true) => return f(cx),
+ // x visible, y hidden
+ (true, false) => Bounds::from_corners(
+ point(min.x, bounds.origin.y),
+ point(max.x, bounds.lower_right().y),
+ ),
+ // x hidden, y visible
+ (false, true) => Bounds::from_corners(
+ point(bounds.origin.x, min.y),
+ point(bounds.lower_right().x, max.y),
+ ),
+ // both hidden
+ (false, false) => bounds,
+ };
+ let mask = ContentMask {
+ bounds: mask_bounds,
+ };
+
+ cx.with_content_mask(Some(mask), f)
+ }
/// Paints the background of an element styled with this style.
pub fn paint(
@@ -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<Hsla>) -> Self {
self.text_style()
.get_or_insert_with(Default::default)
@@ -14,6 +14,7 @@ use taffy::{
pub struct TaffyLayoutEngine {
taffy: Taffy,
+ styles: FxHashMap<LayoutId, Style>,
children_to_parents: FxHashMap<LayoutId, LayoutId>,
absolute_layout_bounds: FxHashMap<LayoutId, Bounds<Pixels>>,
computed_layouts: FxHashSet<LayoutId>,
@@ -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<Option<Pixels>>, Size<AvailableSpace>, &mut WindowContext) -> Size<Pixels>
+ '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<taffy::style::Style> for Style {
}
}
-// impl ToTaffy for Bounds<Length> {
-// type Output = taffy::prelude::Bounds<taffy::prelude::LengthPercentageAuto>;
-
-// fn to_taffy(
-// &self,
-// rem_size: Pixels,
-// ) -> taffy::prelude::Bounds<taffy::prelude::LengthPercentageAuto> {
-// taffy::prelude::Bounds {
-// origin: self.origin.to_taffy(rem_size),
-// size: self.size.to_taffy(rem_size),
-// }
-// }
-// }
-
impl ToTaffy<taffy::style::LengthPercentageAuto> for Length {
fn to_taffy(&self, rem_size: Pixels) -> taffy::prelude::LengthPercentageAuto {
match self {
@@ -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<String> {
+ 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<Vec<u8>>]) -> Result<()> {
self.platform_text_system.add_fonts(fonts)
}
@@ -186,6 +197,10 @@ impl TextSystem {
}
}
+ pub fn with_view<R>(&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<EntityId>) {
+ self.line_layout_cache.finish_frame(reused_views)
}
pub fn line_wrapper(self: &Arc<Self>, font: Font, font_size: Pixels) -> LineWrapperHandle {
@@ -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<Vec<EntityId>>,
previous_frame: Mutex<FxHashMap<CacheKey, Arc<LineLayout>>>,
current_frame: RwLock<FxHashMap<CacheKey, Arc<LineLayout>>>,
previous_frame_wrapped: Mutex<FxHashMap<CacheKey, Arc<WrappedLineLayout>>>,
@@ -246,6 +247,7 @@ pub(crate) struct LineLayoutCache {
impl LineLayoutCache {
pub fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> 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<EntityId>) {
+ 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<R>(&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<EntityId> {
+ 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<Pixels>,
+ parent_view_id: Option<EntityId>,
}
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
@@ -362,6 +401,7 @@ struct CacheKeyRef<'a> {
font_size: Pixels,
runs: &'a [FontRun],
wrap_width: Option<Pixels>,
+ parent_view_id: Option<EntityId>,
}
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,
}
}
}
@@ -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<V> {
impl<V> Sealed for View<V> {}
+pub struct AnyViewState {
+ root_style: Style,
+ cache_key: Option<ViewCacheKey>,
+ element: Option<AnyElement>,
+}
+
+struct ViewCacheKey {
+ bounds: Bounds<Pixels>,
+ stacking_order: StackingOrder,
+ content_mask: ContentMask<Pixels>,
+ text_style: TextStyle,
+}
+
impl<V: 'static> Entity<V> for View<V> {
type Weak = WeakView<V>;
@@ -60,16 +73,6 @@ impl<V: 'static> View<V> {
self.model.read(cx)
}
- // pub fn render_with<E>(&self, component: E) -> RenderViewWith<E, V>
- // 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<V: Render> Element for View<V> {
_state: Option<Self::State>,
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<Pixels>, 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<V> Eq for WeakView<V> {}
#[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<AvailableSpace>,
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<V: Render> From<View<V>> for AnyView {
fn from(value: View<V>) -> Self {
AnyView {
model: value.model.into_any(),
- layout: any_view::layout::<V>,
- paint: any_view::paint,
+ request_layout: any_view::request_layout::<V>,
+ cache: false,
}
}
}
impl Element for AnyView {
- type State = Option<AnyElement>;
+ type State = AnyViewState;
fn request_layout(
&mut self,
- _state: Option<Self::State>,
+ state: Option<Self::State>,
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<Pixels>, 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<Pixels>, 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<V: 'static + Render> From<WeakView<V>> for AnyWeakView {
fn from(view: WeakView<V>) -> Self {
Self {
model: view.model.into(),
- layout: any_view::layout::<V>,
- paint: any_view::paint,
+ layout: any_view::request_layout::<V>,
}
}
}
@@ -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<V: 'static + Render>(
+ pub(crate) fn request_layout<V: 'static + Render>(
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);
- }
}
@@ -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<dyn FnMut(&mut WindowContext) -> bool + 'static>;
-type AnyMouseListener = ArenaBox<dyn FnMut(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>;
+type AnyMouseListener = Box<dyn FnMut(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>;
type AnyWindowFocusListener = Box<dyn FnMut(&FocusEvent, &mut WindowContext) -> 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<EntityId>,
pub(crate) focus_handles: Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>,
focus_listeners: SubscriberSet<(), AnyWindowFocusListener>,
focus_lost_listeners: SubscriberSet<(), AnyObserver>,
default_prevented: bool,
mouse_position: Point<Pixels>,
modifiers: Modifiers,
- requested_cursor_style: Option<CursorStyle>,
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<FocusId>,
@@ -290,22 +290,39 @@ pub struct Window {
pub(crate) struct ElementStateBox {
inner: Box<dyn Any>,
+ parent_view_id: EntityId,
#[cfg(debug_assertions)]
type_name: &'static str,
}
+struct RequestedInputHandler {
+ view_id: EntityId,
+ handler: Option<Box<dyn PlatformInputHandler>>,
+}
+
+struct TooltipRequest {
+ view_id: EntityId,
+ tooltip: AnyTooltip,
+}
+
pub(crate) struct Frame {
focus: Option<FocusId>,
window_active: bool,
pub(crate) element_states: FxHashMap<GlobalElementId, ElementStateBox>,
- mouse_listeners: FxHashMap<TypeId, Vec<(StackingOrder, AnyMouseListener)>>,
+ mouse_listeners: FxHashMap<TypeId, Vec<(StackingOrder, EntityId, AnyMouseListener)>>,
pub(crate) dispatch_tree: DispatchTree,
- pub(crate) scene_builder: SceneBuilder,
- pub(crate) depth_map: Vec<(StackingOrder, Bounds<Pixels>)>,
+ pub(crate) scene: Scene,
+ pub(crate) depth_map: Vec<(StackingOrder, EntityId, Bounds<Pixels>)>,
pub(crate) z_index_stack: StackingOrder,
pub(crate) next_stacking_order_id: u32,
content_mask_stack: Vec<ContentMask<Pixels>>,
element_offset_stack: Vec<Point<Pixels>>,
+ requested_input_handler: Option<RequestedInputHandler>,
+ tooltip_request: Option<TooltipRequest>,
+ cursor_styles: FxHashMap<EntityId, CursorStyle>,
+ requested_cursor_style: Option<CursorStyle>,
+ pub(crate) view_stack: Vec<EntityId>,
+ pub(crate) reused_views: FxHashSet<EntityId>,
}
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::<Event>())
.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::<Event>() {
- 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::<Event>() {
+ 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<Pixels>) {
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<Pixels>, 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<Pixels>,
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<Pixels>, color: impl Into<Hsla>) {
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::<MouseMoveEvent>() {
// 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::<MouseUpEvent>() {
// 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<FocusHandle>, &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<R>(&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<R>(&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<S, R>(
+ &mut self,
+ id: ElementId,
+ f: impl FnOnce(Option<S>, &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::<Option<S>>()
+ .map_err(|_| {
+ #[cfg(debug_assertions)]
+ {
+ anyhow!(
+ "invalid element state type for id, requested_type {:?}, actual type: {:?}",
+ std::any::type_name::<S>(),
+ type_name
+ )
+ }
+
+ #[cfg(not(debug_assertions))]
+ {
+ anyhow!(
+ "invalid element state type for id, requested_type {:?}",
+ std::any::type_name::<S>(),
+ )
+ }
+ })
+ .unwrap();
+
+ // Actual: Option<AnyElement> <- 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::<S>()
+ }
+
+ );
+ 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<Window> + BorrowMut<AppContext> {
.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<S, R>(
- &mut self,
- id: ElementId,
- f: impl FnOnce(Option<S>, &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::<Option<S>>()
- .map_err(|_| {
- #[cfg(debug_assertions)]
- {
- anyhow!(
- "invalid element state type for id, requested_type {:?}, actual type: {:?}",
- std::any::type_name::<S>(),
- type_name
- )
- }
-
- #[cfg(not(debug_assertions))]
- {
- anyhow!(
- "invalid element state type for id, requested_type {:?}",
- std::any::type_name::<S>(),
- )
- }
- })
- .unwrap();
-
- // Actual: Option<AnyElement> <- 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::<S>()
- }
-
- );
- result
- }
- })
- }
-
/// Obtain the current content mask.
fn content_mask(&self) -> ContentMask<Pixels> {
self.window()
@@ -1,16 +1,3 @@
-// Input:
-//
-// struct FooBar {}
-
-// Output:
-//
-// struct FooBar {}
-//
-// #[allow(non_snake_case)]
-// #[gpui2::ctor]
-// fn register_foobar_builder() {
-// gpui2::register_action_builder::<Foo>()
-// }
use proc_macro::TokenStream;
use proc_macro2::Ident;
use quote::{format_ident, quote};
@@ -68,7 +68,7 @@ impl LanguageSelector {
impl Render for LanguageSelector {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
- v_stack().w(rems(34.)).child(self.picker.clone())
+ v_flex().w(rems(34.)).child(self.picker.clone())
}
}
@@ -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<Self>) -> Option<Box<dyn SearchableItemHandle>> {
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(
@@ -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,
@@ -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<RemoteAudioTrack>.fromOpaque(track).takeUnretainedValue()
+ track.start()
+}
+
+@_cdecl("LKRemoteAudioTrackStop")
+public func LKRemoteAudioTrackStop(track: UnsafeRawPointer) {
+ let track = Unmanaged<RemoteAudioTrack>.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
@@ -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<Output = Result<()>> {
- async { Ok(()) }
+ pub fn start(&self) {
+ unsafe { LKRemoteAudioTrackStart(self.native_track) }
}
- pub fn disable(&self) -> impl Future<Output = Result<()>> {
- async { Ok(()) }
+ pub fn stop(&self) {
+ unsafe { LKRemoteAudioTrackStop(self.native_track) }
}
}
@@ -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<bool> {
+ 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<Vec<Arc<RemoteVideoTrack>>> {
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<Vec<Arc<RemoteAudioTrack>>> {
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<Sid, Arc<Room>>,
- video_tracks: Vec<Arc<RemoteVideoTrack>>,
- audio_tracks: Vec<Arc<RemoteAudioTrack>>,
+ video_tracks: Vec<Arc<TestServerVideoTrack>>,
+ audio_tracks: Vec<Arc<TestServerAudioTrack>>,
participant_permissions: HashMap<Sid, proto::ParticipantPermission>,
}
+#[derive(Debug)]
+struct TestServerVideoTrack {
+ sid: Sid,
+ publisher_id: Sid,
+ frames_rx: async_broadcast::Receiver<Frame>,
+}
+
+#[derive(Debug)]
+struct TestServerAudioTrack {
+ sid: Sid,
+ publisher_id: Sid,
+ muted: AtomicBool,
+}
+
impl TestServerRoom {}
pub struct TestApiClient {
@@ -386,6 +498,7 @@ struct RoomState {
watch::Receiver<ConnectionState>,
),
display_sources: Vec<MacOSDisplay>,
+ paused_audio_tracks: HashSet<Sid>,
updates_tx: async_broadcast::Sender<RoomUpdate>,
updates_rx: async_broadcast::Receiver<RoomUpdate>,
}
@@ -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<Self>,
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<AtomicBool>,
+ room: Weak<Room>,
}
impl LocalTrackPublication {
pub fn set_mute(&self, mute: bool) -> impl Future<Output = Result<()>> {
- 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<Frame>,
+ server_track: Arc<TestServerVideoTrack>,
}
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<Frame> {
- self.frames_rx.clone()
+ self.server_track.frames_rx.clone()
}
}
#[derive(Debug)]
pub struct RemoteAudioTrack {
- sid: Sid,
- publisher_id: Sid,
+ server_track: Arc<TestServerAudioTrack>,
+ room: Weak<Room>,
}
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<Output = Result<()>> {
- 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<Output = Result<()>> {
- 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)
}
}
@@ -64,7 +64,7 @@ impl ModalView for OutlineView {
impl Render for OutlineView {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
- v_stack().w(rems(34.)).child(self.picker.clone())
+ v_flex().w(rems(34.)).child(self.picker.clone())
}
}
@@ -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<D: PickerDelegate> {
@@ -236,7 +236,7 @@ impl<D: PickerDelegate> ModalView for Picker<D> {}
impl<D: PickerDelegate> Render for Picker<D> {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- let picker_editor = h_stack()
+ let picker_editor = h_flex()
.overflow_hidden()
.flex_none()
.h_9()
@@ -264,7 +264,7 @@ impl<D: PickerDelegate> Render for Picker<D> {
.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<D: PickerDelegate> Render for Picker<D> {
})
.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)
@@ -99,20 +99,6 @@ pub trait Item {
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
}
-// 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<WorktreeHandle>,
active_entry: Option<ProjectEntryId>,
@@ -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<Workspace>,
width: Option<Pixels>,
pending_serialization: Task<Option<()>>,
+ was_deserialized: bool,
}
#[derive(Copy, Clone, Debug)]
@@ -221,10 +222,10 @@ impl ProjectPanel {
})
.detach();
- // cx.observe_global::<FileAssociations, _>(|_, cx| {
- // cx.notify();
- // })
- // .detach();
+ cx.observe_global::<FileAssociations>(|_, 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::<SettingsStore>(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<Workspace>, cx| {
- // drag_and_drop.cancel_dragging::<ProjectEntryId>(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<Self>) -> 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)
@@ -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)
@@ -93,7 +93,7 @@ impl Render for QuickActionBar {
},
);
- h_stack()
+ h_flex()
.id("quick action bar")
.gap_2()
.children(inlay_hints_button)
@@ -104,7 +104,7 @@ impl FocusableView for RecentProjects {
impl Render for RecentProjects {
fn render(&mut self, cx: &mut ViewContext<Self>) -> 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)
@@ -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<TokenStream2> = fields
.iter()
.map(|field| {
@@ -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<usize>,
pub is_self_mention: bool,
@@ -85,31 +86,6 @@ impl RichText {
})
.into_any_element()
}
-
- // pub fn add_mention(
- // &mut self,
- // range: Range<usize>,
- // 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<LanguageRegistry>,
language: Option<&Arc<Language>>,
) -> 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();
@@ -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"])
@@ -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
@@ -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()
@@ -1677,8 +1677,6 @@ fn elixir_lang() -> Arc<Language> {
#[gpui::test]
fn test_subtract_ranges() {
- // collapsed_ranges: Vec<Range<usize>>, keep_ranges: Vec<Range<usize>>
-
assert_eq!(
subtract_ranges(&[0..5, 10..21], &[0..1, 4..5]),
vec![1..4, 10..21]
@@ -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()
@@ -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"] }
@@ -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()
@@ -117,7 +117,7 @@ impl Render for TextStory {
// type Element = Div;
// fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
-// v_stack()
+// v_flex()
// .bg(blue())
// .child(
// div()
@@ -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::<Vec<_>>();
- 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])
});
@@ -598,7 +598,7 @@ impl TerminalElement {
this.update(cx, |term, _| term.try_modifiers_change(&event.modifiers));
if handled {
- cx.notify();
+ cx.refresh();
}
}
});
@@ -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::<SettingsStore>(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
}
@@ -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<RegexSearch> {
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,
@@ -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(),
@@ -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,
@@ -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<V: 'static>(
+ cx: &mut ViewContext<V>,
+ f: impl 'static + Fn(&mut V, &mut ViewContext<V>),
+) -> Subscription {
+ cx.observe_global::<AdjustedBufferFontSize>(f)
+}
+
pub fn adjusted_font_size(size: Pixels, cx: &mut AppContext) -> Pixels {
if let Some(AdjustedBufferFontSize(adjusted_size)) = cx.try_global::<AdjustedBufferFontSize>() {
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
}
@@ -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::<ThemeRegistry>().load_user_themes(),
}
-
ThemeSettings::register(cx);
+
+ let mut prev_buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
+ cx.observe_global::<SettingsStore>(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 {
@@ -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<Self>) -> impl IntoElement {
- v_stack().w(rems(34.)).child(self.picker.clone())
+ v_flex().w(rems(34.)).child(self.picker.clone())
}
}
@@ -26,6 +26,7 @@ pub enum AvatarShape {
#[derive(IntoElement)]
pub struct Avatar {
image: Img,
+ size: Option<Pixels>,
border_color: Option<Hsla>,
is_available: Option<bool>,
}
@@ -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<Option<Pixels>>) -> Self {
+ self.size = size.into();
+ self
+ }
}
@@ -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(
@@ -300,6 +300,7 @@ pub struct ButtonLike {
pub(super) selected: bool,
pub(super) selected_style: Option<ButtonStyle>,
pub(super) width: Option<DefiniteLength>,
+ pub(super) height: Option<DefiniteLength>,
size: ButtonSize,
rounding: Option<ButtonLikeRounding>,
tooltip: Option<Box<dyn Fn(&mut WindowContext) -> 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<Option<ButtonLikeRounding>>) -> 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(),
@@ -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<ElementId>, 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),
+ )
}
}
@@ -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
@@ -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<Self>) -> 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)
@@ -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()
@@ -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,
@@ -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()
@@ -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()
@@ -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())
@@ -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()
@@ -55,7 +55,7 @@ impl<M: ManagedView> PopoverMenu<M> {
}
}
*menu2.borrow_mut() = None;
- cx.notify();
+ cx.refresh();
})
.detach();
cx.focus_view(&new_menu);
@@ -134,6 +134,7 @@ impl<M: ManagedView> Element for RightClickMenu<M> {
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<M: ManagedView> Element for RightClickMenu<M> {
}
}
*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();
}
});
}
@@ -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()
}
@@ -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::<Checkbox>())
.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()
@@ -117,55 +117,5 @@ impl Render for IconButtonStory {
)
.children(vec![StorySection::new().children(buttons)])
.into_element()
-
- // Story::container()
- // .child(Story::title_for::<IconButton>())
- // .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)),
- // ),
- // )
}
}
@@ -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",
@@ -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::<Tab>())
.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)
@@ -35,7 +35,7 @@ impl Render for TabBarStory {
.child(Story::title_for::<TabBar>())
.child(Story::label("Default"))
.child(
- h_stack().child(
+ h_flex().child(
TabBar::new("tab_bar_1")
.start_child(
IconButton::new("navigate_backward", IconName::ArrowLeft)
@@ -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)
@@ -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()
@@ -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()
@@ -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| {
@@ -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};
@@ -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<Self>) -> 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<Picker<Self>>) -> Option<AnyElement> {
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 {
@@ -62,7 +62,7 @@ impl BaseKeymapSelector {
impl Render for BaseKeymapSelector {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
- v_stack().w(rems(34.)).child(self.picker.clone())
+ v_flex().w(rems(34.)).child(self.picker.clone())
}
}
@@ -60,8 +60,8 @@ pub struct WelcomePage {
impl Render for WelcomePage {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> 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::<VimModeSetting>(
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::<TelemetrySettings>(
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::<TelemetrySettings>(
selection,
@@ -247,7 +251,8 @@ impl WelcomePage {
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
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
}
@@ -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<dyn PanelHandle>,
- _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::<SettingsStore>({
+ 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>) {
self.position = position;
- cx.emit(PanelEvent::ChangePosition);
+ cx.update_global::<SettingsStore, _>(|_, _| {});
}
fn size(&self, _: &WindowContext) -> Pixels {
@@ -114,6 +114,8 @@ pub trait Item: FocusableView + EventEmitter<Self::Event> {
}
fn tab_content(&self, detail: Option<usize>, 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<SharedString>;
fn tab_description(&self, detail: usize, cx: &AppContext) -> Option<SharedString>;
fn tab_content(&self, detail: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement;
+ fn telemetry_event_text(&self, cx: &WindowContext) -> Option<&'static str>;
fn dragged_tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement;
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
@@ -313,6 +316,10 @@ impl<T: Item> ItemHandle for View<T> {
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<SharedString> {
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<Self> {
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<usize>,
@@ -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<Self>) -> 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);
}))
@@ -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<Self>) -> 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(
@@ -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<Pane>,
-// }
-
-// #[derive(Clone, PartialEq)]
-// pub struct CloseItemsToTheLeftById {
-// pub item_id: usize,
-// pub pane: WeakView<Pane>,
-// }
-
-// #[derive(Clone, PartialEq)]
-// pub struct CloseItemsToTheRightById {
-// pub item_id: usize,
-// pub pane: WeakView<Pane>,
-// }
-
#[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<Pane>,
+ pub item: Box<dyn ItemHandle>,
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>) {
- // 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>) {
- // 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<Self>,
- // ) {
- // 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<Toolbar> {
&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>(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<Self>) -> 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<Self>) -> 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)
@@ -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();
}
}
}
@@ -1,5 +1,3 @@
-//#![allow(dead_code)]
-
pub mod model;
use std::path::Path;
@@ -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>) {
self.nav_history = Some(history);
}
@@ -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<Self>) -> 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<Self>) -> impl IntoElement {
- h_stack()
+ h_flex()
.items_center()
.gap_2()
.children(self.right_items.iter().rev().map(|item| item.to_any()))
@@ -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())),
@@ -1271,7 +1271,9 @@ impl Workspace {
}
pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
- 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<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
+ 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<Self>,
) {
- 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<AnyElement<Self>> {
- // 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<A: Action>(
&mut self,
callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
@@ -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| {
@@ -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 {
@@ -113,12 +113,6 @@ pub fn initialize_workspace(app_state: Arc<AppState>, 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<AppState>, 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<AppState>, 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);
}
@@ -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
@@ -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