Merge branch 'gpui2' into gpui2-ui
This commit is contained in:
@@ -2,11 +2,4 @@
|
||||
|
||||
Release Notes:
|
||||
|
||||
- N/A
|
||||
|
||||
or
|
||||
|
||||
- (Added|Fixed|Improved) ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/community/issues/<public_issue_number_if_exists>)).
|
||||
|
||||
If the release notes are only intended for a specific release channel only, add `(<release_channel>-only)` to the end of the release note line.
|
||||
These will be removed by the person making the release.
|
||||
|
||||
@@ -6,8 +6,8 @@ jobs:
|
||||
discord_release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get appropriate URL
|
||||
id: get-appropriate-url
|
||||
- name: Get release URL
|
||||
id: get-release-url
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
|
||||
URL="https://zed.dev/releases/preview/latest"
|
||||
@@ -15,14 +15,19 @@ jobs:
|
||||
URL="https://zed.dev/releases/stable/latest"
|
||||
fi
|
||||
echo "::set-output name=URL::$URL"
|
||||
- name: Get content
|
||||
uses: 2428392/gh-truncate-string-action@v1.2.0
|
||||
id: get-content
|
||||
with:
|
||||
stringToTruncate: |
|
||||
📣 Zed ${{ github.event.release.tag_name }} was just released!
|
||||
|
||||
Restart your Zed or head to ${{ steps.get-release-url.outputs.URL }} to grab it.
|
||||
|
||||
${{ github.event.release.body }}
|
||||
maxLength: 2000
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@v5.3.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
content: |
|
||||
📣 Zed ${{ github.event.release.tag_name }} was just released!
|
||||
|
||||
Restart your Zed or head to ${{ steps.get-appropriate-url.outputs.URL }} to grab it.
|
||||
|
||||
${{ github.event.release.body }}
|
||||
content: ${{ steps.get-content.outputs.string }}
|
||||
|
||||
Generated
+5
-7
@@ -1093,7 +1093,6 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
"audio",
|
||||
"channel",
|
||||
"client",
|
||||
"collections",
|
||||
"fs",
|
||||
@@ -1497,7 +1496,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.23.3"
|
||||
version = "0.24.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -2109,9 +2108,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "curl-sys"
|
||||
version = "0.4.66+curl-8.3.0"
|
||||
version = "0.4.67+curl-8.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70c44a72e830f0e40ad90dda8a6ab6ed6314d39776599a58a2e5e37fbc6db5b9"
|
||||
checksum = "3cc35d066510b197a0f72de863736641539957628c8a42e70e27c66849e77c34"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -2862,7 +2861,6 @@ dependencies = [
|
||||
"parking_lot 0.11.2",
|
||||
"regex",
|
||||
"rope",
|
||||
"rpc",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
@@ -9832,6 +9830,7 @@ dependencies = [
|
||||
"theme",
|
||||
"theme_selector",
|
||||
"util",
|
||||
"vim",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
@@ -10138,7 +10137,6 @@ dependencies = [
|
||||
"async-recursion 1.0.5",
|
||||
"bincode",
|
||||
"call",
|
||||
"channel",
|
||||
"client",
|
||||
"collections",
|
||||
"context_menu",
|
||||
@@ -10250,7 +10248,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.108.0"
|
||||
version = "0.109.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
|
||||
@@ -83,9 +83,7 @@ foreman start
|
||||
If you want to run Zed pointed at the local servers, you can run:
|
||||
|
||||
```
|
||||
script/zed-with-local-servers
|
||||
# or...
|
||||
script/zed-with-local-servers --release
|
||||
script/zed-local
|
||||
```
|
||||
|
||||
### Dump element JSON
|
||||
|
||||
@@ -408,6 +408,7 @@
|
||||
"vim::PushOperator",
|
||||
"Yank"
|
||||
],
|
||||
"shift-y": "vim::YankLine",
|
||||
"i": "vim::InsertBefore",
|
||||
"shift-i": "vim::InsertFirstNonWhitespace",
|
||||
"a": "vim::InsertAfter",
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
// Settings related to calls in Zed
|
||||
"calls": {
|
||||
// Join calls with the microphone muted by default
|
||||
"mute_on_join": true
|
||||
"mute_on_join": false
|
||||
},
|
||||
// Scrollbar related settings
|
||||
"scrollbar": {
|
||||
|
||||
@@ -17,7 +17,7 @@ use editor::{
|
||||
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint,
|
||||
},
|
||||
scroll::autoscroll::{Autoscroll, AutoscrollStrategy},
|
||||
Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset,
|
||||
Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, ToPoint,
|
||||
};
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
@@ -278,22 +278,36 @@ impl AssistantPanel {
|
||||
if selection.start.excerpt_id() != selection.end.excerpt_id() {
|
||||
return;
|
||||
}
|
||||
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||
|
||||
// Extend the selection to the start and the end of the line.
|
||||
let mut point_selection = selection.map(|selection| selection.to_point(&snapshot));
|
||||
if point_selection.end > point_selection.start {
|
||||
point_selection.start.column = 0;
|
||||
// If the selection ends at the start of the line, we don't want to include it.
|
||||
if point_selection.end.column == 0 {
|
||||
point_selection.end.row -= 1;
|
||||
}
|
||||
point_selection.end.column = snapshot.line_len(point_selection.end.row);
|
||||
}
|
||||
|
||||
let codegen_kind = if point_selection.start == point_selection.end {
|
||||
CodegenKind::Generate {
|
||||
position: snapshot.anchor_after(point_selection.start),
|
||||
}
|
||||
} else {
|
||||
CodegenKind::Transform {
|
||||
range: snapshot.anchor_before(point_selection.start)
|
||||
..snapshot.anchor_after(point_selection.end),
|
||||
}
|
||||
};
|
||||
|
||||
let inline_assist_id = post_inc(&mut self.next_inline_assist_id);
|
||||
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||
let provider = Arc::new(OpenAICompletionProvider::new(
|
||||
api_key,
|
||||
cx.background().clone(),
|
||||
));
|
||||
let codegen_kind = if editor.read(cx).selections.newest::<usize>(cx).is_empty() {
|
||||
CodegenKind::Generate {
|
||||
position: selection.start,
|
||||
}
|
||||
} else {
|
||||
CodegenKind::Transform {
|
||||
range: selection.start..selection.end,
|
||||
}
|
||||
};
|
||||
|
||||
let codegen = cx.add_model(|cx| {
|
||||
Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx)
|
||||
});
|
||||
@@ -319,7 +333,7 @@ impl AssistantPanel {
|
||||
editor.insert_blocks(
|
||||
[BlockProperties {
|
||||
style: BlockStyle::Flex,
|
||||
position: selection.head().bias_left(&snapshot),
|
||||
position: snapshot.anchor_before(point_selection.head()),
|
||||
height: 2,
|
||||
render: Arc::new({
|
||||
let inline_assistant = inline_assistant.clone();
|
||||
@@ -578,10 +592,7 @@ impl AssistantPanel {
|
||||
|
||||
let codegen_kind = codegen.read(cx).kind().clone();
|
||||
let user_prompt = user_prompt.to_string();
|
||||
let prompt = cx.background().spawn(async move {
|
||||
let language_name = language_name.as_deref();
|
||||
generate_content_prompt(user_prompt, language_name, &buffer, range, codegen_kind)
|
||||
});
|
||||
|
||||
let mut messages = Vec::new();
|
||||
let mut model = settings::get::<AssistantSettings>(cx)
|
||||
.default_open_ai_model
|
||||
@@ -597,6 +608,11 @@ impl AssistantPanel {
|
||||
model = conversation.model.clone();
|
||||
}
|
||||
|
||||
let prompt = cx.background().spawn(async move {
|
||||
let language_name = language_name.as_deref();
|
||||
generate_content_prompt(user_prompt, language_name, &buffer, range, codegen_kind)
|
||||
});
|
||||
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let prompt = prompt.await;
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use crate::streaming_diff::{Hunk, StreamingDiff};
|
||||
use ai::completion::{CompletionProvider, OpenAIRequest};
|
||||
use anyhow::Result;
|
||||
use editor::{
|
||||
multi_buffer, Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
|
||||
};
|
||||
use editor::{multi_buffer, Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
|
||||
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
|
||||
use gpui::{Entity, ModelContext, ModelHandle, Task};
|
||||
use language::{Rope, TransactionId};
|
||||
@@ -40,26 +38,11 @@ impl Entity for Codegen {
|
||||
impl Codegen {
|
||||
pub fn new(
|
||||
buffer: ModelHandle<MultiBuffer>,
|
||||
mut kind: CodegenKind,
|
||||
kind: CodegenKind,
|
||||
provider: Arc<dyn CompletionProvider>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
match &mut kind {
|
||||
CodegenKind::Transform { range } => {
|
||||
let mut point_range = range.to_point(&snapshot);
|
||||
point_range.start.column = 0;
|
||||
if point_range.end.column > 0 || point_range.start.row == point_range.end.row {
|
||||
point_range.end.column = snapshot.line_len(point_range.end.row);
|
||||
}
|
||||
range.start = snapshot.anchor_before(point_range.start);
|
||||
range.end = snapshot.anchor_after(point_range.end);
|
||||
}
|
||||
CodegenKind::Generate { position } => {
|
||||
*position = position.bias_right(&snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
provider,
|
||||
buffer: buffer.clone(),
|
||||
@@ -386,7 +369,7 @@ mod tests {
|
||||
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let range = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 4))..snapshot.anchor_after(Point::new(4, 4))
|
||||
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
|
||||
});
|
||||
let provider = Arc::new(TestCompletionProvider::new());
|
||||
let codegen = cx.add_model(|cx| {
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::cmp::{self, Reverse};
|
||||
use std::fmt::Write;
|
||||
use std::ops::Range;
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> String {
|
||||
#[derive(Debug)]
|
||||
struct Match {
|
||||
@@ -121,6 +122,7 @@ pub fn generate_content_prompt(
|
||||
range: Range<impl ToOffset>,
|
||||
kind: CodegenKind,
|
||||
) -> String {
|
||||
let range = range.to_offset(buffer);
|
||||
let mut prompt = String::new();
|
||||
|
||||
// General Preamble
|
||||
@@ -130,17 +132,29 @@ pub fn generate_content_prompt(
|
||||
writeln!(prompt, "You're an expert engineer.\n").unwrap();
|
||||
}
|
||||
|
||||
let outline = summarize(buffer, range);
|
||||
let mut content = String::new();
|
||||
content.extend(buffer.text_for_range(0..range.start));
|
||||
if range.start == range.end {
|
||||
content.push_str("<|START|>");
|
||||
} else {
|
||||
content.push_str("<|START|");
|
||||
}
|
||||
content.extend(buffer.text_for_range(range.clone()));
|
||||
if range.start != range.end {
|
||||
content.push_str("|END|>");
|
||||
}
|
||||
content.extend(buffer.text_for_range(range.end..buffer.len()));
|
||||
|
||||
writeln!(
|
||||
prompt,
|
||||
"The file you are currently working on has the following outline:"
|
||||
"The file you are currently working on has the following content:"
|
||||
)
|
||||
.unwrap();
|
||||
if let Some(language_name) = language_name {
|
||||
let language_name = language_name.to_lowercase();
|
||||
writeln!(prompt, "```{language_name}\n{outline}\n```").unwrap();
|
||||
writeln!(prompt, "```{language_name}\n{content}\n```").unwrap();
|
||||
} else {
|
||||
writeln!(prompt, "```\n{outline}\n```").unwrap();
|
||||
writeln!(prompt, "```\n{content}\n```").unwrap();
|
||||
}
|
||||
|
||||
match kind {
|
||||
|
||||
@@ -20,7 +20,6 @@ test-support = [
|
||||
|
||||
[dependencies]
|
||||
audio = { path = "../audio" }
|
||||
channel = { path = "../channel" }
|
||||
client = { path = "../client" }
|
||||
collections = { path = "../collections" }
|
||||
gpui = { path = "../gpui" }
|
||||
|
||||
@@ -5,7 +5,6 @@ pub mod room;
|
||||
use anyhow::{anyhow, Result};
|
||||
use audio::Audio;
|
||||
use call_settings::CallSettings;
|
||||
use channel::ChannelId;
|
||||
use client::{
|
||||
proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore,
|
||||
ZED_ALWAYS_ACTIVE,
|
||||
@@ -79,7 +78,7 @@ impl ActiveCall {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn channel_id(&self, cx: &AppContext) -> Option<ChannelId> {
|
||||
pub fn channel_id(&self, cx: &AppContext) -> Option<u64> {
|
||||
self.room()?.read(cx).channel_id()
|
||||
}
|
||||
|
||||
|
||||
+46
-16
@@ -18,7 +18,7 @@ use live_kit_client::{
|
||||
LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RemoteAudioTrackUpdate,
|
||||
RemoteVideoTrackUpdate,
|
||||
};
|
||||
use postage::stream::Stream;
|
||||
use postage::{sink::Sink, stream::Stream, watch};
|
||||
use project::Project;
|
||||
use std::{future::Future, mem, pin::Pin, sync::Arc, time::Duration};
|
||||
use util::{post_inc, ResultExt, TryFutureExt};
|
||||
@@ -70,6 +70,8 @@ pub struct Room {
|
||||
user_store: ModelHandle<UserStore>,
|
||||
follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec<PeerId>>,
|
||||
subscriptions: Vec<client::Subscription>,
|
||||
room_update_completed_tx: watch::Sender<Option<()>>,
|
||||
room_update_completed_rx: watch::Receiver<Option<()>>,
|
||||
pending_room_update: Option<Task<()>>,
|
||||
maintain_connection: Option<Task<Option<()>>>,
|
||||
}
|
||||
@@ -211,6 +213,8 @@ impl Room {
|
||||
|
||||
Audio::play_sound(Sound::Joined, cx);
|
||||
|
||||
let (room_update_completed_tx, room_update_completed_rx) = watch::channel();
|
||||
|
||||
Self {
|
||||
id,
|
||||
channel_id,
|
||||
@@ -230,6 +234,8 @@ impl Room {
|
||||
user_store,
|
||||
follows_by_leader_id_project_id: Default::default(),
|
||||
maintain_connection: Some(maintain_connection),
|
||||
room_update_completed_tx,
|
||||
room_update_completed_rx,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -599,28 +605,40 @@ impl Room {
|
||||
}
|
||||
|
||||
/// Returns the most 'active' projects, defined as most people in the project
|
||||
pub fn most_active_project(&self) -> Option<(u64, u64)> {
|
||||
let mut projects = HashMap::default();
|
||||
let mut hosts = HashMap::default();
|
||||
pub fn most_active_project(&self, cx: &AppContext) -> Option<(u64, u64)> {
|
||||
let mut project_hosts_and_guest_counts = HashMap::<u64, (Option<u64>, u32)>::default();
|
||||
for participant in self.remote_participants.values() {
|
||||
match participant.location {
|
||||
ParticipantLocation::SharedProject { project_id } => {
|
||||
*projects.entry(project_id).or_insert(0) += 1;
|
||||
project_hosts_and_guest_counts
|
||||
.entry(project_id)
|
||||
.or_default()
|
||||
.1 += 1;
|
||||
}
|
||||
ParticipantLocation::External | ParticipantLocation::UnsharedProject => {}
|
||||
}
|
||||
for project in &participant.projects {
|
||||
*projects.entry(project.id).or_insert(0) += 1;
|
||||
hosts.insert(project.id, participant.user.id);
|
||||
project_hosts_and_guest_counts
|
||||
.entry(project.id)
|
||||
.or_default()
|
||||
.0 = Some(participant.user.id);
|
||||
}
|
||||
}
|
||||
|
||||
let mut pairs: Vec<(u64, usize)> = projects.into_iter().collect();
|
||||
pairs.sort_by_key(|(_, count)| *count as i32);
|
||||
if let Some(user) = self.user_store.read(cx).current_user() {
|
||||
for project in &self.local_participant.projects {
|
||||
project_hosts_and_guest_counts
|
||||
.entry(project.id)
|
||||
.or_default()
|
||||
.0 = Some(user.id);
|
||||
}
|
||||
}
|
||||
|
||||
pairs
|
||||
.first()
|
||||
.map(|(project_id, _)| (*project_id, hosts[&project_id]))
|
||||
project_hosts_and_guest_counts
|
||||
.into_iter()
|
||||
.filter_map(|(id, (host, guest_count))| Some((id, host?, guest_count)))
|
||||
.max_by_key(|(_, _, guest_count)| *guest_count)
|
||||
.map(|(id, host, _)| (id, host))
|
||||
}
|
||||
|
||||
async fn handle_room_updated(
|
||||
@@ -686,6 +704,7 @@ impl Room {
|
||||
let Some(peer_id) = participant.peer_id else {
|
||||
continue;
|
||||
};
|
||||
let participant_index = ParticipantIndex(participant.participant_index);
|
||||
this.participant_user_ids.insert(participant.user_id);
|
||||
|
||||
let old_projects = this
|
||||
@@ -736,8 +755,9 @@ impl Room {
|
||||
if let Some(remote_participant) =
|
||||
this.remote_participants.get_mut(&participant.user_id)
|
||||
{
|
||||
remote_participant.projects = participant.projects;
|
||||
remote_participant.peer_id = peer_id;
|
||||
remote_participant.projects = participant.projects;
|
||||
remote_participant.participant_index = participant_index;
|
||||
if location != remote_participant.location {
|
||||
remote_participant.location = location;
|
||||
cx.emit(Event::ParticipantLocationChanged {
|
||||
@@ -749,9 +769,7 @@ impl Room {
|
||||
participant.user_id,
|
||||
RemoteParticipant {
|
||||
user: user.clone(),
|
||||
participant_index: ParticipantIndex(
|
||||
participant.participant_index,
|
||||
),
|
||||
participant_index,
|
||||
peer_id,
|
||||
projects: participant.projects,
|
||||
location,
|
||||
@@ -855,6 +873,7 @@ impl Room {
|
||||
});
|
||||
|
||||
this.check_invariants();
|
||||
this.room_update_completed_tx.try_send(Some(())).ok();
|
||||
cx.notify();
|
||||
});
|
||||
}));
|
||||
@@ -863,6 +882,17 @@ impl Room {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn room_update_completed(&mut self) -> impl Future<Output = ()> {
|
||||
let mut done_rx = self.room_update_completed_rx.clone();
|
||||
async move {
|
||||
while let Some(result) = done_rx.next().await {
|
||||
if result.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_video_track_updated(
|
||||
&mut self,
|
||||
change: RemoteVideoTrackUpdate,
|
||||
|
||||
@@ -2,19 +2,21 @@ mod channel_buffer;
|
||||
mod channel_chat;
|
||||
mod channel_store;
|
||||
|
||||
use client::{Client, UserStore};
|
||||
use gpui::{AppContext, ModelHandle};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent, ACKNOWLEDGE_DEBOUNCE_INTERVAL};
|
||||
pub use channel_chat::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId};
|
||||
pub use channel_store::{
|
||||
Channel, ChannelData, ChannelEvent, ChannelId, ChannelMembership, ChannelPath, ChannelStore,
|
||||
};
|
||||
|
||||
use client::Client;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(test)]
|
||||
mod channel_store_tests;
|
||||
|
||||
pub fn init(client: &Arc<Client>) {
|
||||
pub fn init(client: &Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut AppContext) {
|
||||
channel_store::init(client, user_store, cx);
|
||||
channel_buffer::init(client);
|
||||
channel_chat::init(client);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ mod channel_index;
|
||||
|
||||
use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat};
|
||||
use anyhow::{anyhow, Result};
|
||||
use channel_index::ChannelIndex;
|
||||
use client::{Client, Subscription, User, UserId, UserStore};
|
||||
use collections::{hash_map, HashMap, HashSet};
|
||||
use db::RELEASE_CHANNEL;
|
||||
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
|
||||
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
|
||||
use rpc::{
|
||||
@@ -14,7 +16,11 @@ use serde_derive::{Deserialize, Serialize};
|
||||
use std::{borrow::Cow, hash::Hash, mem, ops::Deref, sync::Arc, time::Duration};
|
||||
use util::ResultExt;
|
||||
|
||||
use self::channel_index::ChannelIndex;
|
||||
pub fn init(client: &Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut AppContext) {
|
||||
let channel_store =
|
||||
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
|
||||
cx.set_global(channel_store);
|
||||
}
|
||||
|
||||
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
@@ -47,6 +53,26 @@ pub struct Channel {
|
||||
pub unseen_message_id: Option<u64>,
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
pub fn link(&self) -> String {
|
||||
RELEASE_CHANNEL.link_prefix().to_owned()
|
||||
+ "channel/"
|
||||
+ &self.slug()
|
||||
+ "-"
|
||||
+ &self.id.to_string()
|
||||
}
|
||||
|
||||
pub fn slug(&self) -> String {
|
||||
let slug: String = self
|
||||
.name
|
||||
.chars()
|
||||
.map(|c| if c.is_alphanumeric() { c } else { '-' })
|
||||
.collect();
|
||||
|
||||
slug.trim_matches(|c| c == '-').to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
|
||||
pub struct ChannelPath(Arc<[ChannelId]>);
|
||||
|
||||
@@ -71,6 +97,10 @@ enum OpenedModelHandle<E: Entity> {
|
||||
}
|
||||
|
||||
impl ChannelStore {
|
||||
pub fn global(cx: &AppContext) -> ModelHandle<Self> {
|
||||
cx.global::<ModelHandle<Self>>().clone()
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
|
||||
@@ -340,10 +340,10 @@ fn init_test(cx: &mut AppContext) -> ModelHandle<ChannelStore> {
|
||||
|
||||
cx.foreground().forbid_parking();
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
crate::init(&client);
|
||||
client::init(&client, cx);
|
||||
crate::init(&client, user_store, cx);
|
||||
|
||||
cx.add_model(|cx| ChannelStore::new(client, user_store, cx))
|
||||
ChannelStore::global(cx)
|
||||
}
|
||||
|
||||
fn update_channels(
|
||||
|
||||
@@ -182,6 +182,7 @@ impl Bundle {
|
||||
kCFStringEncodingUTF8,
|
||||
ptr::null(),
|
||||
));
|
||||
// equivalent to: open zed-cli:... -a /Applications/Zed\ Preview.app
|
||||
let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
|
||||
LSOpenFromURLSpec(
|
||||
&LSLaunchURLSpec {
|
||||
|
||||
@@ -70,7 +70,7 @@ pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
|
||||
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
|
||||
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
actions!(client, [SignIn, SignOut]);
|
||||
actions!(client, [SignIn, SignOut, Reconnect]);
|
||||
|
||||
pub fn init_settings(cx: &mut AppContext) {
|
||||
settings::register::<TelemetrySettings>(cx);
|
||||
@@ -102,6 +102,17 @@ pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
|
||||
}
|
||||
}
|
||||
});
|
||||
cx.add_global_action({
|
||||
let client = client.clone();
|
||||
move |_: &Reconnect, cx| {
|
||||
if let Some(client) = client.upgrade() {
|
||||
cx.spawn(|cx| async move {
|
||||
client.reconnect(&cx);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub struct Client {
|
||||
@@ -1212,6 +1223,11 @@ impl Client {
|
||||
self.set_status(Status::SignedOut, cx);
|
||||
}
|
||||
|
||||
pub fn reconnect(self: &Arc<Self>, cx: &AsyncAppContext) {
|
||||
self.peer.teardown();
|
||||
self.set_status(Status::ConnectionLost, cx);
|
||||
}
|
||||
|
||||
fn connection_id(&self) -> Result<ConnectionId> {
|
||||
if let Status::Connected { connection_id, .. } = *self.status().borrow() {
|
||||
Ok(connection_id)
|
||||
|
||||
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.23.3"
|
||||
version = "0.24.0"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
|
||||
@@ -37,8 +37,10 @@ CREATE INDEX "index_contacts_user_id_b" ON "contacts" ("user_id_b");
|
||||
CREATE TABLE "rooms" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"live_kit_room" VARCHAR NOT NULL,
|
||||
"enviroment" VARCHAR,
|
||||
"channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");
|
||||
|
||||
CREATE TABLE "projects" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE rooms ADD COLUMN enviroment TEXT;
|
||||
@@ -0,0 +1 @@
|
||||
CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");
|
||||
@@ -19,21 +19,14 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_root_channel(
|
||||
&self,
|
||||
name: &str,
|
||||
live_kit_room: &str,
|
||||
creator_id: UserId,
|
||||
) -> Result<ChannelId> {
|
||||
self.create_channel(name, None, live_kit_room, creator_id)
|
||||
.await
|
||||
pub async fn create_root_channel(&self, name: &str, creator_id: UserId) -> Result<ChannelId> {
|
||||
self.create_channel(name, None, creator_id).await
|
||||
}
|
||||
|
||||
pub async fn create_channel(
|
||||
&self,
|
||||
name: &str,
|
||||
parent: Option<ChannelId>,
|
||||
live_kit_room: &str,
|
||||
creator_id: UserId,
|
||||
) -> Result<ChannelId> {
|
||||
let name = Self::sanitize_channel_name(name)?;
|
||||
@@ -90,14 +83,6 @@ impl Database {
|
||||
.insert(&*tx)
|
||||
.await?;
|
||||
|
||||
room::ActiveModel {
|
||||
channel_id: ActiveValue::Set(Some(channel.id)),
|
||||
live_kit_room: ActiveValue::Set(live_kit_room.to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok(channel.id)
|
||||
})
|
||||
.await
|
||||
@@ -797,18 +782,36 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn room_id_for_channel(&self, channel_id: ChannelId) -> Result<RoomId> {
|
||||
pub async fn get_or_create_channel_room(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
live_kit_room: &str,
|
||||
enviroment: &str,
|
||||
) -> Result<RoomId> {
|
||||
self.transaction(|tx| async move {
|
||||
let tx = tx;
|
||||
let room = channel::Model {
|
||||
id: channel_id,
|
||||
..Default::default()
|
||||
}
|
||||
.find_related(room::Entity)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("invalid channel"))?;
|
||||
Ok(room.id)
|
||||
|
||||
let room = room::Entity::find()
|
||||
.filter(room::Column::ChannelId.eq(channel_id))
|
||||
.one(&*tx)
|
||||
.await?;
|
||||
|
||||
let room_id = if let Some(room) = room {
|
||||
room.id
|
||||
} else {
|
||||
let result = room::Entity::insert(room::ActiveModel {
|
||||
channel_id: ActiveValue::Set(Some(channel_id)),
|
||||
live_kit_room: ActiveValue::Set(live_kit_room.to_string()),
|
||||
enviroment: ActiveValue::Set(Some(enviroment.to_string())),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
result.last_insert_id
|
||||
};
|
||||
|
||||
Ok(room_id)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -107,10 +107,12 @@ impl Database {
|
||||
user_id: UserId,
|
||||
connection: ConnectionId,
|
||||
live_kit_room: &str,
|
||||
release_channel: &str,
|
||||
) -> Result<proto::Room> {
|
||||
self.transaction(|tx| async move {
|
||||
let room = room::ActiveModel {
|
||||
live_kit_room: ActiveValue::set(live_kit_room.into()),
|
||||
enviroment: ActiveValue::set(Some(release_channel.to_string())),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(&*tx)
|
||||
@@ -270,20 +272,31 @@ impl Database {
|
||||
room_id: RoomId,
|
||||
user_id: UserId,
|
||||
connection: ConnectionId,
|
||||
enviroment: &str,
|
||||
) -> Result<RoomGuard<JoinRoom>> {
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
enum QueryChannelId {
|
||||
enum QueryChannelIdAndEnviroment {
|
||||
ChannelId,
|
||||
Enviroment,
|
||||
}
|
||||
|
||||
let (channel_id, release_channel): (Option<ChannelId>, Option<String>) =
|
||||
room::Entity::find()
|
||||
.select_only()
|
||||
.column(room::Column::ChannelId)
|
||||
.column(room::Column::Enviroment)
|
||||
.filter(room::Column::Id.eq(room_id))
|
||||
.into_values::<_, QueryChannelIdAndEnviroment>()
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such room"))?;
|
||||
|
||||
if let Some(release_channel) = release_channel {
|
||||
if &release_channel != enviroment {
|
||||
Err(anyhow!("must join using the {} release", release_channel))?;
|
||||
}
|
||||
}
|
||||
let channel_id: Option<ChannelId> = room::Entity::find()
|
||||
.select_only()
|
||||
.column(room::Column::ChannelId)
|
||||
.filter(room::Column::Id.eq(room_id))
|
||||
.into_values::<_, QueryChannelId>()
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such room"))?;
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
enum QueryParticipantIndices {
|
||||
@@ -300,6 +313,7 @@ impl Database {
|
||||
.into_values::<_, QueryParticipantIndices>()
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut participant_index = 0;
|
||||
while existing_participant_indices.contains(&participant_index) {
|
||||
participant_index += 1;
|
||||
@@ -818,10 +832,7 @@ impl Database {
|
||||
|
||||
let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
|
||||
let deleted = if room.participants.is_empty() {
|
||||
let result = room::Entity::delete_by_id(room_id)
|
||||
.filter(room::Column::ChannelId.is_null())
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
let result = room::Entity::delete_by_id(room_id).exec(&*tx).await?;
|
||||
result.rows_affected > 0
|
||||
} else {
|
||||
false
|
||||
|
||||
@@ -8,6 +8,7 @@ pub struct Model {
|
||||
pub id: RoomId,
|
||||
pub live_kit_room: String,
|
||||
pub channel_id: Option<ChannelId>,
|
||||
pub enviroment: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
||||
@@ -12,6 +12,8 @@ use sea_orm::ConnectionTrait;
|
||||
use sqlx::migrate::MigrateDatabase;
|
||||
use std::sync::Arc;
|
||||
|
||||
const TEST_RELEASE_CHANNEL: &'static str = "test";
|
||||
|
||||
pub struct TestDb {
|
||||
pub db: Option<Arc<Database>>,
|
||||
pub connection: Option<sqlx::AnyConnection>,
|
||||
|
||||
@@ -54,7 +54,7 @@ async fn test_channel_buffers(db: &Arc<Database>) {
|
||||
|
||||
let owner_id = db.create_server("production").await.unwrap().0 as u32;
|
||||
|
||||
let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
|
||||
let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
|
||||
|
||||
db.invite_channel_member(zed_id, b_id, a_id, false)
|
||||
.await
|
||||
@@ -141,7 +141,7 @@ async fn test_channel_buffers(db: &Arc<Database>) {
|
||||
|
||||
assert_eq!(left_buffer.connections, &[connection_id_a],);
|
||||
|
||||
let cargo_id = db.create_root_channel("cargo", "2", a_id).await.unwrap();
|
||||
let cargo_id = db.create_root_channel("cargo", a_id).await.unwrap();
|
||||
let _ = db
|
||||
.join_channel_buffer(cargo_id, a_id, connection_id_a)
|
||||
.await
|
||||
@@ -207,7 +207,7 @@ async fn test_channel_buffers_last_operations(db: &Database) {
|
||||
let mut text_buffers = Vec::new();
|
||||
for i in 0..3 {
|
||||
let channel = db
|
||||
.create_root_channel(&format!("channel-{i}"), &format!("room-{i}"), user_id)
|
||||
.create_root_channel(&format!("channel-{i}"), user_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -5,7 +5,11 @@ use rpc::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
db::{queries::channels::ChannelGraph, tests::graph, ChannelId, Database, NewUserParams},
|
||||
db::{
|
||||
queries::channels::ChannelGraph,
|
||||
tests::{graph, TEST_RELEASE_CHANNEL},
|
||||
ChannelId, Database, NewUserParams,
|
||||
},
|
||||
test_both_dbs,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
@@ -41,7 +45,7 @@ async fn test_channels(db: &Arc<Database>) {
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
|
||||
let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
|
||||
|
||||
// Make sure that people cannot read channels they haven't been invited to
|
||||
assert!(db.get_channel(zed_id, b_id).await.unwrap().is_none());
|
||||
@@ -54,16 +58,13 @@ async fn test_channels(db: &Arc<Database>) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let crdb_id = db
|
||||
.create_channel("crdb", Some(zed_id), "2", a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
let crdb_id = db.create_channel("crdb", Some(zed_id), a_id).await.unwrap();
|
||||
let livestreaming_id = db
|
||||
.create_channel("livestreaming", Some(zed_id), "3", a_id)
|
||||
.create_channel("livestreaming", Some(zed_id), a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
let replace_id = db
|
||||
.create_channel("replace", Some(zed_id), "4", a_id)
|
||||
.create_channel("replace", Some(zed_id), a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -71,14 +72,14 @@ async fn test_channels(db: &Arc<Database>) {
|
||||
members.sort();
|
||||
assert_eq!(members, &[a_id, b_id]);
|
||||
|
||||
let rust_id = db.create_root_channel("rust", "5", a_id).await.unwrap();
|
||||
let rust_id = db.create_root_channel("rust", a_id).await.unwrap();
|
||||
let cargo_id = db
|
||||
.create_channel("cargo", Some(rust_id), "6", a_id)
|
||||
.create_channel("cargo", Some(rust_id), a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let cargo_ra_id = db
|
||||
.create_channel("cargo-ra", Some(cargo_id), "7", a_id)
|
||||
.create_channel("cargo-ra", Some(cargo_id), a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -198,15 +199,20 @@ async fn test_joining_channels(db: &Arc<Database>) {
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let channel_1 = db
|
||||
.create_root_channel("channel_1", "1", user_1)
|
||||
let channel_1 = db.create_root_channel("channel_1", user_1).await.unwrap();
|
||||
let room_1 = db
|
||||
.get_or_create_channel_room(channel_1, "1", TEST_RELEASE_CHANNEL)
|
||||
.await
|
||||
.unwrap();
|
||||
let room_1 = db.room_id_for_channel(channel_1).await.unwrap();
|
||||
|
||||
// can join a room with membership to its channel
|
||||
let joined_room = db
|
||||
.join_room(room_1, user_1, ConnectionId { owner_id, id: 1 })
|
||||
.join_room(
|
||||
room_1,
|
||||
user_1,
|
||||
ConnectionId { owner_id, id: 1 },
|
||||
TEST_RELEASE_CHANNEL,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(joined_room.room.participants.len(), 1);
|
||||
@@ -214,7 +220,12 @@ async fn test_joining_channels(db: &Arc<Database>) {
|
||||
drop(joined_room);
|
||||
// cannot join a room without membership to its channel
|
||||
assert!(db
|
||||
.join_room(room_1, user_2, ConnectionId { owner_id, id: 1 })
|
||||
.join_room(
|
||||
room_1,
|
||||
user_2,
|
||||
ConnectionId { owner_id, id: 1 },
|
||||
TEST_RELEASE_CHANNEL
|
||||
)
|
||||
.await
|
||||
.is_err());
|
||||
}
|
||||
@@ -269,15 +280,9 @@ async fn test_channel_invites(db: &Arc<Database>) {
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let channel_1_1 = db
|
||||
.create_root_channel("channel_1", "1", user_1)
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_1_1 = db.create_root_channel("channel_1", user_1).await.unwrap();
|
||||
|
||||
let channel_1_2 = db
|
||||
.create_root_channel("channel_2", "2", user_1)
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_1_2 = db.create_root_channel("channel_2", user_1).await.unwrap();
|
||||
|
||||
db.invite_channel_member(channel_1_1, user_2, user_1, false)
|
||||
.await
|
||||
@@ -339,7 +344,7 @@ async fn test_channel_invites(db: &Arc<Database>) {
|
||||
.unwrap();
|
||||
|
||||
let channel_1_3 = db
|
||||
.create_channel("channel_3", Some(channel_1_1), "1", user_1)
|
||||
.create_channel("channel_3", Some(channel_1_1), user_1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -401,7 +406,7 @@ async fn test_channel_renames(db: &Arc<Database>) {
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap();
|
||||
let zed_id = db.create_root_channel("zed", user_1).await.unwrap();
|
||||
|
||||
db.rename_channel(zed_id, user_1, "#zed-archive")
|
||||
.await
|
||||
@@ -446,25 +451,22 @@ async fn test_db_channel_moving(db: &Arc<Database>) {
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
|
||||
let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
|
||||
|
||||
let crdb_id = db
|
||||
.create_channel("crdb", Some(zed_id), "2", a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
let crdb_id = db.create_channel("crdb", Some(zed_id), a_id).await.unwrap();
|
||||
|
||||
let gpui2_id = db
|
||||
.create_channel("gpui2", Some(zed_id), "3", a_id)
|
||||
.create_channel("gpui2", Some(zed_id), a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let livestreaming_id = db
|
||||
.create_channel("livestreaming", Some(crdb_id), "4", a_id)
|
||||
.create_channel("livestreaming", Some(crdb_id), a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let livestreaming_dag_id = db
|
||||
.create_channel("livestreaming_dag", Some(livestreaming_id), "5", a_id)
|
||||
.create_channel("livestreaming_dag", Some(livestreaming_id), a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -517,12 +519,7 @@ async fn test_db_channel_moving(db: &Arc<Database>) {
|
||||
// ========================================================================
|
||||
// Create a new channel below a channel with multiple parents
|
||||
let livestreaming_dag_sub_id = db
|
||||
.create_channel(
|
||||
"livestreaming_dag_sub",
|
||||
Some(livestreaming_dag_id),
|
||||
"6",
|
||||
a_id,
|
||||
)
|
||||
.create_channel("livestreaming_dag_sub", Some(livestreaming_dag_id), a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -812,15 +809,15 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let zed_id = db.create_root_channel("zed", "1", user_id).await.unwrap();
|
||||
let zed_id = db.create_root_channel("zed", user_id).await.unwrap();
|
||||
|
||||
let projects_id = db
|
||||
.create_channel("projects", Some(zed_id), "2", user_id)
|
||||
.create_channel("projects", Some(zed_id), user_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let livestreaming_id = db
|
||||
.create_channel("livestreaming", Some(projects_id), "3", user_id)
|
||||
.create_channel("livestreaming", Some(projects_id), user_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -479,7 +479,7 @@ async fn test_project_count(db: &Arc<Database>) {
|
||||
.unwrap();
|
||||
|
||||
let room_id = RoomId::from_proto(
|
||||
db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "")
|
||||
db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "", "dev")
|
||||
.await
|
||||
.unwrap()
|
||||
.id,
|
||||
@@ -493,9 +493,14 @@ async fn test_project_count(db: &Arc<Database>) {
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 })
|
||||
.await
|
||||
.unwrap();
|
||||
db.join_room(
|
||||
room_id,
|
||||
user2.user_id,
|
||||
ConnectionId { owner_id, id: 1 },
|
||||
"dev",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
|
||||
|
||||
db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
|
||||
@@ -575,6 +580,85 @@ async fn test_fuzzy_search_users() {
|
||||
}
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
test_non_matching_release_channels,
|
||||
test_non_matching_release_channels_postgres,
|
||||
test_non_matching_release_channels_sqlite
|
||||
);
|
||||
|
||||
async fn test_non_matching_release_channels(db: &Arc<Database>) {
|
||||
let owner_id = db.create_server("test").await.unwrap().0 as u32;
|
||||
|
||||
let user1 = db
|
||||
.create_user(
|
||||
&format!("admin@example.com"),
|
||||
true,
|
||||
NewUserParams {
|
||||
github_login: "admin".into(),
|
||||
github_user_id: 0,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let user2 = db
|
||||
.create_user(
|
||||
&format!("user@example.com"),
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user".into(),
|
||||
github_user_id: 1,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let room = db
|
||||
.create_room(
|
||||
user1.user_id,
|
||||
ConnectionId { owner_id, id: 0 },
|
||||
"",
|
||||
"stable",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.call(
|
||||
RoomId::from_proto(room.id),
|
||||
user1.user_id,
|
||||
ConnectionId { owner_id, id: 0 },
|
||||
user2.user_id,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// User attempts to join from preview
|
||||
let result = db
|
||||
.join_room(
|
||||
RoomId::from_proto(room.id),
|
||||
user2.user_id,
|
||||
ConnectionId { owner_id, id: 1 },
|
||||
"preview",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
|
||||
// User switches to stable
|
||||
let result = db
|
||||
.join_room(
|
||||
RoomId::from_proto(room.id),
|
||||
user2.user_id,
|
||||
ConnectionId { owner_id, id: 1 },
|
||||
"stable",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok())
|
||||
}
|
||||
|
||||
fn build_background_executor() -> Arc<Background> {
|
||||
Deterministic::new(0).build_background()
|
||||
}
|
||||
|
||||
@@ -25,10 +25,7 @@ async fn test_channel_message_retrieval(db: &Arc<Database>) {
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
let channel = db
|
||||
.create_channel("channel", None, "room", user)
|
||||
.await
|
||||
.unwrap();
|
||||
let channel = db.create_channel("channel", None, user).await.unwrap();
|
||||
|
||||
let owner_id = db.create_server("test").await.unwrap().0 as u32;
|
||||
db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 0 }, user)
|
||||
@@ -90,10 +87,7 @@ async fn test_channel_message_nonces(db: &Arc<Database>) {
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
let channel = db
|
||||
.create_channel("channel", None, "room", user)
|
||||
.await
|
||||
.unwrap();
|
||||
let channel = db.create_channel("channel", None, user).await.unwrap();
|
||||
|
||||
let owner_id = db.create_server("test").await.unwrap().0 as u32;
|
||||
|
||||
@@ -157,15 +151,9 @@ async fn test_channel_message_new_notification(db: &Arc<Database>) {
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let channel_1 = db
|
||||
.create_channel("channel", None, "room", user)
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_1 = db.create_channel("channel", None, user).await.unwrap();
|
||||
|
||||
let channel_2 = db
|
||||
.create_channel("channel-2", None, "room", user)
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_2 = db.create_channel("channel-2", None, user).await.unwrap();
|
||||
|
||||
db.invite_channel_member(channel_1, observer, user, false)
|
||||
.await
|
||||
|
||||
+24
-15
@@ -63,6 +63,7 @@ use time::OffsetDateTime;
|
||||
use tokio::sync::{watch, Semaphore};
|
||||
use tower::ServiceBuilder;
|
||||
use tracing::{info_span, instrument, Instrument};
|
||||
use util::channel::RELEASE_CHANNEL_NAME;
|
||||
|
||||
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
@@ -937,11 +938,6 @@ async fn create_room(
|
||||
util::async_iife!({
|
||||
let live_kit = live_kit?;
|
||||
|
||||
live_kit
|
||||
.create_room(live_kit_room.clone())
|
||||
.await
|
||||
.trace_err()?;
|
||||
|
||||
let token = live_kit
|
||||
.room_token(&live_kit_room, &session.user_id.to_string())
|
||||
.trace_err()?;
|
||||
@@ -957,7 +953,12 @@ async fn create_room(
|
||||
let room = session
|
||||
.db()
|
||||
.await
|
||||
.create_room(session.user_id, session.connection_id, &live_kit_room)
|
||||
.create_room(
|
||||
session.user_id,
|
||||
session.connection_id,
|
||||
&live_kit_room,
|
||||
RELEASE_CHANNEL_NAME.as_str(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
response.send(proto::CreateRoomResponse {
|
||||
@@ -979,7 +980,12 @@ async fn join_room(
|
||||
let room = session
|
||||
.db()
|
||||
.await
|
||||
.join_room(room_id, session.user_id, session.connection_id)
|
||||
.join_room(
|
||||
room_id,
|
||||
session.user_id,
|
||||
session.connection_id,
|
||||
RELEASE_CHANNEL_NAME.as_str(),
|
||||
)
|
||||
.await?;
|
||||
room_updated(&room.room, &session.peer);
|
||||
room.into_inner()
|
||||
@@ -2195,15 +2201,10 @@ async fn create_channel(
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let db = session.db().await;
|
||||
let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
|
||||
|
||||
if let Some(live_kit) = session.live_kit_client.as_ref() {
|
||||
live_kit.create_room(live_kit_room.clone()).await?;
|
||||
}
|
||||
|
||||
let parent_id = request.parent_id.map(|id| ChannelId::from_proto(id));
|
||||
let id = db
|
||||
.create_channel(&request.name, parent_id, &live_kit_room, session.user_id)
|
||||
.create_channel(&request.name, parent_id, session.user_id)
|
||||
.await?;
|
||||
|
||||
let channel = proto::Channel {
|
||||
@@ -2608,15 +2609,23 @@ async fn join_channel(
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
|
||||
|
||||
let joined_room = {
|
||||
leave_room_for_session(&session).await?;
|
||||
let db = session.db().await;
|
||||
|
||||
let room_id = db.room_id_for_channel(channel_id).await?;
|
||||
let room_id = db
|
||||
.get_or_create_channel_room(channel_id, &live_kit_room, &*RELEASE_CHANNEL_NAME)
|
||||
.await?;
|
||||
|
||||
let joined_room = db
|
||||
.join_room(room_id, session.user_id, session.connection_id)
|
||||
.join_room(
|
||||
room_id,
|
||||
session.user_id,
|
||||
session.connection_id,
|
||||
RELEASE_CHANNEL_NAME.as_str(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let live_kit_connection_info = session.live_kit_client.as_ref().and_then(|live_kit| {
|
||||
|
||||
@@ -380,6 +380,8 @@ async fn test_channel_room(
|
||||
|
||||
// Give everyone a chance to observe user A joining
|
||||
deterministic.run_until_parked();
|
||||
let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
|
||||
room_a.read_with(cx_a, |room, _| assert!(room.is_connected()));
|
||||
|
||||
client_a.channel_store().read_with(cx_a, |channels, _| {
|
||||
assert_participants_eq(
|
||||
|
||||
@@ -184,20 +184,12 @@ async fn test_basic_following(
|
||||
|
||||
// All clients see that clients B and C are following client A.
|
||||
cx_c.foreground().run_until_parked();
|
||||
for (name, active_call, cx) in [
|
||||
("A", &active_call_a, &cx_a),
|
||||
("B", &active_call_b, &cx_b),
|
||||
("C", &active_call_c, &cx_c),
|
||||
("D", &active_call_d, &cx_d),
|
||||
] {
|
||||
active_call.read_with(*cx, |call, cx| {
|
||||
let room = call.room().unwrap().read(cx);
|
||||
assert_eq!(
|
||||
room.followers_for(peer_id_a, project_id),
|
||||
&[peer_id_b, peer_id_c],
|
||||
"checking followers for A as {name}"
|
||||
);
|
||||
});
|
||||
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
assert_eq!(
|
||||
followers_by_leader(project_id, cx),
|
||||
&[(peer_id_a, vec![peer_id_b, peer_id_c])],
|
||||
"followers seen by {name}"
|
||||
);
|
||||
}
|
||||
|
||||
// Client C unfollows client A.
|
||||
@@ -207,46 +199,39 @@ async fn test_basic_following(
|
||||
|
||||
// All clients see that clients B is following client A.
|
||||
cx_c.foreground().run_until_parked();
|
||||
for (name, active_call, cx) in [
|
||||
("A", &active_call_a, &cx_a),
|
||||
("B", &active_call_b, &cx_b),
|
||||
("C", &active_call_c, &cx_c),
|
||||
("D", &active_call_d, &cx_d),
|
||||
] {
|
||||
active_call.read_with(*cx, |call, cx| {
|
||||
let room = call.room().unwrap().read(cx);
|
||||
assert_eq!(
|
||||
room.followers_for(peer_id_a, project_id),
|
||||
&[peer_id_b],
|
||||
"checking followers for A as {name}"
|
||||
);
|
||||
});
|
||||
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
assert_eq!(
|
||||
followers_by_leader(project_id, cx),
|
||||
&[(peer_id_a, vec![peer_id_b])],
|
||||
"followers seen by {name}"
|
||||
);
|
||||
}
|
||||
|
||||
// Client C re-follows client A.
|
||||
workspace_c.update(cx_c, |workspace, cx| {
|
||||
workspace.follow(peer_id_a, cx);
|
||||
});
|
||||
workspace_c
|
||||
.update(cx_c, |workspace, cx| {
|
||||
workspace.follow(peer_id_a, cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// All clients see that clients B and C are following client A.
|
||||
cx_c.foreground().run_until_parked();
|
||||
for (name, active_call, cx) in [
|
||||
("A", &active_call_a, &cx_a),
|
||||
("B", &active_call_b, &cx_b),
|
||||
("C", &active_call_c, &cx_c),
|
||||
("D", &active_call_d, &cx_d),
|
||||
] {
|
||||
active_call.read_with(*cx, |call, cx| {
|
||||
let room = call.room().unwrap().read(cx);
|
||||
assert_eq!(
|
||||
room.followers_for(peer_id_a, project_id),
|
||||
&[peer_id_b, peer_id_c],
|
||||
"checking followers for A as {name}"
|
||||
);
|
||||
});
|
||||
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
assert_eq!(
|
||||
followers_by_leader(project_id, cx),
|
||||
&[(peer_id_a, vec![peer_id_b, peer_id_c])],
|
||||
"followers seen by {name}"
|
||||
);
|
||||
}
|
||||
|
||||
// Client D follows client C.
|
||||
// Client D follows client B, then switches to following client C.
|
||||
workspace_d
|
||||
.update(cx_d, |workspace, cx| {
|
||||
workspace.follow(peer_id_b, cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
workspace_d
|
||||
.update(cx_d, |workspace, cx| {
|
||||
workspace.follow(peer_id_c, cx).unwrap()
|
||||
@@ -256,20 +241,15 @@ async fn test_basic_following(
|
||||
|
||||
// All clients see that D is following C
|
||||
cx_d.foreground().run_until_parked();
|
||||
for (name, active_call, cx) in [
|
||||
("A", &active_call_a, &cx_a),
|
||||
("B", &active_call_b, &cx_b),
|
||||
("C", &active_call_c, &cx_c),
|
||||
("D", &active_call_d, &cx_d),
|
||||
] {
|
||||
active_call.read_with(*cx, |call, cx| {
|
||||
let room = call.room().unwrap().read(cx);
|
||||
assert_eq!(
|
||||
room.followers_for(peer_id_c, project_id),
|
||||
&[peer_id_d],
|
||||
"checking followers for C as {name}"
|
||||
);
|
||||
});
|
||||
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
assert_eq!(
|
||||
followers_by_leader(project_id, cx),
|
||||
&[
|
||||
(peer_id_a, vec![peer_id_b, peer_id_c]),
|
||||
(peer_id_c, vec![peer_id_d])
|
||||
],
|
||||
"followers seen by {name}"
|
||||
);
|
||||
}
|
||||
|
||||
// Client C closes the project.
|
||||
@@ -278,32 +258,12 @@ async fn test_basic_following(
|
||||
|
||||
// Clients A and B see that client B is following A, and client C is not present in the followers.
|
||||
cx_c.foreground().run_until_parked();
|
||||
for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] {
|
||||
active_call.read_with(*cx, |call, cx| {
|
||||
let room = call.room().unwrap().read(cx);
|
||||
assert_eq!(
|
||||
room.followers_for(peer_id_a, project_id),
|
||||
&[peer_id_b],
|
||||
"checking followers for A as {name}"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// All clients see that no-one is following C
|
||||
for (name, active_call, cx) in [
|
||||
("A", &active_call_a, &cx_a),
|
||||
("B", &active_call_b, &cx_b),
|
||||
("C", &active_call_c, &cx_c),
|
||||
("D", &active_call_d, &cx_d),
|
||||
] {
|
||||
active_call.read_with(*cx, |call, cx| {
|
||||
let room = call.room().unwrap().read(cx);
|
||||
assert_eq!(
|
||||
room.followers_for(peer_id_c, project_id),
|
||||
&[],
|
||||
"checking followers for C as {name}"
|
||||
);
|
||||
});
|
||||
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
assert_eq!(
|
||||
followers_by_leader(project_id, cx),
|
||||
&[(peer_id_a, vec![peer_id_b]),],
|
||||
"followers seen by {name}"
|
||||
);
|
||||
}
|
||||
|
||||
// When client A activates a different editor, client B does so as well.
|
||||
@@ -1667,6 +1627,30 @@ struct PaneSummary {
|
||||
items: Vec<(bool, String)>,
|
||||
}
|
||||
|
||||
fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec<PeerId>)> {
|
||||
cx.read(|cx| {
|
||||
let active_call = ActiveCall::global(cx).read(cx);
|
||||
let peer_id = active_call.client().peer_id();
|
||||
let room = active_call.room().unwrap().read(cx);
|
||||
let mut result = room
|
||||
.remote_participants()
|
||||
.values()
|
||||
.map(|participant| participant.peer_id)
|
||||
.chain(peer_id)
|
||||
.filter_map(|peer_id| {
|
||||
let followers = room.followers_for(peer_id, project_id);
|
||||
if followers.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some((peer_id, followers.to_vec()))
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
result.sort_by_key(|e| e.0);
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
fn pane_summaries(workspace: &ViewHandle<Workspace>, cx: &mut TestAppContext) -> Vec<PaneSummary> {
|
||||
workspace.read_with(cx, |workspace, cx| {
|
||||
let active_pane = workspace.active_pane();
|
||||
|
||||
@@ -46,12 +46,7 @@ impl RandomizedTest for RandomChannelBufferTest {
|
||||
let db = &server.app_state.db;
|
||||
for ix in 0..CHANNEL_COUNT {
|
||||
let id = db
|
||||
.create_channel(
|
||||
&format!("channel-{ix}"),
|
||||
None,
|
||||
&format!("livekit-room-{ix}"),
|
||||
users[0].user_id,
|
||||
)
|
||||
.create_channel(&format!("channel-{ix}"), None, users[0].user_id)
|
||||
.await
|
||||
.unwrap();
|
||||
for user in &users[1..] {
|
||||
|
||||
@@ -44,6 +44,7 @@ pub struct TestServer {
|
||||
pub struct TestClient {
|
||||
pub username: String,
|
||||
pub app_state: Arc<workspace::AppState>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
state: RefCell<TestClientState>,
|
||||
}
|
||||
|
||||
@@ -206,15 +207,12 @@ impl TestServer {
|
||||
let fs = FakeFs::new(cx.background());
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||
let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
|
||||
let channel_store =
|
||||
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
|
||||
let mut language_registry = LanguageRegistry::test();
|
||||
language_registry.set_executor(cx.background());
|
||||
let app_state = Arc::new(workspace::AppState {
|
||||
client: client.clone(),
|
||||
user_store: user_store.clone(),
|
||||
workspace_store,
|
||||
channel_store: channel_store.clone(),
|
||||
languages: Arc::new(language_registry),
|
||||
fs: fs.clone(),
|
||||
build_window_options: |_, _, _| Default::default(),
|
||||
@@ -231,7 +229,7 @@ impl TestServer {
|
||||
workspace::init(app_state.clone(), cx);
|
||||
audio::init((), cx);
|
||||
call::init(client.clone(), user_store.clone(), cx);
|
||||
channel::init(&client);
|
||||
channel::init(&client, user_store, cx);
|
||||
});
|
||||
|
||||
client
|
||||
@@ -242,6 +240,7 @@ impl TestServer {
|
||||
let client = TestClient {
|
||||
app_state,
|
||||
username: name.to_string(),
|
||||
channel_store: cx.read(ChannelStore::global).clone(),
|
||||
state: Default::default(),
|
||||
};
|
||||
client.wait_for_current_user(cx).await;
|
||||
@@ -310,10 +309,9 @@ impl TestServer {
|
||||
admin: (&TestClient, &mut TestAppContext),
|
||||
members: &mut [(&TestClient, &mut TestAppContext)],
|
||||
) -> u64 {
|
||||
let (admin_client, admin_cx) = admin;
|
||||
let channel_id = admin_client
|
||||
.app_state
|
||||
.channel_store
|
||||
let (_, admin_cx) = admin;
|
||||
let channel_id = admin_cx
|
||||
.read(ChannelStore::global)
|
||||
.update(admin_cx, |channel_store, cx| {
|
||||
channel_store.create_channel(channel, parent, cx)
|
||||
})
|
||||
@@ -321,9 +319,8 @@ impl TestServer {
|
||||
.unwrap();
|
||||
|
||||
for (member_client, member_cx) in members {
|
||||
admin_client
|
||||
.app_state
|
||||
.channel_store
|
||||
admin_cx
|
||||
.read(ChannelStore::global)
|
||||
.update(admin_cx, |channel_store, cx| {
|
||||
channel_store.invite_member(
|
||||
channel_id,
|
||||
@@ -337,9 +334,8 @@ impl TestServer {
|
||||
|
||||
admin_cx.foreground().run_until_parked();
|
||||
|
||||
member_client
|
||||
.app_state
|
||||
.channel_store
|
||||
member_cx
|
||||
.read(ChannelStore::global)
|
||||
.update(*member_cx, |channels, _| {
|
||||
channels.respond_to_channel_invite(channel_id, true)
|
||||
})
|
||||
@@ -447,7 +443,7 @@ impl TestClient {
|
||||
}
|
||||
|
||||
pub fn channel_store(&self) -> &ModelHandle<ChannelStore> {
|
||||
&self.app_state.channel_store
|
||||
&self.channel_store
|
||||
}
|
||||
|
||||
pub fn user_store(&self) -> &ModelHandle<UserStore> {
|
||||
@@ -614,8 +610,8 @@ impl TestClient {
|
||||
) {
|
||||
let (other_client, other_cx) = user;
|
||||
|
||||
self.app_state
|
||||
.channel_store
|
||||
cx_self
|
||||
.read(ChannelStore::global)
|
||||
.update(cx_self, |channel_store, cx| {
|
||||
channel_store.invite_member(channel, other_client.user_id().unwrap(), true, cx)
|
||||
})
|
||||
@@ -624,11 +620,10 @@ impl TestClient {
|
||||
|
||||
cx_self.foreground().run_until_parked();
|
||||
|
||||
other_client
|
||||
.app_state
|
||||
.channel_store
|
||||
.update(other_cx, |channels, _| {
|
||||
channels.respond_to_channel_invite(channel, true)
|
||||
other_cx
|
||||
.read(ChannelStore::global)
|
||||
.update(other_cx, |channel_store, _| {
|
||||
channel_store.respond_to_channel_invite(channel, true)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -73,7 +73,7 @@ impl ChannelView {
|
||||
) -> Task<Result<ViewHandle<Self>>> {
|
||||
let workspace = workspace.read(cx);
|
||||
let project = workspace.project().to_owned();
|
||||
let channel_store = workspace.app_state().channel_store.clone();
|
||||
let channel_store = ChannelStore::global(cx);
|
||||
let markdown = workspace
|
||||
.app_state()
|
||||
.languages
|
||||
|
||||
@@ -81,7 +81,7 @@ impl ChatPanel {
|
||||
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let client = workspace.app_state().client.clone();
|
||||
let channel_store = workspace.app_state().channel_store.clone();
|
||||
let channel_store = ChannelStore::global(cx);
|
||||
let languages = workspace.app_state().languages.clone();
|
||||
|
||||
let input_editor = cx.add_view(|cx| {
|
||||
|
||||
@@ -34,8 +34,8 @@ use gpui::{
|
||||
},
|
||||
impl_actions,
|
||||
platform::{CursorStyle, MouseButton, PromptLevel},
|
||||
serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, FontCache, ModelHandle,
|
||||
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
serde_json, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, FontCache,
|
||||
ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use menu::{Confirm, SelectNext, SelectPrev};
|
||||
use project::{Fs, Project};
|
||||
@@ -100,6 +100,11 @@ pub struct JoinChannelChat {
|
||||
pub channel_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct CopyChannelLink {
|
||||
pub channel_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
struct StartMoveChannelFor {
|
||||
channel_id: ChannelId,
|
||||
@@ -157,6 +162,7 @@ impl_actions!(
|
||||
OpenChannelNotes,
|
||||
JoinChannelCall,
|
||||
JoinChannelChat,
|
||||
CopyChannelLink,
|
||||
LinkChannel,
|
||||
StartMoveChannelFor,
|
||||
StartLinkChannelFor,
|
||||
@@ -205,6 +211,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(CollabPanel::expand_selected_channel);
|
||||
cx.add_action(CollabPanel::open_channel_notes);
|
||||
cx.add_action(CollabPanel::join_channel_chat);
|
||||
cx.add_action(CollabPanel::copy_channel_link);
|
||||
|
||||
cx.add_action(
|
||||
|panel: &mut CollabPanel, action: &ToggleSelectedIx, cx: &mut ViewContext<CollabPanel>| {
|
||||
@@ -648,7 +655,7 @@ impl CollabPanel {
|
||||
channel_editing_state: None,
|
||||
selection: None,
|
||||
user_store: workspace.user_store().clone(),
|
||||
channel_store: workspace.app_state().channel_store.clone(),
|
||||
channel_store: ChannelStore::global(cx),
|
||||
project: workspace.project().clone(),
|
||||
subscriptions: Vec::default(),
|
||||
match_candidates: Vec::default(),
|
||||
@@ -2568,6 +2575,13 @@ impl CollabPanel {
|
||||
},
|
||||
));
|
||||
|
||||
items.push(ContextMenuItem::action(
|
||||
"Copy Channel Link",
|
||||
CopyChannelLink {
|
||||
channel_id: path.channel_id(),
|
||||
},
|
||||
));
|
||||
|
||||
if self.channel_store.read(cx).is_user_admin(path.channel_id()) {
|
||||
let parent_id = path.parent_id();
|
||||
|
||||
@@ -3187,49 +3201,19 @@ impl CollabPanel {
|
||||
}
|
||||
|
||||
fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
|
||||
let workspace = self.workspace.clone();
|
||||
let window = cx.window();
|
||||
let active_call = ActiveCall::global(cx);
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
if active_call.read_with(&mut cx, |active_call, cx| {
|
||||
if let Some(room) = active_call.room() {
|
||||
let room = room.read(cx);
|
||||
room.is_sharing_project() && room.remote_participants().len() > 0
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}) {
|
||||
let answer = window.prompt(
|
||||
PromptLevel::Warning,
|
||||
"Leaving this call will unshare your current project.\nDo you want to switch channels?",
|
||||
&["Yes, Join Channel", "Cancel"],
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
if let Some(mut answer) = answer {
|
||||
if answer.next().await == Some(1) {
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let room = active_call
|
||||
.update(&mut cx, |call, cx| call.join_channel(channel_id, cx))
|
||||
.await?;
|
||||
|
||||
let task = room.update(&mut cx, |room, cx| {
|
||||
let workspace = workspace.upgrade(cx)?;
|
||||
let (project, host) = room.most_active_project()?;
|
||||
let app_state = workspace.read(cx).app_state().clone();
|
||||
Some(workspace::join_remote_project(project, host, app_state, cx))
|
||||
});
|
||||
if let Some(task) = task {
|
||||
task.await?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
let Some(workspace) = self.workspace.upgrade(cx) else {
|
||||
return;
|
||||
};
|
||||
let Some(handle) = cx.window().downcast::<Workspace>() else {
|
||||
return;
|
||||
};
|
||||
workspace::join_channel(
|
||||
channel_id,
|
||||
workspace.read(cx).app_state().clone(),
|
||||
Some(handle),
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx)
|
||||
}
|
||||
|
||||
fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext<Self>) {
|
||||
@@ -3246,6 +3230,15 @@ impl CollabPanel {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_channel_link(&mut self, action: &CopyChannelLink, cx: &mut ViewContext<Self>) {
|
||||
let channel_store = self.channel_store.read(cx);
|
||||
let Some(channel) = channel_store.channel_for_id(action.channel_id) else {
|
||||
return;
|
||||
};
|
||||
let item = ClipboardItem::new(channel.link());
|
||||
cx.write_to_clipboard(item)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_tree_branch(
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::{
|
||||
contact_notification::ContactNotification, face_pile::FacePile, toggle_deafen, toggle_mute,
|
||||
toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute, ToggleScreenSharing,
|
||||
};
|
||||
use auto_update::AutoUpdateStatus;
|
||||
use call::{ActiveCall, ParticipantLocation, Room};
|
||||
use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore};
|
||||
use clock::ReplicaId;
|
||||
@@ -1177,22 +1178,38 @@ impl CollabTitlebarItem {
|
||||
.with_style(theme.titlebar.offline_icon.container)
|
||||
.into_any(),
|
||||
),
|
||||
client::Status::UpgradeRequired => Some(
|
||||
MouseEventHandler::new::<ConnectionStatusButton, _>(0, cx, |_, _| {
|
||||
Label::new(
|
||||
"Please update Zed to collaborate",
|
||||
theme.titlebar.outdated_warning.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.titlebar.outdated_warning.container)
|
||||
.aligned()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
auto_update::check(&Default::default(), cx);
|
||||
})
|
||||
.into_any(),
|
||||
),
|
||||
client::Status::UpgradeRequired => {
|
||||
let auto_updater = auto_update::AutoUpdater::get(cx);
|
||||
let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
|
||||
Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate",
|
||||
Some(AutoUpdateStatus::Installing)
|
||||
| Some(AutoUpdateStatus::Downloading)
|
||||
| Some(AutoUpdateStatus::Checking) => "Updating...",
|
||||
Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
|
||||
"Please update Zed to Collaborate"
|
||||
}
|
||||
};
|
||||
|
||||
Some(
|
||||
MouseEventHandler::new::<ConnectionStatusButton, _>(0, cx, |_, _| {
|
||||
Label::new(label, theme.titlebar.outdated_warning.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.titlebar.outdated_warning.container)
|
||||
.aligned()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
|
||||
if auto_updater.read(cx).status() == AutoUpdateStatus::Updated {
|
||||
workspace::restart(&Default::default(), cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
auto_update::check(&Default::default(), cx);
|
||||
})
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1333,7 +1333,7 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon
|
||||
|
||||
cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
|
||||
cx.assert_editor_state(
|
||||
&r#"ˇone
|
||||
&r#"one
|
||||
two
|
||||
|
||||
three
|
||||
@@ -1344,9 +1344,22 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
|
||||
cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
|
||||
cx.assert_editor_state(
|
||||
&r#"ˇone
|
||||
&r#"one
|
||||
two
|
||||
|
||||
three
|
||||
four
|
||||
five
|
||||
ˇ
|
||||
six"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
|
||||
cx.assert_editor_state(
|
||||
&r#"one
|
||||
two
|
||||
ˇ
|
||||
three
|
||||
@@ -1366,32 +1379,6 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon
|
||||
four
|
||||
five
|
||||
|
||||
sixˇ"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
|
||||
cx.assert_editor_state(
|
||||
&r#"one
|
||||
two
|
||||
|
||||
three
|
||||
four
|
||||
five
|
||||
ˇ
|
||||
sixˇ"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
|
||||
cx.assert_editor_state(
|
||||
&r#"one
|
||||
two
|
||||
ˇ
|
||||
three
|
||||
four
|
||||
five
|
||||
ˇ
|
||||
six"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
@@ -234,7 +234,7 @@ pub fn start_of_paragraph(
|
||||
) -> DisplayPoint {
|
||||
let point = display_point.to_point(map);
|
||||
if point.row == 0 {
|
||||
return map.max_point();
|
||||
return DisplayPoint::zero();
|
||||
}
|
||||
|
||||
let mut found_non_blank_line = false;
|
||||
@@ -261,7 +261,7 @@ pub fn end_of_paragraph(
|
||||
) -> DisplayPoint {
|
||||
let point = display_point.to_point(map);
|
||||
if point.row == map.max_buffer_row() {
|
||||
return DisplayPoint::zero();
|
||||
return map.max_point();
|
||||
}
|
||||
|
||||
let mut found_non_blank_line = false;
|
||||
|
||||
@@ -107,13 +107,23 @@ fn matching_history_item_paths(
|
||||
) -> HashMap<Arc<Path>, PathMatch> {
|
||||
let history_items_by_worktrees = history_items
|
||||
.iter()
|
||||
.map(|found_path| {
|
||||
let path = &found_path.project.path;
|
||||
.filter_map(|found_path| {
|
||||
let candidate = PathMatchCandidate {
|
||||
path,
|
||||
char_bag: CharBag::from_iter(path.to_string_lossy().to_lowercase().chars()),
|
||||
path: &found_path.project.path,
|
||||
// Only match history items names, otherwise their paths may match too many queries, producing false positives.
|
||||
// E.g. `foo` would match both `something/foo/bar.rs` and `something/foo/foo.rs` and if the former is a history item,
|
||||
// it would be shown first always, despite the latter being a better match.
|
||||
char_bag: CharBag::from_iter(
|
||||
found_path
|
||||
.project
|
||||
.path
|
||||
.file_name()?
|
||||
.to_string_lossy()
|
||||
.to_lowercase()
|
||||
.chars(),
|
||||
),
|
||||
};
|
||||
(found_path.project.worktree_id, candidate)
|
||||
Some((found_path.project.worktree_id, candidate))
|
||||
})
|
||||
.fold(
|
||||
HashMap::default(),
|
||||
@@ -212,6 +222,10 @@ fn toggle_or_cycle_file_finder(
|
||||
.as_ref()
|
||||
.and_then(|found_path| found_path.absolute.as_ref())
|
||||
})
|
||||
.filter(|(_, history_abs_path)| match history_abs_path {
|
||||
Some(abs_path) => history_file_exists(abs_path),
|
||||
None => true,
|
||||
})
|
||||
.map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
|
||||
)
|
||||
.collect();
|
||||
@@ -236,6 +250,16 @@ fn toggle_or_cycle_file_finder(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
fn history_file_exists(abs_path: &PathBuf) -> bool {
|
||||
abs_path.exists()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn history_file_exists(abs_path: &PathBuf) -> bool {
|
||||
!abs_path.ends_with("nonexistent.rs")
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
Selected(ProjectPath),
|
||||
Dismissed,
|
||||
@@ -505,12 +529,7 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
project
|
||||
.worktree_for_id(history_item.project.worktree_id, cx)
|
||||
.is_some()
|
||||
|| (project.is_local()
|
||||
&& history_item
|
||||
.absolute
|
||||
.as_ref()
|
||||
.filter(|abs_path| abs_path.exists())
|
||||
.is_some())
|
||||
|| (project.is_local() && history_item.absolute.is_some())
|
||||
})
|
||||
.cloned()
|
||||
.map(|p| (p, None))
|
||||
@@ -1803,6 +1822,202 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_history_items_vs_very_good_external_match(
|
||||
deterministic: Arc<gpui::executor::Deterministic>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
let app_state = init_test(cx);
|
||||
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/src",
|
||||
json!({
|
||||
"collab_ui": {
|
||||
"first.rs": "// First Rust file",
|
||||
"second.rs": "// Second Rust file",
|
||||
"third.rs": "// Third Rust file",
|
||||
"collab_ui.rs": "// Fourth Rust file",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
||||
let window = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||
let workspace = window.root(cx);
|
||||
// generate some history to select from
|
||||
open_close_queried_buffer(
|
||||
"fir",
|
||||
1,
|
||||
"first.rs",
|
||||
window.into(),
|
||||
&workspace,
|
||||
&deterministic,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
open_close_queried_buffer(
|
||||
"sec",
|
||||
1,
|
||||
"second.rs",
|
||||
window.into(),
|
||||
&workspace,
|
||||
&deterministic,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
open_close_queried_buffer(
|
||||
"thi",
|
||||
1,
|
||||
"third.rs",
|
||||
window.into(),
|
||||
&workspace,
|
||||
&deterministic,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
open_close_queried_buffer(
|
||||
"sec",
|
||||
1,
|
||||
"second.rs",
|
||||
window.into(),
|
||||
&workspace,
|
||||
&deterministic,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.dispatch_action(window.into(), Toggle);
|
||||
let query = "collab_ui";
|
||||
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
|
||||
finder
|
||||
.update(cx, |finder, cx| {
|
||||
finder.delegate_mut().update_matches(query.to_string(), cx)
|
||||
})
|
||||
.await;
|
||||
finder.read_with(cx, |finder, _| {
|
||||
let delegate = finder.delegate();
|
||||
assert!(
|
||||
delegate.matches.history.is_empty(),
|
||||
"History items should not math query {query}, they should be matched by name only"
|
||||
);
|
||||
|
||||
let search_entries = delegate
|
||||
.matches
|
||||
.search
|
||||
.iter()
|
||||
.map(|path_match| path_match.path.to_path_buf())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
search_entries,
|
||||
vec![
|
||||
PathBuf::from("collab_ui/collab_ui.rs"),
|
||||
PathBuf::from("collab_ui/third.rs"),
|
||||
PathBuf::from("collab_ui/first.rs"),
|
||||
PathBuf::from("collab_ui/second.rs"),
|
||||
],
|
||||
"Despite all search results having the same directory name, the most matching one should be on top"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_nonexistent_history_items_not_shown(
|
||||
deterministic: Arc<gpui::executor::Deterministic>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
let app_state = init_test(cx);
|
||||
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/src",
|
||||
json!({
|
||||
"test": {
|
||||
"first.rs": "// First Rust file",
|
||||
"nonexistent.rs": "// Second Rust file",
|
||||
"third.rs": "// Third Rust file",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
||||
let window = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||
let workspace = window.root(cx);
|
||||
// generate some history to select from
|
||||
open_close_queried_buffer(
|
||||
"fir",
|
||||
1,
|
||||
"first.rs",
|
||||
window.into(),
|
||||
&workspace,
|
||||
&deterministic,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
open_close_queried_buffer(
|
||||
"non",
|
||||
1,
|
||||
"nonexistent.rs",
|
||||
window.into(),
|
||||
&workspace,
|
||||
&deterministic,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
open_close_queried_buffer(
|
||||
"thi",
|
||||
1,
|
||||
"third.rs",
|
||||
window.into(),
|
||||
&workspace,
|
||||
&deterministic,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
open_close_queried_buffer(
|
||||
"fir",
|
||||
1,
|
||||
"first.rs",
|
||||
window.into(),
|
||||
&workspace,
|
||||
&deterministic,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.dispatch_action(window.into(), Toggle);
|
||||
let query = "rs";
|
||||
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
|
||||
finder
|
||||
.update(cx, |finder, cx| {
|
||||
finder.delegate_mut().update_matches(query.to_string(), cx)
|
||||
})
|
||||
.await;
|
||||
finder.read_with(cx, |finder, _| {
|
||||
let delegate = finder.delegate();
|
||||
let history_entries = delegate
|
||||
.matches
|
||||
.history
|
||||
.iter()
|
||||
.map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
history_entries,
|
||||
vec![
|
||||
PathBuf::from("test/first.rs"),
|
||||
PathBuf::from("test/third.rs"),
|
||||
],
|
||||
"Should have all opened files in the history, except the ones that do not exist on disk"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async fn open_close_queried_buffer(
|
||||
input: &str,
|
||||
expected_matches: usize,
|
||||
|
||||
@@ -13,7 +13,6 @@ rope = { path = "../rope" }
|
||||
text = { path = "../text" }
|
||||
util = { path = "../util" }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
rpc = { path = "../rpc" }
|
||||
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
|
||||
@@ -2,7 +2,6 @@ use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use git2::{BranchType, StatusShow};
|
||||
use parking_lot::Mutex;
|
||||
use rpc::proto;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
@@ -23,6 +22,7 @@ pub struct Branch {
|
||||
/// Timestamp of most recent commit, normalized to Unix Epoch format.
|
||||
pub unix_timestamp: Option<i64>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait GitRepository: Send {
|
||||
fn reload_index(&self);
|
||||
@@ -358,24 +358,6 @@ impl GitFileStatus {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_proto(git_status: Option<i32>) -> Option<GitFileStatus> {
|
||||
git_status.and_then(|status| {
|
||||
proto::GitStatus::from_i32(status).map(|status| match status {
|
||||
proto::GitStatus::Added => GitFileStatus::Added,
|
||||
proto::GitStatus::Modified => GitFileStatus::Modified,
|
||||
proto::GitStatus::Conflict => GitFileStatus::Conflict,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_proto(self) -> i32 {
|
||||
match self {
|
||||
GitFileStatus::Added => proto::GitStatus::Added as i32,
|
||||
GitFileStatus::Modified => proto::GitStatus::Modified as i32,
|
||||
GitFileStatus::Conflict => proto::GitStatus::Conflict as i32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]
|
||||
|
||||
@@ -441,7 +441,7 @@ mod tests {
|
||||
score,
|
||||
worktree_id: 0,
|
||||
positions: Vec::new(),
|
||||
path: candidate.path.clone(),
|
||||
path: Arc::from(candidate.path),
|
||||
path_prefix: "".into(),
|
||||
distance_to_relative_ancestor: usize::MAX,
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::{
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PathMatchCandidate<'a> {
|
||||
pub path: &'a Arc<Path>,
|
||||
pub path: &'a Path,
|
||||
pub char_bag: CharBag,
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ pub fn match_fixed_path_set(
|
||||
score,
|
||||
worktree_id,
|
||||
positions: Vec::new(),
|
||||
path: candidate.path.clone(),
|
||||
path: Arc::from(candidate.path),
|
||||
path_prefix: Arc::from(""),
|
||||
distance_to_relative_ancestor: usize::MAX,
|
||||
},
|
||||
@@ -195,7 +195,7 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
|
||||
score,
|
||||
worktree_id,
|
||||
positions: Vec::new(),
|
||||
path: candidate.path.clone(),
|
||||
path: Arc::from(candidate.path),
|
||||
path_prefix: candidate_set.prefix(),
|
||||
distance_to_relative_ancestor: relative_to.as_ref().map_or(
|
||||
usize::MAX,
|
||||
|
||||
@@ -140,6 +140,10 @@ unsafe fn build_classes() {
|
||||
sel!(application:openURLs:),
|
||||
open_urls as extern "C" fn(&mut Object, Sel, id, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(application:continueUserActivity:restorationHandler:),
|
||||
continue_user_activity as extern "C" fn(&mut Object, Sel, id, id, id),
|
||||
);
|
||||
decl.register()
|
||||
}
|
||||
}
|
||||
@@ -1009,6 +1013,26 @@ extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) {
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn continue_user_activity(this: &mut Object, _: Sel, _: id, user_activity: id, _: id) {
|
||||
let url = unsafe {
|
||||
let url: id = msg_send!(user_activity, webpageURL);
|
||||
if url == nil {
|
||||
log::error!("got unexpected user activity");
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
CStr::from_ptr(url.absoluteString().UTF8String())
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
};
|
||||
let platform = unsafe { get_foreground_platform(this) };
|
||||
if let Some(callback) = platform.0.borrow_mut().open_urls.as_mut() {
|
||||
callback(url.into_iter().collect());
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
|
||||
unsafe {
|
||||
let platform = get_foreground_platform(this);
|
||||
|
||||
@@ -13,12 +13,12 @@ use gpui::{
|
||||
scene::{self},
|
||||
LayoutId,
|
||||
};
|
||||
use refineable::{Refineable, RefinementCascade};
|
||||
use refineable::{Cascade, Refineable};
|
||||
use smallvec::SmallVec;
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct Div<V: 'static> {
|
||||
styles: RefinementCascade<Style>,
|
||||
styles: Cascade<Style>,
|
||||
handlers: InteractionHandlers<V>,
|
||||
children: SmallVec<[AnyElement<V>; 2]>,
|
||||
scroll_state: Option<ScrollState>,
|
||||
@@ -263,7 +263,7 @@ impl<V: 'static> Div<V> {
|
||||
impl<V> Styleable for Div<V> {
|
||||
type Style = Style;
|
||||
|
||||
fn style_cascade(&mut self) -> &mut RefinementCascade<Self::Style> {
|
||||
fn style_cascade(&mut self) -> &mut Cascade<Self::Style> {
|
||||
&mut self.styles
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
};
|
||||
use anyhow::Result;
|
||||
use gpui::{geometry::vector::Vector2F, platform::MouseMovedEvent, LayoutId};
|
||||
use refineable::{CascadeSlot, Refineable, RefinementCascade};
|
||||
use refineable::{Cascade, CascadeSlot, Refineable};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
@@ -29,7 +29,7 @@ pub fn hoverable<E: Styleable>(mut child: E) -> Hoverable<E> {
|
||||
impl<E: Styleable> Styleable for Hoverable<E> {
|
||||
type Style = E::Style;
|
||||
|
||||
fn style_cascade(&mut self) -> &mut RefinementCascade<Self::Style> {
|
||||
fn style_cascade(&mut self) -> &mut Cascade<Self::Style> {
|
||||
self.child.style_cascade()
|
||||
}
|
||||
|
||||
|
||||
@@ -7,19 +7,19 @@ use futures::FutureExt;
|
||||
use gpui::geometry::vector::Vector2F;
|
||||
use gpui::scene;
|
||||
use gpui2_macros::IntoElement;
|
||||
use refineable::RefinementCascade;
|
||||
use refineable::Cascade;
|
||||
use util::arc_cow::ArcCow;
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Img {
|
||||
style: RefinementCascade<Style>,
|
||||
style: Cascade<Style>,
|
||||
uri: Option<ArcCow<'static, str>>,
|
||||
}
|
||||
|
||||
pub fn img() -> Img {
|
||||
Img {
|
||||
style: RefinementCascade::default(),
|
||||
style: Cascade::default(),
|
||||
uri: None,
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ impl<V: 'static> Element<V> for Img {
|
||||
impl Styleable for Img {
|
||||
type Style = Style;
|
||||
|
||||
fn style_cascade(&mut self) -> &mut RefinementCascade<Self::Style> {
|
||||
fn style_cascade(&mut self) -> &mut Cascade<Self::Style> {
|
||||
&mut self.style
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
};
|
||||
use anyhow::Result;
|
||||
use gpui::{geometry::vector::Vector2F, platform::MouseButtonEvent, LayoutId};
|
||||
use refineable::{CascadeSlot, Refineable, RefinementCascade};
|
||||
use refineable::{Cascade, CascadeSlot, Refineable};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
@@ -33,7 +33,7 @@ impl<E: Styleable> Styleable for Pressable<E> {
|
||||
&mut self.pressed_style
|
||||
}
|
||||
|
||||
fn style_cascade(&mut self) -> &mut RefinementCascade<E::Style> {
|
||||
fn style_cascade(&mut self) -> &mut Cascade<E::Style> {
|
||||
self.child.style_cascade()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,20 +4,20 @@ use crate::{
|
||||
Element, IntoElement, Layout, LayoutId, Rgba,
|
||||
};
|
||||
use gpui::geometry::vector::Vector2F;
|
||||
use refineable::RefinementCascade;
|
||||
use refineable::Cascade;
|
||||
use std::borrow::Cow;
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Svg {
|
||||
path: Option<Cow<'static, str>>,
|
||||
style: RefinementCascade<Style>,
|
||||
style: Cascade<Style>,
|
||||
}
|
||||
|
||||
pub fn svg() -> Svg {
|
||||
Svg {
|
||||
path: None,
|
||||
style: RefinementCascade::<Style>::default(),
|
||||
style: Cascade::<Style>::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ impl<V: 'static> Element<V> for Svg {
|
||||
impl Styleable for Svg {
|
||||
type Style = Style;
|
||||
|
||||
fn style_cascade(&mut self) -> &mut refineable::RefinementCascade<Self::Style> {
|
||||
fn style_cascade(&mut self) -> &mut refineable::Cascade<Self::Style> {
|
||||
&mut self.style
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ use gpui::{
|
||||
scene, taffy, WindowContext,
|
||||
};
|
||||
use gpui2_macros::styleable_helpers;
|
||||
use refineable::{Refineable, RefinementCascade};
|
||||
use refineable::{Cascade, Refineable};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone, Refineable, Debug)]
|
||||
@@ -292,7 +292,7 @@ impl CornerRadii {
|
||||
pub trait Styleable {
|
||||
type Style: Refineable + Default;
|
||||
|
||||
fn style_cascade(&mut self) -> &mut RefinementCascade<Self::Style>;
|
||||
fn style_cascade(&mut self) -> &mut Cascade<Self::Style>;
|
||||
fn declared_style(&mut self) -> &mut <Self::Style as Refineable>::Refinement;
|
||||
|
||||
fn computed_style(&mut self) -> Self::Style {
|
||||
|
||||
+109
-55
@@ -9,24 +9,20 @@ use refineable::Refineable;
|
||||
|
||||
use crate::{
|
||||
current_platform, image_cache::ImageCache, AssetSource, Context, DisplayId, Executor, LayoutId,
|
||||
MainThread, MainThreadOnly, Platform, PlatformDisplayLinker, RootView, SvgRenderer, Task,
|
||||
TextStyle, TextStyleRefinement, TextSystem, Window, WindowContext, WindowHandle, WindowId,
|
||||
MainThread, MainThreadOnly, Platform, RootView, SubscriberSet, SvgRenderer, Task, TextStyle,
|
||||
TextStyleRefinement, TextSystem, Window, WindowContext, WindowHandle, WindowId,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::{HashMap, VecDeque};
|
||||
use collections::{HashMap, HashSet, VecDeque};
|
||||
use futures::Future;
|
||||
use parking_lot::Mutex;
|
||||
use slotmap::SlotMap;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
any::{type_name, Any, TypeId},
|
||||
mem,
|
||||
sync::{Arc, Weak},
|
||||
};
|
||||
use util::{
|
||||
http::{self, HttpClient},
|
||||
ResultExt,
|
||||
};
|
||||
use util::http::{self, HttpClient};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct App(Arc<Mutex<AppContext>>);
|
||||
@@ -58,19 +54,21 @@ impl App {
|
||||
this: this.clone(),
|
||||
text_system: Arc::new(TextSystem::new(platform.text_system())),
|
||||
pending_updates: 0,
|
||||
display_linker: platform.display_linker(),
|
||||
next_frame_callbacks: Default::default(),
|
||||
platform: MainThreadOnly::new(platform, executor.clone()),
|
||||
executor,
|
||||
svg_renderer: SvgRenderer::new(asset_source),
|
||||
image_cache: ImageCache::new(http_client),
|
||||
text_style_stack: Vec::new(),
|
||||
state_stacks_by_type: HashMap::default(),
|
||||
global_stacks_by_type: HashMap::default(),
|
||||
unit_entity,
|
||||
entities,
|
||||
windows: SlotMap::with_key(),
|
||||
pending_notifications: Default::default(),
|
||||
pending_effects: Default::default(),
|
||||
observers: Default::default(),
|
||||
observers: SubscriberSet::new(),
|
||||
event_handlers: SubscriberSet::new(),
|
||||
release_handlers: SubscriberSet::new(),
|
||||
layout_id_buffer: Default::default(),
|
||||
})
|
||||
}))
|
||||
@@ -90,26 +88,30 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
type Handlers = SmallVec<[Arc<dyn Fn(&mut AppContext) -> bool + Send + Sync + 'static>; 2]>;
|
||||
type Handler = Box<dyn Fn(&mut AppContext) -> bool + Send + Sync + 'static>;
|
||||
type EventHandler = Box<dyn Fn(&dyn Any, &mut AppContext) -> bool + Send + Sync + 'static>;
|
||||
type ReleaseHandler = Box<dyn Fn(&mut dyn Any, &mut AppContext) + Send + Sync + 'static>;
|
||||
type FrameCallback = Box<dyn FnOnce(&mut WindowContext) + Send>;
|
||||
|
||||
pub struct AppContext {
|
||||
this: Weak<Mutex<AppContext>>,
|
||||
platform: MainThreadOnly<dyn Platform>,
|
||||
pub(crate) platform: MainThreadOnly<dyn Platform>,
|
||||
text_system: Arc<TextSystem>,
|
||||
pending_updates: usize,
|
||||
pub(crate) display_linker: Arc<dyn PlatformDisplayLinker>,
|
||||
pub(crate) next_frame_callbacks: HashMap<DisplayId, Vec<FrameCallback>>,
|
||||
pub(crate) executor: Executor,
|
||||
pub(crate) svg_renderer: SvgRenderer,
|
||||
pub(crate) image_cache: ImageCache,
|
||||
pub(crate) text_style_stack: Vec<TextStyleRefinement>,
|
||||
pub(crate) state_stacks_by_type: HashMap<TypeId, Vec<Box<dyn Any + Send + Sync>>>,
|
||||
pub(crate) global_stacks_by_type: HashMap<TypeId, Vec<Box<dyn Any + Send + Sync>>>,
|
||||
pub(crate) unit_entity: Handle<()>,
|
||||
pub(crate) entities: EntityMap,
|
||||
pub(crate) windows: SlotMap<WindowId, Option<Window>>,
|
||||
pub(crate) pending_effects: VecDeque<Effect>,
|
||||
pub(crate) observers: HashMap<EntityId, Handlers>,
|
||||
pub(crate) pending_notifications: HashSet<EntityId>,
|
||||
pending_effects: VecDeque<Effect>,
|
||||
pub(crate) observers: SubscriberSet<EntityId, Handler>,
|
||||
pub(crate) event_handlers: SubscriberSet<EntityId, EventHandler>,
|
||||
pub(crate) release_handlers: SubscriberSet<EntityId, ReleaseHandler>,
|
||||
pub(crate) layout_id_buffer: Vec<LayoutId>, // We recycle this memory across layout requests.
|
||||
}
|
||||
|
||||
@@ -148,10 +150,27 @@ impl AppContext {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn push_effect(&mut self, effect: Effect) {
|
||||
match &effect {
|
||||
Effect::Notify { emitter } => {
|
||||
if self.pending_notifications.insert(*emitter) {
|
||||
self.pending_effects.push_back(effect);
|
||||
}
|
||||
}
|
||||
Effect::Emit { .. } => self.pending_effects.push_back(effect),
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_effects(&mut self) {
|
||||
while let Some(effect) = self.pending_effects.pop_front() {
|
||||
match effect {
|
||||
Effect::Notify(entity_id) => self.apply_notify_effect(entity_id),
|
||||
loop {
|
||||
self.release_dropped_entities();
|
||||
if let Some(effect) = self.pending_effects.pop_front() {
|
||||
match effect {
|
||||
Effect::Notify { emitter } => self.apply_notify_effect(emitter),
|
||||
Effect::Emit { emitter, event } => self.apply_emit_effect(emitter, event),
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,22 +188,40 @@ impl AppContext {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for dirty_window_id in dirty_window_ids {
|
||||
self.update_window(dirty_window_id, |cx| cx.draw())
|
||||
.unwrap()
|
||||
.log_err();
|
||||
self.update_window(dirty_window_id, |cx| cx.draw()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_notify_effect(&mut self, updated_entity: EntityId) {
|
||||
if let Some(mut handlers) = self.observers.remove(&updated_entity) {
|
||||
handlers.retain(|handler| handler(self));
|
||||
if let Some(new_handlers) = self.observers.remove(&updated_entity) {
|
||||
handlers.extend(new_handlers);
|
||||
fn release_dropped_entities(&mut self) {
|
||||
loop {
|
||||
let dropped = self.entities.take_dropped();
|
||||
if dropped.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
for (entity_id, mut entity) in dropped {
|
||||
self.observers.remove(&entity_id);
|
||||
self.event_handlers.remove(&entity_id);
|
||||
for release_callback in self.release_handlers.remove(&entity_id) {
|
||||
release_callback(&mut entity, self);
|
||||
}
|
||||
}
|
||||
self.observers.insert(updated_entity, handlers);
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_notify_effect(&mut self, emitter: EntityId) {
|
||||
self.pending_notifications.remove(&emitter);
|
||||
self.observers
|
||||
.clone()
|
||||
.retain(&emitter, |handler| handler(self));
|
||||
}
|
||||
|
||||
fn apply_emit_effect(&mut self, emitter: EntityId, event: Box<dyn Any>) {
|
||||
self.event_handlers
|
||||
.clone()
|
||||
.retain(&emitter, |handler| handler(&event, self));
|
||||
}
|
||||
|
||||
pub fn to_async(&self) -> AsyncAppContext {
|
||||
AsyncAppContext(unsafe { mem::transmute(self.this.clone()) })
|
||||
}
|
||||
@@ -218,7 +255,7 @@ impl AppContext {
|
||||
f: impl FnOnce(&mut MainThread<AppContext>) -> F + Send + 'static,
|
||||
) -> Task<R>
|
||||
where
|
||||
F: Future<Output = R> + Send + 'static,
|
||||
F: Future<Output = R> + 'static,
|
||||
R: Send + 'static,
|
||||
{
|
||||
let this = self.this.upgrade().unwrap();
|
||||
@@ -254,24 +291,49 @@ impl AppContext {
|
||||
style
|
||||
}
|
||||
|
||||
pub fn state<S: 'static>(&self) -> &S {
|
||||
self.state_stacks_by_type
|
||||
.get(&TypeId::of::<S>())
|
||||
pub fn global<G: 'static>(&self) -> &G {
|
||||
self.global_stacks_by_type
|
||||
.get(&TypeId::of::<G>())
|
||||
.and_then(|stack| stack.last())
|
||||
.and_then(|any_state| any_state.downcast_ref::<S>())
|
||||
.ok_or_else(|| anyhow!("no state of type {} exists", type_name::<S>()))
|
||||
.and_then(|any_state| any_state.downcast_ref::<G>())
|
||||
.ok_or_else(|| anyhow!("no state of type {} exists", type_name::<G>()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn state_mut<S: 'static>(&mut self) -> &mut S {
|
||||
self.state_stacks_by_type
|
||||
.get_mut(&TypeId::of::<S>())
|
||||
pub fn global_mut<G: 'static>(&mut self) -> &mut G {
|
||||
self.global_stacks_by_type
|
||||
.get_mut(&TypeId::of::<G>())
|
||||
.and_then(|stack| stack.last_mut())
|
||||
.and_then(|any_state| any_state.downcast_mut::<S>())
|
||||
.ok_or_else(|| anyhow!("no state of type {} exists", type_name::<S>()))
|
||||
.and_then(|any_state| any_state.downcast_mut::<G>())
|
||||
.ok_or_else(|| anyhow!("no state of type {} exists", type_name::<G>()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn default_global<G: 'static + Default + Sync + Send>(&mut self) -> &mut G {
|
||||
let stack = self
|
||||
.global_stacks_by_type
|
||||
.entry(TypeId::of::<G>())
|
||||
.or_default();
|
||||
if stack.is_empty() {
|
||||
stack.push(Box::new(G::default()));
|
||||
}
|
||||
stack.last_mut().unwrap().downcast_mut::<G>().unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn push_global<T: Send + Sync + 'static>(&mut self, state: T) {
|
||||
self.global_stacks_by_type
|
||||
.entry(TypeId::of::<T>())
|
||||
.or_default()
|
||||
.push(Box::new(state));
|
||||
}
|
||||
|
||||
pub(crate) fn pop_global<T: 'static>(&mut self) {
|
||||
self.global_stacks_by_type
|
||||
.get_mut(&TypeId::of::<T>())
|
||||
.and_then(|stack| stack.pop())
|
||||
.expect("state stack underflow");
|
||||
}
|
||||
|
||||
pub(crate) fn push_text_style(&mut self, text_style: TextStyleRefinement) {
|
||||
self.text_style_stack.push(text_style);
|
||||
}
|
||||
@@ -279,20 +341,6 @@ impl AppContext {
|
||||
pub(crate) fn pop_text_style(&mut self) {
|
||||
self.text_style_stack.pop();
|
||||
}
|
||||
|
||||
pub(crate) fn push_state<T: Send + Sync + 'static>(&mut self, state: T) {
|
||||
self.state_stacks_by_type
|
||||
.entry(TypeId::of::<T>())
|
||||
.or_default()
|
||||
.push(Box::new(state));
|
||||
}
|
||||
|
||||
pub(crate) fn pop_state<T: 'static>(&mut self) {
|
||||
self.state_stacks_by_type
|
||||
.get_mut(&TypeId::of::<T>())
|
||||
.and_then(|stack| stack.pop())
|
||||
.expect("state stack underflow");
|
||||
}
|
||||
}
|
||||
|
||||
impl Context for AppContext {
|
||||
@@ -371,7 +419,13 @@ impl MainThread<AppContext> {
|
||||
}
|
||||
|
||||
pub(crate) enum Effect {
|
||||
Notify(EntityId),
|
||||
Notify {
|
||||
emitter: EntityId,
|
||||
},
|
||||
Emit {
|
||||
emitter: EntityId,
|
||||
event: Box<dyn Any + Send + Sync + 'static>,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use crate::Context;
|
||||
use anyhow::{anyhow, Result};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use parking_lot::{RwLock, RwLockUpgradableReadGuard};
|
||||
use slotmap::{SecondaryMap, SlotMap};
|
||||
use std::{
|
||||
any::Any,
|
||||
marker::PhantomData,
|
||||
mem,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering::SeqCst},
|
||||
Arc, Weak,
|
||||
@@ -14,29 +15,33 @@ use std::{
|
||||
|
||||
slotmap::new_key_type! { pub struct EntityId; }
|
||||
|
||||
pub(crate) struct EntityMap {
|
||||
ref_counts: Arc<RwLock<RefCounts>>,
|
||||
entities: Arc<Mutex<SecondaryMap<EntityId, Box<dyn Any + Send + Sync>>>>,
|
||||
pub(crate) struct EntityMap(Arc<RwLock<EntityMapState>>);
|
||||
|
||||
struct EntityMapState {
|
||||
ref_counts: SlotMap<EntityId, AtomicUsize>,
|
||||
entities: SecondaryMap<EntityId, Box<dyn Any + Send + Sync>>,
|
||||
dropped_entities: Vec<(EntityId, Box<dyn Any + Send + Sync>)>,
|
||||
}
|
||||
|
||||
impl EntityMap {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
ref_counts: Arc::new(RwLock::new(SlotMap::with_key())),
|
||||
entities: Arc::new(Mutex::new(SecondaryMap::new())),
|
||||
}
|
||||
Self(Arc::new(RwLock::new(EntityMapState {
|
||||
ref_counts: SlotMap::with_key(),
|
||||
entities: SecondaryMap::new(),
|
||||
dropped_entities: Vec::new(),
|
||||
})))
|
||||
}
|
||||
|
||||
/// Reserve a slot for an entity, which you can subsequently use with `insert`.
|
||||
pub fn reserve<T: 'static + Send + Sync>(&self) -> Slot<T> {
|
||||
let id = self.ref_counts.write().insert(1.into());
|
||||
Slot(Handle::new(id, Arc::downgrade(&self.ref_counts)))
|
||||
let id = self.0.write().ref_counts.insert(1.into());
|
||||
Slot(Handle::new(id, Arc::downgrade(&self.0)))
|
||||
}
|
||||
|
||||
/// Insert an entity into a slot obtained by calling `reserve`.
|
||||
pub fn insert<T: 'static + Any + Send + Sync>(&self, slot: Slot<T>, entity: T) -> Handle<T> {
|
||||
let handle = slot.0;
|
||||
self.entities.lock().insert(handle.id, Box::new(entity));
|
||||
self.0.write().entities.insert(handle.id, Box::new(entity));
|
||||
handle
|
||||
}
|
||||
|
||||
@@ -44,8 +49,9 @@ impl EntityMap {
|
||||
pub fn lease<T: 'static + Send + Sync>(&self, handle: &Handle<T>) -> Lease<T> {
|
||||
let id = handle.id;
|
||||
let entity = Some(
|
||||
self.entities
|
||||
.lock()
|
||||
self.0
|
||||
.write()
|
||||
.entities
|
||||
.remove(id)
|
||||
.expect("Circular entity lease. Is the entity already being updated?")
|
||||
.downcast::<T>()
|
||||
@@ -56,8 +62,9 @@ impl EntityMap {
|
||||
|
||||
/// Return an entity after moving it to the stack.
|
||||
pub fn end_lease<T: 'static + Send + Sync>(&mut self, mut lease: Lease<T>) {
|
||||
self.entities
|
||||
.lock()
|
||||
self.0
|
||||
.write()
|
||||
.entities
|
||||
.insert(lease.id, lease.entity.take().unwrap());
|
||||
}
|
||||
|
||||
@@ -65,9 +72,13 @@ impl EntityMap {
|
||||
WeakHandle {
|
||||
id,
|
||||
entity_type: PhantomData,
|
||||
ref_counts: Arc::downgrade(&self.ref_counts),
|
||||
entity_map: Arc::downgrade(&self.0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_dropped(&self) -> Vec<(EntityId, Box<dyn Any + Send + Sync>)> {
|
||||
mem::take(&mut self.0.write().dropped_entities)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Lease<T> {
|
||||
@@ -104,17 +115,15 @@ pub struct Slot<T: Send + Sync + 'static>(Handle<T>);
|
||||
pub struct Handle<T: Send + Sync> {
|
||||
pub(crate) id: EntityId,
|
||||
entity_type: PhantomData<T>,
|
||||
ref_counts: Weak<RwLock<RefCounts>>,
|
||||
entity_map: Weak<RwLock<EntityMapState>>,
|
||||
}
|
||||
|
||||
type RefCounts = SlotMap<EntityId, AtomicUsize>;
|
||||
|
||||
impl<T: 'static + Send + Sync> Handle<T> {
|
||||
pub fn new(id: EntityId, ref_counts: Weak<RwLock<RefCounts>>) -> Self {
|
||||
fn new(id: EntityId, entity_map: Weak<RwLock<EntityMapState>>) -> Self {
|
||||
Self {
|
||||
id,
|
||||
entity_type: PhantomData,
|
||||
ref_counts,
|
||||
entity_map,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +131,7 @@ impl<T: 'static + Send + Sync> Handle<T> {
|
||||
WeakHandle {
|
||||
id: self.id,
|
||||
entity_type: self.entity_type,
|
||||
ref_counts: self.ref_counts.clone(),
|
||||
entity_map: self.entity_map.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,40 +151,66 @@ impl<T: 'static + Send + Sync> Handle<T> {
|
||||
|
||||
impl<T: Send + Sync> Clone for Handle<T> {
|
||||
fn clone(&self) -> Self {
|
||||
if let Some(entity_map) = self.entity_map.upgrade() {
|
||||
let entity_map = entity_map.read();
|
||||
let count = entity_map
|
||||
.ref_counts
|
||||
.get(self.id)
|
||||
.expect("detected over-release of a handle");
|
||||
let prev_count = count.fetch_add(1, SeqCst);
|
||||
assert_ne!(prev_count, 0, "Detected over-release of a handle.");
|
||||
}
|
||||
|
||||
Self {
|
||||
id: self.id,
|
||||
entity_type: PhantomData,
|
||||
ref_counts: self.ref_counts.clone(),
|
||||
entity_map: self.entity_map.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Send + Sync> Drop for Handle<T> {
|
||||
fn drop(&mut self) {
|
||||
if let Some(_ref_counts) = self.ref_counts.upgrade() {
|
||||
// todo!()
|
||||
// if let Some(count) = ref_counts.read().get(self.id) {
|
||||
// let prev_count = count.fetch_sub(1, SeqCst);
|
||||
// assert_ne!(prev_count, 0, "Detected over-release of a handle.");
|
||||
// }
|
||||
if let Some(entity_map) = self.entity_map.upgrade() {
|
||||
let entity_map = entity_map.upgradable_read();
|
||||
let count = entity_map
|
||||
.ref_counts
|
||||
.get(self.id)
|
||||
.expect("Detected over-release of a handle.");
|
||||
let prev_count = count.fetch_sub(1, SeqCst);
|
||||
assert_ne!(prev_count, 0, "Detected over-release of a handle.");
|
||||
if prev_count == 1 {
|
||||
// We were the last reference to this entity, so we can remove it.
|
||||
let mut entity_map = RwLockUpgradableReadGuard::upgrade(entity_map);
|
||||
let entity = entity_map
|
||||
.entities
|
||||
.remove(self.id)
|
||||
.expect("entity was removed twice");
|
||||
entity_map.ref_counts.remove(self.id);
|
||||
entity_map.dropped_entities.push((self.id, entity));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WeakHandle<T> {
|
||||
pub(crate) id: EntityId,
|
||||
pub(crate) entity_type: PhantomData<T>,
|
||||
pub(crate) ref_counts: Weak<RwLock<RefCounts>>,
|
||||
entity_type: PhantomData<T>,
|
||||
entity_map: Weak<RwLock<EntityMapState>>,
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + 'static> WeakHandle<T> {
|
||||
pub fn upgrade(&self, _: &impl Context) -> Option<Handle<T>> {
|
||||
let ref_counts = self.ref_counts.upgrade()?;
|
||||
ref_counts.read().get(self.id).unwrap().fetch_add(1, SeqCst);
|
||||
let entity_map = &self.entity_map.upgrade()?;
|
||||
entity_map
|
||||
.read()
|
||||
.ref_counts
|
||||
.get(self.id)?
|
||||
.fetch_add(1, SeqCst);
|
||||
Some(Handle {
|
||||
id: self.id,
|
||||
entity_type: self.entity_type,
|
||||
ref_counts: self.ref_counts.clone(),
|
||||
entity_map: self.entity_map.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use crate::{AppContext, Context, Effect, EntityId, Handle, Reference, WeakHandle};
|
||||
use std::{marker::PhantomData, sync::Arc};
|
||||
use crate::{
|
||||
AppContext, Context, Effect, EntityId, EventEmitter, Handle, Reference, Subscription,
|
||||
WeakHandle,
|
||||
};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
pub struct ModelContext<'a, T> {
|
||||
app: Reference<'a, AppContext>,
|
||||
@@ -42,27 +45,89 @@ impl<'a, T: Send + Sync + 'static> ModelContext<'a, T> {
|
||||
&mut self,
|
||||
handle: &Handle<E>,
|
||||
on_notify: impl Fn(&mut T, Handle<E>, &mut ModelContext<'_, T>) + Send + Sync + 'static,
|
||||
) {
|
||||
) -> Subscription {
|
||||
let this = self.handle();
|
||||
let handle = handle.downgrade();
|
||||
self.app
|
||||
.observers
|
||||
.entry(handle.id)
|
||||
.or_default()
|
||||
.push(Arc::new(move |cx| {
|
||||
self.app.observers.insert(
|
||||
handle.id,
|
||||
Box::new(move |cx| {
|
||||
if let Some((this, handle)) = this.upgrade(cx).zip(handle.upgrade(cx)) {
|
||||
this.update(cx, |this, cx| on_notify(this, handle, cx));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}));
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn subscribe<E: EventEmitter + Send + Sync + 'static>(
|
||||
&mut self,
|
||||
handle: &Handle<E>,
|
||||
on_event: impl Fn(&mut T, Handle<E>, &E::Event, &mut ModelContext<'_, T>)
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
) -> Subscription {
|
||||
let this = self.handle();
|
||||
let handle = handle.downgrade();
|
||||
self.app.event_handlers.insert(
|
||||
handle.id,
|
||||
Box::new(move |event, cx| {
|
||||
let event = event.downcast_ref().expect("invalid event type");
|
||||
if let Some((this, handle)) = this.upgrade(cx).zip(handle.upgrade(cx)) {
|
||||
this.update(cx, |this, cx| on_event(this, handle, event, cx));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn on_release(
|
||||
&mut self,
|
||||
on_release: impl Fn(&mut T, &mut AppContext) + Send + Sync + 'static,
|
||||
) -> Subscription {
|
||||
self.app.release_handlers.insert(
|
||||
self.entity_id,
|
||||
Box::new(move |this, cx| {
|
||||
let this = this.downcast_mut().expect("invalid entity type");
|
||||
on_release(this, cx);
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn observe_release<E: Send + Sync + 'static>(
|
||||
&mut self,
|
||||
handle: &Handle<E>,
|
||||
on_release: impl Fn(&mut T, &mut E, &mut ModelContext<'_, T>) + Send + Sync + 'static,
|
||||
) -> Subscription {
|
||||
let this = self.handle();
|
||||
self.app.release_handlers.insert(
|
||||
handle.id,
|
||||
Box::new(move |entity, cx| {
|
||||
let entity = entity.downcast_mut().expect("invalid entity type");
|
||||
if let Some(this) = this.upgrade(cx) {
|
||||
this.update(cx, |this, cx| on_release(this, entity, cx));
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn notify(&mut self) {
|
||||
self.app
|
||||
.pending_effects
|
||||
.push_back(Effect::Notify(self.entity_id));
|
||||
self.app.push_effect(Effect::Notify {
|
||||
emitter: self.entity_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: EventEmitter + Send + Sync + 'static> ModelContext<'a, T> {
|
||||
pub fn emit(&mut self, event: T::Event) {
|
||||
self.app.push_effect(Effect::Emit {
|
||||
emitter: self.entity_id,
|
||||
event: Box::new(event),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+94
-57
@@ -1,48 +1,64 @@
|
||||
use crate::{Bounds, Identified, LayoutId, Pixels, Point, Result, ViewContext};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
BorrowWindow, Bounds, Clickable, ElementId, Group, LayoutId, MouseDownEvent, MouseUpEvent,
|
||||
Pixels, Point, SharedString, ViewContext,
|
||||
};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
pub(crate) use smallvec::SmallVec;
|
||||
use util::arc_cow::ArcCow;
|
||||
|
||||
pub trait Element: 'static {
|
||||
type State;
|
||||
type FrameState;
|
||||
pub trait Element: 'static + Send + Sync {
|
||||
type ViewState: 'static + Send + Sync;
|
||||
type ElementState: 'static + Send + Sync;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
fn element_id(&self) -> Option<ElementId>;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
state: &mut Self::State,
|
||||
cx: &mut ViewContext<Self::State>,
|
||||
) -> Result<(LayoutId, Self::FrameState)>;
|
||||
state: &mut Self::ViewState,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<Self::ViewState>,
|
||||
) -> (LayoutId, Self::ElementState);
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
state: &mut Self::State,
|
||||
frame_state: &mut Self::FrameState,
|
||||
cx: &mut ViewContext<Self::State>,
|
||||
) -> Result<()>;
|
||||
state: &mut Self::ViewState,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<Self::ViewState>,
|
||||
);
|
||||
|
||||
fn id(self, id: ElementId) -> Identified<Self>
|
||||
fn group(self, name: impl Into<SharedString>) -> Group<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Identified { element: self, id }
|
||||
Group::new(name.into(), self)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait StatefulElement: Element {
|
||||
pub trait IdentifiedElement: Element {
|
||||
fn element_id(&self) -> ElementId {
|
||||
Element::element_id(self).unwrap()
|
||||
}
|
||||
|
||||
fn on_click(
|
||||
self,
|
||||
listener: impl Fn(
|
||||
&mut Self::ViewState,
|
||||
(&MouseDownEvent, &MouseUpEvent),
|
||||
&mut ViewContext<Self::ViewState>,
|
||||
) + Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
) -> Clickable<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Clickable::new(self, Arc::from(listener))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct ElementId(ArcCow<'static, [u8]>);
|
||||
|
||||
#[derive(Deref, DerefMut, Default, Clone, Debug)]
|
||||
#[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub(crate) struct GlobalElementId(SmallVec<[ElementId; 8]>);
|
||||
|
||||
pub trait ParentElement {
|
||||
@@ -93,19 +109,14 @@ pub trait ParentElement {
|
||||
}
|
||||
}
|
||||
|
||||
trait ElementObject<S> {
|
||||
fn layout(&mut self, state: &mut S, cx: &mut ViewContext<S>) -> Result<LayoutId>;
|
||||
fn paint(
|
||||
&mut self,
|
||||
state: &mut S,
|
||||
offset: Option<Point<Pixels>>,
|
||||
cx: &mut ViewContext<S>,
|
||||
) -> Result<()>;
|
||||
trait ElementObject<S>: 'static + Send + Sync {
|
||||
fn layout(&mut self, state: &mut S, cx: &mut ViewContext<S>) -> LayoutId;
|
||||
fn paint(&mut self, state: &mut S, offset: Option<Point<Pixels>>, cx: &mut ViewContext<S>);
|
||||
}
|
||||
|
||||
struct RenderedElement<E: Element> {
|
||||
element: E,
|
||||
phase: ElementRenderPhase<E::FrameState>,
|
||||
phase: ElementRenderPhase<E::ElementState>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -114,15 +125,15 @@ enum ElementRenderPhase<S> {
|
||||
Rendered,
|
||||
LayoutRequested {
|
||||
layout_id: LayoutId,
|
||||
frame_state: S,
|
||||
frame_state: Option<S>,
|
||||
},
|
||||
Painted {
|
||||
bounds: Bounds<Pixels>,
|
||||
frame_state: S,
|
||||
frame_state: Option<S>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Internal struct that wraps an element to store Layout and FrameState after the element is rendered.
|
||||
/// Internal struct that wraps an element to store Layout and ElementState after the element is rendered.
|
||||
/// It's allocated as a trait object to erase the element type and wrapped in AnyElement<E::State> for
|
||||
/// improved usability.
|
||||
impl<E: Element> RenderedElement<E> {
|
||||
@@ -132,24 +143,58 @@ impl<E: Element> RenderedElement<E> {
|
||||
phase: ElementRenderPhase::Rendered,
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_with_element_state(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
view_state: &mut E::ViewState,
|
||||
frame_state: &mut Option<E::ElementState>,
|
||||
cx: &mut ViewContext<E::ViewState>,
|
||||
) {
|
||||
if let Some(id) = self.element.element_id() {
|
||||
cx.with_element_state(id, |element_state, cx| {
|
||||
let mut element_state = element_state.unwrap();
|
||||
self.element
|
||||
.paint(bounds, view_state, &mut element_state, cx);
|
||||
((), element_state)
|
||||
});
|
||||
} else {
|
||||
self.element
|
||||
.paint(bounds, view_state, frame_state.as_mut().unwrap(), cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Element> ElementObject<E::State> for RenderedElement<E> {
|
||||
fn layout(&mut self, state: &mut E::State, cx: &mut ViewContext<E::State>) -> Result<LayoutId> {
|
||||
let (layout_id, frame_state) = self.element.layout(state, cx)?;
|
||||
impl<E, S> ElementObject<E::ViewState> for RenderedElement<E>
|
||||
where
|
||||
E: Element<ElementState = S>,
|
||||
S: 'static + Send + Sync,
|
||||
{
|
||||
fn layout(&mut self, state: &mut E::ViewState, cx: &mut ViewContext<E::ViewState>) -> LayoutId {
|
||||
let (layout_id, frame_state) = if let Some(id) = self.element.element_id() {
|
||||
let layout_id = cx.with_element_state(id, |element_state, cx| {
|
||||
self.element.layout(state, element_state, cx)
|
||||
});
|
||||
(layout_id, None)
|
||||
} else {
|
||||
let (layout_id, frame_state) = self.element.layout(state, None, cx);
|
||||
(layout_id, Some(frame_state))
|
||||
};
|
||||
|
||||
self.phase = ElementRenderPhase::LayoutRequested {
|
||||
layout_id,
|
||||
frame_state,
|
||||
};
|
||||
Ok(layout_id)
|
||||
|
||||
layout_id
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
state: &mut E::State,
|
||||
view_state: &mut E::ViewState,
|
||||
offset: Option<Point<Pixels>>,
|
||||
cx: &mut ViewContext<E::State>,
|
||||
) -> Result<()> {
|
||||
cx: &mut ViewContext<E::ViewState>,
|
||||
) {
|
||||
self.phase = match std::mem::take(&mut self.phase) {
|
||||
ElementRenderPhase::Rendered => panic!("must call layout before paint"),
|
||||
|
||||
@@ -157,9 +202,9 @@ impl<E: Element> ElementObject<E::State> for RenderedElement<E> {
|
||||
layout_id,
|
||||
mut frame_state,
|
||||
} => {
|
||||
let mut bounds = cx.layout_bounds(layout_id)?.clone();
|
||||
let mut bounds = cx.layout_bounds(layout_id);
|
||||
offset.map(|offset| bounds.origin += offset);
|
||||
self.element.paint(bounds, state, &mut frame_state, cx)?;
|
||||
self.paint_with_element_state(bounds, view_state, &mut frame_state, cx);
|
||||
ElementRenderPhase::Painted {
|
||||
bounds,
|
||||
frame_state,
|
||||
@@ -170,32 +215,24 @@ impl<E: Element> ElementObject<E::State> for RenderedElement<E> {
|
||||
bounds,
|
||||
mut frame_state,
|
||||
} => {
|
||||
self.element
|
||||
.paint(bounds.clone(), state, &mut frame_state, cx)?;
|
||||
self.paint_with_element_state(bounds, view_state, &mut frame_state, cx);
|
||||
ElementRenderPhase::Painted {
|
||||
bounds,
|
||||
frame_state,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AnyElement<S>(Box<dyn ElementObject<S>>);
|
||||
|
||||
impl<S> AnyElement<S> {
|
||||
pub fn layout(&mut self, state: &mut S, cx: &mut ViewContext<S>) -> Result<LayoutId> {
|
||||
impl<S: 'static + Send + Sync> AnyElement<S> {
|
||||
pub fn layout(&mut self, state: &mut S, cx: &mut ViewContext<S>) -> LayoutId {
|
||||
self.0.layout(state, cx)
|
||||
}
|
||||
|
||||
pub fn paint(
|
||||
&mut self,
|
||||
state: &mut S,
|
||||
offset: Option<Point<Pixels>>,
|
||||
cx: &mut ViewContext<S>,
|
||||
) -> Result<()> {
|
||||
pub fn paint(&mut self, state: &mut S, offset: Option<Point<Pixels>>, cx: &mut ViewContext<S>) {
|
||||
self.0.paint(state, offset, cx)
|
||||
}
|
||||
}
|
||||
@@ -204,8 +241,8 @@ pub trait IntoAnyElement<S> {
|
||||
fn into_any(self) -> AnyElement<S>;
|
||||
}
|
||||
|
||||
impl<E: Element> IntoAnyElement<E::State> for E {
|
||||
fn into_any(self) -> AnyElement<E::State> {
|
||||
impl<E: Element> IntoAnyElement<E::ViewState> for E {
|
||||
fn into_any(self) -> AnyElement<E::ViewState> {
|
||||
AnyElement(Box::new(RenderedElement::new(self)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
mod clickable;
|
||||
mod div;
|
||||
mod group;
|
||||
mod hoverable;
|
||||
mod identified;
|
||||
mod img;
|
||||
@@ -6,7 +8,9 @@ mod pressable;
|
||||
mod svg;
|
||||
mod text;
|
||||
|
||||
pub use clickable::*;
|
||||
pub use div::*;
|
||||
pub use group::*;
|
||||
pub use hoverable::*;
|
||||
pub use identified::*;
|
||||
pub use img::*;
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
use crate::{
|
||||
AnyElement, Bounds, DispatchPhase, Element, IdentifiedElement, Interactive, MouseDownEvent,
|
||||
MouseEventListeners, MouseUpEvent, ParentElement, Pixels, Styled, ViewContext,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use refineable::Cascade;
|
||||
use smallvec::SmallVec;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub type ClickListener<S> =
|
||||
dyn Fn(&mut S, (&MouseDownEvent, &MouseUpEvent), &mut ViewContext<S>) + Send + Sync + 'static;
|
||||
|
||||
pub struct Clickable<E: Element> {
|
||||
child: E,
|
||||
listener: Arc<ClickListener<E::ViewState>>,
|
||||
}
|
||||
|
||||
pub struct ClickableState<S> {
|
||||
last_mouse_down: Arc<Mutex<Option<MouseDownEvent>>>,
|
||||
child_state: S,
|
||||
}
|
||||
|
||||
impl<E: Element> Clickable<E> {
|
||||
pub fn new(child: E, listener: Arc<ClickListener<E::ViewState>>) -> Self {
|
||||
Self { child, listener }
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Styled for Clickable<E>
|
||||
where
|
||||
E: Styled + IdentifiedElement,
|
||||
{
|
||||
type Style = E::Style;
|
||||
|
||||
fn style_cascade(&mut self) -> &mut Cascade<E::Style> {
|
||||
self.child.style_cascade()
|
||||
}
|
||||
|
||||
fn declared_style(&mut self) -> &mut <Self::Style as refineable::Refineable>::Refinement {
|
||||
self.child.declared_style()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> Interactive<S> for Clickable<E>
|
||||
where
|
||||
S: 'static + Send + Sync,
|
||||
E: IdentifiedElement + Interactive<S>,
|
||||
{
|
||||
fn listeners(&mut self) -> &mut MouseEventListeners<S> {
|
||||
self.child.listeners()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Element for Clickable<E>
|
||||
where
|
||||
E: IdentifiedElement,
|
||||
{
|
||||
type ViewState = E::ViewState;
|
||||
type ElementState = ClickableState<E::ElementState>;
|
||||
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
Some(IdentifiedElement::element_id(&self.child))
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
state: &mut Self::ViewState,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<Self::ViewState>,
|
||||
) -> (crate::LayoutId, Self::ElementState) {
|
||||
if let Some(element_state) = element_state {
|
||||
let (layout_id, child_state) =
|
||||
self.child
|
||||
.layout(state, Some(element_state.child_state), cx);
|
||||
|
||||
let element_state = ClickableState {
|
||||
last_mouse_down: element_state.last_mouse_down,
|
||||
child_state,
|
||||
};
|
||||
(layout_id, element_state)
|
||||
} else {
|
||||
let (layout_id, child_state) = self.child.layout(state, None, cx);
|
||||
let element_state = ClickableState {
|
||||
last_mouse_down: Default::default(),
|
||||
child_state,
|
||||
};
|
||||
(layout_id, element_state)
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
state: &mut Self::ViewState,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<Self::ViewState>,
|
||||
) {
|
||||
let last_mouse_down = element_state.last_mouse_down.clone();
|
||||
let is_some = last_mouse_down.lock().is_some();
|
||||
|
||||
if is_some {
|
||||
let listener = self.listener.clone();
|
||||
cx.on_mouse_event(move |view, up_event: &MouseUpEvent, phase, cx| {
|
||||
if phase == DispatchPhase::Capture && !bounds.contains_point(up_event.position) {
|
||||
*last_mouse_down.lock() = None;
|
||||
} else if phase == DispatchPhase::Bubble && bounds.contains_point(up_event.position)
|
||||
{
|
||||
if let Some(down_event) = last_mouse_down.lock().take() {
|
||||
listener(view, (&down_event, up_event), cx);
|
||||
} else {
|
||||
log::error!("No mouse down event found for click event");
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
cx.on_mouse_event(move |_, event: &MouseDownEvent, phase, _| {
|
||||
if phase == DispatchPhase::Bubble {
|
||||
if bounds.contains_point(event.position) {
|
||||
*last_mouse_down.lock() = Some(event.clone());
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
self.child
|
||||
.paint(bounds, state, &mut element_state.child_state, cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: IdentifiedElement + ParentElement> ParentElement for Clickable<E> {
|
||||
type State = E::State;
|
||||
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<Self::State>; 2]> {
|
||||
self.child.children_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> IdentifiedElement for Clickable<E> where E: IdentifiedElement + Styled {}
|
||||
@@ -1,77 +1,103 @@
|
||||
use crate::{
|
||||
AnyElement, Bounds, Element, Interactive, LayoutId, MouseEventListeners, Overflow,
|
||||
ParentElement, Pixels, Point, Refineable, RefinementCascade, Result, Style, Styled,
|
||||
ViewContext,
|
||||
AnyElement, BorrowWindow, Bounds, Cascade, Element, ElementId, IdentifiedElement, Interactive,
|
||||
LayoutId, MouseEventListeners, Overflow, ParentElement, Pixels, Point, Refineable, Style,
|
||||
Styled, ViewContext,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use smallvec::SmallVec;
|
||||
use std::sync::Arc;
|
||||
use util::ResultExt;
|
||||
use std::{marker::PhantomData, sync::Arc};
|
||||
|
||||
pub struct Div<S: 'static> {
|
||||
styles: RefinementCascade<Style>,
|
||||
pub enum HasId {}
|
||||
|
||||
pub struct Div<S: 'static, I = ()> {
|
||||
styles: Cascade<Style>,
|
||||
id: Option<ElementId>,
|
||||
listeners: MouseEventListeners<S>,
|
||||
children: SmallVec<[AnyElement<S>; 2]>,
|
||||
scroll_state: Option<ScrollState>,
|
||||
identified: PhantomData<I>,
|
||||
}
|
||||
|
||||
pub fn div<S>() -> Div<S> {
|
||||
Div {
|
||||
styles: Default::default(),
|
||||
id: None,
|
||||
listeners: Default::default(),
|
||||
children: Default::default(),
|
||||
scroll_state: None,
|
||||
identified: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: 'static + Send + Sync> Element for Div<S> {
|
||||
type State = S;
|
||||
type FrameState = Vec<LayoutId>;
|
||||
impl<S: 'static + Send + Sync, Marker: 'static + Send + Sync> Element for Div<S, Marker> {
|
||||
type ViewState = S;
|
||||
type ElementState = ();
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
view: &mut S,
|
||||
_: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<S>,
|
||||
) -> Result<(LayoutId, Self::FrameState)> {
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
let style = self.computed_style();
|
||||
let child_layout_ids = style.apply_text_style(cx, |cx| self.layout_children(view, cx))?;
|
||||
let layout_id = cx.request_layout(style.into(), child_layout_ids.clone())?;
|
||||
Ok((layout_id, child_layout_ids))
|
||||
let child_layout_ids = style.apply_text_style(cx, |cx| {
|
||||
self.with_element_id(cx, |this, cx| this.layout_children(view, cx))
|
||||
});
|
||||
let layout_id = cx.request_layout(style.into(), child_layout_ids.clone());
|
||||
(layout_id, ())
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
state: &mut S,
|
||||
child_layout_ids: &mut Self::FrameState,
|
||||
_: &mut (),
|
||||
cx: &mut ViewContext<S>,
|
||||
) -> Result<()> {
|
||||
) {
|
||||
let style = self.computed_style();
|
||||
let z_index = style.z_index.unwrap_or(0);
|
||||
cx.stack(z_index, |cx| style.paint(bounds, cx));
|
||||
|
||||
let overflow = &style.overflow;
|
||||
|
||||
style.apply_text_style(cx, |cx| {
|
||||
cx.stack(z_index + 1, |cx| {
|
||||
style.apply_overflow(bounds, cx, |cx| {
|
||||
self.listeners.paint(bounds, cx);
|
||||
self.paint_children(overflow, state, cx)
|
||||
self.with_element_id(cx, |this, cx| {
|
||||
this.listeners.paint(bounds, cx);
|
||||
this.paint_children(overflow, state, cx)
|
||||
});
|
||||
})
|
||||
})
|
||||
})?;
|
||||
self.handle_scroll(bounds, style.overflow.clone(), child_layout_ids, cx);
|
||||
|
||||
// todo!("enable inspector")
|
||||
// if cx.is_inspector_enabled() {
|
||||
// self.paint_inspector(parent_origin, layout, cx);
|
||||
// }
|
||||
//
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: 'static> Div<S> {
|
||||
impl<S> Div<S, ()>
|
||||
where
|
||||
S: 'static + Send + Sync,
|
||||
{
|
||||
pub fn id(self, id: impl Into<ElementId>) -> Div<S, HasId> {
|
||||
Div {
|
||||
styles: self.styles,
|
||||
id: Some(id.into()),
|
||||
listeners: self.listeners,
|
||||
children: self.children,
|
||||
scroll_state: self.scroll_state,
|
||||
identified: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, Marker> Div<S, Marker>
|
||||
where
|
||||
S: 'static + Send + Sync,
|
||||
Marker: 'static + Send + Sync,
|
||||
{
|
||||
pub fn z_index(mut self, z_index: u32) -> Self {
|
||||
self.declared_style().z_index = Some(z_index);
|
||||
self
|
||||
@@ -124,11 +150,11 @@ impl<S: 'static> Div<S> {
|
||||
offset
|
||||
}
|
||||
|
||||
fn layout_children(&mut self, view: &mut S, cx: &mut ViewContext<S>) -> Result<Vec<LayoutId>> {
|
||||
fn layout_children(&mut self, view: &mut S, cx: &mut ViewContext<S>) -> Vec<LayoutId> {
|
||||
self.children
|
||||
.iter_mut()
|
||||
.map(|child| child.layout(view, cx))
|
||||
.collect::<Result<Vec<LayoutId>>>()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn paint_children(
|
||||
@@ -136,117 +162,30 @@ impl<S: 'static> Div<S> {
|
||||
overflow: &Point<Overflow>,
|
||||
state: &mut S,
|
||||
cx: &mut ViewContext<S>,
|
||||
) -> Result<()> {
|
||||
) {
|
||||
let scroll_offset = self.scroll_offset(overflow);
|
||||
for child in &mut self.children {
|
||||
child.paint(state, Some(scroll_offset), cx)?;
|
||||
child.paint(state, Some(scroll_offset), cx);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_scroll(
|
||||
fn with_element_id<R>(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
overflow: Point<Overflow>,
|
||||
child_layout_ids: &[LayoutId],
|
||||
cx: &mut ViewContext<S>,
|
||||
) {
|
||||
if overflow.y == Overflow::Scroll || overflow.x == Overflow::Scroll {
|
||||
let mut scroll_max = Point::default();
|
||||
for child_layout_id in child_layout_ids {
|
||||
if let Some(child_bounds) = cx.layout_bounds(*child_layout_id).log_err() {
|
||||
scroll_max = scroll_max.max(&child_bounds.lower_right());
|
||||
}
|
||||
}
|
||||
scroll_max -= bounds.size;
|
||||
|
||||
// todo!("handle scroll")
|
||||
// let scroll_state = self.scroll_state.as_ref().unwrap().clone();
|
||||
// cx.on_event(order, move |_, event: &ScrollWheelEvent, cx| {
|
||||
// if bounds.contains_point(event.position) {
|
||||
// let scroll_delta = match event.delta {
|
||||
// ScrollDelta::Pixels(delta) => delta,
|
||||
// ScrollDelta::Lines(delta) => cx.text_style().font_size * delta,
|
||||
// };
|
||||
// if overflow.x == Overflow::Scroll {
|
||||
// scroll_state.set_x(
|
||||
// (scroll_state.x() - scroll_delta.x())
|
||||
// .max(px(0.))
|
||||
// .min(scroll_max.x),
|
||||
// );
|
||||
// }
|
||||
// if overflow.y == Overflow::Scroll {
|
||||
// scroll_state.set_y(
|
||||
// (scroll_state.y() - scroll_delta.y())
|
||||
// .max(px(0.))
|
||||
// .min(scroll_max.y),
|
||||
// );
|
||||
// }
|
||||
// cx.repaint();
|
||||
// } else {
|
||||
// cx.bubble_event();
|
||||
// }
|
||||
// })
|
||||
f: impl FnOnce(&mut Self, &mut ViewContext<S>) -> R,
|
||||
) -> R {
|
||||
if let Some(element_id) = self.element_id() {
|
||||
cx.with_element_id(element_id, |cx| f(self, cx))
|
||||
} else {
|
||||
f(self, cx)
|
||||
}
|
||||
}
|
||||
|
||||
// fn paint_inspector(
|
||||
// &self,
|
||||
// parent_origin: Point<Pixels>,
|
||||
// layout: &Layout,
|
||||
// cx: &mut ViewContext<V>,
|
||||
// ) {
|
||||
// let style = self.styles.merged();
|
||||
// let bounds = layout.bounds;
|
||||
|
||||
// let hovered = bounds.contains_point(cx.mouse_position());
|
||||
// if hovered {
|
||||
// let rem_size = cx.rem_size();
|
||||
// // cx.scene().push_quad(scene::Quad {
|
||||
// // bounds,
|
||||
// // background: Some(hsla(0., 0., 1., 0.05).into()),
|
||||
// // border: gpui::Border {
|
||||
// // color: hsla(0., 0., 1., 0.2).into(),
|
||||
// // top: 1.,
|
||||
// // right: 1.,
|
||||
// // bottom: 1.,
|
||||
// // left: 1.,
|
||||
// // },
|
||||
// // corner_radii: CornerRadii::default()
|
||||
// // .refined(&style.corner_radii)
|
||||
// // .to_gpui(bounds.size(), rem_size),
|
||||
// // })
|
||||
// }
|
||||
|
||||
// // let pressed = Cell::new(hovered && cx.is_mouse_down(MouseButton::Left));
|
||||
// // cx.on_event(layout.order, move |_, event: &MouseButtonEvent, _| {
|
||||
// // if bounds.contains_point(event.position) {
|
||||
// // if event.is_down {
|
||||
// // pressed.set(true);
|
||||
// // } else if pressed.get() {
|
||||
// // pressed.set(false);
|
||||
// // eprintln!("clicked div {:?} {:#?}", bounds, style);
|
||||
// // }
|
||||
// // }
|
||||
// // });
|
||||
|
||||
// // let hovered = Cell::new(hovered);
|
||||
// // cx.on_event(layout.order, move |_, event: &MouseMovedEvent, cx| {
|
||||
// // cx.bubble_event();
|
||||
// // let hovered_now = bounds.contains_point(event.position);
|
||||
// // if hovered.get() != hovered_now {
|
||||
// // hovered.set(hovered_now);
|
||||
// // cx.repaint();
|
||||
// // }
|
||||
// // });
|
||||
// }
|
||||
//
|
||||
}
|
||||
|
||||
impl<V> Styled for Div<V> {
|
||||
impl<V: 'static + Send + Sync, Marker: 'static + Send + Sync> Styled for Div<V, Marker> {
|
||||
type Style = Style;
|
||||
|
||||
fn style_cascade(&mut self) -> &mut RefinementCascade<Self::Style> {
|
||||
fn style_cascade(&mut self) -> &mut Cascade<Self::Style> {
|
||||
&mut self.styles
|
||||
}
|
||||
|
||||
@@ -255,16 +194,18 @@ impl<V> Styled for Div<V> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: Send + Sync + 'static> Interactive<V> for Div<V> {
|
||||
impl<V: Send + Sync + 'static> IdentifiedElement for Div<V, HasId> {}
|
||||
|
||||
impl<V: Send + Sync + 'static> Interactive<V> for Div<V, HasId> {
|
||||
fn listeners(&mut self) -> &mut MouseEventListeners<V> {
|
||||
&mut self.listeners
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: 'static> ParentElement for Div<S> {
|
||||
type State = S;
|
||||
impl<V: 'static, Marker: 'static + Send + Sync> ParentElement for Div<V, Marker> {
|
||||
type State = V;
|
||||
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<S>; 2]> {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
|
||||
&mut self.children
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
use crate::{
|
||||
AnyElement, AppContext, Bounds, Element, ElementId, IdentifiedElement, Interactive,
|
||||
MouseEventListeners, ParentElement, Pixels, SharedString, Styled, ViewContext,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use refineable::Cascade;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
#[derive(Default)]
|
||||
struct GroupBounds(HashMap<SharedString, SmallVec<[Bounds<Pixels>; 1]>>);
|
||||
|
||||
pub fn group_bounds(name: &SharedString, cx: &mut AppContext) -> Option<Bounds<Pixels>> {
|
||||
cx.default_global::<GroupBounds>()
|
||||
.0
|
||||
.get(name)
|
||||
.and_then(|bounds_stack| bounds_stack.last().cloned())
|
||||
}
|
||||
|
||||
pub struct Group<E> {
|
||||
name: SharedString,
|
||||
child: E,
|
||||
}
|
||||
|
||||
impl<E> Group<E> {
|
||||
pub fn new(name: SharedString, child: E) -> Self {
|
||||
Group { name, child }
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Element> Element for Group<E> {
|
||||
type ViewState = E::ViewState;
|
||||
type ElementState = E::ElementState;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
self.child.element_id()
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
state: &mut Self::ViewState,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<Self::ViewState>,
|
||||
) -> (crate::LayoutId, Self::ElementState) {
|
||||
self.child.layout(state, element_state, cx)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
state: &mut Self::ViewState,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<Self::ViewState>,
|
||||
) {
|
||||
cx.default_global::<GroupBounds>()
|
||||
.0
|
||||
.entry(self.name.clone())
|
||||
.or_default()
|
||||
.push(bounds);
|
||||
self.child.paint(bounds, state, element_state, cx);
|
||||
cx.default_global::<GroupBounds>()
|
||||
.0
|
||||
.get_mut(&self.name)
|
||||
.unwrap()
|
||||
.pop();
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: ParentElement> ParentElement for Group<E> {
|
||||
type State = E::State;
|
||||
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<Self::State>; 2]> {
|
||||
self.child.children_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> IdentifiedElement for Group<E> where E: IdentifiedElement {}
|
||||
|
||||
impl<E> Styled for Group<E>
|
||||
where
|
||||
E: Styled,
|
||||
{
|
||||
type Style = E::Style;
|
||||
|
||||
fn style_cascade(&mut self) -> &mut Cascade<E::Style> {
|
||||
self.child.style_cascade()
|
||||
}
|
||||
|
||||
fn declared_style(&mut self) -> &mut <Self::Style as refineable::Refineable>::Refinement {
|
||||
self.child.declared_style()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: 'static + Send + Sync, E: Interactive<S> + Styled> Interactive<S> for Group<E> {
|
||||
fn listeners(&mut self) -> &mut MouseEventListeners<S> {
|
||||
self.child.listeners()
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::{
|
||||
AnyElement, Bounds, DispatchPhase, Element, ElementId, Interactive, MouseEventListeners,
|
||||
MouseMoveEvent, ParentElement, Pixels, StatefulElement, Styled, ViewContext,
|
||||
group_bounds, AnyElement, Bounds, DispatchPhase, Element, ElementId, IdentifiedElement,
|
||||
Interactive, MouseEventListeners, MouseMoveEvent, ParentElement, Pixels, SharedString, Styled,
|
||||
ViewContext,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use refineable::{CascadeSlot, Refineable, RefinementCascade};
|
||||
use refineable::{Cascade, CascadeSlot, Refineable};
|
||||
use smallvec::SmallVec;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
@@ -11,6 +11,7 @@ use std::sync::{
|
||||
};
|
||||
|
||||
pub struct Hoverable<E: Styled> {
|
||||
group: Option<SharedString>,
|
||||
hovered: Arc<AtomicBool>,
|
||||
cascade_slot: CascadeSlot,
|
||||
hovered_style: <E::Style as Refineable>::Refinement,
|
||||
@@ -18,8 +19,9 @@ pub struct Hoverable<E: Styled> {
|
||||
}
|
||||
|
||||
impl<E: Styled> Hoverable<E> {
|
||||
pub fn new(mut child: E) -> Self {
|
||||
pub fn new(mut child: E, hover_group: Option<SharedString>) -> Self {
|
||||
Self {
|
||||
group: hover_group,
|
||||
hovered: Arc::new(AtomicBool::new(false)),
|
||||
cascade_slot: child.style_cascade().reserve(),
|
||||
hovered_style: Default::default(),
|
||||
@@ -34,7 +36,7 @@ where
|
||||
{
|
||||
type Style = E::Style;
|
||||
|
||||
fn style_cascade(&mut self) -> &mut RefinementCascade<E::Style> {
|
||||
fn style_cascade(&mut self) -> &mut Cascade<E::Style> {
|
||||
self.child.style_cascade()
|
||||
}
|
||||
|
||||
@@ -49,9 +51,14 @@ impl<S: 'static + Send + Sync, E: Interactive<S> + Styled> Interactive<S> for Ho
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Element + Styled> Element for Hoverable<E> {
|
||||
type State = E::State;
|
||||
type FrameState = E::FrameState;
|
||||
impl<E> Element for Hoverable<E>
|
||||
where
|
||||
E: Element + Styled,
|
||||
<E as Styled>::Style: 'static + Refineable + Send + Sync + Default,
|
||||
<<E as Styled>::Style as Refineable>::Refinement: 'static + Refineable + Send + Sync + Default,
|
||||
{
|
||||
type ViewState = E::ViewState;
|
||||
type ElementState = E::ElementState;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
self.child.element_id()
|
||||
@@ -59,36 +66,46 @@ impl<E: Element + Styled> Element for Hoverable<E> {
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
state: &mut Self::State,
|
||||
cx: &mut ViewContext<Self::State>,
|
||||
) -> Result<(crate::LayoutId, Self::FrameState)> {
|
||||
Ok(self.child.layout(state, cx)?)
|
||||
state: &mut Self::ViewState,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<Self::ViewState>,
|
||||
) -> (crate::LayoutId, Self::ElementState) {
|
||||
self.child.layout(state, element_state, cx)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
state: &mut Self::State,
|
||||
frame_state: &mut Self::FrameState,
|
||||
cx: &mut ViewContext<Self::State>,
|
||||
) -> Result<()> {
|
||||
let hovered = bounds.contains_point(cx.mouse_position());
|
||||
state: &mut Self::ViewState,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<Self::ViewState>,
|
||||
) {
|
||||
let target_bounds = self
|
||||
.group
|
||||
.as_ref()
|
||||
.and_then(|group| group_bounds(group, cx))
|
||||
.unwrap_or(bounds);
|
||||
|
||||
let hovered = target_bounds.contains_point(cx.mouse_position());
|
||||
|
||||
let slot = self.cascade_slot;
|
||||
let style = hovered.then_some(self.hovered_style.clone());
|
||||
self.style_cascade().set(slot, style);
|
||||
self.hovered.store(hovered, SeqCst);
|
||||
|
||||
let hovered = self.hovered.clone();
|
||||
cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
|
||||
if phase == DispatchPhase::Capture {
|
||||
if bounds.contains_point(event.position) != hovered.load(SeqCst) {
|
||||
cx.notify();
|
||||
cx.on_mouse_event({
|
||||
let hovered = self.hovered.clone();
|
||||
|
||||
move |_, event: &MouseMoveEvent, phase, cx| {
|
||||
if phase == DispatchPhase::Capture {
|
||||
if target_bounds.contains_point(event.position) != hovered.load(SeqCst) {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self.child.paint(bounds, state, frame_state, cx)?;
|
||||
Ok(())
|
||||
self.child.paint(bounds, state, element_state, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,4 +117,10 @@ impl<E: ParentElement + Styled> ParentElement for Hoverable<E> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: StatefulElement + Styled> StatefulElement for Hoverable<E> {}
|
||||
impl<E> IdentifiedElement for Hoverable<E>
|
||||
where
|
||||
E: IdentifiedElement + Styled,
|
||||
<E as Styled>::Style: 'static + Refineable + Send + Sync + Default,
|
||||
<<E as Styled>::Style as Refineable>::Refinement: 'static + Refineable + Send + Sync + Default,
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
use crate::{BorrowWindow, Bounds, Element, ElementId, LayoutId, StatefulElement, ViewContext};
|
||||
use anyhow::Result;
|
||||
use refineable::{Cascade, Refineable};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{
|
||||
AnyElement, BorrowWindow, Bounds, Element, ElementId, IdentifiedElement, LayoutId,
|
||||
ParentElement, Styled, ViewContext,
|
||||
};
|
||||
|
||||
pub struct Identified<E> {
|
||||
pub(crate) element: E,
|
||||
@@ -7,8 +12,8 @@ pub struct Identified<E> {
|
||||
}
|
||||
|
||||
impl<E: Element> Element for Identified<E> {
|
||||
type State = E::State;
|
||||
type FrameState = E::FrameState;
|
||||
type ViewState = E::ViewState;
|
||||
type ElementState = E::ElementState;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
Some(self.id.clone())
|
||||
@@ -16,23 +21,43 @@ impl<E: Element> Element for Identified<E> {
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
state: &mut Self::State,
|
||||
cx: &mut ViewContext<Self::State>,
|
||||
) -> Result<(LayoutId, Self::FrameState)> {
|
||||
self.element.layout(state, cx)
|
||||
state: &mut Self::ViewState,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<Self::ViewState>,
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
self.element.layout(state, element_state, cx)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<crate::Pixels>,
|
||||
state: &mut Self::State,
|
||||
frame_state: &mut Self::FrameState,
|
||||
cx: &mut ViewContext<Self::State>,
|
||||
) -> Result<()> {
|
||||
state: &mut Self::ViewState,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<Self::ViewState>,
|
||||
) {
|
||||
cx.with_element_id(self.id.clone(), |cx| {
|
||||
self.element.paint(bounds, state, frame_state, cx)
|
||||
self.element.paint(bounds, state, element_state, cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Element> StatefulElement for Identified<E> {}
|
||||
impl<E: Element> IdentifiedElement for Identified<E> {}
|
||||
|
||||
impl<E: Styled> Styled for Identified<E> {
|
||||
type Style = E::Style;
|
||||
|
||||
fn style_cascade(&mut self) -> &mut Cascade<Self::Style> {
|
||||
self.element.style_cascade()
|
||||
}
|
||||
fn declared_style(&mut self) -> &mut <Self::Style as Refineable>::Refinement {
|
||||
self.element.declared_style()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: ParentElement> ParentElement for Identified<E> {
|
||||
type State = E::State;
|
||||
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<Self::State>; 2]> {
|
||||
self.element.children_mut()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
use crate::{
|
||||
BorrowWindow, Bounds, Element, LayoutId, Pixels, Result, SharedString, Style, Styled,
|
||||
ViewContext,
|
||||
BorrowWindow, Bounds, Element, LayoutId, Pixels, SharedString, Style, Styled, ViewContext,
|
||||
};
|
||||
use futures::FutureExt;
|
||||
use refineable::RefinementCascade;
|
||||
use refineable::Cascade;
|
||||
use std::marker::PhantomData;
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct Img<S> {
|
||||
style: RefinementCascade<Style>,
|
||||
style: Cascade<Style>,
|
||||
uri: Option<SharedString>,
|
||||
grayscale: bool,
|
||||
state_type: PhantomData<S>,
|
||||
@@ -16,7 +15,7 @@ pub struct Img<S> {
|
||||
|
||||
pub fn img<S>() -> Img<S> {
|
||||
Img {
|
||||
style: RefinementCascade::default(),
|
||||
style: Cascade::default(),
|
||||
uri: None,
|
||||
grayscale: false,
|
||||
state_type: PhantomData,
|
||||
@@ -36,29 +35,34 @@ impl<S> Img<S> {
|
||||
}
|
||||
|
||||
impl<S: Send + Sync + 'static> Element for Img<S> {
|
||||
type State = S;
|
||||
type FrameState = ();
|
||||
type ViewState = S;
|
||||
type ElementState = ();
|
||||
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_: &mut Self::State,
|
||||
cx: &mut ViewContext<Self::State>,
|
||||
) -> anyhow::Result<(LayoutId, Self::FrameState)>
|
||||
_: &mut Self::ViewState,
|
||||
_: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<Self::ViewState>,
|
||||
) -> (LayoutId, Self::ElementState)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let style = self.computed_style();
|
||||
let layout_id = cx.request_layout(style, [])?;
|
||||
Ok((layout_id, ()))
|
||||
let layout_id = cx.request_layout(style, []);
|
||||
(layout_id, ())
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
_: &mut Self::State,
|
||||
_: &mut Self::FrameState,
|
||||
cx: &mut ViewContext<Self::State>,
|
||||
) -> Result<()> {
|
||||
_: &mut Self::ViewState,
|
||||
_: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<Self::ViewState>,
|
||||
) {
|
||||
let style = self.computed_style();
|
||||
|
||||
style.paint(bounds, cx);
|
||||
@@ -73,7 +77,8 @@ impl<S: Send + Sync + 'static> Element for Img<S> {
|
||||
let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
|
||||
cx.stack(1, |cx| {
|
||||
cx.paint_image(bounds, corner_radii, data, self.grayscale)
|
||||
})?;
|
||||
.log_err()
|
||||
});
|
||||
} else {
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
if image_future.await.log_err().is_some() {
|
||||
@@ -83,14 +88,13 @@ impl<S: Send + Sync + 'static> Element for Img<S> {
|
||||
.detach()
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Styled for Img<S> {
|
||||
type Style = Style;
|
||||
|
||||
fn style_cascade(&mut self) -> &mut RefinementCascade<Self::Style> {
|
||||
fn style_cascade(&mut self) -> &mut Cascade<Self::Style> {
|
||||
&mut self.style
|
||||
}
|
||||
|
||||
|
||||
@@ -1,211 +1,156 @@
|
||||
// use crate::{
|
||||
// AnyElement, Bounds, DispatchPhase, Element, Identified, Interactive, MouseDownEvent,
|
||||
// MouseEventListeners, MouseUpEvent, ParentElement, Pixels, Styled, ViewContext,
|
||||
// };
|
||||
// use anyhow::Result;
|
||||
// use refineable::{CascadeSlot, Refineable, RefinementCascade};
|
||||
// use smallvec::SmallVec;
|
||||
// use std::sync::{
|
||||
// atomic::{AtomicBool, Ordering::SeqCst},
|
||||
// Arc,
|
||||
// };
|
||||
use crate::{
|
||||
group_bounds, AnyElement, Bounds, DispatchPhase, Element, IdentifiedElement, Interactive,
|
||||
MouseDownEvent, MouseEventListeners, MouseUpEvent, ParentElement, Pixels, SharedString, Styled,
|
||||
ViewContext,
|
||||
};
|
||||
use refineable::{Cascade, CascadeSlot, Refineable};
|
||||
use smallvec::SmallVec;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
Arc,
|
||||
};
|
||||
|
||||
// pub struct Pressable<E: Styled> {
|
||||
// pressed: Arc<AtomicBool>,
|
||||
// cascade_slot: CascadeSlot,
|
||||
// pressed_style: <E::Style as Refineable>::Refinement,
|
||||
// child: E,
|
||||
// }
|
||||
pub struct Pressable<E: Styled> {
|
||||
group: Option<SharedString>,
|
||||
cascade_slot: CascadeSlot,
|
||||
pressed_style: <E::Style as Refineable>::Refinement,
|
||||
child: E,
|
||||
}
|
||||
|
||||
// impl<E: Identified + Styled> Pressable<E> {
|
||||
// pub fn new(mut child: E) -> Self {
|
||||
// Self {
|
||||
// pressed: Arc::new(AtomicBool::new(false)),
|
||||
// cascade_slot: child.style_cascade().reserve(),
|
||||
// pressed_style: Default::default(),
|
||||
// child,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
pub struct PressableState<S> {
|
||||
pressed: Arc<AtomicBool>,
|
||||
child_state: S,
|
||||
}
|
||||
|
||||
// impl<E> Styled for Pressable<E>
|
||||
// where
|
||||
// E: Styled,
|
||||
// {
|
||||
// type Style = E::Style;
|
||||
impl<E: Styled> Pressable<E> {
|
||||
pub fn new(mut child: E, group: Option<SharedString>) -> Self {
|
||||
Self {
|
||||
group,
|
||||
cascade_slot: child.style_cascade().reserve(),
|
||||
pressed_style: Default::default(),
|
||||
child,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fn style_cascade(&mut self) -> &mut RefinementCascade<E::Style> {
|
||||
// self.child.style_cascade()
|
||||
// }
|
||||
impl<E> Styled for Pressable<E>
|
||||
where
|
||||
E: Styled,
|
||||
{
|
||||
type Style = E::Style;
|
||||
|
||||
// fn declared_style(&mut self) -> &mut <Self::Style as refineable::Refineable>::Refinement {
|
||||
// &mut self.pressed_style
|
||||
// }
|
||||
// }
|
||||
fn style_cascade(&mut self) -> &mut Cascade<E::Style> {
|
||||
self.child.style_cascade()
|
||||
}
|
||||
|
||||
// impl<S: 'static + Send + Sync, E: Interactive<S> + Styled> Interactive<S> for Pressable<E> {
|
||||
// fn listeners(&mut self) -> &mut MouseEventListeners<S> {
|
||||
// self.child.listeners()
|
||||
// }
|
||||
// }
|
||||
fn declared_style(&mut self) -> &mut <Self::Style as refineable::Refineable>::Refinement {
|
||||
&mut self.pressed_style
|
||||
}
|
||||
}
|
||||
|
||||
// impl<E: Element + Identified + Styled> Element for Pressable<E> {
|
||||
// type State = E::State;
|
||||
// type FrameState = E::FrameState;
|
||||
impl<S: 'static + Send + Sync, E: Interactive<S> + Styled> Interactive<S> for Pressable<E> {
|
||||
fn listeners(&mut self) -> &mut MouseEventListeners<S> {
|
||||
self.child.listeners()
|
||||
}
|
||||
}
|
||||
|
||||
// fn layout(
|
||||
// &mut self,
|
||||
// state: &mut Self::State,
|
||||
// cx: &mut ViewContext<Self::State>,
|
||||
// ) -> Result<(crate::LayoutId, Self::FrameState)> {
|
||||
// Ok(self.child.layout(state, cx)?)
|
||||
// }
|
||||
impl<E> Element for Pressable<E>
|
||||
where
|
||||
E: Styled + IdentifiedElement,
|
||||
<E as Styled>::Style: 'static + Refineable + Send + Sync + Default,
|
||||
<<E as Styled>::Style as Refineable>::Refinement: 'static + Refineable + Send + Sync + Default,
|
||||
{
|
||||
type ViewState = E::ViewState;
|
||||
type ElementState = PressableState<E::ElementState>;
|
||||
|
||||
// fn paint(
|
||||
// &mut self,
|
||||
// bounds: Bounds<Pixels>,
|
||||
// state: &mut Self::State,
|
||||
// frame_state: &mut Self::FrameState,
|
||||
// cx: &mut ViewContext<Self::State>,
|
||||
// ) -> Result<()> {
|
||||
// let pressed = bounds.contains_point(cx.mouse_position());
|
||||
// let slot = self.cascade_slot;
|
||||
// let style = pressed.then_some(self.pressed_style.clone());
|
||||
// self.style_cascade().set(slot, style);
|
||||
// self.pressed.store(pressed, SeqCst);
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
Some(IdentifiedElement::element_id(&self.child))
|
||||
}
|
||||
|
||||
// let hovered = self.pressed.clone();
|
||||
// cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
|
||||
// if phase == DispatchPhase::Capture {
|
||||
// if bounds.contains_point(event.position) != hovered.load(SeqCst) {
|
||||
// cx.notify();
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
|
||||
// if phase == DispatchPhase::Capture {
|
||||
// if bounds.contains_point(event.position) != hovered.load(SeqCst) {
|
||||
// cx.notify();
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
fn layout(
|
||||
&mut self,
|
||||
state: &mut Self::ViewState,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<Self::ViewState>,
|
||||
) -> (crate::LayoutId, Self::ElementState) {
|
||||
if let Some(element_state) = element_state {
|
||||
let (id, child_state) = self
|
||||
.child
|
||||
.layout(state, Some(element_state.child_state), cx);
|
||||
let element_state = PressableState {
|
||||
pressed: element_state.pressed,
|
||||
child_state,
|
||||
};
|
||||
(id, element_state)
|
||||
} else {
|
||||
let (id, child_state) = self.child.layout(state, None, cx);
|
||||
let element_state = PressableState {
|
||||
pressed: Default::default(),
|
||||
child_state,
|
||||
};
|
||||
(id, element_state)
|
||||
}
|
||||
}
|
||||
|
||||
// self.child.paint(bounds, state, frame_state, cx)?;
|
||||
// Ok(())
|
||||
// }
|
||||
// }
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
state: &mut Self::ViewState,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<Self::ViewState>,
|
||||
) {
|
||||
let target_bounds = self
|
||||
.group
|
||||
.as_ref()
|
||||
.and_then(|group| group_bounds(group, cx))
|
||||
.unwrap_or(bounds);
|
||||
|
||||
// impl<E: ParentElement + Styled> ParentElement for Pressable<E> {
|
||||
// type State = E::State;
|
||||
let style = element_state
|
||||
.pressed
|
||||
.load(SeqCst)
|
||||
.then_some(self.pressed_style.clone());
|
||||
let slot = self.cascade_slot;
|
||||
self.style_cascade().set(slot, style);
|
||||
|
||||
// fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<Self::State>; 2]> {
|
||||
// self.child.children_mut()
|
||||
// }
|
||||
// }
|
||||
let pressed = element_state.pressed.clone();
|
||||
cx.on_mouse_event(move |_, event: &MouseDownEvent, phase, cx| {
|
||||
if phase == DispatchPhase::Bubble {
|
||||
if target_bounds.contains_point(event.position) {
|
||||
pressed.store(true, SeqCst);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
});
|
||||
let pressed = element_state.pressed.clone();
|
||||
cx.on_mouse_event(move |_, _: &MouseUpEvent, phase, cx| {
|
||||
if phase == DispatchPhase::Capture {
|
||||
if pressed.load(SeqCst) {
|
||||
pressed.store(false, SeqCst);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// // use crate::{
|
||||
// // element::{AnyElement, Element, IntoElement, Layout, ParentElement},
|
||||
// // interactive::{InteractionHandlers, Interactive},
|
||||
// // style::{Style, StyleHelpers, Styleable},
|
||||
// // ViewContext,
|
||||
// // };
|
||||
// // use anyhow::Result;
|
||||
// // use gpui::{geometry::vector::Vector2F, platform::MouseButtonEvent, LayoutId};
|
||||
// // use refineable::{CascadeSlot, Refineable, RefinementCascade};
|
||||
// // use smallvec::SmallVec;
|
||||
// // use std::{cell::Cell, rc::Rc};
|
||||
self.child
|
||||
.paint(bounds, state, &mut element_state.child_state, cx);
|
||||
}
|
||||
}
|
||||
|
||||
// // pub struct Pressable<E: Styleable> {
|
||||
// // pressed: Rc<Cell<bool>>,
|
||||
// // pressed_style: <E::Style as Refineable>::Refinement,
|
||||
// // cascade_slot: CascadeSlot,
|
||||
// // child: E,
|
||||
// // }
|
||||
impl<E> ParentElement for Pressable<E>
|
||||
where
|
||||
E: ParentElement + IdentifiedElement + Styled,
|
||||
{
|
||||
type State = E::State;
|
||||
|
||||
// // pub fn pressable<E: Styleable>(mut child: E) -> Pressable<E> {
|
||||
// // Pressable {
|
||||
// // pressed: Rc::new(Cell::new(false)),
|
||||
// // pressed_style: Default::default(),
|
||||
// // cascade_slot: child.style_cascade().reserve(),
|
||||
// // child,
|
||||
// // }
|
||||
// // }
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<Self::State>; 2]> {
|
||||
self.child.children_mut()
|
||||
}
|
||||
}
|
||||
|
||||
// // impl<E: Styleable> Styleable for Pressable<E> {
|
||||
// // type Style = E::Style;
|
||||
|
||||
// // fn declared_style(&mut self) -> &mut <Self::Style as Refineable>::Refinement {
|
||||
// // &mut self.pressed_style
|
||||
// // }
|
||||
|
||||
// // fn style_cascade(&mut self) -> &mut RefinementCascade<E::Style> {
|
||||
// // self.child.style_cascade()
|
||||
// // }
|
||||
// // }
|
||||
|
||||
// // impl<V: 'static, E: Element<V> + Styleable> Element<V> for Pressable<E> {
|
||||
// // type PaintState = E::PaintState;
|
||||
|
||||
// // fn layout(
|
||||
// // &mut self,
|
||||
// // view: &mut V,
|
||||
// // cx: &mut ViewContext<V>,
|
||||
// // ) -> Result<(LayoutId, Self::PaintState)>
|
||||
// // where
|
||||
// // Self: Sized,
|
||||
// // {
|
||||
// // self.child.layout(view, cx)
|
||||
// // }
|
||||
|
||||
// // fn paint(
|
||||
// // &mut self,
|
||||
// // view: &mut V,
|
||||
// // parent_origin: Vector2F,
|
||||
// // layout: &Layout,
|
||||
// // paint_state: &mut Self::PaintState,
|
||||
// // cx: &mut ViewContext<V>,
|
||||
// // ) where
|
||||
// // Self: Sized,
|
||||
// // {
|
||||
// // let slot = self.cascade_slot;
|
||||
// // let style = self.pressed.get().then_some(self.pressed_style.clone());
|
||||
// // self.style_cascade().set(slot, style);
|
||||
|
||||
// // let pressed = self.pressed.clone();
|
||||
// // let bounds = layout.bounds + parent_origin;
|
||||
// // cx.on_event(layout.order, move |_view, event: &MouseButtonEvent, cx| {
|
||||
// // if event.is_down {
|
||||
// // if bounds.contains_point(event.position) {
|
||||
// // pressed.set(true);
|
||||
// // cx.repaint();
|
||||
// // }
|
||||
// // } else if pressed.get() {
|
||||
// // pressed.set(false);
|
||||
// // cx.repaint();
|
||||
// // }
|
||||
// // });
|
||||
|
||||
// // self.child
|
||||
// // .paint(view, parent_origin, layout, paint_state, cx);
|
||||
// // }
|
||||
// // }
|
||||
|
||||
// // impl<V: 'static, E: Interactive<V> + Styleable> Interactive<V> for Pressable<E> {
|
||||
// // fn interaction_handlers(&mut self) -> &mut InteractionHandlers<V> {
|
||||
// // self.child.interaction_handlers()
|
||||
// // }
|
||||
// // }
|
||||
|
||||
// // impl<V: 'static, E: ParentElement<V> + Styleable> ParentElement<V> for Pressable<E> {
|
||||
// // fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
|
||||
// // self.child.children_mut()
|
||||
// // }
|
||||
// // }
|
||||
|
||||
// // impl<V: 'static, E: Element<V> + Styleable> IntoElement<V> for Pressable<E> {
|
||||
// // type Element = Self;
|
||||
|
||||
// // fn into_element(self) -> Self::Element {
|
||||
// // self
|
||||
// // }
|
||||
// // }
|
||||
impl<E> IdentifiedElement for Pressable<E>
|
||||
where
|
||||
E: IdentifiedElement + Styled,
|
||||
<E as Styled>::Style: 'static + Refineable + Send + Sync + Default,
|
||||
<<E as Styled>::Style as Refineable>::Refinement: 'static + Refineable + Send + Sync + Default,
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
use crate::{Bounds, Element, LayoutId, Pixels, Result, SharedString, Style, Styled};
|
||||
use refineable::RefinementCascade;
|
||||
use crate::{Bounds, Element, LayoutId, Pixels, SharedString, Style, Styled};
|
||||
use refineable::Cascade;
|
||||
use std::marker::PhantomData;
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct Svg<S> {
|
||||
path: Option<SharedString>,
|
||||
style: RefinementCascade<Style>,
|
||||
style: Cascade<Style>,
|
||||
state_type: PhantomData<S>,
|
||||
}
|
||||
|
||||
pub fn svg<S>() -> Svg<S> {
|
||||
Svg {
|
||||
path: None,
|
||||
style: RefinementCascade::<Style>::default(),
|
||||
style: Cascade::<Style>::default(),
|
||||
state_type: PhantomData,
|
||||
}
|
||||
}
|
||||
@@ -23,44 +24,47 @@ impl<S> Svg<S> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: 'static> Element for Svg<S> {
|
||||
type State = S;
|
||||
type FrameState = ();
|
||||
impl<S: 'static + Send + Sync> Element for Svg<S> {
|
||||
type ViewState = S;
|
||||
type ElementState = ();
|
||||
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_: &mut S,
|
||||
_: Option<Self::ElementState>,
|
||||
cx: &mut crate::ViewContext<S>,
|
||||
) -> anyhow::Result<(LayoutId, Self::FrameState)>
|
||||
) -> (LayoutId, Self::ElementState)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let style = self.computed_style();
|
||||
Ok((cx.request_layout(style, [])?, ()))
|
||||
(cx.request_layout(style, []), ())
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
_: &mut Self::State,
|
||||
_: &mut Self::FrameState,
|
||||
_: &mut Self::ViewState,
|
||||
_: &mut Self::ElementState,
|
||||
cx: &mut crate::ViewContext<S>,
|
||||
) -> Result<()>
|
||||
where
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
let fill_color = self.computed_style().fill.and_then(|fill| fill.color());
|
||||
if let Some((path, fill_color)) = self.path.as_ref().zip(fill_color) {
|
||||
cx.paint_svg(bounds, path.clone(), fill_color)?;
|
||||
cx.paint_svg(bounds, path.clone(), fill_color).log_err();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Styled for Svg<S> {
|
||||
impl<S: 'static + Send + Sync> Styled for Svg<S> {
|
||||
type Style = Style;
|
||||
|
||||
fn style_cascade(&mut self) -> &mut refineable::RefinementCascade<Self::Style> {
|
||||
fn style_cascade(&mut self) -> &mut refineable::Cascade<Self::Style> {
|
||||
&mut self.style
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use crate::{
|
||||
AnyElement, Bounds, Element, IntoAnyElement, LayoutId, Line, Pixels, Result, Size, ViewContext,
|
||||
AnyElement, Bounds, Element, IntoAnyElement, LayoutId, Line, Pixels, Size, ViewContext,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use std::{marker::PhantomData, sync::Arc};
|
||||
use util::{arc_cow::ArcCow, ResultExt};
|
||||
|
||||
impl<S: 'static> IntoAnyElement<S> for ArcCow<'static, str> {
|
||||
impl<S: 'static + Send + Sync> IntoAnyElement<S> for ArcCow<'static, str> {
|
||||
fn into_any(self) -> AnyElement<S> {
|
||||
Text {
|
||||
text: self,
|
||||
@@ -15,7 +15,7 @@ impl<S: 'static> IntoAnyElement<S> for ArcCow<'static, str> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> IntoAnyElement<V> for &'static str {
|
||||
impl<V: 'static + Send + Sync> IntoAnyElement<V> for &'static str {
|
||||
fn into_any(self) -> AnyElement<V> {
|
||||
Text {
|
||||
text: ArcCow::from(self),
|
||||
@@ -42,15 +42,20 @@ pub struct Text<S> {
|
||||
state_type: PhantomData<S>,
|
||||
}
|
||||
|
||||
impl<S: 'static> Element for Text<S> {
|
||||
type State = S;
|
||||
type FrameState = Arc<Mutex<Option<TextFrameState>>>;
|
||||
impl<S: 'static + Send + Sync> Element for Text<S> {
|
||||
type ViewState = S;
|
||||
type ElementState = Arc<Mutex<Option<TextElementState>>>;
|
||||
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_view: &mut S,
|
||||
_element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<S>,
|
||||
) -> Result<(LayoutId, Self::FrameState)> {
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
let text_system = cx.text_system().clone();
|
||||
let text_style = cx.text_style();
|
||||
let font_size = text_style.font_size * cx.rem_size();
|
||||
@@ -58,11 +63,11 @@ impl<S: 'static> Element for Text<S> {
|
||||
.line_height
|
||||
.to_pixels(font_size.into(), cx.rem_size());
|
||||
let text = self.text.clone();
|
||||
let frame_state = Arc::new(Mutex::new(None));
|
||||
let element_state = Arc::new(Mutex::new(None));
|
||||
|
||||
let rem_size = cx.rem_size();
|
||||
let layout_id = cx.request_measured_layout(Default::default(), rem_size, {
|
||||
let frame_state = frame_state.clone();
|
||||
let element_state = element_state.clone();
|
||||
move |_, _| {
|
||||
let Some(line_layout) = text_system
|
||||
.layout_line(
|
||||
@@ -80,7 +85,7 @@ impl<S: 'static> Element for Text<S> {
|
||||
height: line_height,
|
||||
};
|
||||
|
||||
frame_state.lock().replace(TextFrameState {
|
||||
element_state.lock().replace(TextElementState {
|
||||
line: Arc::new(line_layout),
|
||||
line_height,
|
||||
});
|
||||
@@ -89,35 +94,32 @@ impl<S: 'static> Element for Text<S> {
|
||||
}
|
||||
});
|
||||
|
||||
Ok((layout_id?, frame_state))
|
||||
(layout_id, element_state)
|
||||
}
|
||||
|
||||
fn paint<'a>(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
_: &mut Self::State,
|
||||
frame_state: &mut Self::FrameState,
|
||||
_: &mut Self::ViewState,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<S>,
|
||||
) -> Result<()> {
|
||||
) {
|
||||
let line;
|
||||
let line_height;
|
||||
{
|
||||
let frame_state = frame_state.lock();
|
||||
let frame_state = frame_state
|
||||
let element_state = element_state.lock();
|
||||
let element_state = element_state
|
||||
.as_ref()
|
||||
.expect("measurement has not been performed");
|
||||
line = frame_state.line.clone();
|
||||
line_height = frame_state.line_height;
|
||||
line = element_state.line.clone();
|
||||
line_height = element_state.line_height;
|
||||
}
|
||||
|
||||
// todo!("We haven't added visible bounds to the new element system yet, so this is a placeholder.");
|
||||
line.paint(bounds, bounds, line_height, cx)?;
|
||||
|
||||
Ok(())
|
||||
line.paint(bounds, bounds, line_height, cx).log_err();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TextFrameState {
|
||||
pub struct TextElementState {
|
||||
line: Arc<Line>,
|
||||
line_height: Pixels,
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ pub struct Executor {
|
||||
dispatcher: Arc<dyn PlatformDispatcher>,
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub enum Task<T> {
|
||||
Ready(Option<T>),
|
||||
Spawned(async_task::Task<T>),
|
||||
@@ -76,13 +77,22 @@ impl Executor {
|
||||
/// closure returns a future which will be run to completion on the main thread.
|
||||
pub fn spawn_on_main<F, R>(&self, func: impl FnOnce() -> F + Send + 'static) -> Task<R>
|
||||
where
|
||||
F: Future<Output = R> + Send + 'static,
|
||||
F: Future<Output = R> + 'static,
|
||||
R: Send + 'static,
|
||||
{
|
||||
let dispatcher = self.dispatcher.clone();
|
||||
let (runnable, task) = async_task::spawn(async move { func().await }, move |runnable| {
|
||||
dispatcher.dispatch_on_main_thread(runnable)
|
||||
});
|
||||
let (runnable, task) = async_task::spawn(
|
||||
{
|
||||
let this = self.clone();
|
||||
async move {
|
||||
let task = this.spawn_on_main_local(func());
|
||||
task.await
|
||||
}
|
||||
},
|
||||
{
|
||||
let dispatcher = self.dispatcher.clone();
|
||||
move |runnable| dispatcher.dispatch_on_main_thread(runnable)
|
||||
},
|
||||
);
|
||||
runnable.schedule();
|
||||
Task::Spawned(task)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ mod scene;
|
||||
mod style;
|
||||
mod style_helpers;
|
||||
mod styled;
|
||||
mod subscription;
|
||||
mod svg_renderer;
|
||||
mod taffy;
|
||||
mod text_system;
|
||||
@@ -42,6 +43,7 @@ pub use smol::Timer;
|
||||
pub use style::*;
|
||||
pub use style_helpers::*;
|
||||
pub use styled::*;
|
||||
pub use subscription::*;
|
||||
pub use svg_renderer::*;
|
||||
pub use taffy::{AvailableSpace, LayoutId};
|
||||
pub use text_system::*;
|
||||
@@ -50,12 +52,15 @@ pub use view::*;
|
||||
pub use window::*;
|
||||
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
mem,
|
||||
ops::{Deref, DerefMut},
|
||||
sync::Arc,
|
||||
};
|
||||
use taffy::TaffyLayoutEngine;
|
||||
|
||||
type AnyBox = Box<dyn Any + Send + Sync + 'static>;
|
||||
|
||||
pub trait Context {
|
||||
type EntityContext<'a, 'w, T: 'static + Send + Sync>;
|
||||
type Result<T>;
|
||||
@@ -72,6 +77,12 @@ pub trait Context {
|
||||
) -> Self::Result<R>;
|
||||
}
|
||||
|
||||
pub enum GlobalKey {
|
||||
Numeric(usize),
|
||||
View(EntityId),
|
||||
Type(TypeId),
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
pub struct MainThread<T>(T);
|
||||
|
||||
@@ -138,17 +149,21 @@ pub trait BorrowAppContext {
|
||||
result
|
||||
}
|
||||
|
||||
fn with_state<T: Send + Sync + 'static, F, R>(&mut self, state: T, f: F) -> R
|
||||
fn with_global<T: Send + Sync + 'static, F, R>(&mut self, state: T, f: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut Self) -> R,
|
||||
{
|
||||
self.app_mut().push_state(state);
|
||||
self.app_mut().push_global(state);
|
||||
let result = f(self);
|
||||
self.app_mut().pop_state::<T>();
|
||||
self.app_mut().pop_global::<T>();
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub trait EventEmitter {
|
||||
type Event: Any + Send + Sync + 'static;
|
||||
}
|
||||
|
||||
pub trait Flatten<T> {
|
||||
fn flatten(self) -> Result<T>;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ use crate::{
|
||||
Bounds, DispatchPhase, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
|
||||
ScrollWheelEvent, ViewContext,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use smallvec::SmallVec;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -93,37 +92,6 @@ pub trait Interactive<S: 'static + Send + Sync> {
|
||||
self
|
||||
}
|
||||
|
||||
fn on_click(
|
||||
self,
|
||||
button: MouseButton,
|
||||
handler: impl Fn(&mut S, (&MouseDownEvent, &MouseUpEvent), &mut ViewContext<S>)
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let down_event = Arc::new(Mutex::new(None));
|
||||
self.on_mouse_down(button, {
|
||||
let down_event = down_event.clone();
|
||||
move |_, event, _| {
|
||||
down_event.lock().replace(event.clone());
|
||||
}
|
||||
})
|
||||
.on_mouse_up_out(button, {
|
||||
let down_event = down_event.clone();
|
||||
move |_, _, _| {
|
||||
down_event.lock().take();
|
||||
}
|
||||
})
|
||||
.on_mouse_up(button, move |view, event, cx| {
|
||||
if let Some(down_event) = down_event.lock().take() {
|
||||
handler(view, (&down_event, event), cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn on_mouse_move(
|
||||
mut self,
|
||||
handler: impl Fn(&mut S, &MouseMoveEvent, &mut ViewContext<S>) + Send + Sync + 'static,
|
||||
|
||||
@@ -40,7 +40,6 @@ pub(crate) fn current_platform() -> Arc<dyn Platform> {
|
||||
|
||||
pub(crate) trait Platform: 'static {
|
||||
fn executor(&self) -> Executor;
|
||||
fn display_linker(&self) -> Arc<dyn PlatformDisplayLinker>;
|
||||
fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
|
||||
|
||||
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
|
||||
@@ -59,6 +58,14 @@ pub(crate) trait Platform: 'static {
|
||||
handle: AnyWindowHandle,
|
||||
options: WindowOptions,
|
||||
) -> Box<dyn PlatformWindow>;
|
||||
|
||||
fn set_display_link_output_callback(
|
||||
&self,
|
||||
display_id: DisplayId,
|
||||
callback: Box<dyn FnMut(&VideoTimestamp, &VideoTimestamp)>,
|
||||
);
|
||||
fn start_display_link(&self, display_id: DisplayId);
|
||||
fn stop_display_link(&self, display_id: DisplayId);
|
||||
// fn add_status_item(&self, _handle: AnyWindowHandle) -> Box<dyn PlatformWindow>;
|
||||
|
||||
fn open_url(&self, url: &str);
|
||||
@@ -154,16 +161,6 @@ pub trait PlatformDispatcher: Send + Sync {
|
||||
fn dispatch_on_main_thread(&self, task: Runnable);
|
||||
}
|
||||
|
||||
pub trait PlatformDisplayLinker: Send + Sync {
|
||||
fn set_output_callback(
|
||||
&self,
|
||||
display_id: DisplayId,
|
||||
callback: Box<dyn FnMut(&VideoTimestamp, &VideoTimestamp)>,
|
||||
);
|
||||
fn start(&self, display_id: DisplayId);
|
||||
fn stop(&self, display_id: DisplayId);
|
||||
}
|
||||
|
||||
pub trait PlatformTextSystem: Send + Sync {
|
||||
fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> Result<()>;
|
||||
fn all_font_families(&self) -> Vec<String>;
|
||||
|
||||
@@ -4,22 +4,20 @@ use std::{
|
||||
sync::{Arc, Weak},
|
||||
};
|
||||
|
||||
use crate::{DisplayId, PlatformDisplayLinker};
|
||||
use crate::DisplayId;
|
||||
use collections::HashMap;
|
||||
use parking_lot::Mutex;
|
||||
pub use sys::CVTimeStamp as VideoTimestamp;
|
||||
|
||||
pub struct MacDisplayLinker {
|
||||
links: Mutex<HashMap<DisplayId, MacDisplayLink>>,
|
||||
pub(crate) struct MacDisplayLinker {
|
||||
links: HashMap<DisplayId, MacDisplayLink>,
|
||||
}
|
||||
|
||||
struct MacDisplayLink {
|
||||
system_link: Mutex<sys::DisplayLink>,
|
||||
system_link: sys::DisplayLink,
|
||||
_output_callback: Arc<OutputCallback>,
|
||||
}
|
||||
|
||||
unsafe impl Send for MacDisplayLink {}
|
||||
|
||||
impl MacDisplayLinker {
|
||||
pub fn new() -> Self {
|
||||
MacDisplayLinker {
|
||||
@@ -30,9 +28,9 @@ impl MacDisplayLinker {
|
||||
|
||||
type OutputCallback = Mutex<Box<dyn FnMut(&VideoTimestamp, &VideoTimestamp)>>;
|
||||
|
||||
impl PlatformDisplayLinker for MacDisplayLinker {
|
||||
fn set_output_callback(
|
||||
&self,
|
||||
impl MacDisplayLinker {
|
||||
pub fn set_output_callback(
|
||||
&mut self,
|
||||
display_id: DisplayId,
|
||||
output_callback: Box<dyn FnMut(&VideoTimestamp, &VideoTimestamp)>,
|
||||
) {
|
||||
@@ -41,11 +39,11 @@ impl PlatformDisplayLinker for MacDisplayLinker {
|
||||
let weak_callback_ptr: *const OutputCallback = Arc::downgrade(&callback).into_raw();
|
||||
unsafe { system_link.set_output_callback(trampoline, weak_callback_ptr as *mut c_void) }
|
||||
|
||||
self.links.lock().insert(
|
||||
self.links.insert(
|
||||
display_id,
|
||||
MacDisplayLink {
|
||||
_output_callback: callback,
|
||||
system_link: Mutex::new(system_link),
|
||||
system_link,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
@@ -54,20 +52,20 @@ impl PlatformDisplayLinker for MacDisplayLinker {
|
||||
}
|
||||
}
|
||||
|
||||
fn start(&self, display_id: DisplayId) {
|
||||
if let Some(link) = self.links.lock().get_mut(&display_id) {
|
||||
pub fn start(&mut self, display_id: DisplayId) {
|
||||
if let Some(link) = self.links.get_mut(&display_id) {
|
||||
unsafe {
|
||||
link.system_link.lock().start();
|
||||
link.system_link.start();
|
||||
}
|
||||
} else {
|
||||
log::warn!("No DisplayLink callback registered for {:?}", display_id)
|
||||
}
|
||||
}
|
||||
|
||||
fn stop(&self, display_id: DisplayId) {
|
||||
if let Some(link) = self.links.lock().get_mut(&display_id) {
|
||||
pub fn stop(&mut self, display_id: DisplayId) {
|
||||
if let Some(link) = self.links.get_mut(&display_id) {
|
||||
unsafe {
|
||||
link.system_link.lock().stop();
|
||||
link.system_link.stop();
|
||||
}
|
||||
} else {
|
||||
log::warn!("No DisplayLink callback registered for {:?}", display_id)
|
||||
|
||||
@@ -2,8 +2,8 @@ use super::BoolExt;
|
||||
use crate::{
|
||||
AnyWindowHandle, ClipboardItem, CursorStyle, DisplayId, Event, Executor, MacDispatcher,
|
||||
MacDisplay, MacDisplayLinker, MacTextSystem, MacWindow, PathPromptOptions, Platform,
|
||||
PlatformDisplay, PlatformDisplayLinker, PlatformTextSystem, PlatformWindow, Result,
|
||||
SemanticVersion, WindowOptions,
|
||||
PlatformDisplay, PlatformTextSystem, PlatformWindow, Result, SemanticVersion, VideoTimestamp,
|
||||
WindowOptions,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use block::ConcreteBlock;
|
||||
@@ -145,6 +145,7 @@ pub struct MacPlatform(Mutex<MacPlatformState>);
|
||||
pub struct MacPlatformState {
|
||||
executor: Executor,
|
||||
text_system: Arc<MacTextSystem>,
|
||||
display_linker: MacDisplayLinker,
|
||||
pasteboard: id,
|
||||
text_hash_pasteboard_type: id,
|
||||
metadata_pasteboard_type: id,
|
||||
@@ -166,6 +167,7 @@ impl MacPlatform {
|
||||
Self(Mutex::new(MacPlatformState {
|
||||
executor: Executor::new(Arc::new(MacDispatcher)),
|
||||
text_system: Arc::new(MacTextSystem::new()),
|
||||
display_linker: MacDisplayLinker::new(),
|
||||
pasteboard: unsafe { NSPasteboard::generalPasteboard(nil) },
|
||||
text_hash_pasteboard_type: unsafe { ns_string("zed-text-hash") },
|
||||
metadata_pasteboard_type: unsafe { ns_string("zed-metadata") },
|
||||
@@ -348,10 +350,6 @@ impl Platform for MacPlatform {
|
||||
self.0.lock().executor.clone()
|
||||
}
|
||||
|
||||
fn display_linker(&self) -> Arc<dyn PlatformDisplayLinker> {
|
||||
Arc::new(MacDisplayLinker::new())
|
||||
}
|
||||
|
||||
fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
|
||||
self.0.lock().text_system.clone()
|
||||
}
|
||||
@@ -487,6 +485,25 @@ impl Platform for MacPlatform {
|
||||
Box::new(MacWindow::open(handle, options, self.executor()))
|
||||
}
|
||||
|
||||
fn set_display_link_output_callback(
|
||||
&self,
|
||||
display_id: DisplayId,
|
||||
callback: Box<dyn FnMut(&VideoTimestamp, &VideoTimestamp)>,
|
||||
) {
|
||||
self.0
|
||||
.lock()
|
||||
.display_linker
|
||||
.set_output_callback(display_id, callback);
|
||||
}
|
||||
|
||||
fn start_display_link(&self, display_id: DisplayId) {
|
||||
self.0.lock().display_linker.start(display_id);
|
||||
}
|
||||
|
||||
fn stop_display_link(&self, display_id: DisplayId) {
|
||||
self.0.lock().display_linker.stop(display_id);
|
||||
}
|
||||
|
||||
fn open_url(&self, url: &str) {
|
||||
unsafe {
|
||||
let url = NSURL::alloc(nil)
|
||||
@@ -675,6 +692,32 @@ impl Platform for MacPlatform {
|
||||
}
|
||||
}
|
||||
|
||||
// fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>) {
|
||||
// self.0.lock().menu_command = Some(callback);
|
||||
// }
|
||||
|
||||
// fn on_will_open_menu(&self, callback: Box<dyn FnMut()>) {
|
||||
// self.0.lock().will_open_menu = Some(callback);
|
||||
// }
|
||||
|
||||
// fn on_validate_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
|
||||
// self.0.lock().validate_menu_command = Some(callback);
|
||||
// }
|
||||
|
||||
// fn set_menus(&self, menus: Vec<Menu>, keystroke_matcher: &KeymapMatcher) {
|
||||
// unsafe {
|
||||
// let app: id = msg_send![APP_CLASS, sharedApplication];
|
||||
// let mut state = self.0.lock();
|
||||
// let actions = &mut state.menu_actions;
|
||||
// app.setMainMenu_(self.create_menu_bar(
|
||||
// menus,
|
||||
// app.delegate(),
|
||||
// actions,
|
||||
// keystroke_matcher,
|
||||
// ));
|
||||
// }
|
||||
// }
|
||||
|
||||
fn set_cursor_style(&self, style: CursorStyle) {
|
||||
unsafe {
|
||||
let new_cursor: id = match style {
|
||||
@@ -741,32 +784,6 @@ impl Platform for MacPlatform {
|
||||
}
|
||||
}
|
||||
|
||||
// fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>) {
|
||||
// self.0.lock().menu_command = Some(callback);
|
||||
// }
|
||||
|
||||
// fn on_will_open_menu(&self, callback: Box<dyn FnMut()>) {
|
||||
// self.0.lock().will_open_menu = Some(callback);
|
||||
// }
|
||||
|
||||
// fn on_validate_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
|
||||
// self.0.lock().validate_menu_command = Some(callback);
|
||||
// }
|
||||
|
||||
// fn set_menus(&self, menus: Vec<Menu>, keystroke_matcher: &KeymapMatcher) {
|
||||
// unsafe {
|
||||
// let app: id = msg_send![APP_CLASS, sharedApplication];
|
||||
// let mut state = self.0.lock();
|
||||
// let actions = &mut state.menu_actions;
|
||||
// app.setMainMenu_(self.create_menu_bar(
|
||||
// menus,
|
||||
// app.delegate(),
|
||||
// actions,
|
||||
// keystroke_matcher,
|
||||
// ));
|
||||
// }
|
||||
// }
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
let state = self.0.lock();
|
||||
unsafe {
|
||||
|
||||
@@ -15,10 +15,6 @@ impl Platform for TestPlatform {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn display_linker(&self) -> std::sync::Arc<dyn crate::PlatformDisplayLinker> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn text_system(&self) -> std::sync::Arc<dyn crate::PlatformTextSystem> {
|
||||
unimplemented!()
|
||||
}
|
||||
@@ -71,6 +67,22 @@ impl Platform for TestPlatform {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn set_display_link_output_callback(
|
||||
&self,
|
||||
_display_id: DisplayId,
|
||||
_callback: Box<dyn FnMut(&crate::VideoTimestamp, &crate::VideoTimestamp)>,
|
||||
) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn start_display_link(&self, _display_id: DisplayId) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn stop_display_link(&self, _display_id: DisplayId) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn open_url(&self, _url: &str) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ pub trait StyleHelpers: Sized + Styled<Style = Style> {
|
||||
fn shadow_md(mut self) -> Self {
|
||||
self.declared_style().box_shadow = Some(smallvec![
|
||||
BoxShadow {
|
||||
color: hsla(0.5, 0., 0., 1.0),
|
||||
color: hsla(0.5, 0., 0., 0.1),
|
||||
offset: point(px(0.), px(4.)),
|
||||
blur_radius: px(6.),
|
||||
spread_radius: px(-1.),
|
||||
|
||||
+33
-11
@@ -1,9 +1,9 @@
|
||||
use crate::{Hoverable, Refineable, RefinementCascade};
|
||||
use crate::{Cascade, Hoverable, Pressable, Refineable, SharedString};
|
||||
|
||||
pub trait Styled {
|
||||
type Style: Refineable + Default;
|
||||
type Style: 'static + Refineable + Send + Sync + Default;
|
||||
|
||||
fn style_cascade(&mut self) -> &mut RefinementCascade<Self::Style>;
|
||||
fn style_cascade(&mut self) -> &mut Cascade<Self::Style>;
|
||||
fn declared_style(&mut self) -> &mut <Self::Style as Refineable>::Refinement;
|
||||
|
||||
fn computed_style(&mut self) -> Self::Style {
|
||||
@@ -12,15 +12,37 @@ pub trait Styled {
|
||||
|
||||
fn hover(self) -> Hoverable<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
Self: 'static + Sized + Send + Sync,
|
||||
Self::Style: 'static + Refineable + Default + Send + Sync,
|
||||
<Self::Style as Refineable>::Refinement: 'static + Default + Send + Sync,
|
||||
{
|
||||
Hoverable::new(self)
|
||||
Hoverable::new(self, None)
|
||||
}
|
||||
|
||||
// fn active(self) -> Pressable<Self>
|
||||
// where
|
||||
// Self: Sized,
|
||||
// {
|
||||
// pressable(self)
|
||||
// }
|
||||
fn group_hover(self, group_name: impl Into<SharedString>) -> Hoverable<Self>
|
||||
where
|
||||
Self: 'static + Sized + Send + Sync,
|
||||
Self::Style: 'static + Refineable + Default + Send + Sync,
|
||||
<Self::Style as Refineable>::Refinement: 'static + Default + Send + Sync,
|
||||
{
|
||||
Hoverable::new(self, Some(group_name.into()))
|
||||
}
|
||||
|
||||
fn active(self) -> Pressable<Self>
|
||||
where
|
||||
Self: 'static + Sized + Send + Sync,
|
||||
Self::Style: 'static + Refineable + Default + Send + Sync,
|
||||
<Self::Style as Refineable>::Refinement: 'static + Default + Send + Sync,
|
||||
{
|
||||
Pressable::new(self, None)
|
||||
}
|
||||
|
||||
fn group_active(self, group_name: impl Into<SharedString>) -> Pressable<Self>
|
||||
where
|
||||
Self: 'static + Sized + Send + Sync,
|
||||
Self::Style: 'static + Refineable + Default + Send + Sync,
|
||||
<Self::Style as Refineable>::Refinement: 'static + Default + Send + Sync,
|
||||
{
|
||||
Pressable::new(self, Some(group_name.into()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
use collections::{BTreeMap, BTreeSet};
|
||||
use parking_lot::Mutex;
|
||||
use std::{fmt::Debug, mem, sync::Arc};
|
||||
use util::post_inc;
|
||||
|
||||
pub(crate) struct SubscriberSet<EmitterKey, Callback>(
|
||||
Arc<Mutex<SubscriberSetState<EmitterKey, Callback>>>,
|
||||
);
|
||||
|
||||
impl<EmitterKey, Callback> Clone for SubscriberSet<EmitterKey, Callback> {
|
||||
fn clone(&self) -> Self {
|
||||
SubscriberSet(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
struct SubscriberSetState<EmitterKey, Callback> {
|
||||
subscribers: BTreeMap<EmitterKey, BTreeMap<usize, Callback>>,
|
||||
dropped_subscribers: BTreeSet<(EmitterKey, usize)>,
|
||||
next_subscriber_id: usize,
|
||||
}
|
||||
|
||||
impl<EmitterKey, Callback> SubscriberSet<EmitterKey, Callback>
|
||||
where
|
||||
EmitterKey: 'static + Ord + Clone + Debug,
|
||||
Callback: 'static,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Self(Arc::new(Mutex::new(SubscriberSetState {
|
||||
subscribers: Default::default(),
|
||||
dropped_subscribers: Default::default(),
|
||||
next_subscriber_id: 0,
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn insert(&self, emitter: EmitterKey, callback: Callback) -> Subscription {
|
||||
let mut lock = self.0.lock();
|
||||
let subscriber_id = post_inc(&mut lock.next_subscriber_id);
|
||||
lock.subscribers
|
||||
.entry(emitter.clone())
|
||||
.or_default()
|
||||
.insert(subscriber_id, callback);
|
||||
let this = self.0.clone();
|
||||
Subscription {
|
||||
unsubscribe: Some(Box::new(move || {
|
||||
let mut lock = this.lock();
|
||||
if let Some(subscribers) = lock.subscribers.get_mut(&emitter) {
|
||||
subscribers.remove(&subscriber_id);
|
||||
if subscribers.is_empty() {
|
||||
lock.subscribers.remove(&emitter);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// We didn't manage to remove the subscription, which means it was dropped
|
||||
// while invoking the callback. Mark it as dropped so that we can remove it
|
||||
// later.
|
||||
lock.dropped_subscribers.insert((emitter, subscriber_id));
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&self, emitter: &EmitterKey) -> impl IntoIterator<Item = Callback> {
|
||||
let subscribers = self.0.lock().subscribers.remove(&emitter);
|
||||
subscribers.unwrap_or_default().into_values()
|
||||
}
|
||||
|
||||
pub fn retain<F>(&self, emitter: &EmitterKey, mut f: F)
|
||||
where
|
||||
F: FnMut(&mut Callback) -> bool,
|
||||
{
|
||||
let entry = self.0.lock().subscribers.remove_entry(emitter);
|
||||
if let Some((emitter, mut subscribers)) = entry {
|
||||
subscribers.retain(|_, callback| f(callback));
|
||||
let mut lock = self.0.lock();
|
||||
|
||||
// Add any new subscribers that were added while invoking the callback.
|
||||
if let Some(new_subscribers) = lock.subscribers.remove(&emitter) {
|
||||
subscribers.extend(new_subscribers);
|
||||
}
|
||||
|
||||
// Remove any dropped subscriptions that were dropped while invoking the callback.
|
||||
for (dropped_emitter, dropped_subscription_id) in
|
||||
mem::take(&mut lock.dropped_subscribers)
|
||||
{
|
||||
debug_assert_eq!(emitter, dropped_emitter);
|
||||
subscribers.remove(&dropped_subscription_id);
|
||||
}
|
||||
|
||||
if !subscribers.is_empty() {
|
||||
lock.subscribers.insert(emitter, subscribers);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub struct Subscription {
|
||||
unsubscribe: Option<Box<dyn FnOnce()>>,
|
||||
}
|
||||
|
||||
impl Subscription {
|
||||
pub fn detach(mut self) {
|
||||
self.unsubscribe.take();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Subscription {
|
||||
fn drop(&mut self) {
|
||||
if let Some(unsubscribe) = self.unsubscribe.take() {
|
||||
unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
-24
@@ -1,6 +1,4 @@
|
||||
use super::{
|
||||
AbsoluteLength, Bounds, DefiniteLength, Edges, Length, Pixels, Point, Result, Size, Style,
|
||||
};
|
||||
use super::{AbsoluteLength, Bounds, DefiniteLength, Edges, Length, Pixels, Point, Size, Style};
|
||||
use collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
use taffy::{
|
||||
@@ -16,6 +14,9 @@ pub struct TaffyLayoutEngine {
|
||||
absolute_layout_bounds: HashMap<LayoutId, Bounds<Pixels>>,
|
||||
}
|
||||
|
||||
static EXPECT_MESSAGE: &'static str =
|
||||
"we should avoid taffy layout errors by construction if possible";
|
||||
|
||||
impl TaffyLayoutEngine {
|
||||
pub fn new() -> Self {
|
||||
TaffyLayoutEngine {
|
||||
@@ -30,20 +31,21 @@ impl TaffyLayoutEngine {
|
||||
style: Style,
|
||||
rem_size: Pixels,
|
||||
children: &[LayoutId],
|
||||
) -> Result<LayoutId> {
|
||||
) -> LayoutId {
|
||||
let style = style.to_taffy(rem_size);
|
||||
if children.is_empty() {
|
||||
Ok(self.taffy.new_leaf(style)?.into())
|
||||
self.taffy.new_leaf(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(style, unsafe { std::mem::transmute(children) })
|
||||
.expect(EXPECT_MESSAGE)
|
||||
.into();
|
||||
for child_id in children {
|
||||
self.children_to_parents.insert(*child_id, parent_id);
|
||||
}
|
||||
Ok(parent_id)
|
||||
parent_id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,14 +57,14 @@ impl TaffyLayoutEngine {
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
) -> Result<LayoutId> {
|
||||
) -> LayoutId {
|
||||
let style = style.to_taffy(rem_size);
|
||||
|
||||
let measurable = Box::new(Measureable(measure)) as Box<dyn Measurable>;
|
||||
Ok(self
|
||||
.taffy
|
||||
.new_leaf_with_measure(style, MeasureFunc::Boxed(measurable))?
|
||||
.into())
|
||||
self.taffy
|
||||
.new_leaf_with_measure(style, MeasureFunc::Boxed(measurable))
|
||||
.expect(EXPECT_MESSAGE)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn count_all_children(&self, parent: LayoutId) -> Result<u32> {
|
||||
@@ -106,11 +108,7 @@ impl TaffyLayoutEngine {
|
||||
Ok(edges)
|
||||
}
|
||||
|
||||
pub fn compute_layout(
|
||||
&mut self,
|
||||
id: LayoutId,
|
||||
available_space: Size<AvailableSpace>,
|
||||
) -> Result<()> {
|
||||
pub fn compute_layout(&mut self, id: LayoutId, available_space: Size<AvailableSpace>) {
|
||||
// println!("Laying out {} children", self.count_all_children(id)?);
|
||||
// println!("Max layout depth: {}", self.max_depth(0, id)?);
|
||||
|
||||
@@ -123,29 +121,29 @@ impl TaffyLayoutEngine {
|
||||
|
||||
let started_at = std::time::Instant::now();
|
||||
self.taffy
|
||||
.compute_layout(id.into(), available_space.into())?;
|
||||
.compute_layout(id.into(), available_space.into())
|
||||
.expect(EXPECT_MESSAGE);
|
||||
println!("compute_layout took {:?}", started_at.elapsed());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn layout_bounds(&mut self, id: LayoutId) -> Result<Bounds<Pixels>> {
|
||||
pub fn layout_bounds(&mut self, id: LayoutId) -> Bounds<Pixels> {
|
||||
if let Some(layout) = self.absolute_layout_bounds.get(&id).cloned() {
|
||||
return Ok(layout);
|
||||
return layout;
|
||||
}
|
||||
|
||||
let layout = self.taffy.layout(id.into())?;
|
||||
let layout = self.taffy.layout(id.into()).expect(EXPECT_MESSAGE);
|
||||
let mut bounds = Bounds {
|
||||
origin: layout.location.into(),
|
||||
size: layout.size.into(),
|
||||
};
|
||||
|
||||
if let Some(parent_id) = self.children_to_parents.get(&id).copied() {
|
||||
let parent_bounds = self.layout_bounds(parent_id)?;
|
||||
let parent_bounds = self.layout_bounds(parent_id);
|
||||
bounds.origin += parent_bounds.origin;
|
||||
}
|
||||
self.absolute_layout_bounds.insert(id, bounds);
|
||||
|
||||
Ok(bounds)
|
||||
bounds
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -173,8 +173,8 @@ impl TextSystem {
|
||||
Ok(Line::new(layout.clone(), runs))
|
||||
}
|
||||
|
||||
pub fn finish_frame(&self) {
|
||||
self.text_layout_cache.finish_frame()
|
||||
pub fn end_frame(&self) {
|
||||
self.text_layout_cache.end_frame()
|
||||
}
|
||||
|
||||
pub fn line_wrapper(
|
||||
|
||||
@@ -23,7 +23,7 @@ impl TextLayoutCache {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish_frame(&self) {
|
||||
pub fn end_frame(&self) {
|
||||
let mut prev_frame = self.prev_frame.lock();
|
||||
let mut curr_frame = self.curr_frame.write();
|
||||
std::mem::swap(&mut *prev_frame, &mut *curr_frame);
|
||||
|
||||
+55
-49
@@ -1,8 +1,8 @@
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use crate::{
|
||||
AnyElement, Bounds, Element, Handle, IntoAnyElement, LayoutId, Pixels, Result, ViewContext,
|
||||
WindowContext,
|
||||
AnyBox, AnyElement, BorrowWindow, Bounds, Element, ElementId, Handle, IdentifiedElement,
|
||||
IntoAnyElement, LayoutId, Pixels, ViewContext, WindowContext,
|
||||
};
|
||||
use std::{any::Any, marker::PhantomData, sync::Arc};
|
||||
|
||||
@@ -12,7 +12,7 @@ pub struct View<S: Send + Sync, P> {
|
||||
parent_state_type: PhantomData<P>,
|
||||
}
|
||||
|
||||
impl<S: 'static + Send + Sync, P: 'static + Send> View<S, P> {
|
||||
impl<S: 'static + Send + Sync, P: 'static + Send + Sync> View<S, P> {
|
||||
pub fn into_any(self) -> AnyView<P> {
|
||||
AnyView {
|
||||
view: Arc::new(Mutex::new(self)),
|
||||
@@ -40,7 +40,7 @@ pub fn view<S, P, E>(
|
||||
where
|
||||
S: 'static + Send + Sync,
|
||||
P: 'static,
|
||||
E: Element<State = S>,
|
||||
E: Element<ViewState = S>,
|
||||
{
|
||||
View {
|
||||
state,
|
||||
@@ -49,64 +49,65 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Send + Sync + 'static, P: Send + 'static> Element for View<S, P> {
|
||||
type State = P;
|
||||
type FrameState = AnyElement<S>;
|
||||
impl<S: 'static + Send + Sync, P: 'static + Send + Sync> Element for View<S, P> {
|
||||
type ViewState = P;
|
||||
type ElementState = AnyElement<S>;
|
||||
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
Some(ElementId::View(self.state.id))
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_: &mut Self::State,
|
||||
cx: &mut ViewContext<Self::State>,
|
||||
) -> Result<(LayoutId, Self::FrameState)> {
|
||||
_: &mut Self::ViewState,
|
||||
_: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<Self::ViewState>,
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
self.state.update(cx, |state, cx| {
|
||||
let mut element = (self.render)(state, cx);
|
||||
let layout_id = element.layout(state, cx)?;
|
||||
Ok((layout_id, element))
|
||||
let layout_id = element.layout(state, cx);
|
||||
(layout_id, element)
|
||||
})
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_: Bounds<Pixels>,
|
||||
_: &mut Self::State,
|
||||
element: &mut Self::FrameState,
|
||||
cx: &mut ViewContext<Self::State>,
|
||||
) -> Result<()> {
|
||||
_: &mut Self::ViewState,
|
||||
element: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<Self::ViewState>,
|
||||
) {
|
||||
self.state
|
||||
.update(cx, |state, cx| element.paint(state, None, cx))
|
||||
}
|
||||
}
|
||||
|
||||
trait ViewObject: Send + 'static {
|
||||
fn layout(&mut self, cx: &mut WindowContext) -> Result<(LayoutId, Box<dyn Any>)>;
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
element: &mut dyn Any,
|
||||
cx: &mut WindowContext,
|
||||
) -> Result<()>;
|
||||
fn layout(&mut self, cx: &mut WindowContext) -> (LayoutId, AnyBox);
|
||||
fn paint(&mut self, bounds: Bounds<Pixels>, element: &mut dyn Any, cx: &mut WindowContext);
|
||||
}
|
||||
|
||||
impl<S: Send + Sync + 'static, P: Send + 'static> ViewObject for View<S, P> {
|
||||
fn layout(&mut self, cx: &mut WindowContext) -> Result<(LayoutId, Box<dyn Any>)> {
|
||||
self.state.update(cx, |state, cx| {
|
||||
let mut element = (self.render)(state, cx);
|
||||
let layout_id = element.layout(state, cx)?;
|
||||
let element = Box::new(element) as Box<dyn Any>;
|
||||
Ok((layout_id, element))
|
||||
impl<S: Send + Sync + 'static, P: Send + Sync + 'static> IdentifiedElement for View<S, P> {}
|
||||
|
||||
impl<S: Send + Sync + 'static, P: Send + Sync + 'static> ViewObject for View<S, P> {
|
||||
fn layout(&mut self, cx: &mut WindowContext) -> (LayoutId, AnyBox) {
|
||||
cx.with_element_id(IdentifiedElement::element_id(self), |cx| {
|
||||
self.state.update(cx, |state, cx| {
|
||||
let mut element = (self.render)(state, cx);
|
||||
let layout_id = element.layout(state, cx);
|
||||
let element = Box::new(element) as AnyBox;
|
||||
(layout_id, element)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_: Bounds<Pixels>,
|
||||
element: &mut dyn Any,
|
||||
cx: &mut WindowContext,
|
||||
) -> Result<()> {
|
||||
self.state.update(cx, |state, cx| {
|
||||
let element = element.downcast_mut::<AnyElement<S>>().unwrap();
|
||||
element.paint(state, None, cx)
|
||||
})
|
||||
fn paint(&mut self, _: Bounds<Pixels>, element: &mut dyn Any, cx: &mut WindowContext) {
|
||||
cx.with_element_id(IdentifiedElement::element_id(self), |cx| {
|
||||
self.state.update(cx, |state, cx| {
|
||||
let element = element.downcast_mut::<AnyElement<S>>().unwrap();
|
||||
element.paint(state, None, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,15 +116,20 @@ pub struct AnyView<S> {
|
||||
parent_state_type: PhantomData<S>,
|
||||
}
|
||||
|
||||
impl<S: 'static> Element for AnyView<S> {
|
||||
type State = ();
|
||||
type FrameState = Box<dyn Any>;
|
||||
impl<S: 'static + Send + Sync> Element for AnyView<S> {
|
||||
type ViewState = ();
|
||||
type ElementState = AnyBox;
|
||||
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_: &mut Self::State,
|
||||
cx: &mut ViewContext<Self::State>,
|
||||
) -> Result<(LayoutId, Self::FrameState)> {
|
||||
_: &mut Self::ViewState,
|
||||
_: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<Self::ViewState>,
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
self.view.lock().layout(cx)
|
||||
}
|
||||
|
||||
@@ -131,9 +137,9 @@ impl<S: 'static> Element for AnyView<S> {
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
_: &mut (),
|
||||
element: &mut Box<dyn Any>,
|
||||
cx: &mut ViewContext<Self::State>,
|
||||
) -> Result<()> {
|
||||
element: &mut AnyBox,
|
||||
cx: &mut ViewContext<Self::ViewState>,
|
||||
) {
|
||||
self.view.lock().paint(bounds, element.as_mut(), cx)
|
||||
}
|
||||
}
|
||||
|
||||
+221
-75
@@ -1,11 +1,11 @@
|
||||
use crate::{
|
||||
image_cache::RenderImageParams, px, size, AnyView, AppContext, AsyncWindowContext,
|
||||
AvailableSpace, BorrowAppContext, Bounds, BoxShadow, Context, Corners, DevicePixels, DisplayId,
|
||||
Edges, Effect, Element, ElementId, EntityId, Event, FontId, GlobalElementId, GlyphId, Handle,
|
||||
Hsla, ImageData, IsZero, LayoutId, MainThread, MainThreadOnly, MonochromeSprite,
|
||||
MouseMoveEvent, Path, Pixels, PlatformAtlas, PlatformWindow, Point, PolychromeSprite, Quad,
|
||||
Reference, RenderGlyphParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow,
|
||||
SharedString, Size, Style, TaffyLayoutEngine, Task, Underline, UnderlineStyle, WeakHandle,
|
||||
px, size, AnyBox, AnyView, AppContext, AsyncWindowContext, AvailableSpace, BorrowAppContext,
|
||||
Bounds, BoxShadow, Context, Corners, DevicePixels, DisplayId, Edges, Effect, Element, EntityId,
|
||||
Event, EventEmitter, FontId, GlobalElementId, GlyphId, Handle, Hsla, ImageData, IsZero,
|
||||
LayoutId, MainThread, MainThreadOnly, MonochromeSprite, MouseMoveEvent, Path, Pixels, Platform,
|
||||
PlatformAtlas, PlatformWindow, Point, PolychromeSprite, Quad, Reference, RenderGlyphParams,
|
||||
RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size,
|
||||
Style, Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, WeakHandle,
|
||||
WindowOptions, SUBPIXEL_VARIANTS,
|
||||
};
|
||||
use anyhow::Result;
|
||||
@@ -52,6 +52,8 @@ pub struct Window {
|
||||
layout_engine: TaffyLayoutEngine,
|
||||
pub(crate) root_view: Option<AnyView<()>>,
|
||||
pub(crate) element_id_stack: GlobalElementId,
|
||||
prev_element_states: HashMap<GlobalElementId, AnyBox>,
|
||||
element_states: HashMap<GlobalElementId, AnyBox>,
|
||||
z_index_stack: StackingOrder,
|
||||
content_mask_stack: Vec<ContentMask<Pixels>>,
|
||||
mouse_event_handlers: HashMap<TypeId, Vec<(StackingOrder, MouseEventHandler)>>,
|
||||
@@ -114,6 +116,8 @@ impl Window {
|
||||
layout_engine: TaffyLayoutEngine::new(),
|
||||
root_view: None,
|
||||
element_id_stack: GlobalElementId::default(),
|
||||
prev_element_states: HashMap::default(),
|
||||
element_states: HashMap::default(),
|
||||
z_index_stack: StackingOrder(SmallVec::new()),
|
||||
content_mask_stack: Vec::new(),
|
||||
mouse_event_handlers: HashMap::default(),
|
||||
@@ -147,7 +151,7 @@ impl ContentMask<Pixels> {
|
||||
|
||||
pub struct WindowContext<'a, 'w> {
|
||||
app: Reference<'a, AppContext>,
|
||||
window: Reference<'w, Window>,
|
||||
pub(crate) window: Reference<'w, Window>,
|
||||
}
|
||||
|
||||
impl<'a, 'w> WindowContext<'a, 'w> {
|
||||
@@ -186,17 +190,17 @@ impl<'a, 'w> WindowContext<'a, 'w> {
|
||||
pub fn on_next_frame(&mut self, f: impl FnOnce(&mut WindowContext) + Send + 'static) {
|
||||
let f = Box::new(f);
|
||||
let display_id = self.window.display_id;
|
||||
let async_cx = self.to_async();
|
||||
let app_cx = self.app_mut();
|
||||
match app_cx.next_frame_callbacks.entry(display_id) {
|
||||
collections::hash_map::Entry::Occupied(mut entry) => {
|
||||
if entry.get().is_empty() {
|
||||
app_cx.display_linker.start(display_id);
|
||||
self.run_on_main(move |cx| {
|
||||
if let Some(callbacks) = cx.next_frame_callbacks.get_mut(&display_id) {
|
||||
callbacks.push(f);
|
||||
// If there was already a callback, it means that we already scheduled a frame.
|
||||
if callbacks.len() > 1 {
|
||||
return;
|
||||
}
|
||||
entry.get_mut().push(f);
|
||||
}
|
||||
collections::hash_map::Entry::Vacant(entry) => {
|
||||
app_cx.display_linker.set_output_callback(
|
||||
} else {
|
||||
let async_cx = cx.to_async();
|
||||
cx.next_frame_callbacks.insert(display_id, vec![f]);
|
||||
cx.platform().set_display_link_output_callback(
|
||||
display_id,
|
||||
Box::new(move |_current_time, _output_time| {
|
||||
let _ = async_cx.update(|cx| {
|
||||
@@ -210,16 +214,20 @@ impl<'a, 'w> WindowContext<'a, 'w> {
|
||||
callback(cx);
|
||||
}
|
||||
|
||||
if cx.next_frame_callbacks.get(&display_id).unwrap().is_empty() {
|
||||
cx.display_linker.stop(display_id);
|
||||
}
|
||||
cx.run_on_main(move |cx| {
|
||||
if cx.next_frame_callbacks.get(&display_id).unwrap().is_empty() {
|
||||
cx.platform().stop_display_link(display_id);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
}),
|
||||
);
|
||||
app_cx.display_linker.start(display_id);
|
||||
entry.insert(vec![f]);
|
||||
}
|
||||
}
|
||||
|
||||
cx.platform().start_display_link(display_id);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn spawn<Fut, R>(
|
||||
@@ -242,7 +250,7 @@ impl<'a, 'w> WindowContext<'a, 'w> {
|
||||
&mut self,
|
||||
style: Style,
|
||||
children: impl IntoIterator<Item = LayoutId>,
|
||||
) -> Result<LayoutId> {
|
||||
) -> LayoutId {
|
||||
self.app.layout_id_buffer.clear();
|
||||
self.app.layout_id_buffer.extend(children.into_iter());
|
||||
let rem_size = self.rem_size();
|
||||
@@ -259,18 +267,17 @@ impl<'a, 'w> WindowContext<'a, 'w> {
|
||||
style: Style,
|
||||
rem_size: Pixels,
|
||||
measure: F,
|
||||
) -> Result<LayoutId> {
|
||||
) -> LayoutId {
|
||||
self.window
|
||||
.layout_engine
|
||||
.request_measured_layout(style, rem_size, measure)
|
||||
}
|
||||
|
||||
pub fn layout_bounds(&mut self, layout_id: LayoutId) -> Result<Bounds<Pixels>> {
|
||||
Ok(self
|
||||
.window
|
||||
pub fn layout_bounds(&mut self, layout_id: LayoutId) -> Bounds<Pixels> {
|
||||
self.window
|
||||
.layout_engine
|
||||
.layout_bounds(layout_id)
|
||||
.map(Into::into)?)
|
||||
.map(Into::into)
|
||||
}
|
||||
|
||||
pub fn scale_factor(&self) -> f32 {
|
||||
@@ -586,26 +593,25 @@ impl<'a, 'w> WindowContext<'a, 'w> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn draw(&mut self) -> Result<()> {
|
||||
pub(crate) fn draw(&mut self) {
|
||||
let unit_entity = self.unit_entity.clone();
|
||||
self.update_entity(&unit_entity, |view, cx| {
|
||||
cx.window
|
||||
.mouse_event_handlers
|
||||
.values_mut()
|
||||
.for_each(Vec::clear);
|
||||
cx.start_frame();
|
||||
|
||||
let mut root_view = cx.window.root_view.take().unwrap();
|
||||
let (root_layout_id, mut frame_state) = root_view.layout(&mut (), cx)?;
|
||||
let available_space = cx.window.content_size.map(Into::into);
|
||||
|
||||
cx.window
|
||||
.layout_engine
|
||||
.compute_layout(root_layout_id, available_space)?;
|
||||
let layout = cx.window.layout_engine.layout_bounds(root_layout_id)?;
|
||||
if let Some(element_id) = root_view.element_id() {
|
||||
cx.with_element_state(element_id, |element_state, cx| {
|
||||
let element_state = draw_with_element_state(&mut root_view, element_state, cx);
|
||||
((), element_state)
|
||||
});
|
||||
} else {
|
||||
draw_with_element_state(&mut root_view, None, cx);
|
||||
};
|
||||
|
||||
root_view.paint(layout, &mut (), &mut frame_state, cx)?;
|
||||
cx.window.root_view = Some(root_view);
|
||||
let scene = cx.window.scene_builder.build();
|
||||
cx.end_frame();
|
||||
|
||||
cx.run_on_main(view, |_, cx| {
|
||||
cx.window
|
||||
@@ -615,9 +621,42 @@ impl<'a, 'w> WindowContext<'a, 'w> {
|
||||
cx.window.dirty = false;
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
fn draw_with_element_state(
|
||||
root_view: &mut AnyView<()>,
|
||||
element_state: Option<AnyBox>,
|
||||
cx: &mut ViewContext<()>,
|
||||
) -> AnyBox {
|
||||
let (layout_id, mut element_state) = root_view.layout(&mut (), element_state, cx);
|
||||
let available_space = cx.window.content_size.map(Into::into);
|
||||
cx.window
|
||||
.layout_engine
|
||||
.compute_layout(layout_id, available_space);
|
||||
let bounds = cx.window.layout_engine.layout_bounds(layout_id);
|
||||
root_view.paint(bounds, &mut (), &mut element_state, cx);
|
||||
element_state
|
||||
}
|
||||
}
|
||||
|
||||
fn start_frame(&mut self) {
|
||||
// Make the current element states the previous, and then clear the current.
|
||||
// The empty element states map will be populated for any element states we
|
||||
// reference during the upcoming frame.
|
||||
let window = &mut *self.window;
|
||||
mem::swap(&mut window.element_states, &mut window.prev_element_states);
|
||||
self.window.element_states.clear();
|
||||
|
||||
// Clear mouse event listeners, because elements add new element listeners
|
||||
// when the upcoming frame is painted.
|
||||
self.window
|
||||
.mouse_event_handlers
|
||||
.values_mut()
|
||||
.for_each(Vec::clear);
|
||||
}
|
||||
|
||||
fn end_frame(&mut self) {
|
||||
self.text_system().end_frame();
|
||||
}
|
||||
|
||||
fn dispatch_event(&mut self, event: Event) -> bool {
|
||||
@@ -674,6 +713,12 @@ impl<'a, 'w> WindowContext<'a, 'w> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'w> MainThread<WindowContext<'a, 'w>> {
|
||||
fn platform(&self) -> &dyn Platform {
|
||||
self.platform.borrow_on_main_thread()
|
||||
}
|
||||
}
|
||||
|
||||
impl Context for WindowContext<'_, '_> {
|
||||
type EntityContext<'a, 'w, T: 'static + Send + Sync> = ViewContext<'a, 'w, T>;
|
||||
type Result<T> = T;
|
||||
@@ -753,6 +798,40 @@ pub trait BorrowWindow: BorrowAppContext {
|
||||
result
|
||||
}
|
||||
|
||||
fn with_element_state<S: 'static + Send + Sync, R>(
|
||||
&mut self,
|
||||
id: ElementId,
|
||||
f: impl FnOnce(Option<S>, &mut Self) -> (R, S),
|
||||
) -> R {
|
||||
self.with_element_id(id, |cx| {
|
||||
let global_id = cx.window_mut().element_id_stack.clone();
|
||||
if let Some(any) = cx
|
||||
.window_mut()
|
||||
.element_states
|
||||
.remove(&global_id)
|
||||
.or_else(|| cx.window_mut().prev_element_states.remove(&global_id))
|
||||
{
|
||||
// Using the extra inner option to avoid needing to reallocate a new box.
|
||||
let mut state_box = any
|
||||
.downcast::<Option<S>>()
|
||||
.expect("invalid element state type for id");
|
||||
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().element_states.insert(global_id, state_box);
|
||||
result
|
||||
} else {
|
||||
let (result, state) = f(None, cx);
|
||||
cx.window_mut()
|
||||
.element_states
|
||||
.insert(global_id, Box::new(Some(state)));
|
||||
result
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn content_mask(&self) -> ContentMask<Pixels> {
|
||||
self.window()
|
||||
.content_mask_stack
|
||||
@@ -791,26 +870,6 @@ impl<S> BorrowAppContext for ViewContext<'_, '_, S> {
|
||||
fn app_mut(&mut self) -> &mut AppContext {
|
||||
&mut *self.window_cx.app
|
||||
}
|
||||
|
||||
fn with_text_style<F, R>(&mut self, style: crate::TextStyleRefinement, f: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut Self) -> R,
|
||||
{
|
||||
self.window_cx.app.push_text_style(style);
|
||||
let result = f(self);
|
||||
self.window_cx.app.pop_text_style();
|
||||
result
|
||||
}
|
||||
|
||||
fn with_state<T: Send + Sync + 'static, F, R>(&mut self, state: T, f: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut Self) -> R,
|
||||
{
|
||||
self.window_cx.app.push_state(state);
|
||||
let result = f(self);
|
||||
self.window_cx.app.pop_state::<T>();
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> BorrowWindow for ViewContext<'_, '_, S> {
|
||||
@@ -854,15 +913,13 @@ impl<'a, 'w, S: Send + Sync + 'static> ViewContext<'a, 'w, S> {
|
||||
&mut self,
|
||||
handle: &Handle<E>,
|
||||
on_notify: impl Fn(&mut S, Handle<E>, &mut ViewContext<'_, '_, S>) + Send + Sync + 'static,
|
||||
) {
|
||||
) -> Subscription {
|
||||
let this = self.handle();
|
||||
let handle = handle.downgrade();
|
||||
let window_handle = self.window.handle;
|
||||
self.app
|
||||
.observers
|
||||
.entry(handle.id)
|
||||
.or_default()
|
||||
.push(Arc::new(move |cx| {
|
||||
self.app.observers.insert(
|
||||
handle.id,
|
||||
Box::new(move |cx| {
|
||||
cx.update_window(window_handle.id, |cx| {
|
||||
if let Some(handle) = handle.upgrade(cx) {
|
||||
this.update(cx, |this, cx| on_notify(this, handle, cx))
|
||||
@@ -872,15 +929,77 @@ impl<'a, 'w, S: Send + Sync + 'static> ViewContext<'a, 'w, S> {
|
||||
}
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}));
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn subscribe<E: EventEmitter + Send + Sync + 'static>(
|
||||
&mut self,
|
||||
handle: &Handle<E>,
|
||||
on_event: impl Fn(&mut S, Handle<E>, &E::Event, &mut ViewContext<'_, '_, S>)
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
) -> Subscription {
|
||||
let this = self.handle();
|
||||
let handle = handle.downgrade();
|
||||
let window_handle = self.window.handle;
|
||||
self.app.event_handlers.insert(
|
||||
handle.id,
|
||||
Box::new(move |event, cx| {
|
||||
cx.update_window(window_handle.id, |cx| {
|
||||
if let Some(handle) = handle.upgrade(cx) {
|
||||
let event = event.downcast_ref().expect("invalid event type");
|
||||
this.update(cx, |this, cx| on_event(this, handle, event, cx))
|
||||
.is_ok()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn on_release(
|
||||
&mut self,
|
||||
on_release: impl Fn(&mut S, &mut WindowContext) + Send + Sync + 'static,
|
||||
) -> Subscription {
|
||||
let window_handle = self.window.handle;
|
||||
self.app.release_handlers.insert(
|
||||
self.entity_id,
|
||||
Box::new(move |this, cx| {
|
||||
let this = this.downcast_mut().expect("invalid entity type");
|
||||
// todo!("are we okay with silently swallowing the error?")
|
||||
let _ = cx.update_window(window_handle.id, |cx| on_release(this, cx));
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn observe_release<E: Send + Sync + 'static>(
|
||||
&mut self,
|
||||
handle: &Handle<E>,
|
||||
on_release: impl Fn(&mut S, &mut E, &mut ViewContext<'_, '_, S>) + Send + Sync + 'static,
|
||||
) -> Subscription {
|
||||
let this = self.handle();
|
||||
let window_handle = self.window.handle;
|
||||
self.app.release_handlers.insert(
|
||||
handle.id,
|
||||
Box::new(move |entity, cx| {
|
||||
let entity = entity.downcast_mut().expect("invalid entity type");
|
||||
// todo!("are we okay with silently swallowing the error?")
|
||||
let _ = cx.update_window(window_handle.id, |cx| {
|
||||
this.update(cx, |this, cx| on_release(this, entity, cx))
|
||||
});
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn notify(&mut self) {
|
||||
self.window_cx.notify();
|
||||
self.window_cx
|
||||
.app
|
||||
.pending_effects
|
||||
.push_back(Effect::Notify(self.entity_id));
|
||||
self.window_cx.app.push_effect(Effect::Notify {
|
||||
emitter: self.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn run_on_main<R>(
|
||||
@@ -928,6 +1047,15 @@ impl<'a, 'w, S: Send + Sync + 'static> ViewContext<'a, 'w, S> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'w, S: EventEmitter + Send + Sync + 'static> ViewContext<'a, 'w, S> {
|
||||
pub fn emit(&mut self, event: S::Event) {
|
||||
self.window_cx.app.push_effect(Effect::Emit {
|
||||
emitter: self.entity_id,
|
||||
event: Box::new(event),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'w, S> Context for ViewContext<'a, 'w, S> {
|
||||
type EntityContext<'b, 'c, U: 'static + Send + Sync> = ViewContext<'b, 'c, U>;
|
||||
type Result<U> = U;
|
||||
@@ -1012,3 +1140,21 @@ impl From<SmallVec<[u32; 16]>> for StackingOrder {
|
||||
StackingOrder(small_vec)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub enum ElementId {
|
||||
View(EntityId),
|
||||
Number(usize),
|
||||
}
|
||||
|
||||
impl From<usize> for ElementId {
|
||||
fn from(id: usize) -> Self {
|
||||
ElementId::Number(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for ElementId {
|
||||
fn from(id: i32) -> Self {
|
||||
Self::Number(id as usize)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ test-support = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
client = { path = "../client" }
|
||||
clock = { path = "../clock" }
|
||||
collections = { path = "../collections" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
|
||||
@@ -91,9 +91,8 @@ impl TestServer {
|
||||
let identity = claims.sub.unwrap().to_string();
|
||||
let room_name = claims.video.room.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))?;
|
||||
let room = (*server_rooms).entry(room_name.to_string()).or_default();
|
||||
|
||||
if room.client_rooms.contains_key(&identity) {
|
||||
Err(anyhow!(
|
||||
"{:?} attempted to join room {:?} twice",
|
||||
|
||||
@@ -4310,7 +4310,7 @@ impl<'a> From<&'a Entry> for proto::Entry {
|
||||
is_symlink: entry.is_symlink,
|
||||
is_ignored: entry.is_ignored,
|
||||
is_external: entry.is_external,
|
||||
git_status: entry.git_status.map(|status| status.to_proto()),
|
||||
git_status: entry.git_status.map(git_status_to_proto),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4337,7 +4337,7 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
|
||||
is_symlink: entry.is_symlink,
|
||||
is_ignored: entry.is_ignored,
|
||||
is_external: entry.is_external,
|
||||
git_status: GitFileStatus::from_proto(entry.git_status),
|
||||
git_status: git_status_from_proto(entry.git_status),
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
@@ -4366,3 +4366,21 @@ fn combine_git_statuses(
|
||||
unstaged
|
||||
}
|
||||
}
|
||||
|
||||
fn git_status_from_proto(git_status: Option<i32>) -> Option<GitFileStatus> {
|
||||
git_status.and_then(|status| {
|
||||
proto::GitStatus::from_i32(status).map(|status| match status {
|
||||
proto::GitStatus::Added => GitFileStatus::Added,
|
||||
proto::GitStatus::Modified => GitFileStatus::Modified,
|
||||
proto::GitStatus::Conflict => GitFileStatus::Conflict,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn git_status_to_proto(status: GitFileStatus) -> i32 {
|
||||
match status {
|
||||
GitFileStatus::Added => proto::GitStatus::Added as i32,
|
||||
GitFileStatus::Modified => proto::GitStatus::Modified as i32,
|
||||
GitFileStatus::Conflict => proto::GitStatus::Conflict as i32,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ pub trait Refineable: Clone {
|
||||
{
|
||||
Self::default().refined(refinement)
|
||||
}
|
||||
fn from_cascade(cascade: &RefinementCascade<Self>) -> Self
|
||||
fn from_cascade(cascade: &Cascade<Self>) -> Self
|
||||
where
|
||||
Self: Default + Sized,
|
||||
{
|
||||
@@ -25,9 +25,9 @@ pub trait Refineable: Clone {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RefinementCascade<S: Refineable>(Vec<Option<S::Refinement>>);
|
||||
pub struct Cascade<S: Refineable>(Vec<Option<S::Refinement>>);
|
||||
|
||||
impl<S: Refineable + Default> Default for RefinementCascade<S> {
|
||||
impl<S: Refineable + Default> Default for Cascade<S> {
|
||||
fn default() -> Self {
|
||||
Self(vec![Some(Default::default())])
|
||||
}
|
||||
@@ -36,7 +36,7 @@ impl<S: Refineable + Default> Default for RefinementCascade<S> {
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct CascadeSlot(usize);
|
||||
|
||||
impl<S: Refineable + Default> RefinementCascade<S> {
|
||||
impl<S: Refineable + Default> Cascade<S> {
|
||||
pub fn reserve(&mut self) -> CascadeSlot {
|
||||
self.0.push(None);
|
||||
return CascadeSlot(self.0.len() - 1);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use crate::theme::{theme, Theme};
|
||||
use gpui3::{
|
||||
div, img, svg, view, AppContext, Context, Element, Interactive, IntoAnyElement, MouseButton,
|
||||
ParentElement, ScrollState, SharedString, StyleHelpers, Styled, View, ViewContext,
|
||||
WindowContext,
|
||||
div, svg, view, AppContext, Context, Element, ElementId, IntoAnyElement, ParentElement,
|
||||
ScrollState, SharedString, StyleHelpers, Styled, View, ViewContext, WindowContext,
|
||||
};
|
||||
use ui::{theme, Theme};
|
||||
|
||||
pub struct CollabPanel {
|
||||
scroll_state: ScrollState,
|
||||
@@ -22,7 +21,7 @@ impl CollabPanel {
|
||||
}
|
||||
|
||||
impl CollabPanel {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element<State = Self> {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element<ViewState = Self> {
|
||||
let theme = theme(cx);
|
||||
|
||||
// Panel
|
||||
@@ -43,60 +42,61 @@ impl CollabPanel {
|
||||
.flex_col()
|
||||
.overflow_y_scroll(self.scroll_state.clone())
|
||||
// List Container
|
||||
// .child(
|
||||
// div()
|
||||
// .id(0)
|
||||
// .on_click(|_, _, _| {
|
||||
// dbg!("click!");
|
||||
// })
|
||||
// .fill(theme.lowest.base.default.background)
|
||||
// .pb_1()
|
||||
// .border_color(theme.lowest.base.default.border)
|
||||
// .border_b()
|
||||
// //:: https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state
|
||||
// // .group()
|
||||
// // List Section Header
|
||||
// .child(self.list_section_header(0, "#CRDB 🗃️", true, theme))
|
||||
// // List Item Large
|
||||
// .child(self.list_item(
|
||||
// "http://github.com/maxbrunsfeld.png?s=50",
|
||||
// "maxbrunsfeld",
|
||||
// theme,
|
||||
// )),
|
||||
// )
|
||||
.child(
|
||||
div()
|
||||
.on_click(MouseButton::Left, |_, _, _| {
|
||||
dbg!("click!");
|
||||
})
|
||||
.fill(theme.lowest.base.default.background)
|
||||
.pb_1()
|
||||
.border_color(theme.lowest.base.default.border)
|
||||
.border_b()
|
||||
//:: https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state
|
||||
// .group()
|
||||
// List Section Header
|
||||
.child(self.list_section_header("#CRDB 🗃️", true, &theme))
|
||||
// List Item Large
|
||||
.child(self.list_item(
|
||||
"http://github.com/maxbrunsfeld.png?s=50",
|
||||
"maxbrunsfeld",
|
||||
&theme,
|
||||
)),
|
||||
.py_2()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(self.list_section_header(1, "CHANNELS", true, theme)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.py_2()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(self.list_section_header("CHANNELS", true, &theme)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.py_2()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(self.list_section_header("CONTACTS", true, &theme))
|
||||
.child(self.list_section_header(2, "CONTACTS", true, theme))
|
||||
.children(
|
||||
std::iter::repeat_with(|| {
|
||||
vec![
|
||||
self.list_item(
|
||||
"http://github.com/as-cii.png?s=50",
|
||||
"as-cii",
|
||||
&theme,
|
||||
),
|
||||
self.list_item(
|
||||
"http://github.com/nathansobo.png?s=50",
|
||||
"nathansobo",
|
||||
&theme,
|
||||
),
|
||||
self.list_item(
|
||||
"http://github.com/maxbrunsfeld.png?s=50",
|
||||
"maxbrunsfeld",
|
||||
&theme,
|
||||
theme,
|
||||
),
|
||||
// self.list_item(
|
||||
// "http://github.com/nathansobo.png?s=50",
|
||||
// "nathansobo",
|
||||
// theme,
|
||||
// ),
|
||||
// self.list_item(
|
||||
// "http://github.com/maxbrunsfeld.png?s=50",
|
||||
// "maxbrunsfeld",
|
||||
// theme,
|
||||
// ),
|
||||
]
|
||||
})
|
||||
.take(5)
|
||||
.take(1)
|
||||
.flatten(),
|
||||
),
|
||||
),
|
||||
@@ -120,18 +120,20 @@ impl CollabPanel {
|
||||
|
||||
fn list_section_header(
|
||||
&self,
|
||||
id: impl Into<ElementId>,
|
||||
label: impl IntoAnyElement<Self>,
|
||||
expanded: bool,
|
||||
theme: &Theme,
|
||||
) -> impl Element<State = Self> {
|
||||
) -> impl Element<ViewState = Self> {
|
||||
div()
|
||||
.id(id)
|
||||
.h_7()
|
||||
.px_2()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.hover()
|
||||
.fill(theme.lowest.base.active.background)
|
||||
.active()
|
||||
.fill(theme.highest.accent.default.background)
|
||||
.child(div().flex().gap_1().text_sm().child(label))
|
||||
.child(
|
||||
div().flex().h_full().gap_1().items_center().child(
|
||||
@@ -153,14 +155,15 @@ impl CollabPanel {
|
||||
avatar_uri: impl Into<SharedString>,
|
||||
label: impl IntoAnyElement<Self>,
|
||||
theme: &Theme,
|
||||
) -> impl Element<State = Self> {
|
||||
) -> impl Element<ViewState = Self> {
|
||||
div()
|
||||
.group("")
|
||||
.h_7()
|
||||
.px_2()
|
||||
.flex()
|
||||
.items_center()
|
||||
.hover()
|
||||
.fill(theme.lowest.variant.hovered.background)
|
||||
// .hover()
|
||||
// .fill(theme.lowest.variant.hovered.background)
|
||||
// .active()
|
||||
// .fill(theme.lowest.variant.pressed.background)
|
||||
.child(
|
||||
@@ -170,12 +173,19 @@ impl CollabPanel {
|
||||
.gap_1()
|
||||
.text_sm()
|
||||
.child(
|
||||
img()
|
||||
.uri(avatar_uri)
|
||||
div()
|
||||
.id(0)
|
||||
// .uri(avatar_uri)
|
||||
.size_3p5()
|
||||
.rounded_full()
|
||||
.fill(theme.middle.positive.default.foreground)
|
||||
.shadow_md(),
|
||||
.shadow()
|
||||
.group_hover("")
|
||||
.fill(theme.middle.negative.default.foreground)
|
||||
.hover()
|
||||
.fill(theme.middle.warning.default.foreground)
|
||||
.group_active("")
|
||||
.fill(theme.middle.accent.default.foreground),
|
||||
)
|
||||
.child(label),
|
||||
)
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
use gpui3::{
|
||||
div, img, svg, view, Context, Element, ParentElement, RootView, StyleHelpers, View,
|
||||
ViewContext, WindowContext,
|
||||
};
|
||||
use ui::prelude::*;
|
||||
use ui::{themed, Panel, Stack};
|
||||
|
||||
use crate::{
|
||||
collab_panel::{collab_panel, CollabPanel},
|
||||
themes::rose_pine_dawn,
|
||||
theme::{theme, themed},
|
||||
themes::rose_pine,
|
||||
};
|
||||
use gpui3::{
|
||||
div, img, svg, view, Context, Element, ParentElement, RootView, StyleHelpers, Styled, View,
|
||||
ViewContext, WindowContext,
|
||||
};
|
||||
|
||||
pub struct Workspace {
|
||||
@@ -27,78 +25,64 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element<State = Self> {
|
||||
themed(rose_pine_dawn(), cx, |cx| {
|
||||
let theme = theme(cx);
|
||||
fn hover_test(&self, cx: &mut ViewContext<Self>) -> impl Element<ViewState = Self> {
|
||||
let theme = theme(cx);
|
||||
|
||||
div().size_full().child(
|
||||
div()
|
||||
.group("")
|
||||
.w_full()
|
||||
.h_5()
|
||||
.mt_10()
|
||||
.fill(theme.middle.warning.default.foreground)
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_center()
|
||||
.child(
|
||||
div()
|
||||
.size_5()
|
||||
.fill(theme.middle.negative.default.foreground)
|
||||
.group_hover("")
|
||||
.fill(theme.middle.positive.default.foreground)
|
||||
.hover()
|
||||
.fill(theme.middle.variant.default.foreground),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element<ViewState = Self> {
|
||||
themed(rose_pine(), cx, |cx| {
|
||||
// self.hover_test(cx)
|
||||
let theme = theme(cx);
|
||||
div()
|
||||
.size_full()
|
||||
.v_stack()
|
||||
.fill(theme.lowest.base.default.background)
|
||||
.child(Panel::new(
|
||||
ScrollState::default(),
|
||||
|_, _| {
|
||||
vec![div()
|
||||
.font("Courier")
|
||||
.text_color(gpui3::hsla(1., 1., 1., 1.))
|
||||
.child("Hello world")
|
||||
.into_any()]
|
||||
},
|
||||
Box::new(()),
|
||||
))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.font("Courier")
|
||||
.gap_0()
|
||||
.justify_start()
|
||||
.items_start()
|
||||
.text_color(theme.lowest.base.default.foreground)
|
||||
.fill(theme.middle.base.default.background)
|
||||
.child(titlebar(cx))
|
||||
.child(
|
||||
div()
|
||||
.size_full()
|
||||
.h_stack()
|
||||
.gap_3()
|
||||
.children((0..4).map(|i| {
|
||||
div().size_full().flex().fill(gpui3::hsla(
|
||||
0. + (i as f32 / 7.),
|
||||
0. + (i as f32 / 5.),
|
||||
0.5,
|
||||
1.,
|
||||
))
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.size_full()
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.flex()
|
||||
.fill(theme.middle.negative.default.background),
|
||||
.flex_row()
|
||||
.overflow_hidden()
|
||||
.child(self.left_panel.clone())
|
||||
.child(div().h_full().flex_1()), // .child(self.right_panel.clone()),
|
||||
)
|
||||
.child(statusbar::statusbar(cx))
|
||||
})
|
||||
|
||||
// themed(rose_pine_dawn(), cx, |cx| {
|
||||
// div()
|
||||
// .size_full()
|
||||
// .flex()
|
||||
// .flex_col()
|
||||
// .font("Courier")
|
||||
// .gap_0()
|
||||
// .justify_start()
|
||||
// .items_start()
|
||||
// .text_color(theme.lowest.base.default.foreground)
|
||||
// .fill(theme.middle.base.default.background)
|
||||
// .child(titlebar(cx))
|
||||
// .child(
|
||||
// div()
|
||||
// .flex_1()
|
||||
// .w_full()
|
||||
// .flex()
|
||||
// .flex_row()
|
||||
// .overflow_hidden()
|
||||
// .child(self.left_panel.clone())
|
||||
// .child(div().h_full().flex_1())
|
||||
// .child(self.right_panel.clone()),
|
||||
// )
|
||||
// .child(statusbar::statusbar(cx))
|
||||
// })
|
||||
}
|
||||
}
|
||||
|
||||
struct Titlebar;
|
||||
|
||||
pub fn titlebar<S: 'static + Send + Sync>(cx: &mut ViewContext<S>) -> impl Element<State = S> {
|
||||
pub fn titlebar<S: 'static + Send + Sync>(cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
|
||||
let ref mut this = Titlebar;
|
||||
let theme = theme(cx);
|
||||
div()
|
||||
@@ -116,7 +100,7 @@ impl Titlebar {
|
||||
fn render<V: 'static + Send + Sync>(
|
||||
&mut self,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> impl Element<State = V> {
|
||||
) -> impl Element<ViewState = V> {
|
||||
let theme = theme(cx);
|
||||
div()
|
||||
.flex()
|
||||
@@ -132,7 +116,7 @@ impl Titlebar {
|
||||
fn left_group<S: 'static + Send + Sync>(
|
||||
&mut self,
|
||||
cx: &mut ViewContext<S>,
|
||||
) -> impl Element<State = S> {
|
||||
) -> impl Element<ViewState = S> {
|
||||
let theme = theme(cx);
|
||||
div()
|
||||
.flex()
|
||||
@@ -182,8 +166,8 @@ impl Titlebar {
|
||||
.justify_center()
|
||||
.px_2()
|
||||
.rounded_md()
|
||||
.hover()
|
||||
.fill(theme.lowest.base.hovered.background)
|
||||
// .hover()
|
||||
// .fill(theme.lowest.base.hovered.background)
|
||||
// .active()
|
||||
// .fill(theme.lowest.base.pressed.background)
|
||||
.child(div().text_sm().child("project")),
|
||||
@@ -197,8 +181,8 @@ impl Titlebar {
|
||||
.px_2()
|
||||
.rounded_md()
|
||||
.text_color(theme.lowest.variant.default.foreground)
|
||||
.hover()
|
||||
.fill(theme.lowest.base.hovered.background)
|
||||
// .hover()
|
||||
// .fill(theme.lowest.base.hovered.background)
|
||||
// .active()
|
||||
// .fill(theme.lowest.base.pressed.background)
|
||||
.child(
|
||||
@@ -215,7 +199,7 @@ impl Titlebar {
|
||||
fn right_group<S: 'static + Send + Sync>(
|
||||
&mut self,
|
||||
cx: &mut ViewContext<S>,
|
||||
) -> impl Element<State = S> {
|
||||
) -> impl Element<ViewState = S> {
|
||||
let theme = theme(cx);
|
||||
div()
|
||||
.flex()
|
||||
@@ -253,8 +237,8 @@ impl Titlebar {
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.hover()
|
||||
.fill(theme.lowest.base.hovered.background)
|
||||
// .hover()
|
||||
// .fill(theme.lowest.base.hovered.background)
|
||||
// .active()
|
||||
// .fill(theme.lowest.base.pressed.background)
|
||||
.child(
|
||||
@@ -273,8 +257,8 @@ impl Titlebar {
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.hover()
|
||||
.fill(theme.lowest.base.hovered.background)
|
||||
// .hover()
|
||||
// .fill(theme.lowest.base.hovered.background)
|
||||
// .active()
|
||||
// .fill(theme.lowest.base.pressed.background)
|
||||
.child(
|
||||
@@ -293,8 +277,8 @@ impl Titlebar {
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.hover()
|
||||
.fill(theme.lowest.base.hovered.background)
|
||||
// .hover()
|
||||
// .fill(theme.lowest.base.hovered.background)
|
||||
// .active()
|
||||
// .fill(theme.lowest.base.pressed.background)
|
||||
.child(
|
||||
@@ -318,8 +302,8 @@ impl Titlebar {
|
||||
.justify_center()
|
||||
.rounded_md()
|
||||
.gap_0p5()
|
||||
.hover()
|
||||
.fill(theme.lowest.base.hovered.background)
|
||||
// .hover()
|
||||
// .fill(theme.lowest.base.hovered.background)
|
||||
// .active()
|
||||
// .fill(theme.lowest.base.pressed.background)
|
||||
.child(
|
||||
@@ -347,7 +331,9 @@ mod statusbar {
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn statusbar<S: 'static + Send + Sync>(cx: &mut ViewContext<S>) -> impl Element<State = S> {
|
||||
pub fn statusbar<S: 'static + Send + Sync>(
|
||||
cx: &mut ViewContext<S>,
|
||||
) -> impl Element<ViewState = S> {
|
||||
let theme = theme(cx);
|
||||
div()
|
||||
.flex()
|
||||
@@ -360,7 +346,9 @@ mod statusbar {
|
||||
// .child(right_group(cx))
|
||||
}
|
||||
|
||||
fn left_group<V: 'static + Send + Sync>(cx: &mut ViewContext<V>) -> impl Element<State = V> {
|
||||
fn left_group<V: 'static + Send + Sync>(
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> impl Element<ViewState = V> {
|
||||
let theme = theme(cx);
|
||||
div()
|
||||
.flex()
|
||||
@@ -435,8 +423,8 @@ mod statusbar {
|
||||
.gap_0p5()
|
||||
.px_1()
|
||||
.text_color(theme.lowest.variant.default.foreground)
|
||||
.hover()
|
||||
.fill(theme.lowest.base.hovered.background)
|
||||
// .hover()
|
||||
// .fill(theme.lowest.base.hovered.background)
|
||||
// .active()
|
||||
// .fill(theme.lowest.base.pressed.background)
|
||||
.child(
|
||||
@@ -457,7 +445,9 @@ mod statusbar {
|
||||
)
|
||||
}
|
||||
|
||||
fn right_group<S: 'static + Send + Sync>(cx: &mut ViewContext<S>) -> impl Element<State = S> {
|
||||
fn right_group<S: 'static + Send + Sync>(
|
||||
cx: &mut ViewContext<S>,
|
||||
) -> impl Element<ViewState = S> {
|
||||
let theme = theme(cx);
|
||||
div()
|
||||
.flex()
|
||||
|
||||
+24
-18
@@ -132,12 +132,12 @@ where
|
||||
deserializer.deserialize_map(SyntaxVisitor)
|
||||
}
|
||||
|
||||
pub fn themed<E, F>(theme: Theme, cx: &mut ViewContext<E::State>, build_child: F) -> Themed<E>
|
||||
pub fn themed<E, F>(theme: Theme, cx: &mut ViewContext<E::ViewState>, build_child: F) -> Themed<E>
|
||||
where
|
||||
E: Element,
|
||||
F: FnOnce(&mut ViewContext<E::State>) -> E,
|
||||
F: FnOnce(&mut ViewContext<E::ViewState>) -> E,
|
||||
{
|
||||
let child = cx.with_state(theme.clone(), |cx| build_child(cx));
|
||||
let child = cx.with_global(theme.clone(), |cx| build_child(cx));
|
||||
Themed { theme, child }
|
||||
}
|
||||
|
||||
@@ -147,36 +147,42 @@ pub struct Themed<E> {
|
||||
}
|
||||
|
||||
impl<E: Element> Element for Themed<E> {
|
||||
type State = E::State;
|
||||
type FrameState = E::FrameState;
|
||||
type ViewState = E::ViewState;
|
||||
type ElementState = E::ElementState;
|
||||
|
||||
fn element_id(&self) -> Option<gpui3::ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
state: &mut E::State,
|
||||
cx: &mut ViewContext<E::State>,
|
||||
) -> anyhow::Result<(LayoutId, Self::FrameState)>
|
||||
state: &mut E::ViewState,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<E::ViewState>,
|
||||
) -> (LayoutId, Self::ElementState)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
cx.with_state(self.theme.clone(), |cx| self.child.layout(state, cx))
|
||||
cx.with_global(self.theme.clone(), |cx| {
|
||||
self.child.layout(state, element_state, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
state: &mut Self::State,
|
||||
frame_state: &mut Self::FrameState,
|
||||
cx: &mut ViewContext<Self::State>,
|
||||
) -> Result<()>
|
||||
where
|
||||
state: &mut Self::ViewState,
|
||||
frame_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<Self::ViewState>,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
cx.with_state(self.theme.clone(), |cx| {
|
||||
self.child.paint(bounds, state, frame_state, cx)
|
||||
})
|
||||
cx.with_global(self.theme.clone(), |cx| {
|
||||
self.child.paint(bounds, state, frame_state, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn theme(cx: &WindowContext) -> Arc<Theme> {
|
||||
Arc::new(cx.state::<Theme>().clone())
|
||||
Arc::new(cx.global::<Theme>().clone())
|
||||
}
|
||||
|
||||
@@ -57,6 +57,18 @@ impl<'a> From<Cow<'a, str>> for ArcCow<'a, str> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Vec<T>> for ArcCow<'_, [T]> {
|
||||
fn from(vec: Vec<T>) -> Self {
|
||||
ArcCow::Owned(Arc::from(vec))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for ArcCow<'a, [u8]> {
|
||||
fn from(s: &'a str) -> Self {
|
||||
ArcCow::Borrowed(s.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: ?Sized + ToOwned> std::borrow::Borrow<T> for ArcCow<'a, T> {
|
||||
fn borrow(&self) -> &T {
|
||||
match self {
|
||||
|
||||
@@ -41,4 +41,36 @@ impl ReleaseChannel {
|
||||
ReleaseChannel::Stable => "stable",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn url_scheme(&self) -> &'static str {
|
||||
match self {
|
||||
ReleaseChannel::Dev => "zed-dev://",
|
||||
ReleaseChannel::Preview => "zed-preview://",
|
||||
ReleaseChannel::Stable => "zed://",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn link_prefix(&self) -> &'static str {
|
||||
match self {
|
||||
ReleaseChannel::Dev => "https://zed.dev/dev/",
|
||||
ReleaseChannel::Preview => "https://zed.dev/preview/",
|
||||
ReleaseChannel::Stable => "https://zed.dev/",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_zed_link(link: &str) -> Option<&str> {
|
||||
for release in [
|
||||
ReleaseChannel::Dev,
|
||||
ReleaseChannel::Preview,
|
||||
ReleaseChannel::Stable,
|
||||
] {
|
||||
if let Some(stripped) = link.strip_prefix(release.link_prefix()) {
|
||||
return Some(stripped);
|
||||
}
|
||||
if let Some(stripped) = link.strip_prefix(release.url_scheme()) {
|
||||
return Some(stripped);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -139,6 +139,12 @@ impl<P> PathLikeWithPosition<P> {
|
||||
column: None,
|
||||
})
|
||||
} else {
|
||||
let maybe_col_str =
|
||||
if maybe_col_str.ends_with(FILE_ROW_COLUMN_DELIMITER) {
|
||||
&maybe_col_str[..maybe_col_str.len() - 1]
|
||||
} else {
|
||||
maybe_col_str
|
||||
};
|
||||
match maybe_col_str.parse::<u32>() {
|
||||
Ok(col) => Ok(Self {
|
||||
path_like: parse_path_like_str(path_like_str)?,
|
||||
@@ -241,7 +247,6 @@ mod tests {
|
||||
"test_file.rs:1::",
|
||||
"test_file.rs::1:2",
|
||||
"test_file.rs:1::2",
|
||||
"test_file.rs:1:2:",
|
||||
"test_file.rs:1:2:3",
|
||||
] {
|
||||
let actual = parse_str(input);
|
||||
@@ -277,6 +282,14 @@ mod tests {
|
||||
column: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"crates/file_finder/src/file_finder.rs:1902:13:",
|
||||
PathLikeWithPosition {
|
||||
path_like: "crates/file_finder/src/file_finder.rs".to_string(),
|
||||
row: Some(1902),
|
||||
column: Some(13),
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
for (input, expected) in input_and_expected {
|
||||
|
||||
@@ -46,6 +46,7 @@ actions!(
|
||||
ChangeToEndOfLine,
|
||||
DeleteToEndOfLine,
|
||||
Yank,
|
||||
YankLine,
|
||||
ChangeCase,
|
||||
JoinLines,
|
||||
]
|
||||
@@ -66,6 +67,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(insert_line_above);
|
||||
cx.add_action(insert_line_below);
|
||||
cx.add_action(change_case);
|
||||
cx.add_action(yank_line);
|
||||
|
||||
cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
@@ -308,6 +310,13 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
|
||||
});
|
||||
}
|
||||
|
||||
fn yank_line(_: &mut Workspace, _: &YankLine, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
let count = vim.take_count(cx);
|
||||
yank_motion(vim, motion::Motion::CurrentLine, count, cx)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.stop_recording();
|
||||
|
||||
@@ -652,3 +652,28 @@ async fn test_selection_goal(cx: &mut gpui::TestAppContext) {
|
||||
Lorem Ipsum"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_paragraphs_dont_wrap(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
one
|
||||
ˇ
|
||||
two"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["}", "}"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
one
|
||||
|
||||
twˇo"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["{", "{", "{"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
ˇone
|
||||
|
||||
two"})
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ use workspace::{self, Workspace};
|
||||
|
||||
use crate::state::ReplayableAction;
|
||||
|
||||
struct VimModeSetting(bool);
|
||||
pub struct VimModeSetting(pub bool);
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
pub struct SwitchMode(pub Mode);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{"Put":{"state":"one\nˇ\ntwo"}}
|
||||
{"Key":"}"}
|
||||
{"Key":"}"}
|
||||
{"Get":{"state":"one\n\ntwˇo","mode":"Normal"}}
|
||||
{"Key":"{"}
|
||||
{"Key":"{"}
|
||||
{"Key":"{"}
|
||||
{"Get":{"state":"ˇone\n\ntwo","mode":"Normal"}}
|
||||
@@ -25,6 +25,7 @@ theme_selector = { path = "../theme_selector" }
|
||||
util = { path = "../util" }
|
||||
picker = { path = "../picker" }
|
||||
workspace = { path = "../workspace" }
|
||||
vim = { path = "../vim" }
|
||||
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
@@ -10,6 +10,7 @@ use gpui::{
|
||||
};
|
||||
use settings::{update_settings_file, SettingsStore};
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
use vim::VimModeSetting;
|
||||
use workspace::{
|
||||
dock::DockPosition, item::Item, open_new, AppState, PaneBackdrop, Welcome, Workspace,
|
||||
WorkspaceId,
|
||||
@@ -65,6 +66,7 @@ impl View for WelcomePage {
|
||||
let width = theme.welcome.page_width;
|
||||
|
||||
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
|
||||
let vim_mode_setting = settings::get::<VimModeSetting>(cx).0;
|
||||
|
||||
enum Metrics {}
|
||||
enum Diagnostics {}
|
||||
@@ -144,6 +146,27 @@ impl View for WelcomePage {
|
||||
)
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_child(
|
||||
theme::ui::checkbox::<Diagnostics, Self, _>(
|
||||
"Enable vim mode",
|
||||
&theme.welcome.checkbox,
|
||||
vim_mode_setting,
|
||||
0,
|
||||
cx,
|
||||
|this, checked, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
let fs = workspace.read(cx).app_state().fs.clone();
|
||||
update_settings_file::<VimModeSetting>(
|
||||
fs,
|
||||
cx,
|
||||
move |setting| *setting = Some(checked),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.welcome.checkbox_container),
|
||||
)
|
||||
.with_child(
|
||||
theme::ui::checkbox_with_label::<Metrics, _, Self, _>(
|
||||
Flex::column()
|
||||
@@ -186,7 +209,7 @@ impl View for WelcomePage {
|
||||
"Send crash reports",
|
||||
&theme.welcome.checkbox,
|
||||
telemetry_settings.diagnostics,
|
||||
0,
|
||||
1,
|
||||
cx,
|
||||
|this, checked, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
|
||||
@@ -22,7 +22,6 @@ test-support = [
|
||||
db = { path = "../db" }
|
||||
call = { path = "../call" }
|
||||
client = { path = "../client" }
|
||||
channel = { path = "../channel" }
|
||||
collections = { path = "../collections" }
|
||||
context_menu = { path = "../context_menu" }
|
||||
drag_and_drop = { path = "../drag_and_drop" }
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
use std::{cell::RefCell, rc::Rc, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
pane_group::element::PaneAxisElement, AppState, FollowerStatesByLeader, Pane, Workspace,
|
||||
};
|
||||
use crate::{pane_group::element::PaneAxisElement, AppState, FollowerState, Pane, Workspace};
|
||||
use anyhow::{anyhow, Result};
|
||||
use call::{ActiveCall, ParticipantLocation};
|
||||
use collections::HashMap;
|
||||
use gpui::{
|
||||
elements::*,
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
@@ -13,6 +10,7 @@ use gpui::{
|
||||
};
|
||||
use project::Project;
|
||||
use serde::Deserialize;
|
||||
use std::{cell::RefCell, rc::Rc, sync::Arc};
|
||||
use theme::Theme;
|
||||
|
||||
const HANDLE_HITBOX_SIZE: f32 = 4.0;
|
||||
@@ -95,7 +93,7 @@ impl PaneGroup {
|
||||
&self,
|
||||
project: &ModelHandle<Project>,
|
||||
theme: &Theme,
|
||||
follower_states: &FollowerStatesByLeader,
|
||||
follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
|
||||
active_call: Option<&ModelHandle<ActiveCall>>,
|
||||
active_pane: &ViewHandle<Pane>,
|
||||
zoomed: Option<&AnyViewHandle>,
|
||||
@@ -162,7 +160,7 @@ impl Member {
|
||||
project: &ModelHandle<Project>,
|
||||
basis: usize,
|
||||
theme: &Theme,
|
||||
follower_states: &FollowerStatesByLeader,
|
||||
follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
|
||||
active_call: Option<&ModelHandle<ActiveCall>>,
|
||||
active_pane: &ViewHandle<Pane>,
|
||||
zoomed: Option<&AnyViewHandle>,
|
||||
@@ -179,19 +177,10 @@ impl Member {
|
||||
ChildView::new(pane, cx).into_any()
|
||||
};
|
||||
|
||||
let leader = follower_states
|
||||
.iter()
|
||||
.find_map(|(leader_id, follower_states)| {
|
||||
if follower_states.contains_key(pane) {
|
||||
Some(leader_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.and_then(|leader_id| {
|
||||
let room = active_call?.read(cx).room()?.read(cx);
|
||||
room.remote_participant_for_peer_id(*leader_id)
|
||||
});
|
||||
let leader = follower_states.get(pane).and_then(|state| {
|
||||
let room = active_call?.read(cx).room()?.read(cx);
|
||||
room.remote_participant_for_peer_id(state.leader_id)
|
||||
});
|
||||
|
||||
let mut leader_border = Border::default();
|
||||
let mut leader_status_box = None;
|
||||
@@ -486,7 +475,7 @@ impl PaneAxis {
|
||||
project: &ModelHandle<Project>,
|
||||
basis: usize,
|
||||
theme: &Theme,
|
||||
follower_state: &FollowerStatesByLeader,
|
||||
follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
|
||||
active_call: Option<&ModelHandle<ActiveCall>>,
|
||||
active_pane: &ViewHandle<Pane>,
|
||||
zoomed: Option<&AnyViewHandle>,
|
||||
@@ -515,7 +504,7 @@ impl PaneAxis {
|
||||
project,
|
||||
(basis + ix) * 10,
|
||||
theme,
|
||||
follower_state,
|
||||
follower_states,
|
||||
active_call,
|
||||
active_pane,
|
||||
zoomed,
|
||||
|
||||
@@ -12,10 +12,9 @@ mod workspace_settings;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use call::ActiveCall;
|
||||
use channel::ChannelStore;
|
||||
use client::{
|
||||
proto::{self, PeerId},
|
||||
Client, TypedEnvelope, UserStore,
|
||||
Client, Status, TypedEnvelope, UserStore,
|
||||
};
|
||||
use collections::{hash_map, HashMap, HashSet};
|
||||
use drag_and_drop::DragAndDrop;
|
||||
@@ -36,9 +35,9 @@ use gpui::{
|
||||
CursorStyle, ModifiersChangedEvent, MouseButton, PathPromptOptions, Platform, PromptLevel,
|
||||
WindowBounds, WindowOptions,
|
||||
},
|
||||
AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity,
|
||||
ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle,
|
||||
WeakViewHandle, WindowContext, WindowHandle,
|
||||
AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AnyWindowHandle, AppContext, AsyncAppContext,
|
||||
Entity, ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext,
|
||||
ViewHandle, WeakViewHandle, WindowContext, WindowHandle,
|
||||
};
|
||||
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
|
||||
use itertools::Itertools;
|
||||
@@ -450,7 +449,6 @@ pub struct AppState {
|
||||
pub languages: Arc<LanguageRegistry>,
|
||||
pub client: Arc<Client>,
|
||||
pub user_store: ModelHandle<UserStore>,
|
||||
pub channel_store: ModelHandle<ChannelStore>,
|
||||
pub workspace_store: ModelHandle<WorkspaceStore>,
|
||||
pub fs: Arc<dyn fs::Fs>,
|
||||
pub build_window_options:
|
||||
@@ -487,8 +485,6 @@ impl AppState {
|
||||
let http_client = util::http::FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http_client.clone(), cx);
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
||||
let channel_store =
|
||||
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
|
||||
let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
|
||||
|
||||
theme::init((), cx);
|
||||
@@ -500,7 +496,7 @@ impl AppState {
|
||||
fs,
|
||||
languages,
|
||||
user_store,
|
||||
channel_store,
|
||||
// channel_store,
|
||||
workspace_store,
|
||||
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
|
||||
build_window_options: |_, _, _| Default::default(),
|
||||
@@ -578,7 +574,7 @@ pub struct Workspace {
|
||||
titlebar_item: Option<AnyViewHandle>,
|
||||
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
|
||||
project: ModelHandle<Project>,
|
||||
follower_states_by_leader: FollowerStatesByLeader,
|
||||
follower_states: HashMap<ViewHandle<Pane>, FollowerState>,
|
||||
last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
|
||||
window_edited: bool,
|
||||
active_call: Option<(ModelHandle<ActiveCall>, Vec<Subscription>)>,
|
||||
@@ -603,10 +599,9 @@ pub struct ViewId {
|
||||
pub id: u64,
|
||||
}
|
||||
|
||||
type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
|
||||
|
||||
#[derive(Default)]
|
||||
struct FollowerState {
|
||||
leader_id: PeerId,
|
||||
active_view_id: Option<ViewId>,
|
||||
items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
|
||||
}
|
||||
@@ -795,7 +790,7 @@ impl Workspace {
|
||||
bottom_dock,
|
||||
right_dock,
|
||||
project: project.clone(),
|
||||
follower_states_by_leader: Default::default(),
|
||||
follower_states: Default::default(),
|
||||
last_leaders_by_pane: Default::default(),
|
||||
window_edited: false,
|
||||
active_call,
|
||||
@@ -2513,13 +2508,16 @@ impl Workspace {
|
||||
}
|
||||
|
||||
fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
|
||||
if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
|
||||
for state in states_by_pane.into_values() {
|
||||
for item in state.items_by_leader_view_id.into_values() {
|
||||
self.follower_states.retain(|_, state| {
|
||||
if state.leader_id == peer_id {
|
||||
for item in state.items_by_leader_view_id.values() {
|
||||
item.set_leader_peer_id(None, cx);
|
||||
}
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -2532,10 +2530,15 @@ impl Workspace {
|
||||
|
||||
self.last_leaders_by_pane
|
||||
.insert(pane.downgrade(), leader_id);
|
||||
self.follower_states_by_leader
|
||||
.entry(leader_id)
|
||||
.or_default()
|
||||
.insert(pane.clone(), Default::default());
|
||||
self.unfollow(&pane, cx);
|
||||
self.follower_states.insert(
|
||||
pane.clone(),
|
||||
FollowerState {
|
||||
leader_id,
|
||||
active_view_id: None,
|
||||
items_by_leader_view_id: Default::default(),
|
||||
},
|
||||
);
|
||||
cx.notify();
|
||||
|
||||
let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
|
||||
@@ -2550,9 +2553,8 @@ impl Workspace {
|
||||
let response = request.await?;
|
||||
this.update(&mut cx, |this, _| {
|
||||
let state = this
|
||||
.follower_states_by_leader
|
||||
.get_mut(&leader_id)
|
||||
.and_then(|states_by_pane| states_by_pane.get_mut(&pane))
|
||||
.follower_states
|
||||
.get_mut(&pane)
|
||||
.ok_or_else(|| anyhow!("following interrupted"))?;
|
||||
state.active_view_id = if let Some(active_view_id) = response.active_view_id {
|
||||
Some(ViewId::from_proto(active_view_id)?)
|
||||
@@ -2647,12 +2649,10 @@ impl Workspace {
|
||||
}
|
||||
|
||||
// if you're already following, find the right pane and focus it.
|
||||
for (existing_leader_id, states_by_pane) in &mut self.follower_states_by_leader {
|
||||
if leader_id == *existing_leader_id {
|
||||
for (pane, _) in states_by_pane {
|
||||
cx.focus(pane);
|
||||
return None;
|
||||
}
|
||||
for (pane, state) in &self.follower_states {
|
||||
if leader_id == state.leader_id {
|
||||
cx.focus(pane);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2665,36 +2665,37 @@ impl Workspace {
|
||||
pane: &ViewHandle<Pane>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<PeerId> {
|
||||
for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
|
||||
let leader_id = *leader_id;
|
||||
if let Some(state) = states_by_pane.remove(pane) {
|
||||
for (_, item) in state.items_by_leader_view_id {
|
||||
item.set_leader_peer_id(None, cx);
|
||||
}
|
||||
|
||||
if states_by_pane.is_empty() {
|
||||
self.follower_states_by_leader.remove(&leader_id);
|
||||
let project_id = self.project.read(cx).remote_id();
|
||||
let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
|
||||
self.app_state
|
||||
.client
|
||||
.send(proto::Unfollow {
|
||||
room_id,
|
||||
project_id,
|
||||
leader_id: Some(leader_id),
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
return Some(leader_id);
|
||||
}
|
||||
let state = self.follower_states.remove(pane)?;
|
||||
let leader_id = state.leader_id;
|
||||
for (_, item) in state.items_by_leader_view_id {
|
||||
item.set_leader_peer_id(None, cx);
|
||||
}
|
||||
None
|
||||
|
||||
if self
|
||||
.follower_states
|
||||
.values()
|
||||
.all(|state| state.leader_id != state.leader_id)
|
||||
{
|
||||
let project_id = self.project.read(cx).remote_id();
|
||||
let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
|
||||
self.app_state
|
||||
.client
|
||||
.send(proto::Unfollow {
|
||||
room_id,
|
||||
project_id,
|
||||
leader_id: Some(leader_id),
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
Some(leader_id)
|
||||
}
|
||||
|
||||
pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
|
||||
self.follower_states_by_leader.contains_key(&peer_id)
|
||||
self.follower_states
|
||||
.values()
|
||||
.any(|state| state.leader_id == peer_id)
|
||||
}
|
||||
|
||||
fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
@@ -2877,8 +2878,7 @@ impl Workspace {
|
||||
let cx = &cx;
|
||||
move |item| {
|
||||
let item = item.to_followable_item_handle(cx)?;
|
||||
if project_id.is_some()
|
||||
&& project_id != follower_project_id
|
||||
if (project_id.is_none() || project_id != follower_project_id)
|
||||
&& item.is_project_item(cx)
|
||||
{
|
||||
return None;
|
||||
@@ -2917,8 +2917,8 @@ impl Workspace {
|
||||
match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
|
||||
proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
|
||||
this.update(cx, |this, _| {
|
||||
if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
|
||||
for state in state.values_mut() {
|
||||
for (_, state) in &mut this.follower_states {
|
||||
if state.leader_id == leader_id {
|
||||
state.active_view_id =
|
||||
if let Some(active_view_id) = update_active_view.id.clone() {
|
||||
Some(ViewId::from_proto(active_view_id)?)
|
||||
@@ -2940,8 +2940,8 @@ impl Workspace {
|
||||
let mut tasks = Vec::new();
|
||||
this.update(cx, |this, cx| {
|
||||
let project = this.project.clone();
|
||||
if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
|
||||
for state in state.values_mut() {
|
||||
for (_, state) in &mut this.follower_states {
|
||||
if state.leader_id == leader_id {
|
||||
let view_id = ViewId::from_proto(id.clone())?;
|
||||
if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
|
||||
tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
|
||||
@@ -2954,10 +2954,9 @@ impl Workspace {
|
||||
}
|
||||
proto::update_followers::Variant::CreateView(view) => {
|
||||
let panes = this.read_with(cx, |this, _| {
|
||||
this.follower_states_by_leader
|
||||
.get(&leader_id)
|
||||
.into_iter()
|
||||
.flat_map(|states_by_pane| states_by_pane.keys())
|
||||
this.follower_states
|
||||
.iter()
|
||||
.filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane))
|
||||
.cloned()
|
||||
.collect()
|
||||
})?;
|
||||
@@ -3016,11 +3015,7 @@ impl Workspace {
|
||||
for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
|
||||
let items = futures::future::try_join_all(item_tasks).await?;
|
||||
this.update(cx, |this, cx| {
|
||||
let state = this
|
||||
.follower_states_by_leader
|
||||
.get_mut(&leader_id)?
|
||||
.get_mut(&pane)?;
|
||||
|
||||
let state = this.follower_states.get_mut(&pane)?;
|
||||
for (id, item) in leader_view_ids.into_iter().zip(items) {
|
||||
item.set_leader_peer_id(Some(leader_id), cx);
|
||||
state.items_by_leader_view_id.insert(id, item);
|
||||
@@ -3077,15 +3072,7 @@ impl Workspace {
|
||||
}
|
||||
|
||||
pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
|
||||
self.follower_states_by_leader
|
||||
.iter()
|
||||
.find_map(|(leader_id, state)| {
|
||||
if state.contains_key(pane) {
|
||||
Some(*leader_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
self.follower_states.get(pane).map(|state| state.leader_id)
|
||||
}
|
||||
|
||||
fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
|
||||
@@ -3113,17 +3100,23 @@ impl Workspace {
|
||||
}
|
||||
};
|
||||
|
||||
for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
|
||||
if leader_in_this_app {
|
||||
let item = state
|
||||
.active_view_id
|
||||
.and_then(|id| state.items_by_leader_view_id.get(&id));
|
||||
if let Some(item) = item {
|
||||
for (pane, state) in &self.follower_states {
|
||||
if state.leader_id != leader_id {
|
||||
continue;
|
||||
}
|
||||
if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
|
||||
if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
|
||||
if leader_in_this_project || !item.is_project_item(cx) {
|
||||
items_to_activate.push((pane.clone(), item.boxed_clone()));
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
log::warn!(
|
||||
"unknown view id {:?} for leader {:?}",
|
||||
active_view_id,
|
||||
leader_id
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
|
||||
items_to_activate.push((pane.clone(), Box::new(shared_screen)));
|
||||
@@ -3527,15 +3520,12 @@ impl Workspace {
|
||||
let client = project.read(cx).client();
|
||||
let user_store = project.read(cx).user_store();
|
||||
|
||||
let channel_store =
|
||||
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
|
||||
let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
|
||||
let app_state = Arc::new(AppState {
|
||||
languages: project.read(cx).languages().clone(),
|
||||
workspace_store,
|
||||
client,
|
||||
user_store,
|
||||
channel_store,
|
||||
fs: project.read(cx).fs().clone(),
|
||||
build_window_options: |_, _, _| Default::default(),
|
||||
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
|
||||
@@ -3811,7 +3801,7 @@ impl View for Workspace {
|
||||
self.center.render(
|
||||
&project,
|
||||
&theme,
|
||||
&self.follower_states_by_leader,
|
||||
&self.follower_states,
|
||||
self.active_call(),
|
||||
self.active_pane(),
|
||||
self.zoomed
|
||||
@@ -4148,6 +4138,188 @@ pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
|
||||
DB.last_workspace().await.log_err().flatten()
|
||||
}
|
||||
|
||||
async fn join_channel_internal(
|
||||
channel_id: u64,
|
||||
app_state: &Arc<AppState>,
|
||||
requesting_window: Option<WindowHandle<Workspace>>,
|
||||
active_call: &ModelHandle<ActiveCall>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<bool> {
|
||||
let (should_prompt, open_room) = active_call.read_with(cx, |active_call, cx| {
|
||||
let Some(room) = active_call.room().map(|room| room.read(cx)) else {
|
||||
return (false, None);
|
||||
};
|
||||
|
||||
let already_in_channel = room.channel_id() == Some(channel_id);
|
||||
let should_prompt = room.is_sharing_project()
|
||||
&& room.remote_participants().len() > 0
|
||||
&& !already_in_channel;
|
||||
let open_room = if already_in_channel {
|
||||
active_call.room().cloned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(should_prompt, open_room)
|
||||
});
|
||||
|
||||
if let Some(room) = open_room {
|
||||
let task = room.update(cx, |room, cx| {
|
||||
if let Some((project, host)) = room.most_active_project(cx) {
|
||||
return Some(join_remote_project(project, host, app_state.clone(), cx));
|
||||
}
|
||||
|
||||
None
|
||||
});
|
||||
if let Some(task) = task {
|
||||
task.await?;
|
||||
}
|
||||
return anyhow::Ok(true);
|
||||
}
|
||||
|
||||
if should_prompt {
|
||||
if let Some(workspace) = requesting_window {
|
||||
if let Some(window) = workspace.update(cx, |cx| cx.window()) {
|
||||
let answer = window.prompt(
|
||||
PromptLevel::Warning,
|
||||
"Leaving this call will unshare your current project.\nDo you want to switch channels?",
|
||||
&["Yes, Join Channel", "Cancel"],
|
||||
cx,
|
||||
);
|
||||
|
||||
if let Some(mut answer) = answer {
|
||||
if answer.next().await == Some(1) {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Ok(false); // unreachable!() hopefully
|
||||
}
|
||||
} else {
|
||||
return Ok(false); // unreachable!() hopefully
|
||||
}
|
||||
}
|
||||
|
||||
let client = cx.read(|cx| active_call.read(cx).client());
|
||||
|
||||
let mut client_status = client.status();
|
||||
|
||||
// this loop will terminate within client::CONNECTION_TIMEOUT seconds.
|
||||
'outer: loop {
|
||||
let Some(status) = client_status.recv().await else {
|
||||
return Err(anyhow!("error connecting"));
|
||||
};
|
||||
|
||||
match status {
|
||||
Status::Connecting
|
||||
| Status::Authenticating
|
||||
| Status::Reconnecting
|
||||
| Status::Reauthenticating => continue,
|
||||
Status::Connected { .. } => break 'outer,
|
||||
Status::SignedOut => return Err(anyhow!("not signed in")),
|
||||
Status::UpgradeRequired => return Err(anyhow!("zed is out of date")),
|
||||
Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
|
||||
return Err(anyhow!("zed is offline"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let room = active_call
|
||||
.update(cx, |active_call, cx| {
|
||||
active_call.join_channel(channel_id, cx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
room.update(cx, |room, _| room.room_update_completed())
|
||||
.await;
|
||||
|
||||
let task = room.update(cx, |room, cx| {
|
||||
if let Some((project, host)) = room.most_active_project(cx) {
|
||||
return Some(join_remote_project(project, host, app_state.clone(), cx));
|
||||
}
|
||||
|
||||
None
|
||||
});
|
||||
if let Some(task) = task {
|
||||
task.await?;
|
||||
return anyhow::Ok(true);
|
||||
}
|
||||
anyhow::Ok(false)
|
||||
}
|
||||
|
||||
pub fn join_channel(
|
||||
channel_id: u64,
|
||||
app_state: Arc<AppState>,
|
||||
requesting_window: Option<WindowHandle<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
cx.spawn(|mut cx| async move {
|
||||
let result = join_channel_internal(
|
||||
channel_id,
|
||||
&app_state,
|
||||
requesting_window,
|
||||
&active_call,
|
||||
&mut cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
// join channel succeeded, and opened a window
|
||||
if matches!(result, Ok(true)) {
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
|
||||
if requesting_window.is_some() {
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
|
||||
// find an existing workspace to focus and show call controls
|
||||
let mut active_window = activate_any_workspace_window(&mut cx);
|
||||
if active_window.is_none() {
|
||||
// no open workspaces, make one to show the error in (blergh)
|
||||
cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), requesting_window, cx))
|
||||
.await;
|
||||
}
|
||||
|
||||
active_window = activate_any_workspace_window(&mut cx);
|
||||
if active_window.is_none() {
|
||||
return result.map(|_| ()); // unreachable!() assuming new_local always opens a window
|
||||
}
|
||||
|
||||
if let Err(err) = result {
|
||||
let prompt = active_window.unwrap().prompt(
|
||||
PromptLevel::Critical,
|
||||
&format!("Failed to join channel: {}", err),
|
||||
&["Ok"],
|
||||
&mut cx,
|
||||
);
|
||||
if let Some(mut prompt) = prompt {
|
||||
prompt.next().await;
|
||||
} else {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
|
||||
// return ok, we showed the error to the user.
|
||||
return anyhow::Ok(());
|
||||
})
|
||||
}
|
||||
|
||||
pub fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<AnyWindowHandle> {
|
||||
for window in cx.windows() {
|
||||
let found = window.update(cx, |cx| {
|
||||
let is_workspace = cx.root_view().clone().downcast::<Workspace>().is_some();
|
||||
if is_workspace {
|
||||
cx.activate_window();
|
||||
}
|
||||
is_workspace
|
||||
});
|
||||
if found == Some(true) {
|
||||
return Some(window);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn open_paths(
|
||||
abs_paths: &[PathBuf],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user