Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a03d062120 | ||
|
|
a8bea41ad8 | ||
|
|
6a22ab83cf | ||
|
|
5e2eb436ff | ||
|
|
c684f08e30 | ||
|
|
f44f60c6e6 | ||
|
|
6976af5029 | ||
|
|
5cf953d559 | ||
|
|
6680e1e9fe | ||
|
|
9d8184670a | ||
|
|
89e44d4901 | ||
|
|
41ac8ef909 | ||
|
|
1693718637 | ||
|
|
9a3cd073c7 | ||
|
|
2fb13cf1ca | ||
|
|
391ab8fa1b | ||
|
|
9558da8681 | ||
|
|
2d1d75f482 | ||
|
|
2182cb2656 | ||
|
|
6d4276ea5f | ||
|
|
c0ad15756c | ||
|
|
e67c44a562 | ||
|
|
32979f3aca | ||
|
|
f3b6719c76 | ||
|
|
9547e88d88 | ||
|
|
547888942f | ||
|
|
61bd6bab09 | ||
|
|
432572c592 | ||
|
|
17b2b112bc | ||
|
|
49d3e1cc4b | ||
|
|
4de2c0f7ef | ||
|
|
0bed5e4562 | ||
|
|
5f1acae0d3 | ||
|
|
89d73f713a | ||
|
|
b2451d9dd6 | ||
|
|
074a221e0f | ||
|
|
f9650b3111 | ||
|
|
6397c05835 | ||
|
|
9456f716c2 | ||
|
|
83dfb191d6 | ||
|
|
3223e21d9f | ||
|
|
9c5f580012 | ||
|
|
3d8e63b93b | ||
|
|
c2d6d24952 | ||
|
|
a526f23c81 | ||
|
|
267e07472d | ||
|
|
ffd092a098 | ||
|
|
fa9f4a9355 | ||
|
|
a0e976599c | ||
|
|
08dddf0b26 | ||
|
|
b559bfd80f | ||
|
|
d782426491 | ||
|
|
4540f04dbe | ||
|
|
38d0fdc09a | ||
|
|
b2f9c454b0 | ||
|
|
c71e522b4e | ||
|
|
2aa7c6f2b4 | ||
|
|
ab0a3f19ab | ||
|
|
f638d4ce1d | ||
|
|
0f0b7090b8 | ||
|
|
03b4c7c464 | ||
|
|
fd61683c46 | ||
|
|
e3465fbcf9 | ||
|
|
a238368296 | ||
|
|
61d6cb880c | ||
|
|
d89c51135a | ||
|
|
c8f83e2d4d | ||
|
|
9da0b78ead | ||
|
|
e9e1170976 | ||
|
|
c70c0f9ae9 | ||
|
|
613add0aed | ||
|
|
74afa62a55 | ||
|
|
8c14a8fa95 | ||
|
|
0a9fb3978b | ||
|
|
e5ada92b7b | ||
|
|
78cea69172 | ||
|
|
7f70712dac | ||
|
|
0e79a7f26b | ||
|
|
e905ababcd | ||
|
|
793fa6e3a4 | ||
|
|
84bcbf1128 | ||
|
|
faf93aed4e | ||
|
|
4f09633379 | ||
|
|
c6b374ebc9 | ||
|
|
26d90a5e48 | ||
|
|
0a51784dd0 | ||
|
|
19c0b390d2 | ||
|
|
cebc8428c8 | ||
|
|
33a808a49b | ||
|
|
ffcc1cbf77 | ||
|
|
759ce7440c | ||
|
|
1d04dc5dbf | ||
|
|
3978d4e872 | ||
|
|
08dfcba68a | ||
|
|
c225a3e5af | ||
|
|
c7b7f7dfd5 | ||
|
|
d791fc707a | ||
|
|
9ff238921f | ||
|
|
17b8e4a684 | ||
|
|
3ff8c78b58 | ||
|
|
851a60a68e |
15
.github/actions/check_formatting/action.yml
vendored
Normal file
15
.github/actions/check_formatting/action.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: 'Check formatting'
|
||||
description: 'Checks code formatting use cargo fmt'
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install Rust
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
|
||||
- name: cargo fmt
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: cargo fmt --all -- --check
|
||||
42
.github/actions/run_tests/action.yml
vendored
Normal file
42
.github/actions/run_tests/action.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: 'Run tests'
|
||||
description: 'Runs the tests'
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install Rust
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
rustup target add wasm32-wasi
|
||||
cargo install cargo-nextest
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Limit target directory size
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/clear-target-dir-if-larger-than 70
|
||||
|
||||
- name: Run check
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: cargo check --tests --workspace
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: cargo nextest run --workspace --no-fail-fast
|
||||
|
||||
- name: Build collab
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: cargo build -p collab
|
||||
|
||||
- name: Build other binaries
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: cargo build --workspace --bins --all-features
|
||||
43
.github/workflows/ci.yml
vendored
43
.github/workflows/ci.yml
vendored
@@ -23,19 +23,14 @@ jobs:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: cargo fmt
|
||||
run: cargo fmt --all -- --check
|
||||
- name: Run rustfmt
|
||||
uses: ./.github/actions/check_formatting
|
||||
|
||||
tests:
|
||||
name: Run tests
|
||||
@@ -43,41 +38,15 @@ jobs:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: rustfmt
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
rustup target add wasm32-wasi
|
||||
cargo install cargo-nextest
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 70
|
||||
|
||||
- name: Run check
|
||||
run: cargo check --workspace
|
||||
|
||||
- name: Run tests
|
||||
run: cargo nextest run --workspace --no-fail-fast
|
||||
|
||||
- name: Build collab
|
||||
run: cargo build -p collab
|
||||
|
||||
- name: Build other binaries
|
||||
run: cargo build --workspace --bins --all-features
|
||||
uses: ./.github/actions/run_tests
|
||||
|
||||
bundle:
|
||||
name: Bundle app
|
||||
@@ -130,6 +99,8 @@ jobs:
|
||||
expected_tag_name="v${version}";;
|
||||
preview)
|
||||
expected_tag_name="v${version}-pre";;
|
||||
nightly)
|
||||
expected_tag_name="v${version}-nightly";;
|
||||
*)
|
||||
echo "can't publish a release on channel ${channel}"
|
||||
exit 1;;
|
||||
@@ -154,7 +125,9 @@ jobs:
|
||||
|
||||
- uses: softprops/action-gh-release@v1
|
||||
name: Upload app bundle to release
|
||||
if: ${{ env.RELEASE_CHANNEL }}
|
||||
# TODO kb seems that zed.dev relies on GitHub releases for release version tracking.
|
||||
# Find alternatives for `nightly` or just go on with more releases?
|
||||
if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
|
||||
98
.github/workflows/release_nightly.yml
vendored
Normal file
98
.github/workflows/release_nightly.yml
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
name: Release Nightly
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Fire every night at 1:00am
|
||||
- cron: "0 1 * * *"
|
||||
push:
|
||||
tags:
|
||||
- "nightly*"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
rustfmt:
|
||||
name: Check formatting
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Run rustfmt
|
||||
uses: ./.github/actions/check_formatting
|
||||
|
||||
tests:
|
||||
name: Run tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: rustfmt
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
|
||||
bundle:
|
||||
name: Bundle app
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
needs: tests
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
rustup target add wasm32-wasi
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 70
|
||||
|
||||
- name: Set release channel to nightly
|
||||
run: |
|
||||
set -eu
|
||||
version=$(git rev-parse --short HEAD)
|
||||
echo "Publishing version: ${version} on release channel nightly"
|
||||
echo "nightly" > crates/zed/RELEASE_CHANNEL
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
- name: Create app bundle
|
||||
run: script/bundle
|
||||
|
||||
- name: Upload Zed Nightly
|
||||
run: script/upload-nightly
|
||||
111
Cargo.lock
generated
111
Cargo.lock
generated
@@ -1829,6 +1829,47 @@ dependencies = [
|
||||
"zed-actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "collab_ui2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"call2",
|
||||
"channel2",
|
||||
"client2",
|
||||
"clock",
|
||||
"collections",
|
||||
"db2",
|
||||
"editor2",
|
||||
"feature_flags2",
|
||||
"futures 0.3.28",
|
||||
"fuzzy",
|
||||
"gpui2",
|
||||
"language2",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"menu2",
|
||||
"notifications2",
|
||||
"picker2",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"project2",
|
||||
"rich_text2",
|
||||
"rpc2",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"settings2",
|
||||
"smallvec",
|
||||
"theme2",
|
||||
"time",
|
||||
"tree-sitter-markdown",
|
||||
"ui2",
|
||||
"util",
|
||||
"workspace2",
|
||||
"zed_actions2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "collections"
|
||||
version = "0.1.0"
|
||||
@@ -3756,6 +3797,7 @@ dependencies = [
|
||||
"image",
|
||||
"itertools 0.10.5",
|
||||
"lazy_static",
|
||||
"linkme",
|
||||
"log",
|
||||
"media",
|
||||
"metal",
|
||||
@@ -4774,6 +4816,26 @@ version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||
|
||||
[[package]]
|
||||
name = "linkme"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91ed2ee9464ff9707af8e9ad834cffa4802f072caad90639c583dd3c62e6e608"
|
||||
dependencies = [
|
||||
"linkme-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linkme-impl"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba125974b109d512fccbc6c0244e7580143e460895dfd6ea7f8bbb692fd94396"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.0.42"
|
||||
@@ -8761,6 +8823,17 @@ dependencies = [
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "storybook3"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gpui2",
|
||||
"settings2",
|
||||
"theme2",
|
||||
"ui2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stringprep"
|
||||
version = "0.1.4"
|
||||
@@ -9134,6 +9207,39 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "terminal_view2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client2",
|
||||
"db2",
|
||||
"dirs 4.0.0",
|
||||
"editor2",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"itertools 0.10.5",
|
||||
"language2",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"mio-extras",
|
||||
"ordered-float 2.10.0",
|
||||
"procinfo",
|
||||
"project2",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"settings2",
|
||||
"shellexpand",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"terminal2",
|
||||
"theme2",
|
||||
"thiserror",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "text"
|
||||
version = "0.1.0"
|
||||
@@ -10058,6 +10164,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"gpui2",
|
||||
"itertools 0.11.0",
|
||||
"menu2",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"settings2",
|
||||
@@ -11288,7 +11395,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.113.0"
|
||||
version = "0.114.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"ai",
|
||||
@@ -11441,6 +11548,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"cli",
|
||||
"client2",
|
||||
"collab_ui2",
|
||||
"collections",
|
||||
"command_palette2",
|
||||
"copilot2",
|
||||
@@ -11492,6 +11600,7 @@ dependencies = [
|
||||
"smol",
|
||||
"sum_tree",
|
||||
"tempdir",
|
||||
"terminal_view2",
|
||||
"text2",
|
||||
"theme2",
|
||||
"thiserror",
|
||||
|
||||
@@ -18,6 +18,7 @@ members = [
|
||||
"crates/collab",
|
||||
"crates/collab2",
|
||||
"crates/collab_ui",
|
||||
"crates/collab_ui2",
|
||||
"crates/collections",
|
||||
"crates/command_palette",
|
||||
"crates/command_palette2",
|
||||
@@ -94,9 +95,11 @@ members = [
|
||||
"crates/sqlez_macros",
|
||||
"crates/rich_text",
|
||||
"crates/storybook2",
|
||||
"crates/storybook3",
|
||||
"crates/sum_tree",
|
||||
"crates/terminal",
|
||||
"crates/terminal2",
|
||||
"crates/terminal_view2",
|
||||
"crates/text",
|
||||
"crates/theme",
|
||||
"crates/theme2",
|
||||
@@ -203,6 +206,9 @@ core-graphics = { git = "https://github.com/servo/core-foundation-rs", rev = "07
|
||||
[profile.dev]
|
||||
split-debuginfo = "unpacked"
|
||||
|
||||
[profile.dev.package.taffy]
|
||||
opt-level = 3
|
||||
|
||||
[profile.release]
|
||||
debug = true
|
||||
lto = "thin"
|
||||
|
||||
@@ -35,6 +35,15 @@
|
||||
// "custom": 2
|
||||
// },
|
||||
"buffer_line_height": "comfortable",
|
||||
// The name of a font to use for rendering text in the UI
|
||||
"ui_font_family": "Zed Mono",
|
||||
// The OpenType features to enable for text in the UI
|
||||
"ui_font_features": {
|
||||
// Disable ligatures:
|
||||
"calt": false
|
||||
},
|
||||
// The default font size for text in the UI
|
||||
"ui_font_size": 14,
|
||||
// The factor to grow the active pane by. Defaults to 1.0
|
||||
// which gives the same size as all other panes.
|
||||
"active_pane_magnification": 1.0,
|
||||
|
||||
@@ -118,14 +118,18 @@ fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
|
||||
let auto_updater = auto_updater.read(cx);
|
||||
let server_url = &auto_updater.server_url;
|
||||
let current_version = auto_updater.current_version;
|
||||
let latest_release_url = if cx.has_global::<ReleaseChannel>()
|
||||
&& *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
|
||||
{
|
||||
format!("{server_url}/releases/preview/{current_version}")
|
||||
} else {
|
||||
format!("{server_url}/releases/stable/{current_version}")
|
||||
};
|
||||
cx.platform().open_url(&latest_release_url);
|
||||
if cx.has_global::<ReleaseChannel>() {
|
||||
match cx.global::<ReleaseChannel>() {
|
||||
ReleaseChannel::Dev => {}
|
||||
ReleaseChannel::Nightly => {}
|
||||
ReleaseChannel::Preview => cx
|
||||
.platform()
|
||||
.open_url(&format!("{server_url}/releases/preview/{current_version}")),
|
||||
ReleaseChannel::Stable => cx
|
||||
.platform()
|
||||
.open_url(&format!("{server_url}/releases/stable/{current_version}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,22 +228,19 @@ impl AutoUpdater {
|
||||
)
|
||||
});
|
||||
|
||||
let preview_param = cx.read(|cx| {
|
||||
let mut url_string = format!(
|
||||
"{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg"
|
||||
);
|
||||
cx.read(|cx| {
|
||||
if cx.has_global::<ReleaseChannel>() {
|
||||
if *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview {
|
||||
return "&preview=1";
|
||||
if let Some(param) = cx.global::<ReleaseChannel>().release_query_param() {
|
||||
url_string += "&";
|
||||
url_string += param;
|
||||
}
|
||||
}
|
||||
""
|
||||
});
|
||||
|
||||
let mut response = client
|
||||
.get(
|
||||
&format!("{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg{preview_param}"),
|
||||
Default::default(),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
let mut response = client.get(&url_string, Default::default(), true).await?;
|
||||
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
|
||||
@@ -987,9 +987,17 @@ impl Client {
|
||||
self.establish_websocket_connection(credentials, cx)
|
||||
}
|
||||
|
||||
async fn get_rpc_url(http: Arc<dyn HttpClient>, is_preview: bool) -> Result<Url> {
|
||||
let preview_param = if is_preview { "?preview=1" } else { "" };
|
||||
let url = format!("{}/rpc{preview_param}", *ZED_SERVER_URL);
|
||||
async fn get_rpc_url(
|
||||
http: Arc<dyn HttpClient>,
|
||||
release_channel: Option<ReleaseChannel>,
|
||||
) -> Result<Url> {
|
||||
let mut url = format!("{}/rpc", *ZED_SERVER_URL);
|
||||
if let Some(preview_param) =
|
||||
release_channel.and_then(|channel| channel.release_query_param())
|
||||
{
|
||||
url += "?";
|
||||
url += preview_param;
|
||||
}
|
||||
let response = http.get(&url, Default::default(), false).await?;
|
||||
|
||||
// Normally, ZED_SERVER_URL is set to the URL of zed.dev website.
|
||||
@@ -1024,11 +1032,11 @@ impl Client {
|
||||
credentials: &Credentials,
|
||||
cx: &AsyncAppContext,
|
||||
) -> Task<Result<Connection, EstablishConnectionError>> {
|
||||
let use_preview_server = cx.read(|cx| {
|
||||
let release_channel = cx.read(|cx| {
|
||||
if cx.has_global::<ReleaseChannel>() {
|
||||
*cx.global::<ReleaseChannel>() != ReleaseChannel::Stable
|
||||
Some(*cx.global::<ReleaseChannel>())
|
||||
} else {
|
||||
false
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1041,7 +1049,7 @@ impl Client {
|
||||
|
||||
let http = self.http.clone();
|
||||
cx.background().spawn(async move {
|
||||
let mut rpc_url = Self::get_rpc_url(http, use_preview_server).await?;
|
||||
let mut rpc_url = Self::get_rpc_url(http, release_channel).await?;
|
||||
let rpc_host = rpc_url
|
||||
.host_str()
|
||||
.zip(rpc_url.port_or_known_default())
|
||||
@@ -1191,7 +1199,7 @@ impl Client {
|
||||
|
||||
// Use the collab server's admin API to retrieve the id
|
||||
// of the impersonated user.
|
||||
let mut url = Self::get_rpc_url(http.clone(), false).await?;
|
||||
let mut url = Self::get_rpc_url(http.clone(), None).await?;
|
||||
url.set_path("/user");
|
||||
url.set_query(Some(&format!("github_login={login}")));
|
||||
let request = Request::get(url.as_str())
|
||||
|
||||
@@ -20,7 +20,7 @@ pub struct Telemetry {
|
||||
#[derive(Default)]
|
||||
struct TelemetryState {
|
||||
metrics_id: Option<Arc<str>>, // Per logged-in user
|
||||
installation_id: Option<Arc<str>>, // Per app installation (different for dev, preview, and stable)
|
||||
installation_id: Option<Arc<str>>, // Per app installation (different for dev, nightly, preview, and stable)
|
||||
session_id: Option<Arc<str>>, // Per app launch
|
||||
app_version: Option<Arc<str>>,
|
||||
release_channel: Option<&'static str>,
|
||||
|
||||
@@ -923,9 +923,17 @@ impl Client {
|
||||
self.establish_websocket_connection(credentials, cx)
|
||||
}
|
||||
|
||||
async fn get_rpc_url(http: Arc<dyn HttpClient>, is_preview: bool) -> Result<Url> {
|
||||
let preview_param = if is_preview { "?preview=1" } else { "" };
|
||||
let url = format!("{}/rpc{preview_param}", *ZED_SERVER_URL);
|
||||
async fn get_rpc_url(
|
||||
http: Arc<dyn HttpClient>,
|
||||
release_channel: Option<ReleaseChannel>,
|
||||
) -> Result<Url> {
|
||||
let mut url = format!("{}/rpc", *ZED_SERVER_URL);
|
||||
if let Some(preview_param) =
|
||||
release_channel.and_then(|channel| channel.release_query_param())
|
||||
{
|
||||
url += "?";
|
||||
url += preview_param;
|
||||
}
|
||||
let response = http.get(&url, Default::default(), false).await?;
|
||||
|
||||
// Normally, ZED_SERVER_URL is set to the URL of zed.dev website.
|
||||
@@ -960,9 +968,7 @@ impl Client {
|
||||
credentials: &Credentials,
|
||||
cx: &AsyncAppContext,
|
||||
) -> Task<Result<Connection, EstablishConnectionError>> {
|
||||
let use_preview_server = cx
|
||||
.try_read_global(|channel: &ReleaseChannel, _| *channel != ReleaseChannel::Stable)
|
||||
.unwrap_or(false);
|
||||
let release_channel = cx.try_read_global(|channel: &ReleaseChannel, _| *channel);
|
||||
|
||||
let request = Request::builder()
|
||||
.header(
|
||||
@@ -973,7 +979,7 @@ impl Client {
|
||||
|
||||
let http = self.http.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
let mut rpc_url = Self::get_rpc_url(http, use_preview_server).await?;
|
||||
let mut rpc_url = Self::get_rpc_url(http, release_channel).await?;
|
||||
let rpc_host = rpc_url
|
||||
.host_str()
|
||||
.zip(rpc_url.port_or_known_default())
|
||||
@@ -1120,7 +1126,7 @@ impl Client {
|
||||
|
||||
// Use the collab server's admin API to retrieve the id
|
||||
// of the impersonated user.
|
||||
let mut url = Self::get_rpc_url(http.clone(), false).await?;
|
||||
let mut url = Self::get_rpc_url(http.clone(), None).await?;
|
||||
url.set_path("/user");
|
||||
url.set_query(Some(&format!("github_login={login}")));
|
||||
let request = Request::get(url.as_str())
|
||||
|
||||
@@ -20,7 +20,7 @@ pub struct Telemetry {
|
||||
|
||||
struct TelemetryState {
|
||||
metrics_id: Option<Arc<str>>, // Per logged-in user
|
||||
installation_id: Option<Arc<str>>, // Per app installation (different for dev, preview, and stable)
|
||||
installation_id: Option<Arc<str>>, // Per app installation (different for dev, nightly, preview, and stable)
|
||||
session_id: Option<Arc<str>>, // Per app launch
|
||||
release_channel: Option<&'static str>,
|
||||
app_metadata: AppMetadata,
|
||||
|
||||
@@ -220,12 +220,11 @@ impl TestServer {
|
||||
languages: Arc::new(language_registry),
|
||||
fs: fs.clone(),
|
||||
build_window_options: |_, _, _| Default::default(),
|
||||
initialize_workspace: |_, _, _, _| gpui::Task::ready(Ok(())),
|
||||
node_runtime: FakeNodeRuntime::new(),
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
theme::init(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
Project::init(&client, cx);
|
||||
client::init(&client, cx);
|
||||
language::init(cx);
|
||||
|
||||
81
crates/collab_ui2/Cargo.toml
Normal file
81
crates/collab_ui2/Cargo.toml
Normal file
@@ -0,0 +1,81 @@
|
||||
[package]
|
||||
name = "collab_ui2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/collab_ui.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"call/test-support",
|
||||
"client/test-support",
|
||||
"collections/test-support",
|
||||
"editor/test-support",
|
||||
"gpui/test-support",
|
||||
"project/test-support",
|
||||
"settings/test-support",
|
||||
"util/test-support",
|
||||
"workspace/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
# auto_update = { path = "../auto_update" }
|
||||
db = { package = "db2", path = "../db2" }
|
||||
call = { package = "call2", path = "../call2" }
|
||||
client = { package = "client2", path = "../client2" }
|
||||
channel = { package = "channel2", path = "../channel2" }
|
||||
clock = { path = "../clock" }
|
||||
collections = { path = "../collections" }
|
||||
# context_menu = { path = "../context_menu" }
|
||||
# drag_and_drop = { path = "../drag_and_drop" }
|
||||
editor = { package="editor2", path = "../editor2" }
|
||||
#feedback = { path = "../feedback" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
menu = { package = "menu2", path = "../menu2" }
|
||||
notifications = { package = "notifications2", path = "../notifications2" }
|
||||
rich_text = { package = "rich_text2", path = "../rich_text2" }
|
||||
picker = { package = "picker2", path = "../picker2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
# recent_projects = { path = "../recent_projects" }
|
||||
rpc = { package ="rpc2", path = "../rpc2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
feature_flags = { package = "feature_flags2", path = "../feature_flags2"}
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
# theme_selector = { path = "../theme_selector" }
|
||||
# vcs_menu = { path = "../vcs_menu" }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
util = { path = "../util" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
zed-actions = { package="zed_actions2", path = "../zed_actions2"}
|
||||
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
schemars.workspace = true
|
||||
postage.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
time.workspace = true
|
||||
smallvec.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
call = { package = "call2", path = "../call2", features = ["test-support"] }
|
||||
client = { package = "client2", path = "../client2", features = ["test-support"] }
|
||||
collections = { path = "../collections", features = ["test-support"] }
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||
notifications = { package = "notifications2", path = "../notifications2", features = ["test-support"] }
|
||||
project = { package = "project2", path = "../project2", features = ["test-support"] }
|
||||
rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] }
|
||||
settings = { package = "settings2", path = "../settings2", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
|
||||
|
||||
pretty_assertions.workspace = true
|
||||
tree-sitter-markdown.workspace = true
|
||||
454
crates/collab_ui2/src/channel_view.rs
Normal file
454
crates/collab_ui2/src/channel_view.rs
Normal file
@@ -0,0 +1,454 @@
|
||||
// use anyhow::{anyhow, Result};
|
||||
// use call::report_call_event_for_channel;
|
||||
// use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore};
|
||||
// use client::{
|
||||
// proto::{self, PeerId},
|
||||
// Collaborator, ParticipantIndex,
|
||||
// };
|
||||
// use collections::HashMap;
|
||||
// use editor::{CollaborationHub, Editor};
|
||||
// use gpui::{
|
||||
// actions,
|
||||
// elements::{ChildView, Label},
|
||||
// geometry::vector::Vector2F,
|
||||
// AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View,
|
||||
// ViewContext, ViewHandle,
|
||||
// };
|
||||
// use project::Project;
|
||||
// use smallvec::SmallVec;
|
||||
// use std::{
|
||||
// any::{Any, TypeId},
|
||||
// sync::Arc,
|
||||
// };
|
||||
// use util::ResultExt;
|
||||
// use workspace::{
|
||||
// item::{FollowableItem, Item, ItemEvent, ItemHandle},
|
||||
// register_followable_item,
|
||||
// searchable::SearchableItemHandle,
|
||||
// ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
|
||||
// };
|
||||
|
||||
// actions!(channel_view, [Deploy]);
|
||||
|
||||
// pub fn init(cx: &mut AppContext) {
|
||||
// register_followable_item::<ChannelView>(cx)
|
||||
// }
|
||||
|
||||
// pub struct ChannelView {
|
||||
// pub editor: ViewHandle<Editor>,
|
||||
// project: ModelHandle<Project>,
|
||||
// channel_store: ModelHandle<ChannelStore>,
|
||||
// channel_buffer: ModelHandle<ChannelBuffer>,
|
||||
// remote_id: Option<ViewId>,
|
||||
// _editor_event_subscription: Subscription,
|
||||
// }
|
||||
|
||||
// impl ChannelView {
|
||||
// pub fn open(
|
||||
// channel_id: ChannelId,
|
||||
// workspace: ViewHandle<Workspace>,
|
||||
// cx: &mut AppContext,
|
||||
// ) -> Task<Result<ViewHandle<Self>>> {
|
||||
// let pane = workspace.read(cx).active_pane().clone();
|
||||
// let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
|
||||
// cx.spawn(|mut cx| async move {
|
||||
// let channel_view = channel_view.await?;
|
||||
// pane.update(&mut cx, |pane, cx| {
|
||||
// report_call_event_for_channel(
|
||||
// "open channel notes",
|
||||
// channel_id,
|
||||
// &workspace.read(cx).app_state().client,
|
||||
// cx,
|
||||
// );
|
||||
// pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
|
||||
// });
|
||||
// anyhow::Ok(channel_view)
|
||||
// })
|
||||
// }
|
||||
|
||||
// pub fn open_in_pane(
|
||||
// channel_id: ChannelId,
|
||||
// pane: ViewHandle<Pane>,
|
||||
// workspace: ViewHandle<Workspace>,
|
||||
// cx: &mut AppContext,
|
||||
// ) -> Task<Result<ViewHandle<Self>>> {
|
||||
// let workspace = workspace.read(cx);
|
||||
// let project = workspace.project().to_owned();
|
||||
// let channel_store = ChannelStore::global(cx);
|
||||
// let language_registry = workspace.app_state().languages.clone();
|
||||
// let markdown = language_registry.language_for_name("Markdown");
|
||||
// let channel_buffer =
|
||||
// channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
|
||||
|
||||
// cx.spawn(|mut cx| async move {
|
||||
// let channel_buffer = channel_buffer.await?;
|
||||
// let markdown = markdown.await.log_err();
|
||||
|
||||
// channel_buffer.update(&mut cx, |buffer, cx| {
|
||||
// buffer.buffer().update(cx, |buffer, cx| {
|
||||
// buffer.set_language_registry(language_registry);
|
||||
// if let Some(markdown) = markdown {
|
||||
// buffer.set_language(Some(markdown), cx);
|
||||
// }
|
||||
// })
|
||||
// });
|
||||
|
||||
// pane.update(&mut cx, |pane, cx| {
|
||||
// let buffer_id = channel_buffer.read(cx).remote_id(cx);
|
||||
|
||||
// let existing_view = pane
|
||||
// .items_of_type::<Self>()
|
||||
// .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
|
||||
|
||||
// // If this channel buffer is already open in this pane, just return it.
|
||||
// if let Some(existing_view) = existing_view.clone() {
|
||||
// if existing_view.read(cx).channel_buffer == channel_buffer {
|
||||
// return existing_view;
|
||||
// }
|
||||
// }
|
||||
|
||||
// let view = cx.add_view(|cx| {
|
||||
// let mut this = Self::new(project, channel_store, channel_buffer, cx);
|
||||
// this.acknowledge_buffer_version(cx);
|
||||
// this
|
||||
// });
|
||||
|
||||
// // If the pane contained a disconnected view for this channel buffer,
|
||||
// // replace that.
|
||||
// if let Some(existing_item) = existing_view {
|
||||
// if let Some(ix) = pane.index_for_item(&existing_item) {
|
||||
// pane.close_item_by_id(existing_item.id(), SaveIntent::Skip, cx)
|
||||
// .detach();
|
||||
// pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
|
||||
// }
|
||||
// }
|
||||
|
||||
// view
|
||||
// })
|
||||
// .ok_or_else(|| anyhow!("pane was dropped"))
|
||||
// })
|
||||
// }
|
||||
|
||||
// pub fn new(
|
||||
// project: ModelHandle<Project>,
|
||||
// channel_store: ModelHandle<ChannelStore>,
|
||||
// channel_buffer: ModelHandle<ChannelBuffer>,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> Self {
|
||||
// let buffer = channel_buffer.read(cx).buffer();
|
||||
// let editor = cx.add_view(|cx| {
|
||||
// let mut editor = Editor::for_buffer(buffer, None, cx);
|
||||
// editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
|
||||
// channel_buffer.clone(),
|
||||
// )));
|
||||
// editor.set_read_only(
|
||||
// !channel_buffer
|
||||
// .read(cx)
|
||||
// .channel(cx)
|
||||
// .is_some_and(|c| c.can_edit_notes()),
|
||||
// );
|
||||
// editor
|
||||
// });
|
||||
// let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
|
||||
|
||||
// cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
|
||||
// .detach();
|
||||
|
||||
// Self {
|
||||
// editor,
|
||||
// project,
|
||||
// channel_store,
|
||||
// channel_buffer,
|
||||
// remote_id: None,
|
||||
// _editor_event_subscription,
|
||||
// }
|
||||
// }
|
||||
|
||||
// pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
|
||||
// self.channel_buffer.read(cx).channel(cx)
|
||||
// }
|
||||
|
||||
// fn handle_channel_buffer_event(
|
||||
// &mut self,
|
||||
// _: ModelHandle<ChannelBuffer>,
|
||||
// event: &ChannelBufferEvent,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) {
|
||||
// match event {
|
||||
// ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
|
||||
// editor.set_read_only(true);
|
||||
// cx.notify();
|
||||
// }),
|
||||
// ChannelBufferEvent::ChannelChanged => {
|
||||
// self.editor.update(cx, |editor, cx| {
|
||||
// editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes()));
|
||||
// cx.emit(editor::Event::TitleChanged);
|
||||
// cx.notify()
|
||||
// });
|
||||
// }
|
||||
// ChannelBufferEvent::BufferEdited => {
|
||||
// if cx.is_self_focused() || self.editor.is_focused(cx) {
|
||||
// self.acknowledge_buffer_version(cx);
|
||||
// } else {
|
||||
// self.channel_store.update(cx, |store, cx| {
|
||||
// let channel_buffer = self.channel_buffer.read(cx);
|
||||
// store.notes_changed(
|
||||
// channel_buffer.channel_id,
|
||||
// channel_buffer.epoch(),
|
||||
// &channel_buffer.buffer().read(cx).version(),
|
||||
// cx,
|
||||
// )
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// ChannelBufferEvent::CollaboratorsChanged => {}
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<'_, '_, ChannelView>) {
|
||||
// self.channel_store.update(cx, |store, cx| {
|
||||
// let channel_buffer = self.channel_buffer.read(cx);
|
||||
// store.acknowledge_notes_version(
|
||||
// channel_buffer.channel_id,
|
||||
// channel_buffer.epoch(),
|
||||
// &channel_buffer.buffer().read(cx).version(),
|
||||
// cx,
|
||||
// )
|
||||
// });
|
||||
// self.channel_buffer.update(cx, |buffer, cx| {
|
||||
// buffer.acknowledge_buffer_version(cx);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl Entity for ChannelView {
|
||||
// type Event = editor::Event;
|
||||
// }
|
||||
|
||||
// impl View for ChannelView {
|
||||
// fn ui_name() -> &'static str {
|
||||
// "ChannelView"
|
||||
// }
|
||||
|
||||
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
// ChildView::new(self.editor.as_any(), cx).into_any()
|
||||
// }
|
||||
|
||||
// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
// if cx.is_self_focused() {
|
||||
// self.acknowledge_buffer_version(cx);
|
||||
// cx.focus(self.editor.as_any())
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl Item for ChannelView {
|
||||
// fn act_as_type<'a>(
|
||||
// &'a self,
|
||||
// type_id: TypeId,
|
||||
// self_handle: &'a ViewHandle<Self>,
|
||||
// _: &'a AppContext,
|
||||
// ) -> Option<&'a AnyViewHandle> {
|
||||
// if type_id == TypeId::of::<Self>() {
|
||||
// Some(self_handle)
|
||||
// } else if type_id == TypeId::of::<Editor>() {
|
||||
// Some(&self.editor)
|
||||
// } else {
|
||||
// None
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn tab_content<V: 'static>(
|
||||
// &self,
|
||||
// _: Option<usize>,
|
||||
// style: &theme::Tab,
|
||||
// cx: &gpui::AppContext,
|
||||
// ) -> AnyElement<V> {
|
||||
// let label = if let Some(channel) = self.channel(cx) {
|
||||
// match (
|
||||
// channel.can_edit_notes(),
|
||||
// self.channel_buffer.read(cx).is_connected(),
|
||||
// ) {
|
||||
// (true, true) => format!("#{}", channel.name),
|
||||
// (false, true) => format!("#{} (read-only)", channel.name),
|
||||
// (_, false) => format!("#{} (disconnected)", channel.name),
|
||||
// }
|
||||
// } else {
|
||||
// format!("channel notes (disconnected)")
|
||||
// };
|
||||
// Label::new(label, style.label.to_owned()).into_any()
|
||||
// }
|
||||
|
||||
// fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self> {
|
||||
// Some(Self::new(
|
||||
// self.project.clone(),
|
||||
// self.channel_store.clone(),
|
||||
// self.channel_buffer.clone(),
|
||||
// cx,
|
||||
// ))
|
||||
// }
|
||||
|
||||
// fn is_singleton(&self, _cx: &AppContext) -> bool {
|
||||
// false
|
||||
// }
|
||||
|
||||
// fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||
// self.editor
|
||||
// .update(cx, |editor, cx| editor.navigate(data, cx))
|
||||
// }
|
||||
|
||||
// fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
// self.editor
|
||||
// .update(cx, |editor, cx| Item::deactivated(editor, cx))
|
||||
// }
|
||||
|
||||
// fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
|
||||
// self.editor
|
||||
// .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
|
||||
// }
|
||||
|
||||
// fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
// Some(Box::new(self.editor.clone()))
|
||||
// }
|
||||
|
||||
// fn show_toolbar(&self) -> bool {
|
||||
// true
|
||||
// }
|
||||
|
||||
// fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
|
||||
// self.editor.read(cx).pixel_position_of_cursor(cx)
|
||||
// }
|
||||
|
||||
// fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
|
||||
// editor::Editor::to_item_events(event)
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl FollowableItem for ChannelView {
|
||||
// fn remote_id(&self) -> Option<workspace::ViewId> {
|
||||
// self.remote_id
|
||||
// }
|
||||
|
||||
// fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
|
||||
// let channel_buffer = self.channel_buffer.read(cx);
|
||||
// if !channel_buffer.is_connected() {
|
||||
// return None;
|
||||
// }
|
||||
|
||||
// Some(proto::view::Variant::ChannelView(
|
||||
// proto::view::ChannelView {
|
||||
// channel_id: channel_buffer.channel_id,
|
||||
// editor: if let Some(proto::view::Variant::Editor(proto)) =
|
||||
// self.editor.read(cx).to_state_proto(cx)
|
||||
// {
|
||||
// Some(proto)
|
||||
// } else {
|
||||
// None
|
||||
// },
|
||||
// },
|
||||
// ))
|
||||
// }
|
||||
|
||||
// fn from_state_proto(
|
||||
// pane: ViewHandle<workspace::Pane>,
|
||||
// workspace: ViewHandle<workspace::Workspace>,
|
||||
// remote_id: workspace::ViewId,
|
||||
// state: &mut Option<proto::view::Variant>,
|
||||
// cx: &mut AppContext,
|
||||
// ) -> Option<gpui::Task<anyhow::Result<ViewHandle<Self>>>> {
|
||||
// let Some(proto::view::Variant::ChannelView(_)) = state else {
|
||||
// return None;
|
||||
// };
|
||||
// let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
|
||||
// unreachable!()
|
||||
// };
|
||||
|
||||
// let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
|
||||
|
||||
// Some(cx.spawn(|mut cx| async move {
|
||||
// let this = open.await?;
|
||||
|
||||
// let task = this
|
||||
// .update(&mut cx, |this, cx| {
|
||||
// this.remote_id = Some(remote_id);
|
||||
|
||||
// if let Some(state) = state.editor {
|
||||
// Some(this.editor.update(cx, |editor, cx| {
|
||||
// editor.apply_update_proto(
|
||||
// &this.project,
|
||||
// proto::update_view::Variant::Editor(proto::update_view::Editor {
|
||||
// selections: state.selections,
|
||||
// pending_selection: state.pending_selection,
|
||||
// scroll_top_anchor: state.scroll_top_anchor,
|
||||
// scroll_x: state.scroll_x,
|
||||
// scroll_y: state.scroll_y,
|
||||
// ..Default::default()
|
||||
// }),
|
||||
// cx,
|
||||
// )
|
||||
// }))
|
||||
// } else {
|
||||
// None
|
||||
// }
|
||||
// })
|
||||
// .ok_or_else(|| anyhow!("window was closed"))?;
|
||||
|
||||
// if let Some(task) = task {
|
||||
// task.await?;
|
||||
// }
|
||||
|
||||
// Ok(this)
|
||||
// }))
|
||||
// }
|
||||
|
||||
// fn add_event_to_update_proto(
|
||||
// &self,
|
||||
// event: &Self::Event,
|
||||
// update: &mut Option<proto::update_view::Variant>,
|
||||
// cx: &AppContext,
|
||||
// ) -> bool {
|
||||
// self.editor
|
||||
// .read(cx)
|
||||
// .add_event_to_update_proto(event, update, cx)
|
||||
// }
|
||||
|
||||
// fn apply_update_proto(
|
||||
// &mut self,
|
||||
// project: &ModelHandle<Project>,
|
||||
// message: proto::update_view::Variant,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> gpui::Task<anyhow::Result<()>> {
|
||||
// self.editor.update(cx, |editor, cx| {
|
||||
// editor.apply_update_proto(project, message, cx)
|
||||
// })
|
||||
// }
|
||||
|
||||
// fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
|
||||
// self.editor.update(cx, |editor, cx| {
|
||||
// editor.set_leader_peer_id(leader_peer_id, cx)
|
||||
// })
|
||||
// }
|
||||
|
||||
// fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
|
||||
// Editor::should_unfollow_on_event(event, cx)
|
||||
// }
|
||||
|
||||
// fn is_project_item(&self, _cx: &AppContext) -> bool {
|
||||
// false
|
||||
// }
|
||||
// }
|
||||
|
||||
// struct ChannelBufferCollaborationHub(ModelHandle<ChannelBuffer>);
|
||||
|
||||
// impl CollaborationHub for ChannelBufferCollaborationHub {
|
||||
// fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
|
||||
// self.0.read(cx).collaborators()
|
||||
// }
|
||||
|
||||
// fn user_participant_indices<'a>(
|
||||
// &self,
|
||||
// cx: &'a AppContext,
|
||||
// ) -> &'a HashMap<u64, ParticipantIndex> {
|
||||
// self.0.read(cx).user_store().read(cx).participant_indices()
|
||||
// }
|
||||
// }
|
||||
983
crates/collab_ui2/src/chat_panel.rs
Normal file
983
crates/collab_ui2/src/chat_panel.rs
Normal file
@@ -0,0 +1,983 @@
|
||||
// use crate::{
|
||||
// channel_view::ChannelView, is_channels_feature_enabled, render_avatar, ChatPanelSettings,
|
||||
// };
|
||||
// use anyhow::Result;
|
||||
// use call::ActiveCall;
|
||||
// use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
|
||||
// use client::Client;
|
||||
// use collections::HashMap;
|
||||
// use db::kvp::KEY_VALUE_STORE;
|
||||
// use editor::Editor;
|
||||
// use gpui::{
|
||||
// actions,
|
||||
// elements::*,
|
||||
// platform::{CursorStyle, MouseButton},
|
||||
// serde_json,
|
||||
// views::{ItemType, Select, SelectStyle},
|
||||
// AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View,
|
||||
// ViewContext, ViewHandle, WeakViewHandle,
|
||||
// };
|
||||
// use language::LanguageRegistry;
|
||||
// use menu::Confirm;
|
||||
// use message_editor::MessageEditor;
|
||||
// use project::Fs;
|
||||
// use rich_text::RichText;
|
||||
// use serde::{Deserialize, Serialize};
|
||||
// use settings::SettingsStore;
|
||||
// use std::sync::Arc;
|
||||
// use theme::{IconButton, Theme};
|
||||
// use time::{OffsetDateTime, UtcOffset};
|
||||
// use util::{ResultExt, TryFutureExt};
|
||||
// use workspace::{
|
||||
// dock::{DockPosition, Panel},
|
||||
// Workspace,
|
||||
// };
|
||||
|
||||
// mod message_editor;
|
||||
|
||||
// const MESSAGE_LOADING_THRESHOLD: usize = 50;
|
||||
// const CHAT_PANEL_KEY: &'static str = "ChatPanel";
|
||||
|
||||
// pub struct ChatPanel {
|
||||
// client: Arc<Client>,
|
||||
// channel_store: ModelHandle<ChannelStore>,
|
||||
// languages: Arc<LanguageRegistry>,
|
||||
// active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
|
||||
// message_list: ListState<ChatPanel>,
|
||||
// input_editor: ViewHandle<MessageEditor>,
|
||||
// channel_select: ViewHandle<Select>,
|
||||
// local_timezone: UtcOffset,
|
||||
// fs: Arc<dyn Fs>,
|
||||
// width: Option<f32>,
|
||||
// active: bool,
|
||||
// pending_serialization: Task<Option<()>>,
|
||||
// subscriptions: Vec<gpui::Subscription>,
|
||||
// workspace: WeakViewHandle<Workspace>,
|
||||
// is_scrolled_to_bottom: bool,
|
||||
// has_focus: bool,
|
||||
// markdown_data: HashMap<ChannelMessageId, RichText>,
|
||||
// }
|
||||
|
||||
// #[derive(Serialize, Deserialize)]
|
||||
// struct SerializedChatPanel {
|
||||
// width: Option<f32>,
|
||||
// }
|
||||
|
||||
// #[derive(Debug)]
|
||||
// pub enum Event {
|
||||
// DockPositionChanged,
|
||||
// Focus,
|
||||
// Dismissed,
|
||||
// }
|
||||
|
||||
// actions!(
|
||||
// chat_panel,
|
||||
// [LoadMoreMessages, ToggleFocus, OpenChannelNotes, JoinCall]
|
||||
// );
|
||||
|
||||
// pub fn init(cx: &mut AppContext) {
|
||||
// cx.add_action(ChatPanel::send);
|
||||
// cx.add_action(ChatPanel::load_more_messages);
|
||||
// cx.add_action(ChatPanel::open_notes);
|
||||
// cx.add_action(ChatPanel::join_call);
|
||||
// }
|
||||
|
||||
// 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 = ChannelStore::global(cx);
|
||||
// let languages = workspace.app_state().languages.clone();
|
||||
|
||||
// let input_editor = cx.add_view(|cx| {
|
||||
// MessageEditor::new(
|
||||
// languages.clone(),
|
||||
// channel_store.clone(),
|
||||
// cx.add_view(|cx| {
|
||||
// Editor::auto_height(
|
||||
// 4,
|
||||
// Some(Arc::new(|theme| theme.chat_panel.input_editor.clone())),
|
||||
// cx,
|
||||
// )
|
||||
// }),
|
||||
// cx,
|
||||
// )
|
||||
// });
|
||||
|
||||
// let workspace_handle = workspace.weak_handle();
|
||||
|
||||
// let channel_select = cx.add_view(|cx| {
|
||||
// let channel_store = channel_store.clone();
|
||||
// let workspace = workspace_handle.clone();
|
||||
// Select::new(0, cx, {
|
||||
// move |ix, item_type, is_hovered, cx| {
|
||||
// Self::render_channel_name(
|
||||
// &channel_store,
|
||||
// ix,
|
||||
// item_type,
|
||||
// is_hovered,
|
||||
// workspace,
|
||||
// cx,
|
||||
// )
|
||||
// }
|
||||
// })
|
||||
// .with_style(move |cx| {
|
||||
// let style = &theme::current(cx).chat_panel.channel_select;
|
||||
// SelectStyle {
|
||||
// header: Default::default(),
|
||||
// menu: style.menu,
|
||||
// }
|
||||
// })
|
||||
// });
|
||||
|
||||
// let mut message_list =
|
||||
// ListState::<Self>::new(0, Orientation::Bottom, 10., move |this, ix, cx| {
|
||||
// this.render_message(ix, cx)
|
||||
// });
|
||||
// message_list.set_scroll_handler(|visible_range, count, this, cx| {
|
||||
// if visible_range.start < MESSAGE_LOADING_THRESHOLD {
|
||||
// this.load_more_messages(&LoadMoreMessages, cx);
|
||||
// }
|
||||
// this.is_scrolled_to_bottom = visible_range.end == count;
|
||||
// });
|
||||
|
||||
// cx.add_view(|cx| {
|
||||
// let mut this = Self {
|
||||
// fs,
|
||||
// client,
|
||||
// channel_store,
|
||||
// languages,
|
||||
// active_chat: Default::default(),
|
||||
// pending_serialization: Task::ready(None),
|
||||
// message_list,
|
||||
// input_editor,
|
||||
// channel_select,
|
||||
// local_timezone: cx.platform().local_timezone(),
|
||||
// has_focus: false,
|
||||
// subscriptions: Vec::new(),
|
||||
// workspace: workspace_handle,
|
||||
// is_scrolled_to_bottom: true,
|
||||
// active: false,
|
||||
// width: None,
|
||||
// markdown_data: Default::default(),
|
||||
// };
|
||||
|
||||
// let mut old_dock_position = this.position(cx);
|
||||
// this.subscriptions
|
||||
// .push(
|
||||
// cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
|
||||
// let new_dock_position = this.position(cx);
|
||||
// if new_dock_position != old_dock_position {
|
||||
// old_dock_position = new_dock_position;
|
||||
// cx.emit(Event::DockPositionChanged);
|
||||
// }
|
||||
// cx.notify();
|
||||
// }),
|
||||
// );
|
||||
|
||||
// this.update_channel_count(cx);
|
||||
// cx.observe(&this.channel_store, |this, _, cx| {
|
||||
// this.update_channel_count(cx)
|
||||
// })
|
||||
// .detach();
|
||||
|
||||
// cx.observe(&this.channel_select, |this, channel_select, cx| {
|
||||
// let selected_ix = channel_select.read(cx).selected_index();
|
||||
|
||||
// let selected_channel_id = this
|
||||
// .channel_store
|
||||
// .read(cx)
|
||||
// .channel_at(selected_ix)
|
||||
// .map(|e| e.id);
|
||||
// if let Some(selected_channel_id) = selected_channel_id {
|
||||
// this.select_channel(selected_channel_id, None, cx)
|
||||
// .detach_and_log_err(cx);
|
||||
// }
|
||||
// })
|
||||
// .detach();
|
||||
|
||||
// this
|
||||
// })
|
||||
// }
|
||||
|
||||
// pub fn is_scrolled_to_bottom(&self) -> bool {
|
||||
// self.is_scrolled_to_bottom
|
||||
// }
|
||||
|
||||
// pub fn active_chat(&self) -> Option<ModelHandle<ChannelChat>> {
|
||||
// self.active_chat.as_ref().map(|(chat, _)| chat.clone())
|
||||
// }
|
||||
|
||||
// pub fn load(
|
||||
// workspace: WeakViewHandle<Workspace>,
|
||||
// cx: AsyncAppContext,
|
||||
// ) -> Task<Result<ViewHandle<Self>>> {
|
||||
// cx.spawn(|mut cx| async move {
|
||||
// let serialized_panel = if let Some(panel) = cx
|
||||
// .background()
|
||||
// .spawn(async move { KEY_VALUE_STORE.read_kvp(CHAT_PANEL_KEY) })
|
||||
// .await
|
||||
// .log_err()
|
||||
// .flatten()
|
||||
// {
|
||||
// Some(serde_json::from_str::<SerializedChatPanel>(&panel)?)
|
||||
// } else {
|
||||
// None
|
||||
// };
|
||||
|
||||
// workspace.update(&mut cx, |workspace, cx| {
|
||||
// let panel = Self::new(workspace, cx);
|
||||
// if let Some(serialized_panel) = serialized_panel {
|
||||
// panel.update(cx, |panel, cx| {
|
||||
// panel.width = serialized_panel.width;
|
||||
// cx.notify();
|
||||
// });
|
||||
// }
|
||||
// panel
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
|
||||
// fn serialize(&mut self, cx: &mut ViewContext<Self>) {
|
||||
// let width = self.width;
|
||||
// self.pending_serialization = cx.background().spawn(
|
||||
// async move {
|
||||
// KEY_VALUE_STORE
|
||||
// .write_kvp(
|
||||
// CHAT_PANEL_KEY.into(),
|
||||
// serde_json::to_string(&SerializedChatPanel { width })?,
|
||||
// )
|
||||
// .await?;
|
||||
// anyhow::Ok(())
|
||||
// }
|
||||
// .log_err(),
|
||||
// );
|
||||
// }
|
||||
|
||||
// fn update_channel_count(&mut self, cx: &mut ViewContext<Self>) {
|
||||
// let channel_count = self.channel_store.read(cx).channel_count();
|
||||
// self.channel_select.update(cx, |select, cx| {
|
||||
// select.set_item_count(channel_count, cx);
|
||||
// });
|
||||
// }
|
||||
|
||||
// fn set_active_chat(&mut self, chat: ModelHandle<ChannelChat>, cx: &mut ViewContext<Self>) {
|
||||
// if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
|
||||
// let channel_id = chat.read(cx).channel_id;
|
||||
// {
|
||||
// self.markdown_data.clear();
|
||||
// let chat = chat.read(cx);
|
||||
// self.message_list.reset(chat.message_count());
|
||||
|
||||
// let channel_name = chat.channel(cx).map(|channel| channel.name.clone());
|
||||
// self.input_editor.update(cx, |editor, cx| {
|
||||
// editor.set_channel(channel_id, channel_name, cx);
|
||||
// });
|
||||
// };
|
||||
// let subscription = cx.subscribe(&chat, Self::channel_did_change);
|
||||
// self.active_chat = Some((chat, subscription));
|
||||
// self.acknowledge_last_message(cx);
|
||||
// self.channel_select.update(cx, |select, cx| {
|
||||
// if let Some(ix) = self.channel_store.read(cx).index_of_channel(channel_id) {
|
||||
// select.set_selected_index(ix, cx);
|
||||
// }
|
||||
// });
|
||||
// cx.notify();
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn channel_did_change(
|
||||
// &mut self,
|
||||
// _: ModelHandle<ChannelChat>,
|
||||
// event: &ChannelChatEvent,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) {
|
||||
// match event {
|
||||
// ChannelChatEvent::MessagesUpdated {
|
||||
// old_range,
|
||||
// new_count,
|
||||
// } => {
|
||||
// self.message_list.splice(old_range.clone(), *new_count);
|
||||
// if self.active {
|
||||
// self.acknowledge_last_message(cx);
|
||||
// }
|
||||
// }
|
||||
// ChannelChatEvent::NewMessage {
|
||||
// channel_id,
|
||||
// message_id,
|
||||
// } => {
|
||||
// if !self.active {
|
||||
// self.channel_store.update(cx, |store, cx| {
|
||||
// store.new_message(*channel_id, *message_id, cx)
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// cx.notify();
|
||||
// }
|
||||
|
||||
// fn acknowledge_last_message(&mut self, cx: &mut ViewContext<'_, '_, ChatPanel>) {
|
||||
// if self.active && self.is_scrolled_to_bottom {
|
||||
// if let Some((chat, _)) = &self.active_chat {
|
||||
// chat.update(cx, |chat, cx| {
|
||||
// chat.acknowledge_last_message(cx);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
// let theme = theme::current(cx);
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// ChildView::new(&self.channel_select, cx)
|
||||
// .contained()
|
||||
// .with_style(theme.chat_panel.channel_select.container),
|
||||
// )
|
||||
// .with_child(self.render_active_channel_messages(&theme))
|
||||
// .with_child(self.render_input_box(&theme, cx))
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn render_active_channel_messages(&self, theme: &Arc<Theme>) -> AnyElement<Self> {
|
||||
// let messages = if self.active_chat.is_some() {
|
||||
// List::new(self.message_list.clone())
|
||||
// .contained()
|
||||
// .with_style(theme.chat_panel.list)
|
||||
// .into_any()
|
||||
// } else {
|
||||
// Empty::new().into_any()
|
||||
// };
|
||||
|
||||
// messages.flex(1., true).into_any()
|
||||
// }
|
||||
|
||||
// fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
// let (message, is_continuation, is_last, is_admin) = self
|
||||
// .active_chat
|
||||
// .as_ref()
|
||||
// .unwrap()
|
||||
// .0
|
||||
// .update(cx, |active_chat, cx| {
|
||||
// let is_admin = self
|
||||
// .channel_store
|
||||
// .read(cx)
|
||||
// .is_channel_admin(active_chat.channel_id);
|
||||
|
||||
// let last_message = active_chat.message(ix.saturating_sub(1));
|
||||
// let this_message = active_chat.message(ix).clone();
|
||||
// let is_continuation = last_message.id != this_message.id
|
||||
// && this_message.sender.id == last_message.sender.id;
|
||||
|
||||
// if let ChannelMessageId::Saved(id) = this_message.id {
|
||||
// if this_message
|
||||
// .mentions
|
||||
// .iter()
|
||||
// .any(|(_, user_id)| Some(*user_id) == self.client.user_id())
|
||||
// {
|
||||
// active_chat.acknowledge_message(id);
|
||||
// }
|
||||
// }
|
||||
|
||||
// (
|
||||
// this_message,
|
||||
// is_continuation,
|
||||
// active_chat.message_count() == ix + 1,
|
||||
// is_admin,
|
||||
// )
|
||||
// });
|
||||
|
||||
// let is_pending = message.is_pending();
|
||||
// let theme = theme::current(cx);
|
||||
// let text = self.markdown_data.entry(message.id).or_insert_with(|| {
|
||||
// Self::render_markdown_with_mentions(&self.languages, self.client.id(), &message)
|
||||
// });
|
||||
|
||||
// let now = OffsetDateTime::now_utc();
|
||||
|
||||
// let style = if is_pending {
|
||||
// &theme.chat_panel.pending_message
|
||||
// } else if is_continuation {
|
||||
// &theme.chat_panel.continuation_message
|
||||
// } else {
|
||||
// &theme.chat_panel.message
|
||||
// };
|
||||
|
||||
// let belongs_to_user = Some(message.sender.id) == self.client.user_id();
|
||||
// let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
|
||||
// (message.id, belongs_to_user || is_admin)
|
||||
// {
|
||||
// Some(id)
|
||||
// } else {
|
||||
// None
|
||||
// };
|
||||
|
||||
// enum MessageBackgroundHighlight {}
|
||||
// MouseEventHandler::new::<MessageBackgroundHighlight, _>(ix, cx, |state, cx| {
|
||||
// let container = style.style_for(state);
|
||||
// if is_continuation {
|
||||
// Flex::row()
|
||||
// .with_child(
|
||||
// text.element(
|
||||
// theme.editor.syntax.clone(),
|
||||
// theme.chat_panel.rich_text.clone(),
|
||||
// cx,
|
||||
// )
|
||||
// .flex(1., true),
|
||||
// )
|
||||
// .with_child(render_remove(message_id_to_remove, cx, &theme))
|
||||
// .contained()
|
||||
// .with_style(*container)
|
||||
// .with_margin_bottom(if is_last {
|
||||
// theme.chat_panel.last_message_bottom_spacing
|
||||
// } else {
|
||||
// 0.
|
||||
// })
|
||||
// .into_any()
|
||||
// } else {
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// Flex::row()
|
||||
// .with_child(
|
||||
// Flex::row()
|
||||
// .with_child(render_avatar(
|
||||
// message.sender.avatar.clone(),
|
||||
// &theme.chat_panel.avatar,
|
||||
// theme.chat_panel.avatar_container,
|
||||
// ))
|
||||
// .with_child(
|
||||
// Label::new(
|
||||
// message.sender.github_login.clone(),
|
||||
// theme.chat_panel.message_sender.text.clone(),
|
||||
// )
|
||||
// .contained()
|
||||
// .with_style(theme.chat_panel.message_sender.container),
|
||||
// )
|
||||
// .with_child(
|
||||
// Label::new(
|
||||
// format_timestamp(
|
||||
// message.timestamp,
|
||||
// now,
|
||||
// self.local_timezone,
|
||||
// ),
|
||||
// theme.chat_panel.message_timestamp.text.clone(),
|
||||
// )
|
||||
// .contained()
|
||||
// .with_style(theme.chat_panel.message_timestamp.container),
|
||||
// )
|
||||
// .align_children_center()
|
||||
// .flex(1., true),
|
||||
// )
|
||||
// .with_child(render_remove(message_id_to_remove, cx, &theme))
|
||||
// .align_children_center(),
|
||||
// )
|
||||
// .with_child(
|
||||
// Flex::row()
|
||||
// .with_child(
|
||||
// text.element(
|
||||
// theme.editor.syntax.clone(),
|
||||
// theme.chat_panel.rich_text.clone(),
|
||||
// cx,
|
||||
// )
|
||||
// .flex(1., true),
|
||||
// )
|
||||
// // Add a spacer to make everything line up
|
||||
// .with_child(render_remove(None, cx, &theme)),
|
||||
// )
|
||||
// .contained()
|
||||
// .with_style(*container)
|
||||
// .with_margin_bottom(if is_last {
|
||||
// theme.chat_panel.last_message_bottom_spacing
|
||||
// } else {
|
||||
// 0.
|
||||
// })
|
||||
// .into_any()
|
||||
// }
|
||||
// })
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn render_markdown_with_mentions(
|
||||
// language_registry: &Arc<LanguageRegistry>,
|
||||
// current_user_id: u64,
|
||||
// message: &channel::ChannelMessage,
|
||||
// ) -> RichText {
|
||||
// let mentions = message
|
||||
// .mentions
|
||||
// .iter()
|
||||
// .map(|(range, user_id)| rich_text::Mention {
|
||||
// range: range.clone(),
|
||||
// is_self_mention: *user_id == current_user_id,
|
||||
// })
|
||||
// .collect::<Vec<_>>();
|
||||
|
||||
// rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None)
|
||||
// }
|
||||
|
||||
// fn render_input_box(&self, theme: &Arc<Theme>, cx: &AppContext) -> AnyElement<Self> {
|
||||
// ChildView::new(&self.input_editor, cx)
|
||||
// .contained()
|
||||
// .with_style(theme.chat_panel.input_editor.container)
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn render_channel_name(
|
||||
// channel_store: &ModelHandle<ChannelStore>,
|
||||
// ix: usize,
|
||||
// item_type: ItemType,
|
||||
// is_hovered: bool,
|
||||
// workspace: WeakViewHandle<Workspace>,
|
||||
// cx: &mut ViewContext<Select>,
|
||||
// ) -> AnyElement<Select> {
|
||||
// let theme = theme::current(cx);
|
||||
// let tooltip_style = &theme.tooltip;
|
||||
// let theme = &theme.chat_panel;
|
||||
// let style = match (&item_type, is_hovered) {
|
||||
// (ItemType::Header, _) => &theme.channel_select.header,
|
||||
// (ItemType::Selected, _) => &theme.channel_select.active_item,
|
||||
// (ItemType::Unselected, false) => &theme.channel_select.item,
|
||||
// (ItemType::Unselected, true) => &theme.channel_select.hovered_item,
|
||||
// };
|
||||
|
||||
// let channel = &channel_store.read(cx).channel_at(ix).unwrap();
|
||||
// let channel_id = channel.id;
|
||||
|
||||
// let mut row = Flex::row()
|
||||
// .with_child(
|
||||
// Label::new("#".to_string(), style.hash.text.clone())
|
||||
// .contained()
|
||||
// .with_style(style.hash.container),
|
||||
// )
|
||||
// .with_child(Label::new(channel.name.clone(), style.name.clone()));
|
||||
|
||||
// if matches!(item_type, ItemType::Header) {
|
||||
// row.add_children([
|
||||
// MouseEventHandler::new::<OpenChannelNotes, _>(0, cx, |mouse_state, _| {
|
||||
// render_icon_button(theme.icon_button.style_for(mouse_state), "icons/file.svg")
|
||||
// })
|
||||
// .on_click(MouseButton::Left, move |_, _, cx| {
|
||||
// if let Some(workspace) = workspace.upgrade(cx) {
|
||||
// ChannelView::open(channel_id, workspace, cx).detach();
|
||||
// }
|
||||
// })
|
||||
// .with_tooltip::<OpenChannelNotes>(
|
||||
// channel_id as usize,
|
||||
// "Open Notes",
|
||||
// Some(Box::new(OpenChannelNotes)),
|
||||
// tooltip_style.clone(),
|
||||
// cx,
|
||||
// )
|
||||
// .flex_float(),
|
||||
// MouseEventHandler::new::<ActiveCall, _>(0, cx, |mouse_state, _| {
|
||||
// render_icon_button(
|
||||
// theme.icon_button.style_for(mouse_state),
|
||||
// "icons/speaker-loud.svg",
|
||||
// )
|
||||
// })
|
||||
// .on_click(MouseButton::Left, move |_, _, cx| {
|
||||
// ActiveCall::global(cx)
|
||||
// .update(cx, |call, cx| call.join_channel(channel_id, cx))
|
||||
// .detach_and_log_err(cx);
|
||||
// })
|
||||
// .with_tooltip::<ActiveCall>(
|
||||
// channel_id as usize,
|
||||
// "Join Call",
|
||||
// Some(Box::new(JoinCall)),
|
||||
// tooltip_style.clone(),
|
||||
// cx,
|
||||
// )
|
||||
// .flex_float(),
|
||||
// ]);
|
||||
// }
|
||||
|
||||
// row.align_children_center()
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn render_sign_in_prompt(
|
||||
// &self,
|
||||
// theme: &Arc<Theme>,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> AnyElement<Self> {
|
||||
// enum SignInPromptLabel {}
|
||||
|
||||
// MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
|
||||
// Label::new(
|
||||
// "Sign in to use chat".to_string(),
|
||||
// theme
|
||||
// .chat_panel
|
||||
// .sign_in_prompt
|
||||
// .style_for(mouse_state)
|
||||
// .clone(),
|
||||
// )
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||
// let client = this.client.clone();
|
||||
// cx.spawn(|this, mut cx| async move {
|
||||
// if client
|
||||
// .authenticate_and_connect(true, &cx)
|
||||
// .log_err()
|
||||
// .await
|
||||
// .is_some()
|
||||
// {
|
||||
// this.update(&mut cx, |this, cx| {
|
||||
// if cx.handle().is_focused(cx) {
|
||||
// cx.focus(&this.input_editor);
|
||||
// }
|
||||
// })
|
||||
// .ok();
|
||||
// }
|
||||
// })
|
||||
// .detach();
|
||||
// })
|
||||
// .aligned()
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||
// if let Some((chat, _)) = self.active_chat.as_ref() {
|
||||
// let message = self
|
||||
// .input_editor
|
||||
// .update(cx, |editor, cx| editor.take_message(cx));
|
||||
|
||||
// if let Some(task) = chat
|
||||
// .update(cx, |chat, cx| chat.send_message(message, cx))
|
||||
// .log_err()
|
||||
// {
|
||||
// task.detach();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn remove_message(&mut self, id: u64, cx: &mut ViewContext<Self>) {
|
||||
// if let Some((chat, _)) = self.active_chat.as_ref() {
|
||||
// chat.update(cx, |chat, cx| chat.remove_message(id, cx).detach())
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
|
||||
// if let Some((chat, _)) = self.active_chat.as_ref() {
|
||||
// chat.update(cx, |channel, cx| {
|
||||
// if let Some(task) = channel.load_more_messages(cx) {
|
||||
// task.detach();
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
// pub fn select_channel(
|
||||
// &mut self,
|
||||
// selected_channel_id: u64,
|
||||
// scroll_to_message_id: Option<u64>,
|
||||
// cx: &mut ViewContext<ChatPanel>,
|
||||
// ) -> Task<Result<()>> {
|
||||
// let open_chat = self
|
||||
// .active_chat
|
||||
// .as_ref()
|
||||
// .and_then(|(chat, _)| {
|
||||
// (chat.read(cx).channel_id == selected_channel_id)
|
||||
// .then(|| Task::ready(anyhow::Ok(chat.clone())))
|
||||
// })
|
||||
// .unwrap_or_else(|| {
|
||||
// self.channel_store.update(cx, |store, cx| {
|
||||
// store.open_channel_chat(selected_channel_id, cx)
|
||||
// })
|
||||
// });
|
||||
|
||||
// cx.spawn(|this, mut cx| async move {
|
||||
// let chat = open_chat.await?;
|
||||
// this.update(&mut cx, |this, cx| {
|
||||
// this.set_active_chat(chat.clone(), cx);
|
||||
// })?;
|
||||
|
||||
// if let Some(message_id) = scroll_to_message_id {
|
||||
// if let Some(item_ix) =
|
||||
// ChannelChat::load_history_since_message(chat.clone(), message_id, cx.clone())
|
||||
// .await
|
||||
// {
|
||||
// this.update(&mut cx, |this, cx| {
|
||||
// if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) {
|
||||
// this.message_list.scroll_to(ListOffset {
|
||||
// item_ix,
|
||||
// offset_in_item: 0.,
|
||||
// });
|
||||
// cx.notify();
|
||||
// }
|
||||
// })?;
|
||||
// }
|
||||
// }
|
||||
|
||||
// Ok(())
|
||||
// })
|
||||
// }
|
||||
|
||||
// fn open_notes(&mut self, _: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
|
||||
// if let Some((chat, _)) = &self.active_chat {
|
||||
// let channel_id = chat.read(cx).channel_id;
|
||||
// if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
// ChannelView::open(channel_id, workspace, cx).detach();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn join_call(&mut self, _: &JoinCall, cx: &mut ViewContext<Self>) {
|
||||
// if let Some((chat, _)) = &self.active_chat {
|
||||
// let channel_id = chat.read(cx).channel_id;
|
||||
// ActiveCall::global(cx)
|
||||
// .update(cx, |call, cx| call.join_channel(channel_id, cx))
|
||||
// .detach_and_log_err(cx);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn render_remove(
|
||||
// message_id_to_remove: Option<u64>,
|
||||
// cx: &mut ViewContext<'_, '_, ChatPanel>,
|
||||
// theme: &Arc<Theme>,
|
||||
// ) -> AnyElement<ChatPanel> {
|
||||
// enum DeleteMessage {}
|
||||
|
||||
// message_id_to_remove
|
||||
// .map(|id| {
|
||||
// MouseEventHandler::new::<DeleteMessage, _>(id as usize, cx, |mouse_state, _| {
|
||||
// let button_style = theme.chat_panel.icon_button.style_for(mouse_state);
|
||||
// render_icon_button(button_style, "icons/x.svg")
|
||||
// .aligned()
|
||||
// .into_any()
|
||||
// })
|
||||
// .with_padding(Padding::uniform(2.))
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||
// this.remove_message(id, cx);
|
||||
// })
|
||||
// .flex_float()
|
||||
// .into_any()
|
||||
// })
|
||||
// .unwrap_or_else(|| {
|
||||
// let style = theme.chat_panel.icon_button.default;
|
||||
|
||||
// Empty::new()
|
||||
// .constrained()
|
||||
// .with_width(style.icon_width)
|
||||
// .aligned()
|
||||
// .constrained()
|
||||
// .with_width(style.button_width)
|
||||
// .with_height(style.button_width)
|
||||
// .contained()
|
||||
// .with_uniform_padding(2.)
|
||||
// .flex_float()
|
||||
// .into_any()
|
||||
// })
|
||||
// }
|
||||
|
||||
// impl Entity for ChatPanel {
|
||||
// type Event = Event;
|
||||
// }
|
||||
|
||||
// impl View for ChatPanel {
|
||||
// fn ui_name() -> &'static str {
|
||||
// "ChatPanel"
|
||||
// }
|
||||
|
||||
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
// let theme = theme::current(cx);
|
||||
// let element = if self.client.user_id().is_some() {
|
||||
// self.render_channel(cx)
|
||||
// } else {
|
||||
// self.render_sign_in_prompt(&theme, cx)
|
||||
// };
|
||||
// element
|
||||
// .contained()
|
||||
// .with_style(theme.chat_panel.container)
|
||||
// .constrained()
|
||||
// .with_min_width(150.)
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
// self.has_focus = true;
|
||||
// if matches!(
|
||||
// *self.client.status().borrow(),
|
||||
// client::Status::Connected { .. }
|
||||
// ) {
|
||||
// let editor = self.input_editor.read(cx).editor.clone();
|
||||
// cx.focus(&editor);
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||
// self.has_focus = false;
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl Panel for ChatPanel {
|
||||
// fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
|
||||
// settings::get::<ChatPanelSettings>(cx).dock
|
||||
// }
|
||||
|
||||
// fn position_is_valid(&self, position: DockPosition) -> bool {
|
||||
// matches!(position, DockPosition::Left | DockPosition::Right)
|
||||
// }
|
||||
|
||||
// fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
|
||||
// settings::update_settings_file::<ChatPanelSettings>(self.fs.clone(), cx, move |settings| {
|
||||
// settings.dock = Some(position)
|
||||
// });
|
||||
// }
|
||||
|
||||
// fn size(&self, cx: &gpui::WindowContext) -> f32 {
|
||||
// self.width
|
||||
// .unwrap_or_else(|| settings::get::<ChatPanelSettings>(cx).default_width)
|
||||
// }
|
||||
|
||||
// fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
|
||||
// self.width = size;
|
||||
// self.serialize(cx);
|
||||
// cx.notify();
|
||||
// }
|
||||
|
||||
// fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||
// self.active = active;
|
||||
// if active {
|
||||
// self.acknowledge_last_message(cx);
|
||||
// if !is_channels_feature_enabled(cx) {
|
||||
// cx.emit(Event::Dismissed);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
|
||||
// (settings::get::<ChatPanelSettings>(cx).button && is_channels_feature_enabled(cx))
|
||||
// .then(|| "icons/conversations.svg")
|
||||
// }
|
||||
|
||||
// fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
|
||||
// ("Chat Panel".to_string(), Some(Box::new(ToggleFocus)))
|
||||
// }
|
||||
|
||||
// fn should_change_position_on_event(event: &Self::Event) -> bool {
|
||||
// matches!(event, Event::DockPositionChanged)
|
||||
// }
|
||||
|
||||
// fn should_close_on_event(event: &Self::Event) -> bool {
|
||||
// matches!(event, Event::Dismissed)
|
||||
// }
|
||||
|
||||
// fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
|
||||
// self.has_focus
|
||||
// }
|
||||
|
||||
// fn is_focus_event(event: &Self::Event) -> bool {
|
||||
// matches!(event, Event::Focus)
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn format_timestamp(
|
||||
// mut timestamp: OffsetDateTime,
|
||||
// mut now: OffsetDateTime,
|
||||
// local_timezone: UtcOffset,
|
||||
// ) -> String {
|
||||
// timestamp = timestamp.to_offset(local_timezone);
|
||||
// now = now.to_offset(local_timezone);
|
||||
|
||||
// let today = now.date();
|
||||
// let date = timestamp.date();
|
||||
// let mut hour = timestamp.hour();
|
||||
// let mut part = "am";
|
||||
// if hour > 12 {
|
||||
// hour -= 12;
|
||||
// part = "pm";
|
||||
// }
|
||||
// if date == today {
|
||||
// format!("{:02}:{:02}{}", hour, timestamp.minute(), part)
|
||||
// } else if date.next_day() == Some(today) {
|
||||
// format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part)
|
||||
// } else {
|
||||
// format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn render_icon_button<V: View>(style: &IconButton, svg_path: &'static str) -> impl Element<V> {
|
||||
// Svg::new(svg_path)
|
||||
// .with_color(style.color)
|
||||
// .constrained()
|
||||
// .with_width(style.icon_width)
|
||||
// .aligned()
|
||||
// .constrained()
|
||||
// .with_width(style.button_width)
|
||||
// .with_height(style.button_width)
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// }
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use super::*;
|
||||
// use gpui::fonts::HighlightStyle;
|
||||
// use pretty_assertions::assert_eq;
|
||||
// use rich_text::{BackgroundKind, Highlight, RenderedRegion};
|
||||
// use util::test::marked_text_ranges;
|
||||
|
||||
// #[gpui::test]
|
||||
// fn test_render_markdown_with_mentions() {
|
||||
// let language_registry = Arc::new(LanguageRegistry::test());
|
||||
// let (body, ranges) = marked_text_ranges("*hi*, «@abc», let's **call** «@fgh»", false);
|
||||
// let message = channel::ChannelMessage {
|
||||
// id: ChannelMessageId::Saved(0),
|
||||
// body,
|
||||
// timestamp: OffsetDateTime::now_utc(),
|
||||
// sender: Arc::new(client::User {
|
||||
// github_login: "fgh".into(),
|
||||
// avatar: None,
|
||||
// id: 103,
|
||||
// }),
|
||||
// nonce: 5,
|
||||
// mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
|
||||
// };
|
||||
|
||||
// let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
|
||||
|
||||
// // Note that the "'" was replaced with ’ due to smart punctuation.
|
||||
// let (body, ranges) = marked_text_ranges("«hi», «@abc», let’s «call» «@fgh»", false);
|
||||
// assert_eq!(message.text, body);
|
||||
// assert_eq!(
|
||||
// message.highlights,
|
||||
// vec![
|
||||
// (
|
||||
// ranges[0].clone(),
|
||||
// HighlightStyle {
|
||||
// italic: Some(true),
|
||||
// ..Default::default()
|
||||
// }
|
||||
// .into()
|
||||
// ),
|
||||
// (ranges[1].clone(), Highlight::Mention),
|
||||
// (
|
||||
// ranges[2].clone(),
|
||||
// HighlightStyle {
|
||||
// weight: Some(gpui::fonts::Weight::BOLD),
|
||||
// ..Default::default()
|
||||
// }
|
||||
// .into()
|
||||
// ),
|
||||
// (ranges[3].clone(), Highlight::SelfMention)
|
||||
// ]
|
||||
// );
|
||||
// assert_eq!(
|
||||
// message.regions,
|
||||
// vec![
|
||||
// RenderedRegion {
|
||||
// background_kind: Some(BackgroundKind::Mention),
|
||||
// link_url: None
|
||||
// },
|
||||
// RenderedRegion {
|
||||
// background_kind: Some(BackgroundKind::SelfMention),
|
||||
// link_url: None
|
||||
// },
|
||||
// ]
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
313
crates/collab_ui2/src/chat_panel/message_editor.rs
Normal file
313
crates/collab_ui2/src/chat_panel/message_editor.rs
Normal file
@@ -0,0 +1,313 @@
|
||||
use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
|
||||
use client::UserId;
|
||||
use collections::HashMap;
|
||||
use editor::{AnchorRangeExt, Editor};
|
||||
use gpui::{
|
||||
elements::ChildView, AnyElement, AsyncAppContext, Element, Entity, ModelHandle, Task, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
|
||||
use lazy_static::lazy_static;
|
||||
use project::search::SearchQuery;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
|
||||
|
||||
lazy_static! {
|
||||
static ref MENTIONS_SEARCH: SearchQuery = SearchQuery::regex(
|
||||
"@[-_\\w]+",
|
||||
false,
|
||||
false,
|
||||
Default::default(),
|
||||
Default::default()
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub struct MessageEditor {
|
||||
pub editor: ViewHandle<Editor>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
users: HashMap<String, UserId>,
|
||||
mentions: Vec<UserId>,
|
||||
mentions_task: Option<Task<()>>,
|
||||
channel_id: Option<ChannelId>,
|
||||
}
|
||||
|
||||
impl MessageEditor {
|
||||
pub fn new(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
editor: ViewHandle<Editor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
});
|
||||
|
||||
let buffer = editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.expect("message editor must be singleton");
|
||||
|
||||
cx.subscribe(&buffer, Self::on_buffer_event).detach();
|
||||
|
||||
let markdown = language_registry.language_for_name("Markdown");
|
||||
cx.app_context()
|
||||
.spawn(|mut cx| async move {
|
||||
let markdown = markdown.await?;
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_language(Some(markdown), cx)
|
||||
});
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
Self {
|
||||
editor,
|
||||
channel_store,
|
||||
users: HashMap::default(),
|
||||
channel_id: None,
|
||||
mentions: Vec::new(),
|
||||
mentions_task: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_channel(
|
||||
&mut self,
|
||||
channel_id: u64,
|
||||
channel_name: Option<String>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
if let Some(channel_name) = channel_name {
|
||||
editor.set_placeholder_text(format!("Message #{}", channel_name), cx);
|
||||
} else {
|
||||
editor.set_placeholder_text(format!("Message Channel"), cx);
|
||||
}
|
||||
});
|
||||
self.channel_id = Some(channel_id);
|
||||
self.refresh_users(cx);
|
||||
}
|
||||
|
||||
pub fn refresh_users(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(channel_id) = self.channel_id {
|
||||
let members = self.channel_store.update(cx, |store, cx| {
|
||||
store.get_channel_member_details(channel_id, cx)
|
||||
});
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let members = members.await?;
|
||||
this.update(&mut cx, |this, cx| this.set_members(members, cx))?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_members(&mut self, members: Vec<ChannelMembership>, _: &mut ViewContext<Self>) {
|
||||
self.users.clear();
|
||||
self.users.extend(
|
||||
members
|
||||
.into_iter()
|
||||
.map(|member| (member.user.github_login.clone(), member.user.id)),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn take_message(&mut self, cx: &mut ViewContext<Self>) -> MessageParams {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let highlights = editor.text_highlights::<Self>(cx);
|
||||
let text = editor.text(cx);
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let mentions = if let Some((_, ranges)) = highlights {
|
||||
ranges
|
||||
.iter()
|
||||
.map(|range| range.to_offset(&snapshot))
|
||||
.zip(self.mentions.iter().copied())
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
editor.clear(cx);
|
||||
self.mentions.clear();
|
||||
|
||||
MessageParams { text, mentions }
|
||||
})
|
||||
}
|
||||
|
||||
fn on_buffer_event(
|
||||
&mut self,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
event: &language::Event,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let language::Event::Reparsed | language::Event::Edited = event {
|
||||
let buffer = buffer.read(cx).snapshot();
|
||||
self.mentions_task = Some(cx.spawn(|this, cx| async move {
|
||||
cx.background().timer(MENTIONS_DEBOUNCE_INTERVAL).await;
|
||||
Self::find_mentions(this, buffer, cx).await;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async fn find_mentions(
|
||||
this: WeakViewHandle<MessageEditor>,
|
||||
buffer: BufferSnapshot,
|
||||
mut cx: AsyncAppContext,
|
||||
) {
|
||||
let (buffer, ranges) = cx
|
||||
.background()
|
||||
.spawn(async move {
|
||||
let ranges = MENTIONS_SEARCH.search(&buffer, None).await;
|
||||
(buffer, ranges)
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let mut anchor_ranges = Vec::new();
|
||||
let mut mentioned_user_ids = Vec::new();
|
||||
let mut text = String::new();
|
||||
|
||||
this.editor.update(cx, |editor, cx| {
|
||||
let multi_buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
for range in ranges {
|
||||
text.clear();
|
||||
text.extend(buffer.text_for_range(range.clone()));
|
||||
if let Some(username) = text.strip_prefix("@") {
|
||||
if let Some(user_id) = this.users.get(username) {
|
||||
let start = multi_buffer.anchor_after(range.start);
|
||||
let end = multi_buffer.anchor_after(range.end);
|
||||
|
||||
mentioned_user_ids.push(*user_id);
|
||||
anchor_ranges.push(start..end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editor.clear_highlights::<Self>(cx);
|
||||
editor.highlight_text::<Self>(
|
||||
anchor_ranges,
|
||||
theme::current(cx).chat_panel.rich_text.mention_highlight,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
this.mentions = mentioned_user_ids;
|
||||
this.mentions_task.take();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for MessageEditor {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for MessageEditor {
|
||||
fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
|
||||
ChildView::new(&self.editor, cx).into_any()
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if cx.is_self_focused() {
|
||||
cx.focus(&self.editor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use client::{Client, User, UserStore};
|
||||
use gpui::{TestAppContext, WindowHandle};
|
||||
use language::{Language, LanguageConfig};
|
||||
use rpc::proto;
|
||||
use settings::SettingsStore;
|
||||
use util::{http::FakeHttpClient, test::marked_text_ranges};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_message_editor(cx: &mut TestAppContext) {
|
||||
let editor = init_test(cx);
|
||||
let editor = editor.root(cx);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.set_members(
|
||||
vec![
|
||||
ChannelMembership {
|
||||
user: Arc::new(User {
|
||||
github_login: "a-b".into(),
|
||||
id: 101,
|
||||
avatar: None,
|
||||
}),
|
||||
kind: proto::channel_member::Kind::Member,
|
||||
role: proto::ChannelRole::Member,
|
||||
},
|
||||
ChannelMembership {
|
||||
user: Arc::new(User {
|
||||
github_login: "C_D".into(),
|
||||
id: 102,
|
||||
avatar: None,
|
||||
}),
|
||||
kind: proto::channel_member::Kind::Member,
|
||||
role: proto::ChannelRole::Member,
|
||||
},
|
||||
],
|
||||
cx,
|
||||
);
|
||||
|
||||
editor.editor.update(cx, |editor, cx| {
|
||||
editor.set_text("Hello, @a-b! Have you met @C_D?", cx)
|
||||
});
|
||||
});
|
||||
|
||||
cx.foreground().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let (text, ranges) = marked_text_ranges("Hello, «@a-b»! Have you met «@C_D»?", false);
|
||||
assert_eq!(
|
||||
editor.take_message(cx),
|
||||
MessageParams {
|
||||
text,
|
||||
mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) -> WindowHandle<MessageEditor> {
|
||||
cx.foreground().forbid_parking();
|
||||
|
||||
cx.update(|cx| {
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http.clone(), cx);
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
theme::init((), cx);
|
||||
language::init(cx);
|
||||
editor::init(cx);
|
||||
client::init(&client, cx);
|
||||
channel::init(&client, user_store, cx);
|
||||
});
|
||||
|
||||
let language_registry = Arc::new(LanguageRegistry::test());
|
||||
language_registry.add(Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Markdown".into(),
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_markdown::language()),
|
||||
)));
|
||||
|
||||
let editor = cx.add_window(|cx| {
|
||||
MessageEditor::new(
|
||||
language_registry,
|
||||
ChannelStore::global(cx),
|
||||
cx.add_view(|cx| Editor::auto_height(4, None, cx)),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
editor
|
||||
}
|
||||
}
|
||||
3575
crates/collab_ui2/src/collab_panel.rs
Normal file
3575
crates/collab_ui2/src/collab_panel.rs
Normal file
File diff suppressed because it is too large
Load Diff
717
crates/collab_ui2/src/collab_panel/channel_modal.rs
Normal file
717
crates/collab_ui2/src/collab_panel/channel_modal.rs
Normal file
@@ -0,0 +1,717 @@
|
||||
use channel::{ChannelId, ChannelMembership, ChannelStore};
|
||||
use client::{
|
||||
proto::{self, ChannelRole, ChannelVisibility},
|
||||
User, UserId, UserStore,
|
||||
};
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::*,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AppContext, ClipboardItem, Entity, ModelHandle, MouseState, Task, View, ViewContext,
|
||||
ViewHandle,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||
use std::sync::Arc;
|
||||
use util::TryFutureExt;
|
||||
use workspace::Modal;
|
||||
|
||||
actions!(
|
||||
channel_modal,
|
||||
[
|
||||
SelectNextControl,
|
||||
ToggleMode,
|
||||
ToggleMemberAdmin,
|
||||
RemoveMember
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
Picker::<ChannelModalDelegate>::init(cx);
|
||||
cx.add_action(ChannelModal::toggle_mode);
|
||||
cx.add_action(ChannelModal::toggle_member_admin);
|
||||
cx.add_action(ChannelModal::remove_member);
|
||||
cx.add_action(ChannelModal::dismiss);
|
||||
}
|
||||
|
||||
pub struct ChannelModal {
|
||||
picker: ViewHandle<Picker<ChannelModalDelegate>>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
channel_id: ChannelId,
|
||||
has_focus: bool,
|
||||
}
|
||||
|
||||
impl ChannelModal {
|
||||
pub fn new(
|
||||
user_store: ModelHandle<UserStore>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
channel_id: ChannelId,
|
||||
mode: Mode,
|
||||
members: Vec<ChannelMembership>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
|
||||
let picker = cx.add_view(|cx| {
|
||||
Picker::new(
|
||||
ChannelModalDelegate {
|
||||
matching_users: Vec::new(),
|
||||
matching_member_indices: Vec::new(),
|
||||
selected_index: 0,
|
||||
user_store: user_store.clone(),
|
||||
channel_store: channel_store.clone(),
|
||||
channel_id,
|
||||
match_candidates: Vec::new(),
|
||||
members,
|
||||
mode,
|
||||
context_menu: cx.add_view(|cx| {
|
||||
let mut menu = ContextMenu::new(cx.view_id(), cx);
|
||||
menu.set_position_mode(OverlayPositionMode::Local);
|
||||
menu
|
||||
}),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
|
||||
});
|
||||
|
||||
cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
|
||||
|
||||
let has_focus = picker.read(cx).has_focus();
|
||||
|
||||
Self {
|
||||
picker,
|
||||
channel_store,
|
||||
channel_id,
|
||||
has_focus,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_mode(&mut self, _: &ToggleMode, cx: &mut ViewContext<Self>) {
|
||||
let mode = match self.picker.read(cx).delegate().mode {
|
||||
Mode::ManageMembers => Mode::InviteMembers,
|
||||
Mode::InviteMembers => Mode::ManageMembers,
|
||||
};
|
||||
self.set_mode(mode, cx);
|
||||
}
|
||||
|
||||
fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
|
||||
let channel_store = self.channel_store.clone();
|
||||
let channel_id = self.channel_id;
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if mode == Mode::ManageMembers {
|
||||
let mut members = channel_store
|
||||
.update(&mut cx, |channel_store, cx| {
|
||||
channel_store.get_channel_member_details(channel_id, cx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.picker
|
||||
.update(cx, |picker, _| picker.delegate_mut().members = members);
|
||||
})?;
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.picker.update(cx, |picker, cx| {
|
||||
let delegate = picker.delegate_mut();
|
||||
delegate.mode = mode;
|
||||
delegate.selected_index = 0;
|
||||
picker.set_query("", cx);
|
||||
picker.update_matches(picker.query(cx), cx);
|
||||
cx.notify()
|
||||
});
|
||||
cx.notify()
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn toggle_member_admin(&mut self, _: &ToggleMemberAdmin, cx: &mut ViewContext<Self>) {
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
picker.delegate_mut().toggle_selected_member_admin(cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_member(&mut self, _: &RemoveMember, cx: &mut ViewContext<Self>) {
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
picker.delegate_mut().remove_selected_member(cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(PickerEvent::Dismiss);
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ChannelModal {
|
||||
type Event = PickerEvent;
|
||||
}
|
||||
|
||||
impl View for ChannelModal {
|
||||
fn ui_name() -> &'static str {
|
||||
"ChannelModal"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let theme = &theme::current(cx).collab_panel.tabbed_modal;
|
||||
|
||||
let mode = self.picker.read(cx).delegate().mode;
|
||||
let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else {
|
||||
return Empty::new().into_any();
|
||||
};
|
||||
|
||||
enum InviteMembers {}
|
||||
enum ManageMembers {}
|
||||
|
||||
fn render_mode_button<T: 'static>(
|
||||
mode: Mode,
|
||||
text: &'static str,
|
||||
current_mode: Mode,
|
||||
theme: &theme::TabbedModal,
|
||||
cx: &mut ViewContext<ChannelModal>,
|
||||
) -> AnyElement<ChannelModal> {
|
||||
let active = mode == current_mode;
|
||||
MouseEventHandler::new::<T, _>(0, cx, move |state, _| {
|
||||
let contained_text = theme.tab_button.style_for(active, state);
|
||||
Label::new(text, contained_text.text.clone())
|
||||
.contained()
|
||||
.with_style(contained_text.container.clone())
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if !active {
|
||||
this.set_mode(mode, cx);
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_visibility(
|
||||
channel_id: ChannelId,
|
||||
visibility: ChannelVisibility,
|
||||
theme: &theme::TabbedModal,
|
||||
cx: &mut ViewContext<ChannelModal>,
|
||||
) -> AnyElement<ChannelModal> {
|
||||
enum TogglePublic {}
|
||||
|
||||
if visibility == ChannelVisibility::Members {
|
||||
return Flex::row()
|
||||
.with_child(
|
||||
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
|
||||
let style = theme.visibility_toggle.style_for(state);
|
||||
Label::new(format!("{}", "Public access: OFF"), style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container.clone())
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.channel_store
|
||||
.update(cx, |channel_store, cx| {
|
||||
channel_store.set_channel_visibility(
|
||||
channel_id,
|
||||
ChannelVisibility::Public,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand),
|
||||
)
|
||||
.into_any();
|
||||
}
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
|
||||
let style = theme.visibility_toggle.style_for(state);
|
||||
Label::new(format!("{}", "Public access: ON"), style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container.clone())
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.channel_store
|
||||
.update(cx, |channel_store, cx| {
|
||||
channel_store.set_channel_visibility(
|
||||
channel_id,
|
||||
ChannelVisibility::Members,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand),
|
||||
)
|
||||
.with_spacing(14.0)
|
||||
.with_child(
|
||||
MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
|
||||
let style = theme.channel_link.style_for(state);
|
||||
Label::new(format!("{}", "copy link"), style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container.clone())
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if let Some(channel) =
|
||||
this.channel_store.read(cx).channel_for_id(channel_id)
|
||||
{
|
||||
let item = ClipboardItem::new(channel.link());
|
||||
cx.write_to_clipboard(item);
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Label::new(format!("#{}", channel.name), theme.title.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.title.container.clone()),
|
||||
)
|
||||
.with_child(render_visibility(channel.id, channel.visibility, theme, cx))
|
||||
.with_child(Flex::row().with_children([
|
||||
render_mode_button::<InviteMembers>(
|
||||
Mode::InviteMembers,
|
||||
"Invite members",
|
||||
mode,
|
||||
theme,
|
||||
cx,
|
||||
),
|
||||
render_mode_button::<ManageMembers>(
|
||||
Mode::ManageMembers,
|
||||
"Manage members",
|
||||
mode,
|
||||
theme,
|
||||
cx,
|
||||
),
|
||||
]))
|
||||
.expanded()
|
||||
.contained()
|
||||
.with_style(theme.header),
|
||||
)
|
||||
.with_child(
|
||||
ChildView::new(&self.picker, cx)
|
||||
.contained()
|
||||
.with_style(theme.body),
|
||||
)
|
||||
.constrained()
|
||||
.with_max_height(theme.max_height)
|
||||
.with_max_width(theme.max_width)
|
||||
.contained()
|
||||
.with_style(theme.modal)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
self.has_focus = true;
|
||||
if cx.is_self_focused() {
|
||||
cx.focus(&self.picker)
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||
self.has_focus = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Modal for ChannelModal {
|
||||
fn has_focus(&self) -> bool {
|
||||
self.has_focus
|
||||
}
|
||||
|
||||
fn dismiss_on_event(event: &Self::Event) -> bool {
|
||||
match event {
|
||||
PickerEvent::Dismiss => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
pub enum Mode {
|
||||
ManageMembers,
|
||||
InviteMembers,
|
||||
}
|
||||
|
||||
pub struct ChannelModalDelegate {
|
||||
matching_users: Vec<Arc<User>>,
|
||||
matching_member_indices: Vec<usize>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
channel_id: ChannelId,
|
||||
selected_index: usize,
|
||||
mode: Mode,
|
||||
match_candidates: Vec<StringMatchCandidate>,
|
||||
members: Vec<ChannelMembership>,
|
||||
context_menu: ViewHandle<ContextMenu>,
|
||||
}
|
||||
|
||||
impl PickerDelegate for ChannelModalDelegate {
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Search collaborator by username...".into()
|
||||
}
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
match self.mode {
|
||||
Mode::ManageMembers => self.matching_member_indices.len(),
|
||||
Mode::InviteMembers => self.matching_users.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
match self.mode {
|
||||
Mode::ManageMembers => {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates
|
||||
.extend(self.members.iter().enumerate().map(|(id, member)| {
|
||||
StringMatchCandidate {
|
||||
id,
|
||||
string: member.user.github_login.clone(),
|
||||
char_bag: member.user.github_login.chars().collect(),
|
||||
}
|
||||
}));
|
||||
|
||||
let matches = cx.background().block(match_strings(
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
true,
|
||||
usize::MAX,
|
||||
&Default::default(),
|
||||
cx.background().clone(),
|
||||
));
|
||||
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
picker
|
||||
.update(&mut cx, |picker, cx| {
|
||||
let delegate = picker.delegate_mut();
|
||||
delegate.matching_member_indices.clear();
|
||||
delegate
|
||||
.matching_member_indices
|
||||
.extend(matches.into_iter().map(|m| m.candidate_id));
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
Mode::InviteMembers => {
|
||||
let search_users = self
|
||||
.user_store
|
||||
.update(cx, |store, cx| store.fuzzy_search_users(query, cx));
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
async {
|
||||
let users = search_users.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
let delegate = picker.delegate_mut();
|
||||
delegate.matching_users = users;
|
||||
cx.notify();
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
.await;
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if let Some((selected_user, role)) = self.user_at_index(self.selected_index) {
|
||||
match self.mode {
|
||||
Mode::ManageMembers => {
|
||||
self.show_context_menu(role.unwrap_or(ChannelRole::Member), cx)
|
||||
}
|
||||
Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
|
||||
Some(proto::channel_member::Kind::Invitee) => {
|
||||
self.remove_selected_member(cx);
|
||||
}
|
||||
Some(proto::channel_member::Kind::AncestorMember) | None => {
|
||||
self.invite_member(selected_user, cx)
|
||||
}
|
||||
Some(proto::channel_member::Kind::Member) => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
cx.emit(PickerEvent::Dismiss);
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
mouse_state: &mut MouseState,
|
||||
selected: bool,
|
||||
cx: &gpui::AppContext,
|
||||
) -> AnyElement<Picker<Self>> {
|
||||
let full_theme = &theme::current(cx);
|
||||
let theme = &full_theme.collab_panel.channel_modal;
|
||||
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
|
||||
let (user, role) = self.user_at_index(ix).unwrap();
|
||||
let request_status = self.member_status(user.id, cx);
|
||||
|
||||
let style = tabbed_modal
|
||||
.picker
|
||||
.item
|
||||
.in_state(selected)
|
||||
.style_for(mouse_state);
|
||||
|
||||
let in_manage = matches!(self.mode, Mode::ManageMembers);
|
||||
|
||||
let mut result = Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
}))
|
||||
.with_child(
|
||||
Label::new(user.github_login.clone(), style.label.clone())
|
||||
.contained()
|
||||
.with_style(theme.contact_username)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
.with_children({
|
||||
(in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
|
||||
|| {
|
||||
Label::new("Invited", theme.member_tag.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.member_tag.container)
|
||||
.aligned()
|
||||
.left()
|
||||
},
|
||||
)
|
||||
})
|
||||
.with_children(if in_manage && role == Some(ChannelRole::Admin) {
|
||||
Some(
|
||||
Label::new("Admin", theme.member_tag.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.member_tag.container)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
} else if in_manage && role == Some(ChannelRole::Guest) {
|
||||
Some(
|
||||
Label::new("Guest", theme.member_tag.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.member_tag.container)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.with_children({
|
||||
let svg = match self.mode {
|
||||
Mode::ManageMembers => Some(
|
||||
Svg::new("icons/ellipsis.svg")
|
||||
.with_color(theme.member_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.member_icon.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(theme.member_icon.button_width)
|
||||
.with_height(theme.member_icon.button_width)
|
||||
.contained()
|
||||
.with_style(theme.member_icon.container),
|
||||
),
|
||||
Mode::InviteMembers => match request_status {
|
||||
Some(proto::channel_member::Kind::Member) => Some(
|
||||
Svg::new("icons/check.svg")
|
||||
.with_color(theme.member_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.member_icon.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(theme.member_icon.button_width)
|
||||
.with_height(theme.member_icon.button_width)
|
||||
.contained()
|
||||
.with_style(theme.member_icon.container),
|
||||
),
|
||||
Some(proto::channel_member::Kind::Invitee) => Some(
|
||||
Svg::new("icons/check.svg")
|
||||
.with_color(theme.invitee_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.invitee_icon.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(theme.invitee_icon.button_width)
|
||||
.with_height(theme.invitee_icon.button_width)
|
||||
.contained()
|
||||
.with_style(theme.invitee_icon.container),
|
||||
),
|
||||
Some(proto::channel_member::Kind::AncestorMember) | None => None,
|
||||
},
|
||||
};
|
||||
|
||||
svg.map(|svg| svg.aligned().flex_float().into_any())
|
||||
})
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_height(tabbed_modal.row_height)
|
||||
.into_any();
|
||||
|
||||
if selected {
|
||||
result = Stack::new()
|
||||
.with_child(result)
|
||||
.with_child(
|
||||
ChildView::new(&self.context_menu, cx)
|
||||
.aligned()
|
||||
.top()
|
||||
.right(),
|
||||
)
|
||||
.into_any();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl ChannelModalDelegate {
|
||||
fn member_status(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
cx: &AppContext,
|
||||
) -> Option<proto::channel_member::Kind> {
|
||||
self.members
|
||||
.iter()
|
||||
.find_map(|membership| (membership.user.id == user_id).then_some(membership.kind))
|
||||
.or_else(|| {
|
||||
self.channel_store
|
||||
.read(cx)
|
||||
.has_pending_channel_invite(self.channel_id, user_id)
|
||||
.then_some(proto::channel_member::Kind::Invitee)
|
||||
})
|
||||
}
|
||||
|
||||
fn user_at_index(&self, ix: usize) -> Option<(Arc<User>, Option<ChannelRole>)> {
|
||||
match self.mode {
|
||||
Mode::ManageMembers => self.matching_member_indices.get(ix).and_then(|ix| {
|
||||
let channel_membership = self.members.get(*ix)?;
|
||||
Some((
|
||||
channel_membership.user.clone(),
|
||||
Some(channel_membership.role),
|
||||
))
|
||||
}),
|
||||
Mode::InviteMembers => Some((self.matching_users.get(ix).cloned()?, None)),
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_selected_member_admin(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
|
||||
let (user, role) = self.user_at_index(self.selected_index)?;
|
||||
let new_role = if role == Some(ChannelRole::Admin) {
|
||||
ChannelRole::Member
|
||||
} else {
|
||||
ChannelRole::Admin
|
||||
};
|
||||
let update = self.channel_store.update(cx, |store, cx| {
|
||||
store.set_member_role(self.channel_id, user.id, new_role, cx)
|
||||
});
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
update.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
let this = picker.delegate_mut();
|
||||
if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
|
||||
member.role = new_role;
|
||||
}
|
||||
cx.focus_self();
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn remove_selected_member(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
|
||||
let (user, _) = self.user_at_index(self.selected_index)?;
|
||||
let user_id = user.id;
|
||||
let update = self.channel_store.update(cx, |store, cx| {
|
||||
store.remove_member(self.channel_id, user_id, cx)
|
||||
});
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
update.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
let this = picker.delegate_mut();
|
||||
if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) {
|
||||
this.members.remove(ix);
|
||||
this.matching_member_indices.retain_mut(|member_ix| {
|
||||
if *member_ix == ix {
|
||||
return false;
|
||||
} else if *member_ix > ix {
|
||||
*member_ix -= 1;
|
||||
}
|
||||
true
|
||||
})
|
||||
}
|
||||
|
||||
this.selected_index = this
|
||||
.selected_index
|
||||
.min(this.matching_member_indices.len().saturating_sub(1));
|
||||
|
||||
cx.focus_self();
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn invite_member(&mut self, user: Arc<User>, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let invite_member = self.channel_store.update(cx, |store, cx| {
|
||||
store.invite_member(self.channel_id, user.id, ChannelRole::Member, cx)
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
invite_member.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let new_member = ChannelMembership {
|
||||
user,
|
||||
kind: proto::channel_member::Kind::Invitee,
|
||||
role: ChannelRole::Member,
|
||||
};
|
||||
let members = &mut this.delegate_mut().members;
|
||||
match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) {
|
||||
Ok(ix) | Err(ix) => members.insert(ix, new_member),
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn show_context_menu(&mut self, role: ChannelRole, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.context_menu.update(cx, |context_menu, cx| {
|
||||
context_menu.show(
|
||||
Default::default(),
|
||||
AnchorCorner::TopRight,
|
||||
vec![
|
||||
ContextMenuItem::action("Remove", RemoveMember),
|
||||
ContextMenuItem::action(
|
||||
if role == ChannelRole::Admin {
|
||||
"Make non-admin"
|
||||
} else {
|
||||
"Make admin"
|
||||
},
|
||||
ToggleMemberAdmin,
|
||||
),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
261
crates/collab_ui2/src/collab_panel/contact_finder.rs
Normal file
261
crates/collab_ui2/src/collab_panel/contact_finder.rs
Normal file
@@ -0,0 +1,261 @@
|
||||
use client::{ContactRequestStatus, User, UserStore};
|
||||
use gpui::{
|
||||
elements::*, AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||
use std::sync::Arc;
|
||||
use util::TryFutureExt;
|
||||
use workspace::Modal;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
Picker::<ContactFinderDelegate>::init(cx);
|
||||
cx.add_action(ContactFinder::dismiss)
|
||||
}
|
||||
|
||||
pub struct ContactFinder {
|
||||
picker: ViewHandle<Picker<ContactFinderDelegate>>,
|
||||
has_focus: bool,
|
||||
}
|
||||
|
||||
impl ContactFinder {
|
||||
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let picker = cx.add_view(|cx| {
|
||||
Picker::new(
|
||||
ContactFinderDelegate {
|
||||
user_store,
|
||||
potential_contacts: Arc::from([]),
|
||||
selected_index: 0,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
|
||||
});
|
||||
|
||||
cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
|
||||
|
||||
Self {
|
||||
picker,
|
||||
has_focus: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
picker.set_query(query, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(PickerEvent::Dismiss);
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ContactFinder {
|
||||
type Event = PickerEvent;
|
||||
}
|
||||
|
||||
impl View for ContactFinder {
|
||||
fn ui_name() -> &'static str {
|
||||
"ContactFinder"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let full_theme = &theme::current(cx);
|
||||
let theme = &full_theme.collab_panel.tabbed_modal;
|
||||
|
||||
fn render_mode_button(
|
||||
text: &'static str,
|
||||
theme: &theme::TabbedModal,
|
||||
_cx: &mut ViewContext<ContactFinder>,
|
||||
) -> AnyElement<ContactFinder> {
|
||||
let contained_text = &theme.tab_button.active_state().default;
|
||||
Label::new(text, contained_text.text.clone())
|
||||
.contained()
|
||||
.with_style(contained_text.container.clone())
|
||||
.into_any()
|
||||
}
|
||||
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Label::new("Contacts", theme.title.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.title.container.clone()),
|
||||
)
|
||||
.with_child(Flex::row().with_children([render_mode_button(
|
||||
"Invite new contacts",
|
||||
&theme,
|
||||
cx,
|
||||
)]))
|
||||
.expanded()
|
||||
.contained()
|
||||
.with_style(theme.header),
|
||||
)
|
||||
.with_child(
|
||||
ChildView::new(&self.picker, cx)
|
||||
.contained()
|
||||
.with_style(theme.body),
|
||||
)
|
||||
.constrained()
|
||||
.with_max_height(theme.max_height)
|
||||
.with_max_width(theme.max_width)
|
||||
.contained()
|
||||
.with_style(theme.modal)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
self.has_focus = true;
|
||||
if cx.is_self_focused() {
|
||||
cx.focus(&self.picker)
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||
self.has_focus = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Modal for ContactFinder {
|
||||
fn has_focus(&self) -> bool {
|
||||
self.has_focus
|
||||
}
|
||||
|
||||
fn dismiss_on_event(event: &Self::Event) -> bool {
|
||||
match event {
|
||||
PickerEvent::Dismiss => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ContactFinderDelegate {
|
||||
potential_contacts: Arc<[Arc<User>]>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl PickerDelegate for ContactFinderDelegate {
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Search collaborator by username...".into()
|
||||
}
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.potential_contacts.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
let search_users = self
|
||||
.user_store
|
||||
.update(cx, |store, cx| store.fuzzy_search_users(query, cx));
|
||||
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
async {
|
||||
let potential_contacts = search_users.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
picker.delegate_mut().potential_contacts = potential_contacts.into();
|
||||
cx.notify();
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
.await;
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if let Some(user) = self.potential_contacts.get(self.selected_index) {
|
||||
let user_store = self.user_store.read(cx);
|
||||
match user_store.contact_request_status(user) {
|
||||
ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
|
||||
self.user_store
|
||||
.update(cx, |store, cx| store.request_contact(user.id, cx))
|
||||
.detach();
|
||||
}
|
||||
ContactRequestStatus::RequestSent => {
|
||||
self.user_store
|
||||
.update(cx, |store, cx| store.remove_contact(user.id, cx))
|
||||
.detach();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
cx.emit(PickerEvent::Dismiss);
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
mouse_state: &mut MouseState,
|
||||
selected: bool,
|
||||
cx: &gpui::AppContext,
|
||||
) -> AnyElement<Picker<Self>> {
|
||||
let full_theme = &theme::current(cx);
|
||||
let theme = &full_theme.collab_panel.contact_finder;
|
||||
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
|
||||
let user = &self.potential_contacts[ix];
|
||||
let request_status = self.user_store.read(cx).contact_request_status(user);
|
||||
|
||||
let icon_path = match request_status {
|
||||
ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
|
||||
Some("icons/check.svg")
|
||||
}
|
||||
ContactRequestStatus::RequestSent => Some("icons/x.svg"),
|
||||
ContactRequestStatus::RequestAccepted => None,
|
||||
};
|
||||
let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
|
||||
&theme.disabled_contact_button
|
||||
} else {
|
||||
&theme.contact_button
|
||||
};
|
||||
let style = tabbed_modal
|
||||
.picker
|
||||
.item
|
||||
.in_state(selected)
|
||||
.style_for(mouse_state);
|
||||
Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
}))
|
||||
.with_child(
|
||||
Label::new(user.github_login.clone(), style.label.clone())
|
||||
.contained()
|
||||
.with_style(theme.contact_username)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
.with_children(icon_path.map(|icon_path| {
|
||||
Svg::new(icon_path)
|
||||
.with_color(button_style.color)
|
||||
.constrained()
|
||||
.with_width(button_style.icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(button_style.container)
|
||||
.constrained()
|
||||
.with_width(button_style.button_width)
|
||||
.with_height(button_style.button_width)
|
||||
.aligned()
|
||||
.flex_float()
|
||||
}))
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_height(tabbed_modal.row_height)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
1371
crates/collab_ui2/src/collab_titlebar_item.rs
Normal file
1371
crates/collab_ui2/src/collab_titlebar_item.rs
Normal file
File diff suppressed because it is too large
Load Diff
154
crates/collab_ui2/src/collab_ui.rs
Normal file
154
crates/collab_ui2/src/collab_ui.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
pub mod channel_view;
|
||||
pub mod chat_panel;
|
||||
pub mod collab_panel;
|
||||
mod collab_titlebar_item;
|
||||
mod face_pile;
|
||||
pub mod notification_panel;
|
||||
pub mod notifications;
|
||||
mod panel_settings;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use collab_panel::CollabPanel;
|
||||
pub use collab_titlebar_item::CollabTitlebarItem;
|
||||
use gpui::AppContext;
|
||||
pub use panel_settings::{
|
||||
ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
|
||||
};
|
||||
use settings::Settings;
|
||||
use workspace::AppState;
|
||||
|
||||
// actions!(
|
||||
// collab,
|
||||
// [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
|
||||
// );
|
||||
|
||||
pub fn init(_app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
CollaborationPanelSettings::register(cx);
|
||||
ChatPanelSettings::register(cx);
|
||||
NotificationPanelSettings::register(cx);
|
||||
|
||||
// vcs_menu::init(cx);
|
||||
collab_titlebar_item::init(cx);
|
||||
collab_panel::init(cx);
|
||||
// chat_panel::init(cx);
|
||||
// notifications::init(&app_state, cx);
|
||||
|
||||
// cx.add_global_action(toggle_screen_sharing);
|
||||
// cx.add_global_action(toggle_mute);
|
||||
// cx.add_global_action(toggle_deafen);
|
||||
}
|
||||
|
||||
// pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
|
||||
// let call = ActiveCall::global(cx).read(cx);
|
||||
// if let Some(room) = call.room().cloned() {
|
||||
// let client = call.client();
|
||||
// let toggle_screen_sharing = room.update(cx, |room, cx| {
|
||||
// if room.is_screen_sharing() {
|
||||
// report_call_event_for_room(
|
||||
// "disable screen share",
|
||||
// room.id(),
|
||||
// room.channel_id(),
|
||||
// &client,
|
||||
// cx,
|
||||
// );
|
||||
// Task::ready(room.unshare_screen(cx))
|
||||
// } else {
|
||||
// report_call_event_for_room(
|
||||
// "enable screen share",
|
||||
// room.id(),
|
||||
// room.channel_id(),
|
||||
// &client,
|
||||
// cx,
|
||||
// );
|
||||
// room.share_screen(cx)
|
||||
// }
|
||||
// });
|
||||
// toggle_screen_sharing.detach_and_log_err(cx);
|
||||
// }
|
||||
// }
|
||||
|
||||
// pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
|
||||
// let call = ActiveCall::global(cx).read(cx);
|
||||
// if let Some(room) = call.room().cloned() {
|
||||
// let client = call.client();
|
||||
// room.update(cx, |room, cx| {
|
||||
// let operation = if room.is_muted(cx) {
|
||||
// "enable microphone"
|
||||
// } else {
|
||||
// "disable microphone"
|
||||
// };
|
||||
// report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx);
|
||||
|
||||
// room.toggle_mute(cx)
|
||||
// })
|
||||
// .map(|task| task.detach_and_log_err(cx))
|
||||
// .log_err();
|
||||
// }
|
||||
// }
|
||||
|
||||
// pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
|
||||
// if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
|
||||
// room.update(cx, Room::toggle_deafen)
|
||||
// .map(|task| task.detach_and_log_err(cx))
|
||||
// .log_err();
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn notification_window_options(
|
||||
// screen: Rc<dyn Screen>,
|
||||
// window_size: Vector2F,
|
||||
// ) -> WindowOptions<'static> {
|
||||
// const NOTIFICATION_PADDING: f32 = 16.;
|
||||
|
||||
// let screen_bounds = screen.content_bounds();
|
||||
// WindowOptions {
|
||||
// bounds: WindowBounds::Fixed(RectF::new(
|
||||
// screen_bounds.upper_right()
|
||||
// + vec2f(
|
||||
// -NOTIFICATION_PADDING - window_size.x(),
|
||||
// NOTIFICATION_PADDING,
|
||||
// ),
|
||||
// window_size,
|
||||
// )),
|
||||
// titlebar: None,
|
||||
// center: false,
|
||||
// focus: false,
|
||||
// show: true,
|
||||
// kind: WindowKind::PopUp,
|
||||
// is_movable: false,
|
||||
// screen: Some(screen),
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn render_avatar<T: 'static>(
|
||||
// avatar: Option<Arc<ImageData>>,
|
||||
// avatar_style: &AvatarStyle,
|
||||
// container: ContainerStyle,
|
||||
// ) -> AnyElement<T> {
|
||||
// avatar
|
||||
// .map(|avatar| {
|
||||
// Image::from_data(avatar)
|
||||
// .with_style(avatar_style.image)
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_corner_radius(avatar_style.outer_corner_radius)
|
||||
// .constrained()
|
||||
// .with_width(avatar_style.outer_width)
|
||||
// .with_height(avatar_style.outer_width)
|
||||
// .into_any()
|
||||
// })
|
||||
// .unwrap_or_else(|| {
|
||||
// Empty::new()
|
||||
// .constrained()
|
||||
// .with_width(avatar_style.outer_width)
|
||||
// .into_any()
|
||||
// })
|
||||
// .contained()
|
||||
// .with_style(container)
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
|
||||
// cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
|
||||
// }
|
||||
113
crates/collab_ui2/src/face_pile.rs
Normal file
113
crates/collab_ui2/src/face_pile.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
// use std::ops::Range;
|
||||
|
||||
// use gpui::{
|
||||
// geometry::{
|
||||
// rect::RectF,
|
||||
// vector::{vec2f, Vector2F},
|
||||
// },
|
||||
// json::ToJson,
|
||||
// serde_json::{self, json},
|
||||
// AnyElement, Axis, Element, View, ViewContext,
|
||||
// };
|
||||
|
||||
// pub(crate) struct FacePile<V: View> {
|
||||
// overlap: f32,
|
||||
// faces: Vec<AnyElement<V>>,
|
||||
// }
|
||||
|
||||
// impl<V: View> FacePile<V> {
|
||||
// pub fn new(overlap: f32) -> Self {
|
||||
// Self {
|
||||
// overlap,
|
||||
// faces: Vec::new(),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl<V: View> Element<V> for FacePile<V> {
|
||||
// type LayoutState = ();
|
||||
// type PaintState = ();
|
||||
|
||||
// fn layout(
|
||||
// &mut self,
|
||||
// constraint: gpui::SizeConstraint,
|
||||
// view: &mut V,
|
||||
// cx: &mut ViewContext<V>,
|
||||
// ) -> (Vector2F, Self::LayoutState) {
|
||||
// debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
|
||||
|
||||
// let mut width = 0.;
|
||||
// let mut max_height = 0.;
|
||||
// for face in &mut self.faces {
|
||||
// let layout = face.layout(constraint, view, cx);
|
||||
// width += layout.x();
|
||||
// max_height = f32::max(max_height, layout.y());
|
||||
// }
|
||||
// width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
|
||||
|
||||
// (
|
||||
// Vector2F::new(width, max_height.clamp(1., constraint.max.y())),
|
||||
// (),
|
||||
// )
|
||||
// }
|
||||
|
||||
// fn paint(
|
||||
// &mut self,
|
||||
// bounds: RectF,
|
||||
// visible_bounds: RectF,
|
||||
// _layout: &mut Self::LayoutState,
|
||||
// view: &mut V,
|
||||
// cx: &mut ViewContext<V>,
|
||||
// ) -> Self::PaintState {
|
||||
// let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
|
||||
|
||||
// let origin_y = bounds.upper_right().y();
|
||||
// let mut origin_x = bounds.upper_right().x();
|
||||
|
||||
// for face in self.faces.iter_mut().rev() {
|
||||
// let size = face.size();
|
||||
// origin_x -= size.x();
|
||||
// let origin_y = origin_y + (bounds.height() - size.y()) / 2.0;
|
||||
|
||||
// cx.scene().push_layer(None);
|
||||
// face.paint(vec2f(origin_x, origin_y), visible_bounds, view, cx);
|
||||
// cx.scene().pop_layer();
|
||||
// origin_x += self.overlap;
|
||||
// }
|
||||
|
||||
// ()
|
||||
// }
|
||||
|
||||
// fn rect_for_text_range(
|
||||
// &self,
|
||||
// _: Range<usize>,
|
||||
// _: RectF,
|
||||
// _: RectF,
|
||||
// _: &Self::LayoutState,
|
||||
// _: &Self::PaintState,
|
||||
// _: &V,
|
||||
// _: &ViewContext<V>,
|
||||
// ) -> Option<RectF> {
|
||||
// None
|
||||
// }
|
||||
|
||||
// fn debug(
|
||||
// &self,
|
||||
// bounds: RectF,
|
||||
// _: &Self::LayoutState,
|
||||
// _: &Self::PaintState,
|
||||
// _: &V,
|
||||
// _: &ViewContext<V>,
|
||||
// ) -> serde_json::Value {
|
||||
// json!({
|
||||
// "type": "FacePile",
|
||||
// "bounds": bounds.to_json()
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl<V: View> Extend<AnyElement<V>> for FacePile<V> {
|
||||
// fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
|
||||
// self.faces.extend(children);
|
||||
// }
|
||||
// }
|
||||
884
crates/collab_ui2/src/notification_panel.rs
Normal file
884
crates/collab_ui2/src/notification_panel.rs
Normal file
@@ -0,0 +1,884 @@
|
||||
// use crate::{chat_panel::ChatPanel, render_avatar, NotificationPanelSettings};
|
||||
// use anyhow::Result;
|
||||
// use channel::ChannelStore;
|
||||
// use client::{Client, Notification, User, UserStore};
|
||||
// use collections::HashMap;
|
||||
// use db::kvp::KEY_VALUE_STORE;
|
||||
// use futures::StreamExt;
|
||||
// use gpui::{
|
||||
// actions,
|
||||
// elements::*,
|
||||
// platform::{CursorStyle, MouseButton},
|
||||
// serde_json, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Task, View,
|
||||
// ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
||||
// };
|
||||
// use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
|
||||
// use project::Fs;
|
||||
// use rpc::proto;
|
||||
// use serde::{Deserialize, Serialize};
|
||||
// use settings::SettingsStore;
|
||||
// use std::{sync::Arc, time::Duration};
|
||||
// use theme::{ui, Theme};
|
||||
// use time::{OffsetDateTime, UtcOffset};
|
||||
// use util::{ResultExt, TryFutureExt};
|
||||
// use workspace::{
|
||||
// dock::{DockPosition, Panel},
|
||||
// Workspace,
|
||||
// };
|
||||
|
||||
// const LOADING_THRESHOLD: usize = 30;
|
||||
// const MARK_AS_READ_DELAY: Duration = Duration::from_secs(1);
|
||||
// const TOAST_DURATION: Duration = Duration::from_secs(5);
|
||||
// const NOTIFICATION_PANEL_KEY: &'static str = "NotificationPanel";
|
||||
|
||||
// pub struct NotificationPanel {
|
||||
// client: Arc<Client>,
|
||||
// user_store: ModelHandle<UserStore>,
|
||||
// channel_store: ModelHandle<ChannelStore>,
|
||||
// notification_store: ModelHandle<NotificationStore>,
|
||||
// fs: Arc<dyn Fs>,
|
||||
// width: Option<f32>,
|
||||
// active: bool,
|
||||
// notification_list: ListState<Self>,
|
||||
// pending_serialization: Task<Option<()>>,
|
||||
// subscriptions: Vec<gpui::Subscription>,
|
||||
// workspace: WeakViewHandle<Workspace>,
|
||||
// current_notification_toast: Option<(u64, Task<()>)>,
|
||||
// local_timezone: UtcOffset,
|
||||
// has_focus: bool,
|
||||
// mark_as_read_tasks: HashMap<u64, Task<Result<()>>>,
|
||||
// }
|
||||
|
||||
// #[derive(Serialize, Deserialize)]
|
||||
// struct SerializedNotificationPanel {
|
||||
// width: Option<f32>,
|
||||
// }
|
||||
|
||||
// #[derive(Debug)]
|
||||
// pub enum Event {
|
||||
// DockPositionChanged,
|
||||
// Focus,
|
||||
// Dismissed,
|
||||
// }
|
||||
|
||||
// pub struct NotificationPresenter {
|
||||
// pub actor: Option<Arc<client::User>>,
|
||||
// pub text: String,
|
||||
// pub icon: &'static str,
|
||||
// pub needs_response: bool,
|
||||
// pub can_navigate: bool,
|
||||
// }
|
||||
|
||||
// actions!(notification_panel, [ToggleFocus]);
|
||||
|
||||
// pub fn init(_cx: &mut AppContext) {}
|
||||
|
||||
// impl NotificationPanel {
|
||||
// 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 user_store = workspace.app_state().user_store.clone();
|
||||
// let workspace_handle = workspace.weak_handle();
|
||||
|
||||
// cx.add_view(|cx| {
|
||||
// let mut status = client.status();
|
||||
// cx.spawn(|this, mut cx| async move {
|
||||
// while let Some(_) = status.next().await {
|
||||
// if this
|
||||
// .update(&mut cx, |_, cx| {
|
||||
// cx.notify();
|
||||
// })
|
||||
// .is_err()
|
||||
// {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// .detach();
|
||||
|
||||
// let mut notification_list =
|
||||
// ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
|
||||
// this.render_notification(ix, cx)
|
||||
// .unwrap_or_else(|| Empty::new().into_any())
|
||||
// });
|
||||
// notification_list.set_scroll_handler(|visible_range, count, this, cx| {
|
||||
// if count.saturating_sub(visible_range.end) < LOADING_THRESHOLD {
|
||||
// if let Some(task) = this
|
||||
// .notification_store
|
||||
// .update(cx, |store, cx| store.load_more_notifications(false, cx))
|
||||
// {
|
||||
// task.detach();
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
// let mut this = Self {
|
||||
// fs,
|
||||
// client,
|
||||
// user_store,
|
||||
// local_timezone: cx.platform().local_timezone(),
|
||||
// channel_store: ChannelStore::global(cx),
|
||||
// notification_store: NotificationStore::global(cx),
|
||||
// notification_list,
|
||||
// pending_serialization: Task::ready(None),
|
||||
// workspace: workspace_handle,
|
||||
// has_focus: false,
|
||||
// current_notification_toast: None,
|
||||
// subscriptions: Vec::new(),
|
||||
// active: false,
|
||||
// mark_as_read_tasks: HashMap::default(),
|
||||
// width: None,
|
||||
// };
|
||||
|
||||
// let mut old_dock_position = this.position(cx);
|
||||
// this.subscriptions.extend([
|
||||
// cx.observe(&this.notification_store, |_, _, cx| cx.notify()),
|
||||
// cx.subscribe(&this.notification_store, Self::on_notification_event),
|
||||
// cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
|
||||
// let new_dock_position = this.position(cx);
|
||||
// if new_dock_position != old_dock_position {
|
||||
// old_dock_position = new_dock_position;
|
||||
// cx.emit(Event::DockPositionChanged);
|
||||
// }
|
||||
// cx.notify();
|
||||
// }),
|
||||
// ]);
|
||||
// this
|
||||
// })
|
||||
// }
|
||||
|
||||
// pub fn load(
|
||||
// workspace: WeakViewHandle<Workspace>,
|
||||
// cx: AsyncAppContext,
|
||||
// ) -> Task<Result<ViewHandle<Self>>> {
|
||||
// cx.spawn(|mut cx| async move {
|
||||
// let serialized_panel = if let Some(panel) = cx
|
||||
// .background()
|
||||
// .spawn(async move { KEY_VALUE_STORE.read_kvp(NOTIFICATION_PANEL_KEY) })
|
||||
// .await
|
||||
// .log_err()
|
||||
// .flatten()
|
||||
// {
|
||||
// Some(serde_json::from_str::<SerializedNotificationPanel>(&panel)?)
|
||||
// } else {
|
||||
// None
|
||||
// };
|
||||
|
||||
// workspace.update(&mut cx, |workspace, cx| {
|
||||
// let panel = Self::new(workspace, cx);
|
||||
// if let Some(serialized_panel) = serialized_panel {
|
||||
// panel.update(cx, |panel, cx| {
|
||||
// panel.width = serialized_panel.width;
|
||||
// cx.notify();
|
||||
// });
|
||||
// }
|
||||
// panel
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
|
||||
// fn serialize(&mut self, cx: &mut ViewContext<Self>) {
|
||||
// let width = self.width;
|
||||
// self.pending_serialization = cx.background().spawn(
|
||||
// async move {
|
||||
// KEY_VALUE_STORE
|
||||
// .write_kvp(
|
||||
// NOTIFICATION_PANEL_KEY.into(),
|
||||
// serde_json::to_string(&SerializedNotificationPanel { width })?,
|
||||
// )
|
||||
// .await?;
|
||||
// anyhow::Ok(())
|
||||
// }
|
||||
// .log_err(),
|
||||
// );
|
||||
// }
|
||||
|
||||
// fn render_notification(
|
||||
// &mut self,
|
||||
// ix: usize,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> Option<AnyElement<Self>> {
|
||||
// let entry = self.notification_store.read(cx).notification_at(ix)?;
|
||||
// let notification_id = entry.id;
|
||||
// let now = OffsetDateTime::now_utc();
|
||||
// let timestamp = entry.timestamp;
|
||||
// let NotificationPresenter {
|
||||
// actor,
|
||||
// text,
|
||||
// needs_response,
|
||||
// can_navigate,
|
||||
// ..
|
||||
// } = self.present_notification(entry, cx)?;
|
||||
|
||||
// let theme = theme::current(cx);
|
||||
// let style = &theme.notification_panel;
|
||||
// let response = entry.response;
|
||||
// let notification = entry.notification.clone();
|
||||
|
||||
// let message_style = if entry.is_read {
|
||||
// style.read_text.clone()
|
||||
// } else {
|
||||
// style.unread_text.clone()
|
||||
// };
|
||||
|
||||
// if self.active && !entry.is_read {
|
||||
// self.did_render_notification(notification_id, ¬ification, cx);
|
||||
// }
|
||||
|
||||
// enum Decline {}
|
||||
// enum Accept {}
|
||||
|
||||
// Some(
|
||||
// MouseEventHandler::new::<NotificationEntry, _>(ix, cx, |_, cx| {
|
||||
// let container = message_style.container;
|
||||
|
||||
// Flex::row()
|
||||
// .with_children(actor.map(|actor| {
|
||||
// render_avatar(actor.avatar.clone(), &style.avatar, style.avatar_container)
|
||||
// }))
|
||||
// .with_child(
|
||||
// Flex::column()
|
||||
// .with_child(Text::new(text, message_style.text.clone()))
|
||||
// .with_child(
|
||||
// Flex::row()
|
||||
// .with_child(
|
||||
// Label::new(
|
||||
// format_timestamp(timestamp, now, self.local_timezone),
|
||||
// style.timestamp.text.clone(),
|
||||
// )
|
||||
// .contained()
|
||||
// .with_style(style.timestamp.container),
|
||||
// )
|
||||
// .with_children(if let Some(is_accepted) = response {
|
||||
// Some(
|
||||
// Label::new(
|
||||
// if is_accepted {
|
||||
// "You accepted"
|
||||
// } else {
|
||||
// "You declined"
|
||||
// },
|
||||
// style.read_text.text.clone(),
|
||||
// )
|
||||
// .flex_float()
|
||||
// .into_any(),
|
||||
// )
|
||||
// } else if needs_response {
|
||||
// Some(
|
||||
// Flex::row()
|
||||
// .with_children([
|
||||
// MouseEventHandler::new::<Decline, _>(
|
||||
// ix,
|
||||
// cx,
|
||||
// |state, _| {
|
||||
// let button =
|
||||
// style.button.style_for(state);
|
||||
// Label::new(
|
||||
// "Decline",
|
||||
// button.text.clone(),
|
||||
// )
|
||||
// .contained()
|
||||
// .with_style(button.container)
|
||||
// },
|
||||
// )
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_click(MouseButton::Left, {
|
||||
// let notification = notification.clone();
|
||||
// move |_, view, cx| {
|
||||
// view.respond_to_notification(
|
||||
// notification.clone(),
|
||||
// false,
|
||||
// cx,
|
||||
// );
|
||||
// }
|
||||
// }),
|
||||
// MouseEventHandler::new::<Accept, _>(
|
||||
// ix,
|
||||
// cx,
|
||||
// |state, _| {
|
||||
// let button =
|
||||
// style.button.style_for(state);
|
||||
// Label::new(
|
||||
// "Accept",
|
||||
// button.text.clone(),
|
||||
// )
|
||||
// .contained()
|
||||
// .with_style(button.container)
|
||||
// },
|
||||
// )
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_click(MouseButton::Left, {
|
||||
// let notification = notification.clone();
|
||||
// move |_, view, cx| {
|
||||
// view.respond_to_notification(
|
||||
// notification.clone(),
|
||||
// true,
|
||||
// cx,
|
||||
// );
|
||||
// }
|
||||
// }),
|
||||
// ])
|
||||
// .flex_float()
|
||||
// .into_any(),
|
||||
// )
|
||||
// } else {
|
||||
// None
|
||||
// }),
|
||||
// )
|
||||
// .flex(1.0, true),
|
||||
// )
|
||||
// .contained()
|
||||
// .with_style(container)
|
||||
// .into_any()
|
||||
// })
|
||||
// .with_cursor_style(if can_navigate {
|
||||
// CursorStyle::PointingHand
|
||||
// } else {
|
||||
// CursorStyle::default()
|
||||
// })
|
||||
// .on_click(MouseButton::Left, {
|
||||
// let notification = notification.clone();
|
||||
// move |_, this, cx| this.did_click_notification(¬ification, cx)
|
||||
// })
|
||||
// .into_any(),
|
||||
// )
|
||||
// }
|
||||
|
||||
// fn present_notification(
|
||||
// &self,
|
||||
// entry: &NotificationEntry,
|
||||
// cx: &AppContext,
|
||||
// ) -> Option<NotificationPresenter> {
|
||||
// let user_store = self.user_store.read(cx);
|
||||
// let channel_store = self.channel_store.read(cx);
|
||||
// match entry.notification {
|
||||
// Notification::ContactRequest { sender_id } => {
|
||||
// let requester = user_store.get_cached_user(sender_id)?;
|
||||
// Some(NotificationPresenter {
|
||||
// icon: "icons/plus.svg",
|
||||
// text: format!("{} wants to add you as a contact", requester.github_login),
|
||||
// needs_response: user_store.has_incoming_contact_request(requester.id),
|
||||
// actor: Some(requester),
|
||||
// can_navigate: false,
|
||||
// })
|
||||
// }
|
||||
// Notification::ContactRequestAccepted { responder_id } => {
|
||||
// let responder = user_store.get_cached_user(responder_id)?;
|
||||
// Some(NotificationPresenter {
|
||||
// icon: "icons/plus.svg",
|
||||
// text: format!("{} accepted your contact invite", responder.github_login),
|
||||
// needs_response: false,
|
||||
// actor: Some(responder),
|
||||
// can_navigate: false,
|
||||
// })
|
||||
// }
|
||||
// Notification::ChannelInvitation {
|
||||
// ref channel_name,
|
||||
// channel_id,
|
||||
// inviter_id,
|
||||
// } => {
|
||||
// let inviter = user_store.get_cached_user(inviter_id)?;
|
||||
// Some(NotificationPresenter {
|
||||
// icon: "icons/hash.svg",
|
||||
// text: format!(
|
||||
// "{} invited you to join the #{channel_name} channel",
|
||||
// inviter.github_login
|
||||
// ),
|
||||
// needs_response: channel_store.has_channel_invitation(channel_id),
|
||||
// actor: Some(inviter),
|
||||
// can_navigate: false,
|
||||
// })
|
||||
// }
|
||||
// Notification::ChannelMessageMention {
|
||||
// sender_id,
|
||||
// channel_id,
|
||||
// message_id,
|
||||
// } => {
|
||||
// let sender = user_store.get_cached_user(sender_id)?;
|
||||
// let channel = channel_store.channel_for_id(channel_id)?;
|
||||
// let message = self
|
||||
// .notification_store
|
||||
// .read(cx)
|
||||
// .channel_message_for_id(message_id)?;
|
||||
// Some(NotificationPresenter {
|
||||
// icon: "icons/conversations.svg",
|
||||
// text: format!(
|
||||
// "{} mentioned you in #{}:\n{}",
|
||||
// sender.github_login, channel.name, message.body,
|
||||
// ),
|
||||
// needs_response: false,
|
||||
// actor: Some(sender),
|
||||
// can_navigate: true,
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn did_render_notification(
|
||||
// &mut self,
|
||||
// notification_id: u64,
|
||||
// notification: &Notification,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) {
|
||||
// let should_mark_as_read = match notification {
|
||||
// Notification::ContactRequestAccepted { .. } => true,
|
||||
// Notification::ContactRequest { .. }
|
||||
// | Notification::ChannelInvitation { .. }
|
||||
// | Notification::ChannelMessageMention { .. } => false,
|
||||
// };
|
||||
|
||||
// if should_mark_as_read {
|
||||
// self.mark_as_read_tasks
|
||||
// .entry(notification_id)
|
||||
// .or_insert_with(|| {
|
||||
// let client = self.client.clone();
|
||||
// cx.spawn(|this, mut cx| async move {
|
||||
// cx.background().timer(MARK_AS_READ_DELAY).await;
|
||||
// client
|
||||
// .request(proto::MarkNotificationRead { notification_id })
|
||||
// .await?;
|
||||
// this.update(&mut cx, |this, _| {
|
||||
// this.mark_as_read_tasks.remove(¬ification_id);
|
||||
// })?;
|
||||
// Ok(())
|
||||
// })
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn did_click_notification(&mut self, notification: &Notification, cx: &mut ViewContext<Self>) {
|
||||
// if let Notification::ChannelMessageMention {
|
||||
// message_id,
|
||||
// channel_id,
|
||||
// ..
|
||||
// } = notification.clone()
|
||||
// {
|
||||
// if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
// cx.app_context().defer(move |cx| {
|
||||
// workspace.update(cx, |workspace, cx| {
|
||||
// if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
|
||||
// panel.update(cx, |panel, cx| {
|
||||
// panel
|
||||
// .select_channel(channel_id, Some(message_id), cx)
|
||||
// .detach_and_log_err(cx);
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn is_showing_notification(&self, notification: &Notification, cx: &AppContext) -> bool {
|
||||
// if let Notification::ChannelMessageMention { channel_id, .. } = ¬ification {
|
||||
// if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
// return workspace
|
||||
// .read_with(cx, |workspace, cx| {
|
||||
// if let Some(panel) = workspace.panel::<ChatPanel>(cx) {
|
||||
// return panel.read_with(cx, |panel, cx| {
|
||||
// panel.is_scrolled_to_bottom()
|
||||
// && panel.active_chat().map_or(false, |chat| {
|
||||
// chat.read(cx).channel_id == *channel_id
|
||||
// })
|
||||
// });
|
||||
// }
|
||||
// false
|
||||
// })
|
||||
// .unwrap_or_default();
|
||||
// }
|
||||
// }
|
||||
|
||||
// false
|
||||
// }
|
||||
|
||||
// fn render_sign_in_prompt(
|
||||
// &self,
|
||||
// theme: &Arc<Theme>,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> AnyElement<Self> {
|
||||
// enum SignInPromptLabel {}
|
||||
|
||||
// MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
|
||||
// Label::new(
|
||||
// "Sign in to view your notifications".to_string(),
|
||||
// theme
|
||||
// .chat_panel
|
||||
// .sign_in_prompt
|
||||
// .style_for(mouse_state)
|
||||
// .clone(),
|
||||
// )
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||
// let client = this.client.clone();
|
||||
// cx.spawn(|_, cx| async move {
|
||||
// client.authenticate_and_connect(true, &cx).log_err().await;
|
||||
// })
|
||||
// .detach();
|
||||
// })
|
||||
// .aligned()
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn render_empty_state(
|
||||
// &self,
|
||||
// theme: &Arc<Theme>,
|
||||
// _cx: &mut ViewContext<Self>,
|
||||
// ) -> AnyElement<Self> {
|
||||
// Label::new(
|
||||
// "You have no notifications".to_string(),
|
||||
// theme.chat_panel.sign_in_prompt.default.clone(),
|
||||
// )
|
||||
// .aligned()
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn on_notification_event(
|
||||
// &mut self,
|
||||
// _: ModelHandle<NotificationStore>,
|
||||
// event: &NotificationEvent,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) {
|
||||
// match event {
|
||||
// NotificationEvent::NewNotification { entry } => self.add_toast(entry, cx),
|
||||
// NotificationEvent::NotificationRemoved { entry }
|
||||
// | NotificationEvent::NotificationRead { entry } => self.remove_toast(entry.id, cx),
|
||||
// NotificationEvent::NotificationsUpdated {
|
||||
// old_range,
|
||||
// new_count,
|
||||
// } => {
|
||||
// self.notification_list.splice(old_range.clone(), *new_count);
|
||||
// cx.notify();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext<Self>) {
|
||||
// if self.is_showing_notification(&entry.notification, cx) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// let Some(NotificationPresenter { actor, text, .. }) = self.present_notification(entry, cx)
|
||||
// else {
|
||||
// return;
|
||||
// };
|
||||
|
||||
// let notification_id = entry.id;
|
||||
// self.current_notification_toast = Some((
|
||||
// notification_id,
|
||||
// cx.spawn(|this, mut cx| async move {
|
||||
// cx.background().timer(TOAST_DURATION).await;
|
||||
// this.update(&mut cx, |this, cx| this.remove_toast(notification_id, cx))
|
||||
// .ok();
|
||||
// }),
|
||||
// ));
|
||||
|
||||
// self.workspace
|
||||
// .update(cx, |workspace, cx| {
|
||||
// workspace.dismiss_notification::<NotificationToast>(0, cx);
|
||||
// workspace.show_notification(0, cx, |cx| {
|
||||
// let workspace = cx.weak_handle();
|
||||
// cx.add_view(|_| NotificationToast {
|
||||
// notification_id,
|
||||
// actor,
|
||||
// text,
|
||||
// workspace,
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// .ok();
|
||||
// }
|
||||
|
||||
// fn remove_toast(&mut self, notification_id: u64, cx: &mut ViewContext<Self>) {
|
||||
// if let Some((current_id, _)) = &self.current_notification_toast {
|
||||
// if *current_id == notification_id {
|
||||
// self.current_notification_toast.take();
|
||||
// self.workspace
|
||||
// .update(cx, |workspace, cx| {
|
||||
// workspace.dismiss_notification::<NotificationToast>(0, cx)
|
||||
// })
|
||||
// .ok();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn respond_to_notification(
|
||||
// &mut self,
|
||||
// notification: Notification,
|
||||
// response: bool,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) {
|
||||
// self.notification_store.update(cx, |store, cx| {
|
||||
// store.respond_to_notification(notification, response, cx);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl Entity for NotificationPanel {
|
||||
// type Event = Event;
|
||||
// }
|
||||
|
||||
// impl View for NotificationPanel {
|
||||
// fn ui_name() -> &'static str {
|
||||
// "NotificationPanel"
|
||||
// }
|
||||
|
||||
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
// let theme = theme::current(cx);
|
||||
// let style = &theme.notification_panel;
|
||||
// let element = if self.client.user_id().is_none() {
|
||||
// self.render_sign_in_prompt(&theme, cx)
|
||||
// } else if self.notification_list.item_count() == 0 {
|
||||
// self.render_empty_state(&theme, cx)
|
||||
// } else {
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// Flex::row()
|
||||
// .with_child(Label::new("Notifications", style.title.text.clone()))
|
||||
// .with_child(ui::svg(&style.title_icon).flex_float())
|
||||
// .align_children_center()
|
||||
// .contained()
|
||||
// .with_style(style.title.container)
|
||||
// .constrained()
|
||||
// .with_height(style.title_height),
|
||||
// )
|
||||
// .with_child(
|
||||
// List::new(self.notification_list.clone())
|
||||
// .contained()
|
||||
// .with_style(style.list)
|
||||
// .flex(1., true),
|
||||
// )
|
||||
// .into_any()
|
||||
// };
|
||||
// element
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// .constrained()
|
||||
// .with_min_width(150.)
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||
// self.has_focus = true;
|
||||
// }
|
||||
|
||||
// fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||
// self.has_focus = false;
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl Panel for NotificationPanel {
|
||||
// fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
|
||||
// settings::get::<NotificationPanelSettings>(cx).dock
|
||||
// }
|
||||
|
||||
// fn position_is_valid(&self, position: DockPosition) -> bool {
|
||||
// matches!(position, DockPosition::Left | DockPosition::Right)
|
||||
// }
|
||||
|
||||
// fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
|
||||
// settings::update_settings_file::<NotificationPanelSettings>(
|
||||
// self.fs.clone(),
|
||||
// cx,
|
||||
// move |settings| settings.dock = Some(position),
|
||||
// );
|
||||
// }
|
||||
|
||||
// fn size(&self, cx: &gpui::WindowContext) -> f32 {
|
||||
// self.width
|
||||
// .unwrap_or_else(|| settings::get::<NotificationPanelSettings>(cx).default_width)
|
||||
// }
|
||||
|
||||
// fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
|
||||
// self.width = size;
|
||||
// self.serialize(cx);
|
||||
// cx.notify();
|
||||
// }
|
||||
|
||||
// fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||
// self.active = active;
|
||||
// if self.notification_store.read(cx).notification_count() == 0 {
|
||||
// cx.emit(Event::Dismissed);
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
|
||||
// (settings::get::<NotificationPanelSettings>(cx).button
|
||||
// && self.notification_store.read(cx).notification_count() > 0)
|
||||
// .then(|| "icons/bell.svg")
|
||||
// }
|
||||
|
||||
// fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
|
||||
// (
|
||||
// "Notification Panel".to_string(),
|
||||
// Some(Box::new(ToggleFocus)),
|
||||
// )
|
||||
// }
|
||||
|
||||
// fn icon_label(&self, cx: &WindowContext) -> Option<String> {
|
||||
// let count = self.notification_store.read(cx).unread_notification_count();
|
||||
// if count == 0 {
|
||||
// None
|
||||
// } else {
|
||||
// Some(count.to_string())
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn should_change_position_on_event(event: &Self::Event) -> bool {
|
||||
// matches!(event, Event::DockPositionChanged)
|
||||
// }
|
||||
|
||||
// fn should_close_on_event(event: &Self::Event) -> bool {
|
||||
// matches!(event, Event::Dismissed)
|
||||
// }
|
||||
|
||||
// fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
|
||||
// self.has_focus
|
||||
// }
|
||||
|
||||
// fn is_focus_event(event: &Self::Event) -> bool {
|
||||
// matches!(event, Event::Focus)
|
||||
// }
|
||||
// }
|
||||
|
||||
// pub struct NotificationToast {
|
||||
// notification_id: u64,
|
||||
// actor: Option<Arc<User>>,
|
||||
// text: String,
|
||||
// workspace: WeakViewHandle<Workspace>,
|
||||
// }
|
||||
|
||||
// pub enum ToastEvent {
|
||||
// Dismiss,
|
||||
// }
|
||||
|
||||
// impl NotificationToast {
|
||||
// fn focus_notification_panel(&self, cx: &mut AppContext) {
|
||||
// let workspace = self.workspace.clone();
|
||||
// let notification_id = self.notification_id;
|
||||
// cx.defer(move |cx| {
|
||||
// workspace
|
||||
// .update(cx, |workspace, cx| {
|
||||
// if let Some(panel) = workspace.focus_panel::<NotificationPanel>(cx) {
|
||||
// panel.update(cx, |panel, cx| {
|
||||
// let store = panel.notification_store.read(cx);
|
||||
// if let Some(entry) = store.notification_for_id(notification_id) {
|
||||
// panel.did_click_notification(&entry.clone().notification, cx);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// })
|
||||
// .ok();
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl Entity for NotificationToast {
|
||||
// type Event = ToastEvent;
|
||||
// }
|
||||
|
||||
// impl View for NotificationToast {
|
||||
// fn ui_name() -> &'static str {
|
||||
// "ContactNotification"
|
||||
// }
|
||||
|
||||
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
// let user = self.actor.clone();
|
||||
// let theme = theme::current(cx).clone();
|
||||
// let theme = &theme.contact_notification;
|
||||
|
||||
// MouseEventHandler::new::<Self, _>(0, cx, |_, cx| {
|
||||
// Flex::row()
|
||||
// .with_children(user.and_then(|user| {
|
||||
// Some(
|
||||
// Image::from_data(user.avatar.clone()?)
|
||||
// .with_style(theme.header_avatar)
|
||||
// .aligned()
|
||||
// .constrained()
|
||||
// .with_height(
|
||||
// cx.font_cache()
|
||||
// .line_height(theme.header_message.text.font_size),
|
||||
// )
|
||||
// .aligned()
|
||||
// .top(),
|
||||
// )
|
||||
// }))
|
||||
// .with_child(
|
||||
// Text::new(self.text.clone(), theme.header_message.text.clone())
|
||||
// .contained()
|
||||
// .with_style(theme.header_message.container)
|
||||
// .aligned()
|
||||
// .top()
|
||||
// .left()
|
||||
// .flex(1., true),
|
||||
// )
|
||||
// .with_child(
|
||||
// MouseEventHandler::new::<ToastEvent, _>(0, cx, |state, _| {
|
||||
// let style = theme.dismiss_button.style_for(state);
|
||||
// Svg::new("icons/x.svg")
|
||||
// .with_color(style.color)
|
||||
// .constrained()
|
||||
// .with_width(style.icon_width)
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// .constrained()
|
||||
// .with_width(style.button_width)
|
||||
// .with_height(style.button_width)
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .with_padding(Padding::uniform(5.))
|
||||
// .on_click(MouseButton::Left, move |_, _, cx| {
|
||||
// cx.emit(ToastEvent::Dismiss)
|
||||
// })
|
||||
// .aligned()
|
||||
// .constrained()
|
||||
// .with_height(
|
||||
// cx.font_cache()
|
||||
// .line_height(theme.header_message.text.font_size),
|
||||
// )
|
||||
// .aligned()
|
||||
// .top()
|
||||
// .flex_float(),
|
||||
// )
|
||||
// .contained()
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||
// this.focus_notification_panel(cx);
|
||||
// cx.emit(ToastEvent::Dismiss);
|
||||
// })
|
||||
// .into_any()
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl workspace::notifications::Notification for NotificationToast {
|
||||
// fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
|
||||
// matches!(event, ToastEvent::Dismiss)
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn format_timestamp(
|
||||
// mut timestamp: OffsetDateTime,
|
||||
// mut now: OffsetDateTime,
|
||||
// local_timezone: UtcOffset,
|
||||
// ) -> String {
|
||||
// timestamp = timestamp.to_offset(local_timezone);
|
||||
// now = now.to_offset(local_timezone);
|
||||
|
||||
// let today = now.date();
|
||||
// let date = timestamp.date();
|
||||
// if date == today {
|
||||
// let difference = now - timestamp;
|
||||
// if difference >= Duration::from_secs(3600) {
|
||||
// format!("{}h", difference.whole_seconds() / 3600)
|
||||
// } else if difference >= Duration::from_secs(60) {
|
||||
// format!("{}m", difference.whole_seconds() / 60)
|
||||
// } else {
|
||||
// "just now".to_string()
|
||||
// }
|
||||
// } else if date.next_day() == Some(today) {
|
||||
// format!("yesterday")
|
||||
// } else {
|
||||
// format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
|
||||
// }
|
||||
// }
|
||||
11
crates/collab_ui2/src/notifications.rs
Normal file
11
crates/collab_ui2/src/notifications.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
// use gpui::AppContext;
|
||||
// use std::sync::Arc;
|
||||
// use workspace::AppState;
|
||||
|
||||
// pub mod incoming_call_notification;
|
||||
// pub mod project_shared_notification;
|
||||
|
||||
// pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
// incoming_call_notification::init(app_state, cx);
|
||||
// project_shared_notification::init(app_state, cx);
|
||||
// }
|
||||
@@ -0,0 +1,213 @@
|
||||
use crate::notification_window_options;
|
||||
use call::{ActiveCall, IncomingCall};
|
||||
use client::proto;
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
elements::*,
|
||||
geometry::vector::vec2f,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AnyElement, AppContext, Entity, View, ViewContext, WindowHandle,
|
||||
};
|
||||
use std::sync::{Arc, Weak};
|
||||
use util::ResultExt;
|
||||
use workspace::AppState;
|
||||
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
let app_state = Arc::downgrade(app_state);
|
||||
let mut incoming_call = ActiveCall::global(cx).read(cx).incoming();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let mut notification_windows: Vec<WindowHandle<IncomingCallNotification>> = Vec::new();
|
||||
while let Some(incoming_call) = incoming_call.next().await {
|
||||
for window in notification_windows.drain(..) {
|
||||
window.remove(&mut cx);
|
||||
}
|
||||
|
||||
if let Some(incoming_call) = incoming_call {
|
||||
let window_size = cx.read(|cx| {
|
||||
let theme = &theme::current(cx).incoming_call_notification;
|
||||
vec2f(theme.window_width, theme.window_height)
|
||||
});
|
||||
|
||||
for screen in cx.platform().screens() {
|
||||
let window = cx
|
||||
.add_window(notification_window_options(screen, window_size), |_| {
|
||||
IncomingCallNotification::new(incoming_call.clone(), app_state.clone())
|
||||
});
|
||||
|
||||
notification_windows.push(window);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct RespondToCall {
|
||||
accept: bool,
|
||||
}
|
||||
|
||||
pub struct IncomingCallNotification {
|
||||
call: IncomingCall,
|
||||
app_state: Weak<AppState>,
|
||||
}
|
||||
|
||||
impl IncomingCallNotification {
|
||||
pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
|
||||
Self { call, app_state }
|
||||
}
|
||||
|
||||
fn respond(&mut self, accept: bool, cx: &mut ViewContext<Self>) {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
if accept {
|
||||
let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
|
||||
let caller_user_id = self.call.calling_user.id;
|
||||
let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
|
||||
let app_state = self.app_state.clone();
|
||||
cx.app_context()
|
||||
.spawn(|mut cx| async move {
|
||||
join.await?;
|
||||
if let Some(project_id) = initial_project_id {
|
||||
cx.update(|cx| {
|
||||
if let Some(app_state) = app_state.upgrade() {
|
||||
workspace::join_remote_project(
|
||||
project_id,
|
||||
caller_user_id,
|
||||
app_state,
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
} else {
|
||||
active_call.update(cx, |active_call, cx| {
|
||||
active_call.decline_incoming(cx).log_err();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn render_caller(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let theme = &theme::current(cx).incoming_call_notification;
|
||||
let default_project = proto::ParticipantProject::default();
|
||||
let initial_project = self
|
||||
.call
|
||||
.initial_project
|
||||
.as_ref()
|
||||
.unwrap_or(&default_project);
|
||||
Flex::row()
|
||||
.with_children(self.call.calling_user.avatar.clone().map(|avatar| {
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.caller_avatar)
|
||||
.aligned()
|
||||
}))
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Label::new(
|
||||
self.call.calling_user.github_login.clone(),
|
||||
theme.caller_username.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.caller_username.container),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(
|
||||
format!(
|
||||
"is sharing a project in Zed{}",
|
||||
if initial_project.worktree_root_names.is_empty() {
|
||||
""
|
||||
} else {
|
||||
":"
|
||||
}
|
||||
),
|
||||
theme.caller_message.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.caller_message.container),
|
||||
)
|
||||
.with_children(if initial_project.worktree_root_names.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Label::new(
|
||||
initial_project.worktree_root_names.join(", "),
|
||||
theme.worktree_roots.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.worktree_roots.container),
|
||||
)
|
||||
})
|
||||
.contained()
|
||||
.with_style(theme.caller_metadata)
|
||||
.aligned(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.caller_container)
|
||||
.flex(1., true)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
enum Accept {}
|
||||
enum Decline {}
|
||||
|
||||
let theme = theme::current(cx);
|
||||
Flex::column()
|
||||
.with_child(
|
||||
MouseEventHandler::new::<Accept, _>(0, cx, |_, _| {
|
||||
let theme = &theme.incoming_call_notification;
|
||||
Label::new("Accept", theme.accept_button.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.accept_button.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
this.respond(true, cx);
|
||||
})
|
||||
.flex(1., true),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::new::<Decline, _>(0, cx, |_, _| {
|
||||
let theme = &theme.incoming_call_notification;
|
||||
Label::new("Decline", theme.decline_button.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.decline_button.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
this.respond(false, cx);
|
||||
})
|
||||
.flex(1., true),
|
||||
)
|
||||
.constrained()
|
||||
.with_width(theme.incoming_call_notification.button_width)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for IncomingCallNotification {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for IncomingCallNotification {
|
||||
fn ui_name() -> &'static str {
|
||||
"IncomingCallNotification"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let background = theme::current(cx).incoming_call_notification.background;
|
||||
Flex::row()
|
||||
.with_child(self.render_caller(cx))
|
||||
.with_child(self.render_buttons(cx))
|
||||
.contained()
|
||||
.with_background_color(background)
|
||||
.expanded()
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
use crate::notification_window_options;
|
||||
use call::{room, ActiveCall};
|
||||
use client::User;
|
||||
use collections::HashMap;
|
||||
use gpui::{
|
||||
elements::*,
|
||||
geometry::vector::vec2f,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AppContext, Entity, View, ViewContext,
|
||||
};
|
||||
use std::sync::{Arc, Weak};
|
||||
use workspace::AppState;
|
||||
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
let app_state = Arc::downgrade(app_state);
|
||||
let active_call = ActiveCall::global(cx);
|
||||
let mut notification_windows = HashMap::default();
|
||||
cx.subscribe(&active_call, move |_, event, cx| match event {
|
||||
room::Event::RemoteProjectShared {
|
||||
owner,
|
||||
project_id,
|
||||
worktree_root_names,
|
||||
} => {
|
||||
let theme = &theme::current(cx).project_shared_notification;
|
||||
let window_size = vec2f(theme.window_width, theme.window_height);
|
||||
|
||||
for screen in cx.platform().screens() {
|
||||
let window =
|
||||
cx.add_window(notification_window_options(screen, window_size), |_| {
|
||||
ProjectSharedNotification::new(
|
||||
owner.clone(),
|
||||
*project_id,
|
||||
worktree_root_names.clone(),
|
||||
app_state.clone(),
|
||||
)
|
||||
});
|
||||
notification_windows
|
||||
.entry(*project_id)
|
||||
.or_insert(Vec::new())
|
||||
.push(window);
|
||||
}
|
||||
}
|
||||
room::Event::RemoteProjectUnshared { project_id }
|
||||
| room::Event::RemoteProjectJoined { project_id }
|
||||
| room::Event::RemoteProjectInvitationDiscarded { project_id } => {
|
||||
if let Some(windows) = notification_windows.remove(&project_id) {
|
||||
for window in windows {
|
||||
window.remove(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
room::Event::Left => {
|
||||
for (_, windows) in notification_windows.drain() {
|
||||
for window in windows {
|
||||
window.remove(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub struct ProjectSharedNotification {
|
||||
project_id: u64,
|
||||
worktree_root_names: Vec<String>,
|
||||
owner: Arc<User>,
|
||||
app_state: Weak<AppState>,
|
||||
}
|
||||
|
||||
impl ProjectSharedNotification {
|
||||
fn new(
|
||||
owner: Arc<User>,
|
||||
project_id: u64,
|
||||
worktree_root_names: Vec<String>,
|
||||
app_state: Weak<AppState>,
|
||||
) -> Self {
|
||||
Self {
|
||||
project_id,
|
||||
worktree_root_names,
|
||||
owner,
|
||||
app_state,
|
||||
}
|
||||
}
|
||||
|
||||
fn join(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(app_state) = self.app_state.upgrade() {
|
||||
workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(active_room) =
|
||||
ActiveCall::global(cx).read_with(cx, |call, _| call.room().cloned())
|
||||
{
|
||||
active_room.update(cx, |_, cx| {
|
||||
cx.emit(room::Event::RemoteProjectInvitationDiscarded {
|
||||
project_id: self.project_id,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn render_owner(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let theme = &theme::current(cx).project_shared_notification;
|
||||
Flex::row()
|
||||
.with_children(self.owner.avatar.clone().map(|avatar| {
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.owner_avatar)
|
||||
.aligned()
|
||||
}))
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Label::new(
|
||||
self.owner.github_login.clone(),
|
||||
theme.owner_username.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.owner_username.container),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(
|
||||
format!(
|
||||
"is sharing a project in Zed{}",
|
||||
if self.worktree_root_names.is_empty() {
|
||||
""
|
||||
} else {
|
||||
":"
|
||||
}
|
||||
),
|
||||
theme.message.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.message.container),
|
||||
)
|
||||
.with_children(if self.worktree_root_names.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Label::new(
|
||||
self.worktree_root_names.join(", "),
|
||||
theme.worktree_roots.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.worktree_roots.container),
|
||||
)
|
||||
})
|
||||
.contained()
|
||||
.with_style(theme.owner_metadata)
|
||||
.aligned(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.owner_container)
|
||||
.flex(1., true)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
enum Open {}
|
||||
enum Dismiss {}
|
||||
|
||||
let theme = theme::current(cx);
|
||||
Flex::column()
|
||||
.with_child(
|
||||
MouseEventHandler::new::<Open, _>(0, cx, |_, _| {
|
||||
let theme = &theme.project_shared_notification;
|
||||
Label::new("Open", theme.open_button.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.open_button.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| this.join(cx))
|
||||
.flex(1., true),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::new::<Dismiss, _>(0, cx, |_, _| {
|
||||
let theme = &theme.project_shared_notification;
|
||||
Label::new("Dismiss", theme.dismiss_button.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.dismiss_button.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
this.dismiss(cx);
|
||||
})
|
||||
.flex(1., true),
|
||||
)
|
||||
.constrained()
|
||||
.with_width(theme.project_shared_notification.button_width)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ProjectSharedNotification {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for ProjectSharedNotification {
|
||||
fn ui_name() -> &'static str {
|
||||
"ProjectSharedNotification"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
|
||||
let background = theme::current(cx).project_shared_notification.background;
|
||||
Flex::row()
|
||||
.with_child(self.render_owner(cx))
|
||||
.with_child(self.render_buttons(cx))
|
||||
.contained()
|
||||
.with_background_color(background)
|
||||
.expanded()
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
69
crates/collab_ui2/src/panel_settings.rs
Normal file
69
crates/collab_ui2/src/panel_settings.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use anyhow;
|
||||
use schemars::JsonSchema;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use workspace::dock::DockPosition;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct CollaborationPanelSettings {
|
||||
pub button: bool,
|
||||
pub dock: DockPosition,
|
||||
pub default_width: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ChatPanelSettings {
|
||||
pub button: bool,
|
||||
pub dock: DockPosition,
|
||||
pub default_width: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct NotificationPanelSettings {
|
||||
pub button: bool,
|
||||
pub dock: DockPosition,
|
||||
pub default_width: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct PanelSettingsContent {
|
||||
pub button: Option<bool>,
|
||||
pub dock: Option<DockPosition>,
|
||||
pub default_width: Option<f32>,
|
||||
}
|
||||
|
||||
impl Settings for CollaborationPanelSettings {
|
||||
const KEY: Option<&'static str> = Some("collaboration_panel");
|
||||
type FileContent = PanelSettingsContent;
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings for ChatPanelSettings {
|
||||
const KEY: Option<&'static str> = Some("chat_panel");
|
||||
type FileContent = PanelSettingsContent;
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings for NotificationPanelSettings {
|
||||
const KEY: Option<&'static str> = Some("notification_panel");
|
||||
type FileContent = PanelSettingsContent;
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
use collections::{CommandPaletteFilter, HashMap};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, div, prelude::*, Action, AppContext, Component, Div, EventEmitter, FocusHandle,
|
||||
Keystroke, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView,
|
||||
WindowContext,
|
||||
actions, div, prelude::*, Action, AppContext, Component, Dismiss, Div, FocusHandle, Keystroke,
|
||||
ManagedView, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::{
|
||||
@@ -16,7 +15,7 @@ use util::{
|
||||
channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
|
||||
ResultExt,
|
||||
};
|
||||
use workspace::{Modal, ModalEvent, Workspace};
|
||||
use workspace::Workspace;
|
||||
use zed_actions::OpenZedURL;
|
||||
|
||||
actions!(Toggle);
|
||||
@@ -47,7 +46,7 @@ impl CommandPalette {
|
||||
.available_actions()
|
||||
.into_iter()
|
||||
.filter_map(|action| {
|
||||
let name = action.name();
|
||||
let name = gpui::remove_the_2(action.name());
|
||||
let namespace = name.split("::").next().unwrap_or("malformed action name");
|
||||
if filter.is_some_and(|f| f.filtered_namespaces.contains(namespace)) {
|
||||
return None;
|
||||
@@ -69,10 +68,9 @@ impl CommandPalette {
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ModalEvent> for CommandPalette {}
|
||||
impl Modal for CommandPalette {
|
||||
fn focus(&self, cx: &mut WindowContext) {
|
||||
self.picker.update(cx, |picker, cx| picker.focus(cx));
|
||||
impl ManagedView for CommandPalette {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,7 +265,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.command_palette
|
||||
.update(cx, |_, cx| cx.emit(ModalEvent::Dismissed))
|
||||
.update(cx, |_, cx| cx.emit(Dismiss))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
@@ -385,8 +383,7 @@ mod tests {
|
||||
let app_state = init_test(cx);
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||
let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let cx = &mut cx;
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
let editor = cx.build_view(|cx| {
|
||||
let mut editor = Editor::single_line(cx);
|
||||
@@ -403,7 +400,7 @@ mod tests {
|
||||
|
||||
let palette = workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.current_modal::<CommandPalette>(cx)
|
||||
.active_modal::<CommandPalette>(cx)
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.picker
|
||||
@@ -417,7 +414,7 @@ mod tests {
|
||||
assert!(is_sorted(&palette.delegate.commands));
|
||||
});
|
||||
|
||||
cx.simulate_keystrokes("b c k s p");
|
||||
cx.simulate_input("bcksp");
|
||||
|
||||
palette.update(cx, |palette, _| {
|
||||
assert_eq!(palette.delegate.matches[0].string, "editor: backspace");
|
||||
@@ -426,7 +423,7 @@ mod tests {
|
||||
cx.simulate_keystrokes("enter");
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
assert!(workspace.current_modal::<CommandPalette>(cx).is_none());
|
||||
assert!(workspace.active_modal::<CommandPalette>(cx).is_none());
|
||||
assert_eq!(editor.read(cx).text(cx), "ab")
|
||||
});
|
||||
|
||||
@@ -439,11 +436,11 @@ mod tests {
|
||||
});
|
||||
|
||||
cx.simulate_keystrokes("cmd-shift-p");
|
||||
cx.simulate_keystrokes("b c k s p");
|
||||
cx.simulate_input("bcksp");
|
||||
|
||||
let palette = workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.current_modal::<CommandPalette>(cx)
|
||||
.active_modal::<CommandPalette>(cx)
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.picker
|
||||
@@ -457,7 +454,7 @@ mod tests {
|
||||
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
|
||||
cx.update(|cx| {
|
||||
let app_state = AppState::test(cx);
|
||||
theme::init(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
language::init(cx);
|
||||
editor::init(cx);
|
||||
workspace::init(app_state.clone(), cx);
|
||||
|
||||
@@ -1051,17 +1051,15 @@ mod tests {
|
||||
);
|
||||
|
||||
// Ensure updates to the file are reflected in the LSP.
|
||||
buffer_1
|
||||
.update(cx, |buffer, cx| {
|
||||
buffer.file_updated(
|
||||
Arc::new(File {
|
||||
abs_path: "/root/child/buffer-1".into(),
|
||||
path: Path::new("child/buffer-1").into(),
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
buffer_1.update(cx, |buffer, cx| {
|
||||
buffer.file_updated(
|
||||
Arc::new(File {
|
||||
abs_path: "/root/child/buffer-1".into(),
|
||||
path: Path::new("child/buffer-1").into(),
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
.await,
|
||||
|
||||
@@ -13,7 +13,8 @@ pub use block_map::{BlockMap, BlockPoint};
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use fold_map::FoldMap;
|
||||
use gpui::{
|
||||
Font, FontId, HighlightStyle, Hsla, Line, Model, ModelContext, Pixels, TextRun, UnderlineStyle,
|
||||
Font, FontId, HighlightStyle, Hsla, LineLayout, Model, ModelContext, Pixels, ShapedLine,
|
||||
TextRun, UnderlineStyle, WrappedLine,
|
||||
};
|
||||
use inlay_map::InlayMap;
|
||||
use language::{
|
||||
@@ -31,7 +32,7 @@ pub use block_map::{
|
||||
BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
|
||||
};
|
||||
|
||||
pub use self::fold_map::FoldPoint;
|
||||
pub use self::fold_map::{Fold, FoldPoint};
|
||||
pub use self::inlay_map::{Inlay, InlayOffset, InlayPoint};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
@@ -124,7 +125,7 @@ impl DisplayMap {
|
||||
self.fold(
|
||||
other
|
||||
.folds_in_range(0..other.buffer_snapshot.len())
|
||||
.map(|fold| fold.to_offset(&other.buffer_snapshot)),
|
||||
.map(|fold| fold.range.to_offset(&other.buffer_snapshot)),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -561,7 +562,7 @@ impl DisplaySnapshot {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn lay_out_line_for_row(
|
||||
pub fn layout_row(
|
||||
&self,
|
||||
display_row: u32,
|
||||
TextLayoutDetails {
|
||||
@@ -569,7 +570,7 @@ impl DisplaySnapshot {
|
||||
editor_style,
|
||||
rem_size,
|
||||
}: &TextLayoutDetails,
|
||||
) -> Line {
|
||||
) -> Arc<LineLayout> {
|
||||
let mut runs = Vec::new();
|
||||
let mut line = String::new();
|
||||
|
||||
@@ -598,29 +599,27 @@ impl DisplaySnapshot {
|
||||
|
||||
let font_size = editor_style.text.font_size.to_pixels(*rem_size);
|
||||
text_system
|
||||
.layout_text(&line, font_size, &runs, None)
|
||||
.unwrap()
|
||||
.pop()
|
||||
.unwrap()
|
||||
.layout_line(&line, font_size, &runs)
|
||||
.expect("we expect the font to be loaded because it's rendered by the editor")
|
||||
}
|
||||
|
||||
pub fn x_for_point(
|
||||
pub fn x_for_display_point(
|
||||
&self,
|
||||
display_point: DisplayPoint,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
) -> Pixels {
|
||||
let layout_line = self.lay_out_line_for_row(display_point.row(), text_layout_details);
|
||||
layout_line.x_for_index(display_point.column() as usize)
|
||||
let line = self.layout_row(display_point.row(), text_layout_details);
|
||||
line.x_for_index(display_point.column() as usize)
|
||||
}
|
||||
|
||||
pub fn column_for_x(
|
||||
pub fn display_column_for_x(
|
||||
&self,
|
||||
display_row: u32,
|
||||
x_coordinate: Pixels,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
x: Pixels,
|
||||
details: &TextLayoutDetails,
|
||||
) -> u32 {
|
||||
let layout_line = self.lay_out_line_for_row(display_row, text_layout_details);
|
||||
layout_line.closest_index_for_x(x_coordinate) as u32
|
||||
let layout_line = self.layout_row(display_row, details);
|
||||
layout_line.closest_index_for_x(x) as u32
|
||||
}
|
||||
|
||||
pub fn chars_at(
|
||||
@@ -723,7 +722,7 @@ impl DisplaySnapshot {
|
||||
DisplayPoint(point)
|
||||
}
|
||||
|
||||
pub fn folds_in_range<T>(&self, range: Range<T>) -> impl Iterator<Item = &Range<Anchor>>
|
||||
pub fn folds_in_range<T>(&self, range: Range<T>) -> impl Iterator<Item = &Fold>
|
||||
where
|
||||
T: ToOffset,
|
||||
{
|
||||
|
||||
@@ -3,15 +3,16 @@ use super::{
|
||||
Highlights,
|
||||
};
|
||||
use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset};
|
||||
use gpui::{HighlightStyle, Hsla};
|
||||
use gpui::{ElementId, HighlightStyle, Hsla};
|
||||
use language::{Chunk, Edit, Point, TextSummary};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
cmp::{self, Ordering},
|
||||
iter,
|
||||
ops::{Add, AddAssign, Range, Sub},
|
||||
ops::{Add, AddAssign, Deref, DerefMut, Range, Sub},
|
||||
};
|
||||
use sum_tree::{Bias, Cursor, FilterCursor, SumTree};
|
||||
use util::post_inc;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||
pub struct FoldPoint(pub Point);
|
||||
@@ -90,12 +91,16 @@ impl<'a> FoldMapWriter<'a> {
|
||||
}
|
||||
|
||||
// For now, ignore any ranges that span an excerpt boundary.
|
||||
let fold = Fold(buffer.anchor_after(range.start)..buffer.anchor_before(range.end));
|
||||
if fold.0.start.excerpt_id != fold.0.end.excerpt_id {
|
||||
let fold_range =
|
||||
FoldRange(buffer.anchor_after(range.start)..buffer.anchor_before(range.end));
|
||||
if fold_range.0.start.excerpt_id != fold_range.0.end.excerpt_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
folds.push(fold);
|
||||
folds.push(Fold {
|
||||
id: FoldId(post_inc(&mut self.0.next_fold_id.0)),
|
||||
range: fold_range,
|
||||
});
|
||||
|
||||
let inlay_range =
|
||||
snapshot.to_inlay_offset(range.start)..snapshot.to_inlay_offset(range.end);
|
||||
@@ -106,13 +111,13 @@ impl<'a> FoldMapWriter<'a> {
|
||||
}
|
||||
|
||||
let buffer = &snapshot.buffer;
|
||||
folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(a, b, buffer));
|
||||
folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(&a.range, &b.range, buffer));
|
||||
|
||||
self.0.snapshot.folds = {
|
||||
let mut new_tree = SumTree::new();
|
||||
let mut cursor = self.0.snapshot.folds.cursor::<Fold>();
|
||||
let mut cursor = self.0.snapshot.folds.cursor::<FoldRange>();
|
||||
for fold in folds {
|
||||
new_tree.append(cursor.slice(&fold, Bias::Right, buffer), buffer);
|
||||
new_tree.append(cursor.slice(&fold.range, Bias::Right, buffer), buffer);
|
||||
new_tree.push(fold, buffer);
|
||||
}
|
||||
new_tree.append(cursor.suffix(buffer), buffer);
|
||||
@@ -138,7 +143,8 @@ impl<'a> FoldMapWriter<'a> {
|
||||
let mut folds_cursor =
|
||||
intersecting_folds(&snapshot, &self.0.snapshot.folds, range, inclusive);
|
||||
while let Some(fold) = folds_cursor.item() {
|
||||
let offset_range = fold.0.start.to_offset(buffer)..fold.0.end.to_offset(buffer);
|
||||
let offset_range =
|
||||
fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer);
|
||||
if offset_range.end > offset_range.start {
|
||||
let inlay_range = snapshot.to_inlay_offset(offset_range.start)
|
||||
..snapshot.to_inlay_offset(offset_range.end);
|
||||
@@ -175,6 +181,7 @@ impl<'a> FoldMapWriter<'a> {
|
||||
pub struct FoldMap {
|
||||
snapshot: FoldSnapshot,
|
||||
ellipses_color: Option<Hsla>,
|
||||
next_fold_id: FoldId,
|
||||
}
|
||||
|
||||
impl FoldMap {
|
||||
@@ -197,6 +204,7 @@ impl FoldMap {
|
||||
ellipses_color: None,
|
||||
},
|
||||
ellipses_color: None,
|
||||
next_fold_id: FoldId::default(),
|
||||
};
|
||||
let snapshot = this.snapshot.clone();
|
||||
(this, snapshot)
|
||||
@@ -242,8 +250,8 @@ impl FoldMap {
|
||||
while let Some(fold) = folds.next() {
|
||||
if let Some(next_fold) = folds.peek() {
|
||||
let comparison = fold
|
||||
.0
|
||||
.cmp(&next_fold.0, &self.snapshot.inlay_snapshot.buffer);
|
||||
.range
|
||||
.cmp(&next_fold.range, &self.snapshot.inlay_snapshot.buffer);
|
||||
assert!(comparison.is_le());
|
||||
}
|
||||
}
|
||||
@@ -304,9 +312,9 @@ impl FoldMap {
|
||||
let anchor = inlay_snapshot
|
||||
.buffer
|
||||
.anchor_before(inlay_snapshot.to_buffer_offset(edit.new.start));
|
||||
let mut folds_cursor = self.snapshot.folds.cursor::<Fold>();
|
||||
let mut folds_cursor = self.snapshot.folds.cursor::<FoldRange>();
|
||||
folds_cursor.seek(
|
||||
&Fold(anchor..Anchor::max()),
|
||||
&FoldRange(anchor..Anchor::max()),
|
||||
Bias::Left,
|
||||
&inlay_snapshot.buffer,
|
||||
);
|
||||
@@ -315,8 +323,8 @@ impl FoldMap {
|
||||
let inlay_snapshot = &inlay_snapshot;
|
||||
move || {
|
||||
let item = folds_cursor.item().map(|f| {
|
||||
let buffer_start = f.0.start.to_offset(&inlay_snapshot.buffer);
|
||||
let buffer_end = f.0.end.to_offset(&inlay_snapshot.buffer);
|
||||
let buffer_start = f.range.start.to_offset(&inlay_snapshot.buffer);
|
||||
let buffer_end = f.range.end.to_offset(&inlay_snapshot.buffer);
|
||||
inlay_snapshot.to_inlay_offset(buffer_start)
|
||||
..inlay_snapshot.to_inlay_offset(buffer_end)
|
||||
});
|
||||
@@ -596,13 +604,13 @@ impl FoldSnapshot {
|
||||
self.transforms.summary().output.longest_row
|
||||
}
|
||||
|
||||
pub fn folds_in_range<T>(&self, range: Range<T>) -> impl Iterator<Item = &Range<Anchor>>
|
||||
pub fn folds_in_range<T>(&self, range: Range<T>) -> impl Iterator<Item = &Fold>
|
||||
where
|
||||
T: ToOffset,
|
||||
{
|
||||
let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false);
|
||||
iter::from_fn(move || {
|
||||
let item = folds.item().map(|f| &f.0);
|
||||
let item = folds.item();
|
||||
folds.next(&self.inlay_snapshot.buffer);
|
||||
item
|
||||
})
|
||||
@@ -830,10 +838,39 @@ impl sum_tree::Summary for TransformSummary {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Fold(Range<Anchor>);
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
|
||||
pub struct FoldId(usize);
|
||||
|
||||
impl Default for Fold {
|
||||
impl Into<ElementId> for FoldId {
|
||||
fn into(self) -> ElementId {
|
||||
ElementId::Integer(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Fold {
|
||||
pub id: FoldId,
|
||||
pub range: FoldRange,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct FoldRange(Range<Anchor>);
|
||||
|
||||
impl Deref for FoldRange {
|
||||
type Target = Range<Anchor>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for FoldRange {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FoldRange {
|
||||
fn default() -> Self {
|
||||
Self(Anchor::min()..Anchor::max())
|
||||
}
|
||||
@@ -844,17 +881,17 @@ impl sum_tree::Item for Fold {
|
||||
|
||||
fn summary(&self) -> Self::Summary {
|
||||
FoldSummary {
|
||||
start: self.0.start.clone(),
|
||||
end: self.0.end.clone(),
|
||||
min_start: self.0.start.clone(),
|
||||
max_end: self.0.end.clone(),
|
||||
start: self.range.start.clone(),
|
||||
end: self.range.end.clone(),
|
||||
min_start: self.range.start.clone(),
|
||||
max_end: self.range.end.clone(),
|
||||
count: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct FoldSummary {
|
||||
pub struct FoldSummary {
|
||||
start: Anchor,
|
||||
end: Anchor,
|
||||
min_start: Anchor,
|
||||
@@ -900,14 +937,14 @@ impl sum_tree::Summary for FoldSummary {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, FoldSummary> for Fold {
|
||||
impl<'a> sum_tree::Dimension<'a, FoldSummary> for FoldRange {
|
||||
fn add_summary(&mut self, summary: &'a FoldSummary, _: &MultiBufferSnapshot) {
|
||||
self.0.start = summary.start.clone();
|
||||
self.0.end = summary.end.clone();
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::SeekTarget<'a, FoldSummary, Fold> for Fold {
|
||||
impl<'a> sum_tree::SeekTarget<'a, FoldSummary, FoldRange> for FoldRange {
|
||||
fn cmp(&self, other: &Self, buffer: &MultiBufferSnapshot) -> Ordering {
|
||||
self.0.cmp(&other.0, buffer)
|
||||
}
|
||||
@@ -1321,7 +1358,10 @@ mod tests {
|
||||
let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
|
||||
let fold_ranges = snapshot
|
||||
.folds_in_range(Point::new(1, 0)..Point::new(1, 3))
|
||||
.map(|fold| fold.start.to_point(&buffer_snapshot)..fold.end.to_point(&buffer_snapshot))
|
||||
.map(|fold| {
|
||||
fold.range.start.to_point(&buffer_snapshot)
|
||||
..fold.range.end.to_point(&buffer_snapshot)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
fold_ranges,
|
||||
@@ -1553,10 +1593,9 @@ mod tests {
|
||||
.filter(|fold| {
|
||||
let start = buffer_snapshot.anchor_before(start);
|
||||
let end = buffer_snapshot.anchor_after(end);
|
||||
start.cmp(&fold.0.end, &buffer_snapshot) == Ordering::Less
|
||||
&& end.cmp(&fold.0.start, &buffer_snapshot) == Ordering::Greater
|
||||
start.cmp(&fold.range.end, &buffer_snapshot) == Ordering::Less
|
||||
&& end.cmp(&fold.range.start, &buffer_snapshot) == Ordering::Greater
|
||||
})
|
||||
.map(|fold| fold.0)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
@@ -1639,10 +1678,10 @@ mod tests {
|
||||
let buffer = &inlay_snapshot.buffer;
|
||||
let mut folds = self.snapshot.folds.items(buffer);
|
||||
// Ensure sorting doesn't change how folds get merged and displayed.
|
||||
folds.sort_by(|a, b| a.0.cmp(&b.0, buffer));
|
||||
folds.sort_by(|a, b| a.range.cmp(&b.range, buffer));
|
||||
let mut fold_ranges = folds
|
||||
.iter()
|
||||
.map(|fold| fold.0.start.to_offset(buffer)..fold.0.end.to_offset(buffer))
|
||||
.map(|fold| fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer))
|
||||
.peekable();
|
||||
|
||||
let mut merged_ranges = Vec::new();
|
||||
|
||||
@@ -1891,6 +1891,6 @@ mod tests {
|
||||
fn init_test(cx: &mut AppContext) {
|
||||
let store = SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
theme::init(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,12 +39,12 @@ use futures::FutureExt;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use git::diff_hunk_to_display;
|
||||
use gpui::{
|
||||
action, actions, div, point, prelude::*, px, relative, rems, render_view, size, uniform_list,
|
||||
AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem,
|
||||
Component, Context, EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight,
|
||||
HighlightStyle, Hsla, InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels,
|
||||
Render, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext,
|
||||
VisualContext, WeakView, WindowContext,
|
||||
actions, div, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement,
|
||||
AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context,
|
||||
EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle,
|
||||
Hsla, InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels, Render, Styled,
|
||||
Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext,
|
||||
WeakView, WindowContext,
|
||||
};
|
||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||
use hover_popover::{hide_hover, HoverState};
|
||||
@@ -97,7 +97,7 @@ use text::{OffsetUtf16, Rope};
|
||||
use theme::{
|
||||
ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings,
|
||||
};
|
||||
use ui::{v_stack, HighlightedLabel, IconButton, StyledExt, TextTooltip};
|
||||
use ui::{v_stack, HighlightedLabel, IconButton, StyledExt, Tooltip};
|
||||
use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
item::{ItemEvent, ItemHandle},
|
||||
@@ -180,78 +180,78 @@ pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
// // .with_soft_wrap(true)
|
||||
// }
|
||||
|
||||
#[action]
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
pub struct SelectNext {
|
||||
#[serde(default)]
|
||||
pub replace_newest: bool,
|
||||
}
|
||||
|
||||
#[action]
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
pub struct SelectPrevious {
|
||||
#[serde(default)]
|
||||
pub replace_newest: bool,
|
||||
}
|
||||
|
||||
#[action]
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
pub struct SelectAllMatches {
|
||||
#[serde(default)]
|
||||
pub replace_newest: bool,
|
||||
}
|
||||
|
||||
#[action]
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
pub struct SelectToBeginningOfLine {
|
||||
#[serde(default)]
|
||||
stop_at_soft_wraps: bool,
|
||||
}
|
||||
|
||||
#[action]
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
pub struct MovePageUp {
|
||||
#[serde(default)]
|
||||
center_cursor: bool,
|
||||
}
|
||||
|
||||
#[action]
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
pub struct MovePageDown {
|
||||
#[serde(default)]
|
||||
center_cursor: bool,
|
||||
}
|
||||
|
||||
#[action]
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
pub struct SelectToEndOfLine {
|
||||
#[serde(default)]
|
||||
stop_at_soft_wraps: bool,
|
||||
}
|
||||
|
||||
#[action]
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
pub struct ToggleCodeActions {
|
||||
#[serde(default)]
|
||||
pub deployed_from_indicator: bool,
|
||||
}
|
||||
|
||||
#[action]
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
pub struct ConfirmCompletion {
|
||||
#[serde(default)]
|
||||
pub item_ix: Option<usize>,
|
||||
}
|
||||
|
||||
#[action]
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
pub struct ConfirmCodeAction {
|
||||
#[serde(default)]
|
||||
pub item_ix: Option<usize>,
|
||||
}
|
||||
|
||||
#[action]
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
pub struct ToggleComments {
|
||||
#[serde(default)]
|
||||
pub advance_downwards: bool,
|
||||
}
|
||||
|
||||
#[action]
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
pub struct FoldAt {
|
||||
pub buffer_row: u32,
|
||||
}
|
||||
|
||||
#[action]
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
pub struct UnfoldAt {
|
||||
pub buffer_row: u32,
|
||||
}
|
||||
@@ -4372,69 +4372,42 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn render_fold_indicators(
|
||||
// &self,
|
||||
// fold_data: Vec<Option<(FoldStatus, u32, bool)>>,
|
||||
// style: &EditorStyle,
|
||||
// gutter_hovered: bool,
|
||||
// line_height: f32,
|
||||
// gutter_margin: f32,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> Vec<Option<AnyElement<Self>>> {
|
||||
// enum FoldIndicators {}
|
||||
|
||||
// let style = style.folds.clone();
|
||||
|
||||
// fold_data
|
||||
// .iter()
|
||||
// .enumerate()
|
||||
// .map(|(ix, fold_data)| {
|
||||
// fold_data
|
||||
// .map(|(fold_status, buffer_row, active)| {
|
||||
// (active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| {
|
||||
// MouseEventHandler::new::<FoldIndicators, _>(
|
||||
// ix as usize,
|
||||
// cx,
|
||||
// |mouse_state, _| {
|
||||
// Svg::new(match fold_status {
|
||||
// FoldStatus::Folded => style.folded_icon.clone(),
|
||||
// FoldStatus::Foldable => style.foldable_icon.clone(),
|
||||
// })
|
||||
// .with_color(
|
||||
// style
|
||||
// .indicator
|
||||
// .in_state(fold_status == FoldStatus::Folded)
|
||||
// .style_for(mouse_state)
|
||||
// .color,
|
||||
// )
|
||||
// .constrained()
|
||||
// .with_width(gutter_margin * style.icon_margin_scale)
|
||||
// .aligned()
|
||||
// .constrained()
|
||||
// .with_height(line_height)
|
||||
// .with_width(gutter_margin)
|
||||
// .aligned()
|
||||
// },
|
||||
// )
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .with_padding(Padding::uniform(3.))
|
||||
// .on_click(MouseButton::Left, {
|
||||
// move |_, editor, cx| match fold_status {
|
||||
// FoldStatus::Folded => {
|
||||
// editor.unfold_at(&UnfoldAt { buffer_row }, cx);
|
||||
// }
|
||||
// FoldStatus::Foldable => {
|
||||
// editor.fold_at(&FoldAt { buffer_row }, cx);
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// .into_any()
|
||||
// })
|
||||
// })
|
||||
// .flatten()
|
||||
// })
|
||||
// .collect()
|
||||
// }
|
||||
pub fn render_fold_indicators(
|
||||
&self,
|
||||
fold_data: Vec<Option<(FoldStatus, u32, bool)>>,
|
||||
style: &EditorStyle,
|
||||
gutter_hovered: bool,
|
||||
line_height: Pixels,
|
||||
gutter_margin: Pixels,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Vec<Option<AnyElement<Self>>> {
|
||||
fold_data
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, fold_data)| {
|
||||
fold_data
|
||||
.map(|(fold_status, buffer_row, active)| {
|
||||
(active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| {
|
||||
let icon = match fold_status {
|
||||
FoldStatus::Folded => ui::Icon::ChevronRight,
|
||||
FoldStatus::Foldable => ui::Icon::ChevronDown,
|
||||
};
|
||||
IconButton::new(ix as usize, icon)
|
||||
.on_click(move |editor: &mut Editor, cx| match fold_status {
|
||||
FoldStatus::Folded => {
|
||||
editor.unfold_at(&UnfoldAt { buffer_row }, cx);
|
||||
}
|
||||
FoldStatus::Foldable => {
|
||||
editor.fold_at(&FoldAt { buffer_row }, cx);
|
||||
}
|
||||
})
|
||||
.render()
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn context_menu_visible(&self) -> bool {
|
||||
self.context_menu
|
||||
@@ -5330,8 +5303,8 @@ impl Editor {
|
||||
buffer.anchor_before(range_to_move.start)
|
||||
..buffer.anchor_after(range_to_move.end),
|
||||
) {
|
||||
let mut start = fold.start.to_point(&buffer);
|
||||
let mut end = fold.end.to_point(&buffer);
|
||||
let mut start = fold.range.start.to_point(&buffer);
|
||||
let mut end = fold.range.end.to_point(&buffer);
|
||||
start.row -= row_delta;
|
||||
end.row -= row_delta;
|
||||
refold_ranges.push(start..end);
|
||||
@@ -5421,8 +5394,8 @@ impl Editor {
|
||||
buffer.anchor_before(range_to_move.start)
|
||||
..buffer.anchor_after(range_to_move.end),
|
||||
) {
|
||||
let mut start = fold.start.to_point(&buffer);
|
||||
let mut end = fold.end.to_point(&buffer);
|
||||
let mut start = fold.range.start.to_point(&buffer);
|
||||
let mut end = fold.range.end.to_point(&buffer);
|
||||
start.row += row_delta;
|
||||
end.row += row_delta;
|
||||
refold_ranges.push(start..end);
|
||||
@@ -5472,7 +5445,9 @@ impl Editor {
|
||||
*head.column_mut() += 1;
|
||||
head = display_map.clip_point(head, Bias::Right);
|
||||
let goal = SelectionGoal::HorizontalPosition(
|
||||
display_map.x_for_point(head, &text_layout_details).into(),
|
||||
display_map
|
||||
.x_for_display_point(head, &text_layout_details)
|
||||
.into(),
|
||||
);
|
||||
selection.collapse_to(head, goal);
|
||||
|
||||
@@ -6418,8 +6393,8 @@ impl Editor {
|
||||
let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone();
|
||||
let range = oldest_selection.display_range(&display_map).sorted();
|
||||
|
||||
let start_x = display_map.x_for_point(range.start, &text_layout_details);
|
||||
let end_x = display_map.x_for_point(range.end, &text_layout_details);
|
||||
let start_x = display_map.x_for_display_point(range.start, &text_layout_details);
|
||||
let end_x = display_map.x_for_display_point(range.end, &text_layout_details);
|
||||
let positions = start_x.min(end_x)..start_x.max(end_x);
|
||||
|
||||
selections.clear();
|
||||
@@ -6458,15 +6433,16 @@ impl Editor {
|
||||
let range = selection.display_range(&display_map).sorted();
|
||||
debug_assert_eq!(range.start.row(), range.end.row());
|
||||
let mut row = range.start.row();
|
||||
let positions = if let SelectionGoal::HorizontalRange { start, end } =
|
||||
selection.goal
|
||||
{
|
||||
px(start)..px(end)
|
||||
} else {
|
||||
let start_x = display_map.x_for_point(range.start, &text_layout_details);
|
||||
let end_x = display_map.x_for_point(range.end, &text_layout_details);
|
||||
start_x.min(end_x)..start_x.max(end_x)
|
||||
};
|
||||
let positions =
|
||||
if let SelectionGoal::HorizontalRange { start, end } = selection.goal {
|
||||
px(start)..px(end)
|
||||
} else {
|
||||
let start_x =
|
||||
display_map.x_for_display_point(range.start, &text_layout_details);
|
||||
let end_x =
|
||||
display_map.x_for_display_point(range.end, &text_layout_details);
|
||||
start_x.min(end_x)..start_x.max(end_x)
|
||||
};
|
||||
|
||||
while row != end_row {
|
||||
if above {
|
||||
@@ -7019,7 +6995,7 @@ impl Editor {
|
||||
let display_point = point.to_display_point(display_snapshot);
|
||||
let goal = SelectionGoal::HorizontalPosition(
|
||||
display_snapshot
|
||||
.x_for_point(display_point, &text_layout_details)
|
||||
.x_for_display_point(display_point, &text_layout_details)
|
||||
.into(),
|
||||
);
|
||||
(display_point, goal)
|
||||
@@ -7804,25 +7780,18 @@ impl Editor {
|
||||
}
|
||||
div()
|
||||
.pl(cx.anchor_x)
|
||||
.child(render_view(
|
||||
.child(rename_editor.render_with(EditorElement::new(
|
||||
&rename_editor,
|
||||
EditorElement::new(
|
||||
&rename_editor,
|
||||
EditorStyle {
|
||||
background: cx.theme().system().transparent,
|
||||
local_player: cx.editor_style.local_player,
|
||||
text: text_style,
|
||||
scrollbar_width: cx
|
||||
.editor_style
|
||||
.scrollbar_width,
|
||||
syntax: cx.editor_style.syntax.clone(),
|
||||
diagnostic_style: cx
|
||||
.editor_style
|
||||
.diagnostic_style
|
||||
.clone(),
|
||||
},
|
||||
),
|
||||
))
|
||||
EditorStyle {
|
||||
background: cx.theme().system().transparent,
|
||||
local_player: cx.editor_style.local_player,
|
||||
text: text_style,
|
||||
scrollbar_width: cx.editor_style.scrollbar_width,
|
||||
syntax: cx.editor_style.syntax.clone(),
|
||||
diagnostic_style:
|
||||
cx.editor_style.diagnostic_style.clone(),
|
||||
},
|
||||
)))
|
||||
.render()
|
||||
}
|
||||
}),
|
||||
@@ -9401,24 +9370,28 @@ pub struct EditorReleased(pub WeakView<Editor>);
|
||||
//
|
||||
impl EventEmitter<Event> for Editor {}
|
||||
|
||||
impl FocusableView for Editor {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Editor {
|
||||
type Element = EditorElement;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = match self.mode {
|
||||
EditorMode::SingleLine => {
|
||||
TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
font_family: settings.ui_font.family.clone(), // todo!()
|
||||
font_features: settings.ui_font.features,
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: relative(1.3).into(), // TODO relative(settings.buffer_line_height.value()),
|
||||
underline: None,
|
||||
}
|
||||
}
|
||||
EditorMode::SingleLine => TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features,
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: relative(1.).into(),
|
||||
underline: None,
|
||||
},
|
||||
|
||||
EditorMode::AutoHeight { max_lines } => todo!(),
|
||||
|
||||
@@ -9789,7 +9762,8 @@ impl InputHandler for Editor {
|
||||
let scroll_left = scroll_position.x * em_width;
|
||||
|
||||
let start = OffsetUtf16(range_utf16.start).to_display_point(&snapshot);
|
||||
let x = snapshot.x_for_point(start, &text_layout_details) - scroll_left + self.gutter_width;
|
||||
let x = snapshot.x_for_display_point(start, &text_layout_details) - scroll_left
|
||||
+ self.gutter_width;
|
||||
let y = line_height * (start.row() as f32 - scroll_position.y);
|
||||
|
||||
Some(Bounds {
|
||||
@@ -10019,7 +9993,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new(message.clone()));
|
||||
})
|
||||
.tooltip(|_, cx| cx.build_view(|cx| TextTooltip::new("Copy diagnostic message")))
|
||||
.tooltip(|_, cx| Tooltip::text("Copy diagnostic message", cx))
|
||||
.render()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3851,12 +3851,12 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
|
||||
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
||||
});
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
|
||||
view.condition::<crate::Event>(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
||||
.await;
|
||||
|
||||
view.update(&mut cx, |view, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
view.change_selections(None, cx, |s| {
|
||||
s.select_display_ranges([
|
||||
DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
|
||||
@@ -3867,7 +3867,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
|
||||
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
view.update(&mut cx, |view, cx| { view.selections.display_ranges(cx) }),
|
||||
view.update(cx, |view, cx| { view.selections.display_ranges(cx) }),
|
||||
&[
|
||||
DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
|
||||
DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
|
||||
@@ -3875,50 +3875,50 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
|
||||
]
|
||||
);
|
||||
|
||||
view.update(&mut cx, |view, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
&[
|
||||
DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
|
||||
DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
|
||||
]
|
||||
);
|
||||
|
||||
view.update(&mut cx, |view, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
&[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
|
||||
);
|
||||
|
||||
// Trying to expand the selected syntax node one more time has no effect.
|
||||
view.update(&mut cx, |view, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
&[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
|
||||
);
|
||||
|
||||
view.update(&mut cx, |view, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
&[
|
||||
DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
|
||||
DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
|
||||
]
|
||||
);
|
||||
|
||||
view.update(&mut cx, |view, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
&[
|
||||
DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
|
||||
DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
|
||||
@@ -3926,11 +3926,11 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
|
||||
]
|
||||
);
|
||||
|
||||
view.update(&mut cx, |view, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
&[
|
||||
DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
|
||||
DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
|
||||
@@ -3939,11 +3939,11 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
|
||||
);
|
||||
|
||||
// Trying to shrink the selected syntax node one more time has no effect.
|
||||
view.update(&mut cx, |view, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
&[
|
||||
DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
|
||||
DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
|
||||
@@ -3953,7 +3953,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
// Ensure that we keep expanding the selection if the larger selection starts or ends within
|
||||
// a fold.
|
||||
view.update(&mut cx, |view, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
view.fold_ranges(
|
||||
vec![
|
||||
Point::new(0, 21)..Point::new(0, 24),
|
||||
@@ -3965,7 +3965,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
|
||||
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
&[
|
||||
DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
|
||||
DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
|
||||
@@ -4017,8 +4017,7 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
|
||||
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
||||
});
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
let cx = &mut cx;
|
||||
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
editor
|
||||
.condition::<crate::Event>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
|
||||
.await;
|
||||
@@ -4583,8 +4582,7 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
|
||||
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
||||
});
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
let cx = &mut cx;
|
||||
let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
view.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
||||
.await;
|
||||
|
||||
@@ -4734,8 +4732,7 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
|
||||
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
||||
});
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
let cx = &mut cx;
|
||||
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
editor
|
||||
.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
||||
.await;
|
||||
@@ -4957,8 +4954,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
let cx = &mut cx;
|
||||
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
||||
assert!(cx.read(|cx| editor.is_dirty(cx)));
|
||||
|
||||
@@ -5077,8 +5073,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
let cx = &mut cx;
|
||||
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
||||
assert!(cx.read(|cx| editor.is_dirty(cx)));
|
||||
|
||||
@@ -5205,8 +5200,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
let cx = &mut cx;
|
||||
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
||||
|
||||
let format = editor
|
||||
@@ -5993,8 +5987,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
|
||||
multibuffer
|
||||
});
|
||||
|
||||
let (view, mut cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
|
||||
let cx = &mut cx;
|
||||
let (view, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
|
||||
view.update(cx, |view, cx| {
|
||||
assert_eq!(view.text(cx), "aaaa\nbbbb");
|
||||
view.change_selections(None, cx, |s| {
|
||||
@@ -6064,8 +6057,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
|
||||
multibuffer
|
||||
});
|
||||
|
||||
let (view, mut cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
|
||||
let cx = &mut cx;
|
||||
let (view, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
|
||||
view.update(cx, |view, cx| {
|
||||
let (expected_text, selection_ranges) = marked_text_ranges(
|
||||
indoc! {"
|
||||
@@ -6302,8 +6294,7 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
|
||||
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
||||
});
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
let cx = &mut cx;
|
||||
let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
view.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
||||
.await;
|
||||
|
||||
@@ -8112,8 +8103,7 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
let buffer_text = "one\ntwo\nthree\n";
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
let cx = &mut cx;
|
||||
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx));
|
||||
|
||||
editor
|
||||
@@ -8287,7 +8277,7 @@ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsC
|
||||
cx.update(|cx| {
|
||||
let store = SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
theme::init(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
client::init_settings(cx);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -60,8 +60,8 @@ pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) ->
|
||||
let folds_end = Point::new(hunk.buffer_range.end + 2, 0);
|
||||
let folds_range = folds_start..folds_end;
|
||||
|
||||
let containing_fold = snapshot.folds_in_range(folds_range).find(|fold_range| {
|
||||
let fold_point_range = fold_range.to_point(&snapshot.buffer_snapshot);
|
||||
let containing_fold = snapshot.folds_in_range(folds_range).find(|fold| {
|
||||
let fold_point_range = fold.range.to_point(&snapshot.buffer_snapshot);
|
||||
let fold_point_range = fold_point_range.start..=fold_point_range.end;
|
||||
|
||||
let folded_start = fold_point_range.contains(&hunk_start_point);
|
||||
@@ -72,7 +72,7 @@ pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) ->
|
||||
});
|
||||
|
||||
if let Some(fold) = containing_fold {
|
||||
let row = fold.start.to_display_point(snapshot).row();
|
||||
let row = fold.range.start.to_display_point(snapshot).row();
|
||||
DisplayDiffHunk::Folded { display_row: row }
|
||||
} else {
|
||||
let start = hunk_start_point.to_display_point(snapshot).row();
|
||||
|
||||
@@ -3179,7 +3179,7 @@ all hints should be invalidated and requeried for all of its visible excerpts"
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
theme::init(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
client::init_settings(cx);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
|
||||
@@ -527,10 +527,6 @@ fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor)
|
||||
}
|
||||
|
||||
impl Item for Editor {
|
||||
fn focus_handle(&self) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
|
||||
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||
todo!();
|
||||
// if let Ok(data) = data.downcast::<NavigationData>() {
|
||||
@@ -801,7 +797,7 @@ impl Item for Editor {
|
||||
|
||||
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
|
||||
let workspace_id = workspace.database_id();
|
||||
let item_id = cx.view().entity_id().as_u64() as ItemId;
|
||||
let item_id = cx.view().item_id().as_u64() as ItemId;
|
||||
self.workspace = Some((workspace.weak_handle(), workspace.database_id()));
|
||||
|
||||
fn serialize(
|
||||
@@ -832,7 +828,7 @@ impl Item for Editor {
|
||||
serialize(
|
||||
buffer,
|
||||
*workspace_id,
|
||||
cx.view().entity_id().as_u64() as ItemId,
|
||||
cx.view().item_id().as_u64() as ItemId,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ pub fn up_by_rows(
|
||||
SelectionGoal::HorizontalPosition(x) => x.into(), // todo!("Can the fields in SelectionGoal by Pixels? We should extract a geometry crate and depend on that.")
|
||||
SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
|
||||
SelectionGoal::HorizontalRange { end, .. } => end.into(),
|
||||
_ => map.x_for_point(start, text_layout_details),
|
||||
_ => map.x_for_display_point(start, text_layout_details),
|
||||
};
|
||||
|
||||
let prev_row = start.row().saturating_sub(row_count);
|
||||
@@ -107,7 +107,7 @@ pub fn up_by_rows(
|
||||
Bias::Left,
|
||||
);
|
||||
if point.row() < start.row() {
|
||||
*point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
|
||||
*point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details)
|
||||
} else if preserve_column_at_start {
|
||||
return (start, goal);
|
||||
} else {
|
||||
@@ -137,18 +137,18 @@ pub fn down_by_rows(
|
||||
SelectionGoal::HorizontalPosition(x) => x.into(),
|
||||
SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
|
||||
SelectionGoal::HorizontalRange { end, .. } => end.into(),
|
||||
_ => map.x_for_point(start, text_layout_details),
|
||||
_ => map.x_for_display_point(start, text_layout_details),
|
||||
};
|
||||
|
||||
let new_row = start.row() + row_count;
|
||||
let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
|
||||
if point.row() > start.row() {
|
||||
*point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
|
||||
*point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details)
|
||||
} else if preserve_column_at_end {
|
||||
return (start, goal);
|
||||
} else {
|
||||
point = map.max_point();
|
||||
goal_x = map.x_for_point(point, text_layout_details)
|
||||
goal_x = map.x_for_display_point(point, text_layout_details)
|
||||
}
|
||||
|
||||
let mut clipped_point = map.clip_point(point, Bias::Right);
|
||||
|
||||
@@ -426,7 +426,7 @@ impl Editor {
|
||||
|
||||
pub fn read_scroll_position_from_db(
|
||||
&mut self,
|
||||
item_id: usize,
|
||||
item_id: u64,
|
||||
workspace_id: WorkspaceId,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
|
||||
@@ -313,14 +313,14 @@ impl SelectionsCollection {
|
||||
let is_empty = positions.start == positions.end;
|
||||
let line_len = display_map.line_len(row);
|
||||
|
||||
let layed_out_line = display_map.lay_out_line_for_row(row, &text_layout_details);
|
||||
let line = display_map.layout_row(row, &text_layout_details);
|
||||
|
||||
dbg!("****START COL****");
|
||||
let start_col = layed_out_line.closest_index_for_x(positions.start) as u32;
|
||||
if start_col < line_len || (is_empty && positions.start == layed_out_line.width) {
|
||||
let start_col = line.closest_index_for_x(positions.start) as u32;
|
||||
if start_col < line_len || (is_empty && positions.start == line.width) {
|
||||
let start = DisplayPoint::new(row, start_col);
|
||||
dbg!("****END COL****");
|
||||
let end_col = layed_out_line.closest_index_for_x(positions.end) as u32;
|
||||
let end_col = line.closest_index_for_x(positions.end) as u32;
|
||||
let end = DisplayPoint::new(row, end_col);
|
||||
dbg!(start_col, end_col);
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,13 @@
|
||||
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
|
||||
use gpui::{
|
||||
actions, div, prelude::*, AppContext, Div, EventEmitter, ParentComponent, Render, SharedString,
|
||||
Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
|
||||
actions, div, prelude::*, AppContext, Dismiss, Div, FocusHandle, ManagedView, ParentComponent,
|
||||
Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
use text::{Bias, Point};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{h_stack, v_stack, Label, StyledExt, TextColor};
|
||||
use util::paths::FILE_ROW_COLUMN_DELIMITER;
|
||||
use workspace::{Modal, ModalEvent, Workspace};
|
||||
use workspace::Workspace;
|
||||
|
||||
actions!(Toggle);
|
||||
|
||||
@@ -23,10 +23,9 @@ pub struct GoToLine {
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl EventEmitter<ModalEvent> for GoToLine {}
|
||||
impl Modal for GoToLine {
|
||||
fn focus(&self, cx: &mut WindowContext) {
|
||||
self.line_editor.update(cx, |editor, cx| editor.focus(cx))
|
||||
impl ManagedView for GoToLine {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.line_editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +87,7 @@ impl GoToLine {
|
||||
) {
|
||||
match event {
|
||||
// todo!() this isn't working...
|
||||
editor::Event::Blurred => cx.emit(ModalEvent::Dismissed),
|
||||
editor::Event::Blurred => cx.emit(Dismiss),
|
||||
editor::Event::BufferEdited { .. } => self.highlight_current_line(cx),
|
||||
_ => {}
|
||||
}
|
||||
@@ -123,7 +122,7 @@ impl GoToLine {
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(ModalEvent::Dismissed);
|
||||
cx.emit(Dismiss);
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||
@@ -140,7 +139,7 @@ impl GoToLine {
|
||||
self.prev_scroll_position.take();
|
||||
}
|
||||
|
||||
cx.emit(ModalEvent::Dismissed);
|
||||
cx.emit(Dismiss);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ sqlez = { path = "../sqlez" }
|
||||
async-task = "4.0.3"
|
||||
backtrace = { version = "0.3", optional = true }
|
||||
ctor.workspace = true
|
||||
linkme = "0.3"
|
||||
derive_more.workspace = true
|
||||
dhat = { version = "0.3", optional = true }
|
||||
env_logger = { version = "0.9", optional = true }
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use crate::SharedString;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use collections::HashMap;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard};
|
||||
use serde::Deserialize;
|
||||
use std::any::{type_name, Any, TypeId};
|
||||
pub use no_action::NoAction;
|
||||
use serde_json::json;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
/// Actions are used to implement keyboard-driven UI.
|
||||
/// When you declare an action, you can bind keys to the action in the keymap and
|
||||
@@ -15,24 +17,16 @@ use std::any::{type_name, Any, TypeId};
|
||||
/// ```rust
|
||||
/// actions!(MoveUp, MoveDown, MoveLeft, MoveRight, Newline);
|
||||
/// ```
|
||||
/// More complex data types can also be actions. If you annotate your type with the `#[action]` proc macro,
|
||||
/// it will automatically
|
||||
/// More complex data types can also be actions. If you annotate your type with the action derive macro
|
||||
/// it will be implemented and registered automatically.
|
||||
/// ```
|
||||
/// #[action]
|
||||
/// #[derive(Clone, PartialEq, serde_derive::Deserialize, Action)]
|
||||
/// pub struct SelectNext {
|
||||
/// pub replace_newest: bool,
|
||||
/// }
|
||||
///
|
||||
/// Any type A that satisfies the following bounds is automatically an action:
|
||||
///
|
||||
/// ```
|
||||
/// A: for<'a> Deserialize<'a> + PartialEq + Clone + Default + std::fmt::Debug + 'static,
|
||||
/// ```
|
||||
///
|
||||
/// The `#[action]` annotation will derive these implementations for your struct automatically. If you
|
||||
/// want to control them manually, you can use the lower-level `#[register_action]` macro, which only
|
||||
/// generates the code needed to register your action before `main`. Then you'll need to implement all
|
||||
/// the traits manually.
|
||||
/// If you want to control the behavior of the action trait manually, you can use the lower-level `#[register_action]`
|
||||
/// macro, which only generates the code needed to register your action before `main`.
|
||||
///
|
||||
/// ```
|
||||
/// #[gpui::register_action]
|
||||
@@ -41,77 +35,29 @@ use std::any::{type_name, Any, TypeId};
|
||||
/// pub content: SharedString,
|
||||
/// }
|
||||
///
|
||||
/// impl std::default::Default for Paste {
|
||||
/// fn default() -> Self {
|
||||
/// Self {
|
||||
/// content: SharedString::from("🍝"),
|
||||
/// }
|
||||
/// }
|
||||
/// impl gpui::Action for Paste {
|
||||
/// ///...
|
||||
/// }
|
||||
/// ```
|
||||
pub trait Action: std::fmt::Debug + 'static {
|
||||
fn qualified_name() -> SharedString
|
||||
where
|
||||
Self: Sized;
|
||||
fn build(value: Option<serde_json::Value>) -> Result<Box<dyn Action>>
|
||||
where
|
||||
Self: Sized;
|
||||
fn is_registered() -> bool
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
fn partial_eq(&self, action: &dyn Action) -> bool;
|
||||
pub trait Action: 'static {
|
||||
fn boxed_clone(&self) -> Box<dyn Action>;
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn partial_eq(&self, action: &dyn Action) -> bool;
|
||||
fn name(&self) -> &str;
|
||||
|
||||
fn debug_name() -> &'static str
|
||||
where
|
||||
Self: Sized;
|
||||
fn build(value: serde_json::Value) -> Result<Box<dyn Action>>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
// Types become actions by satisfying a list of trait bounds.
|
||||
impl<A> Action for A
|
||||
where
|
||||
A: for<'a> Deserialize<'a> + PartialEq + Clone + Default + std::fmt::Debug + 'static,
|
||||
{
|
||||
fn qualified_name() -> SharedString {
|
||||
let name = type_name::<A>();
|
||||
let mut separator_matches = name.rmatch_indices("::");
|
||||
separator_matches.next().unwrap();
|
||||
let name_start_ix = separator_matches.next().map_or(0, |(ix, _)| ix + 2);
|
||||
// todo!() remove the 2 replacement when migration is done
|
||||
name[name_start_ix..].replace("2::", "::").into()
|
||||
}
|
||||
|
||||
fn build(params: Option<serde_json::Value>) -> Result<Box<dyn Action>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let action = if let Some(params) = params {
|
||||
serde_json::from_value(params).context("failed to deserialize action")?
|
||||
} else {
|
||||
Self::default()
|
||||
};
|
||||
Ok(Box::new(action))
|
||||
}
|
||||
|
||||
fn is_registered() -> bool {
|
||||
ACTION_REGISTRY
|
||||
.read()
|
||||
.names_by_type_id
|
||||
.get(&TypeId::of::<A>())
|
||||
.is_some()
|
||||
}
|
||||
|
||||
fn partial_eq(&self, action: &dyn Action) -> bool {
|
||||
action
|
||||
.as_any()
|
||||
.downcast_ref::<Self>()
|
||||
.map_or(false, |a| self == a)
|
||||
}
|
||||
|
||||
fn boxed_clone(&self) -> Box<dyn Action> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
impl std::fmt::Debug for dyn Action {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("dyn Action")
|
||||
.field("type_name", &self.name())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,69 +65,93 @@ impl dyn Action {
|
||||
pub fn type_id(&self) -> TypeId {
|
||||
self.as_any().type_id()
|
||||
}
|
||||
|
||||
pub fn name(&self) -> SharedString {
|
||||
ACTION_REGISTRY
|
||||
.read()
|
||||
.names_by_type_id
|
||||
.get(&self.type_id())
|
||||
.expect("type is not a registered action")
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
||||
type ActionBuilder = fn(json: Option<serde_json::Value>) -> anyhow::Result<Box<dyn Action>>;
|
||||
type ActionBuilder = fn(json: serde_json::Value) -> anyhow::Result<Box<dyn Action>>;
|
||||
|
||||
lazy_static! {
|
||||
static ref ACTION_REGISTRY: RwLock<ActionRegistry> = RwLock::default();
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ActionRegistry {
|
||||
pub(crate) struct ActionRegistry {
|
||||
builders_by_name: HashMap<SharedString, ActionBuilder>,
|
||||
names_by_type_id: HashMap<TypeId, SharedString>,
|
||||
all_names: Vec<SharedString>, // So we can return a static slice.
|
||||
}
|
||||
|
||||
/// Register an action type to allow it to be referenced in keymaps.
|
||||
pub fn register_action<A: Action>() {
|
||||
let name = A::qualified_name();
|
||||
let mut lock = ACTION_REGISTRY.write();
|
||||
lock.builders_by_name.insert(name.clone(), A::build);
|
||||
lock.names_by_type_id
|
||||
.insert(TypeId::of::<A>(), name.clone());
|
||||
lock.all_names.push(name);
|
||||
impl Default for ActionRegistry {
|
||||
fn default() -> Self {
|
||||
let mut this = ActionRegistry {
|
||||
builders_by_name: Default::default(),
|
||||
names_by_type_id: Default::default(),
|
||||
all_names: Default::default(),
|
||||
};
|
||||
|
||||
this.load_actions();
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
|
||||
pub fn build_action_from_type(type_id: &TypeId) -> Result<Box<dyn Action>> {
|
||||
let lock = ACTION_REGISTRY.read();
|
||||
let name = lock
|
||||
.names_by_type_id
|
||||
.get(type_id)
|
||||
.ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))?
|
||||
.clone();
|
||||
drop(lock);
|
||||
/// This type must be public so that our macros can build it in other crates.
|
||||
/// But this is an implementation detail and should not be used directly.
|
||||
#[doc(hidden)]
|
||||
pub type MacroActionBuilder = fn() -> ActionData;
|
||||
|
||||
build_action(&name, None)
|
||||
/// This type must be public so that our macros can build it in other crates.
|
||||
/// But this is an implementation detail and should not be used directly.
|
||||
#[doc(hidden)]
|
||||
pub struct ActionData {
|
||||
pub name: &'static str,
|
||||
pub type_id: TypeId,
|
||||
pub build: ActionBuilder,
|
||||
}
|
||||
|
||||
/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
|
||||
pub fn build_action(name: &str, params: Option<serde_json::Value>) -> Result<Box<dyn Action>> {
|
||||
let lock = ACTION_REGISTRY.read();
|
||||
/// This constant must be public to be accessible from other crates.
|
||||
/// But it's existence is an implementation detail and should not be used directly.
|
||||
#[doc(hidden)]
|
||||
#[linkme::distributed_slice]
|
||||
pub static __GPUI_ACTIONS: [MacroActionBuilder];
|
||||
|
||||
let build_action = lock
|
||||
.builders_by_name
|
||||
.get(name)
|
||||
.ok_or_else(|| anyhow!("no action type registered for {}", name))?;
|
||||
(build_action)(params)
|
||||
}
|
||||
impl ActionRegistry {
|
||||
/// Load all registered actions into the registry.
|
||||
pub(crate) fn load_actions(&mut self) {
|
||||
for builder in __GPUI_ACTIONS {
|
||||
let action = builder();
|
||||
//todo(remove)
|
||||
let name: SharedString = remove_the_2(action.name).into();
|
||||
self.builders_by_name.insert(name.clone(), action.build);
|
||||
self.names_by_type_id.insert(action.type_id, name.clone());
|
||||
self.all_names.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_action_names() -> MappedRwLockReadGuard<'static, [SharedString]> {
|
||||
let lock = ACTION_REGISTRY.read();
|
||||
RwLockReadGuard::map(lock, |registry: &ActionRegistry| {
|
||||
registry.all_names.as_slice()
|
||||
})
|
||||
/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
|
||||
pub fn build_action_type(&self, type_id: &TypeId) -> Result<Box<dyn Action>> {
|
||||
let name = self
|
||||
.names_by_type_id
|
||||
.get(type_id)
|
||||
.ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))?
|
||||
.clone();
|
||||
|
||||
self.build_action(&name, None)
|
||||
}
|
||||
|
||||
/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
|
||||
pub fn build_action(
|
||||
&self,
|
||||
name: &str,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<Box<dyn Action>> {
|
||||
//todo(remove)
|
||||
let name = remove_the_2(name);
|
||||
let build_action = self
|
||||
.builders_by_name
|
||||
.get(name.deref())
|
||||
.ok_or_else(|| anyhow!("no action type registered for {}", name))?;
|
||||
(build_action)(params.unwrap_or_else(|| json!({})))
|
||||
.with_context(|| format!("Attempting to build action {}", name))
|
||||
}
|
||||
|
||||
pub fn all_action_names(&self) -> &[SharedString] {
|
||||
self.all_names.as_slice()
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines unit structs that can be used as actions.
|
||||
@@ -191,7 +161,7 @@ macro_rules! actions {
|
||||
() => {};
|
||||
|
||||
( $name:ident ) => {
|
||||
#[gpui::action]
|
||||
#[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize, gpui::Action)]
|
||||
pub struct $name;
|
||||
};
|
||||
|
||||
@@ -200,3 +170,20 @@ macro_rules! actions {
|
||||
actions!($($rest)*);
|
||||
};
|
||||
}
|
||||
|
||||
//todo!(remove)
|
||||
pub fn remove_the_2(action_name: &str) -> String {
|
||||
let mut separator_matches = action_name.rmatch_indices("::");
|
||||
separator_matches.next().unwrap();
|
||||
let name_start_ix = separator_matches.next().map_or(0, |(ix, _)| ix + 2);
|
||||
// todo!() remove the 2 replacement when migration is done
|
||||
action_name[name_start_ix..]
|
||||
.replace("2::", "::")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
mod no_action {
|
||||
use crate as gpui;
|
||||
|
||||
actions!(NoAction);
|
||||
}
|
||||
|
||||
@@ -14,12 +14,13 @@ use smallvec::SmallVec;
|
||||
pub use test_context::*;
|
||||
|
||||
use crate::{
|
||||
current_platform, image_cache::ImageCache, Action, AnyBox, AnyView, AnyWindowHandle,
|
||||
AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context, DispatchPhase, DisplayId,
|
||||
Entity, EventEmitter, FocusEvent, FocusHandle, FocusId, ForegroundExecutor, KeyBinding, Keymap,
|
||||
LayoutId, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point, Render, SubscriberSet,
|
||||
Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View, ViewContext,
|
||||
Window, WindowContext, WindowHandle, WindowId,
|
||||
current_platform, image_cache::ImageCache, Action, ActionRegistry, AnyBox, AnyView,
|
||||
AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
|
||||
DispatchPhase, DisplayId, Entity, EventEmitter, FocusEvent, FocusHandle, FocusId,
|
||||
ForegroundExecutor, KeyBinding, Keymap, LayoutId, PathPromptOptions, Pixels, Platform,
|
||||
PlatformDisplay, Point, Render, SharedString, SubscriberSet, Subscription, SvgRenderer, Task,
|
||||
TextStyle, TextStyleRefinement, TextSystem, View, ViewContext, Window, WindowContext,
|
||||
WindowHandle, WindowId,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::{HashMap, HashSet, VecDeque};
|
||||
@@ -182,6 +183,7 @@ pub struct AppContext {
|
||||
text_system: Arc<TextSystem>,
|
||||
flushing_effects: bool,
|
||||
pending_updates: usize,
|
||||
pub(crate) actions: Rc<ActionRegistry>,
|
||||
pub(crate) active_drag: Option<AnyDrag>,
|
||||
pub(crate) active_tooltip: Option<AnyTooltip>,
|
||||
pub(crate) next_frame_callbacks: HashMap<DisplayId, Vec<FrameCallback>>,
|
||||
@@ -240,6 +242,7 @@ impl AppContext {
|
||||
platform: platform.clone(),
|
||||
app_metadata,
|
||||
text_system,
|
||||
actions: Rc::new(ActionRegistry::default()),
|
||||
flushing_effects: false,
|
||||
pending_updates: 0,
|
||||
active_drag: None,
|
||||
@@ -964,6 +967,18 @@ impl AppContext {
|
||||
pub fn propagate(&mut self) {
|
||||
self.propagate_event = true;
|
||||
}
|
||||
|
||||
pub fn build_action(
|
||||
&self,
|
||||
name: &str,
|
||||
data: Option<serde_json::Value>,
|
||||
) -> Result<Box<dyn Action>> {
|
||||
self.actions.build_action(name, data)
|
||||
}
|
||||
|
||||
pub fn all_action_names(&self) -> &[SharedString] {
|
||||
self.actions.all_action_names()
|
||||
}
|
||||
}
|
||||
|
||||
impl Context for AppContext {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, ForegroundExecutor,
|
||||
Model, ModelContext, Render, Result, Task, View, ViewContext, VisualContext, WindowContext,
|
||||
WindowHandle,
|
||||
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, FocusableView,
|
||||
ForegroundExecutor, Model, ModelContext, Render, Result, Task, View, ViewContext,
|
||||
VisualContext, WindowContext, WindowHandle,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
@@ -182,6 +182,10 @@ pub struct AsyncWindowContext {
|
||||
}
|
||||
|
||||
impl AsyncWindowContext {
|
||||
pub fn window_handle(&self) -> AnyWindowHandle {
|
||||
self.window
|
||||
}
|
||||
|
||||
pub(crate) fn new(app: AsyncAppContext, window: AnyWindowHandle) -> Self {
|
||||
Self { app, window }
|
||||
}
|
||||
@@ -307,4 +311,13 @@ impl VisualContext for AsyncWindowContext {
|
||||
self.window
|
||||
.update(self, |_, cx| cx.replace_root_view(build_view))
|
||||
}
|
||||
|
||||
fn focus_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
|
||||
where
|
||||
V: FocusableView,
|
||||
{
|
||||
self.window.update(self, |_, cx| {
|
||||
view.read(cx).focus_handle(cx).clone().focus(cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ impl EntityMap {
|
||||
}
|
||||
|
||||
/// Move an entity to the stack.
|
||||
#[track_caller]
|
||||
pub fn lease<'a, T>(&mut self, model: &'a Model<T>) -> Lease<'a, T> {
|
||||
self.assert_valid_context(model);
|
||||
let entity = Some(
|
||||
|
||||
@@ -140,7 +140,7 @@ impl TestAppContext {
|
||||
.any_handle
|
||||
}
|
||||
|
||||
pub fn add_window_view<F, V>(&mut self, build_window: F) -> (View<V>, VisualTestContext)
|
||||
pub fn add_window_view<F, V>(&mut self, build_window: F) -> (View<V>, &mut VisualTestContext)
|
||||
where
|
||||
F: FnOnce(&mut ViewContext<V>) -> V,
|
||||
V: Render,
|
||||
@@ -149,7 +149,9 @@ impl TestAppContext {
|
||||
let window = cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window));
|
||||
drop(cx);
|
||||
let view = window.root_view(self).unwrap();
|
||||
(view, VisualTestContext::from_window(*window.deref(), self))
|
||||
let cx = Box::new(VisualTestContext::from_window(*window.deref(), self));
|
||||
// it might be nice to try and cleanup these at the end of each test.
|
||||
(view, Box::leak(cx))
|
||||
}
|
||||
|
||||
pub fn simulate_new_path_selection(
|
||||
@@ -225,6 +227,9 @@ impl TestAppContext {
|
||||
self.background_executor.run_until_parked()
|
||||
}
|
||||
|
||||
/// simulate_keystrokes takes a space-separated list of keys to type.
|
||||
/// cx.simulate_keystrokes("cmd-shift-p b k s p enter")
|
||||
/// will run backspace on the current editor through the command palette.
|
||||
pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) {
|
||||
for keystroke in keystrokes
|
||||
.split(" ")
|
||||
@@ -237,6 +242,17 @@ impl TestAppContext {
|
||||
self.background_executor.run_until_parked()
|
||||
}
|
||||
|
||||
/// simulate_input takes a string of text to type.
|
||||
/// cx.simulate_input("abc")
|
||||
/// will type abc into your current editor.
|
||||
pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) {
|
||||
for keystroke in input.split("").map(Keystroke::parse).map(Result::unwrap) {
|
||||
self.dispatch_keystroke(window, keystroke.into(), false);
|
||||
}
|
||||
|
||||
self.background_executor.run_until_parked()
|
||||
}
|
||||
|
||||
pub fn dispatch_keystroke(
|
||||
&mut self,
|
||||
window: AnyWindowHandle,
|
||||
@@ -354,10 +370,19 @@ impl<T: Send> Model<T> {
|
||||
})
|
||||
});
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
rx.try_next()
|
||||
.expect("no event received")
|
||||
.expect("model was dropped")
|
||||
// Run other tasks until the event is emitted.
|
||||
loop {
|
||||
match rx.try_next() {
|
||||
Ok(Some(event)) => return event,
|
||||
Ok(None) => panic!("model was dropped"),
|
||||
Err(_) => {
|
||||
if !cx.executor().tick() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
panic!("no event received")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,6 +480,10 @@ impl<'a> VisualTestContext<'a> {
|
||||
pub fn simulate_keystrokes(&mut self, keystrokes: &str) {
|
||||
self.cx.simulate_keystrokes(self.window, keystrokes)
|
||||
}
|
||||
|
||||
pub fn simulate_input(&mut self, input: &str) {
|
||||
self.cx.simulate_input(self.window, input)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Context for VisualTestContext<'a> {
|
||||
@@ -542,6 +571,14 @@ impl<'a> VisualContext for VisualTestContext<'a> {
|
||||
.update(self.cx, |_, cx| cx.replace_root_view(build_view))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn focus_view<V: crate::FocusableView>(&mut self, view: &View<V>) -> Self::Result<()> {
|
||||
self.window
|
||||
.update(self.cx, |_, cx| {
|
||||
view.read(cx).focus_handle(cx).clone().focus(cx)
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl AnyWindowHandle {
|
||||
|
||||
@@ -3,28 +3,19 @@ use crate::{
|
||||
};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
pub(crate) use smallvec::SmallVec;
|
||||
use std::{any::Any, mem};
|
||||
use std::{any::Any, fmt::Debug, mem};
|
||||
|
||||
pub trait Element<V: 'static> {
|
||||
type ElementState: 'static;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId>;
|
||||
|
||||
/// Called to initialize this element for the current frame. If this
|
||||
/// element had state in a previous frame, it will be passed in for the 3rd argument.
|
||||
fn initialize(
|
||||
fn layout(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Self::ElementState;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> LayoutId;
|
||||
) -> (LayoutId, Self::ElementState);
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
@@ -33,6 +24,42 @@ pub trait Element<V: 'static> {
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
);
|
||||
|
||||
fn draw<T, R>(
|
||||
self,
|
||||
origin: Point<Pixels>,
|
||||
available_space: Size<T>,
|
||||
view_state: &mut V,
|
||||
cx: &mut ViewContext<V>,
|
||||
f: impl FnOnce(&Self::ElementState, &mut ViewContext<V>) -> R,
|
||||
) -> R
|
||||
where
|
||||
Self: Sized,
|
||||
T: Clone + Default + Debug + Into<AvailableSpace>,
|
||||
{
|
||||
let mut element = RenderedElement {
|
||||
element: self,
|
||||
phase: ElementRenderPhase::Start,
|
||||
};
|
||||
element.draw(origin, available_space.map(Into::into), view_state, cx);
|
||||
if let ElementRenderPhase::Painted { frame_state } = &element.phase {
|
||||
if let Some(frame_state) = frame_state.as_ref() {
|
||||
f(&frame_state, cx)
|
||||
} else {
|
||||
let element_id = element
|
||||
.element
|
||||
.element_id()
|
||||
.expect("we either have some frame_state or some element_id");
|
||||
cx.with_element_state(element_id, |element_state, cx| {
|
||||
let element_state = element_state.unwrap();
|
||||
let result = f(&element_state, cx);
|
||||
(result, element_state)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
@@ -60,7 +87,6 @@ pub trait ParentComponent<V: 'static> {
|
||||
}
|
||||
|
||||
trait ElementObject<V> {
|
||||
fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext<V>);
|
||||
fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId;
|
||||
fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>);
|
||||
fn measure(
|
||||
@@ -87,9 +113,6 @@ struct RenderedElement<V: 'static, E: Element<V>> {
|
||||
enum ElementRenderPhase<V> {
|
||||
#[default]
|
||||
Start,
|
||||
Initialized {
|
||||
frame_state: Option<V>,
|
||||
},
|
||||
LayoutRequested {
|
||||
layout_id: LayoutId,
|
||||
frame_state: Option<V>,
|
||||
@@ -99,7 +122,9 @@ enum ElementRenderPhase<V> {
|
||||
available_space: Size<AvailableSpace>,
|
||||
frame_state: Option<V>,
|
||||
},
|
||||
Painted,
|
||||
Painted {
|
||||
frame_state: Option<V>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Internal struct that wraps an element to store Layout and ElementState after the element is rendered.
|
||||
@@ -119,45 +144,22 @@ where
|
||||
E: Element<V>,
|
||||
E::ElementState: 'static,
|
||||
{
|
||||
fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
|
||||
let frame_state = if let Some(id) = self.element.element_id() {
|
||||
cx.with_element_state(id, |element_state, cx| {
|
||||
let element_state = self.element.initialize(view_state, element_state, cx);
|
||||
((), element_state)
|
||||
});
|
||||
None
|
||||
} else {
|
||||
let frame_state = self.element.initialize(view_state, None, cx);
|
||||
Some(frame_state)
|
||||
};
|
||||
|
||||
self.phase = ElementRenderPhase::Initialized { frame_state };
|
||||
}
|
||||
|
||||
fn layout(&mut self, state: &mut V, cx: &mut ViewContext<V>) -> LayoutId {
|
||||
let layout_id;
|
||||
let mut frame_state;
|
||||
match mem::take(&mut self.phase) {
|
||||
ElementRenderPhase::Initialized {
|
||||
frame_state: initial_frame_state,
|
||||
} => {
|
||||
frame_state = initial_frame_state;
|
||||
let (layout_id, frame_state) = match mem::take(&mut self.phase) {
|
||||
ElementRenderPhase::Start => {
|
||||
if let Some(id) = self.element.element_id() {
|
||||
layout_id = cx.with_element_state(id, |element_state, cx| {
|
||||
let mut element_state = element_state.unwrap();
|
||||
let layout_id = self.element.layout(state, &mut element_state, cx);
|
||||
(layout_id, element_state)
|
||||
let layout_id = cx.with_element_state(id, |element_state, cx| {
|
||||
self.element.layout(state, element_state, cx)
|
||||
});
|
||||
(layout_id, None)
|
||||
} else {
|
||||
layout_id = self
|
||||
.element
|
||||
.layout(state, frame_state.as_mut().unwrap(), cx);
|
||||
let (layout_id, frame_state) = self.element.layout(state, None, cx);
|
||||
(layout_id, Some(frame_state))
|
||||
}
|
||||
}
|
||||
ElementRenderPhase::Start => panic!("must call initialize before layout"),
|
||||
ElementRenderPhase::LayoutRequested { .. }
|
||||
| ElementRenderPhase::LayoutComputed { .. }
|
||||
| ElementRenderPhase::Painted => {
|
||||
| ElementRenderPhase::Painted { .. } => {
|
||||
panic!("element rendered twice")
|
||||
}
|
||||
};
|
||||
@@ -192,7 +194,7 @@ where
|
||||
self.element
|
||||
.paint(bounds, view_state, frame_state.as_mut().unwrap(), cx);
|
||||
}
|
||||
ElementRenderPhase::Painted
|
||||
ElementRenderPhase::Painted { frame_state }
|
||||
}
|
||||
|
||||
_ => panic!("must call layout before paint"),
|
||||
@@ -206,10 +208,6 @@ where
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Size<Pixels> {
|
||||
if matches!(&self.phase, ElementRenderPhase::Start) {
|
||||
self.initialize(view_state, cx);
|
||||
}
|
||||
|
||||
if matches!(&self.phase, ElementRenderPhase::Initialized { .. }) {
|
||||
self.layout(view_state, cx);
|
||||
}
|
||||
|
||||
@@ -246,16 +244,13 @@ where
|
||||
|
||||
fn draw(
|
||||
&mut self,
|
||||
mut origin: Point<Pixels>,
|
||||
origin: Point<Pixels>,
|
||||
available_space: Size<AvailableSpace>,
|
||||
view_state: &mut V,
|
||||
cx: &mut ViewContext<V>,
|
||||
) {
|
||||
self.measure(available_space, view_state, cx);
|
||||
// Ignore the element offset when drawing this element, as the origin is already specified
|
||||
// in absolute terms.
|
||||
origin -= cx.element_offset();
|
||||
cx.with_element_offset(origin, |cx| self.paint(view_state, cx))
|
||||
cx.with_absolute_element_offset(origin, |cx| self.paint(view_state, cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,10 +266,6 @@ impl<V> AnyElement<V> {
|
||||
AnyElement(Box::new(RenderedElement::new(element)))
|
||||
}
|
||||
|
||||
pub fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
|
||||
self.0.initialize(view_state, cx);
|
||||
}
|
||||
|
||||
pub fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId {
|
||||
self.0.layout(view_state, cx)
|
||||
}
|
||||
@@ -355,25 +346,16 @@ where
|
||||
None
|
||||
}
|
||||
|
||||
fn initialize(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
_rendered_element: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Self::ElementState {
|
||||
let render = self.take().unwrap();
|
||||
let mut rendered_element = (render)(view_state, cx).render();
|
||||
rendered_element.initialize(view_state, cx);
|
||||
rendered_element
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
rendered_element: &mut Self::ElementState,
|
||||
_: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> LayoutId {
|
||||
rendered_element.layout(view_state, cx)
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
let render = self.take().unwrap();
|
||||
let mut rendered_element = (render)(view_state, cx).render();
|
||||
let layout_id = rendered_element.layout(view_state, cx);
|
||||
(layout_id, rendered_element)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
|
||||
@@ -6,15 +6,15 @@ use crate::{
|
||||
SharedString, Size, Style, StyleRefinement, Styled, Task, View, ViewContext, Visibility,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use parking_lot::Mutex;
|
||||
use refineable::Refineable;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cell::RefCell,
|
||||
fmt::Debug,
|
||||
marker::PhantomData,
|
||||
mem,
|
||||
sync::Arc,
|
||||
rc::Rc,
|
||||
time::Duration,
|
||||
};
|
||||
use taffy::style::Overflow;
|
||||
@@ -22,7 +22,6 @@ use util::ResultExt;
|
||||
|
||||
const DRAG_THRESHOLD: f64 = 2.;
|
||||
const TOOLTIP_DELAY: Duration = Duration::from_millis(500);
|
||||
const TOOLTIP_OFFSET: Point<Pixels> = Point::new(px(10.0), px(8.0));
|
||||
|
||||
pub struct GroupStyle {
|
||||
pub group: SharedString,
|
||||
@@ -238,11 +237,11 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
|
||||
//
|
||||
// if we are relying on this side-effect still, removing the debug_assert!
|
||||
// likely breaks the command_palette tests.
|
||||
debug_assert!(
|
||||
A::is_registered(),
|
||||
"{:?} is not registered as an action",
|
||||
A::qualified_name()
|
||||
);
|
||||
// debug_assert!(
|
||||
// A::is_registered(),
|
||||
// "{:?} is not registered as an action",
|
||||
// A::qualified_name()
|
||||
// );
|
||||
self.interactivity().action_listeners.push((
|
||||
TypeId::of::<A>(),
|
||||
Box::new(move |view, action, phase, cx| {
|
||||
@@ -408,21 +407,19 @@ pub trait StatefulInteractiveComponent<V: 'static, E: Element<V>>: InteractiveCo
|
||||
self
|
||||
}
|
||||
|
||||
fn tooltip<W>(
|
||||
fn tooltip(
|
||||
mut self,
|
||||
build_tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> View<W> + 'static,
|
||||
build_tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static,
|
||||
) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
W: 'static + Render,
|
||||
{
|
||||
debug_assert!(
|
||||
self.interactivity().tooltip_builder.is_none(),
|
||||
"calling tooltip more than once on the same element is not supported"
|
||||
);
|
||||
self.interactivity().tooltip_builder = Some(Arc::new(move |view_state, cx| {
|
||||
build_tooltip(view_state, cx).into()
|
||||
}));
|
||||
self.interactivity().tooltip_builder =
|
||||
Some(Rc::new(move |view_state, cx| build_tooltip(view_state, cx)));
|
||||
|
||||
self
|
||||
}
|
||||
@@ -437,14 +434,6 @@ pub trait FocusableComponent<V: 'static>: InteractiveComponent<V> {
|
||||
self
|
||||
}
|
||||
|
||||
fn focus_in(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.interactivity().focus_in_style = f(StyleRefinement::default());
|
||||
self
|
||||
}
|
||||
|
||||
fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
@@ -569,7 +558,7 @@ type DropListener<V> = dyn Fn(&mut V, AnyView, &mut ViewContext<V>) + 'static;
|
||||
|
||||
pub type HoverListener<V> = Box<dyn Fn(&mut V, bool, &mut ViewContext<V>) + 'static>;
|
||||
|
||||
pub type TooltipBuilder<V> = Arc<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>;
|
||||
pub type TooltipBuilder<V> = Rc<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>;
|
||||
|
||||
pub type KeyDownListener<V> =
|
||||
Box<dyn Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext<V>) + 'static>;
|
||||
@@ -617,46 +606,36 @@ impl<V: 'static> Element<V> for Div<V> {
|
||||
self.interactivity.element_id.clone()
|
||||
}
|
||||
|
||||
fn initialize(
|
||||
fn layout(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Self::ElementState {
|
||||
let interactive_state = self
|
||||
.interactivity
|
||||
.initialize(element_state.map(|s| s.interactive_state), cx);
|
||||
|
||||
for child in &mut self.children {
|
||||
child.initialize(view_state, cx);
|
||||
}
|
||||
|
||||
DivState {
|
||||
interactive_state,
|
||||
child_layout_ids: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> crate::LayoutId {
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
let mut child_layout_ids = SmallVec::new();
|
||||
let mut interactivity = mem::take(&mut self.interactivity);
|
||||
let layout_id =
|
||||
interactivity.layout(&mut element_state.interactive_state, cx, |style, cx| {
|
||||
let (layout_id, interactive_state) = interactivity.layout(
|
||||
element_state.map(|s| s.interactive_state),
|
||||
cx,
|
||||
|style, cx| {
|
||||
cx.with_text_style(style.text_style().cloned(), |cx| {
|
||||
element_state.child_layout_ids = self
|
||||
child_layout_ids = self
|
||||
.children
|
||||
.iter_mut()
|
||||
.map(|child| child.layout(view_state, cx))
|
||||
.collect::<SmallVec<_>>();
|
||||
cx.request_layout(&style, element_state.child_layout_ids.iter().copied())
|
||||
cx.request_layout(&style, child_layout_ids.iter().copied())
|
||||
})
|
||||
});
|
||||
},
|
||||
);
|
||||
self.interactivity = interactivity;
|
||||
layout_id
|
||||
(
|
||||
layout_id,
|
||||
DivState {
|
||||
interactive_state,
|
||||
child_layout_ids,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
@@ -725,6 +704,12 @@ pub struct DivState {
|
||||
interactive_state: InteractiveElementState,
|
||||
}
|
||||
|
||||
impl DivState {
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.interactive_state.pending_mouse_down.borrow().is_some()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Interactivity<V> {
|
||||
pub element_id: Option<ElementId>,
|
||||
pub key_context: KeyContext,
|
||||
@@ -734,7 +719,6 @@ pub struct Interactivity<V> {
|
||||
pub group: Option<SharedString>,
|
||||
pub base_style: StyleRefinement,
|
||||
pub focus_style: StyleRefinement,
|
||||
pub focus_in_style: StyleRefinement,
|
||||
pub in_focus_style: StyleRefinement,
|
||||
pub hover_style: StyleRefinement,
|
||||
pub group_hover_style: Option<GroupStyle>,
|
||||
@@ -760,11 +744,12 @@ impl<V> Interactivity<V>
|
||||
where
|
||||
V: 'static,
|
||||
{
|
||||
pub fn initialize(
|
||||
pub fn layout(
|
||||
&mut self,
|
||||
element_state: Option<InteractiveElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> InteractiveElementState {
|
||||
f: impl FnOnce(Style, &mut ViewContext<V>) -> LayoutId,
|
||||
) -> (LayoutId, InteractiveElementState) {
|
||||
let mut element_state = element_state.unwrap_or_default();
|
||||
|
||||
// Ensure we store a focus handle in our element state if we're focusable.
|
||||
@@ -779,17 +764,9 @@ where
|
||||
});
|
||||
}
|
||||
|
||||
element_state
|
||||
}
|
||||
|
||||
pub fn layout(
|
||||
&mut self,
|
||||
element_state: &mut InteractiveElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
f: impl FnOnce(Style, &mut ViewContext<V>) -> LayoutId,
|
||||
) -> LayoutId {
|
||||
let style = self.compute_style(None, element_state, cx);
|
||||
f(style, cx)
|
||||
let style = self.compute_style(None, &mut element_state, cx);
|
||||
let layout_id = f(style, cx);
|
||||
(layout_id, element_state)
|
||||
}
|
||||
|
||||
pub fn paint(
|
||||
@@ -890,7 +867,7 @@ where
|
||||
|
||||
if !click_listeners.is_empty() || drag_listener.is_some() {
|
||||
let pending_mouse_down = element_state.pending_mouse_down.clone();
|
||||
let mouse_down = pending_mouse_down.lock().clone();
|
||||
let mouse_down = pending_mouse_down.borrow().clone();
|
||||
if let Some(mouse_down) = mouse_down {
|
||||
if let Some(drag_listener) = drag_listener {
|
||||
let active_state = element_state.clicked_state.clone();
|
||||
@@ -904,7 +881,7 @@ where
|
||||
&& bounds.contains_point(&event.position)
|
||||
&& (event.position - mouse_down.position).magnitude() > DRAG_THRESHOLD
|
||||
{
|
||||
*active_state.lock() = ElementClickedState::default();
|
||||
*active_state.borrow_mut() = ElementClickedState::default();
|
||||
let cursor_offset = event.position - bounds.origin;
|
||||
let drag = drag_listener(view_state, cursor_offset, cx);
|
||||
cx.active_drag = Some(drag);
|
||||
@@ -924,13 +901,13 @@ where
|
||||
listener(view_state, &mouse_click, cx);
|
||||
}
|
||||
}
|
||||
*pending_mouse_down.lock() = None;
|
||||
*pending_mouse_down.borrow_mut() = None;
|
||||
cx.notify();
|
||||
});
|
||||
} else {
|
||||
cx.on_mouse_event(move |_view_state, event: &MouseDownEvent, phase, cx| {
|
||||
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
|
||||
*pending_mouse_down.lock() = Some(event.clone());
|
||||
*pending_mouse_down.borrow_mut() = Some(event.clone());
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
@@ -946,8 +923,8 @@ where
|
||||
return;
|
||||
}
|
||||
let is_hovered =
|
||||
bounds.contains_point(&event.position) && has_mouse_down.lock().is_none();
|
||||
let mut was_hovered = was_hovered.lock();
|
||||
bounds.contains_point(&event.position) && has_mouse_down.borrow().is_none();
|
||||
let mut was_hovered = was_hovered.borrow_mut();
|
||||
|
||||
if is_hovered != was_hovered.clone() {
|
||||
*was_hovered = is_hovered;
|
||||
@@ -968,13 +945,13 @@ where
|
||||
}
|
||||
|
||||
let is_hovered =
|
||||
bounds.contains_point(&event.position) && pending_mouse_down.lock().is_none();
|
||||
bounds.contains_point(&event.position) && pending_mouse_down.borrow().is_none();
|
||||
if !is_hovered {
|
||||
active_tooltip.lock().take();
|
||||
active_tooltip.borrow_mut().take();
|
||||
return;
|
||||
}
|
||||
|
||||
if active_tooltip.lock().is_none() {
|
||||
if active_tooltip.borrow().is_none() {
|
||||
let task = cx.spawn({
|
||||
let active_tooltip = active_tooltip.clone();
|
||||
let tooltip_builder = tooltip_builder.clone();
|
||||
@@ -982,26 +959,31 @@ where
|
||||
move |view, mut cx| async move {
|
||||
cx.background_executor().timer(TOOLTIP_DELAY).await;
|
||||
view.update(&mut cx, move |view_state, cx| {
|
||||
active_tooltip.lock().replace(ActiveTooltip {
|
||||
waiting: None,
|
||||
active_tooltip.borrow_mut().replace(ActiveTooltip {
|
||||
tooltip: Some(AnyTooltip {
|
||||
view: tooltip_builder(view_state, cx),
|
||||
cursor_offset: cx.mouse_position() + TOOLTIP_OFFSET,
|
||||
cursor_offset: cx.mouse_position(),
|
||||
}),
|
||||
_task: None,
|
||||
});
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
active_tooltip.lock().replace(ActiveTooltip {
|
||||
waiting: Some(task),
|
||||
active_tooltip.borrow_mut().replace(ActiveTooltip {
|
||||
tooltip: None,
|
||||
_task: Some(task),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(active_tooltip) = element_state.active_tooltip.lock().as_ref() {
|
||||
let active_tooltip = element_state.active_tooltip.clone();
|
||||
cx.on_mouse_event(move |_, _: &MouseDownEvent, _, _| {
|
||||
active_tooltip.borrow_mut().take();
|
||||
});
|
||||
|
||||
if let Some(active_tooltip) = element_state.active_tooltip.borrow().as_ref() {
|
||||
if active_tooltip.tooltip.is_some() {
|
||||
cx.active_tooltip = active_tooltip.tooltip.clone()
|
||||
}
|
||||
@@ -1009,10 +991,10 @@ where
|
||||
}
|
||||
|
||||
let active_state = element_state.clicked_state.clone();
|
||||
if !active_state.lock().is_clicked() {
|
||||
if !active_state.borrow().is_clicked() {
|
||||
cx.on_mouse_event(move |_, _: &MouseUpEvent, phase, cx| {
|
||||
if phase == DispatchPhase::Capture {
|
||||
*active_state.lock() = ElementClickedState::default();
|
||||
*active_state.borrow_mut() = ElementClickedState::default();
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
@@ -1027,7 +1009,7 @@ where
|
||||
.map_or(false, |bounds| bounds.contains_point(&down.position));
|
||||
let element = bounds.contains_point(&down.position);
|
||||
if group || element {
|
||||
*active_state.lock() = ElementClickedState { group, element };
|
||||
*active_state.borrow_mut() = ElementClickedState { group, element };
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -1038,14 +1020,14 @@ where
|
||||
if overflow.x == Overflow::Scroll || overflow.y == Overflow::Scroll {
|
||||
let scroll_offset = element_state
|
||||
.scroll_offset
|
||||
.get_or_insert_with(Arc::default)
|
||||
.get_or_insert_with(Rc::default)
|
||||
.clone();
|
||||
let line_height = cx.line_height();
|
||||
let scroll_max = (content_size - bounds.size).max(&Size::default());
|
||||
|
||||
cx.on_mouse_event(move |_, event: &ScrollWheelEvent, phase, cx| {
|
||||
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
|
||||
let mut scroll_offset = scroll_offset.lock();
|
||||
let mut scroll_offset = scroll_offset.borrow_mut();
|
||||
let old_scroll_offset = *scroll_offset;
|
||||
let delta = event.delta.pixel_delta(line_height);
|
||||
|
||||
@@ -1074,7 +1056,7 @@ where
|
||||
let scroll_offset = element_state
|
||||
.scroll_offset
|
||||
.as_ref()
|
||||
.map(|scroll_offset| *scroll_offset.lock());
|
||||
.map(|scroll_offset| *scroll_offset.borrow());
|
||||
|
||||
cx.with_key_dispatch(
|
||||
self.key_context.clone(),
|
||||
@@ -1124,10 +1106,6 @@ where
|
||||
style.refine(&self.base_style);
|
||||
|
||||
if let Some(focus_handle) = self.tracked_focus_handle.as_ref() {
|
||||
if focus_handle.contains_focused(cx) {
|
||||
style.refine(&self.focus_in_style);
|
||||
}
|
||||
|
||||
if focus_handle.within_focused(cx) {
|
||||
style.refine(&self.in_focus_style);
|
||||
}
|
||||
@@ -1173,7 +1151,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
let clicked_state = element_state.clicked_state.lock();
|
||||
let clicked_state = element_state.clicked_state.borrow();
|
||||
if clicked_state.group {
|
||||
if let Some(group) = self.group_active_style.as_ref() {
|
||||
style.refine(&group.style)
|
||||
@@ -1200,7 +1178,6 @@ impl<V: 'static> Default for Interactivity<V> {
|
||||
group: None,
|
||||
base_style: StyleRefinement::default(),
|
||||
focus_style: StyleRefinement::default(),
|
||||
focus_in_style: StyleRefinement::default(),
|
||||
in_focus_style: StyleRefinement::default(),
|
||||
hover_style: StyleRefinement::default(),
|
||||
group_hover_style: None,
|
||||
@@ -1227,17 +1204,16 @@ impl<V: 'static> Default for Interactivity<V> {
|
||||
#[derive(Default)]
|
||||
pub struct InteractiveElementState {
|
||||
pub focus_handle: Option<FocusHandle>,
|
||||
pub clicked_state: Arc<Mutex<ElementClickedState>>,
|
||||
pub hover_state: Arc<Mutex<bool>>,
|
||||
pub pending_mouse_down: Arc<Mutex<Option<MouseDownEvent>>>,
|
||||
pub scroll_offset: Option<Arc<Mutex<Point<Pixels>>>>,
|
||||
pub active_tooltip: Arc<Mutex<Option<ActiveTooltip>>>,
|
||||
pub clicked_state: Rc<RefCell<ElementClickedState>>,
|
||||
pub hover_state: Rc<RefCell<bool>>,
|
||||
pub pending_mouse_down: Rc<RefCell<Option<MouseDownEvent>>>,
|
||||
pub scroll_offset: Option<Rc<RefCell<Point<Pixels>>>>,
|
||||
pub active_tooltip: Rc<RefCell<Option<ActiveTooltip>>>,
|
||||
}
|
||||
|
||||
pub struct ActiveTooltip {
|
||||
#[allow(unused)] // used to drop the task
|
||||
waiting: Option<Task<()>>,
|
||||
tooltip: Option<AnyTooltip>,
|
||||
_task: Option<Task<()>>,
|
||||
}
|
||||
|
||||
/// Whether or not the element or a group that contains it is clicked by the mouse.
|
||||
@@ -1321,21 +1297,12 @@ where
|
||||
self.element.element_id()
|
||||
}
|
||||
|
||||
fn initialize(
|
||||
fn layout(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Self::ElementState {
|
||||
self.element.initialize(view_state, element_state, cx)
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> LayoutId {
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
self.element.layout(view_state, element_state, cx)
|
||||
}
|
||||
|
||||
@@ -1416,21 +1383,12 @@ where
|
||||
self.element.element_id()
|
||||
}
|
||||
|
||||
fn initialize(
|
||||
fn layout(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Self::ElementState {
|
||||
self.element.initialize(view_state, element_state, cx)
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> LayoutId {
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
self.element.layout(view_state, element_state, cx)
|
||||
}
|
||||
|
||||
|
||||
@@ -48,21 +48,12 @@ impl<V> Element<V> for Img<V> {
|
||||
self.interactivity.element_id.clone()
|
||||
}
|
||||
|
||||
fn initialize(
|
||||
fn layout(
|
||||
&mut self,
|
||||
_view_state: &mut V,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Self::ElementState {
|
||||
self.interactivity.initialize(element_state, cx)
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_view_state: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> LayoutId {
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
self.interactivity.layout(element_state, cx, |style, cx| {
|
||||
cx.request_layout(&style, None)
|
||||
})
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
mod div;
|
||||
mod img;
|
||||
mod overlay;
|
||||
mod svg;
|
||||
mod text;
|
||||
mod uniform_list;
|
||||
|
||||
pub use div::*;
|
||||
pub use img::*;
|
||||
pub use overlay::*;
|
||||
pub use svg::*;
|
||||
pub use text::*;
|
||||
pub use uniform_list::*;
|
||||
|
||||
232
crates/gpui2/src/elements/overlay.rs
Normal file
232
crates/gpui2/src/elements/overlay.rs
Normal file
@@ -0,0 +1,232 @@
|
||||
use smallvec::SmallVec;
|
||||
use taffy::style::{Display, Position};
|
||||
|
||||
use crate::{
|
||||
point, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, ParentComponent, Pixels,
|
||||
Point, Size, Style,
|
||||
};
|
||||
|
||||
pub struct OverlayState {
|
||||
child_layout_ids: SmallVec<[LayoutId; 4]>,
|
||||
}
|
||||
|
||||
pub struct Overlay<V> {
|
||||
children: SmallVec<[AnyElement<V>; 2]>,
|
||||
anchor_corner: AnchorCorner,
|
||||
fit_mode: OverlayFitMode,
|
||||
// todo!();
|
||||
anchor_position: Option<Point<Pixels>>,
|
||||
// position_mode: OverlayPositionMode,
|
||||
}
|
||||
|
||||
/// overlay gives you a floating element that will avoid overflowing the window bounds.
|
||||
/// Its children should have no margin to avoid measurement issues.
|
||||
pub fn overlay<V: 'static>() -> Overlay<V> {
|
||||
Overlay {
|
||||
children: SmallVec::new(),
|
||||
anchor_corner: AnchorCorner::TopLeft,
|
||||
fit_mode: OverlayFitMode::SwitchAnchor,
|
||||
anchor_position: None,
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Overlay<V> {
|
||||
/// Sets which corner of the overlay should be anchored to the current position.
|
||||
pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
|
||||
self.anchor_corner = anchor;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the position in window co-ordinates
|
||||
/// (otherwise the location the overlay is rendered is used)
|
||||
pub fn position(mut self, anchor: Point<Pixels>) -> Self {
|
||||
self.anchor_position = Some(anchor);
|
||||
self
|
||||
}
|
||||
|
||||
/// Snap to window edge instead of switching anchor corner when an overflow would occur.
|
||||
pub fn snap_to_window(mut self) -> Self {
|
||||
self.fit_mode = OverlayFitMode::SnapToWindow;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> ParentComponent<V> for Overlay<V> {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
|
||||
&mut self.children
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> Component<V> for Overlay<V> {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
AnyElement::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> Element<V> for Overlay<V> {
|
||||
type ElementState = OverlayState;
|
||||
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
_: Option<Self::ElementState>,
|
||||
cx: &mut crate::ViewContext<V>,
|
||||
) -> (crate::LayoutId, Self::ElementState) {
|
||||
let child_layout_ids = self
|
||||
.children
|
||||
.iter_mut()
|
||||
.map(|child| child.layout(view_state, cx))
|
||||
.collect::<SmallVec<_>>();
|
||||
|
||||
let mut overlay_style = Style::default();
|
||||
overlay_style.position = Position::Absolute;
|
||||
overlay_style.display = Display::Flex;
|
||||
|
||||
let layout_id = cx.request_layout(&overlay_style, child_layout_ids.iter().copied());
|
||||
|
||||
(layout_id, OverlayState { child_layout_ids })
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: crate::Bounds<crate::Pixels>,
|
||||
view_state: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut crate::ViewContext<V>,
|
||||
) {
|
||||
if element_state.child_layout_ids.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut child_min = point(Pixels::MAX, Pixels::MAX);
|
||||
let mut child_max = Point::default();
|
||||
for child_layout_id in &element_state.child_layout_ids {
|
||||
let child_bounds = cx.layout_bounds(*child_layout_id);
|
||||
child_min = child_min.min(&child_bounds.origin);
|
||||
child_max = child_max.max(&child_bounds.lower_right());
|
||||
}
|
||||
let size: Size<Pixels> = (child_max - child_min).into();
|
||||
let origin = self.anchor_position.unwrap_or(bounds.origin);
|
||||
|
||||
let mut desired = self.anchor_corner.get_bounds(origin, size);
|
||||
let limits = Bounds {
|
||||
origin: Point::zero(),
|
||||
size: cx.viewport_size(),
|
||||
};
|
||||
|
||||
match self.fit_mode {
|
||||
OverlayFitMode::SnapToWindow => {
|
||||
// Snap the horizontal edges of the overlay to the horizontal edges of the window if
|
||||
// its horizontal bounds overflow
|
||||
if desired.right() > limits.right() {
|
||||
desired.origin.x -= desired.right() - limits.right();
|
||||
} else if desired.left() < limits.left() {
|
||||
desired.origin.x = limits.origin.x;
|
||||
}
|
||||
|
||||
// Snap the vertical edges of the overlay to the vertical edges of the window if
|
||||
// its vertical bounds overflow.
|
||||
if desired.bottom() > limits.bottom() {
|
||||
desired.origin.y -= desired.bottom() - limits.bottom();
|
||||
} else if desired.top() < limits.top() {
|
||||
desired.origin.y = limits.origin.y;
|
||||
}
|
||||
}
|
||||
OverlayFitMode::SwitchAnchor => {
|
||||
let mut anchor_corner = self.anchor_corner;
|
||||
|
||||
if desired.left() < limits.left() || desired.right() > limits.right() {
|
||||
anchor_corner = anchor_corner.switch_axis(Axis::Horizontal);
|
||||
}
|
||||
|
||||
if bounds.top() < limits.top() || bounds.bottom() > limits.bottom() {
|
||||
anchor_corner = anchor_corner.switch_axis(Axis::Vertical);
|
||||
}
|
||||
|
||||
// Update bounds if needed
|
||||
if anchor_corner != self.anchor_corner {
|
||||
desired = anchor_corner.get_bounds(origin, size)
|
||||
}
|
||||
}
|
||||
OverlayFitMode::None => {}
|
||||
}
|
||||
|
||||
cx.with_element_offset(desired.origin - bounds.origin, |cx| {
|
||||
for child in &mut self.children {
|
||||
child.paint(view_state, cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
enum Axis {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum OverlayFitMode {
|
||||
SnapToWindow,
|
||||
SwitchAnchor,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AnchorCorner {
|
||||
TopLeft,
|
||||
TopRight,
|
||||
BottomLeft,
|
||||
BottomRight,
|
||||
}
|
||||
|
||||
impl AnchorCorner {
|
||||
fn get_bounds(&self, origin: Point<Pixels>, size: Size<Pixels>) -> Bounds<Pixels> {
|
||||
let origin = match self {
|
||||
Self::TopLeft => origin,
|
||||
Self::TopRight => Point {
|
||||
x: origin.x - size.width,
|
||||
y: origin.y,
|
||||
},
|
||||
Self::BottomLeft => Point {
|
||||
x: origin.x,
|
||||
y: origin.y - size.height,
|
||||
},
|
||||
Self::BottomRight => Point {
|
||||
x: origin.x - size.width,
|
||||
y: origin.y - size.height,
|
||||
},
|
||||
};
|
||||
|
||||
Bounds { origin, size }
|
||||
}
|
||||
|
||||
pub fn corner(&self, bounds: Bounds<Pixels>) -> Point<Pixels> {
|
||||
match self {
|
||||
Self::TopLeft => bounds.origin,
|
||||
Self::TopRight => bounds.upper_right(),
|
||||
Self::BottomLeft => bounds.lower_left(),
|
||||
Self::BottomRight => bounds.lower_right(),
|
||||
}
|
||||
}
|
||||
|
||||
fn switch_axis(self, axis: Axis) -> Self {
|
||||
match axis {
|
||||
Axis::Vertical => match self {
|
||||
AnchorCorner::TopLeft => AnchorCorner::BottomLeft,
|
||||
AnchorCorner::TopRight => AnchorCorner::BottomRight,
|
||||
AnchorCorner::BottomLeft => AnchorCorner::TopLeft,
|
||||
AnchorCorner::BottomRight => AnchorCorner::TopRight,
|
||||
},
|
||||
Axis::Horizontal => match self {
|
||||
AnchorCorner::TopLeft => AnchorCorner::TopRight,
|
||||
AnchorCorner::TopRight => AnchorCorner::TopLeft,
|
||||
AnchorCorner::BottomLeft => AnchorCorner::BottomRight,
|
||||
AnchorCorner::BottomRight => AnchorCorner::BottomLeft,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,21 +37,12 @@ impl<V> Element<V> for Svg<V> {
|
||||
self.interactivity.element_id.clone()
|
||||
}
|
||||
|
||||
fn initialize(
|
||||
fn layout(
|
||||
&mut self,
|
||||
_view_state: &mut V,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Self::ElementState {
|
||||
self.interactivity.initialize(element_state, cx)
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_view_state: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> LayoutId {
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
self.interactivity.layout(element_state, cx, |style, cx| {
|
||||
cx.request_layout(&style, None)
|
||||
})
|
||||
|
||||
@@ -1,96 +1,51 @@
|
||||
use crate::{
|
||||
AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, Line, Pixels, SharedString,
|
||||
Size, TextRun, ViewContext,
|
||||
AnyElement, BorrowWindow, Bounds, Component, Element, ElementId, LayoutId, Pixels,
|
||||
SharedString, Size, TextRun, ViewContext, WrappedLine,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use parking_lot::{Mutex, MutexGuard};
|
||||
use smallvec::SmallVec;
|
||||
use std::{marker::PhantomData, sync::Arc};
|
||||
use std::{cell::Cell, rc::Rc, sync::Arc};
|
||||
use util::ResultExt;
|
||||
|
||||
impl<V: 'static> Component<V> for SharedString {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
Text {
|
||||
text: self,
|
||||
runs: None,
|
||||
state_type: PhantomData,
|
||||
}
|
||||
.render()
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> Component<V> for &'static str {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
Text {
|
||||
text: self.into(),
|
||||
runs: None,
|
||||
state_type: PhantomData,
|
||||
}
|
||||
.render()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Figure out how to pass `String` to `child` without this.
|
||||
// This impl doesn't exist in the `gpui2` crate.
|
||||
impl<V: 'static> Component<V> for String {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
Text {
|
||||
text: self.into(),
|
||||
runs: None,
|
||||
state_type: PhantomData,
|
||||
}
|
||||
.render()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Text<V> {
|
||||
pub struct Text {
|
||||
text: SharedString,
|
||||
runs: Option<Vec<TextRun>>,
|
||||
state_type: PhantomData<V>,
|
||||
}
|
||||
|
||||
impl<V: 'static> Text<V> {
|
||||
/// styled renders text that has different runs of different styles.
|
||||
/// callers are responsible for setting the correct style for each run.
|
||||
////
|
||||
/// For uniform text you can usually just pass a string as a child, and
|
||||
/// cx.text_style() will be used automatically.
|
||||
impl Text {
|
||||
/// Renders text with runs of different styles.
|
||||
///
|
||||
/// Callers are responsible for setting the correct style for each run.
|
||||
/// For text with a uniform style, you can usually avoid calling this constructor
|
||||
/// and just pass text directly.
|
||||
pub fn styled(text: SharedString, runs: Vec<TextRun>) -> Self {
|
||||
Text {
|
||||
text,
|
||||
runs: Some(runs),
|
||||
state_type: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> Component<V> for Text<V> {
|
||||
impl<V: 'static> Component<V> for Text {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
AnyElement::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> Element<V> for Text<V> {
|
||||
type ElementState = Arc<Mutex<Option<TextElementState>>>;
|
||||
impl<V: 'static> Element<V> for Text {
|
||||
type ElementState = TextState;
|
||||
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn initialize(
|
||||
&mut self,
|
||||
_view_state: &mut V,
|
||||
element_state: Option<Self::ElementState>,
|
||||
_cx: &mut ViewContext<V>,
|
||||
) -> Self::ElementState {
|
||||
element_state.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_view: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> LayoutId {
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
let element_state = element_state.unwrap_or_default();
|
||||
let text_system = cx.text_system().clone();
|
||||
let text_style = cx.text_style();
|
||||
let font_size = text_style.font_size.to_pixels(cx.rem_size());
|
||||
@@ -111,7 +66,7 @@ impl<V: 'static> Element<V> for Text<V> {
|
||||
let element_state = element_state.clone();
|
||||
move |known_dimensions, _| {
|
||||
let Some(lines) = text_system
|
||||
.layout_text(
|
||||
.shape_text(
|
||||
&text,
|
||||
font_size,
|
||||
&runs[..],
|
||||
@@ -119,36 +74,29 @@ impl<V: 'static> Element<V> for Text<V> {
|
||||
)
|
||||
.log_err()
|
||||
else {
|
||||
element_state.lock().replace(TextElementState {
|
||||
element_state.lock().replace(TextStateInner {
|
||||
lines: Default::default(),
|
||||
line_height,
|
||||
});
|
||||
return Size::default();
|
||||
};
|
||||
|
||||
let line_count = lines
|
||||
.iter()
|
||||
.map(|line| line.wrap_count() + 1)
|
||||
.sum::<usize>();
|
||||
let size = Size {
|
||||
width: lines
|
||||
.iter()
|
||||
.map(|line| line.layout.width)
|
||||
.max()
|
||||
.unwrap()
|
||||
.ceil(),
|
||||
height: line_height * line_count,
|
||||
};
|
||||
let mut size: Size<Pixels> = Size::default();
|
||||
for line in &lines {
|
||||
let line_size = line.size(line_height);
|
||||
size.height += line_size.height;
|
||||
size.width = size.width.max(line_size.width);
|
||||
}
|
||||
|
||||
element_state
|
||||
.lock()
|
||||
.replace(TextElementState { lines, line_height });
|
||||
.replace(TextStateInner { lines, line_height });
|
||||
|
||||
size
|
||||
}
|
||||
});
|
||||
|
||||
layout_id
|
||||
(layout_id, element_state)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
@@ -173,7 +121,104 @@ impl<V: 'static> Element<V> for Text<V> {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TextElementState {
|
||||
lines: SmallVec<[Line; 1]>,
|
||||
#[derive(Default, Clone)]
|
||||
pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
|
||||
|
||||
impl TextState {
|
||||
fn lock(&self) -> MutexGuard<Option<TextStateInner>> {
|
||||
self.0.lock()
|
||||
}
|
||||
}
|
||||
|
||||
struct TextStateInner {
|
||||
lines: SmallVec<[WrappedLine; 1]>,
|
||||
line_height: Pixels,
|
||||
}
|
||||
|
||||
struct InteractiveText {
|
||||
id: ElementId,
|
||||
text: Text,
|
||||
}
|
||||
|
||||
struct InteractiveTextState {
|
||||
text_state: TextState,
|
||||
clicked_range_ixs: Rc<Cell<SmallVec<[usize; 1]>>>,
|
||||
}
|
||||
|
||||
impl<V: 'static> Element<V> for InteractiveText {
|
||||
type ElementState = InteractiveTextState;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
if let Some(InteractiveTextState {
|
||||
text_state,
|
||||
clicked_range_ixs,
|
||||
}) = element_state
|
||||
{
|
||||
let (layout_id, text_state) = self.text.layout(view_state, Some(text_state), cx);
|
||||
let element_state = InteractiveTextState {
|
||||
text_state,
|
||||
clicked_range_ixs,
|
||||
};
|
||||
(layout_id, element_state)
|
||||
} else {
|
||||
let (layout_id, text_state) = self.text.layout(view_state, None, cx);
|
||||
let element_state = InteractiveTextState {
|
||||
text_state,
|
||||
clicked_range_ixs: Rc::default(),
|
||||
};
|
||||
(layout_id, element_state)
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
view_state: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
) {
|
||||
self.text
|
||||
.paint(bounds, view_state, &mut element_state.text_state, cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> Component<V> for SharedString {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
Text {
|
||||
text: self,
|
||||
runs: None,
|
||||
}
|
||||
.render()
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> Component<V> for &'static str {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
Text {
|
||||
text: self.into(),
|
||||
runs: None,
|
||||
}
|
||||
.render()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Figure out how to pass `String` to `child` without this.
|
||||
// This impl doesn't exist in the `gpui2` crate.
|
||||
impl<V: 'static> Component<V> for String {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
Text {
|
||||
text: self.into(),
|
||||
runs: None,
|
||||
}
|
||||
.render()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,8 @@ use crate::{
|
||||
ElementId, InteractiveComponent, InteractiveElementState, Interactivity, LayoutId, Pixels,
|
||||
Point, Size, StyleRefinement, Styled, ViewContext,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use smallvec::SmallVec;
|
||||
use std::{cmp, mem, ops::Range, sync::Arc};
|
||||
use std::{cell::RefCell, cmp, mem, ops::Range, rc::Rc};
|
||||
use taffy::style::Overflow;
|
||||
|
||||
/// uniform_list provides lazy rendering for a set of items that are of uniform height.
|
||||
@@ -61,23 +60,23 @@ pub struct UniformList<V: 'static> {
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct UniformListScrollHandle(Arc<Mutex<Option<ScrollHandleState>>>);
|
||||
pub struct UniformListScrollHandle(Rc<RefCell<Option<ScrollHandleState>>>);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ScrollHandleState {
|
||||
item_height: Pixels,
|
||||
list_height: Pixels,
|
||||
scroll_offset: Arc<Mutex<Point<Pixels>>>,
|
||||
scroll_offset: Rc<RefCell<Point<Pixels>>>,
|
||||
}
|
||||
|
||||
impl UniformListScrollHandle {
|
||||
pub fn new() -> Self {
|
||||
Self(Arc::new(Mutex::new(None)))
|
||||
Self(Rc::new(RefCell::new(None)))
|
||||
}
|
||||
|
||||
pub fn scroll_to_item(&self, ix: usize) {
|
||||
if let Some(state) = &*self.0.lock() {
|
||||
let mut scroll_offset = state.scroll_offset.lock();
|
||||
if let Some(state) = &*self.0.borrow() {
|
||||
let mut scroll_offset = state.scroll_offset.borrow_mut();
|
||||
let item_top = state.item_height * ix;
|
||||
let item_bottom = item_top + state.item_height;
|
||||
let scroll_top = -scroll_offset.y;
|
||||
@@ -109,62 +108,54 @@ impl<V: 'static> Element<V> for UniformList<V> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
fn initialize(
|
||||
fn layout(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Self::ElementState {
|
||||
if let Some(mut element_state) = element_state {
|
||||
element_state.interactive = self
|
||||
.interactivity
|
||||
.initialize(Some(element_state.interactive), cx);
|
||||
element_state
|
||||
} else {
|
||||
let item_size = self.measure_item(view_state, None, cx);
|
||||
UniformListState {
|
||||
interactive: self.interactivity.initialize(None, cx),
|
||||
item_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_view_state: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> LayoutId {
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
let max_items = self.item_count;
|
||||
let item_size = element_state.item_size;
|
||||
let rem_size = cx.rem_size();
|
||||
let item_size = element_state
|
||||
.as_ref()
|
||||
.map(|s| s.item_size)
|
||||
.unwrap_or_else(|| self.measure_item(view_state, None, cx));
|
||||
|
||||
self.interactivity
|
||||
.layout(&mut element_state.interactive, cx, |style, cx| {
|
||||
cx.request_measured_layout(
|
||||
style,
|
||||
rem_size,
|
||||
move |known_dimensions: Size<Option<Pixels>>,
|
||||
available_space: Size<AvailableSpace>| {
|
||||
let desired_height = item_size.height * max_items;
|
||||
let width = known_dimensions
|
||||
.width
|
||||
.unwrap_or(match available_space.width {
|
||||
AvailableSpace::Definite(x) => x,
|
||||
let (layout_id, interactive) =
|
||||
self.interactivity
|
||||
.layout(element_state.map(|s| s.interactive), cx, |style, cx| {
|
||||
cx.request_measured_layout(
|
||||
style,
|
||||
rem_size,
|
||||
move |known_dimensions: Size<Option<Pixels>>,
|
||||
available_space: Size<AvailableSpace>| {
|
||||
let desired_height = item_size.height * max_items;
|
||||
let width =
|
||||
known_dimensions
|
||||
.width
|
||||
.unwrap_or(match available_space.width {
|
||||
AvailableSpace::Definite(x) => x,
|
||||
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
|
||||
item_size.width
|
||||
}
|
||||
});
|
||||
let height = match available_space.height {
|
||||
AvailableSpace::Definite(x) => desired_height.min(x),
|
||||
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
|
||||
item_size.width
|
||||
desired_height
|
||||
}
|
||||
});
|
||||
let height = match available_space.height {
|
||||
AvailableSpace::Definite(x) => desired_height.min(x),
|
||||
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
|
||||
desired_height
|
||||
}
|
||||
};
|
||||
size(width, height)
|
||||
},
|
||||
)
|
||||
})
|
||||
};
|
||||
size(width, height)
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
let element_state = UniformListState {
|
||||
interactive,
|
||||
item_size,
|
||||
};
|
||||
|
||||
(layout_id, element_state)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
@@ -196,7 +187,7 @@ impl<V: 'static> Element<V> for UniformList<V> {
|
||||
let shared_scroll_offset = element_state
|
||||
.interactive
|
||||
.scroll_offset
|
||||
.get_or_insert_with(Arc::default)
|
||||
.get_or_insert_with(Rc::default)
|
||||
.clone();
|
||||
|
||||
interactivity.paint(
|
||||
@@ -222,7 +213,7 @@ impl<V: 'static> Element<V> for UniformList<V> {
|
||||
.measure_item(view_state, Some(padded_bounds.size.width), cx)
|
||||
.height;
|
||||
if let Some(scroll_handle) = self.scroll_handle.clone() {
|
||||
scroll_handle.0.lock().replace(ScrollHandleState {
|
||||
scroll_handle.0.borrow_mut().replace(ScrollHandleState {
|
||||
item_height,
|
||||
list_height: padded_bounds.size.height,
|
||||
scroll_offset: shared_scroll_offset,
|
||||
|
||||
@@ -5,10 +5,11 @@ use std::{
|
||||
fmt::Debug,
|
||||
marker::PhantomData,
|
||||
mem,
|
||||
num::NonZeroUsize,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
|
||||
Arc,
|
||||
},
|
||||
task::{Context, Poll},
|
||||
@@ -71,30 +72,57 @@ impl<T> Future for Task<T> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
pub struct TaskLabel(NonZeroUsize);
|
||||
|
||||
impl TaskLabel {
|
||||
pub fn new() -> Self {
|
||||
static NEXT_TASK_LABEL: AtomicUsize = AtomicUsize::new(1);
|
||||
Self(NEXT_TASK_LABEL.fetch_add(1, SeqCst).try_into().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
type AnyLocalFuture<R> = Pin<Box<dyn 'static + Future<Output = R>>>;
|
||||
|
||||
type AnyFuture<R> = Pin<Box<dyn 'static + Send + Future<Output = R>>>;
|
||||
|
||||
impl BackgroundExecutor {
|
||||
pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
|
||||
Self { dispatcher }
|
||||
}
|
||||
|
||||
/// Enqueues the given closure to be run on any thread. The closure returns
|
||||
/// a future which will be run to completion on any available thread.
|
||||
/// Enqueues the given future to be run to completion on a background thread.
|
||||
pub fn spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
|
||||
where
|
||||
R: Send + 'static,
|
||||
{
|
||||
self.spawn_internal::<R>(Box::pin(future), None)
|
||||
}
|
||||
|
||||
/// Enqueues the given future to be run to completion on a background thread.
|
||||
/// The given label can be used to control the priority of the task in tests.
|
||||
pub fn spawn_labeled<R>(
|
||||
&self,
|
||||
label: TaskLabel,
|
||||
future: impl Future<Output = R> + Send + 'static,
|
||||
) -> Task<R>
|
||||
where
|
||||
R: Send + 'static,
|
||||
{
|
||||
self.spawn_internal::<R>(Box::pin(future), Some(label))
|
||||
}
|
||||
|
||||
fn spawn_internal<R: Send + 'static>(
|
||||
&self,
|
||||
future: AnyFuture<R>,
|
||||
label: Option<TaskLabel>,
|
||||
) -> Task<R> {
|
||||
let dispatcher = self.dispatcher.clone();
|
||||
fn inner<R: Send + 'static>(
|
||||
dispatcher: Arc<dyn PlatformDispatcher>,
|
||||
future: AnyFuture<R>,
|
||||
) -> Task<R> {
|
||||
let (runnable, task) =
|
||||
async_task::spawn(future, move |runnable| dispatcher.dispatch(runnable));
|
||||
runnable.schedule();
|
||||
Task::Spawned(task)
|
||||
}
|
||||
inner::<R>(dispatcher, Box::pin(future))
|
||||
let (runnable, task) =
|
||||
async_task::spawn(future, move |runnable| dispatcher.dispatch(runnable, label));
|
||||
runnable.schedule();
|
||||
Task::Spawned(task)
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@@ -130,7 +158,7 @@ impl BackgroundExecutor {
|
||||
match future.as_mut().poll(&mut cx) {
|
||||
Poll::Ready(result) => return result,
|
||||
Poll::Pending => {
|
||||
if !self.dispatcher.poll(background_only) {
|
||||
if !self.dispatcher.tick(background_only) {
|
||||
if awoken.swap(false, SeqCst) {
|
||||
continue;
|
||||
}
|
||||
@@ -216,11 +244,21 @@ impl BackgroundExecutor {
|
||||
self.dispatcher.as_test().unwrap().simulate_random_delay()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn deprioritize(&self, task_label: TaskLabel) {
|
||||
self.dispatcher.as_test().unwrap().deprioritize(task_label)
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn advance_clock(&self, duration: Duration) {
|
||||
self.dispatcher.as_test().unwrap().advance_clock(duration)
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn tick(&self) -> bool {
|
||||
self.dispatcher.as_test().unwrap().tick(false)
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn run_until_parked(&self) {
|
||||
self.dispatcher.as_test().unwrap().run_until_parked()
|
||||
|
||||
@@ -335,11 +335,15 @@ where
|
||||
};
|
||||
Bounds { origin, size }
|
||||
}
|
||||
|
||||
pub fn new(origin: Point<T>, size: Size<T>) -> Self {
|
||||
Bounds { origin, size }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Bounds<T>
|
||||
where
|
||||
T: Clone + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T> + Default,
|
||||
T: Clone + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T> + Default + Half,
|
||||
{
|
||||
pub fn intersects(&self, other: &Bounds<T>) -> bool {
|
||||
let my_lower_right = self.lower_right();
|
||||
@@ -358,6 +362,13 @@ where
|
||||
self.size.width = self.size.width.clone() + double_amount.clone();
|
||||
self.size.height = self.size.height.clone() + double_amount;
|
||||
}
|
||||
|
||||
pub fn center(&self) -> Point<T> {
|
||||
Point {
|
||||
x: self.origin.x.clone() + self.size.width.clone().half(),
|
||||
y: self.origin.y.clone() + self.size.height.clone().half(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + Default + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T>> Bounds<T> {
|
||||
@@ -421,6 +432,22 @@ impl<T> Bounds<T>
|
||||
where
|
||||
T: Add<T, Output = T> + Clone + Default + Debug,
|
||||
{
|
||||
pub fn top(&self) -> T {
|
||||
self.origin.y.clone()
|
||||
}
|
||||
|
||||
pub fn bottom(&self) -> T {
|
||||
self.origin.y.clone() + self.size.height.clone()
|
||||
}
|
||||
|
||||
pub fn left(&self) -> T {
|
||||
self.origin.x.clone()
|
||||
}
|
||||
|
||||
pub fn right(&self) -> T {
|
||||
self.origin.x.clone() + self.size.width.clone()
|
||||
}
|
||||
|
||||
pub fn upper_right(&self) -> Point<T> {
|
||||
Point {
|
||||
x: self.origin.x.clone() + self.size.width.clone(),
|
||||
@@ -1191,6 +1218,46 @@ impl From<()> for Length {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Half {
|
||||
fn half(&self) -> Self;
|
||||
}
|
||||
|
||||
impl Half for f32 {
|
||||
fn half(&self) -> Self {
|
||||
self / 2.
|
||||
}
|
||||
}
|
||||
|
||||
impl Half for DevicePixels {
|
||||
fn half(&self) -> Self {
|
||||
Self(self.0 / 2)
|
||||
}
|
||||
}
|
||||
|
||||
impl Half for ScaledPixels {
|
||||
fn half(&self) -> Self {
|
||||
Self(self.0 / 2.)
|
||||
}
|
||||
}
|
||||
|
||||
impl Half for Pixels {
|
||||
fn half(&self) -> Self {
|
||||
Self(self.0 / 2.)
|
||||
}
|
||||
}
|
||||
|
||||
impl Half for Rems {
|
||||
fn half(&self) -> Self {
|
||||
Self(self.0 / 2.)
|
||||
}
|
||||
}
|
||||
|
||||
impl Half for GlobalPixels {
|
||||
fn half(&self) -> Self {
|
||||
Self(self.0 / 2.)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IsZero {
|
||||
fn is_zero(&self) -> bool;
|
||||
}
|
||||
|
||||
@@ -49,11 +49,13 @@ pub use input::*;
|
||||
pub use interactive::*;
|
||||
pub use key_dispatch::*;
|
||||
pub use keymap::*;
|
||||
pub use linkme;
|
||||
pub use platform::*;
|
||||
use private::Sealed;
|
||||
pub use refineable::*;
|
||||
pub use scene::*;
|
||||
pub use serde;
|
||||
pub use serde_derive;
|
||||
pub use serde_json;
|
||||
pub use smallvec;
|
||||
pub use smol::Timer;
|
||||
@@ -135,6 +137,10 @@ pub trait VisualContext: Context {
|
||||
) -> Self::Result<View<V>>
|
||||
where
|
||||
V: Render;
|
||||
|
||||
fn focus_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
|
||||
where
|
||||
V: FocusableView;
|
||||
}
|
||||
|
||||
pub trait Entity<T>: Sealed {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
build_action_from_type, Action, DispatchPhase, FocusId, KeyBinding, KeyContext, KeyMatch,
|
||||
Keymap, Keystroke, KeystrokeMatcher, WindowContext,
|
||||
Action, ActionRegistry, DispatchPhase, FocusId, KeyBinding, KeyContext, KeyMatch, Keymap,
|
||||
Keystroke, KeystrokeMatcher, WindowContext,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use parking_lot::Mutex;
|
||||
@@ -10,7 +10,6 @@ use std::{
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct DispatchNodeId(usize);
|
||||
@@ -22,6 +21,7 @@ pub(crate) struct DispatchTree {
|
||||
focusable_node_ids: HashMap<FocusId, DispatchNodeId>,
|
||||
keystroke_matchers: HashMap<SmallVec<[KeyContext; 4]>, KeystrokeMatcher>,
|
||||
keymap: Arc<Mutex<Keymap>>,
|
||||
action_registry: Rc<ActionRegistry>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -41,7 +41,7 @@ pub(crate) struct DispatchActionListener {
|
||||
}
|
||||
|
||||
impl DispatchTree {
|
||||
pub fn new(keymap: Arc<Mutex<Keymap>>) -> Self {
|
||||
pub fn new(keymap: Arc<Mutex<Keymap>>, action_registry: Rc<ActionRegistry>) -> Self {
|
||||
Self {
|
||||
node_stack: Vec::new(),
|
||||
context_stack: Vec::new(),
|
||||
@@ -49,6 +49,7 @@ impl DispatchTree {
|
||||
focusable_node_ids: HashMap::default(),
|
||||
keystroke_matchers: HashMap::default(),
|
||||
keymap,
|
||||
action_registry,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +61,7 @@ impl DispatchTree {
|
||||
self.keystroke_matchers.clear();
|
||||
}
|
||||
|
||||
pub fn push_node(&mut self, context: KeyContext, old_dispatcher: &mut Self) {
|
||||
pub fn push_node(&mut self, context: KeyContext) {
|
||||
let parent = self.node_stack.last().copied();
|
||||
let node_id = DispatchNodeId(self.nodes.len());
|
||||
self.nodes.push(DispatchNode {
|
||||
@@ -71,12 +72,6 @@ impl DispatchTree {
|
||||
if !context.is_empty() {
|
||||
self.active_node().context = context.clone();
|
||||
self.context_stack.push(context);
|
||||
if let Some((context_stack, matcher)) = old_dispatcher
|
||||
.keystroke_matchers
|
||||
.remove_entry(self.context_stack.as_slice())
|
||||
{
|
||||
self.keystroke_matchers.insert(context_stack, matcher);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +82,33 @@ impl DispatchTree {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_keystroke_matchers(&mut self) {
|
||||
self.keystroke_matchers.clear();
|
||||
}
|
||||
|
||||
/// Preserve keystroke matchers from previous frames to support multi-stroke
|
||||
/// bindings across multiple frames.
|
||||
pub fn preserve_keystroke_matchers(&mut self, old_tree: &mut Self, focus_id: Option<FocusId>) {
|
||||
if let Some(node_id) = focus_id.and_then(|focus_id| self.focusable_node_id(focus_id)) {
|
||||
let dispatch_path = self.dispatch_path(node_id);
|
||||
|
||||
self.context_stack.clear();
|
||||
for node_id in dispatch_path {
|
||||
let node = self.node(node_id);
|
||||
if !node.context.is_empty() {
|
||||
self.context_stack.push(node.context.clone());
|
||||
}
|
||||
|
||||
if let Some((context_stack, matcher)) = old_tree
|
||||
.keystroke_matchers
|
||||
.remove_entry(self.context_stack.as_slice())
|
||||
{
|
||||
self.keystroke_matchers.insert(context_stack, matcher);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_key_event(&mut self, listener: KeyListener) {
|
||||
self.active_node().key_listeners.push(listener);
|
||||
}
|
||||
@@ -132,7 +154,9 @@ impl DispatchTree {
|
||||
for node_id in self.dispatch_path(*node) {
|
||||
let node = &self.nodes[node_id.0];
|
||||
for DispatchActionListener { action_type, .. } in &node.action_listeners {
|
||||
actions.extend(build_action_from_type(action_type).log_err());
|
||||
// Intentionally silence these errors without logging.
|
||||
// If an action cannot be built by default, it's not available.
|
||||
actions.extend(self.action_registry.build_action_type(action_type).ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use crate::{KeyBinding, KeyBindingContextPredicate, Keystroke};
|
||||
use crate::{KeyBinding, KeyBindingContextPredicate, Keystroke, NoAction};
|
||||
use collections::HashSet;
|
||||
use smallvec::SmallVec;
|
||||
use std::{any::TypeId, collections::HashMap};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
collections::HashMap,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Default)]
|
||||
pub struct KeymapVersion(usize);
|
||||
@@ -37,20 +40,19 @@ impl Keymap {
|
||||
}
|
||||
|
||||
pub fn add_bindings<T: IntoIterator<Item = KeyBinding>>(&mut self, bindings: T) {
|
||||
// todo!("no action")
|
||||
// let no_action_id = (NoAction {}).id();
|
||||
let no_action_id = &(NoAction {}).type_id();
|
||||
let mut new_bindings = Vec::new();
|
||||
let has_new_disabled_keystrokes = false;
|
||||
let mut has_new_disabled_keystrokes = false;
|
||||
for binding in bindings {
|
||||
// if binding.action().id() == no_action_id {
|
||||
// has_new_disabled_keystrokes |= self
|
||||
// .disabled_keystrokes
|
||||
// .entry(binding.keystrokes)
|
||||
// .or_default()
|
||||
// .insert(binding.context_predicate);
|
||||
// } else {
|
||||
new_bindings.push(binding);
|
||||
// }
|
||||
if binding.action.type_id() == *no_action_id {
|
||||
has_new_disabled_keystrokes |= self
|
||||
.disabled_keystrokes
|
||||
.entry(binding.keystrokes)
|
||||
.or_default()
|
||||
.insert(binding.context_predicate);
|
||||
} else {
|
||||
new_bindings.push(binding);
|
||||
}
|
||||
}
|
||||
|
||||
if has_new_disabled_keystrokes {
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::{
|
||||
point, size, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId,
|
||||
FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, LineLayout,
|
||||
Pixels, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, Scene,
|
||||
SharedString, Size,
|
||||
SharedString, Size, TaskLabel,
|
||||
};
|
||||
use anyhow::{anyhow, bail};
|
||||
use async_task::Runnable;
|
||||
@@ -162,10 +162,10 @@ pub(crate) trait PlatformWindow {
|
||||
|
||||
pub trait PlatformDispatcher: Send + Sync {
|
||||
fn is_main_thread(&self) -> bool;
|
||||
fn dispatch(&self, runnable: Runnable);
|
||||
fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>);
|
||||
fn dispatch_on_main_thread(&self, runnable: Runnable);
|
||||
fn dispatch_after(&self, duration: Duration, runnable: Runnable);
|
||||
fn poll(&self, background_only: bool) -> bool;
|
||||
fn tick(&self, background_only: bool) -> bool;
|
||||
fn park(&self);
|
||||
fn unparker(&self) -> Unparker;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use crate::PlatformDispatcher;
|
||||
use crate::{PlatformDispatcher, TaskLabel};
|
||||
use async_task::Runnable;
|
||||
use objc::{
|
||||
class, msg_send,
|
||||
@@ -37,7 +37,7 @@ impl PlatformDispatcher for MacDispatcher {
|
||||
is_main_thread == YES
|
||||
}
|
||||
|
||||
fn dispatch(&self, runnable: Runnable) {
|
||||
fn dispatch(&self, runnable: Runnable, _: Option<TaskLabel>) {
|
||||
unsafe {
|
||||
dispatch_async_f(
|
||||
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.try_into().unwrap(), 0),
|
||||
@@ -71,7 +71,7 @@ impl PlatformDispatcher for MacDispatcher {
|
||||
}
|
||||
}
|
||||
|
||||
fn poll(&self, _background_only: bool) -> bool {
|
||||
fn tick(&self, _background_only: bool) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
@@ -343,10 +343,10 @@ impl MacTextSystemState {
|
||||
// Construct the attributed string, converting UTF8 ranges to UTF16 ranges.
|
||||
let mut string = CFMutableAttributedString::new();
|
||||
{
|
||||
string.replace_str(&CFString::new(text), CFRange::init(0, 0));
|
||||
string.replace_str(&CFString::new(text.as_ref()), CFRange::init(0, 0));
|
||||
let utf16_line_len = string.char_len() as usize;
|
||||
|
||||
let mut ix_converter = StringIndexConverter::new(text);
|
||||
let mut ix_converter = StringIndexConverter::new(text.as_ref());
|
||||
for run in font_runs {
|
||||
let utf8_end = ix_converter.utf8_ix + run.len;
|
||||
let utf16_start = ix_converter.utf16_ix;
|
||||
@@ -390,7 +390,7 @@ impl MacTextSystemState {
|
||||
};
|
||||
let font_id = self.id_for_native_font(font);
|
||||
|
||||
let mut ix_converter = StringIndexConverter::new(text);
|
||||
let mut ix_converter = StringIndexConverter::new(text.as_ref());
|
||||
let mut glyphs = SmallVec::new();
|
||||
for ((glyph_id, position), glyph_utf16_ix) in run
|
||||
.glyphs()
|
||||
@@ -413,11 +413,11 @@ impl MacTextSystemState {
|
||||
|
||||
let typographic_bounds = line.get_typographic_bounds();
|
||||
LineLayout {
|
||||
runs,
|
||||
font_size,
|
||||
width: typographic_bounds.width.into(),
|
||||
ascent: typographic_bounds.ascent.into(),
|
||||
descent: typographic_bounds.descent.into(),
|
||||
runs,
|
||||
font_size,
|
||||
len: text.len(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1141,7 +1141,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
|
||||
let event = unsafe { InputEvent::from_native(native_event, Some(window_height)) };
|
||||
|
||||
if let Some(mut event) = event {
|
||||
let synthesized_second_event = match &mut event {
|
||||
match &mut event {
|
||||
InputEvent::MouseDown(
|
||||
event @ MouseDownEvent {
|
||||
button: MouseButton::Left,
|
||||
@@ -1149,6 +1149,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
|
||||
..
|
||||
},
|
||||
) => {
|
||||
// On mac, a ctrl-left click should be handled as a right click.
|
||||
*event = MouseDownEvent {
|
||||
button: MouseButton::Right,
|
||||
modifiers: Modifiers {
|
||||
@@ -1158,26 +1159,30 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
|
||||
click_count: 1,
|
||||
..*event
|
||||
};
|
||||
|
||||
Some(InputEvent::MouseDown(MouseDownEvent {
|
||||
button: MouseButton::Right,
|
||||
..*event
|
||||
}))
|
||||
}
|
||||
|
||||
// Because we map a ctrl-left_down to a right_down -> right_up let's ignore
|
||||
// the ctrl-left_up to avoid having a mismatch in button down/up events if the
|
||||
// user is still holding ctrl when releasing the left mouse button
|
||||
InputEvent::MouseUp(MouseUpEvent {
|
||||
button: MouseButton::Left,
|
||||
modifiers: Modifiers { control: true, .. },
|
||||
..
|
||||
}) => {
|
||||
lock.synthetic_drag_counter += 1;
|
||||
return;
|
||||
InputEvent::MouseUp(
|
||||
event @ MouseUpEvent {
|
||||
button: MouseButton::Left,
|
||||
modifiers: Modifiers { control: true, .. },
|
||||
..
|
||||
},
|
||||
) => {
|
||||
*event = MouseUpEvent {
|
||||
button: MouseButton::Right,
|
||||
modifiers: Modifiers {
|
||||
control: false,
|
||||
..event.modifiers
|
||||
},
|
||||
click_count: 1,
|
||||
..*event
|
||||
};
|
||||
}
|
||||
|
||||
_ => None,
|
||||
_ => {}
|
||||
};
|
||||
|
||||
match &event {
|
||||
@@ -1227,9 +1232,6 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
|
||||
if let Some(mut callback) = lock.event_callback.take() {
|
||||
drop(lock);
|
||||
callback(event);
|
||||
if let Some(event) = synthesized_second_event {
|
||||
callback(event);
|
||||
}
|
||||
window_state.lock().event_callback = Some(callback);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::PlatformDispatcher;
|
||||
use crate::{PlatformDispatcher, TaskLabel};
|
||||
use async_task::Runnable;
|
||||
use backtrace::Backtrace;
|
||||
use collections::{HashMap, VecDeque};
|
||||
use collections::{HashMap, HashSet, VecDeque};
|
||||
use parking::{Parker, Unparker};
|
||||
use parking_lot::Mutex;
|
||||
use rand::prelude::*;
|
||||
@@ -28,12 +28,14 @@ struct TestDispatcherState {
|
||||
random: StdRng,
|
||||
foreground: HashMap<TestDispatcherId, VecDeque<Runnable>>,
|
||||
background: Vec<Runnable>,
|
||||
deprioritized_background: Vec<Runnable>,
|
||||
delayed: Vec<(Duration, Runnable)>,
|
||||
time: Duration,
|
||||
is_main_thread: bool,
|
||||
next_id: TestDispatcherId,
|
||||
allow_parking: bool,
|
||||
waiting_backtrace: Option<Backtrace>,
|
||||
deprioritized_task_labels: HashSet<TaskLabel>,
|
||||
}
|
||||
|
||||
impl TestDispatcher {
|
||||
@@ -43,12 +45,14 @@ impl TestDispatcher {
|
||||
random,
|
||||
foreground: HashMap::default(),
|
||||
background: Vec::new(),
|
||||
deprioritized_background: Vec::new(),
|
||||
delayed: Vec::new(),
|
||||
time: Duration::ZERO,
|
||||
is_main_thread: true,
|
||||
next_id: TestDispatcherId(1),
|
||||
allow_parking: false,
|
||||
waiting_backtrace: None,
|
||||
deprioritized_task_labels: Default::default(),
|
||||
};
|
||||
|
||||
TestDispatcher {
|
||||
@@ -101,8 +105,15 @@ impl TestDispatcher {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deprioritize(&self, task_label: TaskLabel) {
|
||||
self.state
|
||||
.lock()
|
||||
.deprioritized_task_labels
|
||||
.insert(task_label);
|
||||
}
|
||||
|
||||
pub fn run_until_parked(&self) {
|
||||
while self.poll(false) {}
|
||||
while self.tick(false) {}
|
||||
}
|
||||
|
||||
pub fn parking_allowed(&self) -> bool {
|
||||
@@ -150,8 +161,17 @@ impl PlatformDispatcher for TestDispatcher {
|
||||
self.state.lock().is_main_thread
|
||||
}
|
||||
|
||||
fn dispatch(&self, runnable: Runnable) {
|
||||
self.state.lock().background.push(runnable);
|
||||
fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>) {
|
||||
{
|
||||
let mut state = self.state.lock();
|
||||
if label.map_or(false, |label| {
|
||||
state.deprioritized_task_labels.contains(&label)
|
||||
}) {
|
||||
state.deprioritized_background.push(runnable);
|
||||
} else {
|
||||
state.background.push(runnable);
|
||||
}
|
||||
}
|
||||
self.unparker.unpark();
|
||||
}
|
||||
|
||||
@@ -174,7 +194,7 @@ impl PlatformDispatcher for TestDispatcher {
|
||||
state.delayed.insert(ix, (next_time, runnable));
|
||||
}
|
||||
|
||||
fn poll(&self, background_only: bool) -> bool {
|
||||
fn tick(&self, background_only: bool) -> bool {
|
||||
let mut state = self.state.lock();
|
||||
|
||||
while let Some((deadline, _)) = state.delayed.first() {
|
||||
@@ -196,34 +216,41 @@ impl PlatformDispatcher for TestDispatcher {
|
||||
};
|
||||
let background_len = state.background.len();
|
||||
|
||||
let runnable;
|
||||
let main_thread;
|
||||
if foreground_len == 0 && background_len == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let main_thread = state.random.gen_ratio(
|
||||
foreground_len as u32,
|
||||
(foreground_len + background_len) as u32,
|
||||
);
|
||||
let was_main_thread = state.is_main_thread;
|
||||
state.is_main_thread = main_thread;
|
||||
|
||||
let runnable = if main_thread {
|
||||
let state = &mut *state;
|
||||
let runnables = state
|
||||
.foreground
|
||||
.values_mut()
|
||||
.filter(|runnables| !runnables.is_empty())
|
||||
.choose(&mut state.random)
|
||||
.unwrap();
|
||||
runnables.pop_front().unwrap()
|
||||
let deprioritized_background_len = state.deprioritized_background.len();
|
||||
if deprioritized_background_len == 0 {
|
||||
return false;
|
||||
}
|
||||
let ix = state.random.gen_range(0..deprioritized_background_len);
|
||||
main_thread = false;
|
||||
runnable = state.deprioritized_background.swap_remove(ix);
|
||||
} else {
|
||||
let ix = state.random.gen_range(0..background_len);
|
||||
state.background.swap_remove(ix)
|
||||
main_thread = state.random.gen_ratio(
|
||||
foreground_len as u32,
|
||||
(foreground_len + background_len) as u32,
|
||||
);
|
||||
if main_thread {
|
||||
let state = &mut *state;
|
||||
runnable = state
|
||||
.foreground
|
||||
.values_mut()
|
||||
.filter(|runnables| !runnables.is_empty())
|
||||
.choose(&mut state.random)
|
||||
.unwrap()
|
||||
.pop_front()
|
||||
.unwrap();
|
||||
} else {
|
||||
let ix = state.random.gen_range(0..background_len);
|
||||
runnable = state.background.swap_remove(ix);
|
||||
};
|
||||
};
|
||||
|
||||
let was_main_thread = state.is_main_thread;
|
||||
state.is_main_thread = main_thread;
|
||||
drop(state);
|
||||
runnable.run();
|
||||
|
||||
self.state.lock().is_main_thread = was_main_thread;
|
||||
|
||||
true
|
||||
|
||||
@@ -203,6 +203,7 @@ impl TextStyle {
|
||||
style: self.font_style,
|
||||
},
|
||||
color: self.color,
|
||||
background_color: None,
|
||||
underline: self.underline.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,20 +3,20 @@ mod line;
|
||||
mod line_layout;
|
||||
mod line_wrapper;
|
||||
|
||||
use anyhow::anyhow;
|
||||
pub use font_features::*;
|
||||
pub use line::*;
|
||||
pub use line_layout::*;
|
||||
pub use line_wrapper::*;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{
|
||||
px, Bounds, DevicePixels, Hsla, Pixels, PlatformTextSystem, Point, Result, SharedString, Size,
|
||||
UnderlineStyle,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use collections::HashMap;
|
||||
use core::fmt;
|
||||
use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
cmp,
|
||||
fmt::{Debug, Display, Formatter},
|
||||
@@ -151,13 +151,79 @@ impl TextSystem {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn layout_text(
|
||||
pub fn layout_line(
|
||||
&self,
|
||||
text: &str,
|
||||
font_size: Pixels,
|
||||
runs: &[TextRun],
|
||||
) -> Result<Arc<LineLayout>> {
|
||||
let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
|
||||
for run in runs.iter() {
|
||||
let font_id = self.font_id(&run.font)?;
|
||||
if let Some(last_run) = font_runs.last_mut() {
|
||||
if last_run.font_id == font_id {
|
||||
last_run.len += run.len;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
font_runs.push(FontRun {
|
||||
len: run.len,
|
||||
font_id,
|
||||
});
|
||||
}
|
||||
|
||||
let layout = self
|
||||
.line_layout_cache
|
||||
.layout_line(&text, font_size, &font_runs);
|
||||
|
||||
font_runs.clear();
|
||||
self.font_runs_pool.lock().push(font_runs);
|
||||
|
||||
Ok(layout)
|
||||
}
|
||||
|
||||
pub fn shape_line(
|
||||
&self,
|
||||
text: SharedString,
|
||||
font_size: Pixels,
|
||||
runs: &[TextRun],
|
||||
) -> Result<ShapedLine> {
|
||||
debug_assert!(
|
||||
text.find('\n').is_none(),
|
||||
"text argument should not contain newlines"
|
||||
);
|
||||
|
||||
let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
|
||||
for run in runs {
|
||||
if let Some(last_run) = decoration_runs.last_mut() {
|
||||
if last_run.color == run.color && last_run.underline == run.underline {
|
||||
last_run.len += run.len as u32;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
decoration_runs.push(DecorationRun {
|
||||
len: run.len as u32,
|
||||
color: run.color,
|
||||
underline: run.underline.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
let layout = self.layout_line(text.as_ref(), font_size, runs)?;
|
||||
|
||||
Ok(ShapedLine {
|
||||
layout,
|
||||
text,
|
||||
decoration_runs,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn shape_text(
|
||||
&self,
|
||||
text: &str, // todo!("pass a SharedString and preserve it when passed a single line?")
|
||||
font_size: Pixels,
|
||||
runs: &[TextRun],
|
||||
wrap_width: Option<Pixels>,
|
||||
) -> Result<SmallVec<[Line; 1]>> {
|
||||
) -> Result<SmallVec<[WrappedLine; 1]>> {
|
||||
let mut runs = runs.iter().cloned().peekable();
|
||||
let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
|
||||
|
||||
@@ -210,10 +276,11 @@ impl TextSystem {
|
||||
|
||||
let layout = self
|
||||
.line_layout_cache
|
||||
.layout_line(&line_text, font_size, &font_runs, wrap_width);
|
||||
lines.push(Line {
|
||||
.layout_wrapped_line(&line_text, font_size, &font_runs, wrap_width);
|
||||
lines.push(WrappedLine {
|
||||
layout,
|
||||
decorations: decoration_runs,
|
||||
decoration_runs,
|
||||
text: SharedString::from(line_text),
|
||||
});
|
||||
|
||||
line_start = line_end + 1; // Skip `\n` character.
|
||||
@@ -384,6 +451,7 @@ pub struct TextRun {
|
||||
pub len: usize,
|
||||
pub font: Font,
|
||||
pub color: Hsla,
|
||||
pub background_color: Option<Hsla>,
|
||||
pub underline: Option<UnderlineStyle>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
black, point, px, size, BorrowWindow, Bounds, Hsla, Pixels, Point, Result, Size,
|
||||
black, point, px, BorrowWindow, Bounds, Hsla, LineLayout, Pixels, Point, Result, SharedString,
|
||||
UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout,
|
||||
};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
@@ -14,23 +14,17 @@ pub struct DecorationRun {
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, Deref, DerefMut)]
|
||||
pub struct Line {
|
||||
pub struct ShapedLine {
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
pub(crate) layout: Arc<WrappedLineLayout>,
|
||||
pub(crate) decorations: SmallVec<[DecorationRun; 32]>,
|
||||
pub(crate) layout: Arc<LineLayout>,
|
||||
pub text: SharedString,
|
||||
pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>,
|
||||
}
|
||||
|
||||
impl Line {
|
||||
pub fn size(&self, line_height: Pixels) -> Size<Pixels> {
|
||||
size(
|
||||
self.layout.width,
|
||||
line_height * (self.layout.wrap_boundaries.len() + 1),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn wrap_count(&self) -> usize {
|
||||
self.layout.wrap_boundaries.len()
|
||||
impl ShapedLine {
|
||||
pub fn len(&self) -> usize {
|
||||
self.layout.len
|
||||
}
|
||||
|
||||
pub fn paint(
|
||||
@@ -39,75 +33,84 @@ impl Line {
|
||||
line_height: Pixels,
|
||||
cx: &mut WindowContext,
|
||||
) -> Result<()> {
|
||||
let padding_top =
|
||||
(line_height - self.layout.layout.ascent - self.layout.layout.descent) / 2.;
|
||||
let baseline_offset = point(px(0.), padding_top + self.layout.layout.ascent);
|
||||
paint_line(
|
||||
origin,
|
||||
&self.layout,
|
||||
line_height,
|
||||
&self.decoration_runs,
|
||||
None,
|
||||
&[],
|
||||
cx,
|
||||
)?;
|
||||
|
||||
let mut style_runs = self.decorations.iter();
|
||||
let mut wraps = self.layout.wrap_boundaries.iter().peekable();
|
||||
let mut run_end = 0;
|
||||
let mut color = black();
|
||||
let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
|
||||
let text_system = cx.text_system().clone();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
let mut glyph_origin = origin;
|
||||
let mut prev_glyph_position = Point::default();
|
||||
for (run_ix, run) in self.layout.layout.runs.iter().enumerate() {
|
||||
let max_glyph_size = text_system
|
||||
.bounding_box(run.font_id, self.layout.layout.font_size)?
|
||||
.size;
|
||||
#[derive(Clone, Default, Debug, Deref, DerefMut)]
|
||||
pub struct WrappedLine {
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
pub(crate) layout: Arc<WrappedLineLayout>,
|
||||
pub text: SharedString,
|
||||
pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>,
|
||||
}
|
||||
|
||||
for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
|
||||
glyph_origin.x += glyph.position.x - prev_glyph_position.x;
|
||||
impl WrappedLine {
|
||||
pub fn len(&self) -> usize {
|
||||
self.layout.len()
|
||||
}
|
||||
|
||||
if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
|
||||
wraps.next();
|
||||
if let Some((underline_origin, underline_style)) = current_underline.take() {
|
||||
cx.paint_underline(
|
||||
underline_origin,
|
||||
glyph_origin.x - underline_origin.x,
|
||||
&underline_style,
|
||||
)?;
|
||||
}
|
||||
pub fn paint(
|
||||
&self,
|
||||
origin: Point<Pixels>,
|
||||
line_height: Pixels,
|
||||
cx: &mut WindowContext,
|
||||
) -> Result<()> {
|
||||
paint_line(
|
||||
origin,
|
||||
&self.layout.unwrapped_layout,
|
||||
line_height,
|
||||
&self.decoration_runs,
|
||||
self.wrap_width,
|
||||
&self.wrap_boundaries,
|
||||
cx,
|
||||
)?;
|
||||
|
||||
glyph_origin.x = origin.x;
|
||||
glyph_origin.y += line_height;
|
||||
}
|
||||
prev_glyph_position = glyph.position;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
|
||||
if glyph.index >= run_end {
|
||||
if let Some(style_run) = style_runs.next() {
|
||||
if let Some((_, underline_style)) = &mut current_underline {
|
||||
if style_run.underline.as_ref() != Some(underline_style) {
|
||||
finished_underline = current_underline.take();
|
||||
}
|
||||
}
|
||||
if let Some(run_underline) = style_run.underline.as_ref() {
|
||||
current_underline.get_or_insert((
|
||||
point(
|
||||
glyph_origin.x,
|
||||
origin.y
|
||||
+ baseline_offset.y
|
||||
+ (self.layout.layout.descent * 0.618),
|
||||
),
|
||||
UnderlineStyle {
|
||||
color: Some(run_underline.color.unwrap_or(style_run.color)),
|
||||
thickness: run_underline.thickness,
|
||||
wavy: run_underline.wavy,
|
||||
},
|
||||
));
|
||||
}
|
||||
fn paint_line(
|
||||
origin: Point<Pixels>,
|
||||
layout: &LineLayout,
|
||||
line_height: Pixels,
|
||||
decoration_runs: &[DecorationRun],
|
||||
wrap_width: Option<Pixels>,
|
||||
wrap_boundaries: &[WrapBoundary],
|
||||
cx: &mut WindowContext<'_>,
|
||||
) -> Result<()> {
|
||||
let padding_top = (line_height - layout.ascent - layout.descent) / 2.;
|
||||
let baseline_offset = point(px(0.), padding_top + layout.ascent);
|
||||
let mut decoration_runs = decoration_runs.iter();
|
||||
let mut wraps = wrap_boundaries.iter().peekable();
|
||||
let mut run_end = 0;
|
||||
let mut color = black();
|
||||
let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
|
||||
let text_system = cx.text_system().clone();
|
||||
let mut glyph_origin = origin;
|
||||
let mut prev_glyph_position = Point::default();
|
||||
for (run_ix, run) in layout.runs.iter().enumerate() {
|
||||
let max_glyph_size = text_system
|
||||
.bounding_box(run.font_id, layout.font_size)?
|
||||
.size;
|
||||
|
||||
run_end += style_run.len as usize;
|
||||
color = style_run.color;
|
||||
} else {
|
||||
run_end = self.layout.text.len();
|
||||
finished_underline = current_underline.take();
|
||||
}
|
||||
}
|
||||
for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
|
||||
glyph_origin.x += glyph.position.x - prev_glyph_position.x;
|
||||
|
||||
if let Some((underline_origin, underline_style)) = finished_underline {
|
||||
if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
|
||||
wraps.next();
|
||||
if let Some((underline_origin, underline_style)) = current_underline.take() {
|
||||
cx.paint_underline(
|
||||
underline_origin,
|
||||
glyph_origin.x - underline_origin.x,
|
||||
@@ -115,42 +118,84 @@ impl Line {
|
||||
)?;
|
||||
}
|
||||
|
||||
let max_glyph_bounds = Bounds {
|
||||
origin: glyph_origin,
|
||||
size: max_glyph_size,
|
||||
};
|
||||
glyph_origin.x = origin.x;
|
||||
glyph_origin.y += line_height;
|
||||
}
|
||||
prev_glyph_position = glyph.position;
|
||||
|
||||
let content_mask = cx.content_mask();
|
||||
if max_glyph_bounds.intersects(&content_mask.bounds) {
|
||||
if glyph.is_emoji {
|
||||
cx.paint_emoji(
|
||||
glyph_origin + baseline_offset,
|
||||
run.font_id,
|
||||
glyph.id,
|
||||
self.layout.layout.font_size,
|
||||
)?;
|
||||
} else {
|
||||
cx.paint_glyph(
|
||||
glyph_origin + baseline_offset,
|
||||
run.font_id,
|
||||
glyph.id,
|
||||
self.layout.layout.font_size,
|
||||
color,
|
||||
)?;
|
||||
let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
|
||||
if glyph.index >= run_end {
|
||||
if let Some(style_run) = decoration_runs.next() {
|
||||
if let Some((_, underline_style)) = &mut current_underline {
|
||||
if style_run.underline.as_ref() != Some(underline_style) {
|
||||
finished_underline = current_underline.take();
|
||||
}
|
||||
}
|
||||
if let Some(run_underline) = style_run.underline.as_ref() {
|
||||
current_underline.get_or_insert((
|
||||
point(
|
||||
glyph_origin.x,
|
||||
origin.y + baseline_offset.y + (layout.descent * 0.618),
|
||||
),
|
||||
UnderlineStyle {
|
||||
color: Some(run_underline.color.unwrap_or(style_run.color)),
|
||||
thickness: run_underline.thickness,
|
||||
wavy: run_underline.wavy,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
run_end += style_run.len as usize;
|
||||
color = style_run.color;
|
||||
} else {
|
||||
run_end = layout.len;
|
||||
finished_underline = current_underline.take();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((underline_origin, underline_style)) = finished_underline {
|
||||
cx.paint_underline(
|
||||
underline_origin,
|
||||
glyph_origin.x - underline_origin.x,
|
||||
&underline_style,
|
||||
)?;
|
||||
}
|
||||
|
||||
let max_glyph_bounds = Bounds {
|
||||
origin: glyph_origin,
|
||||
size: max_glyph_size,
|
||||
};
|
||||
|
||||
let content_mask = cx.content_mask();
|
||||
if max_glyph_bounds.intersects(&content_mask.bounds) {
|
||||
if glyph.is_emoji {
|
||||
cx.paint_emoji(
|
||||
glyph_origin + baseline_offset,
|
||||
run.font_id,
|
||||
glyph.id,
|
||||
layout.font_size,
|
||||
)?;
|
||||
} else {
|
||||
cx.paint_glyph(
|
||||
glyph_origin + baseline_offset,
|
||||
run.font_id,
|
||||
glyph.id,
|
||||
layout.font_size,
|
||||
color,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((underline_start, underline_style)) = current_underline.take() {
|
||||
let line_end_x = origin.x + self.layout.layout.width;
|
||||
cx.paint_underline(
|
||||
underline_start,
|
||||
line_end_x - underline_start.x,
|
||||
&underline_style,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
if let Some((underline_start, underline_style)) = current_underline.take() {
|
||||
let line_end_x = origin.x + wrap_width.unwrap_or(Pixels::MAX).min(layout.width);
|
||||
cx.paint_underline(
|
||||
underline_start,
|
||||
line_end_x - underline_start.x,
|
||||
&underline_style,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::{px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, SharedString};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use crate::{px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, Size};
|
||||
use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
@@ -149,13 +148,11 @@ impl LineLayout {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deref, DerefMut, Default, Debug)]
|
||||
#[derive(Default, Debug)]
|
||||
pub struct WrappedLineLayout {
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
pub layout: LineLayout,
|
||||
pub text: SharedString,
|
||||
pub unwrapped_layout: Arc<LineLayout>,
|
||||
pub wrap_boundaries: SmallVec<[WrapBoundary; 1]>,
|
||||
pub wrap_width: Option<Pixels>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
@@ -164,31 +161,74 @@ pub struct WrapBoundary {
|
||||
pub glyph_ix: usize,
|
||||
}
|
||||
|
||||
impl WrappedLineLayout {
|
||||
pub fn len(&self) -> usize {
|
||||
self.unwrapped_layout.len
|
||||
}
|
||||
|
||||
pub fn width(&self) -> Pixels {
|
||||
self.wrap_width
|
||||
.unwrap_or(Pixels::MAX)
|
||||
.min(self.unwrapped_layout.width)
|
||||
}
|
||||
|
||||
pub fn size(&self, line_height: Pixels) -> Size<Pixels> {
|
||||
Size {
|
||||
width: self.width(),
|
||||
height: line_height * (self.wrap_boundaries.len() + 1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ascent(&self) -> Pixels {
|
||||
self.unwrapped_layout.ascent
|
||||
}
|
||||
|
||||
pub fn descent(&self) -> Pixels {
|
||||
self.unwrapped_layout.descent
|
||||
}
|
||||
|
||||
pub fn wrap_boundaries(&self) -> &[WrapBoundary] {
|
||||
&self.wrap_boundaries
|
||||
}
|
||||
|
||||
pub fn font_size(&self) -> Pixels {
|
||||
self.unwrapped_layout.font_size
|
||||
}
|
||||
|
||||
pub fn runs(&self) -> &[ShapedRun] {
|
||||
&self.unwrapped_layout.runs
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct LineLayoutCache {
|
||||
prev_frame: Mutex<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
|
||||
curr_frame: RwLock<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
|
||||
previous_frame: Mutex<HashMap<CacheKey, Arc<LineLayout>>>,
|
||||
current_frame: RwLock<HashMap<CacheKey, Arc<LineLayout>>>,
|
||||
previous_frame_wrapped: Mutex<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
|
||||
current_frame_wrapped: RwLock<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
|
||||
platform_text_system: Arc<dyn PlatformTextSystem>,
|
||||
}
|
||||
|
||||
impl LineLayoutCache {
|
||||
pub fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
|
||||
Self {
|
||||
prev_frame: Mutex::new(HashMap::new()),
|
||||
curr_frame: RwLock::new(HashMap::new()),
|
||||
previous_frame: Mutex::default(),
|
||||
current_frame: RwLock::default(),
|
||||
previous_frame_wrapped: Mutex::default(),
|
||||
current_frame_wrapped: RwLock::default(),
|
||||
platform_text_system,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_frame(&self) {
|
||||
let mut prev_frame = self.prev_frame.lock();
|
||||
let mut curr_frame = self.curr_frame.write();
|
||||
let mut prev_frame = self.previous_frame.lock();
|
||||
let mut curr_frame = self.current_frame.write();
|
||||
std::mem::swap(&mut *prev_frame, &mut *curr_frame);
|
||||
curr_frame.clear();
|
||||
}
|
||||
|
||||
pub fn layout_line(
|
||||
pub fn layout_wrapped_line(
|
||||
&self,
|
||||
text: &SharedString,
|
||||
text: &str,
|
||||
font_size: Pixels,
|
||||
runs: &[FontRun],
|
||||
wrap_width: Option<Pixels>,
|
||||
@@ -199,34 +239,66 @@ impl LineLayoutCache {
|
||||
runs,
|
||||
wrap_width,
|
||||
} as &dyn AsCacheKeyRef;
|
||||
let curr_frame = self.curr_frame.upgradable_read();
|
||||
if let Some(layout) = curr_frame.get(key) {
|
||||
|
||||
let current_frame = self.current_frame_wrapped.upgradable_read();
|
||||
if let Some(layout) = current_frame.get(key) {
|
||||
return layout.clone();
|
||||
}
|
||||
|
||||
let mut curr_frame = RwLockUpgradableReadGuard::upgrade(curr_frame);
|
||||
if let Some((key, layout)) = self.prev_frame.lock().remove_entry(key) {
|
||||
curr_frame.insert(key, layout.clone());
|
||||
let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
|
||||
if let Some((key, layout)) = self.previous_frame_wrapped.lock().remove_entry(key) {
|
||||
current_frame.insert(key, layout.clone());
|
||||
layout
|
||||
} else {
|
||||
let layout = self.platform_text_system.layout_line(text, font_size, runs);
|
||||
let wrap_boundaries = wrap_width
|
||||
.map(|wrap_width| layout.compute_wrap_boundaries(text.as_ref(), wrap_width))
|
||||
.unwrap_or_default();
|
||||
let wrapped_line = Arc::new(WrappedLineLayout {
|
||||
layout,
|
||||
text: text.clone(),
|
||||
let unwrapped_layout = self.layout_line(text, font_size, runs);
|
||||
let wrap_boundaries = if let Some(wrap_width) = wrap_width {
|
||||
unwrapped_layout.compute_wrap_boundaries(text.as_ref(), wrap_width)
|
||||
} else {
|
||||
SmallVec::new()
|
||||
};
|
||||
let layout = Arc::new(WrappedLineLayout {
|
||||
unwrapped_layout,
|
||||
wrap_boundaries,
|
||||
wrap_width,
|
||||
});
|
||||
|
||||
let key = CacheKey {
|
||||
text: text.clone(),
|
||||
text: text.into(),
|
||||
font_size,
|
||||
runs: SmallVec::from(runs),
|
||||
wrap_width,
|
||||
};
|
||||
curr_frame.insert(key, wrapped_line.clone());
|
||||
wrapped_line
|
||||
current_frame.insert(key, layout.clone());
|
||||
layout
|
||||
}
|
||||
}
|
||||
|
||||
pub fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> Arc<LineLayout> {
|
||||
let key = &CacheKeyRef {
|
||||
text,
|
||||
font_size,
|
||||
runs,
|
||||
wrap_width: None,
|
||||
} as &dyn AsCacheKeyRef;
|
||||
|
||||
let current_frame = self.current_frame.upgradable_read();
|
||||
if let Some(layout) = current_frame.get(key) {
|
||||
return layout.clone();
|
||||
}
|
||||
|
||||
let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
|
||||
if let Some((key, layout)) = self.previous_frame.lock().remove_entry(key) {
|
||||
current_frame.insert(key, layout.clone());
|
||||
layout
|
||||
} else {
|
||||
let layout = Arc::new(self.platform_text_system.layout_line(text, font_size, runs));
|
||||
let key = CacheKey {
|
||||
text: text.into(),
|
||||
font_size,
|
||||
runs: SmallVec::from(runs),
|
||||
wrap_width: None,
|
||||
};
|
||||
current_frame.insert(key, layout.clone());
|
||||
layout
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -243,7 +315,7 @@ trait AsCacheKeyRef {
|
||||
|
||||
#[derive(Eq)]
|
||||
struct CacheKey {
|
||||
text: SharedString,
|
||||
text: String,
|
||||
font_size: Pixels,
|
||||
runs: SmallVec<[FontRun; 1]>,
|
||||
wrap_width: Option<Pixels>,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::{
|
||||
private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace,
|
||||
BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, LayoutId,
|
||||
Model, Pixels, Size, ViewContext, VisualContext, WeakModel, WindowContext,
|
||||
BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, FocusHandle,
|
||||
FocusableView, LayoutId, Model, Pixels, Point, Size, ViewContext, VisualContext, WeakModel,
|
||||
WindowContext,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use std::{
|
||||
@@ -63,6 +64,23 @@ impl<V: 'static> View<V> {
|
||||
pub fn read<'a>(&self, cx: &'a AppContext) -> &'a V {
|
||||
self.model.read(cx)
|
||||
}
|
||||
|
||||
pub fn render_with<C>(&self, component: C) -> RenderViewWith<C, V>
|
||||
where
|
||||
C: 'static + Component<V>,
|
||||
{
|
||||
RenderViewWith {
|
||||
view: self.clone(),
|
||||
component: Some(component),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focus_handle(&self, cx: &AppContext) -> FocusHandle
|
||||
where
|
||||
V: FocusableView,
|
||||
{
|
||||
self.read(cx).focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Clone for View<V> {
|
||||
@@ -145,8 +163,7 @@ impl<V> Eq for WeakView<V> {}
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AnyView {
|
||||
model: AnyModel,
|
||||
initialize: fn(&AnyView, &mut WindowContext) -> AnyBox,
|
||||
layout: fn(&AnyView, &mut AnyBox, &mut WindowContext) -> LayoutId,
|
||||
layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box<dyn Any>),
|
||||
paint: fn(&AnyView, &mut AnyBox, &mut WindowContext),
|
||||
}
|
||||
|
||||
@@ -154,7 +171,6 @@ impl AnyView {
|
||||
pub fn downgrade(&self) -> AnyWeakView {
|
||||
AnyWeakView {
|
||||
model: self.model.downgrade(),
|
||||
initialize: self.initialize,
|
||||
layout: self.layout,
|
||||
paint: self.paint,
|
||||
}
|
||||
@@ -165,7 +181,6 @@ impl AnyView {
|
||||
Ok(model) => Ok(View { model }),
|
||||
Err(model) => Err(Self {
|
||||
model,
|
||||
initialize: self.initialize,
|
||||
layout: self.layout,
|
||||
paint: self.paint,
|
||||
}),
|
||||
@@ -176,13 +191,19 @@ impl AnyView {
|
||||
self.model.entity_type
|
||||
}
|
||||
|
||||
pub(crate) fn draw(&self, available_space: Size<AvailableSpace>, cx: &mut WindowContext) {
|
||||
let mut rendered_element = (self.initialize)(self, cx);
|
||||
let layout_id = (self.layout)(self, &mut rendered_element, cx);
|
||||
cx.window
|
||||
.layout_engine
|
||||
.compute_layout(layout_id, available_space);
|
||||
(self.paint)(self, &mut rendered_element, cx);
|
||||
pub(crate) fn draw(
|
||||
&self,
|
||||
origin: Point<Pixels>,
|
||||
available_space: Size<AvailableSpace>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
cx.with_absolute_element_offset(origin, |cx| {
|
||||
let (layout_id, mut rendered_element) = (self.layout)(self, cx);
|
||||
cx.window
|
||||
.layout_engine
|
||||
.compute_layout(layout_id, available_space);
|
||||
(self.paint)(self, &mut rendered_element, cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +217,6 @@ impl<V: Render> From<View<V>> for AnyView {
|
||||
fn from(value: View<V>) -> Self {
|
||||
AnyView {
|
||||
model: value.model.into_any(),
|
||||
initialize: any_view::initialize::<V>,
|
||||
layout: any_view::layout::<V>,
|
||||
paint: any_view::paint::<V>,
|
||||
}
|
||||
@@ -210,22 +230,13 @@ impl<ParentViewState: 'static> Element<ParentViewState> for AnyView {
|
||||
Some(self.model.entity_id.into())
|
||||
}
|
||||
|
||||
fn initialize(
|
||||
fn layout(
|
||||
&mut self,
|
||||
_view_state: &mut ParentViewState,
|
||||
_element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<ParentViewState>,
|
||||
) -> Self::ElementState {
|
||||
(self.initialize)(self, cx)
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_view_state: &mut ParentViewState,
|
||||
rendered_element: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<ParentViewState>,
|
||||
) -> LayoutId {
|
||||
(self.layout)(self, rendered_element, cx)
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
(self.layout)(self, cx)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
@@ -241,8 +252,7 @@ impl<ParentViewState: 'static> Element<ParentViewState> for AnyView {
|
||||
|
||||
pub struct AnyWeakView {
|
||||
model: AnyWeakModel,
|
||||
initialize: fn(&AnyView, &mut WindowContext) -> AnyBox,
|
||||
layout: fn(&AnyView, &mut AnyBox, &mut WindowContext) -> LayoutId,
|
||||
layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box<dyn Any>),
|
||||
paint: fn(&AnyView, &mut AnyBox, &mut WindowContext),
|
||||
}
|
||||
|
||||
@@ -251,7 +261,6 @@ impl AnyWeakView {
|
||||
let model = self.model.upgrade()?;
|
||||
Some(AnyView {
|
||||
model,
|
||||
initialize: self.initialize,
|
||||
layout: self.layout,
|
||||
paint: self.paint,
|
||||
})
|
||||
@@ -262,7 +271,6 @@ impl<V: Render> From<WeakView<V>> for AnyWeakView {
|
||||
fn from(view: WeakView<V>) -> Self {
|
||||
Self {
|
||||
model: view.model.into(),
|
||||
initialize: any_view::initialize::<V>,
|
||||
layout: any_view::layout::<V>,
|
||||
paint: any_view::paint::<V>,
|
||||
}
|
||||
@@ -281,12 +289,12 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RenderView<C, V> {
|
||||
pub struct RenderViewWith<C, V> {
|
||||
view: View<V>,
|
||||
component: Option<C>,
|
||||
}
|
||||
|
||||
impl<C, ParentViewState, ViewState> Component<ParentViewState> for RenderView<C, ViewState>
|
||||
impl<C, ParentViewState, ViewState> Component<ParentViewState> for RenderViewWith<C, ViewState>
|
||||
where
|
||||
C: 'static + Component<ViewState>,
|
||||
ParentViewState: 'static,
|
||||
@@ -297,7 +305,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, ParentViewState, ViewState> Element<ParentViewState> for RenderView<C, ViewState>
|
||||
impl<C, ParentViewState, ViewState> Element<ParentViewState> for RenderViewWith<C, ViewState>
|
||||
where
|
||||
C: 'static + Component<ViewState>,
|
||||
ParentViewState: 'static,
|
||||
@@ -309,29 +317,16 @@ where
|
||||
Some(self.view.entity_id().into())
|
||||
}
|
||||
|
||||
fn initialize(
|
||||
fn layout(
|
||||
&mut self,
|
||||
_: &mut ParentViewState,
|
||||
_: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<ParentViewState>,
|
||||
) -> Self::ElementState {
|
||||
cx.with_element_id(Some(self.view.entity_id()), |cx| {
|
||||
self.view.update(cx, |view, cx| {
|
||||
let mut element = self.component.take().unwrap().render();
|
||||
element.initialize(view, cx);
|
||||
element
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_: &mut ParentViewState,
|
||||
element: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<ParentViewState>,
|
||||
) -> LayoutId {
|
||||
cx.with_element_id(Some(self.view.entity_id()), |cx| {
|
||||
self.view.update(cx, |view, cx| element.layout(view, cx))
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
self.view.update(cx, |view, cx| {
|
||||
let mut element = self.component.take().unwrap().render();
|
||||
let layout_id = element.layout(view, cx);
|
||||
(layout_id, element)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -342,20 +337,7 @@ where
|
||||
element: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<ParentViewState>,
|
||||
) {
|
||||
cx.with_element_id(Some(self.view.entity_id()), |cx| {
|
||||
self.view.update(cx, |view, cx| element.paint(view, cx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_view<C, V>(view: &View<V>, component: C) -> RenderView<C, V>
|
||||
where
|
||||
C: 'static + Component<V>,
|
||||
V: 'static,
|
||||
{
|
||||
RenderView {
|
||||
view: view.clone(),
|
||||
component: Some(component),
|
||||
self.view.update(cx, |view, cx| element.paint(view, cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,27 +345,17 @@ mod any_view {
|
||||
use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext};
|
||||
use std::any::Any;
|
||||
|
||||
pub(crate) fn initialize<V: Render>(view: &AnyView, cx: &mut WindowContext) -> Box<dyn Any> {
|
||||
cx.with_element_id(Some(view.model.entity_id), |cx| {
|
||||
let view = view.clone().downcast::<V>().unwrap();
|
||||
let element = view.update(cx, |view, cx| {
|
||||
let mut element = AnyElement::new(view.render(cx));
|
||||
element.initialize(view, cx);
|
||||
element
|
||||
});
|
||||
Box::new(element)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn layout<V: Render>(
|
||||
view: &AnyView,
|
||||
element: &mut Box<dyn Any>,
|
||||
cx: &mut WindowContext,
|
||||
) -> LayoutId {
|
||||
) -> (LayoutId, Box<dyn Any>) {
|
||||
cx.with_element_id(Some(view.model.entity_id), |cx| {
|
||||
let view = view.clone().downcast::<V>().unwrap();
|
||||
let element = element.downcast_mut::<AnyElement<V>>().unwrap();
|
||||
view.update(cx, |view, cx| element.layout(view, cx))
|
||||
view.update(cx, |view, cx| {
|
||||
let mut element = AnyElement::new(view.render(cx));
|
||||
let layout_id = element.layout(view, cx);
|
||||
(layout_id, Box::new(element) as Box<dyn Any>)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -185,6 +185,27 @@ impl Drop for FocusHandle {
|
||||
}
|
||||
}
|
||||
|
||||
/// FocusableView allows users of your view to easily
|
||||
/// focus it (using cx.focus_view(view))
|
||||
pub trait FocusableView: Render {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
|
||||
}
|
||||
|
||||
/// ManagedView is a view (like a Modal, Popover, Menu, etc.)
|
||||
/// where the lifecycle of the view is handled by another view.
|
||||
pub trait ManagedView: Render {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
|
||||
}
|
||||
|
||||
pub struct Dismiss;
|
||||
impl<T: ManagedView> EventEmitter<Dismiss> for T {}
|
||||
|
||||
impl<T: ManagedView> FocusableView for T {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
// Holds the state for a specific window.
|
||||
pub struct Window {
|
||||
pub(crate) handle: AnyWindowHandle,
|
||||
@@ -216,7 +237,7 @@ pub struct Window {
|
||||
|
||||
// #[derive(Default)]
|
||||
pub(crate) struct Frame {
|
||||
element_states: HashMap<GlobalElementId, AnyBox>,
|
||||
pub(crate) element_states: HashMap<GlobalElementId, AnyBox>,
|
||||
mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyMouseListener)>>,
|
||||
pub(crate) dispatch_tree: DispatchTree,
|
||||
pub(crate) focus_listeners: Vec<AnyFocusListener>,
|
||||
@@ -307,8 +328,8 @@ impl Window {
|
||||
layout_engine: TaffyLayoutEngine::new(),
|
||||
root_view: None,
|
||||
element_id_stack: GlobalElementId::default(),
|
||||
previous_frame: Frame::new(DispatchTree::new(cx.keymap.clone())),
|
||||
current_frame: Frame::new(DispatchTree::new(cx.keymap.clone())),
|
||||
previous_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
|
||||
current_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
|
||||
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
|
||||
focus_listeners: SubscriberSet::new(),
|
||||
default_prevented: true,
|
||||
@@ -393,6 +414,10 @@ impl<'a> WindowContext<'a> {
|
||||
|
||||
/// Move focus to the element associated with the given `FocusHandle`.
|
||||
pub fn focus(&mut self, handle: &FocusHandle) {
|
||||
if self.window.focus == Some(handle.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
let focus_id = handle.id;
|
||||
|
||||
if self.window.last_blur.is_none() {
|
||||
@@ -400,6 +425,10 @@ impl<'a> WindowContext<'a> {
|
||||
}
|
||||
|
||||
self.window.focus = Some(focus_id);
|
||||
self.window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.clear_keystroke_matchers();
|
||||
self.app.push_effect(Effect::FocusChanged {
|
||||
window_handle: self.window.handle,
|
||||
focused: Some(focus_id),
|
||||
@@ -562,6 +591,7 @@ impl<'a> WindowContext<'a> {
|
||||
result
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Add a node to the layout tree for the current frame. Takes the `Style` of the element for which
|
||||
/// layout is being requested, along with the layout ids of any children. This method is called during
|
||||
/// calls to the `Element::layout` trait method and enables any element to participate in layout.
|
||||
@@ -1068,29 +1098,33 @@ impl<'a> WindowContext<'a> {
|
||||
|
||||
self.with_z_index(0, |cx| {
|
||||
let available_space = cx.window.viewport_size.map(Into::into);
|
||||
root_view.draw(available_space, cx);
|
||||
root_view.draw(Point::zero(), available_space, cx);
|
||||
});
|
||||
|
||||
if let Some(active_drag) = self.app.active_drag.take() {
|
||||
self.with_z_index(1, |cx| {
|
||||
let offset = cx.mouse_position() - active_drag.cursor_offset;
|
||||
cx.with_element_offset(offset, |cx| {
|
||||
let available_space =
|
||||
size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
||||
active_drag.view.draw(available_space, cx);
|
||||
cx.active_drag = Some(active_drag);
|
||||
});
|
||||
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
||||
active_drag.view.draw(offset, available_space, cx);
|
||||
cx.active_drag = Some(active_drag);
|
||||
});
|
||||
} else if let Some(active_tooltip) = self.app.active_tooltip.take() {
|
||||
self.with_z_index(1, |cx| {
|
||||
cx.with_element_offset(active_tooltip.cursor_offset, |cx| {
|
||||
let available_space =
|
||||
size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
||||
active_tooltip.view.draw(available_space, cx);
|
||||
});
|
||||
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
||||
active_tooltip
|
||||
.view
|
||||
.draw(active_tooltip.cursor_offset, available_space, cx);
|
||||
});
|
||||
}
|
||||
|
||||
self.window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.preserve_keystroke_matchers(
|
||||
&mut self.window.previous_frame.dispatch_tree,
|
||||
self.window.focus,
|
||||
);
|
||||
|
||||
self.window.root_view = Some(root_view);
|
||||
let scene = self.window.current_frame.scene_builder.build();
|
||||
|
||||
@@ -1134,6 +1168,14 @@ impl<'a> WindowContext<'a> {
|
||||
self.window.mouse_position = mouse_move.position;
|
||||
InputEvent::MouseMove(mouse_move)
|
||||
}
|
||||
InputEvent::MouseDown(mouse_down) => {
|
||||
self.window.mouse_position = mouse_down.position;
|
||||
InputEvent::MouseDown(mouse_down)
|
||||
}
|
||||
InputEvent::MouseUp(mouse_up) => {
|
||||
self.window.mouse_position = mouse_up.position;
|
||||
InputEvent::MouseUp(mouse_up)
|
||||
}
|
||||
// Translate dragging and dropping of external files from the operating system
|
||||
// to internal drag and drop events.
|
||||
InputEvent::FileDrop(file_drop) => match file_drop {
|
||||
@@ -1534,6 +1576,12 @@ impl VisualContext for WindowContext<'_> {
|
||||
self.window.root_view = Some(view.clone().into());
|
||||
view
|
||||
}
|
||||
|
||||
fn focus_view<V: crate::FocusableView>(&mut self, view: &View<V>) -> Self::Result<()> {
|
||||
self.update_view(view, |view, cx| {
|
||||
view.focus_handle(cx).clone().focus(cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> std::ops::Deref for WindowContext<'a> {
|
||||
@@ -1617,8 +1665,8 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the global element offset based on the given offset. This is used to implement
|
||||
/// scrolling and position drag handles.
|
||||
/// Update the global element offset relative to the current offset. This is used to implement
|
||||
/// scrolling.
|
||||
fn with_element_offset<R>(
|
||||
&mut self,
|
||||
offset: Point<Pixels>,
|
||||
@@ -1628,7 +1676,17 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
|
||||
return f(self);
|
||||
};
|
||||
|
||||
let offset = self.element_offset() + offset;
|
||||
let abs_offset = self.element_offset() + offset;
|
||||
self.with_absolute_element_offset(abs_offset, f)
|
||||
}
|
||||
|
||||
/// Update the global element offset based on the given offset. This is used to implement
|
||||
/// drag handles and other manual painting of elements.
|
||||
fn with_absolute_element_offset<R>(
|
||||
&mut self,
|
||||
offset: Point<Pixels>,
|
||||
f: impl FnOnce(&mut Self) -> R,
|
||||
) -> R {
|
||||
self.window_mut()
|
||||
.current_frame
|
||||
.element_offset_stack
|
||||
@@ -1798,8 +1856,8 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||
self.view
|
||||
}
|
||||
|
||||
pub fn model(&self) -> Model<V> {
|
||||
self.view.model.clone()
|
||||
pub fn model(&self) -> &Model<V> {
|
||||
&self.view.model
|
||||
}
|
||||
|
||||
/// Access the underlying window context.
|
||||
@@ -2093,7 +2151,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||
window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.push_node(context.clone(), &mut window.previous_frame.dispatch_tree);
|
||||
.push_node(context.clone());
|
||||
if let Some(focus_handle) = focus_handle.as_ref() {
|
||||
window
|
||||
.current_frame
|
||||
@@ -2131,7 +2189,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||
|
||||
pub fn observe_global<G: 'static>(
|
||||
&mut self,
|
||||
f: impl Fn(&mut V, &mut ViewContext<'_, V>) + 'static,
|
||||
mut f: impl FnMut(&mut V, &mut ViewContext<'_, V>) + 'static,
|
||||
) -> Subscription {
|
||||
let window_handle = self.window.handle;
|
||||
let view = self.view().downgrade();
|
||||
@@ -2197,9 +2255,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||
.set_input_handler(Box::new(input_handler));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> ViewContext<'_, V> {
|
||||
pub fn emit<Evt>(&mut self, event: Evt)
|
||||
where
|
||||
Evt: 'static,
|
||||
@@ -2212,6 +2268,13 @@ impl<V> ViewContext<'_, V> {
|
||||
event: Box::new(event),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn focus_self(&mut self)
|
||||
where
|
||||
V: FocusableView,
|
||||
{
|
||||
self.defer(|view, cx| view.focus_handle(cx).focus(cx))
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Context for ViewContext<'_, V> {
|
||||
@@ -2287,6 +2350,10 @@ impl<V: 'static> VisualContext for ViewContext<'_, V> {
|
||||
{
|
||||
self.window_cx.replace_root_view(build_view)
|
||||
}
|
||||
|
||||
fn focus_view<W: FocusableView>(&mut self, view: &View<W>) -> Self::Result<()> {
|
||||
self.window_cx.focus_view(view)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, V> std::ops::Deref for ViewContext<'a, V> {
|
||||
@@ -2471,7 +2538,7 @@ impl From<SmallVec<[u32; 16]>> for StackingOrder {
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub enum ElementId {
|
||||
View(EntityId),
|
||||
Number(usize),
|
||||
Integer(usize),
|
||||
Name(SharedString),
|
||||
FocusHandle(FocusId),
|
||||
}
|
||||
@@ -2484,13 +2551,13 @@ impl From<EntityId> for ElementId {
|
||||
|
||||
impl From<usize> for ElementId {
|
||||
fn from(id: usize) -> Self {
|
||||
ElementId::Number(id)
|
||||
ElementId::Integer(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for ElementId {
|
||||
fn from(id: i32) -> Self {
|
||||
Self::Number(id as usize)
|
||||
Self::Integer(id as usize)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
45
crates/gpui2/tests/action_macros.rs
Normal file
45
crates/gpui2/tests/action_macros.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
#[test]
|
||||
fn test_derive() {
|
||||
use gpui2 as gpui;
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, gpui2_macros::Action)]
|
||||
struct AnotherTestAction;
|
||||
|
||||
#[gpui2_macros::register_action]
|
||||
#[derive(PartialEq, Clone, gpui::serde_derive::Deserialize)]
|
||||
struct RegisterableAction {}
|
||||
|
||||
impl gpui::Action for RegisterableAction {
|
||||
fn boxed_clone(&self) -> Box<dyn gpui::Action> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn partial_eq(&self, _action: &dyn gpui::Action) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn debug_name() -> &'static str
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn build(_value: serde_json::Value) -> anyhow::Result<Box<dyn gpui::Action>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,6 @@ path = "src/gpui2_macros.rs"
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = "1.0.72"
|
||||
syn = { version = "1.0.72", features = ["full"] }
|
||||
quote = "1.0.9"
|
||||
proc-macro2 = "1.0.66"
|
||||
|
||||
@@ -15,48 +15,81 @@
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, DeriveInput};
|
||||
use syn::{parse_macro_input, DeriveInput, Error};
|
||||
|
||||
use crate::register_action::register_action;
|
||||
|
||||
pub fn action(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as DeriveInput);
|
||||
|
||||
pub fn action(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(item as DeriveInput);
|
||||
let name = &input.ident;
|
||||
let attrs = input
|
||||
.attrs
|
||||
.into_iter()
|
||||
.filter(|attr| !attr.path.is_ident("action"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let attributes = quote! {
|
||||
#[gpui::register_action]
|
||||
#[derive(gpui::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone, std::default::Default, std::fmt::Debug)]
|
||||
#(#attrs)*
|
||||
if input.generics.lt_token.is_some() {
|
||||
return Error::new(name.span(), "Actions must be a concrete type")
|
||||
.into_compile_error()
|
||||
.into();
|
||||
}
|
||||
|
||||
let is_unit_struct = match input.data {
|
||||
syn::Data::Struct(struct_data) => struct_data.fields.is_empty(),
|
||||
syn::Data::Enum(_) => false,
|
||||
syn::Data::Union(_) => false,
|
||||
};
|
||||
let visibility = input.vis;
|
||||
|
||||
let output = match input.data {
|
||||
syn::Data::Struct(ref struct_data) => match &struct_data.fields {
|
||||
syn::Fields::Named(_) | syn::Fields::Unnamed(_) => {
|
||||
let fields = &struct_data.fields;
|
||||
quote! {
|
||||
#attributes
|
||||
#visibility struct #name #fields
|
||||
}
|
||||
let build_impl = if is_unit_struct {
|
||||
quote! {
|
||||
Ok(std::boxed::Box::new(Self {}))
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
Ok(std::boxed::Box::new(gpui::serde_json::from_value::<Self>(value)?))
|
||||
}
|
||||
};
|
||||
|
||||
let register_action = register_action(&name);
|
||||
|
||||
let output = quote! {
|
||||
const _: fn() = || {
|
||||
fn assert_impl<T: ?Sized + for<'a> gpui::serde::Deserialize<'a> + ::std::cmp::PartialEq + ::std::clone::Clone>() {}
|
||||
assert_impl::<#name>();
|
||||
};
|
||||
|
||||
impl gpui::Action for #name {
|
||||
fn name(&self) -> &'static str
|
||||
{
|
||||
::std::any::type_name::<#name>()
|
||||
}
|
||||
syn::Fields::Unit => {
|
||||
quote! {
|
||||
#attributes
|
||||
#visibility struct #name;
|
||||
}
|
||||
|
||||
fn debug_name() -> &'static str
|
||||
where
|
||||
Self: ::std::marker::Sized
|
||||
{
|
||||
::std::any::type_name::<#name>()
|
||||
}
|
||||
},
|
||||
syn::Data::Enum(ref enum_data) => {
|
||||
let variants = &enum_data.variants;
|
||||
quote! {
|
||||
#attributes
|
||||
#visibility enum #name { #variants }
|
||||
|
||||
fn build(value: gpui::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>>
|
||||
where
|
||||
Self: ::std::marker::Sized {
|
||||
#build_impl
|
||||
}
|
||||
|
||||
fn partial_eq(&self, action: &dyn gpui::Action) -> bool {
|
||||
action
|
||||
.as_any()
|
||||
.downcast_ref::<Self>()
|
||||
.map_or(false, |a| self == a)
|
||||
}
|
||||
|
||||
fn boxed_clone(&self) -> std::boxed::Box<dyn gpui::Action> {
|
||||
::std::boxed::Box::new(self.clone())
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn ::std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
_ => panic!("Expected a struct or an enum."),
|
||||
|
||||
#register_action
|
||||
};
|
||||
|
||||
TokenStream::from(output)
|
||||
|
||||
@@ -11,14 +11,14 @@ pub fn style_helpers(args: TokenStream) -> TokenStream {
|
||||
style_helpers::style_helpers(args)
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn action(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
action::action(attr, item)
|
||||
#[proc_macro_derive(Action)]
|
||||
pub fn action(input: TokenStream) -> TokenStream {
|
||||
action::action(input)
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn register_action(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
register_action::register_action(attr, item)
|
||||
register_action::register_action_macro(attr, item)
|
||||
}
|
||||
|
||||
#[proc_macro_derive(Component, attributes(component))]
|
||||
|
||||
@@ -12,22 +12,76 @@
|
||||
// gpui2::register_action_builder::<Foo>()
|
||||
// }
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::Ident;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::{parse_macro_input, DeriveInput};
|
||||
use syn::{parse_macro_input, DeriveInput, Error};
|
||||
|
||||
pub fn register_action(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
pub fn register_action_macro(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(item as DeriveInput);
|
||||
let type_name = &input.ident;
|
||||
let ctor_fn_name = format_ident!("register_{}_builder", type_name.to_string().to_lowercase());
|
||||
let registration = register_action(&input.ident);
|
||||
|
||||
let expanded = quote! {
|
||||
let has_action_derive = input
|
||||
.attrs
|
||||
.iter()
|
||||
.find(|attr| {
|
||||
(|| {
|
||||
let meta = attr.parse_meta().ok()?;
|
||||
meta.path().is_ident("derive").then(|| match meta {
|
||||
syn::Meta::Path(_) => None,
|
||||
syn::Meta::NameValue(_) => None,
|
||||
syn::Meta::List(list) => list
|
||||
.nested
|
||||
.iter()
|
||||
.find(|list| match list {
|
||||
syn::NestedMeta::Meta(meta) => meta.path().is_ident("Action"),
|
||||
syn::NestedMeta::Lit(_) => false,
|
||||
})
|
||||
.map(|_| true),
|
||||
})?
|
||||
})()
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.is_some();
|
||||
|
||||
if has_action_derive {
|
||||
return Error::new(
|
||||
input.ident.span(),
|
||||
"The Action derive macro has already registered this action",
|
||||
)
|
||||
.into_compile_error()
|
||||
.into();
|
||||
}
|
||||
|
||||
TokenStream::from(quote! {
|
||||
#input
|
||||
#[allow(non_snake_case)]
|
||||
#[gpui::ctor]
|
||||
fn #ctor_fn_name() {
|
||||
gpui::register_action::<#type_name>()
|
||||
}
|
||||
};
|
||||
|
||||
TokenStream::from(expanded)
|
||||
#registration
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn register_action(type_name: &Ident) -> proc_macro2::TokenStream {
|
||||
let static_slice_name =
|
||||
format_ident!("__GPUI_ACTIONS_{}", type_name.to_string().to_uppercase());
|
||||
|
||||
let action_builder_fn_name = format_ident!(
|
||||
"__gpui_actions_builder_{}",
|
||||
type_name.to_string().to_lowercase()
|
||||
);
|
||||
|
||||
quote! {
|
||||
#[doc(hidden)]
|
||||
#[gpui::linkme::distributed_slice(gpui::__GPUI_ACTIONS)]
|
||||
#[linkme(crate = gpui::linkme)]
|
||||
static #static_slice_name: gpui::MacroActionBuilder = #action_builder_fn_name;
|
||||
|
||||
/// This is an auto generated function, do not use.
|
||||
#[doc(hidden)]
|
||||
fn #action_builder_fn_name() -> gpui::ActionData {
|
||||
gpui::ActionData {
|
||||
name: ::std::any::type_name::<#type_name>(),
|
||||
type_id: ::std::any::TypeId::of::<#type_name>(),
|
||||
build: <#type_name as gpui::Action>::build,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ use crate::{
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
pub use clock::ReplicaId;
|
||||
use futures::FutureExt as _;
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, Task};
|
||||
use lsp::LanguageServerId;
|
||||
use parking_lot::Mutex;
|
||||
@@ -45,7 +45,7 @@ pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, *};
|
||||
use theme::SyntaxTheme;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use util::RandomCharIter;
|
||||
use util::{RangeExt, TryFutureExt as _};
|
||||
use util::RangeExt;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use {tree_sitter_rust, tree_sitter_typescript};
|
||||
@@ -62,6 +62,7 @@ pub struct Buffer {
|
||||
saved_mtime: SystemTime,
|
||||
transaction_depth: usize,
|
||||
was_dirty_before_starting_transaction: Option<bool>,
|
||||
reload_task: Option<Task<Result<()>>>,
|
||||
language: Option<Arc<Language>>,
|
||||
autoindent_requests: Vec<Arc<AutoindentRequest>>,
|
||||
pending_autoindent: Option<Task<()>>,
|
||||
@@ -509,6 +510,7 @@ impl Buffer {
|
||||
saved_mtime,
|
||||
saved_version: buffer.version(),
|
||||
saved_version_fingerprint: buffer.as_rope().fingerprint(),
|
||||
reload_task: None,
|
||||
transaction_depth: 0,
|
||||
was_dirty_before_starting_transaction: None,
|
||||
text: buffer,
|
||||
@@ -608,37 +610,52 @@ impl Buffer {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<Option<Transaction>>> {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if let Some((new_mtime, new_text)) = this.read_with(&cx, |this, cx| {
|
||||
pub fn reload(
|
||||
&mut self,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> oneshot::Receiver<Option<Transaction>> {
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
let prev_version = self.text.version();
|
||||
self.reload_task = Some(cx.spawn(|this, mut cx| async move {
|
||||
let Some((new_mtime, new_text)) = this.update(&mut cx, |this, cx| {
|
||||
let file = this.file.as_ref()?.as_local()?;
|
||||
Some((file.mtime(), file.load(cx)))
|
||||
}) {
|
||||
let new_text = new_text.await?;
|
||||
let diff = this
|
||||
.read_with(&cx, |this, cx| this.diff(new_text, cx))
|
||||
.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if this.version() == diff.base_version {
|
||||
this.finalize_last_transaction();
|
||||
this.apply_diff(diff, cx);
|
||||
if let Some(transaction) = this.finalize_last_transaction().cloned() {
|
||||
this.did_reload(
|
||||
this.version(),
|
||||
this.as_rope().fingerprint(),
|
||||
this.line_ending(),
|
||||
new_mtime,
|
||||
cx,
|
||||
);
|
||||
return Ok(Some(transaction));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
})
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
})
|
||||
}) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let new_text = new_text.await?;
|
||||
let diff = this
|
||||
.update(&mut cx, |this, cx| this.diff(new_text.clone(), cx))
|
||||
.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if this.version() == diff.base_version {
|
||||
this.finalize_last_transaction();
|
||||
this.apply_diff(diff, cx);
|
||||
tx.send(this.finalize_last_transaction().cloned()).ok();
|
||||
|
||||
this.did_reload(
|
||||
this.version(),
|
||||
this.as_rope().fingerprint(),
|
||||
this.line_ending(),
|
||||
new_mtime,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
this.did_reload(
|
||||
prev_version,
|
||||
Rope::text_fingerprint(&new_text),
|
||||
this.line_ending(),
|
||||
this.saved_mtime,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
this.reload_task.take();
|
||||
});
|
||||
Ok(())
|
||||
}));
|
||||
rx
|
||||
}
|
||||
|
||||
pub fn did_reload(
|
||||
@@ -667,13 +684,8 @@ impl Buffer {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn file_updated(
|
||||
&mut self,
|
||||
new_file: Arc<dyn File>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<()> {
|
||||
pub fn file_updated(&mut self, new_file: Arc<dyn File>, cx: &mut ModelContext<Self>) {
|
||||
let mut file_changed = false;
|
||||
let mut task = Task::ready(());
|
||||
|
||||
if let Some(old_file) = self.file.as_ref() {
|
||||
if new_file.path() != old_file.path() {
|
||||
@@ -693,8 +705,7 @@ impl Buffer {
|
||||
file_changed = true;
|
||||
|
||||
if !self.is_dirty() {
|
||||
let reload = self.reload(cx).log_err().map(drop);
|
||||
task = cx.foreground().spawn(reload);
|
||||
self.reload(cx).close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -708,7 +719,6 @@ impl Buffer {
|
||||
cx.emit(Event::FileHandleChanged);
|
||||
cx.notify();
|
||||
}
|
||||
task
|
||||
}
|
||||
|
||||
pub fn diff_base(&self) -> Option<&str> {
|
||||
|
||||
@@ -16,8 +16,9 @@ use crate::{
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
pub use clock::ReplicaId;
|
||||
use futures::FutureExt as _;
|
||||
use gpui::{AppContext, EventEmitter, HighlightStyle, ModelContext, Task};
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{AppContext, EventEmitter, HighlightStyle, ModelContext, Task, TaskLabel};
|
||||
use lazy_static::lazy_static;
|
||||
use lsp::LanguageServerId;
|
||||
use parking_lot::Mutex;
|
||||
use similar::{ChangeTag, TextDiff};
|
||||
@@ -44,23 +45,33 @@ pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, *};
|
||||
use theme::SyntaxTheme;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use util::RandomCharIter;
|
||||
use util::{RangeExt, TryFutureExt as _};
|
||||
use util::RangeExt;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use {tree_sitter_rust, tree_sitter_typescript};
|
||||
|
||||
pub use lsp::DiagnosticSeverity;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref BUFFER_DIFF_TASK: TaskLabel = TaskLabel::new();
|
||||
}
|
||||
|
||||
pub struct Buffer {
|
||||
text: TextBuffer,
|
||||
diff_base: Option<String>,
|
||||
git_diff: git::diff::BufferDiff,
|
||||
file: Option<Arc<dyn File>>,
|
||||
saved_version: clock::Global,
|
||||
saved_version_fingerprint: RopeFingerprint,
|
||||
/// The mtime of the file when this buffer was last loaded from
|
||||
/// or saved to disk.
|
||||
saved_mtime: SystemTime,
|
||||
/// The version vector when this buffer was last loaded from
|
||||
/// or saved to disk.
|
||||
saved_version: clock::Global,
|
||||
/// A hash of the current contents of the buffer's file.
|
||||
file_fingerprint: RopeFingerprint,
|
||||
transaction_depth: usize,
|
||||
was_dirty_before_starting_transaction: Option<bool>,
|
||||
reload_task: Option<Task<Result<()>>>,
|
||||
language: Option<Arc<Language>>,
|
||||
autoindent_requests: Vec<Arc<AutoindentRequest>>,
|
||||
pending_autoindent: Option<Task<()>>,
|
||||
@@ -380,8 +391,7 @@ impl Buffer {
|
||||
.ok_or_else(|| anyhow!("missing line_ending"))?,
|
||||
));
|
||||
this.saved_version = proto::deserialize_version(&message.saved_version);
|
||||
this.saved_version_fingerprint =
|
||||
proto::deserialize_fingerprint(&message.saved_version_fingerprint)?;
|
||||
this.file_fingerprint = proto::deserialize_fingerprint(&message.saved_version_fingerprint)?;
|
||||
this.saved_mtime = message
|
||||
.saved_mtime
|
||||
.ok_or_else(|| anyhow!("invalid saved_mtime"))?
|
||||
@@ -397,7 +407,7 @@ impl Buffer {
|
||||
diff_base: self.diff_base.as_ref().map(|h| h.to_string()),
|
||||
line_ending: proto::serialize_line_ending(self.line_ending()) as i32,
|
||||
saved_version: proto::serialize_version(&self.saved_version),
|
||||
saved_version_fingerprint: proto::serialize_fingerprint(self.saved_version_fingerprint),
|
||||
saved_version_fingerprint: proto::serialize_fingerprint(self.file_fingerprint),
|
||||
saved_mtime: Some(self.saved_mtime.into()),
|
||||
}
|
||||
}
|
||||
@@ -467,7 +477,8 @@ impl Buffer {
|
||||
Self {
|
||||
saved_mtime,
|
||||
saved_version: buffer.version(),
|
||||
saved_version_fingerprint: buffer.as_rope().fingerprint(),
|
||||
file_fingerprint: buffer.as_rope().fingerprint(),
|
||||
reload_task: None,
|
||||
transaction_depth: 0,
|
||||
was_dirty_before_starting_transaction: None,
|
||||
text: buffer,
|
||||
@@ -533,7 +544,7 @@ impl Buffer {
|
||||
}
|
||||
|
||||
pub fn saved_version_fingerprint(&self) -> RopeFingerprint {
|
||||
self.saved_version_fingerprint
|
||||
self.file_fingerprint
|
||||
}
|
||||
|
||||
pub fn saved_mtime(&self) -> SystemTime {
|
||||
@@ -561,43 +572,58 @@ impl Buffer {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.saved_version = version;
|
||||
self.saved_version_fingerprint = fingerprint;
|
||||
self.file_fingerprint = fingerprint;
|
||||
self.saved_mtime = mtime;
|
||||
cx.emit(Event::Saved);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<Option<Transaction>>> {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if let Some((new_mtime, new_text)) = this.update(&mut cx, |this, cx| {
|
||||
pub fn reload(
|
||||
&mut self,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> oneshot::Receiver<Option<Transaction>> {
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
let prev_version = self.text.version();
|
||||
self.reload_task = Some(cx.spawn(|this, mut cx| async move {
|
||||
let Some((new_mtime, new_text)) = this.update(&mut cx, |this, cx| {
|
||||
let file = this.file.as_ref()?.as_local()?;
|
||||
Some((file.mtime(), file.load(cx)))
|
||||
})? {
|
||||
let new_text = new_text.await?;
|
||||
let diff = this
|
||||
.update(&mut cx, |this, cx| this.diff(new_text, cx))?
|
||||
.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if this.version() == diff.base_version {
|
||||
this.finalize_last_transaction();
|
||||
this.apply_diff(diff, cx);
|
||||
if let Some(transaction) = this.finalize_last_transaction().cloned() {
|
||||
this.did_reload(
|
||||
this.version(),
|
||||
this.as_rope().fingerprint(),
|
||||
this.line_ending(),
|
||||
new_mtime,
|
||||
cx,
|
||||
);
|
||||
return Some(transaction);
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
})
|
||||
})?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let new_text = new_text.await?;
|
||||
let diff = this
|
||||
.update(&mut cx, |this, cx| this.diff(new_text.clone(), cx))?
|
||||
.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if this.version() == diff.base_version {
|
||||
this.finalize_last_transaction();
|
||||
this.apply_diff(diff, cx);
|
||||
tx.send(this.finalize_last_transaction().cloned()).ok();
|
||||
|
||||
this.did_reload(
|
||||
this.version(),
|
||||
this.as_rope().fingerprint(),
|
||||
this.line_ending(),
|
||||
new_mtime,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
this.did_reload(
|
||||
prev_version,
|
||||
Rope::text_fingerprint(&new_text),
|
||||
this.line_ending(),
|
||||
this.saved_mtime,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
this.reload_task.take();
|
||||
})
|
||||
}));
|
||||
rx
|
||||
}
|
||||
|
||||
pub fn did_reload(
|
||||
@@ -609,14 +635,14 @@ impl Buffer {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.saved_version = version;
|
||||
self.saved_version_fingerprint = fingerprint;
|
||||
self.file_fingerprint = fingerprint;
|
||||
self.text.set_line_ending(line_ending);
|
||||
self.saved_mtime = mtime;
|
||||
if let Some(file) = self.file.as_ref().and_then(|f| f.as_local()) {
|
||||
file.buffer_reloaded(
|
||||
self.remote_id(),
|
||||
&self.saved_version,
|
||||
self.saved_version_fingerprint,
|
||||
self.file_fingerprint,
|
||||
self.line_ending(),
|
||||
self.saved_mtime,
|
||||
cx,
|
||||
@@ -626,13 +652,8 @@ impl Buffer {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn file_updated(
|
||||
&mut self,
|
||||
new_file: Arc<dyn File>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<()> {
|
||||
pub fn file_updated(&mut self, new_file: Arc<dyn File>, cx: &mut ModelContext<Self>) {
|
||||
let mut file_changed = false;
|
||||
let mut task = Task::ready(());
|
||||
|
||||
if let Some(old_file) = self.file.as_ref() {
|
||||
if new_file.path() != old_file.path() {
|
||||
@@ -652,8 +673,7 @@ impl Buffer {
|
||||
file_changed = true;
|
||||
|
||||
if !self.is_dirty() {
|
||||
let reload = self.reload(cx).log_err().map(drop);
|
||||
task = cx.background_executor().spawn(reload);
|
||||
self.reload(cx).close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -667,7 +687,6 @@ impl Buffer {
|
||||
cx.emit(Event::FileHandleChanged);
|
||||
cx.notify();
|
||||
}
|
||||
task
|
||||
}
|
||||
|
||||
pub fn diff_base(&self) -> Option<&str> {
|
||||
@@ -1118,36 +1137,72 @@ impl Buffer {
|
||||
pub fn diff(&self, mut new_text: String, cx: &AppContext) -> Task<Diff> {
|
||||
let old_text = self.as_rope().clone();
|
||||
let base_version = self.version();
|
||||
cx.background_executor().spawn(async move {
|
||||
let old_text = old_text.to_string();
|
||||
let line_ending = LineEnding::detect(&new_text);
|
||||
LineEnding::normalize(&mut new_text);
|
||||
let diff = TextDiff::from_chars(old_text.as_str(), new_text.as_str());
|
||||
let mut edits = Vec::new();
|
||||
let mut offset = 0;
|
||||
let empty: Arc<str> = "".into();
|
||||
for change in diff.iter_all_changes() {
|
||||
let value = change.value();
|
||||
let end_offset = offset + value.len();
|
||||
match change.tag() {
|
||||
ChangeTag::Equal => {
|
||||
offset = end_offset;
|
||||
cx.background_executor()
|
||||
.spawn_labeled(*BUFFER_DIFF_TASK, async move {
|
||||
let old_text = old_text.to_string();
|
||||
let line_ending = LineEnding::detect(&new_text);
|
||||
LineEnding::normalize(&mut new_text);
|
||||
|
||||
let diff = TextDiff::from_chars(old_text.as_str(), new_text.as_str());
|
||||
let empty: Arc<str> = "".into();
|
||||
|
||||
let mut edits = Vec::new();
|
||||
let mut old_offset = 0;
|
||||
let mut new_offset = 0;
|
||||
let mut last_edit: Option<(Range<usize>, Range<usize>)> = None;
|
||||
for change in diff.iter_all_changes().map(Some).chain([None]) {
|
||||
if let Some(change) = &change {
|
||||
let len = change.value().len();
|
||||
match change.tag() {
|
||||
ChangeTag::Equal => {
|
||||
old_offset += len;
|
||||
new_offset += len;
|
||||
}
|
||||
ChangeTag::Delete => {
|
||||
let old_end_offset = old_offset + len;
|
||||
if let Some((last_old_range, _)) = &mut last_edit {
|
||||
last_old_range.end = old_end_offset;
|
||||
} else {
|
||||
last_edit =
|
||||
Some((old_offset..old_end_offset, new_offset..new_offset));
|
||||
}
|
||||
old_offset = old_end_offset;
|
||||
}
|
||||
ChangeTag::Insert => {
|
||||
let new_end_offset = new_offset + len;
|
||||
if let Some((_, last_new_range)) = &mut last_edit {
|
||||
last_new_range.end = new_end_offset;
|
||||
} else {
|
||||
last_edit =
|
||||
Some((old_offset..old_offset, new_offset..new_end_offset));
|
||||
}
|
||||
new_offset = new_end_offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
ChangeTag::Delete => {
|
||||
edits.push((offset..end_offset, empty.clone()));
|
||||
offset = end_offset;
|
||||
}
|
||||
ChangeTag::Insert => {
|
||||
edits.push((offset..offset, value.into()));
|
||||
|
||||
if let Some((old_range, new_range)) = &last_edit {
|
||||
if old_offset > old_range.end
|
||||
|| new_offset > new_range.end
|
||||
|| change.is_none()
|
||||
{
|
||||
let text = if new_range.is_empty() {
|
||||
empty.clone()
|
||||
} else {
|
||||
new_text[new_range.clone()].into()
|
||||
};
|
||||
edits.push((old_range.clone(), text));
|
||||
last_edit.take();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Diff {
|
||||
base_version,
|
||||
line_ending,
|
||||
edits,
|
||||
}
|
||||
})
|
||||
|
||||
Diff {
|
||||
base_version,
|
||||
line_ending,
|
||||
edits,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Spawn a background task that searches the buffer for any whitespace
|
||||
@@ -1231,12 +1286,12 @@ impl Buffer {
|
||||
}
|
||||
|
||||
pub fn is_dirty(&self) -> bool {
|
||||
self.saved_version_fingerprint != self.as_rope().fingerprint()
|
||||
self.file_fingerprint != self.as_rope().fingerprint()
|
||||
|| self.file.as_ref().map_or(false, |file| file.is_deleted())
|
||||
}
|
||||
|
||||
pub fn has_conflict(&self) -> bool {
|
||||
self.saved_version_fingerprint != self.as_rope().fingerprint()
|
||||
self.file_fingerprint != self.as_rope().fingerprint()
|
||||
&& self
|
||||
.file
|
||||
.as_ref()
|
||||
|
||||
@@ -10,7 +10,7 @@ path = "src/live_kit_client2.rs"
|
||||
doctest = false
|
||||
|
||||
[[example]]
|
||||
name = "test_app"
|
||||
name = "test_app2"
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use futures::StreamExt;
|
||||
use gpui::KeyBinding;
|
||||
use gpui::{Action, KeyBinding};
|
||||
use live_kit_client2::{
|
||||
LocalAudioTrack, LocalVideoTrack, RemoteAudioTrackUpdate, RemoteVideoTrackUpdate, Room,
|
||||
};
|
||||
@@ -10,7 +10,7 @@ use log::LevelFilter;
|
||||
use serde_derive::Deserialize;
|
||||
use simplelog::SimpleLogger;
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Action)]
|
||||
struct Quit;
|
||||
|
||||
fn main() {
|
||||
@@ -1,7 +1,7 @@
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
div, prelude::*, uniform_list, Component, Div, MouseButton, Render, Task,
|
||||
UniformListScrollHandle, View, ViewContext, WindowContext,
|
||||
div, prelude::*, uniform_list, AppContext, Component, Div, FocusHandle, FocusableView,
|
||||
MouseButton, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,
|
||||
};
|
||||
use std::{cmp, sync::Arc};
|
||||
use ui::{prelude::*, v_stack, Divider, Label, TextColor};
|
||||
@@ -35,6 +35,12 @@ pub trait PickerDelegate: Sized + 'static {
|
||||
) -> Self::ListItem;
|
||||
}
|
||||
|
||||
impl<D: PickerDelegate> FocusableView for Picker<D> {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: PickerDelegate> Picker<D> {
|
||||
pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
|
||||
let editor = cx.build_view(|cx| {
|
||||
|
||||
@@ -6190,7 +6190,7 @@ impl Project {
|
||||
.log_err();
|
||||
}
|
||||
|
||||
buffer.file_updated(Arc::new(new_file), cx).detach();
|
||||
buffer.file_updated(Arc::new(new_file), cx);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -7182,7 +7182,7 @@ impl Project {
|
||||
.ok_or_else(|| anyhow!("no such worktree"))?;
|
||||
let file = File::from_proto(file, worktree, cx)?;
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.file_updated(Arc::new(file), cx).detach();
|
||||
buffer.file_updated(Arc::new(file), cx);
|
||||
});
|
||||
this.detect_language_for_buffer(&buffer, cx);
|
||||
}
|
||||
|
||||
@@ -959,7 +959,7 @@ impl LocalWorktree {
|
||||
|
||||
buffer_handle.update(&mut cx, |buffer, cx| {
|
||||
if has_changed_file {
|
||||
buffer.file_updated(new_file, cx).detach();
|
||||
buffer.file_updated(new_file, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6262,7 +6262,7 @@ impl Project {
|
||||
.log_err();
|
||||
}
|
||||
|
||||
buffer.file_updated(Arc::new(new_file), cx).detach();
|
||||
buffer.file_updated(Arc::new(new_file), cx);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -7256,7 +7256,7 @@ impl Project {
|
||||
.ok_or_else(|| anyhow!("no such worktree"))?;
|
||||
let file = File::from_proto(file, worktree, cx)?;
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.file_updated(Arc::new(file), cx).detach();
|
||||
buffer.file_updated(Arc::new(file), cx);
|
||||
});
|
||||
this.detect_language_for_buffer(&buffer, cx);
|
||||
}
|
||||
|
||||
@@ -2587,6 +2587,73 @@ async fn test_save_file(cx: &mut gpui::TestAppContext) {
|
||||
assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text()));
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 30)]
|
||||
async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
"file1": "the original contents",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||
let worktree = project.read_with(cx, |project, _| project.worktrees().next().unwrap());
|
||||
let buffer = project
|
||||
.update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Simulate buffer diffs being slow, so that they don't complete before
|
||||
// the next file change occurs.
|
||||
cx.executor().deprioritize(*language::BUFFER_DIFF_TASK);
|
||||
|
||||
// Change the buffer's file on disk, and then wait for the file change
|
||||
// to be detected by the worktree, so that the buffer starts reloading.
|
||||
fs.save(
|
||||
"/dir/file1".as_ref(),
|
||||
&"the first contents".into(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
worktree.next_event(cx);
|
||||
|
||||
// Change the buffer's file again. Depending on the random seed, the
|
||||
// previous file change may still be in progress.
|
||||
fs.save(
|
||||
"/dir/file1".as_ref(),
|
||||
&"the second contents".into(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
worktree.next_event(cx);
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
let on_disk_text = fs.load(Path::new("/dir/file1")).await.unwrap();
|
||||
buffer.read_with(cx, |buffer, _| {
|
||||
let buffer_text = buffer.text();
|
||||
if buffer_text == on_disk_text {
|
||||
assert!(
|
||||
!buffer.is_dirty() && !buffer.has_conflict(),
|
||||
"buffer shouldn't be dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}",
|
||||
);
|
||||
}
|
||||
// If the file change occurred while the buffer was processing the first
|
||||
// change, the buffer will be in a conflicting state.
|
||||
else {
|
||||
assert!(
|
||||
buffer.is_dirty() && buffer.has_conflict(),
|
||||
"buffer should report that it has a conflict. text: {buffer_text:?}, disk text: {on_disk_text:?}"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
@@ -276,6 +276,7 @@ struct ShareState {
|
||||
_maintain_remote_snapshot: Task<Option<()>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Event {
|
||||
UpdatedEntries(UpdatedEntriesSet),
|
||||
UpdatedGitRepositories(UpdatedGitRepositoriesSet),
|
||||
@@ -961,7 +962,7 @@ impl LocalWorktree {
|
||||
|
||||
buffer_handle.update(&mut cx, |buffer, cx| {
|
||||
if has_changed_file {
|
||||
buffer.file_updated(new_file, cx).detach();
|
||||
buffer.file_updated(new_file, cx);
|
||||
}
|
||||
})?;
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ use file_associations::FileAssociations;
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::{
|
||||
actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
|
||||
ClipboardItem, Component, Div, EventEmitter, FocusHandle, Focusable, InteractiveComponent,
|
||||
Model, MouseButton, ParentComponent, Pixels, Point, PromptLevel, Render, Stateful,
|
||||
StatefulInteractiveComponent, Styled, Task, UniformListScrollHandle, View, ViewContext,
|
||||
VisualContext as _, WeakView, WindowContext,
|
||||
ClipboardItem, Component, Div, EventEmitter, FocusHandle, Focusable, FocusableView,
|
||||
InteractiveComponent, Model, MouseButton, ParentComponent, Pixels, Point, PromptLevel, Render,
|
||||
Stateful, StatefulInteractiveComponent, Styled, Task, UniformListScrollHandle, View,
|
||||
ViewContext, VisualContext as _, WeakView, WindowContext,
|
||||
};
|
||||
use menu::{Confirm, SelectNext, SelectPrev};
|
||||
use project::{
|
||||
@@ -32,7 +32,7 @@ use std::{
|
||||
use theme::ActiveTheme as _;
|
||||
use ui::{h_stack, v_stack, IconElement, Label};
|
||||
use unicase::UniCase;
|
||||
use util::{maybe, TryFutureExt};
|
||||
use util::{maybe, ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
dock::{DockPosition, PanelEvent},
|
||||
Workspace,
|
||||
@@ -130,6 +130,13 @@ pub fn init_settings(cx: &mut AppContext) {
|
||||
pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
|
||||
init_settings(cx);
|
||||
file_associations::init(assets, cx);
|
||||
|
||||
cx.observe_new_views(|workspace: &mut Workspace, _| {
|
||||
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
|
||||
workspace.toggle_panel_focus::<ProjectPanel>(cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -303,32 +310,31 @@ impl ProjectPanel {
|
||||
project_panel
|
||||
}
|
||||
|
||||
pub fn load(
|
||||
pub async fn load(
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: AsyncWindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
// let serialized_panel = if let Some(panel) = cx
|
||||
// .background_executor()
|
||||
// .spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) })
|
||||
// .await
|
||||
// .log_err()
|
||||
// .flatten()
|
||||
// {
|
||||
// Some(serde_json::from_str::<SerializedProjectPanel>(&panel)?)
|
||||
// } else {
|
||||
// None
|
||||
// };
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let panel = ProjectPanel::new(workspace, cx);
|
||||
// if let Some(serialized_panel) = serialized_panel {
|
||||
// panel.update(cx, |panel, cx| {
|
||||
// panel.width = serialized_panel.width;
|
||||
// cx.notify();
|
||||
// });
|
||||
// }
|
||||
panel
|
||||
})
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> Result<View<Self>> {
|
||||
let serialized_panel = cx
|
||||
.background_executor()
|
||||
.spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) })
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to load project panel: {}", e))
|
||||
.log_err()
|
||||
.flatten()
|
||||
.map(|panel| serde_json::from_str::<SerializedProjectPanel>(&panel))
|
||||
.transpose()
|
||||
.log_err()
|
||||
.flatten();
|
||||
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let panel = ProjectPanel::new(workspace, cx);
|
||||
if let Some(serialized_panel) = serialized_panel {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.width = serialized_panel.width;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
panel
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1516,33 +1522,27 @@ impl workspace::dock::Panel for ProjectPanel {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
|
||||
Some("icons/project.svg")
|
||||
fn icon(&self, _: &WindowContext) -> Option<ui::Icon> {
|
||||
Some(ui::Icon::FileTree)
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
|
||||
("Project Panel".into(), Some(Box::new(ToggleFocus)))
|
||||
fn toggle_action(&self) -> Box<dyn Action> {
|
||||
Box::new(ToggleFocus)
|
||||
}
|
||||
|
||||
// fn should_change_position_on_event(event: &Self::Event) -> bool {
|
||||
// matches!(event, Event::DockPositionChanged)
|
||||
// }
|
||||
|
||||
fn has_focus(&self, _: &WindowContext) -> bool {
|
||||
self.has_focus
|
||||
}
|
||||
|
||||
fn persistent_name(&self) -> &'static str {
|
||||
fn persistent_name() -> &'static str {
|
||||
"Project Panel"
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_handle(&self, _cx: &WindowContext) -> FocusHandle {
|
||||
impl FocusableView for ProjectPanel {
|
||||
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
|
||||
// fn is_focus_event(event: &Self::Event) -> bool {
|
||||
// matches!(event, Event::Focus)
|
||||
// }
|
||||
}
|
||||
|
||||
impl ClipboardEntry {
|
||||
@@ -1579,7 +1579,7 @@ mod tests {
|
||||
path::{Path, PathBuf},
|
||||
sync::atomic::{self, AtomicUsize},
|
||||
};
|
||||
use workspace::{pane, AppState};
|
||||
use workspace::AppState;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_visible_list(cx: &mut gpui::TestAppContext) {
|
||||
@@ -2785,7 +2785,7 @@ mod tests {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
init_settings(cx);
|
||||
theme::init(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
language::init(cx);
|
||||
editor::init_settings(cx);
|
||||
crate::init((), cx);
|
||||
@@ -2798,11 +2798,10 @@ mod tests {
|
||||
fn init_test_with_editor(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let app_state = AppState::test(cx);
|
||||
theme::init(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
init_settings(cx);
|
||||
language::init(cx);
|
||||
editor::init(cx);
|
||||
pane::init(cx);
|
||||
crate::init((), cx);
|
||||
workspace::init(app_state.clone(), cx);
|
||||
Project::init_settings(cx);
|
||||
|
||||
@@ -41,6 +41,10 @@ impl Rope {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn text_fingerprint(text: &str) -> RopeFingerprint {
|
||||
bromberg_sl2::hash_strict(text.as_bytes())
|
||||
}
|
||||
|
||||
pub fn append(&mut self, rope: Rope) {
|
||||
let mut chunks = rope.chunks.cursor::<()>();
|
||||
chunks.next(&());
|
||||
@@ -931,7 +935,7 @@ impl<'a> From<&'a str> for ChunkSummary {
|
||||
fn from(text: &'a str) -> Self {
|
||||
Self {
|
||||
text: TextSummary::from(text),
|
||||
fingerprint: bromberg_sl2::hash_strict(text.as_bytes()),
|
||||
fingerprint: Rope::text_fingerprint(text),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,10 @@ impl Rope {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn text_fingerprint(text: &str) -> RopeFingerprint {
|
||||
bromberg_sl2::hash_strict(text.as_bytes())
|
||||
}
|
||||
|
||||
pub fn append(&mut self, rope: Rope) {
|
||||
let mut chunks = rope.chunks.cursor::<()>();
|
||||
chunks.next(&());
|
||||
@@ -931,7 +935,7 @@ impl<'a> From<&'a str> for ChunkSummary {
|
||||
fn from(text: &'a str) -> Self {
|
||||
Self {
|
||||
text: TextSummary::from(text),
|
||||
fingerprint: bromberg_sl2::hash_strict(text.as_bytes()),
|
||||
fingerprint: Rope::text_fingerprint(text),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use schemars::{
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use util::asset_str;
|
||||
use util::{asset_str, ResultExt};
|
||||
|
||||
#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
|
||||
#[serde(transparent)]
|
||||
@@ -73,9 +73,9 @@ impl KeymapFile {
|
||||
"Expected first item in array to be a string."
|
||||
)));
|
||||
};
|
||||
gpui::build_action(&name, Some(data))
|
||||
cx.build_action(&name, Some(data))
|
||||
}
|
||||
Value::String(name) => gpui::build_action(&name, None),
|
||||
Value::String(name) => cx.build_action(&name, None),
|
||||
Value::Null => Ok(no_action()),
|
||||
_ => {
|
||||
return Some(Err(anyhow!("Expected two-element array, got {action:?}")))
|
||||
@@ -86,9 +86,7 @@ impl KeymapFile {
|
||||
"invalid binding value for keystroke {keystroke}, context {context:?}"
|
||||
)
|
||||
})
|
||||
// todo!()
|
||||
.ok()
|
||||
// .log_err()
|
||||
.log_err()
|
||||
.map(|action| KeyBinding::load(&keystroke, action, context.as_deref()))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
@@ -16,6 +16,9 @@ pub fn test_settings() -> String {
|
||||
.unwrap();
|
||||
util::merge_non_null_json_value_into(
|
||||
serde_json::json!({
|
||||
"ui_font_family": "Courier",
|
||||
"ui_font_features": {},
|
||||
"ui_font_size": 14,
|
||||
"buffer_font_family": "Courier",
|
||||
"buffer_font_features": {},
|
||||
"buffer_font_size": 14,
|
||||
|
||||
@@ -164,6 +164,23 @@ impl Column for i64 {
|
||||
}
|
||||
}
|
||||
|
||||
impl StaticColumnCount for u64 {}
|
||||
impl Bind for u64 {
|
||||
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
||||
statement
|
||||
.bind_int64(start_index, (*self) as i64)
|
||||
.with_context(|| format!("Failed to bind i64 at index {start_index}"))?;
|
||||
Ok(start_index + 1)
|
||||
}
|
||||
}
|
||||
|
||||
impl Column for u64 {
|
||||
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
||||
let result = statement.column_int64(start_index)? as u64;
|
||||
Ok((result, start_index + 1))
|
||||
}
|
||||
}
|
||||
|
||||
impl StaticColumnCount for u32 {}
|
||||
impl Bind for u32 {
|
||||
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
||||
|
||||
@@ -57,7 +57,6 @@ impl Render for FocusStory {
|
||||
.size_full()
|
||||
.bg(color_1)
|
||||
.focus(|style| style.bg(color_2))
|
||||
.focus_in(|style| style.bg(color_3))
|
||||
.child(
|
||||
div()
|
||||
.track_focus(&self.child_1_focus)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use gpui::{div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext};
|
||||
use theme2::ActiveTheme;
|
||||
use ui::Tooltip;
|
||||
|
||||
pub struct ScrollStory;
|
||||
|
||||
@@ -35,16 +36,18 @@ impl Render for ScrollStory {
|
||||
} else {
|
||||
color_2
|
||||
};
|
||||
div().id(id).bg(bg).size(px(100. as f32)).when(
|
||||
row >= 5 && column >= 5,
|
||||
|d| {
|
||||
div()
|
||||
.id(id)
|
||||
.tooltip(move |_, cx| Tooltip::text(format!("{}, {}", row, column), cx))
|
||||
.bg(bg)
|
||||
.size(px(100. as f32))
|
||||
.when(row >= 5 && column >= 5, |d| {
|
||||
d.overflow_scroll()
|
||||
.child(div().size(px(50.)).bg(color_1))
|
||||
.child(div().size(px(50.)).bg(color_2))
|
||||
.child(div().size(px(50.)).bg(color_1))
|
||||
.child(div().size(px(50.)).bg(color_2))
|
||||
},
|
||||
)
|
||||
})
|
||||
}))
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -60,13 +60,12 @@ fn main() {
|
||||
.unwrap();
|
||||
cx.set_global(store);
|
||||
|
||||
theme2::init(cx);
|
||||
theme2::init(theme2::LoadThemes::All, cx);
|
||||
|
||||
let selector =
|
||||
story_selector.unwrap_or(StorySelector::Component(ComponentStory::Workspace));
|
||||
|
||||
let theme_registry = cx.global::<ThemeRegistry>();
|
||||
|
||||
let mut theme_settings = ThemeSettings::get_global(cx).clone();
|
||||
theme_settings.active_theme = theme_registry.get(&theme_name).unwrap();
|
||||
ThemeSettings::override_global(theme_settings, cx);
|
||||
@@ -114,6 +113,7 @@ impl Render for StoryWrapper {
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.font("Zed Mono")
|
||||
.child(self.story.clone())
|
||||
}
|
||||
}
|
||||
|
||||
17
crates/storybook3/Cargo.toml
Normal file
17
crates/storybook3/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "storybook3"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "storybook"
|
||||
path = "src/storybook3.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
ui = { package = "ui2", path = "../ui2", features = ["stories"] }
|
||||
theme = { package = "theme2", path = "../theme2", features = ["stories"] }
|
||||
settings = { package = "settings2", path = "../settings2"}
|
||||
73
crates/storybook3/src/storybook3.rs
Normal file
73
crates/storybook3/src/storybook3.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use anyhow::Result;
|
||||
use gpui::AssetSource;
|
||||
use gpui::{
|
||||
div, px, size, AnyView, Bounds, Div, Render, ViewContext, VisualContext, WindowBounds,
|
||||
WindowOptions,
|
||||
};
|
||||
use settings::{default_settings, Settings, SettingsStore};
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, ContextMenuStory};
|
||||
|
||||
struct Assets;
|
||||
|
||||
impl AssetSource for Assets {
|
||||
fn load(&self, _path: &str) -> Result<Cow<[u8]>> {
|
||||
todo!();
|
||||
}
|
||||
|
||||
fn list(&self, _path: &str) -> Result<Vec<SharedString>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let asset_source = Arc::new(Assets);
|
||||
gpui::App::production(asset_source).run(move |cx| {
|
||||
let mut store = SettingsStore::default();
|
||||
store
|
||||
.set_default_settings(default_settings().as_ref(), cx)
|
||||
.unwrap();
|
||||
cx.set_global(store);
|
||||
ui::settings::init(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
bounds: WindowBounds::Fixed(Bounds {
|
||||
origin: Default::default(),
|
||||
size: size(px(1500.), px(780.)).into(),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
move |cx| {
|
||||
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size;
|
||||
cx.set_rem_size(ui_font_size);
|
||||
|
||||
cx.build_view(|cx| TestView {
|
||||
story: cx.build_view(|_| ContextMenuStory).into(),
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
cx.activate(true);
|
||||
})
|
||||
}
|
||||
|
||||
struct TestView {
|
||||
story: AnyView,
|
||||
}
|
||||
|
||||
impl Render for TestView {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.font("Helvetica")
|
||||
.child(self.story.clone())
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user